Repository: networkx/networkx
Pull request: add special processing of `multigraph_input` upon graph init
Fixes: #4720

Adding a keyword argument `multigraph_input=True`
to a graph construction call should treat any incoming input data for
the graph as a dict-of-dict-of-dict multigraph data structure.

This matches the behavior of `nx.to_networkx_graph` which is called by the `__init__` dunder.

Previously, the multigraph_input argument would be added to the graph
attribute dict and ignored when processing the input data.


Problem statement:
MultiDiGraph initialised via dict_of_dicts does not maintain class
### Current Behavior

If I load in a dict_of_dicts using `nx.MultiDiGraph(dict_of_dicts)` if doesn't pass on the class to the `create_using` parameter, so it doesn't read in multiple edges correctly.

### Expected Behavior

`nx.MultiDiGraph(dict_of_dicts, multigraph_input=True)` should behave like `nx.from_dict_of_dicts(dict_of_dicts, create_using=nx.MultiDiGraph, multigraph_input=True)`

### Steps to Reproduce

```
G = nx.MultiDiGraph()
G.add_edge(2, 3, weight=5)
G.add_edge(2, 3, weight=4)  # Create 2 edges

dict_of_dicts = nx.to_dict_of_dicts(G)
G2 = nx.MultiDiGraph(dict_of_dicts, multigraph_input=True)  # Fails
G3 = nx.from_dict_of_dicts(dict_of_dicts, create_using=nx.MultiDiGraph, multigraph_input=True)  # Works

print("Original", G.edges(data=True))
print("Round tripped", G2.edges(data=True))
print(G2.number_of_edges(), "edge(s) in round tripped graph")
```

Giving

```
Original [(2, 3, {'weight': 5}), (2, 3, {'weight': 4})]
Round tripped [(2, 3, {0: {'weight': 5}, 1: {'weight': 4}})]
1 edge(s) in round tripped graph
```

### Environment

Python version: 3.9
NetworkX version: 2.5


MultiGraph initialization with dict-of-dict-of-dict-of-dict doesn't work properly
### Current Behavior
The initialization of a multigraph with dictionaries treats the third dictionary, which its keys suppose to be the edges IDs, as the attribute of the edge.
### Expected Behavior
Initializing a multigraph should not accept dictionaries, or it should use reserved keys for edges' IDs.
### Steps to Reproduce
```
g = networkx.MultiGraph({'a': {'b': {0: {'w': 200}, 1: {'w': 201}}}})
print(g.edges)
h = networkx.MultiGraph()
h.add_edges_from([('a', 'b', {"w": 200}), ('a', 'b', {"w": 201})])
print(h.edges)

output

[('a', 'b', 0)]
[('a', 'b', 0), ('a', 'b', 1)]
```
### Environment
Python version: 3.9.5
NetworkX version: 2.5.1

Hints from issue discussion:
I haven't looked carefully yet, but this looks like a defect and your idea that it could be fixed by passing along the `multigraph_input` parameter seems like a good fix.
Thanks!!

Commit message: add special processing of `multigraph_input` upon graph init

Fixes: #4720

Adding a keyword argument `multigraph_input=True`
to a graph construction call should treat any incoming input data for
the graph as a dict-of-dict-of-dict multigraph data structure.

Previously the multigraph_input argument would be added to the graph
attribute dict and ignored when processing the input data.

Patch to review:
diff --git a/networkx/classes/digraph.py b/networkx/classes/digraph.py
index 04cd6b5c446..e6445450a37 100644
--- a/networkx/classes/digraph.py
+++ b/networkx/classes/digraph.py
@@ -314,7 +314,13 @@ def __init__(self, incoming_graph_data=None, **attr):
 
         # attempt to load graph with data
         if incoming_graph_data is not None:
-            convert.to_networkx_graph(incoming_graph_data, create_using=self)
+            if "multigraph_input" in attr:
+                mgi = attr.pop("multigraph_input")
+                convert.to_networkx_graph(
+                    incoming_graph_data, create_using=self, multigraph_input=mgi
+                )
+            else:
+                convert.to_networkx_graph(incoming_graph_data, create_using=self)
         # load graph attributes (must be after convert)
         self.graph.update(attr)
 
