diff options
-rw-r--r-- | networkx/classes/multidigraph.py | 46 | ||||
-rw-r--r-- | networkx/classes/multigraph.py | 46 | ||||
-rw-r--r-- | networkx/classes/tests/test_graph.py | 3 | ||||
-rw-r--r-- | networkx/classes/tests/test_multigraph.py | 89 | ||||
-rw-r--r-- | networkx/convert.py | 17 | ||||
-rw-r--r-- | networkx/tests/test_convert.py | 5 |
6 files changed, 190 insertions, 16 deletions
diff --git a/networkx/classes/multidigraph.py b/networkx/classes/multidigraph.py index d8903606..51e590fe 100644 --- a/networkx/classes/multidigraph.py +++ b/networkx/classes/multidigraph.py @@ -13,6 +13,7 @@ from networkx.classes.reportviews import ( InMultiDegreeView, ) from networkx.exception import NetworkXError +import networkx.convert as convert __all__ = ["MultiDiGraph"] @@ -40,6 +41,19 @@ class MultiDiGraph(MultiGraph, DiGraph): dict of dicts, dict of lists, NetworkX graph, NumPy matrix or 2d ndarray, SciPy sparse matrix, or PyGraphviz graph. + multigraph_input : bool or None (default None) + Note: Only used when `incoming_graph_data` is a dict. + If True, `incoming_graph_data` is assumed to be a + dict-of-dict-of-dict-of-dict structure keyed by + node to neighbor to edge keys to edge data for multi-edges. + A NetworkXError is raised if this is not the case. + If False, :func:`to_networkx_graph` is used to try to determine + the dict's graph data structure as either a dict-of-dict-of-dict + keyed by node to neighbor to edge data, or a dict-of-iterable + keyed by node to neighbors. + If None, the treatment for True is tried, but if it fails, + the treatment for False is tried. + attr : keyword arguments, optional (default= no attributes) Attributes to add to graph as key=value pairs. @@ -265,7 +279,7 @@ class MultiDiGraph(MultiGraph, DiGraph): edge_key_dict_factory = dict # edge_attr_dict_factory = dict - def __init__(self, incoming_graph_data=None, **attr): + def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr): """Initialize a graph with edges, name, or graph attributes. Parameters @@ -277,6 +291,19 @@ class MultiDiGraph(MultiGraph, DiGraph): packages are installed the data can also be a NumPy matrix or 2d ndarray, a SciPy sparse matrix, or a PyGraphviz graph. + multigraph_input : bool or None (default None) + Note: Only used when `incoming_graph_data` is a dict. + If True, `incoming_graph_data` is assumed to be a + dict-of-dict-of-dict-of-dict structure keyed by + node to neighbor to edge keys to edge data for multi-edges. + A NetworkXError is raised if this is not the case. + If False, :func:`to_networkx_graph` is used to try to determine + the dict's graph data structure as either a dict-of-dict-of-dict + keyed by node to neighbor to edge data, or a dict-of-iterable + keyed by node to neighbors. + If None, the treatment for True is tried, but if it fails, + the treatment for False is tried. + attr : keyword arguments, optional (default= no attributes) Attributes to add to graph as key=value pairs. @@ -299,7 +326,22 @@ class MultiDiGraph(MultiGraph, DiGraph): """ self.edge_key_dict_factory = self.edge_key_dict_factory - DiGraph.__init__(self, incoming_graph_data, **attr) + # multigraph_input can be None/True/False. So check "is not False" + if isinstance(incoming_graph_data, dict) and multigraph_input is not False: + DiGraph.__init__(self) + try: + convert.from_dict_of_dicts( + incoming_graph_data, create_using=self, multigraph_input=True + ) + self.graph.update(attr) + except Exception as e: + if multigraph_input is True: + raise nx.NetworkXError( + f"converting multigraph_input raised:\n{type(e)}: {e}" + ) + DiGraph.__init__(self, incoming_graph_data, **attr) + else: + DiGraph.__init__(self, incoming_graph_data, **attr) @property def adj(self): diff --git a/networkx/classes/multigraph.py b/networkx/classes/multigraph.py index f8044c62..b2e69561 100644 --- a/networkx/classes/multigraph.py +++ b/networkx/classes/multigraph.py @@ -6,6 +6,7 @@ from networkx.classes.graph import Graph from networkx.classes.coreviews import MultiAdjacencyView from networkx.classes.reportviews import MultiEdgeView, MultiDegreeView from networkx import NetworkXError +import networkx.convert as convert __all__ = ["MultiGraph"] @@ -34,6 +35,19 @@ class MultiGraph(Graph): dict of dicts, dict of lists, NetworkX graph, NumPy matrix or 2d ndarray, SciPy sparse matrix, or PyGraphviz graph. + multigraph_input : bool or None (default None) + Note: Only used when `incoming_graph_data` is a dict. + If True, `incoming_graph_data` is assumed to be a + dict-of-dict-of-dict-of-dict structure keyed by + node to neighbor to edge keys to edge data for multi-edges. + A NetworkXError is raised if this is not the case. + If False, :func:`to_networkx_graph` is used to try to determine + the dict's graph data structure as either a dict-of-dict-of-dict + keyed by node to neighbor to edge data, or a dict-of-iterable + keyed by node to neighbors. + If None, the treatment for True is tried, but if it fails, + the treatment for False is tried. + attr : keyword arguments, optional (default= no attributes) Attributes to add to graph as key=value pairs. @@ -274,7 +288,7 @@ class MultiGraph(Graph): """ return MultiGraph - def __init__(self, incoming_graph_data=None, **attr): + def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr): """Initialize a graph with edges, name, or graph attributes. Parameters @@ -286,6 +300,19 @@ class MultiGraph(Graph): packages are installed the data can also be a NumPy matrix or 2d ndarray, a SciPy sparse matrix, or a PyGraphviz graph. + multigraph_input : bool or None (default None) + Note: Only used when `incoming_graph_data` is a dict. + If True, `incoming_graph_data` is assumed to be a + dict-of-dict-of-dict-of-dict structure keyed by + node to neighbor to edge keys to edge data for multi-edges. + A NetworkXError is raised if this is not the case. + If False, :func:`to_networkx_graph` is used to try to determine + the dict's graph data structure as either a dict-of-dict-of-dict + keyed by node to neighbor to edge data, or a dict-of-iterable + keyed by node to neighbors. + If None, the treatment for True is tried, but if it fails, + the treatment for False is tried. + attr : keyword arguments, optional (default= no attributes) Attributes to add to graph as key=value pairs. @@ -308,7 +335,22 @@ class MultiGraph(Graph): """ self.edge_key_dict_factory = self.edge_key_dict_factory - Graph.__init__(self, incoming_graph_data, **attr) + # multigraph_input can be None/True/False. So check "is not False" + if isinstance(incoming_graph_data, dict) and multigraph_input is not False: + Graph.__init__(self) + try: + convert.from_dict_of_dicts( + incoming_graph_data, create_using=self, multigraph_input=True + ) + self.graph.update(attr) + except Exception as e: + if multigraph_input is True: + raise nx.NetworkXError( + f"converting multigraph_input raised:\n{type(e)}: {e}" + ) + Graph.__init__(self, incoming_graph_data, **attr) + else: + Graph.__init__(self, incoming_graph_data, **attr) @property def adj(self): diff --git a/networkx/classes/tests/test_graph.py b/networkx/classes/tests/test_graph.py index 9b31cd5f..86744403 100644 --- a/networkx/classes/tests/test_graph.py +++ b/networkx/classes/tests/test_graph.py @@ -505,9 +505,6 @@ class TestGraph(BaseAttrGraphTester): 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 bf2e98e0..8428acf2 100644 --- a/networkx/classes/tests/test_multigraph.py +++ b/networkx/classes/tests/test_multigraph.py @@ -182,6 +182,95 @@ class TestMultiGraph(BaseMultiGraphTester, _TestGraph): expected = [(1, {2: {0: {}}}), (2, {1: {0: {}}})] assert sorted(G.adj.items()) == expected + def test_data_multigraph_input(self): + # standard case with edge keys and edge data + edata0 = dict(w=200, s="foo") + edata1 = dict(w=201, s="bar") + keydict = {0: edata0, 1: edata1} + dododod = {"a": {"b": keydict}} + + multiple_edge = [("a", "b", 0, edata0), ("a", "b", 1, edata1)] + single_edge = [("a", "b", 0, keydict)] + + G = self.Graph(dododod, multigraph_input=True) + assert list(G.edges(keys=True, data=True)) == multiple_edge + G = self.Graph(dododod, multigraph_input=None) + assert list(G.edges(keys=True, data=True)) == multiple_edge + G = self.Graph(dododod, multigraph_input=False) + assert list(G.edges(keys=True, data=True)) == single_edge + + # test round-trip to_dict_of_dict and MultiGraph constructor + G = self.Graph(dododod, multigraph_input=True) + H = self.Graph(nx.to_dict_of_dicts(G)) + assert nx.is_isomorphic(G, H) is True # test that default is True + for mgi in [True, False]: + H = self.Graph(nx.to_dict_of_dicts(G), multigraph_input=mgi) + assert nx.is_isomorphic(G, H) == mgi + + # Set up cases for when incoming_graph_data is not multigraph_input + etraits = {"w": 200, "s": "foo"} + egraphics = {"color": "blue", "shape": "box"} + edata = {"traits": etraits, "graphics": egraphics} + dodod1 = {"a": {"b": edata}} + dodod2 = {"a": {"b": etraits}} + dodod3 = {"a": {"b": {"traits": etraits, "s": "foo"}}} + dol = {"a": ["b"]} + + multiple_edge = [ + ("a", "b", "traits", etraits), + ("a", "b", "graphics", egraphics), + ] + single_edge = [("a", "b", 0, {})] + single_edge1 = [("a", "b", 0, edata)] + single_edge2 = [("a", "b", 0, etraits)] + single_edge3 = [("a", "b", 0, {"traits": etraits, "s": "foo"})] + + cases = [ # (dod, mgi, edges) + (dodod1, True, multiple_edge), + (dodod1, False, single_edge1), + (dodod2, False, single_edge2), + (dodod3, False, single_edge3), + (dol, False, single_edge), + ] + + @pytest.mark.parametrize("dod, mgi, edges", cases) + def test_non_multigraph_input(self, dod, mgi, edges): + G = self.Graph(dod, multigraph_input=mgi) + assert list(G.edges(keys=True, data=True)) == edges + G = nx.to_networkx_graph(dod, create_using=self.Graph, multigraph_input=mgi) + assert list(G.edges(keys=True, data=True)) == edges + + mgi_none_cases = [ + (dodod1, multiple_edge), + (dodod2, single_edge2), + (dodod3, single_edge3), + ] + + @pytest.mark.parametrize("dod, edges", mgi_none_cases) + def test_non_multigraph_input_mgi_none(self, dod, edges): + # test constructor without to_networkx_graph for mgi=None + G = self.Graph(dod) + assert list(G.edges(keys=True, data=True)) == edges + + raise_cases = [dodod2, dodod3, dol] + + @pytest.mark.parametrize("dod", raise_cases) + def test_non_multigraph_input_raise(self, dod): + # cases where NetworkXError is raised + pytest.raises( + nx.NetworkXError, + self.Graph, + dod, + multigraph_input=True, + ) + pytest.raises( + nx.NetworkXError, + nx.to_networkx_graph, + dod, + create_using=self.Graph, + multigraph_input=True, + ) + def test_getitem(self): G = self.K3 assert G[0] == {1: {0: {}}, 2: {0: {}}} diff --git a/networkx/convert.py b/networkx/convert.py index 3cc80e3c..1db67976 100644 --- a/networkx/convert.py +++ b/networkx/convert.py @@ -103,7 +103,11 @@ def to_networkx_graph(data, create_using=None, multigraph_input=False): return from_dict_of_dicts( data, create_using=create_using, multigraph_input=multigraph_input ) - except: + except Exception as e: + if multigraph_input is True: + raise nx.NetworkXError( + f"converting multigraph_input raised:\n{type(e)}: {e}" + ) try: return from_dict_of_lists(data, create_using=create_using) except Exception as e: @@ -370,9 +374,11 @@ def from_dict_of_dicts(d, create_using=None, multigraph_input=False): Graph type to create. If graph instance, then cleared before populated. multigraph_input : bool (default False) - When True, the values of the inner dict are assumed - to be containers of edge data for multiple edges. - Otherwise this routine assumes the edge data are singletons. + When True, the dict `d` is assumed + to be a dict-of-dict-of-dict-of-dict structure keyed by + node to neighbor to edge keys to edge data for multi-edges. + Otherwise this routine assumes dict-of-dict-of-dict keyed by + node to neighbor to edge data. Examples -------- @@ -386,9 +392,8 @@ def from_dict_of_dicts(d, create_using=None, multigraph_input=False): """ G = nx.empty_graph(0, create_using) G.add_nodes_from(d) - # is dict a MultiGraph or MultiDiGraph? + # does dict d represent a MultiGraph or MultiDiGraph? if multigraph_input: - # make a copy of the list of edge data (but not the edge data) if G.is_directed(): if G.is_multigraph(): G.add_edges_from( diff --git a/networkx/tests/test_convert.py b/networkx/tests/test_convert.py index a99b6001..62a5b05d 100644 --- a/networkx/tests/test_convert.py +++ b/networkx/tests/test_convert.py @@ -182,10 +182,9 @@ class TestConvert: GW = to_networkx_graph(dod, create_using=nx.MultiGraph, multigraph_input=True) assert nodes_equal(sorted(XGM.nodes()), sorted(GW.nodes())) assert edges_equal(sorted(XGM.edges()), sorted(GW.edges())) - GI = nx.MultiGraph(dod) # convert can't tell whether to duplicate edges! + GI = nx.MultiGraph(dod) assert nodes_equal(sorted(XGM.nodes()), sorted(GI.nodes())) - # assert_not_equal(sorted(XGM.edges()), sorted(GI.edges())) - assert not sorted(XGM.edges()) == sorted(GI.edges()) + assert sorted(XGM.edges()) == sorted(GI.edges()) GE = from_dict_of_dicts(dod, create_using=nx.MultiGraph, multigraph_input=False) assert nodes_equal(sorted(XGM.nodes()), sorted(GE.nodes())) assert sorted(XGM.edges()) != sorted(GE.edges()) |