diff --git a/networkx/classes/graph.py b/networkx/classes/graph.py
index 216a37d2a27..4ba9de628e9 100644
--- a/networkx/classes/graph.py
+++ b/networkx/classes/graph.py
@@ -329,7 +329,13 @@ def __init__(self, incoming_graph_data=None, **attr):
         self._adj = self.adjlist_outer_dict_factory()  # empty adjacency dict
         # attempt to load graph with data
         if incoming_graph_data is not None:
-            convert.to_networkx_graph(incoming_graph_data, create_using=self)
+            if "multigraph_input" in attr:
+                mgi = attr.pop("multigraph_input")
+                convert.to_networkx_graph(
+                    incoming_graph_data, create_using=self, multigraph_input=mgi
+                )
+            else:
+                convert.to_networkx_graph(incoming_graph_data, create_using=self)
         # load graph attributes (must be after convert)
         self.graph.update(attr)
 
diff --git a/networkx/classes/tests/test_graph.py b/networkx/classes/tests/test_graph.py
index 476f5e14db8..a881b53825f 100644
--- a/networkx/classes/tests/test_graph.py
+++ b/networkx/classes/tests/test_graph.py
@@ -510,9 +510,6 @@ def test_data_input(self):
         G = self.Graph({1: [2], 2: [1]}, name="test")
         assert G.name == "test"
         assert sorted(G.adj.items()) == [(1, {2: {}}), (2, {1: {}})]
-        G = self.Graph({1: [2], 2: [1]}, name="test")
-        assert G.name == "test"
-        assert sorted(G.adj.items()) == [(1, {2: {}}), (2, {1: {}})]
 
     def test_adjacency(self):
         G = self.K3
diff --git a/networkx/classes/tests/test_multigraph.py b/networkx/classes/tests/test_multigraph.py
index ec07c229b7d..a7f10723e07 100644
--- a/networkx/classes/tests/test_multigraph.py
+++ b/networkx/classes/tests/test_multigraph.py
@@ -182,6 +182,22 @@ def test_data_input(self):
         expected = [(1, {2: {0: {}}}), (2, {1: {0: {}}})]
         assert sorted(G.adj.items()) == expected
 
+    def test_data_multigraph_input(self):
+        G = self.Graph({1: {2: {4: {}}}}, multigraph_input=True)
+        if G.is_directed():
+            expected = [(1, {2: {4: {}}}), (2, {})]
+        else:
+            expected = [(1, {2: {4: {}}}), (2, {1: {4: {}}})]
+        assert sorted(G.adj.items()) == expected
+
+        G = self.Graph({1: {2: {4: {}}}, 2: {1: {4: {}}}}, multigraph_input=False)
+        expected = [(1, {2: {0: {4: {}}}}), (2, {1: {0: {4: {}}}})]
+        assert sorted(G.adj.items()) == expected
+
+        G = self.Graph({1: {2: {4: {}}}, 2: {1: {4: {}}}}, multigraph_input=True)
+        expected = [(1, {2: {4: {}}}), (2, {1: {4: {}}})]
+        assert sorted(G.adj.items()) == expected
+
     def test_getitem(self):
         G = self.K3
         assert G[0] == {1: {0: {}}, 2: {0: {}}}


Code review comments:
diff_hunk: @@ -182,6 +182,22 @@ def test_data_input(self):
         expected = [(1, {2: {0: {}}}), (2, {1: {0: {}}})]
         assert sorted(G.adj.items()) == expected
 
+    def test_data_multigraph_input(self):
+        G = self.Graph({1: {2: {4: {}}}}, multigraph_input=True)
+        if G.is_directed():
+            expected = [(1, {2: {4: {}}}), (2, {})]
path: networkx/classes/tests/test_multigraph.py
text: @user1:
Does this branch ever get hit in this test? AFAICT `self.Graph` always results in a `MultiGraph` object within this test class... unless the class itself is inherited somewhere?

@author:
This test function is used to test `MultiDiGraph` as well as `MultiGraph`.  We could overwrite the method in `tests/test_multidigraph.py` if that is better.  I guess that is probably better style and perhaps less confusing. I'll also check how much of the method writing is written to handle both directed and undirected (and thus reduce overwriting methods)... Thanks for asking about that.