aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/classes
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/classes')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/__init__.py13
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/coreviews.py431
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/digraph.py1352
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/filters.py95
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/function.py1407
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/graph.py2058
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/graphviews.py269
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/multidigraph.py966
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/multigraph.py1283
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/reportviews.py1447
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/dispatch_interface.py185
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/historical_tests.py475
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_coreviews.py362
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_digraph.py331
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_digraph_historical.py111
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_filters.py177
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_function.py1035
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graph.py920
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graph_historical.py13
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graphviews.py350
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_multidigraph.py459
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_multigraph.py528
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_reportviews.py1435
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_special.py131
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/classes/tests/test_subgraphviews.py362
26 files changed, 16195 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/__init__.py b/.venv/lib/python3.12/site-packages/networkx/classes/__init__.py
new file mode 100644
index 00000000..721fa8b4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/__init__.py
@@ -0,0 +1,13 @@
+from .graph import Graph
+from .digraph import DiGraph
+from .multigraph import MultiGraph
+from .multidigraph import MultiDiGraph
+
+from .function import *
+from .graphviews import subgraph_view, reverse_view
+
+from networkx.classes import filters
+
+from networkx.classes import coreviews
+from networkx.classes import graphviews
+from networkx.classes import reportviews
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/coreviews.py b/.venv/lib/python3.12/site-packages/networkx/classes/coreviews.py
new file mode 100644
index 00000000..a6e85213
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/coreviews.py
@@ -0,0 +1,431 @@
+"""Views of core data structures such as nested Mappings (e.g. dict-of-dicts).
+These ``Views`` often restrict element access, with either the entire view or
+layers of nested mappings being read-only.
+"""
+
+from collections.abc import Mapping
+
+__all__ = [
+ "AtlasView",
+ "AdjacencyView",
+ "MultiAdjacencyView",
+ "UnionAtlas",
+ "UnionAdjacency",
+ "UnionMultiInner",
+ "UnionMultiAdjacency",
+ "FilterAtlas",
+ "FilterAdjacency",
+ "FilterMultiInner",
+ "FilterMultiAdjacency",
+]
+
+
+class AtlasView(Mapping):
+ """An AtlasView is a Read-only Mapping of Mappings.
+
+ It is a View into a dict-of-dict data structure.
+ The inner level of dict is read-write. But the
+ outer level is read-only.
+
+ See Also
+ ========
+ AdjacencyView: View into dict-of-dict-of-dict
+ MultiAdjacencyView: View into dict-of-dict-of-dict-of-dict
+ """
+
+ __slots__ = ("_atlas",)
+
+ def __getstate__(self):
+ return {"_atlas": self._atlas}
+
+ def __setstate__(self, state):
+ self._atlas = state["_atlas"]
+
+ def __init__(self, d):
+ self._atlas = d
+
+ def __len__(self):
+ return len(self._atlas)
+
+ def __iter__(self):
+ return iter(self._atlas)
+
+ def __getitem__(self, key):
+ return self._atlas[key]
+
+ def copy(self):
+ return {n: self[n].copy() for n in self._atlas}
+
+ def __str__(self):
+ return str(self._atlas) # {nbr: self[nbr] for nbr in self})
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self._atlas!r})"
+
+
+class AdjacencyView(AtlasView):
+ """An AdjacencyView is a Read-only Map of Maps of Maps.
+
+ It is a View into a dict-of-dict-of-dict data structure.
+ The inner level of dict is read-write. But the
+ outer levels are read-only.
+
+ See Also
+ ========
+ AtlasView: View into dict-of-dict
+ MultiAdjacencyView: View into dict-of-dict-of-dict-of-dict
+ """
+
+ __slots__ = () # Still uses AtlasView slots names _atlas
+
+ def __getitem__(self, name):
+ return AtlasView(self._atlas[name])
+
+ def copy(self):
+ return {n: self[n].copy() for n in self._atlas}
+
+
+class MultiAdjacencyView(AdjacencyView):
+ """An MultiAdjacencyView is a Read-only Map of Maps of Maps of Maps.
+
+ It is a View into a dict-of-dict-of-dict-of-dict data structure.
+ The inner level of dict is read-write. But the
+ outer levels are read-only.
+
+ See Also
+ ========
+ AtlasView: View into dict-of-dict
+ AdjacencyView: View into dict-of-dict-of-dict
+ """
+
+ __slots__ = () # Still uses AtlasView slots names _atlas
+
+ def __getitem__(self, name):
+ return AdjacencyView(self._atlas[name])
+
+ def copy(self):
+ return {n: self[n].copy() for n in self._atlas}
+
+
+class UnionAtlas(Mapping):
+ """A read-only union of two atlases (dict-of-dict).
+
+ The two dict-of-dicts represent the inner dict of
+ an Adjacency: `G.succ[node]` and `G.pred[node]`.
+ The inner level of dict of both hold attribute key:value
+ pairs and is read-write. But the outer level is read-only.
+
+ See Also
+ ========
+ UnionAdjacency: View into dict-of-dict-of-dict
+ UnionMultiAdjacency: View into dict-of-dict-of-dict-of-dict
+ """
+
+ __slots__ = ("_succ", "_pred")
+
+ def __getstate__(self):
+ return {"_succ": self._succ, "_pred": self._pred}
+
+ def __setstate__(self, state):
+ self._succ = state["_succ"]
+ self._pred = state["_pred"]
+
+ def __init__(self, succ, pred):
+ self._succ = succ
+ self._pred = pred
+
+ def __len__(self):
+ return len(self._succ.keys() | self._pred.keys())
+
+ def __iter__(self):
+ return iter(set(self._succ.keys()) | set(self._pred.keys()))
+
+ def __getitem__(self, key):
+ try:
+ return self._succ[key]
+ except KeyError:
+ return self._pred[key]
+
+ def copy(self):
+ result = {nbr: dd.copy() for nbr, dd in self._succ.items()}
+ for nbr, dd in self._pred.items():
+ if nbr in result:
+ result[nbr].update(dd)
+ else:
+ result[nbr] = dd.copy()
+ return result
+
+ def __str__(self):
+ return str({nbr: self[nbr] for nbr in self})
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self._succ!r}, {self._pred!r})"
+
+
+class UnionAdjacency(Mapping):
+ """A read-only union of dict Adjacencies as a Map of Maps of Maps.
+
+ The two input dict-of-dict-of-dicts represent the union of
+ `G.succ` and `G.pred`. Return values are UnionAtlas
+ The inner level of dict is read-write. But the
+ middle and outer levels are read-only.
+
+ succ : a dict-of-dict-of-dict {node: nbrdict}
+ pred : a dict-of-dict-of-dict {node: nbrdict}
+ The keys for the two dicts should be the same
+
+ See Also
+ ========
+ UnionAtlas: View into dict-of-dict
+ UnionMultiAdjacency: View into dict-of-dict-of-dict-of-dict
+ """
+
+ __slots__ = ("_succ", "_pred")
+
+ def __getstate__(self):
+ return {"_succ": self._succ, "_pred": self._pred}
+
+ def __setstate__(self, state):
+ self._succ = state["_succ"]
+ self._pred = state["_pred"]
+
+ def __init__(self, succ, pred):
+ # keys must be the same for two input dicts
+ assert len(set(succ.keys()) ^ set(pred.keys())) == 0
+ self._succ = succ
+ self._pred = pred
+
+ def __len__(self):
+ return len(self._succ) # length of each dict should be the same
+
+ def __iter__(self):
+ return iter(self._succ)
+
+ def __getitem__(self, nbr):
+ return UnionAtlas(self._succ[nbr], self._pred[nbr])
+
+ def copy(self):
+ return {n: self[n].copy() for n in self._succ}
+
+ def __str__(self):
+ return str({nbr: self[nbr] for nbr in self})
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self._succ!r}, {self._pred!r})"
+
+
+class UnionMultiInner(UnionAtlas):
+ """A read-only union of two inner dicts of MultiAdjacencies.
+
+ The two input dict-of-dict-of-dicts represent the union of
+ `G.succ[node]` and `G.pred[node]` for MultiDiGraphs.
+ Return values are UnionAtlas.
+ The inner level of dict is read-write. But the outer levels are read-only.
+
+ See Also
+ ========
+ UnionAtlas: View into dict-of-dict
+ UnionAdjacency: View into dict-of-dict-of-dict
+ UnionMultiAdjacency: View into dict-of-dict-of-dict-of-dict
+ """
+
+ __slots__ = () # Still uses UnionAtlas slots names _succ, _pred
+
+ def __getitem__(self, node):
+ in_succ = node in self._succ
+ in_pred = node in self._pred
+ if in_succ:
+ if in_pred:
+ return UnionAtlas(self._succ[node], self._pred[node])
+ return UnionAtlas(self._succ[node], {})
+ return UnionAtlas({}, self._pred[node])
+
+ def copy(self):
+ nodes = set(self._succ.keys()) | set(self._pred.keys())
+ return {n: self[n].copy() for n in nodes}
+
+
+class UnionMultiAdjacency(UnionAdjacency):
+ """A read-only union of two dict MultiAdjacencies.
+
+ The two input dict-of-dict-of-dict-of-dicts represent the union of
+ `G.succ` and `G.pred` for MultiDiGraphs. Return values are UnionAdjacency.
+ The inner level of dict is read-write. But the outer levels are read-only.
+
+ See Also
+ ========
+ UnionAtlas: View into dict-of-dict
+ UnionMultiInner: View into dict-of-dict-of-dict
+ """
+
+ __slots__ = () # Still uses UnionAdjacency slots names _succ, _pred
+
+ def __getitem__(self, node):
+ return UnionMultiInner(self._succ[node], self._pred[node])
+
+
+class FilterAtlas(Mapping): # nodedict, nbrdict, keydict
+ """A read-only Mapping of Mappings with filtering criteria for nodes.
+
+ It is a view into a dict-of-dict data structure, and it selects only
+ nodes that meet the criteria defined by ``NODE_OK``.
+
+ See Also
+ ========
+ FilterAdjacency
+ FilterMultiInner
+ FilterMultiAdjacency
+ """
+
+ def __init__(self, d, NODE_OK):
+ self._atlas = d
+ self.NODE_OK = NODE_OK
+
+ def __len__(self):
+ # check whether NODE_OK stores the number of nodes as `length`
+ # or the nodes themselves as a set `nodes`. If not, count the nodes.
+ if hasattr(self.NODE_OK, "length"):
+ return self.NODE_OK.length
+ if hasattr(self.NODE_OK, "nodes"):
+ return len(self.NODE_OK.nodes & self._atlas.keys())
+ return sum(1 for n in self._atlas if self.NODE_OK(n))
+
+ def __iter__(self):
+ try: # check that NODE_OK has attr 'nodes'
+ node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
+ except AttributeError:
+ node_ok_shorter = False
+ if node_ok_shorter:
+ return (n for n in self.NODE_OK.nodes if n in self._atlas)
+ return (n for n in self._atlas if self.NODE_OK(n))
+
+ def __getitem__(self, key):
+ if key in self._atlas and self.NODE_OK(key):
+ return self._atlas[key]
+ raise KeyError(f"Key {key} not found")
+
+ def __str__(self):
+ return str({nbr: self[nbr] for nbr in self})
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self._atlas!r}, {self.NODE_OK!r})"
+
+
+class FilterAdjacency(Mapping): # edgedict
+ """A read-only Mapping of Mappings with filtering criteria for nodes and edges.
+
+ It is a view into a dict-of-dict-of-dict data structure, and it selects nodes
+ and edges that satisfy specific criteria defined by ``NODE_OK`` and ``EDGE_OK``,
+ respectively.
+
+ See Also
+ ========
+ FilterAtlas
+ FilterMultiInner
+ FilterMultiAdjacency
+ """
+
+ def __init__(self, d, NODE_OK, EDGE_OK):
+ self._atlas = d
+ self.NODE_OK = NODE_OK
+ self.EDGE_OK = EDGE_OK
+
+ def __len__(self):
+ # check whether NODE_OK stores the number of nodes as `length`
+ # or the nodes themselves as a set `nodes`. If not, count the nodes.
+ if hasattr(self.NODE_OK, "length"):
+ return self.NODE_OK.length
+ if hasattr(self.NODE_OK, "nodes"):
+ return len(self.NODE_OK.nodes & self._atlas.keys())
+ return sum(1 for n in self._atlas if self.NODE_OK(n))
+
+ def __iter__(self):
+ try: # check that NODE_OK has attr 'nodes'
+ node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
+ except AttributeError:
+ node_ok_shorter = False
+ if node_ok_shorter:
+ return (n for n in self.NODE_OK.nodes if n in self._atlas)
+ return (n for n in self._atlas if self.NODE_OK(n))
+
+ def __getitem__(self, node):
+ if node in self._atlas and self.NODE_OK(node):
+
+ def new_node_ok(nbr):
+ return self.NODE_OK(nbr) and self.EDGE_OK(node, nbr)
+
+ return FilterAtlas(self._atlas[node], new_node_ok)
+ raise KeyError(f"Key {node} not found")
+
+ def __str__(self):
+ return str({nbr: self[nbr] for nbr in self})
+
+ def __repr__(self):
+ name = self.__class__.__name__
+ return f"{name}({self._atlas!r}, {self.NODE_OK!r}, {self.EDGE_OK!r})"
+
+
+class FilterMultiInner(FilterAdjacency): # muliedge_seconddict
+ """A read-only Mapping of Mappings with filtering criteria for nodes and edges.
+
+ It is a view into a dict-of-dict-of-dict-of-dict data structure, and it selects nodes
+ and edges that meet specific criteria defined by ``NODE_OK`` and ``EDGE_OK``.
+
+ See Also
+ ========
+ FilterAtlas
+ FilterAdjacency
+ FilterMultiAdjacency
+ """
+
+ def __iter__(self):
+ try: # check that NODE_OK has attr 'nodes'
+ node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas)
+ except AttributeError:
+ node_ok_shorter = False
+ if node_ok_shorter:
+ my_nodes = (n for n in self.NODE_OK.nodes if n in self._atlas)
+ else:
+ my_nodes = (n for n in self._atlas if self.NODE_OK(n))
+ for n in my_nodes:
+ some_keys_ok = False
+ for key in self._atlas[n]:
+ if self.EDGE_OK(n, key):
+ some_keys_ok = True
+ break
+ if some_keys_ok is True:
+ yield n
+
+ def __getitem__(self, nbr):
+ if nbr in self._atlas and self.NODE_OK(nbr):
+
+ def new_node_ok(key):
+ return self.EDGE_OK(nbr, key)
+
+ return FilterAtlas(self._atlas[nbr], new_node_ok)
+ raise KeyError(f"Key {nbr} not found")
+
+
+class FilterMultiAdjacency(FilterAdjacency): # multiedgedict
+ """A read-only Mapping of Mappings with filtering criteria
+ for nodes and edges.
+
+ It is a view into a dict-of-dict-of-dict-of-dict data structure,
+ and it selects nodes and edges that satisfy specific criteria
+ defined by ``NODE_OK`` and ``EDGE_OK``, respectively.
+
+ See Also
+ ========
+ FilterAtlas
+ FilterAdjacency
+ FilterMultiInner
+ """
+
+ def __getitem__(self, node):
+ if node in self._atlas and self.NODE_OK(node):
+
+ def edge_ok(nbr, key):
+ return self.NODE_OK(nbr) and self.EDGE_OK(node, nbr, key)
+
+ return FilterMultiInner(self._atlas[node], self.NODE_OK, edge_ok)
+ raise KeyError(f"Key {node} not found")
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/digraph.py b/.venv/lib/python3.12/site-packages/networkx/classes/digraph.py
new file mode 100644
index 00000000..2ba56dea
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/digraph.py
@@ -0,0 +1,1352 @@
+"""Base class for directed graphs."""
+
+from copy import deepcopy
+from functools import cached_property
+
+import networkx as nx
+from networkx import convert
+from networkx.classes.coreviews import AdjacencyView
+from networkx.classes.graph import Graph
+from networkx.classes.reportviews import (
+ DiDegreeView,
+ InDegreeView,
+ InEdgeView,
+ OutDegreeView,
+ OutEdgeView,
+)
+from networkx.exception import NetworkXError
+
+__all__ = ["DiGraph"]
+
+
+class _CachedPropertyResetterAdjAndSucc:
+ """Data Descriptor class that syncs and resets cached properties adj and succ
+
+ The cached properties `adj` and `succ` are reset whenever `_adj` or `_succ`
+ are set to new objects. In addition, the attributes `_succ` and `_adj`
+ are synced so these two names point to the same object.
+
+ Warning: most of the time, when ``G._adj`` is set, ``G._pred`` should also
+ be set to maintain a valid data structure. They share datadicts.
+
+ This object sits on a class and ensures that any instance of that
+ class clears its cached properties "succ" and "adj" whenever the
+ underlying instance attributes "_succ" or "_adj" are set to a new object.
+ It only affects the set process of the obj._adj and obj._succ attribute.
+ All get/del operations act as they normally would.
+
+ For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html
+ """
+
+ def __set__(self, obj, value):
+ od = obj.__dict__
+ od["_adj"] = value
+ od["_succ"] = value
+ # reset cached properties
+ props = [
+ "adj",
+ "succ",
+ "edges",
+ "out_edges",
+ "degree",
+ "out_degree",
+ "in_degree",
+ ]
+ for prop in props:
+ if prop in od:
+ del od[prop]
+
+
+class _CachedPropertyResetterPred:
+ """Data Descriptor class for _pred that resets ``pred`` cached_property when needed
+
+ This assumes that the ``cached_property`` ``G.pred`` should be reset whenever
+ ``G._pred`` is set to a new value.
+
+ Warning: most of the time, when ``G._pred`` is set, ``G._adj`` should also
+ be set to maintain a valid data structure. They share datadicts.
+
+ This object sits on a class and ensures that any instance of that
+ class clears its cached property "pred" whenever the underlying
+ instance attribute "_pred" is set to a new object. It only affects
+ the set process of the obj._pred attribute. All get/del operations
+ act as they normally would.
+
+ For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html
+ """
+
+ def __set__(self, obj, value):
+ od = obj.__dict__
+ od["_pred"] = value
+ # reset cached properties
+ props = ["pred", "in_edges", "degree", "out_degree", "in_degree"]
+ for prop in props:
+ if prop in od:
+ del od[prop]
+
+
+class DiGraph(Graph):
+ """
+ Base class for directed graphs.
+
+ A DiGraph stores nodes and edges with optional data, or attributes.
+
+ DiGraphs hold directed edges. Self loops are allowed but multiple
+ (parallel) edges are not.
+
+ Nodes can be arbitrary (hashable) Python objects with optional
+ key/value attributes. By convention `None` is not used as a node.
+
+ Edges are represented as links between nodes with optional
+ key/value attributes.
+
+ Parameters
+ ----------
+ incoming_graph_data : input graph (optional, default: None)
+ Data to initialize graph. If None (default) an empty
+ graph is created. The data can be any format that is supported
+ by the to_networkx_graph() function, currently including edge list,
+ dict of dicts, dict of lists, NetworkX graph, 2D NumPy array, SciPy
+ sparse matrix, or PyGraphviz graph.
+
+ attr : keyword arguments, optional (default= no attributes)
+ Attributes to add to graph as key=value pairs.
+
+ See Also
+ --------
+ Graph
+ MultiGraph
+ MultiDiGraph
+
+ Examples
+ --------
+ Create an empty graph structure (a "null graph") with no nodes and
+ no edges.
+
+ >>> G = nx.DiGraph()
+
+ G can be grown in several ways.
+
+ **Nodes:**
+
+ Add one node at a time:
+
+ >>> G.add_node(1)
+
+ Add the nodes from any container (a list, dict, set or
+ even the lines from a file or the nodes from another graph).
+
+ >>> G.add_nodes_from([2, 3])
+ >>> G.add_nodes_from(range(100, 110))
+ >>> H = nx.path_graph(10)
+ >>> G.add_nodes_from(H)
+
+ In addition to strings and integers any hashable Python object
+ (except None) can represent a node, e.g. a customized node object,
+ or even another Graph.
+
+ >>> G.add_node(H)
+
+ **Edges:**
+
+ G can also be grown by adding edges.
+
+ Add one edge,
+
+ >>> G.add_edge(1, 2)
+
+ a list of edges,
+
+ >>> G.add_edges_from([(1, 2), (1, 3)])
+
+ or a collection of edges,
+
+ >>> G.add_edges_from(H.edges)
+
+ If some edges connect nodes not yet in the graph, the nodes
+ are added automatically. There are no errors when adding
+ nodes or edges that already exist.
+
+ **Attributes:**
+
+ Each graph, node, and edge can hold key/value attribute pairs
+ in an associated attribute dictionary (the keys must be hashable).
+ By default these are empty, but can be added or changed using
+ add_edge, add_node or direct manipulation of the attribute
+ dictionaries named graph, node and edge respectively.
+
+ >>> G = nx.DiGraph(day="Friday")
+ >>> G.graph
+ {'day': 'Friday'}
+
+ Add node attributes using add_node(), add_nodes_from() or G.nodes
+
+ >>> G.add_node(1, time="5pm")
+ >>> G.add_nodes_from([3], time="2pm")
+ >>> G.nodes[1]
+ {'time': '5pm'}
+ >>> G.nodes[1]["room"] = 714
+ >>> del G.nodes[1]["room"] # remove attribute
+ >>> list(G.nodes(data=True))
+ [(1, {'time': '5pm'}), (3, {'time': '2pm'})]
+
+ Add edge attributes using add_edge(), add_edges_from(), subscript
+ notation, or G.edges.
+
+ >>> G.add_edge(1, 2, weight=4.7)
+ >>> G.add_edges_from([(3, 4), (4, 5)], color="red")
+ >>> G.add_edges_from([(1, 2, {"color": "blue"}), (2, 3, {"weight": 8})])
+ >>> G[1][2]["weight"] = 4.7
+ >>> G.edges[1, 2]["weight"] = 4
+
+ Warning: we protect the graph data structure by making `G.edges[1, 2]` a
+ read-only dict-like structure. However, you can assign to attributes
+ in e.g. `G.edges[1, 2]`. Thus, use 2 sets of brackets to add/change
+ data attributes: `G.edges[1, 2]['weight'] = 4`
+ (For multigraphs: `MG.edges[u, v, key][name] = value`).
+
+ **Shortcuts:**
+
+ Many common graph features allow python syntax to speed reporting.
+
+ >>> 1 in G # check if node in graph
+ True
+ >>> [n for n in G if n < 3] # iterate through nodes
+ [1, 2]
+ >>> len(G) # number of nodes in graph
+ 5
+
+ Often the best way to traverse all edges of a graph is via the neighbors.
+ The neighbors are reported as an adjacency-dict `G.adj` or `G.adjacency()`
+
+ >>> for n, nbrsdict in G.adjacency():
+ ... for nbr, eattr in nbrsdict.items():
+ ... if "weight" in eattr:
+ ... # Do something useful with the edges
+ ... pass
+
+ But the edges reporting object is often more convenient:
+
+ >>> for u, v, weight in G.edges(data="weight"):
+ ... if weight is not None:
+ ... # Do something useful with the edges
+ ... pass
+
+ **Reporting:**
+
+ Simple graph information is obtained using object-attributes and methods.
+ Reporting usually provides views instead of containers to reduce memory
+ usage. The views update as the graph is updated similarly to dict-views.
+ The objects `nodes`, `edges` and `adj` provide access to data attributes
+ via lookup (e.g. `nodes[n]`, `edges[u, v]`, `adj[u][v]`) and iteration
+ (e.g. `nodes.items()`, `nodes.data('color')`,
+ `nodes.data('color', default='blue')` and similarly for `edges`)
+ Views exist for `nodes`, `edges`, `neighbors()`/`adj` and `degree`.
+
+ For details on these and other miscellaneous methods, see below.
+
+ **Subclasses (Advanced):**
+
+ The Graph class uses a dict-of-dict-of-dict data structure.
+ The outer dict (node_dict) holds adjacency information keyed by node.
+ The next dict (adjlist_dict) represents the adjacency information and holds
+ edge data keyed by neighbor. The inner dict (edge_attr_dict) represents
+ the edge data and holds edge attribute values keyed by attribute names.
+
+ Each of these three dicts can be replaced in a subclass by a user defined
+ dict-like object. In general, the dict-like features should be
+ maintained but extra features can be added. To replace one of the
+ dicts create a new graph class by changing the class(!) variable
+ holding the factory for that dict-like structure. The variable names are
+ node_dict_factory, node_attr_dict_factory, adjlist_inner_dict_factory,
+ adjlist_outer_dict_factory, edge_attr_dict_factory and graph_attr_dict_factory.
+
+ node_dict_factory : function, (default: dict)
+ Factory function to be used to create the dict containing node
+ attributes, keyed by node id.
+ It should require no arguments and return a dict-like object
+
+ node_attr_dict_factory: function, (default: dict)
+ Factory function to be used to create the node attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object
+
+ adjlist_outer_dict_factory : function, (default: dict)
+ Factory function to be used to create the outer-most dict
+ in the data structure that holds adjacency info keyed by node.
+ It should require no arguments and return a dict-like object.
+
+ adjlist_inner_dict_factory : function, optional (default: dict)
+ Factory function to be used to create the adjacency list
+ dict which holds edge data keyed by neighbor.
+ It should require no arguments and return a dict-like object
+
+ edge_attr_dict_factory : function, optional (default: dict)
+ Factory function to be used to create the edge attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object.
+
+ graph_attr_dict_factory : function, (default: dict)
+ Factory function to be used to create the graph attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object.
+
+ Typically, if your extension doesn't impact the data structure all
+ methods will inherited without issue except: `to_directed/to_undirected`.
+ By default these methods create a DiGraph/Graph class and you probably
+ want them to create your extension of a DiGraph/Graph. To facilitate
+ this we define two class variables that you can set in your subclass.
+
+ to_directed_class : callable, (default: DiGraph or MultiDiGraph)
+ Class to create a new graph structure in the `to_directed` method.
+ If `None`, a NetworkX class (DiGraph or MultiDiGraph) is used.
+
+ to_undirected_class : callable, (default: Graph or MultiGraph)
+ Class to create a new graph structure in the `to_undirected` method.
+ If `None`, a NetworkX class (Graph or MultiGraph) is used.
+
+ **Subclassing Example**
+
+ Create a low memory graph class that effectively disallows edge
+ attributes by using a single attribute dict for all edges.
+ This reduces the memory used, but you lose edge attributes.
+
+ >>> class ThinGraph(nx.Graph):
+ ... all_edge_dict = {"weight": 1}
+ ...
+ ... def single_edge_dict(self):
+ ... return self.all_edge_dict
+ ...
+ ... edge_attr_dict_factory = single_edge_dict
+ >>> G = ThinGraph()
+ >>> G.add_edge(2, 1)
+ >>> G[2][1]
+ {'weight': 1}
+ >>> G.add_edge(2, 2)
+ >>> G[2][1] is G[2][2]
+ True
+ """
+
+ _adj = _CachedPropertyResetterAdjAndSucc() # type: ignore[assignment]
+ _succ = _adj # type: ignore[has-type]
+ _pred = _CachedPropertyResetterPred()
+
+ def __init__(self, incoming_graph_data=None, **attr):
+ """Initialize a graph with edges, name, or graph attributes.
+
+ Parameters
+ ----------
+ incoming_graph_data : input graph (optional, default: None)
+ Data to initialize graph. If None (default) an empty
+ graph is created. The data can be an edge list, or any
+ NetworkX graph object. If the corresponding optional Python
+ packages are installed the data can also be a 2D NumPy array, a
+ SciPy sparse array, or a PyGraphviz graph.
+
+ attr : keyword arguments, optional (default= no attributes)
+ Attributes to add to graph as key=value pairs.
+
+ See Also
+ --------
+ convert
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G = nx.Graph(name="my graph")
+ >>> e = [(1, 2), (2, 3), (3, 4)] # list of edges
+ >>> G = nx.Graph(e)
+
+ Arbitrary graph attribute pairs (key=value) may be assigned
+
+ >>> G = nx.Graph(e, day="Friday")
+ >>> G.graph
+ {'day': 'Friday'}
+
+ """
+ self.graph = self.graph_attr_dict_factory() # dictionary for graph attributes
+ self._node = self.node_dict_factory() # dictionary for node attr
+ # We store two adjacency lists:
+ # the predecessors of node n are stored in the dict self._pred
+ # the successors of node n are stored in the dict self._succ=self._adj
+ self._adj = self.adjlist_outer_dict_factory() # empty adjacency dict successor
+ self._pred = self.adjlist_outer_dict_factory() # predecessor
+ # Note: self._succ = self._adj # successor
+
+ self.__networkx_cache__ = {}
+ # attempt to load graph with data
+ if incoming_graph_data is not None:
+ convert.to_networkx_graph(incoming_graph_data, create_using=self)
+ # load graph attributes (must be after convert)
+ self.graph.update(attr)
+
+ @cached_property
+ def adj(self):
+ """Graph adjacency object holding the neighbors of each node.
+
+ This object is a read-only dict-like structure with node keys
+ and neighbor-dict values. The neighbor-dict is keyed by neighbor
+ to the edge-data-dict. So `G.adj[3][2]['color'] = 'blue'` sets
+ the color of the edge `(3, 2)` to `"blue"`.
+
+ Iterating over G.adj behaves like a dict. Useful idioms include
+ `for nbr, datadict in G.adj[n].items():`.
+
+ The neighbor information is also provided by subscripting the graph.
+ So `for nbr, foovalue in G[node].data('foo', default=1):` works.
+
+ For directed graphs, `G.adj` holds outgoing (successor) info.
+ """
+ return AdjacencyView(self._succ)
+
+ @cached_property
+ def succ(self):
+ """Graph adjacency object holding the successors of each node.
+
+ This object is a read-only dict-like structure with node keys
+ and neighbor-dict values. The neighbor-dict is keyed by neighbor
+ to the edge-data-dict. So `G.succ[3][2]['color'] = 'blue'` sets
+ the color of the edge `(3, 2)` to `"blue"`.
+
+ Iterating over G.succ behaves like a dict. Useful idioms include
+ `for nbr, datadict in G.succ[n].items():`. A data-view not provided
+ by dicts also exists: `for nbr, foovalue in G.succ[node].data('foo'):`
+ and a default can be set via a `default` argument to the `data` method.
+
+ The neighbor information is also provided by subscripting the graph.
+ So `for nbr, foovalue in G[node].data('foo', default=1):` works.
+
+ For directed graphs, `G.adj` is identical to `G.succ`.
+ """
+ return AdjacencyView(self._succ)
+
+ @cached_property
+ def pred(self):
+ """Graph adjacency object holding the predecessors of each node.
+
+ This object is a read-only dict-like structure with node keys
+ and neighbor-dict values. The neighbor-dict is keyed by neighbor
+ to the edge-data-dict. So `G.pred[2][3]['color'] = 'blue'` sets
+ the color of the edge `(3, 2)` to `"blue"`.
+
+ Iterating over G.pred behaves like a dict. Useful idioms include
+ `for nbr, datadict in G.pred[n].items():`. A data-view not provided
+ by dicts also exists: `for nbr, foovalue in G.pred[node].data('foo'):`
+ A default can be set via a `default` argument to the `data` method.
+ """
+ return AdjacencyView(self._pred)
+
+ def add_node(self, node_for_adding, **attr):
+ """Add a single node `node_for_adding` and update node attributes.
+
+ Parameters
+ ----------
+ node_for_adding : node
+ A node can be any hashable Python object except None.
+ attr : keyword arguments, optional
+ Set or change node attributes using key=value.
+
+ See Also
+ --------
+ add_nodes_from
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_node(1)
+ >>> G.add_node("Hello")
+ >>> K3 = nx.Graph([(0, 1), (1, 2), (2, 0)])
+ >>> G.add_node(K3)
+ >>> G.number_of_nodes()
+ 3
+
+ Use keywords set/change node attributes:
+
+ >>> G.add_node(1, size=10)
+ >>> G.add_node(3, weight=0.4, UTM=("13S", 382871, 3972649))
+
+ Notes
+ -----
+ A hashable object is one that can be used as a key in a Python
+ dictionary. This includes strings, numbers, tuples of strings
+ and numbers, etc.
+
+ On many platforms hashable items also include mutables such as
+ NetworkX Graphs, though one should be careful that the hash
+ doesn't change on mutables.
+ """
+ if node_for_adding not in self._succ:
+ if node_for_adding is None:
+ raise ValueError("None cannot be a node")
+ self._succ[node_for_adding] = self.adjlist_inner_dict_factory()
+ self._pred[node_for_adding] = self.adjlist_inner_dict_factory()
+ attr_dict = self._node[node_for_adding] = self.node_attr_dict_factory()
+ attr_dict.update(attr)
+ else: # update attr even if node already exists
+ self._node[node_for_adding].update(attr)
+ nx._clear_cache(self)
+
+ def add_nodes_from(self, nodes_for_adding, **attr):
+ """Add multiple nodes.
+
+ Parameters
+ ----------
+ nodes_for_adding : iterable container
+ A container of nodes (list, dict, set, etc.).
+ OR
+ A container of (node, attribute dict) tuples.
+ Node attributes are updated using the attribute dict.
+ attr : keyword arguments, optional (default= no attributes)
+ Update attributes for all nodes in nodes.
+ Node attributes specified in nodes as a tuple take
+ precedence over attributes specified via keyword arguments.
+
+ See Also
+ --------
+ add_node
+
+ Notes
+ -----
+ When adding nodes from an iterator over the graph you are changing,
+ a `RuntimeError` can be raised with message:
+ `RuntimeError: dictionary changed size during iteration`. This
+ happens when the graph's underlying dictionary is modified during
+ iteration. To avoid this error, evaluate the iterator into a separate
+ object, e.g. by using `list(iterator_of_nodes)`, and pass this
+ object to `G.add_nodes_from`.
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_nodes_from("Hello")
+ >>> K3 = nx.Graph([(0, 1), (1, 2), (2, 0)])
+ >>> G.add_nodes_from(K3)
+ >>> sorted(G.nodes(), key=str)
+ [0, 1, 2, 'H', 'e', 'l', 'o']
+
+ Use keywords to update specific node attributes for every node.
+
+ >>> G.add_nodes_from([1, 2], size=10)
+ >>> G.add_nodes_from([3, 4], weight=0.4)
+
+ Use (node, attrdict) tuples to update attributes for specific nodes.
+
+ >>> G.add_nodes_from([(1, dict(size=11)), (2, {"color": "blue"})])
+ >>> G.nodes[1]["size"]
+ 11
+ >>> H = nx.Graph()
+ >>> H.add_nodes_from(G.nodes(data=True))
+ >>> H.nodes[1]["size"]
+ 11
+
+ Evaluate an iterator over a graph if using it to modify the same graph
+
+ >>> G = nx.DiGraph([(0, 1), (1, 2), (3, 4)])
+ >>> # wrong way - will raise RuntimeError
+ >>> # G.add_nodes_from(n + 1 for n in G.nodes)
+ >>> # correct way
+ >>> G.add_nodes_from(list(n + 1 for n in G.nodes))
+ """
+ for n in nodes_for_adding:
+ try:
+ newnode = n not in self._node
+ newdict = attr
+ except TypeError:
+ n, ndict = n
+ newnode = n not in self._node
+ newdict = attr.copy()
+ newdict.update(ndict)
+ if newnode:
+ if n is None:
+ raise ValueError("None cannot be a node")
+ self._succ[n] = self.adjlist_inner_dict_factory()
+ self._pred[n] = self.adjlist_inner_dict_factory()
+ self._node[n] = self.node_attr_dict_factory()
+ self._node[n].update(newdict)
+ nx._clear_cache(self)
+
+ def remove_node(self, n):
+ """Remove node n.
+
+ Removes the node n and all adjacent edges.
+ Attempting to remove a nonexistent node will raise an exception.
+
+ Parameters
+ ----------
+ n : node
+ A node in the graph
+
+ Raises
+ ------
+ NetworkXError
+ If n is not in the graph.
+
+ See Also
+ --------
+ remove_nodes_from
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> list(G.edges)
+ [(0, 1), (1, 2)]
+ >>> G.remove_node(1)
+ >>> list(G.edges)
+ []
+
+ """
+ try:
+ nbrs = self._succ[n]
+ del self._node[n]
+ except KeyError as err: # NetworkXError if n not in self
+ raise NetworkXError(f"The node {n} is not in the digraph.") from err
+ for u in nbrs:
+ del self._pred[u][n] # remove all edges n-u in digraph
+ del self._succ[n] # remove node from succ
+ for u in self._pred[n]:
+ del self._succ[u][n] # remove all edges n-u in digraph
+ del self._pred[n] # remove node from pred
+ nx._clear_cache(self)
+
+ def remove_nodes_from(self, nodes):
+ """Remove multiple nodes.
+
+ Parameters
+ ----------
+ nodes : iterable container
+ A container of nodes (list, dict, set, etc.). If a node
+ in the container is not in the graph it is silently ignored.
+
+ See Also
+ --------
+ remove_node
+
+ Notes
+ -----
+ When removing nodes from an iterator over the graph you are changing,
+ a `RuntimeError` will be raised with message:
+ `RuntimeError: dictionary changed size during iteration`. This
+ happens when the graph's underlying dictionary is modified during
+ iteration. To avoid this error, evaluate the iterator into a separate
+ object, e.g. by using `list(iterator_of_nodes)`, and pass this
+ object to `G.remove_nodes_from`.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> e = list(G.nodes)
+ >>> e
+ [0, 1, 2]
+ >>> G.remove_nodes_from(e)
+ >>> list(G.nodes)
+ []
+
+ Evaluate an iterator over a graph if using it to modify the same graph
+
+ >>> G = nx.DiGraph([(0, 1), (1, 2), (3, 4)])
+ >>> # this command will fail, as the graph's dict is modified during iteration
+ >>> # G.remove_nodes_from(n for n in G.nodes if n < 2)
+ >>> # this command will work, since the dictionary underlying graph is not modified
+ >>> G.remove_nodes_from(list(n for n in G.nodes if n < 2))
+ """
+ for n in nodes:
+ try:
+ succs = self._succ[n]
+ del self._node[n]
+ for u in succs:
+ del self._pred[u][n] # remove all edges n-u in digraph
+ del self._succ[n] # now remove node
+ for u in self._pred[n]:
+ del self._succ[u][n] # remove all edges n-u in digraph
+ del self._pred[n] # now remove node
+ except KeyError:
+ pass # silent failure on remove
+ nx._clear_cache(self)
+
+ def add_edge(self, u_of_edge, v_of_edge, **attr):
+ """Add an edge between u and v.
+
+ The nodes u and v will be automatically added if they are
+ not already in the graph.
+
+ Edge attributes can be specified with keywords or by directly
+ accessing the edge's attribute dictionary. See examples below.
+
+ Parameters
+ ----------
+ u_of_edge, v_of_edge : nodes
+ Nodes can be, for example, strings or numbers.
+ Nodes must be hashable (and not None) Python objects.
+ attr : keyword arguments, optional
+ Edge data (or labels or objects) can be assigned using
+ keyword arguments.
+
+ See Also
+ --------
+ add_edges_from : add a collection of edges
+
+ Notes
+ -----
+ Adding an edge that already exists updates the edge data.
+
+ Many NetworkX algorithms designed for weighted graphs use
+ an edge attribute (by default `weight`) to hold a numerical value.
+
+ Examples
+ --------
+ The following all add the edge e=(1, 2) to graph G:
+
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> e = (1, 2)
+ >>> G.add_edge(1, 2) # explicit two-node form
+ >>> G.add_edge(*e) # single edge as tuple of two nodes
+ >>> G.add_edges_from([(1, 2)]) # add edges from iterable container
+
+ Associate data to edges using keywords:
+
+ >>> G.add_edge(1, 2, weight=3)
+ >>> G.add_edge(1, 3, weight=7, capacity=15, length=342.7)
+
+ For non-string attribute keys, use subscript notation.
+
+ >>> G.add_edge(1, 2)
+ >>> G[1][2].update({0: 5})
+ >>> G.edges[1, 2].update({0: 5})
+ """
+ u, v = u_of_edge, v_of_edge
+ # add nodes
+ if u not in self._succ:
+ if u is None:
+ raise ValueError("None cannot be a node")
+ self._succ[u] = self.adjlist_inner_dict_factory()
+ self._pred[u] = self.adjlist_inner_dict_factory()
+ self._node[u] = self.node_attr_dict_factory()
+ if v not in self._succ:
+ if v is None:
+ raise ValueError("None cannot be a node")
+ self._succ[v] = self.adjlist_inner_dict_factory()
+ self._pred[v] = self.adjlist_inner_dict_factory()
+ self._node[v] = self.node_attr_dict_factory()
+ # add the edge
+ datadict = self._adj[u].get(v, self.edge_attr_dict_factory())
+ datadict.update(attr)
+ self._succ[u][v] = datadict
+ self._pred[v][u] = datadict
+ nx._clear_cache(self)
+
+ def add_edges_from(self, ebunch_to_add, **attr):
+ """Add all the edges in ebunch_to_add.
+
+ Parameters
+ ----------
+ ebunch_to_add : container of edges
+ Each edge given in the container will be added to the
+ graph. The edges must be given as 2-tuples (u, v) or
+ 3-tuples (u, v, d) where d is a dictionary containing edge data.
+ attr : keyword arguments, optional
+ Edge data (or labels or objects) can be assigned using
+ keyword arguments.
+
+ See Also
+ --------
+ add_edge : add a single edge
+ add_weighted_edges_from : convenient way to add weighted edges
+
+ Notes
+ -----
+ Adding the same edge twice has no effect but any edge data
+ will be updated when each duplicate edge is added.
+
+ Edge attributes specified in an ebunch take precedence over
+ attributes specified via keyword arguments.
+
+ When adding edges from an iterator over the graph you are changing,
+ a `RuntimeError` can be raised with message:
+ `RuntimeError: dictionary changed size during iteration`. This
+ happens when the graph's underlying dictionary is modified during
+ iteration. To avoid this error, evaluate the iterator into a separate
+ object, e.g. by using `list(iterator_of_edges)`, and pass this
+ object to `G.add_edges_from`.
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_edges_from([(0, 1), (1, 2)]) # using a list of edge tuples
+ >>> e = zip(range(0, 3), range(1, 4))
+ >>> G.add_edges_from(e) # Add the path graph 0-1-2-3
+
+ Associate data to edges
+
+ >>> G.add_edges_from([(1, 2), (2, 3)], weight=3)
+ >>> G.add_edges_from([(3, 4), (1, 4)], label="WN2898")
+
+ Evaluate an iterator over a graph if using it to modify the same graph
+
+ >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
+ >>> # Grow graph by one new node, adding edges to all existing nodes.
+ >>> # wrong way - will raise RuntimeError
+ >>> # G.add_edges_from(((5, n) for n in G.nodes))
+ >>> # right way - note that there will be no self-edge for node 5
+ >>> G.add_edges_from(list((5, n) for n in G.nodes))
+ """
+ for e in ebunch_to_add:
+ ne = len(e)
+ if ne == 3:
+ u, v, dd = e
+ elif ne == 2:
+ u, v = e
+ dd = {}
+ else:
+ raise NetworkXError(f"Edge tuple {e} must be a 2-tuple or 3-tuple.")
+ if u not in self._succ:
+ if u is None:
+ raise ValueError("None cannot be a node")
+ self._succ[u] = self.adjlist_inner_dict_factory()
+ self._pred[u] = self.adjlist_inner_dict_factory()
+ self._node[u] = self.node_attr_dict_factory()
+ if v not in self._succ:
+ if v is None:
+ raise ValueError("None cannot be a node")
+ self._succ[v] = self.adjlist_inner_dict_factory()
+ self._pred[v] = self.adjlist_inner_dict_factory()
+ self._node[v] = self.node_attr_dict_factory()
+ datadict = self._adj[u].get(v, self.edge_attr_dict_factory())
+ datadict.update(attr)
+ datadict.update(dd)
+ self._succ[u][v] = datadict
+ self._pred[v][u] = datadict
+ nx._clear_cache(self)
+
+ def remove_edge(self, u, v):
+ """Remove the edge between u and v.
+
+ Parameters
+ ----------
+ u, v : nodes
+ Remove the edge between nodes u and v.
+
+ Raises
+ ------
+ NetworkXError
+ If there is not an edge between u and v.
+
+ See Also
+ --------
+ remove_edges_from : remove a collection of edges
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, etc
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.remove_edge(0, 1)
+ >>> e = (1, 2)
+ >>> G.remove_edge(*e) # unpacks e from an edge tuple
+ >>> e = (2, 3, {"weight": 7}) # an edge with attribute data
+ >>> G.remove_edge(*e[:2]) # select first part of edge tuple
+ """
+ try:
+ del self._succ[u][v]
+ del self._pred[v][u]
+ except KeyError as err:
+ raise NetworkXError(f"The edge {u}-{v} not in graph.") from err
+ nx._clear_cache(self)
+
+ def remove_edges_from(self, ebunch):
+ """Remove all edges specified in ebunch.
+
+ Parameters
+ ----------
+ ebunch: list or container of edge tuples
+ Each edge given in the list or container will be removed
+ from the graph. The edges can be:
+
+ - 2-tuples (u, v) edge between u and v.
+ - 3-tuples (u, v, k) where k is ignored.
+
+ See Also
+ --------
+ remove_edge : remove a single edge
+
+ Notes
+ -----
+ Will fail silently if an edge in ebunch is not in the graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> ebunch = [(1, 2), (2, 3)]
+ >>> G.remove_edges_from(ebunch)
+ """
+ for e in ebunch:
+ u, v = e[:2] # ignore edge data
+ if u in self._succ and v in self._succ[u]:
+ del self._succ[u][v]
+ del self._pred[v][u]
+ nx._clear_cache(self)
+
+ def has_successor(self, u, v):
+ """Returns True if node u has successor v.
+
+ This is true if graph has the edge u->v.
+ """
+ return u in self._succ and v in self._succ[u]
+
+ def has_predecessor(self, u, v):
+ """Returns True if node u has predecessor v.
+
+ This is true if graph has the edge u<-v.
+ """
+ return u in self._pred and v in self._pred[u]
+
+ def successors(self, n):
+ """Returns an iterator over successor nodes of n.
+
+ A successor of n is a node m such that there exists a directed
+ edge from n to m.
+
+ Parameters
+ ----------
+ n : node
+ A node in the graph
+
+ Raises
+ ------
+ NetworkXError
+ If n is not in the graph.
+
+ See Also
+ --------
+ predecessors
+
+ Notes
+ -----
+ neighbors() and successors() are the same.
+ """
+ try:
+ return iter(self._succ[n])
+ except KeyError as err:
+ raise NetworkXError(f"The node {n} is not in the digraph.") from err
+
+ # digraph definitions
+ neighbors = successors
+
+ def predecessors(self, n):
+ """Returns an iterator over predecessor nodes of n.
+
+ A predecessor of n is a node m such that there exists a directed
+ edge from m to n.
+
+ Parameters
+ ----------
+ n : node
+ A node in the graph
+
+ Raises
+ ------
+ NetworkXError
+ If n is not in the graph.
+
+ See Also
+ --------
+ successors
+ """
+ try:
+ return iter(self._pred[n])
+ except KeyError as err:
+ raise NetworkXError(f"The node {n} is not in the digraph.") from err
+
+ @cached_property
+ def edges(self):
+ """An OutEdgeView of the DiGraph as G.edges or G.edges().
+
+ edges(self, nbunch=None, data=False, default=None)
+
+ The OutEdgeView provides set-like operations on the edge-tuples
+ as well as edge attribute lookup. When called, it also provides
+ an EdgeDataView object which allows control of access to edge
+ attributes (but does not provide set-like operations).
+ Hence, `G.edges[u, v]['color']` provides the value of the color
+ attribute for edge `(u, v)` while
+ `for (u, v, c) in G.edges.data('color', default='red'):`
+ iterates through all the edges yielding the color attribute
+ with default `'red'` if no color attribute exists.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges from these nodes.
+ data : string or bool, optional (default=False)
+ The edge attribute returned in 3-tuple (u, v, ddict[data]).
+ If True, return edge attribute dict in 3-tuple (u, v, ddict).
+ If False, return 2-tuple (u, v).
+ default : value, optional (default=None)
+ Value used for edges that don't have the requested attribute.
+ Only relevant if data is not True or False.
+
+ Returns
+ -------
+ edges : OutEdgeView
+ A view of edge attributes, usually it iterates over (u, v)
+ or (u, v, d) tuples of edges, but can also be used for
+ attribute lookup as `edges[u, v]['foo']`.
+
+ See Also
+ --------
+ in_edges, out_edges
+
+ Notes
+ -----
+ Nodes in nbunch that are not in the graph will be (quietly) ignored.
+ For directed graphs this returns the out-edges.
+
+ Examples
+ --------
+ >>> G = nx.DiGraph() # or MultiDiGraph, etc
+ >>> nx.add_path(G, [0, 1, 2])
+ >>> G.add_edge(2, 3, weight=5)
+ >>> [e for e in G.edges]
+ [(0, 1), (1, 2), (2, 3)]
+ >>> G.edges.data() # default data is {} (empty dict)
+ OutEdgeDataView([(0, 1, {}), (1, 2, {}), (2, 3, {'weight': 5})])
+ >>> G.edges.data("weight", default=1)
+ OutEdgeDataView([(0, 1, 1), (1, 2, 1), (2, 3, 5)])
+ >>> G.edges([0, 2]) # only edges originating from these nodes
+ OutEdgeDataView([(0, 1), (2, 3)])
+ >>> G.edges(0) # only edges from node 0
+ OutEdgeDataView([(0, 1)])
+
+ """
+ return OutEdgeView(self)
+
+ # alias out_edges to edges
+ @cached_property
+ def out_edges(self):
+ return OutEdgeView(self)
+
+ out_edges.__doc__ = edges.__doc__
+
+ @cached_property
+ def in_edges(self):
+ """A view of the in edges of the graph as G.in_edges or G.in_edges().
+
+ in_edges(self, nbunch=None, data=False, default=None):
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+ data : string or bool, optional (default=False)
+ The edge attribute returned in 3-tuple (u, v, ddict[data]).
+ If True, return edge attribute dict in 3-tuple (u, v, ddict).
+ If False, return 2-tuple (u, v).
+ default : value, optional (default=None)
+ Value used for edges that don't have the requested attribute.
+ Only relevant if data is not True or False.
+
+ Returns
+ -------
+ in_edges : InEdgeView or InEdgeDataView
+ A view of edge attributes, usually it iterates over (u, v)
+ or (u, v, d) tuples of edges, but can also be used for
+ attribute lookup as `edges[u, v]['foo']`.
+
+ Examples
+ --------
+ >>> G = nx.DiGraph()
+ >>> G.add_edge(1, 2, color="blue")
+ >>> G.in_edges()
+ InEdgeView([(1, 2)])
+ >>> G.in_edges(nbunch=2)
+ InEdgeDataView([(1, 2)])
+
+ See Also
+ --------
+ edges
+ """
+ return InEdgeView(self)
+
+ @cached_property
+ def degree(self):
+ """A DegreeView for the Graph as G.degree or G.degree().
+
+ The node degree is the number of edges adjacent to the node.
+ The weighted node degree is the sum of the edge weights for
+ edges incident to that node.
+
+ This object provides an iterator for (node, degree) as well as
+ lookup for the degree for a single node.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+
+ weight : string or None, optional (default=None)
+ The name of an edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ Returns
+ -------
+ DiDegreeView or int
+ If multiple nodes are requested (the default), returns a `DiDegreeView`
+ mapping nodes to their degree.
+ If a single node is requested, returns the degree of the node as an integer.
+
+ See Also
+ --------
+ in_degree, out_degree
+
+ Examples
+ --------
+ >>> G = nx.DiGraph() # or MultiDiGraph
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.degree(0) # node 0 with degree 1
+ 1
+ >>> list(G.degree([0, 1, 2]))
+ [(0, 1), (1, 2), (2, 2)]
+
+ """
+ return DiDegreeView(self)
+
+ @cached_property
+ def in_degree(self):
+ """An InDegreeView for (node, in_degree) or in_degree for single node.
+
+ The node in_degree is the number of edges pointing to the node.
+ The weighted node degree is the sum of the edge weights for
+ edges incident to that node.
+
+ This object provides an iteration over (node, in_degree) as well as
+ lookup for the degree for a single node.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+
+ weight : string or None, optional (default=None)
+ The name of an edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ Returns
+ -------
+ If a single node is requested
+ deg : int
+ In-degree of the node
+
+ OR if multiple nodes are requested
+ nd_iter : iterator
+ The iterator returns two-tuples of (node, in-degree).
+
+ See Also
+ --------
+ degree, out_degree
+
+ Examples
+ --------
+ >>> G = nx.DiGraph()
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.in_degree(0) # node 0 with degree 0
+ 0
+ >>> list(G.in_degree([0, 1, 2]))
+ [(0, 0), (1, 1), (2, 1)]
+
+ """
+ return InDegreeView(self)
+
+ @cached_property
+ def out_degree(self):
+ """An OutDegreeView for (node, out_degree)
+
+ The node out_degree is the number of edges pointing out of the node.
+ The weighted node degree is the sum of the edge weights for
+ edges incident to that node.
+
+ This object provides an iterator over (node, out_degree) as well as
+ lookup for the degree for a single node.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+
+ weight : string or None, optional (default=None)
+ The name of an edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ Returns
+ -------
+ If a single node is requested
+ deg : int
+ Out-degree of the node
+
+ OR if multiple nodes are requested
+ nd_iter : iterator
+ The iterator returns two-tuples of (node, out-degree).
+
+ See Also
+ --------
+ degree, in_degree
+
+ Examples
+ --------
+ >>> G = nx.DiGraph()
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.out_degree(0) # node 0 with degree 1
+ 1
+ >>> list(G.out_degree([0, 1, 2]))
+ [(0, 1), (1, 1), (2, 1)]
+
+ """
+ return OutDegreeView(self)
+
+ def clear(self):
+ """Remove all nodes and edges from the graph.
+
+ This also removes the name, and all graph, node, and edge attributes.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.clear()
+ >>> list(G.nodes)
+ []
+ >>> list(G.edges)
+ []
+
+ """
+ self._succ.clear()
+ self._pred.clear()
+ self._node.clear()
+ self.graph.clear()
+ nx._clear_cache(self)
+
+ def clear_edges(self):
+ """Remove all edges from the graph without altering nodes.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.clear_edges()
+ >>> list(G.nodes)
+ [0, 1, 2, 3]
+ >>> list(G.edges)
+ []
+
+ """
+ for predecessor_dict in self._pred.values():
+ predecessor_dict.clear()
+ for successor_dict in self._succ.values():
+ successor_dict.clear()
+ nx._clear_cache(self)
+
+ def is_multigraph(self):
+ """Returns True if graph is a multigraph, False otherwise."""
+ return False
+
+ def is_directed(self):
+ """Returns True if graph is directed, False otherwise."""
+ return True
+
+ def to_undirected(self, reciprocal=False, as_view=False):
+ """Returns an undirected representation of the digraph.
+
+ Parameters
+ ----------
+ reciprocal : bool (optional)
+ If True only keep edges that appear in both directions
+ in the original digraph.
+ as_view : bool (optional, default=False)
+ If True return an undirected view of the original directed graph.
+
+ Returns
+ -------
+ G : Graph
+ An undirected graph with the same name and nodes and
+ with edge (u, v, data) if either (u, v, data) or (v, u, data)
+ is in the digraph. If both edges exist in digraph and
+ their edge data is different, only one edge is created
+ with an arbitrary choice of which edge data to use.
+ You must check and correct for this manually if desired.
+
+ See Also
+ --------
+ Graph, copy, add_edge, add_edges_from
+
+ Notes
+ -----
+ If edges in both directions (u, v) and (v, u) exist in the
+ graph, attributes for the new undirected edge will be a combination of
+ the attributes of the directed edges. The edge data is updated
+ in the (arbitrary) order that the edges are encountered. For
+ more customized control of the edge attributes use add_edge().
+
+ This returns a "deepcopy" of the edge, node, and
+ graph attributes which attempts to completely copy
+ all of the data and references.
+
+ This is in contrast to the similar G=DiGraph(D) which returns a
+ shallow copy of the data.
+
+ See the Python copy module for more information on shallow
+ and deep copies, https://docs.python.org/3/library/copy.html.
+
+ Warning: If you have subclassed DiGraph to use dict-like objects
+ in the data structure, those changes do not transfer to the
+ Graph created by this method.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(2) # or MultiGraph, etc
+ >>> H = G.to_directed()
+ >>> list(H.edges)
+ [(0, 1), (1, 0)]
+ >>> G2 = H.to_undirected()
+ >>> list(G2.edges)
+ [(0, 1)]
+ """
+ graph_class = self.to_undirected_class()
+ if as_view is True:
+ return nx.graphviews.generic_graph_view(self, graph_class)
+ # deepcopy when not a view
+ G = graph_class()
+ G.graph.update(deepcopy(self.graph))
+ G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
+ if reciprocal is True:
+ G.add_edges_from(
+ (u, v, deepcopy(d))
+ for u, nbrs in self._adj.items()
+ for v, d in nbrs.items()
+ if v in self._pred[u]
+ )
+ else:
+ G.add_edges_from(
+ (u, v, deepcopy(d))
+ for u, nbrs in self._adj.items()
+ for v, d in nbrs.items()
+ )
+ return G
+
+ def reverse(self, copy=True):
+ """Returns the reverse of the graph.
+
+ The reverse is a graph with the same nodes and edges
+ but with the directions of the edges reversed.
+
+ Parameters
+ ----------
+ copy : bool optional (default=True)
+ If True, return a new DiGraph holding the reversed edges.
+ If False, the reverse graph is created using a view of
+ the original graph.
+ """
+ if copy:
+ H = self.__class__()
+ H.graph.update(deepcopy(self.graph))
+ H.add_nodes_from((n, deepcopy(d)) for n, d in self.nodes.items())
+ H.add_edges_from((v, u, deepcopy(d)) for u, v, d in self.edges(data=True))
+ return H
+ return nx.reverse_view(self)
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/filters.py b/.venv/lib/python3.12/site-packages/networkx/classes/filters.py
new file mode 100644
index 00000000..e989e22b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/filters.py
@@ -0,0 +1,95 @@
+"""Filter factories to hide or show sets of nodes and edges.
+
+These filters return the function used when creating `SubGraph`.
+"""
+
+__all__ = [
+ "no_filter",
+ "hide_nodes",
+ "hide_edges",
+ "hide_multiedges",
+ "hide_diedges",
+ "hide_multidiedges",
+ "show_nodes",
+ "show_edges",
+ "show_multiedges",
+ "show_diedges",
+ "show_multidiedges",
+]
+
+
+def no_filter(*items):
+ """Returns a filter function that always evaluates to True."""
+ return True
+
+
+def hide_nodes(nodes):
+ """Returns a filter function that hides specific nodes."""
+ nodes = set(nodes)
+ return lambda node: node not in nodes
+
+
+def hide_diedges(edges):
+ """Returns a filter function that hides specific directed edges."""
+ edges = {(u, v) for u, v in edges}
+ return lambda u, v: (u, v) not in edges
+
+
+def hide_edges(edges):
+ """Returns a filter function that hides specific undirected edges."""
+ alledges = set(edges) | {(v, u) for (u, v) in edges}
+ return lambda u, v: (u, v) not in alledges
+
+
+def hide_multidiedges(edges):
+ """Returns a filter function that hides specific multi-directed edges."""
+ edges = {(u, v, k) for u, v, k in edges}
+ return lambda u, v, k: (u, v, k) not in edges
+
+
+def hide_multiedges(edges):
+ """Returns a filter function that hides specific multi-undirected edges."""
+ alledges = set(edges) | {(v, u, k) for (u, v, k) in edges}
+ return lambda u, v, k: (u, v, k) not in alledges
+
+
+# write show_nodes as a class to make SubGraph pickleable
+class show_nodes:
+ """Filter class to show specific nodes.
+
+ Attach the set of nodes as an attribute to speed up this commonly used filter
+
+ Note that another allowed attribute for filters is to store the number of nodes
+ on the filter as attribute `length` (used in `__len__`). It is a user
+ responsibility to ensure this attribute is accurate if present.
+ """
+
+ def __init__(self, nodes):
+ self.nodes = set(nodes)
+
+ def __call__(self, node):
+ return node in self.nodes
+
+
+def show_diedges(edges):
+ """Returns a filter function that shows specific directed edges."""
+ edges = {(u, v) for u, v in edges}
+ return lambda u, v: (u, v) in edges
+
+
+def show_edges(edges):
+ """Returns a filter function that shows specific undirected edges."""
+ alledges = set(edges) | {(v, u) for (u, v) in edges}
+ return lambda u, v: (u, v) in alledges
+
+
+def show_multidiedges(edges):
+ """Returns a filter function that shows specific multi-directed edges."""
+ edges = {(u, v, k) for u, v, k in edges}
+ return lambda u, v, k: (u, v, k) in edges
+
+
+def show_multiedges(edges):
+ """Returns a filter function that shows specific multi-undirected edges."""
+ alledges = set(edges) | {(v, u, k) for (u, v, k) in edges}
+ return lambda u, v, k: (u, v, k) in alledges
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/function.py b/.venv/lib/python3.12/site-packages/networkx/classes/function.py
new file mode 100644
index 00000000..7f42f93e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/function.py
@@ -0,0 +1,1407 @@
+"""Functional interface to graph methods and assorted utilities."""
+
+from collections import Counter
+from itertools import chain
+
+import networkx as nx
+from networkx.utils import not_implemented_for, pairwise
+
+__all__ = [
+ "nodes",
+ "edges",
+ "degree",
+ "degree_histogram",
+ "neighbors",
+ "number_of_nodes",
+ "number_of_edges",
+ "density",
+ "is_directed",
+ "freeze",
+ "is_frozen",
+ "subgraph",
+ "induced_subgraph",
+ "edge_subgraph",
+ "restricted_view",
+ "to_directed",
+ "to_undirected",
+ "add_star",
+ "add_path",
+ "add_cycle",
+ "create_empty_copy",
+ "set_node_attributes",
+ "get_node_attributes",
+ "remove_node_attributes",
+ "set_edge_attributes",
+ "get_edge_attributes",
+ "remove_edge_attributes",
+ "all_neighbors",
+ "non_neighbors",
+ "non_edges",
+ "common_neighbors",
+ "is_weighted",
+ "is_negatively_weighted",
+ "is_empty",
+ "selfloop_edges",
+ "nodes_with_selfloops",
+ "number_of_selfloops",
+ "path_weight",
+ "is_path",
+]
+
+
+def nodes(G):
+ """Returns a NodeView over the graph nodes.
+
+ This function wraps the :func:`G.nodes <networkx.Graph.nodes>` property.
+ """
+ return G.nodes()
+
+
+def edges(G, nbunch=None):
+ """Returns an edge view of edges incident to nodes in nbunch.
+
+ Return all edges if nbunch is unspecified or nbunch=None.
+
+ For digraphs, edges=out_edges
+
+ This function wraps the :func:`G.edges <networkx.Graph.edges>` property.
+ """
+ return G.edges(nbunch)
+
+
+def degree(G, nbunch=None, weight=None):
+ """Returns a degree view of single node or of nbunch of nodes.
+ If nbunch is omitted, then return degrees of *all* nodes.
+
+ This function wraps the :func:`G.degree <networkx.Graph.degree>` property.
+ """
+ return G.degree(nbunch, weight)
+
+
+def neighbors(G, n):
+ """Returns an iterator over all neighbors of node n.
+
+ This function wraps the :func:`G.neighbors <networkx.Graph.neighbors>` function.
+ """
+ return G.neighbors(n)
+
+
+def number_of_nodes(G):
+ """Returns the number of nodes in the graph.
+
+ This function wraps the :func:`G.number_of_nodes <networkx.Graph.number_of_nodes>` function.
+ """
+ return G.number_of_nodes()
+
+
+def number_of_edges(G):
+ """Returns the number of edges in the graph.
+
+ This function wraps the :func:`G.number_of_edges <networkx.Graph.number_of_edges>` function.
+ """
+ return G.number_of_edges()
+
+
+def density(G):
+ r"""Returns the density of a graph.
+
+ The density for undirected graphs is
+
+ .. math::
+
+ d = \frac{2m}{n(n-1)},
+
+ and for directed graphs is
+
+ .. math::
+
+ d = \frac{m}{n(n-1)},
+
+ where `n` is the number of nodes and `m` is the number of edges in `G`.
+
+ Notes
+ -----
+ The density is 0 for a graph without edges and 1 for a complete graph.
+ The density of multigraphs can be higher than 1.
+
+ Self loops are counted in the total number of edges so graphs with self
+ loops can have density higher than 1.
+ """
+ n = number_of_nodes(G)
+ m = number_of_edges(G)
+ if m == 0 or n <= 1:
+ return 0
+ d = m / (n * (n - 1))
+ if not G.is_directed():
+ d *= 2
+ return d
+
+
+def degree_histogram(G):
+ """Returns a list of the frequency of each degree value.
+
+ Parameters
+ ----------
+ G : Networkx graph
+ A graph
+
+ Returns
+ -------
+ hist : list
+ A list of frequencies of degrees.
+ The degree values are the index in the list.
+
+ Notes
+ -----
+ Note: the bins are width one, hence len(list) can be large
+ (Order(number_of_edges))
+ """
+ counts = Counter(d for n, d in G.degree())
+ return [counts.get(i, 0) for i in range(max(counts) + 1 if counts else 0)]
+
+
+def is_directed(G):
+ """Return True if graph is directed."""
+ return G.is_directed()
+
+
+def frozen(*args, **kwargs):
+ """Dummy method for raising errors when trying to modify frozen graphs"""
+ raise nx.NetworkXError("Frozen graph can't be modified")
+
+
+def freeze(G):
+ """Modify graph to prevent further change by adding or removing
+ nodes or edges.
+
+ Node and edge data can still be modified.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> G = nx.freeze(G)
+ >>> try:
+ ... G.add_edge(4, 5)
+ ... except nx.NetworkXError as err:
+ ... print(str(err))
+ Frozen graph can't be modified
+
+ Notes
+ -----
+ To "unfreeze" a graph you must make a copy by creating a new graph object:
+
+ >>> graph = nx.path_graph(4)
+ >>> frozen_graph = nx.freeze(graph)
+ >>> unfrozen_graph = nx.Graph(frozen_graph)
+ >>> nx.is_frozen(unfrozen_graph)
+ False
+
+ See Also
+ --------
+ is_frozen
+ """
+ G.add_node = frozen
+ G.add_nodes_from = frozen
+ G.remove_node = frozen
+ G.remove_nodes_from = frozen
+ G.add_edge = frozen
+ G.add_edges_from = frozen
+ G.add_weighted_edges_from = frozen
+ G.remove_edge = frozen
+ G.remove_edges_from = frozen
+ G.clear = frozen
+ G.clear_edges = frozen
+ G.frozen = True
+ return G
+
+
+def is_frozen(G):
+ """Returns True if graph is frozen.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ See Also
+ --------
+ freeze
+ """
+ try:
+ return G.frozen
+ except AttributeError:
+ return False
+
+
+def add_star(G_to_add_to, nodes_for_star, **attr):
+ """Add a star to Graph G_to_add_to.
+
+ The first node in `nodes_for_star` is the middle of the star.
+ It is connected to all other nodes.
+
+ Parameters
+ ----------
+ G_to_add_to : graph
+ A NetworkX graph
+ nodes_for_star : iterable container
+ A container of nodes.
+ attr : keyword arguments, optional (default= no attributes)
+ Attributes to add to every edge in star.
+
+ See Also
+ --------
+ add_path, add_cycle
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> nx.add_star(G, [0, 1, 2, 3])
+ >>> nx.add_star(G, [10, 11, 12], weight=2)
+ """
+ nlist = iter(nodes_for_star)
+ try:
+ v = next(nlist)
+ except StopIteration:
+ return
+ G_to_add_to.add_node(v)
+ edges = ((v, n) for n in nlist)
+ G_to_add_to.add_edges_from(edges, **attr)
+
+
+def add_path(G_to_add_to, nodes_for_path, **attr):
+ """Add a path to the Graph G_to_add_to.
+
+ Parameters
+ ----------
+ G_to_add_to : graph
+ A NetworkX graph
+ nodes_for_path : iterable container
+ A container of nodes. A path will be constructed from
+ the nodes (in order) and added to the graph.
+ attr : keyword arguments, optional (default= no attributes)
+ Attributes to add to every edge in path.
+
+ See Also
+ --------
+ add_star, add_cycle
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> nx.add_path(G, [10, 11, 12], weight=7)
+ """
+ nlist = iter(nodes_for_path)
+ try:
+ first_node = next(nlist)
+ except StopIteration:
+ return
+ G_to_add_to.add_node(first_node)
+ G_to_add_to.add_edges_from(pairwise(chain((first_node,), nlist)), **attr)
+
+
+def add_cycle(G_to_add_to, nodes_for_cycle, **attr):
+ """Add a cycle to the Graph G_to_add_to.
+
+ Parameters
+ ----------
+ G_to_add_to : graph
+ A NetworkX graph
+ nodes_for_cycle: iterable container
+ A container of nodes. A cycle will be constructed from
+ the nodes (in order) and added to the graph.
+ attr : keyword arguments, optional (default= no attributes)
+ Attributes to add to every edge in cycle.
+
+ See Also
+ --------
+ add_path, add_star
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> nx.add_cycle(G, [0, 1, 2, 3])
+ >>> nx.add_cycle(G, [10, 11, 12], weight=7)
+ """
+ nlist = iter(nodes_for_cycle)
+ try:
+ first_node = next(nlist)
+ except StopIteration:
+ return
+ G_to_add_to.add_node(first_node)
+ G_to_add_to.add_edges_from(
+ pairwise(chain((first_node,), nlist), cyclic=True), **attr
+ )
+
+
+def subgraph(G, nbunch):
+ """Returns the subgraph induced on nodes in nbunch.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ nbunch : list, iterable
+ A container of nodes that will be iterated through once (thus
+ it should be an iterator or be iterable). Each element of the
+ container should be a valid node type: any hashable type except
+ None. If nbunch is None, return all edges data in the graph.
+ Nodes in nbunch that are not in the graph will be (quietly)
+ ignored.
+
+ Notes
+ -----
+ subgraph(G) calls G.subgraph()
+ """
+ return G.subgraph(nbunch)
+
+
+def induced_subgraph(G, nbunch):
+ """Returns a SubGraph view of `G` showing only nodes in nbunch.
+
+ The induced subgraph of a graph on a set of nodes N is the
+ graph with nodes N and edges from G which have both ends in N.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+ nbunch : node, container of nodes or None (for all nodes)
+
+ Returns
+ -------
+ subgraph : SubGraph View
+ A read-only view of the subgraph in `G` induced by the nodes.
+ Changes to the graph `G` will be reflected in the view.
+
+ Notes
+ -----
+ To create a mutable subgraph with its own copies of nodes
+ edges and attributes use `subgraph.copy()` or `Graph(subgraph)`
+
+ For an inplace reduction of a graph to a subgraph you can remove nodes:
+ `G.remove_nodes_from(n in G if n not in set(nbunch))`
+
+ If you are going to compute subgraphs of your subgraphs you could
+ end up with a chain of views that can be very slow once the chain
+ has about 15 views in it. If they are all induced subgraphs, you
+ can short-cut the chain by making them all subgraphs of the original
+ graph. The graph class method `G.subgraph` does this when `G` is
+ a subgraph. In contrast, this function allows you to choose to build
+ chains or not, as you wish. The returned subgraph is a view on `G`.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> H = nx.induced_subgraph(G, [0, 1, 3])
+ >>> list(H.edges)
+ [(0, 1)]
+ >>> list(H.nodes)
+ [0, 1, 3]
+ """
+ induced_nodes = nx.filters.show_nodes(G.nbunch_iter(nbunch))
+ return nx.subgraph_view(G, filter_node=induced_nodes)
+
+
+def edge_subgraph(G, edges):
+ """Returns a view of the subgraph induced by the specified edges.
+
+ The induced subgraph contains each edge in `edges` and each
+ node incident to any of those edges.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+ edges : iterable
+ An iterable of edges. Edges not present in `G` are ignored.
+
+ Returns
+ -------
+ subgraph : SubGraph View
+ A read-only edge-induced subgraph of `G`.
+ Changes to `G` are reflected in the view.
+
+ Notes
+ -----
+ To create a mutable subgraph with its own copies of nodes
+ edges and attributes use `subgraph.copy()` or `Graph(subgraph)`
+
+ If you create a subgraph of a subgraph recursively you can end up
+ with a chain of subgraphs that becomes very slow with about 15
+ nested subgraph views. Luckily the edge_subgraph filter nests
+ nicely so you can use the original graph as G in this function
+ to avoid chains. We do not rule out chains programmatically so
+ that odd cases like an `edge_subgraph` of a `restricted_view`
+ can be created.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(5)
+ >>> H = G.edge_subgraph([(0, 1), (3, 4)])
+ >>> list(H.nodes)
+ [0, 1, 3, 4]
+ >>> list(H.edges)
+ [(0, 1), (3, 4)]
+ """
+ nxf = nx.filters
+ edges = set(edges)
+ nodes = set()
+ for e in edges:
+ nodes.update(e[:2])
+ induced_nodes = nxf.show_nodes(nodes)
+ if G.is_multigraph():
+ if G.is_directed():
+ induced_edges = nxf.show_multidiedges(edges)
+ else:
+ induced_edges = nxf.show_multiedges(edges)
+ else:
+ if G.is_directed():
+ induced_edges = nxf.show_diedges(edges)
+ else:
+ induced_edges = nxf.show_edges(edges)
+ return nx.subgraph_view(G, filter_node=induced_nodes, filter_edge=induced_edges)
+
+
+def restricted_view(G, nodes, edges):
+ """Returns a view of `G` with hidden nodes and edges.
+
+ The resulting subgraph filters out node `nodes` and edges `edges`.
+ Filtered out nodes also filter out any of their edges.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+ nodes : iterable
+ An iterable of nodes. Nodes not present in `G` are ignored.
+ edges : iterable
+ An iterable of edges. Edges not present in `G` are ignored.
+
+ Returns
+ -------
+ subgraph : SubGraph View
+ A read-only restricted view of `G` filtering out nodes and edges.
+ Changes to `G` are reflected in the view.
+
+ Notes
+ -----
+ To create a mutable subgraph with its own copies of nodes
+ edges and attributes use `subgraph.copy()` or `Graph(subgraph)`
+
+ If you create a subgraph of a subgraph recursively you may end up
+ with a chain of subgraph views. Such chains can get quite slow
+ for lengths near 15. To avoid long chains, try to make your subgraph
+ based on the original graph. We do not rule out chains programmatically
+ so that odd cases like an `edge_subgraph` of a `restricted_view`
+ can be created.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(5)
+ >>> H = nx.restricted_view(G, [0], [(1, 2), (3, 4)])
+ >>> list(H.nodes)
+ [1, 2, 3, 4]
+ >>> list(H.edges)
+ [(2, 3)]
+ """
+ nxf = nx.filters
+ hide_nodes = nxf.hide_nodes(nodes)
+ if G.is_multigraph():
+ if G.is_directed():
+ hide_edges = nxf.hide_multidiedges(edges)
+ else:
+ hide_edges = nxf.hide_multiedges(edges)
+ else:
+ if G.is_directed():
+ hide_edges = nxf.hide_diedges(edges)
+ else:
+ hide_edges = nxf.hide_edges(edges)
+ return nx.subgraph_view(G, filter_node=hide_nodes, filter_edge=hide_edges)
+
+
+def to_directed(graph):
+ """Returns a directed view of the graph `graph`.
+
+ Identical to graph.to_directed(as_view=True)
+ Note that graph.to_directed defaults to `as_view=False`
+ while this function always provides a view.
+ """
+ return graph.to_directed(as_view=True)
+
+
+def to_undirected(graph):
+ """Returns an undirected view of the graph `graph`.
+
+ Identical to graph.to_undirected(as_view=True)
+ Note that graph.to_undirected defaults to `as_view=False`
+ while this function always provides a view.
+ """
+ return graph.to_undirected(as_view=True)
+
+
+def create_empty_copy(G, with_data=True):
+ """Returns a copy of the graph G with all of the edges removed.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ with_data : bool (default=True)
+ Propagate Graph and Nodes data to the new graph.
+
+ See Also
+ --------
+ empty_graph
+
+ """
+ H = G.__class__()
+ H.add_nodes_from(G.nodes(data=with_data))
+ if with_data:
+ H.graph.update(G.graph)
+ return H
+
+
+def set_node_attributes(G, values, name=None):
+ """Sets node attributes from a given value or dictionary of values.
+
+ .. Warning:: The call order of arguments `values` and `name`
+ switched between v1.x & v2.x.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ values : scalar value, dict-like
+ What the node attribute should be set to. If `values` is
+ not a dictionary, then it is treated as a single attribute value
+ that is then applied to every node in `G`. This means that if
+ you provide a mutable object, like a list, updates to that object
+ will be reflected in the node attribute for every node.
+ The attribute name will be `name`.
+
+ If `values` is a dict or a dict of dict, it should be keyed
+ by node to either an attribute value or a dict of attribute key/value
+ pairs used to update the node's attributes.
+
+ name : string (optional, default=None)
+ Name of the node attribute to set if values is a scalar.
+
+ Examples
+ --------
+ After computing some property of the nodes of a graph, you may want
+ to assign a node attribute to store the value of that property for
+ each node::
+
+ >>> G = nx.path_graph(3)
+ >>> bb = nx.betweenness_centrality(G)
+ >>> isinstance(bb, dict)
+ True
+ >>> nx.set_node_attributes(G, bb, "betweenness")
+ >>> G.nodes[1]["betweenness"]
+ 1.0
+
+ If you provide a list as the second argument, updates to the list
+ will be reflected in the node attribute for each node::
+
+ >>> G = nx.path_graph(3)
+ >>> labels = []
+ >>> nx.set_node_attributes(G, labels, "labels")
+ >>> labels.append("foo")
+ >>> G.nodes[0]["labels"]
+ ['foo']
+ >>> G.nodes[1]["labels"]
+ ['foo']
+ >>> G.nodes[2]["labels"]
+ ['foo']
+
+ If you provide a dictionary of dictionaries as the second argument,
+ the outer dictionary is assumed to be keyed by node to an inner
+ dictionary of node attributes for that node::
+
+ >>> G = nx.path_graph(3)
+ >>> attrs = {0: {"attr1": 20, "attr2": "nothing"}, 1: {"attr2": 3}}
+ >>> nx.set_node_attributes(G, attrs)
+ >>> G.nodes[0]["attr1"]
+ 20
+ >>> G.nodes[0]["attr2"]
+ 'nothing'
+ >>> G.nodes[1]["attr2"]
+ 3
+ >>> G.nodes[2]
+ {}
+
+ Note that if the dictionary contains nodes that are not in `G`, the
+ values are silently ignored::
+
+ >>> G = nx.Graph()
+ >>> G.add_node(0)
+ >>> nx.set_node_attributes(G, {0: "red", 1: "blue"}, name="color")
+ >>> G.nodes[0]["color"]
+ 'red'
+ >>> 1 in G.nodes
+ False
+
+ """
+ # Set node attributes based on type of `values`
+ if name is not None: # `values` must not be a dict of dict
+ try: # `values` is a dict
+ for n, v in values.items():
+ try:
+ G.nodes[n][name] = values[n]
+ except KeyError:
+ pass
+ except AttributeError: # `values` is a constant
+ for n in G:
+ G.nodes[n][name] = values
+ else: # `values` must be dict of dict
+ for n, d in values.items():
+ try:
+ G.nodes[n].update(d)
+ except KeyError:
+ pass
+ nx._clear_cache(G)
+
+
+def get_node_attributes(G, name, default=None):
+ """Get node attributes from graph
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ name : string
+ Attribute name
+
+ default: object (default=None)
+ Default value of the node attribute if there is no value set for that
+ node in graph. If `None` then nodes without this attribute are not
+ included in the returned dict.
+
+ Returns
+ -------
+ Dictionary of attributes keyed by node.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> G.add_nodes_from([1, 2, 3], color="red")
+ >>> color = nx.get_node_attributes(G, "color")
+ >>> color[1]
+ 'red'
+ >>> G.add_node(4)
+ >>> color = nx.get_node_attributes(G, "color", default="yellow")
+ >>> color[4]
+ 'yellow'
+ """
+ if default is not None:
+ return {n: d.get(name, default) for n, d in G.nodes.items()}
+ return {n: d[name] for n, d in G.nodes.items() if name in d}
+
+
+def remove_node_attributes(G, *attr_names, nbunch=None):
+ """Remove node attributes from all nodes in the graph.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ *attr_names : List of Strings
+ The attribute names to remove from the graph.
+
+ nbunch : List of Nodes
+ Remove the node attributes only from the nodes in this list.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> G.add_nodes_from([1, 2, 3], color="blue")
+ >>> nx.get_node_attributes(G, "color")
+ {1: 'blue', 2: 'blue', 3: 'blue'}
+ >>> nx.remove_node_attributes(G, "color")
+ >>> nx.get_node_attributes(G, "color")
+ {}
+ """
+
+ if nbunch is None:
+ nbunch = G.nodes()
+
+ for attr in attr_names:
+ for n, d in G.nodes(data=True):
+ if n in nbunch:
+ try:
+ del d[attr]
+ except KeyError:
+ pass
+
+
+def set_edge_attributes(G, values, name=None):
+ """Sets edge attributes from a given value or dictionary of values.
+
+ .. Warning:: The call order of arguments `values` and `name`
+ switched between v1.x & v2.x.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ values : scalar value, dict-like
+ What the edge attribute should be set to. If `values` is
+ not a dictionary, then it is treated as a single attribute value
+ that is then applied to every edge in `G`. This means that if
+ you provide a mutable object, like a list, updates to that object
+ will be reflected in the edge attribute for each edge. The attribute
+ name will be `name`.
+
+ If `values` is a dict or a dict of dict, it should be keyed
+ by edge tuple to either an attribute value or a dict of attribute
+ key/value pairs used to update the edge's attributes.
+ For multigraphs, the edge tuples must be of the form ``(u, v, key)``,
+ where `u` and `v` are nodes and `key` is the edge key.
+ For non-multigraphs, the keys must be tuples of the form ``(u, v)``.
+
+ name : string (optional, default=None)
+ Name of the edge attribute to set if values is a scalar.
+
+ Examples
+ --------
+ After computing some property of the edges of a graph, you may want
+ to assign a edge attribute to store the value of that property for
+ each edge::
+
+ >>> G = nx.path_graph(3)
+ >>> bb = nx.edge_betweenness_centrality(G, normalized=False)
+ >>> nx.set_edge_attributes(G, bb, "betweenness")
+ >>> G.edges[1, 2]["betweenness"]
+ 2.0
+
+ If you provide a list as the second argument, updates to the list
+ will be reflected in the edge attribute for each edge::
+
+ >>> labels = []
+ >>> nx.set_edge_attributes(G, labels, "labels")
+ >>> labels.append("foo")
+ >>> G.edges[0, 1]["labels"]
+ ['foo']
+ >>> G.edges[1, 2]["labels"]
+ ['foo']
+
+ If you provide a dictionary of dictionaries as the second argument,
+ the entire dictionary will be used to update edge attributes::
+
+ >>> G = nx.path_graph(3)
+ >>> attrs = {(0, 1): {"attr1": 20, "attr2": "nothing"}, (1, 2): {"attr2": 3}}
+ >>> nx.set_edge_attributes(G, attrs)
+ >>> G[0][1]["attr1"]
+ 20
+ >>> G[0][1]["attr2"]
+ 'nothing'
+ >>> G[1][2]["attr2"]
+ 3
+
+ The attributes of one Graph can be used to set those of another.
+
+ >>> H = nx.path_graph(3)
+ >>> nx.set_edge_attributes(H, G.edges)
+
+ Note that if the dict contains edges that are not in `G`, they are
+ silently ignored::
+
+ >>> G = nx.Graph([(0, 1)])
+ >>> nx.set_edge_attributes(G, {(1, 2): {"weight": 2.0}})
+ >>> (1, 2) in G.edges()
+ False
+
+ For multigraphs, the `values` dict is expected to be keyed by 3-tuples
+ including the edge key::
+
+ >>> MG = nx.MultiGraph()
+ >>> edges = [(0, 1), (0, 1)]
+ >>> MG.add_edges_from(edges) # Returns list of edge keys
+ [0, 1]
+ >>> attributes = {(0, 1, 0): {"cost": 21}, (0, 1, 1): {"cost": 7}}
+ >>> nx.set_edge_attributes(MG, attributes)
+ >>> MG[0][1][0]["cost"]
+ 21
+ >>> MG[0][1][1]["cost"]
+ 7
+
+ If MultiGraph attributes are desired for a Graph, you must convert the 3-tuple
+ multiedge to a 2-tuple edge and the last multiedge's attribute value will
+ overwrite the previous values. Continuing from the previous case we get::
+
+ >>> H = nx.path_graph([0, 1, 2])
+ >>> nx.set_edge_attributes(H, {(u, v): ed for u, v, ed in MG.edges.data()})
+ >>> nx.get_edge_attributes(H, "cost")
+ {(0, 1): 7}
+
+ """
+ if name is not None:
+ # `values` does not contain attribute names
+ try:
+ # if `values` is a dict using `.items()` => {edge: value}
+ if G.is_multigraph():
+ for (u, v, key), value in values.items():
+ try:
+ G._adj[u][v][key][name] = value
+ except KeyError:
+ pass
+ else:
+ for (u, v), value in values.items():
+ try:
+ G._adj[u][v][name] = value
+ except KeyError:
+ pass
+ except AttributeError:
+ # treat `values` as a constant
+ for u, v, data in G.edges(data=True):
+ data[name] = values
+ else:
+ # `values` consists of doct-of-dict {edge: {attr: value}} shape
+ if G.is_multigraph():
+ for (u, v, key), d in values.items():
+ try:
+ G._adj[u][v][key].update(d)
+ except KeyError:
+ pass
+ else:
+ for (u, v), d in values.items():
+ try:
+ G._adj[u][v].update(d)
+ except KeyError:
+ pass
+ nx._clear_cache(G)
+
+
+def get_edge_attributes(G, name, default=None):
+ """Get edge attributes from graph
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ name : string
+ Attribute name
+
+ default: object (default=None)
+ Default value of the edge attribute if there is no value set for that
+ edge in graph. If `None` then edges without this attribute are not
+ included in the returned dict.
+
+ Returns
+ -------
+ Dictionary of attributes keyed by edge. For (di)graphs, the keys are
+ 2-tuples of the form: (u, v). For multi(di)graphs, the keys are 3-tuples of
+ the form: (u, v, key).
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> nx.add_path(G, [1, 2, 3], color="red")
+ >>> color = nx.get_edge_attributes(G, "color")
+ >>> color[(1, 2)]
+ 'red'
+ >>> G.add_edge(3, 4)
+ >>> color = nx.get_edge_attributes(G, "color", default="yellow")
+ >>> color[(3, 4)]
+ 'yellow'
+ """
+ if G.is_multigraph():
+ edges = G.edges(keys=True, data=True)
+ else:
+ edges = G.edges(data=True)
+ if default is not None:
+ return {x[:-1]: x[-1].get(name, default) for x in edges}
+ return {x[:-1]: x[-1][name] for x in edges if name in x[-1]}
+
+
+def remove_edge_attributes(G, *attr_names, ebunch=None):
+ """Remove edge attributes from all edges in the graph.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ *attr_names : List of Strings
+ The attribute names to remove from the graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3)
+ >>> nx.set_edge_attributes(G, {(u, v): u + v for u, v in G.edges()}, name="weight")
+ >>> nx.get_edge_attributes(G, "weight")
+ {(0, 1): 1, (1, 2): 3}
+ >>> remove_edge_attributes(G, "weight")
+ >>> nx.get_edge_attributes(G, "weight")
+ {}
+ """
+ if ebunch is None:
+ ebunch = G.edges(keys=True) if G.is_multigraph() else G.edges()
+
+ for attr in attr_names:
+ edges = (
+ G.edges(keys=True, data=True) if G.is_multigraph() else G.edges(data=True)
+ )
+ for *e, d in edges:
+ if tuple(e) in ebunch:
+ try:
+ del d[attr]
+ except KeyError:
+ pass
+
+
+def all_neighbors(graph, node):
+ """Returns all of the neighbors of a node in the graph.
+
+ If the graph is directed returns predecessors as well as successors.
+
+ Parameters
+ ----------
+ graph : NetworkX graph
+ Graph to find neighbors.
+
+ node : node
+ The node whose neighbors will be returned.
+
+ Returns
+ -------
+ neighbors : iterator
+ Iterator of neighbors
+ """
+ if graph.is_directed():
+ values = chain(graph.predecessors(node), graph.successors(node))
+ else:
+ values = graph.neighbors(node)
+ return values
+
+
+def non_neighbors(graph, node):
+ """Returns the non-neighbors of the node in the graph.
+
+ Parameters
+ ----------
+ graph : NetworkX graph
+ Graph to find neighbors.
+
+ node : node
+ The node whose neighbors will be returned.
+
+ Returns
+ -------
+ non_neighbors : set
+ Set of nodes in the graph that are not neighbors of the node.
+ """
+ return graph._adj.keys() - graph._adj[node].keys() - {node}
+
+
+def non_edges(graph):
+ """Returns the nonexistent edges in the graph.
+
+ Parameters
+ ----------
+ graph : NetworkX graph.
+ Graph to find nonexistent edges.
+
+ Returns
+ -------
+ non_edges : iterator
+ Iterator of edges that are not in the graph.
+ """
+ if graph.is_directed():
+ for u in graph:
+ for v in non_neighbors(graph, u):
+ yield (u, v)
+ else:
+ nodes = set(graph)
+ while nodes:
+ u = nodes.pop()
+ for v in nodes - set(graph[u]):
+ yield (u, v)
+
+
+@not_implemented_for("directed")
+def common_neighbors(G, u, v):
+ """Returns the common neighbors of two nodes in a graph.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX undirected graph.
+
+ u, v : nodes
+ Nodes in the graph.
+
+ Returns
+ -------
+ cnbors : set
+ Set of common neighbors of u and v in the graph.
+
+ Raises
+ ------
+ NetworkXError
+ If u or v is not a node in the graph.
+
+ Examples
+ --------
+ >>> G = nx.complete_graph(5)
+ >>> sorted(nx.common_neighbors(G, 0, 1))
+ [2, 3, 4]
+ """
+ if u not in G:
+ raise nx.NetworkXError("u is not in the graph.")
+ if v not in G:
+ raise nx.NetworkXError("v is not in the graph.")
+
+ return G._adj[u].keys() & G._adj[v].keys() - {u, v}
+
+
+def is_weighted(G, edge=None, weight="weight"):
+ """Returns True if `G` has weighted edges.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph.
+
+ edge : tuple, optional
+ A 2-tuple specifying the only edge in `G` that will be tested. If
+ None, then every edge in `G` is tested.
+
+ weight: string, optional
+ The attribute name used to query for edge weights.
+
+ Returns
+ -------
+ bool
+ A boolean signifying if `G`, or the specified edge, is weighted.
+
+ Raises
+ ------
+ NetworkXError
+ If the specified edge does not exist.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.is_weighted(G)
+ False
+ >>> nx.is_weighted(G, (2, 3))
+ False
+
+ >>> G = nx.DiGraph()
+ >>> G.add_edge(1, 2, weight=1)
+ >>> nx.is_weighted(G)
+ True
+
+ """
+ if edge is not None:
+ data = G.get_edge_data(*edge)
+ if data is None:
+ msg = f"Edge {edge!r} does not exist."
+ raise nx.NetworkXError(msg)
+ return weight in data
+
+ if is_empty(G):
+ # Special handling required since: all([]) == True
+ return False
+
+ return all(weight in data for u, v, data in G.edges(data=True))
+
+
+@nx._dispatchable(edge_attrs="weight")
+def is_negatively_weighted(G, edge=None, weight="weight"):
+ """Returns True if `G` has negatively weighted edges.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph.
+
+ edge : tuple, optional
+ A 2-tuple specifying the only edge in `G` that will be tested. If
+ None, then every edge in `G` is tested.
+
+ weight: string, optional
+ The attribute name used to query for edge weights.
+
+ Returns
+ -------
+ bool
+ A boolean signifying if `G`, or the specified edge, is negatively
+ weighted.
+
+ Raises
+ ------
+ NetworkXError
+ If the specified edge does not exist.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> G.add_edges_from([(1, 3), (2, 4), (2, 6)])
+ >>> G.add_edge(1, 2, weight=4)
+ >>> nx.is_negatively_weighted(G, (1, 2))
+ False
+ >>> G[2][4]["weight"] = -2
+ >>> nx.is_negatively_weighted(G)
+ True
+ >>> G = nx.DiGraph()
+ >>> edges = [("0", "3", 3), ("0", "1", -5), ("1", "0", -2)]
+ >>> G.add_weighted_edges_from(edges)
+ >>> nx.is_negatively_weighted(G)
+ True
+
+ """
+ if edge is not None:
+ data = G.get_edge_data(*edge)
+ if data is None:
+ msg = f"Edge {edge!r} does not exist."
+ raise nx.NetworkXError(msg)
+ return weight in data and data[weight] < 0
+
+ return any(weight in data and data[weight] < 0 for u, v, data in G.edges(data=True))
+
+
+def is_empty(G):
+ """Returns True if `G` has no edges.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph.
+
+ Returns
+ -------
+ bool
+ True if `G` has no edges, and False otherwise.
+
+ Notes
+ -----
+ An empty graph can have nodes but not edges. The empty graph with zero
+ nodes is known as the null graph. This is an $O(n)$ operation where n
+ is the number of nodes in the graph.
+
+ """
+ return not any(G._adj.values())
+
+
+def nodes_with_selfloops(G):
+ """Returns an iterator over nodes with self loops.
+
+ A node with a self loop has an edge with both ends adjacent
+ to that node.
+
+ Returns
+ -------
+ nodelist : iterator
+ A iterator over nodes with self loops.
+
+ See Also
+ --------
+ selfloop_edges, number_of_selfloops
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_edge(1, 1)
+ >>> G.add_edge(1, 2)
+ >>> list(nx.nodes_with_selfloops(G))
+ [1]
+
+ """
+ return (n for n, nbrs in G._adj.items() if n in nbrs)
+
+
+def selfloop_edges(G, data=False, keys=False, default=None):
+ """Returns an iterator over selfloop edges.
+
+ A selfloop edge has the same node at both ends.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph.
+ data : string or bool, optional (default=False)
+ Return selfloop edges as two tuples (u, v) (data=False)
+ or three-tuples (u, v, datadict) (data=True)
+ or three-tuples (u, v, datavalue) (data='attrname')
+ keys : bool, optional (default=False)
+ If True, return edge keys with each edge.
+ default : value, optional (default=None)
+ Value used for edges that don't have the requested attribute.
+ Only relevant if data is not True or False.
+
+ Returns
+ -------
+ edgeiter : iterator over edge tuples
+ An iterator over all selfloop edges.
+
+ See Also
+ --------
+ nodes_with_selfloops, number_of_selfloops
+
+ Examples
+ --------
+ >>> G = nx.MultiGraph() # or Graph, DiGraph, MultiDiGraph, etc
+ >>> ekey = G.add_edge(1, 1)
+ >>> ekey = G.add_edge(1, 2)
+ >>> list(nx.selfloop_edges(G))
+ [(1, 1)]
+ >>> list(nx.selfloop_edges(G, data=True))
+ [(1, 1, {})]
+ >>> list(nx.selfloop_edges(G, keys=True))
+ [(1, 1, 0)]
+ >>> list(nx.selfloop_edges(G, keys=True, data=True))
+ [(1, 1, 0, {})]
+ """
+ if data is True:
+ if G.is_multigraph():
+ if keys is True:
+ return (
+ (n, n, k, d)
+ for n, nbrs in G._adj.items()
+ if n in nbrs
+ for k, d in nbrs[n].items()
+ )
+ else:
+ return (
+ (n, n, d)
+ for n, nbrs in G._adj.items()
+ if n in nbrs
+ for d in nbrs[n].values()
+ )
+ else:
+ return ((n, n, nbrs[n]) for n, nbrs in G._adj.items() if n in nbrs)
+ elif data is not False:
+ if G.is_multigraph():
+ if keys is True:
+ return (
+ (n, n, k, d.get(data, default))
+ for n, nbrs in G._adj.items()
+ if n in nbrs
+ for k, d in nbrs[n].items()
+ )
+ else:
+ return (
+ (n, n, d.get(data, default))
+ for n, nbrs in G._adj.items()
+ if n in nbrs
+ for d in nbrs[n].values()
+ )
+ else:
+ return (
+ (n, n, nbrs[n].get(data, default))
+ for n, nbrs in G._adj.items()
+ if n in nbrs
+ )
+ else:
+ if G.is_multigraph():
+ if keys is True:
+ return (
+ (n, n, k)
+ for n, nbrs in G._adj.items()
+ if n in nbrs
+ for k in nbrs[n]
+ )
+ else:
+ return (
+ (n, n)
+ for n, nbrs in G._adj.items()
+ if n in nbrs
+ for i in range(len(nbrs[n])) # for easy edge removal (#4068)
+ )
+ else:
+ return ((n, n) for n, nbrs in G._adj.items() if n in nbrs)
+
+
+def number_of_selfloops(G):
+ """Returns the number of selfloop edges.
+
+ A selfloop edge has the same node at both ends.
+
+ Returns
+ -------
+ nloops : int
+ The number of selfloops.
+
+ See Also
+ --------
+ nodes_with_selfloops, selfloop_edges
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_edge(1, 1)
+ >>> G.add_edge(1, 2)
+ >>> nx.number_of_selfloops(G)
+ 1
+ """
+ return sum(1 for _ in nx.selfloop_edges(G))
+
+
+def is_path(G, path):
+ """Returns whether or not the specified path exists.
+
+ For it to return True, every node on the path must exist and
+ each consecutive pair must be connected via one or more edges.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph.
+
+ path : list
+ A list of nodes which defines the path to traverse
+
+ Returns
+ -------
+ bool
+ True if `path` is a valid path in `G`
+
+ """
+ try:
+ return all(nbr in G._adj[node] for node, nbr in nx.utils.pairwise(path))
+ except (KeyError, TypeError):
+ return False
+
+
+def path_weight(G, path, weight):
+ """Returns total cost associated with specified path and weight
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph.
+
+ path: list
+ A list of node labels which defines the path to traverse
+
+ weight: string
+ A string indicating which edge attribute to use for path cost
+
+ Returns
+ -------
+ cost: int or float
+ An integer or a float representing the total cost with respect to the
+ specified weight of the specified path
+
+ Raises
+ ------
+ NetworkXNoPath
+ If the specified edge does not exist.
+ """
+ multigraph = G.is_multigraph()
+ cost = 0
+
+ if not nx.is_path(G, path):
+ raise nx.NetworkXNoPath("path does not exist")
+ for node, nbr in nx.utils.pairwise(path):
+ if multigraph:
+ cost += min(v[weight] for v in G._adj[node][nbr].values())
+ else:
+ cost += G._adj[node][nbr][weight]
+ return cost
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/graph.py b/.venv/lib/python3.12/site-packages/networkx/classes/graph.py
new file mode 100644
index 00000000..6828705d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/graph.py
@@ -0,0 +1,2058 @@
+"""Base class for undirected graphs.
+
+The Graph class allows any hashable object as a node
+and can associate key/value attribute pairs with each undirected edge.
+
+Self-loops are allowed but multiple edges are not (see MultiGraph).
+
+For directed graphs see DiGraph and MultiDiGraph.
+"""
+
+from copy import deepcopy
+from functools import cached_property
+
+import networkx as nx
+from networkx import convert
+from networkx.classes.coreviews import AdjacencyView
+from networkx.classes.reportviews import DegreeView, EdgeView, NodeView
+from networkx.exception import NetworkXError
+
+__all__ = ["Graph"]
+
+
+class _CachedPropertyResetterAdj:
+ """Data Descriptor class for _adj that resets ``adj`` cached_property when needed
+
+ This assumes that the ``cached_property`` ``G.adj`` should be reset whenever
+ ``G._adj`` is set to a new value.
+
+ This object sits on a class and ensures that any instance of that
+ class clears its cached property "adj" whenever the underlying
+ instance attribute "_adj" is set to a new object. It only affects
+ the set process of the obj._adj attribute. All get/del operations
+ act as they normally would.
+
+ For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html
+ """
+
+ def __set__(self, obj, value):
+ od = obj.__dict__
+ od["_adj"] = value
+ # reset cached properties
+ props = ["adj", "edges", "degree"]
+ for prop in props:
+ if prop in od:
+ del od[prop]
+
+
+class _CachedPropertyResetterNode:
+ """Data Descriptor class for _node that resets ``nodes`` cached_property when needed
+
+ This assumes that the ``cached_property`` ``G.node`` should be reset whenever
+ ``G._node`` is set to a new value.
+
+ This object sits on a class and ensures that any instance of that
+ class clears its cached property "nodes" whenever the underlying
+ instance attribute "_node" is set to a new object. It only affects
+ the set process of the obj._adj attribute. All get/del operations
+ act as they normally would.
+
+ For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html
+ """
+
+ def __set__(self, obj, value):
+ od = obj.__dict__
+ od["_node"] = value
+ # reset cached properties
+ if "nodes" in od:
+ del od["nodes"]
+
+
+class Graph:
+ """
+ Base class for undirected graphs.
+
+ A Graph stores nodes and edges with optional data, or attributes.
+
+ Graphs hold undirected edges. Self loops are allowed but multiple
+ (parallel) edges are not.
+
+ Nodes can be arbitrary (hashable) Python objects with optional
+ key/value attributes, except that `None` is not allowed as a node.
+
+ Edges are represented as links between nodes with optional
+ key/value attributes.
+
+ Parameters
+ ----------
+ incoming_graph_data : input graph (optional, default: None)
+ Data to initialize graph. If None (default) an empty
+ graph is created. The data can be any format that is supported
+ by the to_networkx_graph() function, currently including edge list,
+ dict of dicts, dict of lists, NetworkX graph, 2D NumPy array, SciPy
+ sparse matrix, or PyGraphviz graph.
+
+ attr : keyword arguments, optional (default= no attributes)
+ Attributes to add to graph as key=value pairs.
+
+ See Also
+ --------
+ DiGraph
+ MultiGraph
+ MultiDiGraph
+
+ Examples
+ --------
+ Create an empty graph structure (a "null graph") with no nodes and
+ no edges.
+
+ >>> G = nx.Graph()
+
+ G can be grown in several ways.
+
+ **Nodes:**
+
+ Add one node at a time:
+
+ >>> G.add_node(1)
+
+ Add the nodes from any container (a list, dict, set or
+ even the lines from a file or the nodes from another graph).
+
+ >>> G.add_nodes_from([2, 3])
+ >>> G.add_nodes_from(range(100, 110))
+ >>> H = nx.path_graph(10)
+ >>> G.add_nodes_from(H)
+
+ In addition to strings and integers any hashable Python object
+ (except None) can represent a node, e.g. a customized node object,
+ or even another Graph.
+
+ >>> G.add_node(H)
+
+ **Edges:**
+
+ G can also be grown by adding edges.
+
+ Add one edge,
+
+ >>> G.add_edge(1, 2)
+
+ a list of edges,
+
+ >>> G.add_edges_from([(1, 2), (1, 3)])
+
+ or a collection of edges,
+
+ >>> G.add_edges_from(H.edges)
+
+ If some edges connect nodes not yet in the graph, the nodes
+ are added automatically. There are no errors when adding
+ nodes or edges that already exist.
+
+ **Attributes:**
+
+ Each graph, node, and edge can hold key/value attribute pairs
+ in an associated attribute dictionary (the keys must be hashable).
+ By default these are empty, but can be added or changed using
+ add_edge, add_node or direct manipulation of the attribute
+ dictionaries named graph, node and edge respectively.
+
+ >>> G = nx.Graph(day="Friday")
+ >>> G.graph
+ {'day': 'Friday'}
+
+ Add node attributes using add_node(), add_nodes_from() or G.nodes
+
+ >>> G.add_node(1, time="5pm")
+ >>> G.add_nodes_from([3], time="2pm")
+ >>> G.nodes[1]
+ {'time': '5pm'}
+ >>> G.nodes[1]["room"] = 714 # node must exist already to use G.nodes
+ >>> del G.nodes[1]["room"] # remove attribute
+ >>> list(G.nodes(data=True))
+ [(1, {'time': '5pm'}), (3, {'time': '2pm'})]
+
+ Add edge attributes using add_edge(), add_edges_from(), subscript
+ notation, or G.edges.
+
+ >>> G.add_edge(1, 2, weight=4.7)
+ >>> G.add_edges_from([(3, 4), (4, 5)], color="red")
+ >>> G.add_edges_from([(1, 2, {"color": "blue"}), (2, 3, {"weight": 8})])
+ >>> G[1][2]["weight"] = 4.7
+ >>> G.edges[1, 2]["weight"] = 4
+
+ Warning: we protect the graph data structure by making `G.edges` a
+ read-only dict-like structure. However, you can assign to attributes
+ in e.g. `G.edges[1, 2]`. Thus, use 2 sets of brackets to add/change
+ data attributes: `G.edges[1, 2]['weight'] = 4`
+ (For multigraphs: `MG.edges[u, v, key][name] = value`).
+
+ **Shortcuts:**
+
+ Many common graph features allow python syntax to speed reporting.
+
+ >>> 1 in G # check if node in graph
+ True
+ >>> [n for n in G if n < 3] # iterate through nodes
+ [1, 2]
+ >>> len(G) # number of nodes in graph
+ 5
+
+ Often the best way to traverse all edges of a graph is via the neighbors.
+ The neighbors are reported as an adjacency-dict `G.adj` or `G.adjacency()`
+
+ >>> for n, nbrsdict in G.adjacency():
+ ... for nbr, eattr in nbrsdict.items():
+ ... if "weight" in eattr:
+ ... # Do something useful with the edges
+ ... pass
+
+ But the edges() method is often more convenient:
+
+ >>> for u, v, weight in G.edges.data("weight"):
+ ... if weight is not None:
+ ... # Do something useful with the edges
+ ... pass
+
+ **Reporting:**
+
+ Simple graph information is obtained using object-attributes and methods.
+ Reporting typically provides views instead of containers to reduce memory
+ usage. The views update as the graph is updated similarly to dict-views.
+ The objects `nodes`, `edges` and `adj` provide access to data attributes
+ via lookup (e.g. `nodes[n]`, `edges[u, v]`, `adj[u][v]`) and iteration
+ (e.g. `nodes.items()`, `nodes.data('color')`,
+ `nodes.data('color', default='blue')` and similarly for `edges`)
+ Views exist for `nodes`, `edges`, `neighbors()`/`adj` and `degree`.
+
+ For details on these and other miscellaneous methods, see below.
+
+ **Subclasses (Advanced):**
+
+ The Graph class uses a dict-of-dict-of-dict data structure.
+ The outer dict (node_dict) holds adjacency information keyed by node.
+ The next dict (adjlist_dict) represents the adjacency information and holds
+ edge data keyed by neighbor. The inner dict (edge_attr_dict) represents
+ the edge data and holds edge attribute values keyed by attribute names.
+
+ Each of these three dicts can be replaced in a subclass by a user defined
+ dict-like object. In general, the dict-like features should be
+ maintained but extra features can be added. To replace one of the
+ dicts create a new graph class by changing the class(!) variable
+ holding the factory for that dict-like structure.
+
+ node_dict_factory : function, (default: dict)
+ Factory function to be used to create the dict containing node
+ attributes, keyed by node id.
+ It should require no arguments and return a dict-like object
+
+ node_attr_dict_factory: function, (default: dict)
+ Factory function to be used to create the node attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object
+
+ adjlist_outer_dict_factory : function, (default: dict)
+ Factory function to be used to create the outer-most dict
+ in the data structure that holds adjacency info keyed by node.
+ It should require no arguments and return a dict-like object.
+
+ adjlist_inner_dict_factory : function, (default: dict)
+ Factory function to be used to create the adjacency list
+ dict which holds edge data keyed by neighbor.
+ It should require no arguments and return a dict-like object
+
+ edge_attr_dict_factory : function, (default: dict)
+ Factory function to be used to create the edge attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object.
+
+ graph_attr_dict_factory : function, (default: dict)
+ Factory function to be used to create the graph attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object.
+
+ Typically, if your extension doesn't impact the data structure all
+ methods will inherit without issue except: `to_directed/to_undirected`.
+ By default these methods create a DiGraph/Graph class and you probably
+ want them to create your extension of a DiGraph/Graph. To facilitate
+ this we define two class variables that you can set in your subclass.
+
+ to_directed_class : callable, (default: DiGraph or MultiDiGraph)
+ Class to create a new graph structure in the `to_directed` method.
+ If `None`, a NetworkX class (DiGraph or MultiDiGraph) is used.
+
+ to_undirected_class : callable, (default: Graph or MultiGraph)
+ Class to create a new graph structure in the `to_undirected` method.
+ If `None`, a NetworkX class (Graph or MultiGraph) is used.
+
+ **Subclassing Example**
+
+ Create a low memory graph class that effectively disallows edge
+ attributes by using a single attribute dict for all edges.
+ This reduces the memory used, but you lose edge attributes.
+
+ >>> class ThinGraph(nx.Graph):
+ ... all_edge_dict = {"weight": 1}
+ ...
+ ... def single_edge_dict(self):
+ ... return self.all_edge_dict
+ ...
+ ... edge_attr_dict_factory = single_edge_dict
+ >>> G = ThinGraph()
+ >>> G.add_edge(2, 1)
+ >>> G[2][1]
+ {'weight': 1}
+ >>> G.add_edge(2, 2)
+ >>> G[2][1] is G[2][2]
+ True
+ """
+
+ __networkx_backend__ = "networkx"
+
+ _adj = _CachedPropertyResetterAdj()
+ _node = _CachedPropertyResetterNode()
+
+ node_dict_factory = dict
+ node_attr_dict_factory = dict
+ adjlist_outer_dict_factory = dict
+ adjlist_inner_dict_factory = dict
+ edge_attr_dict_factory = dict
+ graph_attr_dict_factory = dict
+
+ def to_directed_class(self):
+ """Returns the class to use for empty directed copies.
+
+ If you subclass the base classes, use this to designate
+ what directed class to use for `to_directed()` copies.
+ """
+ return nx.DiGraph
+
+ def to_undirected_class(self):
+ """Returns the class to use for empty undirected copies.
+
+ If you subclass the base classes, use this to designate
+ what directed class to use for `to_directed()` copies.
+ """
+ return Graph
+
+ def __init__(self, incoming_graph_data=None, **attr):
+ """Initialize a graph with edges, name, or graph attributes.
+
+ Parameters
+ ----------
+ incoming_graph_data : input graph (optional, default: None)
+ Data to initialize graph. If None (default) an empty
+ graph is created. The data can be an edge list, or any
+ NetworkX graph object. If the corresponding optional Python
+ packages are installed the data can also be a 2D NumPy array, a
+ SciPy sparse array, or a PyGraphviz graph.
+
+ attr : keyword arguments, optional (default= no attributes)
+ Attributes to add to graph as key=value pairs.
+
+ See Also
+ --------
+ convert
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G = nx.Graph(name="my graph")
+ >>> e = [(1, 2), (2, 3), (3, 4)] # list of edges
+ >>> G = nx.Graph(e)
+
+ Arbitrary graph attribute pairs (key=value) may be assigned
+
+ >>> G = nx.Graph(e, day="Friday")
+ >>> G.graph
+ {'day': 'Friday'}
+
+ """
+ self.graph = self.graph_attr_dict_factory() # dictionary for graph attributes
+ self._node = self.node_dict_factory() # empty node attribute dict
+ self._adj = self.adjlist_outer_dict_factory() # empty adjacency dict
+ self.__networkx_cache__ = {}
+ # attempt to load graph with data
+ if incoming_graph_data is not None:
+ convert.to_networkx_graph(incoming_graph_data, create_using=self)
+ # load graph attributes (must be after convert)
+ self.graph.update(attr)
+
+ @cached_property
+ def adj(self):
+ """Graph adjacency object holding the neighbors of each node.
+
+ This object is a read-only dict-like structure with node keys
+ and neighbor-dict values. The neighbor-dict is keyed by neighbor
+ to the edge-data-dict. So `G.adj[3][2]['color'] = 'blue'` sets
+ the color of the edge `(3, 2)` to `"blue"`.
+
+ Iterating over G.adj behaves like a dict. Useful idioms include
+ `for nbr, datadict in G.adj[n].items():`.
+
+ The neighbor information is also provided by subscripting the graph.
+ So `for nbr, foovalue in G[node].data('foo', default=1):` works.
+
+ For directed graphs, `G.adj` holds outgoing (successor) info.
+ """
+ return AdjacencyView(self._adj)
+
+ @property
+ def name(self):
+ """String identifier of the graph.
+
+ This graph attribute appears in the attribute dict G.graph
+ keyed by the string `"name"`. as well as an attribute (technically
+ a property) `G.name`. This is entirely user controlled.
+ """
+ return self.graph.get("name", "")
+
+ @name.setter
+ def name(self, s):
+ self.graph["name"] = s
+ nx._clear_cache(self)
+
+ def __str__(self):
+ """Returns a short summary of the graph.
+
+ Returns
+ -------
+ info : string
+ Graph information including the graph name (if any), graph type, and the
+ number of nodes and edges.
+
+ Examples
+ --------
+ >>> G = nx.Graph(name="foo")
+ >>> str(G)
+ "Graph named 'foo' with 0 nodes and 0 edges"
+
+ >>> G = nx.path_graph(3)
+ >>> str(G)
+ 'Graph with 3 nodes and 2 edges'
+
+ """
+ return "".join(
+ [
+ type(self).__name__,
+ f" named {self.name!r}" if self.name else "",
+ f" with {self.number_of_nodes()} nodes and {self.number_of_edges()} edges",
+ ]
+ )
+
+ def __iter__(self):
+ """Iterate over the nodes. Use: 'for n in G'.
+
+ Returns
+ -------
+ niter : iterator
+ An iterator over all nodes in the graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> [n for n in G]
+ [0, 1, 2, 3]
+ >>> list(G)
+ [0, 1, 2, 3]
+ """
+ return iter(self._node)
+
+ def __contains__(self, n):
+ """Returns True if n is a node, False otherwise. Use: 'n in G'.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> 1 in G
+ True
+ """
+ try:
+ return n in self._node
+ except TypeError:
+ return False
+
+ def __len__(self):
+ """Returns the number of nodes in the graph. Use: 'len(G)'.
+
+ Returns
+ -------
+ nnodes : int
+ The number of nodes in the graph.
+
+ See Also
+ --------
+ number_of_nodes: identical method
+ order: identical method
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> len(G)
+ 4
+
+ """
+ return len(self._node)
+
+ def __getitem__(self, n):
+ """Returns a dict of neighbors of node n. Use: 'G[n]'.
+
+ Parameters
+ ----------
+ n : node
+ A node in the graph.
+
+ Returns
+ -------
+ adj_dict : dictionary
+ The adjacency dictionary for nodes connected to n.
+
+ Notes
+ -----
+ G[n] is the same as G.adj[n] and similar to G.neighbors(n)
+ (which is an iterator over G.adj[n])
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G[0]
+ AtlasView({1: {}})
+ """
+ return self.adj[n]
+
+ def add_node(self, node_for_adding, **attr):
+ """Add a single node `node_for_adding` and update node attributes.
+
+ Parameters
+ ----------
+ node_for_adding : node
+ A node can be any hashable Python object except None.
+ attr : keyword arguments, optional
+ Set or change node attributes using key=value.
+
+ See Also
+ --------
+ add_nodes_from
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_node(1)
+ >>> G.add_node("Hello")
+ >>> K3 = nx.Graph([(0, 1), (1, 2), (2, 0)])
+ >>> G.add_node(K3)
+ >>> G.number_of_nodes()
+ 3
+
+ Use keywords set/change node attributes:
+
+ >>> G.add_node(1, size=10)
+ >>> G.add_node(3, weight=0.4, UTM=("13S", 382871, 3972649))
+
+ Notes
+ -----
+ A hashable object is one that can be used as a key in a Python
+ dictionary. This includes strings, numbers, tuples of strings
+ and numbers, etc.
+
+ On many platforms hashable items also include mutables such as
+ NetworkX Graphs, though one should be careful that the hash
+ doesn't change on mutables.
+ """
+ if node_for_adding not in self._node:
+ if node_for_adding is None:
+ raise ValueError("None cannot be a node")
+ self._adj[node_for_adding] = self.adjlist_inner_dict_factory()
+ attr_dict = self._node[node_for_adding] = self.node_attr_dict_factory()
+ attr_dict.update(attr)
+ else: # update attr even if node already exists
+ self._node[node_for_adding].update(attr)
+ nx._clear_cache(self)
+
+ def add_nodes_from(self, nodes_for_adding, **attr):
+ """Add multiple nodes.
+
+ Parameters
+ ----------
+ nodes_for_adding : iterable container
+ A container of nodes (list, dict, set, etc.).
+ OR
+ A container of (node, attribute dict) tuples.
+ Node attributes are updated using the attribute dict.
+ attr : keyword arguments, optional (default= no attributes)
+ Update attributes for all nodes in nodes.
+ Node attributes specified in nodes as a tuple take
+ precedence over attributes specified via keyword arguments.
+
+ See Also
+ --------
+ add_node
+
+ Notes
+ -----
+ When adding nodes from an iterator over the graph you are changing,
+ a `RuntimeError` can be raised with message:
+ `RuntimeError: dictionary changed size during iteration`. This
+ happens when the graph's underlying dictionary is modified during
+ iteration. To avoid this error, evaluate the iterator into a separate
+ object, e.g. by using `list(iterator_of_nodes)`, and pass this
+ object to `G.add_nodes_from`.
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_nodes_from("Hello")
+ >>> K3 = nx.Graph([(0, 1), (1, 2), (2, 0)])
+ >>> G.add_nodes_from(K3)
+ >>> sorted(G.nodes(), key=str)
+ [0, 1, 2, 'H', 'e', 'l', 'o']
+
+ Use keywords to update specific node attributes for every node.
+
+ >>> G.add_nodes_from([1, 2], size=10)
+ >>> G.add_nodes_from([3, 4], weight=0.4)
+
+ Use (node, attrdict) tuples to update attributes for specific nodes.
+
+ >>> G.add_nodes_from([(1, dict(size=11)), (2, {"color": "blue"})])
+ >>> G.nodes[1]["size"]
+ 11
+ >>> H = nx.Graph()
+ >>> H.add_nodes_from(G.nodes(data=True))
+ >>> H.nodes[1]["size"]
+ 11
+
+ Evaluate an iterator over a graph if using it to modify the same graph
+
+ >>> G = nx.Graph([(0, 1), (1, 2), (3, 4)])
+ >>> # wrong way - will raise RuntimeError
+ >>> # G.add_nodes_from(n + 1 for n in G.nodes)
+ >>> # correct way
+ >>> G.add_nodes_from(list(n + 1 for n in G.nodes))
+ """
+ for n in nodes_for_adding:
+ try:
+ newnode = n not in self._node
+ newdict = attr
+ except TypeError:
+ n, ndict = n
+ newnode = n not in self._node
+ newdict = attr.copy()
+ newdict.update(ndict)
+ if newnode:
+ if n is None:
+ raise ValueError("None cannot be a node")
+ self._adj[n] = self.adjlist_inner_dict_factory()
+ self._node[n] = self.node_attr_dict_factory()
+ self._node[n].update(newdict)
+ nx._clear_cache(self)
+
+ def remove_node(self, n):
+ """Remove node n.
+
+ Removes the node n and all adjacent edges.
+ Attempting to remove a nonexistent node will raise an exception.
+
+ Parameters
+ ----------
+ n : node
+ A node in the graph
+
+ Raises
+ ------
+ NetworkXError
+ If n is not in the graph.
+
+ See Also
+ --------
+ remove_nodes_from
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> list(G.edges)
+ [(0, 1), (1, 2)]
+ >>> G.remove_node(1)
+ >>> list(G.edges)
+ []
+
+ """
+ adj = self._adj
+ try:
+ nbrs = list(adj[n]) # list handles self-loops (allows mutation)
+ del self._node[n]
+ except KeyError as err: # NetworkXError if n not in self
+ raise NetworkXError(f"The node {n} is not in the graph.") from err
+ for u in nbrs:
+ del adj[u][n] # remove all edges n-u in graph
+ del adj[n] # now remove node
+ nx._clear_cache(self)
+
+ def remove_nodes_from(self, nodes):
+ """Remove multiple nodes.
+
+ Parameters
+ ----------
+ nodes : iterable container
+ A container of nodes (list, dict, set, etc.). If a node
+ in the container is not in the graph it is silently
+ ignored.
+
+ See Also
+ --------
+ remove_node
+
+ Notes
+ -----
+ When removing nodes from an iterator over the graph you are changing,
+ a `RuntimeError` will be raised with message:
+ `RuntimeError: dictionary changed size during iteration`. This
+ happens when the graph's underlying dictionary is modified during
+ iteration. To avoid this error, evaluate the iterator into a separate
+ object, e.g. by using `list(iterator_of_nodes)`, and pass this
+ object to `G.remove_nodes_from`.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> e = list(G.nodes)
+ >>> e
+ [0, 1, 2]
+ >>> G.remove_nodes_from(e)
+ >>> list(G.nodes)
+ []
+
+ Evaluate an iterator over a graph if using it to modify the same graph
+
+ >>> G = nx.Graph([(0, 1), (1, 2), (3, 4)])
+ >>> # this command will fail, as the graph's dict is modified during iteration
+ >>> # G.remove_nodes_from(n for n in G.nodes if n < 2)
+ >>> # this command will work, since the dictionary underlying graph is not modified
+ >>> G.remove_nodes_from(list(n for n in G.nodes if n < 2))
+ """
+ adj = self._adj
+ for n in nodes:
+ try:
+ del self._node[n]
+ for u in list(adj[n]): # list handles self-loops
+ del adj[u][n] # (allows mutation of dict in loop)
+ del adj[n]
+ except KeyError:
+ pass
+ nx._clear_cache(self)
+
+ @cached_property
+ def nodes(self):
+ """A NodeView of the Graph as G.nodes or G.nodes().
+
+ Can be used as `G.nodes` for data lookup and for set-like operations.
+ Can also be used as `G.nodes(data='color', default=None)` to return a
+ NodeDataView which reports specific node data but no set operations.
+ It presents a dict-like interface as well with `G.nodes.items()`
+ iterating over `(node, nodedata)` 2-tuples and `G.nodes[3]['foo']`
+ providing the value of the `foo` attribute for node `3`. In addition,
+ a view `G.nodes.data('foo')` provides a dict-like interface to the
+ `foo` attribute of each node. `G.nodes.data('foo', default=1)`
+ provides a default for nodes that do not have attribute `foo`.
+
+ Parameters
+ ----------
+ data : string or bool, optional (default=False)
+ The node attribute returned in 2-tuple (n, ddict[data]).
+ If True, return entire node attribute dict as (n, ddict).
+ If False, return just the nodes n.
+
+ default : value, optional (default=None)
+ Value used for nodes that don't have the requested attribute.
+ Only relevant if data is not True or False.
+
+ Returns
+ -------
+ NodeView
+ Allows set-like operations over the nodes as well as node
+ attribute dict lookup and calling to get a NodeDataView.
+ A NodeDataView iterates over `(n, data)` and has no set operations.
+ A NodeView iterates over `n` and includes set operations.
+
+ When called, if data is False, an iterator over nodes.
+ Otherwise an iterator of 2-tuples (node, attribute value)
+ where the attribute is specified in `data`.
+ If data is True then the attribute becomes the
+ entire data dictionary.
+
+ Notes
+ -----
+ If your node data is not needed, it is simpler and equivalent
+ to use the expression ``for n in G``, or ``list(G)``.
+
+ Examples
+ --------
+ There are two simple ways of getting a list of all nodes in the graph:
+
+ >>> G = nx.path_graph(3)
+ >>> list(G.nodes)
+ [0, 1, 2]
+ >>> list(G)
+ [0, 1, 2]
+
+ To get the node data along with the nodes:
+
+ >>> G.add_node(1, time="5pm")
+ >>> G.nodes[0]["foo"] = "bar"
+ >>> list(G.nodes(data=True))
+ [(0, {'foo': 'bar'}), (1, {'time': '5pm'}), (2, {})]
+ >>> list(G.nodes.data())
+ [(0, {'foo': 'bar'}), (1, {'time': '5pm'}), (2, {})]
+
+ >>> list(G.nodes(data="foo"))
+ [(0, 'bar'), (1, None), (2, None)]
+ >>> list(G.nodes.data("foo"))
+ [(0, 'bar'), (1, None), (2, None)]
+
+ >>> list(G.nodes(data="time"))
+ [(0, None), (1, '5pm'), (2, None)]
+ >>> list(G.nodes.data("time"))
+ [(0, None), (1, '5pm'), (2, None)]
+
+ >>> list(G.nodes(data="time", default="Not Available"))
+ [(0, 'Not Available'), (1, '5pm'), (2, 'Not Available')]
+ >>> list(G.nodes.data("time", default="Not Available"))
+ [(0, 'Not Available'), (1, '5pm'), (2, 'Not Available')]
+
+ If some of your nodes have an attribute and the rest are assumed
+ to have a default attribute value you can create a dictionary
+ from node/attribute pairs using the `default` keyword argument
+ to guarantee the value is never None::
+
+ >>> G = nx.Graph()
+ >>> G.add_node(0)
+ >>> G.add_node(1, weight=2)
+ >>> G.add_node(2, weight=3)
+ >>> dict(G.nodes(data="weight", default=1))
+ {0: 1, 1: 2, 2: 3}
+
+ """
+ return NodeView(self)
+
+ def number_of_nodes(self):
+ """Returns the number of nodes in the graph.
+
+ Returns
+ -------
+ nnodes : int
+ The number of nodes in the graph.
+
+ See Also
+ --------
+ order: identical method
+ __len__: identical method
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.number_of_nodes()
+ 3
+ """
+ return len(self._node)
+
+ def order(self):
+ """Returns the number of nodes in the graph.
+
+ Returns
+ -------
+ nnodes : int
+ The number of nodes in the graph.
+
+ See Also
+ --------
+ number_of_nodes: identical method
+ __len__: identical method
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.order()
+ 3
+ """
+ return len(self._node)
+
+ def has_node(self, n):
+ """Returns True if the graph contains the node n.
+
+ Identical to `n in G`
+
+ Parameters
+ ----------
+ n : node
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.has_node(0)
+ True
+
+ It is more readable and simpler to use
+
+ >>> 0 in G
+ True
+
+ """
+ try:
+ return n in self._node
+ except TypeError:
+ return False
+
+ def add_edge(self, u_of_edge, v_of_edge, **attr):
+ """Add an edge between u and v.
+
+ The nodes u and v will be automatically added if they are
+ not already in the graph.
+
+ Edge attributes can be specified with keywords or by directly
+ accessing the edge's attribute dictionary. See examples below.
+
+ Parameters
+ ----------
+ u_of_edge, v_of_edge : nodes
+ Nodes can be, for example, strings or numbers.
+ Nodes must be hashable (and not None) Python objects.
+ attr : keyword arguments, optional
+ Edge data (or labels or objects) can be assigned using
+ keyword arguments.
+
+ See Also
+ --------
+ add_edges_from : add a collection of edges
+
+ Notes
+ -----
+ Adding an edge that already exists updates the edge data.
+
+ Many NetworkX algorithms designed for weighted graphs use
+ an edge attribute (by default `weight`) to hold a numerical value.
+
+ Examples
+ --------
+ The following all add the edge e=(1, 2) to graph G:
+
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> e = (1, 2)
+ >>> G.add_edge(1, 2) # explicit two-node form
+ >>> G.add_edge(*e) # single edge as tuple of two nodes
+ >>> G.add_edges_from([(1, 2)]) # add edges from iterable container
+
+ Associate data to edges using keywords:
+
+ >>> G.add_edge(1, 2, weight=3)
+ >>> G.add_edge(1, 3, weight=7, capacity=15, length=342.7)
+
+ For non-string attribute keys, use subscript notation.
+
+ >>> G.add_edge(1, 2)
+ >>> G[1][2].update({0: 5})
+ >>> G.edges[1, 2].update({0: 5})
+ """
+ u, v = u_of_edge, v_of_edge
+ # add nodes
+ if u not in self._node:
+ if u is None:
+ raise ValueError("None cannot be a node")
+ self._adj[u] = self.adjlist_inner_dict_factory()
+ self._node[u] = self.node_attr_dict_factory()
+ if v not in self._node:
+ if v is None:
+ raise ValueError("None cannot be a node")
+ self._adj[v] = self.adjlist_inner_dict_factory()
+ self._node[v] = self.node_attr_dict_factory()
+ # add the edge
+ datadict = self._adj[u].get(v, self.edge_attr_dict_factory())
+ datadict.update(attr)
+ self._adj[u][v] = datadict
+ self._adj[v][u] = datadict
+ nx._clear_cache(self)
+
+ def add_edges_from(self, ebunch_to_add, **attr):
+ """Add all the edges in ebunch_to_add.
+
+ Parameters
+ ----------
+ ebunch_to_add : container of edges
+ Each edge given in the container will be added to the
+ graph. The edges must be given as 2-tuples (u, v) or
+ 3-tuples (u, v, d) where d is a dictionary containing edge data.
+ attr : keyword arguments, optional
+ Edge data (or labels or objects) can be assigned using
+ keyword arguments.
+
+ See Also
+ --------
+ add_edge : add a single edge
+ add_weighted_edges_from : convenient way to add weighted edges
+
+ Notes
+ -----
+ Adding the same edge twice has no effect but any edge data
+ will be updated when each duplicate edge is added.
+
+ Edge attributes specified in an ebunch take precedence over
+ attributes specified via keyword arguments.
+
+ When adding edges from an iterator over the graph you are changing,
+ a `RuntimeError` can be raised with message:
+ `RuntimeError: dictionary changed size during iteration`. This
+ happens when the graph's underlying dictionary is modified during
+ iteration. To avoid this error, evaluate the iterator into a separate
+ object, e.g. by using `list(iterator_of_edges)`, and pass this
+ object to `G.add_edges_from`.
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_edges_from([(0, 1), (1, 2)]) # using a list of edge tuples
+ >>> e = zip(range(0, 3), range(1, 4))
+ >>> G.add_edges_from(e) # Add the path graph 0-1-2-3
+
+ Associate data to edges
+
+ >>> G.add_edges_from([(1, 2), (2, 3)], weight=3)
+ >>> G.add_edges_from([(3, 4), (1, 4)], label="WN2898")
+
+ Evaluate an iterator over a graph if using it to modify the same graph
+
+ >>> G = nx.Graph([(1, 2), (2, 3), (3, 4)])
+ >>> # Grow graph by one new node, adding edges to all existing nodes.
+ >>> # wrong way - will raise RuntimeError
+ >>> # G.add_edges_from(((5, n) for n in G.nodes))
+ >>> # correct way - note that there will be no self-edge for node 5
+ >>> G.add_edges_from(list((5, n) for n in G.nodes))
+ """
+ for e in ebunch_to_add:
+ ne = len(e)
+ if ne == 3:
+ u, v, dd = e
+ elif ne == 2:
+ u, v = e
+ dd = {} # doesn't need edge_attr_dict_factory
+ else:
+ raise NetworkXError(f"Edge tuple {e} must be a 2-tuple or 3-tuple.")
+ if u not in self._node:
+ if u is None:
+ raise ValueError("None cannot be a node")
+ self._adj[u] = self.adjlist_inner_dict_factory()
+ self._node[u] = self.node_attr_dict_factory()
+ if v not in self._node:
+ if v is None:
+ raise ValueError("None cannot be a node")
+ self._adj[v] = self.adjlist_inner_dict_factory()
+ self._node[v] = self.node_attr_dict_factory()
+ datadict = self._adj[u].get(v, self.edge_attr_dict_factory())
+ datadict.update(attr)
+ datadict.update(dd)
+ self._adj[u][v] = datadict
+ self._adj[v][u] = datadict
+ nx._clear_cache(self)
+
+ def add_weighted_edges_from(self, ebunch_to_add, weight="weight", **attr):
+ """Add weighted edges in `ebunch_to_add` with specified weight attr
+
+ Parameters
+ ----------
+ ebunch_to_add : container of edges
+ Each edge given in the list or container will be added
+ to the graph. The edges must be given as 3-tuples (u, v, w)
+ where w is a number.
+ weight : string, optional (default= 'weight')
+ The attribute name for the edge weights to be added.
+ attr : keyword arguments, optional (default= no attributes)
+ Edge attributes to add/update for all edges.
+
+ See Also
+ --------
+ add_edge : add a single edge
+ add_edges_from : add multiple edges
+
+ Notes
+ -----
+ Adding the same edge twice for Graph/DiGraph simply updates
+ the edge data. For MultiGraph/MultiDiGraph, duplicate edges
+ are stored.
+
+ When adding edges from an iterator over the graph you are changing,
+ a `RuntimeError` can be raised with message:
+ `RuntimeError: dictionary changed size during iteration`. This
+ happens when the graph's underlying dictionary is modified during
+ iteration. To avoid this error, evaluate the iterator into a separate
+ object, e.g. by using `list(iterator_of_edges)`, and pass this
+ object to `G.add_weighted_edges_from`.
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_weighted_edges_from([(0, 1, 3.0), (1, 2, 7.5)])
+
+ Evaluate an iterator over edges before passing it
+
+ >>> G = nx.Graph([(1, 2), (2, 3), (3, 4)])
+ >>> weight = 0.1
+ >>> # Grow graph by one new node, adding edges to all existing nodes.
+ >>> # wrong way - will raise RuntimeError
+ >>> # G.add_weighted_edges_from(((5, n, weight) for n in G.nodes))
+ >>> # correct way - note that there will be no self-edge for node 5
+ >>> G.add_weighted_edges_from(list((5, n, weight) for n in G.nodes))
+ """
+ self.add_edges_from(((u, v, {weight: d}) for u, v, d in ebunch_to_add), **attr)
+ nx._clear_cache(self)
+
+ def remove_edge(self, u, v):
+ """Remove the edge between u and v.
+
+ Parameters
+ ----------
+ u, v : nodes
+ Remove the edge between nodes u and v.
+
+ Raises
+ ------
+ NetworkXError
+ If there is not an edge between u and v.
+
+ See Also
+ --------
+ remove_edges_from : remove a collection of edges
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, etc
+ >>> G.remove_edge(0, 1)
+ >>> e = (1, 2)
+ >>> G.remove_edge(*e) # unpacks e from an edge tuple
+ >>> e = (2, 3, {"weight": 7}) # an edge with attribute data
+ >>> G.remove_edge(*e[:2]) # select first part of edge tuple
+ """
+ try:
+ del self._adj[u][v]
+ if u != v: # self-loop needs only one entry removed
+ del self._adj[v][u]
+ except KeyError as err:
+ raise NetworkXError(f"The edge {u}-{v} is not in the graph") from err
+ nx._clear_cache(self)
+
+ def remove_edges_from(self, ebunch):
+ """Remove all edges specified in ebunch.
+
+ Parameters
+ ----------
+ ebunch: list or container of edge tuples
+ Each edge given in the list or container will be removed
+ from the graph. The edges can be:
+
+ - 2-tuples (u, v) edge between u and v.
+ - 3-tuples (u, v, k) where k is ignored.
+
+ See Also
+ --------
+ remove_edge : remove a single edge
+
+ Notes
+ -----
+ Will fail silently if an edge in ebunch is not in the graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> ebunch = [(1, 2), (2, 3)]
+ >>> G.remove_edges_from(ebunch)
+ """
+ adj = self._adj
+ for e in ebunch:
+ u, v = e[:2] # ignore edge data if present
+ if u in adj and v in adj[u]:
+ del adj[u][v]
+ if u != v: # self loop needs only one entry removed
+ del adj[v][u]
+ nx._clear_cache(self)
+
+ def update(self, edges=None, nodes=None):
+ """Update the graph using nodes/edges/graphs as input.
+
+ Like dict.update, this method takes a graph as input, adding the
+ graph's nodes and edges to this graph. It can also take two inputs:
+ edges and nodes. Finally it can take either edges or nodes.
+ To specify only nodes the keyword `nodes` must be used.
+
+ The collections of edges and nodes are treated similarly to
+ the add_edges_from/add_nodes_from methods. When iterated, they
+ should yield 2-tuples (u, v) or 3-tuples (u, v, datadict).
+
+ Parameters
+ ----------
+ edges : Graph object, collection of edges, or None
+ The first parameter can be a graph or some edges. If it has
+ attributes `nodes` and `edges`, then it is taken to be a
+ Graph-like object and those attributes are used as collections
+ of nodes and edges to be added to the graph.
+ If the first parameter does not have those attributes, it is
+ treated as a collection of edges and added to the graph.
+ If the first argument is None, no edges are added.
+ nodes : collection of nodes, or None
+ The second parameter is treated as a collection of nodes
+ to be added to the graph unless it is None.
+ If `edges is None` and `nodes is None` an exception is raised.
+ If the first parameter is a Graph, then `nodes` is ignored.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(5)
+ >>> G.update(nx.complete_graph(range(4, 10)))
+ >>> from itertools import combinations
+ >>> edges = (
+ ... (u, v, {"power": u * v})
+ ... for u, v in combinations(range(10, 20), 2)
+ ... if u * v < 225
+ ... )
+ >>> nodes = [1000] # for singleton, use a container
+ >>> G.update(edges, nodes)
+
+ Notes
+ -----
+ It you want to update the graph using an adjacency structure
+ it is straightforward to obtain the edges/nodes from adjacency.
+ The following examples provide common cases, your adjacency may
+ be slightly different and require tweaks of these examples::
+
+ >>> # dict-of-set/list/tuple
+ >>> adj = {1: {2, 3}, 2: {1, 3}, 3: {1, 2}}
+ >>> e = [(u, v) for u, nbrs in adj.items() for v in nbrs]
+ >>> G.update(edges=e, nodes=adj)
+
+ >>> DG = nx.DiGraph()
+ >>> # dict-of-dict-of-attribute
+ >>> adj = {1: {2: 1.3, 3: 0.7}, 2: {1: 1.4}, 3: {1: 0.7}}
+ >>> e = [
+ ... (u, v, {"weight": d})
+ ... for u, nbrs in adj.items()
+ ... for v, d in nbrs.items()
+ ... ]
+ >>> DG.update(edges=e, nodes=adj)
+
+ >>> # dict-of-dict-of-dict
+ >>> adj = {1: {2: {"weight": 1.3}, 3: {"color": 0.7, "weight": 1.2}}}
+ >>> e = [
+ ... (u, v, {"weight": d})
+ ... for u, nbrs in adj.items()
+ ... for v, d in nbrs.items()
+ ... ]
+ >>> DG.update(edges=e, nodes=adj)
+
+ >>> # predecessor adjacency (dict-of-set)
+ >>> pred = {1: {2, 3}, 2: {3}, 3: {3}}
+ >>> e = [(v, u) for u, nbrs in pred.items() for v in nbrs]
+
+ >>> # MultiGraph dict-of-dict-of-dict-of-attribute
+ >>> MDG = nx.MultiDiGraph()
+ >>> adj = {
+ ... 1: {2: {0: {"weight": 1.3}, 1: {"weight": 1.2}}},
+ ... 3: {2: {0: {"weight": 0.7}}},
+ ... }
+ >>> e = [
+ ... (u, v, ekey, d)
+ ... for u, nbrs in adj.items()
+ ... for v, keydict in nbrs.items()
+ ... for ekey, d in keydict.items()
+ ... ]
+ >>> MDG.update(edges=e)
+
+ See Also
+ --------
+ add_edges_from: add multiple edges to a graph
+ add_nodes_from: add multiple nodes to a graph
+ """
+ if edges is not None:
+ if nodes is not None:
+ self.add_nodes_from(nodes)
+ self.add_edges_from(edges)
+ else:
+ # check if edges is a Graph object
+ try:
+ graph_nodes = edges.nodes
+ graph_edges = edges.edges
+ except AttributeError:
+ # edge not Graph-like
+ self.add_edges_from(edges)
+ else: # edges is Graph-like
+ self.add_nodes_from(graph_nodes.data())
+ self.add_edges_from(graph_edges.data())
+ self.graph.update(edges.graph)
+ elif nodes is not None:
+ self.add_nodes_from(nodes)
+ else:
+ raise NetworkXError("update needs nodes or edges input")
+
+ def has_edge(self, u, v):
+ """Returns True if the edge (u, v) is in the graph.
+
+ This is the same as `v in G[u]` without KeyError exceptions.
+
+ Parameters
+ ----------
+ u, v : nodes
+ Nodes can be, for example, strings or numbers.
+ Nodes must be hashable (and not None) Python objects.
+
+ Returns
+ -------
+ edge_ind : bool
+ True if edge is in the graph, False otherwise.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.has_edge(0, 1) # using two nodes
+ True
+ >>> e = (0, 1)
+ >>> G.has_edge(*e) # e is a 2-tuple (u, v)
+ True
+ >>> e = (0, 1, {"weight": 7})
+ >>> G.has_edge(*e[:2]) # e is a 3-tuple (u, v, data_dictionary)
+ True
+
+ The following syntax are equivalent:
+
+ >>> G.has_edge(0, 1)
+ True
+ >>> 1 in G[0] # though this gives KeyError if 0 not in G
+ True
+
+ """
+ try:
+ return v in self._adj[u]
+ except KeyError:
+ return False
+
+ def neighbors(self, n):
+ """Returns an iterator over all neighbors of node n.
+
+ This is identical to `iter(G[n])`
+
+ Parameters
+ ----------
+ n : node
+ A node in the graph
+
+ Returns
+ -------
+ neighbors : iterator
+ An iterator over all neighbors of node n
+
+ Raises
+ ------
+ NetworkXError
+ If the node n is not in the graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> [n for n in G.neighbors(0)]
+ [1]
+
+ Notes
+ -----
+ Alternate ways to access the neighbors are ``G.adj[n]`` or ``G[n]``:
+
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_edge("a", "b", weight=7)
+ >>> G["a"]
+ AtlasView({'b': {'weight': 7}})
+ >>> G = nx.path_graph(4)
+ >>> [n for n in G[0]]
+ [1]
+ """
+ try:
+ return iter(self._adj[n])
+ except KeyError as err:
+ raise NetworkXError(f"The node {n} is not in the graph.") from err
+
+ @cached_property
+ def edges(self):
+ """An EdgeView of the Graph as G.edges or G.edges().
+
+ edges(self, nbunch=None, data=False, default=None)
+
+ The EdgeView provides set-like operations on the edge-tuples
+ as well as edge attribute lookup. When called, it also provides
+ an EdgeDataView object which allows control of access to edge
+ attributes (but does not provide set-like operations).
+ Hence, `G.edges[u, v]['color']` provides the value of the color
+ attribute for edge `(u, v)` while
+ `for (u, v, c) in G.edges.data('color', default='red'):`
+ iterates through all the edges yielding the color attribute
+ with default `'red'` if no color attribute exists.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges from these nodes.
+ data : string or bool, optional (default=False)
+ The edge attribute returned in 3-tuple (u, v, ddict[data]).
+ If True, return edge attribute dict in 3-tuple (u, v, ddict).
+ If False, return 2-tuple (u, v).
+ default : value, optional (default=None)
+ Value used for edges that don't have the requested attribute.
+ Only relevant if data is not True or False.
+
+ Returns
+ -------
+ edges : EdgeView
+ A view of edge attributes, usually it iterates over (u, v)
+ or (u, v, d) tuples of edges, but can also be used for
+ attribute lookup as `edges[u, v]['foo']`.
+
+ Notes
+ -----
+ Nodes in nbunch that are not in the graph will be (quietly) ignored.
+ For directed graphs this returns the out-edges.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3) # or MultiGraph, etc
+ >>> G.add_edge(2, 3, weight=5)
+ >>> [e for e in G.edges]
+ [(0, 1), (1, 2), (2, 3)]
+ >>> G.edges.data() # default data is {} (empty dict)
+ EdgeDataView([(0, 1, {}), (1, 2, {}), (2, 3, {'weight': 5})])
+ >>> G.edges.data("weight", default=1)
+ EdgeDataView([(0, 1, 1), (1, 2, 1), (2, 3, 5)])
+ >>> G.edges([0, 3]) # only edges from these nodes
+ EdgeDataView([(0, 1), (3, 2)])
+ >>> G.edges(0) # only edges from node 0
+ EdgeDataView([(0, 1)])
+ """
+ return EdgeView(self)
+
+ def get_edge_data(self, u, v, default=None):
+ """Returns the attribute dictionary associated with edge (u, v).
+
+ This is identical to `G[u][v]` except the default is returned
+ instead of an exception if the edge doesn't exist.
+
+ Parameters
+ ----------
+ u, v : nodes
+ default: any Python object (default=None)
+ Value to return if the edge (u, v) is not found.
+
+ Returns
+ -------
+ edge_dict : dictionary
+ The edge attribute dictionary.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G[0][1]
+ {}
+
+ Warning: Assigning to `G[u][v]` is not permitted.
+ But it is safe to assign attributes `G[u][v]['foo']`
+
+ >>> G[0][1]["weight"] = 7
+ >>> G[0][1]["weight"]
+ 7
+ >>> G[1][0]["weight"]
+ 7
+
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.get_edge_data(0, 1) # default edge data is {}
+ {}
+ >>> e = (0, 1)
+ >>> G.get_edge_data(*e) # tuple form
+ {}
+ >>> G.get_edge_data("a", "b", default=0) # edge not in graph, return 0
+ 0
+ """
+ try:
+ return self._adj[u][v]
+ except KeyError:
+ return default
+
+ def adjacency(self):
+ """Returns an iterator over (node, adjacency dict) tuples for all nodes.
+
+ For directed graphs, only outgoing neighbors/adjacencies are included.
+
+ Returns
+ -------
+ adj_iter : iterator
+ An iterator over (node, adjacency dictionary) for all nodes in
+ the graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> [(n, nbrdict) for n, nbrdict in G.adjacency()]
+ [(0, {1: {}}), (1, {0: {}, 2: {}}), (2, {1: {}, 3: {}}), (3, {2: {}})]
+
+ """
+ return iter(self._adj.items())
+
+ @cached_property
+ def degree(self):
+ """A DegreeView for the Graph as G.degree or G.degree().
+
+ The node degree is the number of edges adjacent to the node.
+ The weighted node degree is the sum of the edge weights for
+ edges incident to that node.
+
+ This object provides an iterator for (node, degree) as well as
+ lookup for the degree for a single node.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+
+ weight : string or None, optional (default=None)
+ The name of an edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ Returns
+ -------
+ DegreeView or int
+ If multiple nodes are requested (the default), returns a `DegreeView`
+ mapping nodes to their degree.
+ If a single node is requested, returns the degree of the node as an integer.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.degree[0] # node 0 has degree 1
+ 1
+ >>> list(G.degree([0, 1, 2]))
+ [(0, 1), (1, 2), (2, 2)]
+ """
+ return DegreeView(self)
+
+ def clear(self):
+ """Remove all nodes and edges from the graph.
+
+ This also removes the name, and all graph, node, and edge attributes.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.clear()
+ >>> list(G.nodes)
+ []
+ >>> list(G.edges)
+ []
+
+ """
+ self._adj.clear()
+ self._node.clear()
+ self.graph.clear()
+ nx._clear_cache(self)
+
+ def clear_edges(self):
+ """Remove all edges from the graph without altering nodes.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.clear_edges()
+ >>> list(G.nodes)
+ [0, 1, 2, 3]
+ >>> list(G.edges)
+ []
+ """
+ for nbr_dict in self._adj.values():
+ nbr_dict.clear()
+ nx._clear_cache(self)
+
+ def is_multigraph(self):
+ """Returns True if graph is a multigraph, False otherwise."""
+ return False
+
+ def is_directed(self):
+ """Returns True if graph is directed, False otherwise."""
+ return False
+
+ def copy(self, as_view=False):
+ """Returns a copy of the graph.
+
+ The copy method by default returns an independent shallow copy
+ of the graph and attributes. That is, if an attribute is a
+ container, that container is shared by the original an the copy.
+ Use Python's `copy.deepcopy` for new containers.
+
+ If `as_view` is True then a view is returned instead of a copy.
+
+ Notes
+ -----
+ All copies reproduce the graph structure, but data attributes
+ may be handled in different ways. There are four types of copies
+ of a graph that people might want.
+
+ Deepcopy -- A "deepcopy" copies the graph structure as well as
+ all data attributes and any objects they might contain.
+ The entire graph object is new so that changes in the copy
+ do not affect the original object. (see Python's copy.deepcopy)
+
+ Data Reference (Shallow) -- For a shallow copy the graph structure
+ is copied but the edge, node and graph attribute dicts are
+ references to those in the original graph. This saves
+ time and memory but could cause confusion if you change an attribute
+ in one graph and it changes the attribute in the other.
+ NetworkX does not provide this level of shallow copy.
+
+ Independent Shallow -- This copy creates new independent attribute
+ dicts and then does a shallow copy of the attributes. That is, any
+ attributes that are containers are shared between the new graph
+ and the original. This is exactly what `dict.copy()` provides.
+ You can obtain this style copy using:
+
+ >>> G = nx.path_graph(5)
+ >>> H = G.copy()
+ >>> H = G.copy(as_view=False)
+ >>> H = nx.Graph(G)
+ >>> H = G.__class__(G)
+
+ Fresh Data -- For fresh data, the graph structure is copied while
+ new empty data attribute dicts are created. The resulting graph
+ is independent of the original and it has no edge, node or graph
+ attributes. Fresh copies are not enabled. Instead use:
+
+ >>> H = G.__class__()
+ >>> H.add_nodes_from(G)
+ >>> H.add_edges_from(G.edges)
+
+ View -- Inspired by dict-views, graph-views act like read-only
+ versions of the original graph, providing a copy of the original
+ structure without requiring any memory for copying the information.
+
+ See the Python copy module for more information on shallow
+ and deep copies, https://docs.python.org/3/library/copy.html.
+
+ Parameters
+ ----------
+ as_view : bool, optional (default=False)
+ If True, the returned graph-view provides a read-only view
+ of the original graph without actually copying any data.
+
+ Returns
+ -------
+ G : Graph
+ A copy of the graph.
+
+ See Also
+ --------
+ to_directed: return a directed copy of the graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> H = G.copy()
+
+ """
+ if as_view is True:
+ return nx.graphviews.generic_graph_view(self)
+ G = self.__class__()
+ G.graph.update(self.graph)
+ G.add_nodes_from((n, d.copy()) for n, d in self._node.items())
+ G.add_edges_from(
+ (u, v, datadict.copy())
+ for u, nbrs in self._adj.items()
+ for v, datadict in nbrs.items()
+ )
+ return G
+
+ def to_directed(self, as_view=False):
+ """Returns a directed representation of the graph.
+
+ Returns
+ -------
+ G : DiGraph
+ A directed graph with the same name, same nodes, and with
+ each edge (u, v, data) replaced by two directed edges
+ (u, v, data) and (v, u, data).
+
+ Notes
+ -----
+ This returns a "deepcopy" of the edge, node, and
+ graph attributes which attempts to completely copy
+ all of the data and references.
+
+ This is in contrast to the similar D=DiGraph(G) which returns a
+ shallow copy of the data.
+
+ See the Python copy module for more information on shallow
+ and deep copies, https://docs.python.org/3/library/copy.html.
+
+ Warning: If you have subclassed Graph to use dict-like objects
+ in the data structure, those changes do not transfer to the
+ DiGraph created by this method.
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or MultiGraph, etc
+ >>> G.add_edge(0, 1)
+ >>> H = G.to_directed()
+ >>> list(H.edges)
+ [(0, 1), (1, 0)]
+
+ If already directed, return a (deep) copy
+
+ >>> G = nx.DiGraph() # or MultiDiGraph, etc
+ >>> G.add_edge(0, 1)
+ >>> H = G.to_directed()
+ >>> list(H.edges)
+ [(0, 1)]
+ """
+ graph_class = self.to_directed_class()
+ if as_view is True:
+ return nx.graphviews.generic_graph_view(self, graph_class)
+ # deepcopy when not a view
+ G = graph_class()
+ G.graph.update(deepcopy(self.graph))
+ G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
+ G.add_edges_from(
+ (u, v, deepcopy(data))
+ for u, nbrs in self._adj.items()
+ for v, data in nbrs.items()
+ )
+ return G
+
+ def to_undirected(self, as_view=False):
+ """Returns an undirected copy of the graph.
+
+ Parameters
+ ----------
+ as_view : bool (optional, default=False)
+ If True return a view of the original undirected graph.
+
+ Returns
+ -------
+ G : Graph/MultiGraph
+ A deepcopy of the graph.
+
+ See Also
+ --------
+ Graph, copy, add_edge, add_edges_from
+
+ Notes
+ -----
+ This returns a "deepcopy" of the edge, node, and
+ graph attributes which attempts to completely copy
+ all of the data and references.
+
+ This is in contrast to the similar `G = nx.DiGraph(D)` which returns a
+ shallow copy of the data.
+
+ See the Python copy module for more information on shallow
+ and deep copies, https://docs.python.org/3/library/copy.html.
+
+ Warning: If you have subclassed DiGraph to use dict-like objects
+ in the data structure, those changes do not transfer to the
+ Graph created by this method.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(2) # or MultiGraph, etc
+ >>> H = G.to_directed()
+ >>> list(H.edges)
+ [(0, 1), (1, 0)]
+ >>> G2 = H.to_undirected()
+ >>> list(G2.edges)
+ [(0, 1)]
+ """
+ graph_class = self.to_undirected_class()
+ if as_view is True:
+ return nx.graphviews.generic_graph_view(self, graph_class)
+ # deepcopy when not a view
+ G = graph_class()
+ G.graph.update(deepcopy(self.graph))
+ G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
+ G.add_edges_from(
+ (u, v, deepcopy(d))
+ for u, nbrs in self._adj.items()
+ for v, d in nbrs.items()
+ )
+ return G
+
+ def subgraph(self, nodes):
+ """Returns a SubGraph view of the subgraph induced on `nodes`.
+
+ The induced subgraph of the graph contains the nodes in `nodes`
+ and the edges between those nodes.
+
+ Parameters
+ ----------
+ nodes : list, iterable
+ A container of nodes which will be iterated through once.
+
+ Returns
+ -------
+ G : SubGraph View
+ A subgraph view of the graph. The graph structure cannot be
+ changed but node/edge attributes can and are shared with the
+ original graph.
+
+ Notes
+ -----
+ The graph, edge and node attributes are shared with the original graph.
+ Changes to the graph structure is ruled out by the view, but changes
+ to attributes are reflected in the original graph.
+
+ To create a subgraph with its own copy of the edge/node attributes use:
+ G.subgraph(nodes).copy()
+
+ For an inplace reduction of a graph to a subgraph you can remove nodes:
+ G.remove_nodes_from([n for n in G if n not in set(nodes)])
+
+ Subgraph views are sometimes NOT what you want. In most cases where
+ you want to do more than simply look at the induced edges, it makes
+ more sense to just create the subgraph as its own graph with code like:
+
+ ::
+
+ # Create a subgraph SG based on a (possibly multigraph) G
+ SG = G.__class__()
+ SG.add_nodes_from((n, G.nodes[n]) for n in largest_wcc)
+ if SG.is_multigraph():
+ SG.add_edges_from(
+ (n, nbr, key, d)
+ for n, nbrs in G.adj.items()
+ if n in largest_wcc
+ for nbr, keydict in nbrs.items()
+ if nbr in largest_wcc
+ for key, d in keydict.items()
+ )
+ else:
+ SG.add_edges_from(
+ (n, nbr, d)
+ for n, nbrs in G.adj.items()
+ if n in largest_wcc
+ for nbr, d in nbrs.items()
+ if nbr in largest_wcc
+ )
+ SG.graph.update(G.graph)
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> H = G.subgraph([0, 1, 2])
+ >>> list(H.edges)
+ [(0, 1), (1, 2)]
+ """
+ induced_nodes = nx.filters.show_nodes(self.nbunch_iter(nodes))
+ # if already a subgraph, don't make a chain
+ subgraph = nx.subgraph_view
+ if hasattr(self, "_NODE_OK"):
+ return subgraph(
+ self._graph, filter_node=induced_nodes, filter_edge=self._EDGE_OK
+ )
+ return subgraph(self, filter_node=induced_nodes)
+
+ def edge_subgraph(self, edges):
+ """Returns the subgraph induced by the specified edges.
+
+ The induced subgraph contains each edge in `edges` and each
+ node incident to any one of those edges.
+
+ Parameters
+ ----------
+ edges : iterable
+ An iterable of edges in this graph.
+
+ Returns
+ -------
+ G : Graph
+ An edge-induced subgraph of this graph with the same edge
+ attributes.
+
+ Notes
+ -----
+ The graph, edge, and node attributes in the returned subgraph
+ view are references to the corresponding attributes in the original
+ graph. The view is read-only.
+
+ To create a full graph version of the subgraph with its own copy
+ of the edge or node attributes, use::
+
+ G.edge_subgraph(edges).copy()
+
+ Examples
+ --------
+ >>> G = nx.path_graph(5)
+ >>> H = G.edge_subgraph([(0, 1), (3, 4)])
+ >>> list(H.nodes)
+ [0, 1, 3, 4]
+ >>> list(H.edges)
+ [(0, 1), (3, 4)]
+
+ """
+ return nx.edge_subgraph(self, edges)
+
+ def size(self, weight=None):
+ """Returns the number of edges or total of all edge weights.
+
+ Parameters
+ ----------
+ weight : string or None, optional (default=None)
+ The edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+
+ Returns
+ -------
+ size : numeric
+ The number of edges or
+ (if weight keyword is provided) the total weight sum.
+
+ If weight is None, returns an int. Otherwise a float
+ (or more general numeric if the weights are more general).
+
+ See Also
+ --------
+ number_of_edges
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.size()
+ 3
+
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_edge("a", "b", weight=2)
+ >>> G.add_edge("b", "c", weight=4)
+ >>> G.size()
+ 2
+ >>> G.size(weight="weight")
+ 6.0
+ """
+ s = sum(d for v, d in self.degree(weight=weight))
+ # If `weight` is None, the sum of the degrees is guaranteed to be
+ # even, so we can perform integer division and hence return an
+ # integer. Otherwise, the sum of the weighted degrees is not
+ # guaranteed to be an integer, so we perform "real" division.
+ return s // 2 if weight is None else s / 2
+
+ def number_of_edges(self, u=None, v=None):
+ """Returns the number of edges between two nodes.
+
+ Parameters
+ ----------
+ u, v : nodes, optional (default=all edges)
+ If u and v are specified, return the number of edges between
+ u and v. Otherwise return the total number of all edges.
+
+ Returns
+ -------
+ nedges : int
+ The number of edges in the graph. If nodes `u` and `v` are
+ specified return the number of edges between those nodes. If
+ the graph is directed, this only returns the number of edges
+ from `u` to `v`.
+
+ See Also
+ --------
+ size
+
+ Examples
+ --------
+ For undirected graphs, this method counts the total number of
+ edges in the graph:
+
+ >>> G = nx.path_graph(4)
+ >>> G.number_of_edges()
+ 3
+
+ If you specify two nodes, this counts the total number of edges
+ joining the two nodes:
+
+ >>> G.number_of_edges(0, 1)
+ 1
+
+ For directed graphs, this method can count the total number of
+ directed edges from `u` to `v`:
+
+ >>> G = nx.DiGraph()
+ >>> G.add_edge(0, 1)
+ >>> G.add_edge(1, 0)
+ >>> G.number_of_edges(0, 1)
+ 1
+
+ """
+ if u is None:
+ return int(self.size())
+ if v in self._adj[u]:
+ return 1
+ return 0
+
+ def nbunch_iter(self, nbunch=None):
+ """Returns an iterator over nodes contained in nbunch that are
+ also in the graph.
+
+ The nodes in nbunch are checked for membership in the graph
+ and if not are silently ignored.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+
+ Returns
+ -------
+ niter : iterator
+ An iterator over nodes in nbunch that are also in the graph.
+ If nbunch is None, iterate over all nodes in the graph.
+
+ Raises
+ ------
+ NetworkXError
+ If nbunch is not a node or sequence of nodes.
+ If a node in nbunch is not hashable.
+
+ See Also
+ --------
+ Graph.__iter__
+
+ Notes
+ -----
+ When nbunch is an iterator, the returned iterator yields values
+ directly from nbunch, becoming exhausted when nbunch is exhausted.
+
+ To test whether nbunch is a single node, one can use
+ "if nbunch in self:", even after processing with this routine.
+
+ If nbunch is not a node or a (possibly empty) sequence/iterator
+ or None, a :exc:`NetworkXError` is raised. Also, if any object in
+ nbunch is not hashable, a :exc:`NetworkXError` is raised.
+ """
+ if nbunch is None: # include all nodes via iterator
+ bunch = iter(self._adj)
+ elif nbunch in self: # if nbunch is a single node
+ bunch = iter([nbunch])
+ else: # if nbunch is a sequence of nodes
+
+ def bunch_iter(nlist, adj):
+ try:
+ for n in nlist:
+ if n in adj:
+ yield n
+ except TypeError as err:
+ exc, message = err, err.args[0]
+ # capture error for non-sequence/iterator nbunch.
+ if "iter" in message:
+ exc = NetworkXError(
+ "nbunch is not a node or a sequence of nodes."
+ )
+ # capture error for unhashable node.
+ if "hashable" in message:
+ exc = NetworkXError(
+ f"Node {n} in sequence nbunch is not a valid node."
+ )
+ raise exc
+
+ bunch = bunch_iter(nbunch, self._adj)
+ return bunch
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/graphviews.py b/.venv/lib/python3.12/site-packages/networkx/classes/graphviews.py
new file mode 100644
index 00000000..0b09df64
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/graphviews.py
@@ -0,0 +1,269 @@
+"""View of Graphs as SubGraph, Reverse, Directed, Undirected.
+
+In some algorithms it is convenient to temporarily morph
+a graph to exclude some nodes or edges. It should be better
+to do that via a view than to remove and then re-add.
+In other algorithms it is convenient to temporarily morph
+a graph to reverse directed edges, or treat a directed graph
+as undirected, etc. This module provides those graph views.
+
+The resulting views are essentially read-only graphs that
+report data from the original graph object. We provide an
+attribute G._graph which points to the underlying graph object.
+
+Note: Since graphviews look like graphs, one can end up with
+view-of-view-of-view chains. Be careful with chains because
+they become very slow with about 15 nested views.
+For the common simple case of node induced subgraphs created
+from the graph class, we short-cut the chain by returning a
+subgraph of the original graph directly rather than a subgraph
+of a subgraph. We are careful not to disrupt any edge filter in
+the middle subgraph. In general, determining how to short-cut
+the chain is tricky and much harder with restricted_views than
+with induced subgraphs.
+Often it is easiest to use .copy() to avoid chains.
+"""
+
+import networkx as nx
+from networkx.classes.coreviews import (
+ FilterAdjacency,
+ FilterAtlas,
+ FilterMultiAdjacency,
+ UnionAdjacency,
+ UnionMultiAdjacency,
+)
+from networkx.classes.filters import no_filter
+from networkx.exception import NetworkXError
+from networkx.utils import not_implemented_for
+
+__all__ = ["generic_graph_view", "subgraph_view", "reverse_view"]
+
+
+def generic_graph_view(G, create_using=None):
+ """Returns a read-only view of `G`.
+
+ The graph `G` and its attributes are not copied but viewed through the new graph object
+ of the same class as `G` (or of the class specified in `create_using`).
+
+ Parameters
+ ----------
+ G : graph
+ A directed/undirected graph/multigraph.
+
+ create_using : NetworkX graph constructor, optional (default=None)
+ Graph type to create. If graph instance, then cleared before populated.
+ If `None`, then the appropriate Graph type is inferred from `G`.
+
+ Returns
+ -------
+ newG : graph
+ A view of the input graph `G` and its attributes as viewed through
+ the `create_using` class.
+
+ Raises
+ ------
+ NetworkXError
+ If `G` is a multigraph (or multidigraph) but `create_using` is not, or vice versa.
+
+ Notes
+ -----
+ The returned graph view is read-only (cannot modify the graph).
+ Yet the view reflects any changes in `G`. The intent is to mimic dict views.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> G.add_edge(1, 2, weight=0.3)
+ >>> G.add_edge(2, 3, weight=0.5)
+ >>> G.edges(data=True)
+ EdgeDataView([(1, 2, {'weight': 0.3}), (2, 3, {'weight': 0.5})])
+
+ The view exposes the attributes from the original graph.
+
+ >>> viewG = nx.graphviews.generic_graph_view(G)
+ >>> viewG.edges(data=True)
+ EdgeDataView([(1, 2, {'weight': 0.3}), (2, 3, {'weight': 0.5})])
+
+ Changes to `G` are reflected in `viewG`.
+
+ >>> G.remove_edge(2, 3)
+ >>> G.edges(data=True)
+ EdgeDataView([(1, 2, {'weight': 0.3})])
+
+ >>> viewG.edges(data=True)
+ EdgeDataView([(1, 2, {'weight': 0.3})])
+
+ We can change the graph type with the `create_using` parameter.
+
+ >>> type(G)
+ <class 'networkx.classes.graph.Graph'>
+ >>> viewDG = nx.graphviews.generic_graph_view(G, create_using=nx.DiGraph)
+ >>> type(viewDG)
+ <class 'networkx.classes.digraph.DiGraph'>
+ """
+ if create_using is None:
+ newG = G.__class__()
+ else:
+ newG = nx.empty_graph(0, create_using)
+ if G.is_multigraph() != newG.is_multigraph():
+ raise NetworkXError("Multigraph for G must agree with create_using")
+ newG = nx.freeze(newG)
+
+ # create view by assigning attributes from G
+ newG._graph = G
+ newG.graph = G.graph
+
+ newG._node = G._node
+ if newG.is_directed():
+ if G.is_directed():
+ newG._succ = G._succ
+ newG._pred = G._pred
+ # newG._adj is synced with _succ
+ else:
+ newG._succ = G._adj
+ newG._pred = G._adj
+ # newG._adj is synced with _succ
+ elif G.is_directed():
+ if G.is_multigraph():
+ newG._adj = UnionMultiAdjacency(G._succ, G._pred)
+ else:
+ newG._adj = UnionAdjacency(G._succ, G._pred)
+ else:
+ newG._adj = G._adj
+ return newG
+
+
+def subgraph_view(G, *, filter_node=no_filter, filter_edge=no_filter):
+ """View of `G` applying a filter on nodes and edges.
+
+ `subgraph_view` provides a read-only view of the input graph that excludes
+ nodes and edges based on the outcome of two filter functions `filter_node`
+ and `filter_edge`.
+
+ The `filter_node` function takes one argument --- the node --- and returns
+ `True` if the node should be included in the subgraph, and `False` if it
+ should not be included.
+
+ The `filter_edge` function takes two (or three arguments if `G` is a
+ multi-graph) --- the nodes describing an edge, plus the edge-key if
+ parallel edges are possible --- and returns `True` if the edge should be
+ included in the subgraph, and `False` if it should not be included.
+
+ Both node and edge filter functions are called on graph elements as they
+ are queried, meaning there is no up-front cost to creating the view.
+
+ Parameters
+ ----------
+ G : networkx.Graph
+ A directed/undirected graph/multigraph
+
+ filter_node : callable, optional
+ A function taking a node as input, which returns `True` if the node
+ should appear in the view.
+
+ filter_edge : callable, optional
+ A function taking as input the two nodes describing an edge (plus the
+ edge-key if `G` is a multi-graph), which returns `True` if the edge
+ should appear in the view.
+
+ Returns
+ -------
+ graph : networkx.Graph
+ A read-only graph view of the input graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(6)
+
+ Filter functions operate on the node, and return `True` if the node should
+ appear in the view:
+
+ >>> def filter_node(n1):
+ ... return n1 != 5
+ >>> view = nx.subgraph_view(G, filter_node=filter_node)
+ >>> view.nodes()
+ NodeView((0, 1, 2, 3, 4))
+
+ We can use a closure pattern to filter graph elements based on additional
+ data --- for example, filtering on edge data attached to the graph:
+
+ >>> G[3][4]["cross_me"] = False
+ >>> def filter_edge(n1, n2):
+ ... return G[n1][n2].get("cross_me", True)
+ >>> view = nx.subgraph_view(G, filter_edge=filter_edge)
+ >>> view.edges()
+ EdgeView([(0, 1), (1, 2), (2, 3), (4, 5)])
+
+ >>> view = nx.subgraph_view(
+ ... G,
+ ... filter_node=filter_node,
+ ... filter_edge=filter_edge,
+ ... )
+ >>> view.nodes()
+ NodeView((0, 1, 2, 3, 4))
+ >>> view.edges()
+ EdgeView([(0, 1), (1, 2), (2, 3)])
+ """
+ newG = nx.freeze(G.__class__())
+ newG._NODE_OK = filter_node
+ newG._EDGE_OK = filter_edge
+
+ # create view by assigning attributes from G
+ newG._graph = G
+ newG.graph = G.graph
+
+ newG._node = FilterAtlas(G._node, filter_node)
+ if G.is_multigraph():
+ Adj = FilterMultiAdjacency
+
+ def reverse_edge(u, v, k=None):
+ return filter_edge(v, u, k)
+
+ else:
+ Adj = FilterAdjacency
+
+ def reverse_edge(u, v, k=None):
+ return filter_edge(v, u)
+
+ if G.is_directed():
+ newG._succ = Adj(G._succ, filter_node, filter_edge)
+ newG._pred = Adj(G._pred, filter_node, reverse_edge)
+ # newG._adj is synced with _succ
+ else:
+ newG._adj = Adj(G._adj, filter_node, filter_edge)
+ return newG
+
+
+@not_implemented_for("undirected")
+def reverse_view(G):
+ """View of `G` with edge directions reversed
+
+ `reverse_view` returns a read-only view of the input graph where
+ edge directions are reversed.
+
+ Identical to digraph.reverse(copy=False)
+
+ Parameters
+ ----------
+ G : networkx.DiGraph
+
+ Returns
+ -------
+ graph : networkx.DiGraph
+
+ Examples
+ --------
+ >>> G = nx.DiGraph()
+ >>> G.add_edge(1, 2)
+ >>> G.add_edge(2, 3)
+ >>> G.edges()
+ OutEdgeView([(1, 2), (2, 3)])
+
+ >>> view = nx.reverse_view(G)
+ >>> view.edges()
+ OutEdgeView([(2, 1), (3, 2)])
+ """
+ newG = generic_graph_view(G)
+ newG._succ, newG._pred = G._pred, G._succ
+ # newG._adj is synced with _succ
+ return newG
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/multidigraph.py b/.venv/lib/python3.12/site-packages/networkx/classes/multidigraph.py
new file mode 100644
index 00000000..597af796
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/multidigraph.py
@@ -0,0 +1,966 @@
+"""Base class for MultiDiGraph."""
+
+from copy import deepcopy
+from functools import cached_property
+
+import networkx as nx
+from networkx import convert
+from networkx.classes.coreviews import MultiAdjacencyView
+from networkx.classes.digraph import DiGraph
+from networkx.classes.multigraph import MultiGraph
+from networkx.classes.reportviews import (
+ DiMultiDegreeView,
+ InMultiDegreeView,
+ InMultiEdgeView,
+ OutMultiDegreeView,
+ OutMultiEdgeView,
+)
+from networkx.exception import NetworkXError
+
+__all__ = ["MultiDiGraph"]
+
+
+class MultiDiGraph(MultiGraph, DiGraph):
+ """A directed graph class that can store multiedges.
+
+ Multiedges are multiple edges between two nodes. Each edge
+ can hold optional data or attributes.
+
+ A MultiDiGraph holds directed edges. Self loops are allowed.
+
+ Nodes can be arbitrary (hashable) Python objects with optional
+ key/value attributes. By convention `None` is not used as a node.
+
+ Edges are represented as links between nodes with optional
+ key/value attributes.
+
+ Parameters
+ ----------
+ incoming_graph_data : input graph (optional, default: None)
+ Data to initialize graph. If None (default) an empty
+ graph is created. The data can be any format that is supported
+ by the to_networkx_graph() function, currently including edge list,
+ dict of dicts, dict of lists, NetworkX graph, 2D NumPy array, 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.
+
+ See Also
+ --------
+ Graph
+ DiGraph
+ MultiGraph
+
+ Examples
+ --------
+ Create an empty graph structure (a "null graph") with no nodes and
+ no edges.
+
+ >>> G = nx.MultiDiGraph()
+
+ G can be grown in several ways.
+
+ **Nodes:**
+
+ Add one node at a time:
+
+ >>> G.add_node(1)
+
+ Add the nodes from any container (a list, dict, set or
+ even the lines from a file or the nodes from another graph).
+
+ >>> G.add_nodes_from([2, 3])
+ >>> G.add_nodes_from(range(100, 110))
+ >>> H = nx.path_graph(10)
+ >>> G.add_nodes_from(H)
+
+ In addition to strings and integers any hashable Python object
+ (except None) can represent a node, e.g. a customized node object,
+ or even another Graph.
+
+ >>> G.add_node(H)
+
+ **Edges:**
+
+ G can also be grown by adding edges.
+
+ Add one edge,
+
+ >>> key = G.add_edge(1, 2)
+
+ a list of edges,
+
+ >>> keys = G.add_edges_from([(1, 2), (1, 3)])
+
+ or a collection of edges,
+
+ >>> keys = G.add_edges_from(H.edges)
+
+ If some edges connect nodes not yet in the graph, the nodes
+ are added automatically. If an edge already exists, an additional
+ edge is created and stored using a key to identify the edge.
+ By default the key is the lowest unused integer.
+
+ >>> keys = G.add_edges_from([(4, 5, dict(route=282)), (4, 5, dict(route=37))])
+ >>> G[4]
+ AdjacencyView({5: {0: {}, 1: {'route': 282}, 2: {'route': 37}}})
+
+ **Attributes:**
+
+ Each graph, node, and edge can hold key/value attribute pairs
+ in an associated attribute dictionary (the keys must be hashable).
+ By default these are empty, but can be added or changed using
+ add_edge, add_node or direct manipulation of the attribute
+ dictionaries named graph, node and edge respectively.
+
+ >>> G = nx.MultiDiGraph(day="Friday")
+ >>> G.graph
+ {'day': 'Friday'}
+
+ Add node attributes using add_node(), add_nodes_from() or G.nodes
+
+ >>> G.add_node(1, time="5pm")
+ >>> G.add_nodes_from([3], time="2pm")
+ >>> G.nodes[1]
+ {'time': '5pm'}
+ >>> G.nodes[1]["room"] = 714
+ >>> del G.nodes[1]["room"] # remove attribute
+ >>> list(G.nodes(data=True))
+ [(1, {'time': '5pm'}), (3, {'time': '2pm'})]
+
+ Add edge attributes using add_edge(), add_edges_from(), subscript
+ notation, or G.edges.
+
+ >>> key = G.add_edge(1, 2, weight=4.7)
+ >>> keys = G.add_edges_from([(3, 4), (4, 5)], color="red")
+ >>> keys = G.add_edges_from([(1, 2, {"color": "blue"}), (2, 3, {"weight": 8})])
+ >>> G[1][2][0]["weight"] = 4.7
+ >>> G.edges[1, 2, 0]["weight"] = 4
+
+ Warning: we protect the graph data structure by making `G.edges[1,
+ 2, 0]` a read-only dict-like structure. However, you can assign to
+ attributes in e.g. `G.edges[1, 2, 0]`. Thus, use 2 sets of brackets
+ to add/change data attributes: `G.edges[1, 2, 0]['weight'] = 4`
+ (for multigraphs the edge key is required: `MG.edges[u, v,
+ key][name] = value`).
+
+ **Shortcuts:**
+
+ Many common graph features allow python syntax to speed reporting.
+
+ >>> 1 in G # check if node in graph
+ True
+ >>> [n for n in G if n < 3] # iterate through nodes
+ [1, 2]
+ >>> len(G) # number of nodes in graph
+ 5
+ >>> G[1] # adjacency dict-like view mapping neighbor -> edge key -> edge attributes
+ AdjacencyView({2: {0: {'weight': 4}, 1: {'color': 'blue'}}})
+
+ Often the best way to traverse all edges of a graph is via the neighbors.
+ The neighbors are available as an adjacency-view `G.adj` object or via
+ the method `G.adjacency()`.
+
+ >>> for n, nbrsdict in G.adjacency():
+ ... for nbr, keydict in nbrsdict.items():
+ ... for key, eattr in keydict.items():
+ ... if "weight" in eattr:
+ ... # Do something useful with the edges
+ ... pass
+
+ But the edges() method is often more convenient:
+
+ >>> for u, v, keys, weight in G.edges(data="weight", keys=True):
+ ... if weight is not None:
+ ... # Do something useful with the edges
+ ... pass
+
+ **Reporting:**
+
+ Simple graph information is obtained using methods and object-attributes.
+ Reporting usually provides views instead of containers to reduce memory
+ usage. The views update as the graph is updated similarly to dict-views.
+ The objects `nodes`, `edges` and `adj` provide access to data attributes
+ via lookup (e.g. `nodes[n]`, `edges[u, v, k]`, `adj[u][v]`) and iteration
+ (e.g. `nodes.items()`, `nodes.data('color')`,
+ `nodes.data('color', default='blue')` and similarly for `edges`)
+ Views exist for `nodes`, `edges`, `neighbors()`/`adj` and `degree`.
+
+ For details on these and other miscellaneous methods, see below.
+
+ **Subclasses (Advanced):**
+
+ The MultiDiGraph class uses a dict-of-dict-of-dict-of-dict structure.
+ The outer dict (node_dict) holds adjacency information keyed by node.
+ The next dict (adjlist_dict) represents the adjacency information
+ and holds edge_key dicts keyed by neighbor. The edge_key dict holds
+ each edge_attr dict keyed by edge key. The inner dict
+ (edge_attr_dict) represents the edge data and holds edge attribute
+ values keyed by attribute names.
+
+ Each of these four dicts in the dict-of-dict-of-dict-of-dict
+ structure can be replaced by a user defined dict-like object.
+ In general, the dict-like features should be maintained but
+ extra features can be added. To replace one of the dicts create
+ a new graph class by changing the class(!) variable holding the
+ factory for that dict-like structure. The variable names are
+ node_dict_factory, node_attr_dict_factory, adjlist_inner_dict_factory,
+ adjlist_outer_dict_factory, edge_key_dict_factory, edge_attr_dict_factory
+ and graph_attr_dict_factory.
+
+ node_dict_factory : function, (default: dict)
+ Factory function to be used to create the dict containing node
+ attributes, keyed by node id.
+ It should require no arguments and return a dict-like object
+
+ node_attr_dict_factory: function, (default: dict)
+ Factory function to be used to create the node attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object
+
+ adjlist_outer_dict_factory : function, (default: dict)
+ Factory function to be used to create the outer-most dict
+ in the data structure that holds adjacency info keyed by node.
+ It should require no arguments and return a dict-like object.
+
+ adjlist_inner_dict_factory : function, (default: dict)
+ Factory function to be used to create the adjacency list
+ dict which holds multiedge key dicts keyed by neighbor.
+ It should require no arguments and return a dict-like object.
+
+ edge_key_dict_factory : function, (default: dict)
+ Factory function to be used to create the edge key dict
+ which holds edge data keyed by edge key.
+ It should require no arguments and return a dict-like object.
+
+ edge_attr_dict_factory : function, (default: dict)
+ Factory function to be used to create the edge attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object.
+
+ graph_attr_dict_factory : function, (default: dict)
+ Factory function to be used to create the graph attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object.
+
+ Typically, if your extension doesn't impact the data structure all
+ methods will inherited without issue except: `to_directed/to_undirected`.
+ By default these methods create a DiGraph/Graph class and you probably
+ want them to create your extension of a DiGraph/Graph. To facilitate
+ this we define two class variables that you can set in your subclass.
+
+ to_directed_class : callable, (default: DiGraph or MultiDiGraph)
+ Class to create a new graph structure in the `to_directed` method.
+ If `None`, a NetworkX class (DiGraph or MultiDiGraph) is used.
+
+ to_undirected_class : callable, (default: Graph or MultiGraph)
+ Class to create a new graph structure in the `to_undirected` method.
+ If `None`, a NetworkX class (Graph or MultiGraph) is used.
+
+ **Subclassing Example**
+
+ Create a low memory graph class that effectively disallows edge
+ attributes by using a single attribute dict for all edges.
+ This reduces the memory used, but you lose edge attributes.
+
+ >>> class ThinGraph(nx.Graph):
+ ... all_edge_dict = {"weight": 1}
+ ...
+ ... def single_edge_dict(self):
+ ... return self.all_edge_dict
+ ...
+ ... edge_attr_dict_factory = single_edge_dict
+ >>> G = ThinGraph()
+ >>> G.add_edge(2, 1)
+ >>> G[2][1]
+ {'weight': 1}
+ >>> G.add_edge(2, 2)
+ >>> G[2][1] is G[2][2]
+ True
+ """
+
+ # node_dict_factory = dict # already assigned in Graph
+ # adjlist_outer_dict_factory = dict
+ # adjlist_inner_dict_factory = dict
+ edge_key_dict_factory = dict
+ # edge_attr_dict_factory = dict
+
+ def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr):
+ """Initialize a graph with edges, name, or graph attributes.
+
+ Parameters
+ ----------
+ incoming_graph_data : input graph
+ Data to initialize graph. If incoming_graph_data=None (default)
+ an empty graph is created. The data can be an edge list, or any
+ NetworkX graph object. If the corresponding optional Python
+ packages are installed the data can also be a 2D NumPy array, a
+ SciPy sparse array, 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.
+
+ See Also
+ --------
+ convert
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G = nx.Graph(name="my graph")
+ >>> e = [(1, 2), (2, 3), (3, 4)] # list of edges
+ >>> G = nx.Graph(e)
+
+ Arbitrary graph attribute pairs (key=value) may be assigned
+
+ >>> G = nx.Graph(e, day="Friday")
+ >>> G.graph
+ {'day': 'Friday'}
+
+ """
+ # 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 err:
+ if multigraph_input is True:
+ raise nx.NetworkXError(
+ f"converting multigraph_input raised:\n{type(err)}: {err}"
+ )
+ DiGraph.__init__(self, incoming_graph_data, **attr)
+ else:
+ DiGraph.__init__(self, incoming_graph_data, **attr)
+
+ @cached_property
+ def adj(self):
+ """Graph adjacency object holding the neighbors of each node.
+
+ This object is a read-only dict-like structure with node keys
+ and neighbor-dict values. The neighbor-dict is keyed by neighbor
+ to the edgekey-dict. So `G.adj[3][2][0]['color'] = 'blue'` sets
+ the color of the edge `(3, 2, 0)` to `"blue"`.
+
+ Iterating over G.adj behaves like a dict. Useful idioms include
+ `for nbr, datadict in G.adj[n].items():`.
+
+ The neighbor information is also provided by subscripting the graph.
+ So `for nbr, foovalue in G[node].data('foo', default=1):` works.
+
+ For directed graphs, `G.adj` holds outgoing (successor) info.
+ """
+ return MultiAdjacencyView(self._succ)
+
+ @cached_property
+ def succ(self):
+ """Graph adjacency object holding the successors of each node.
+
+ This object is a read-only dict-like structure with node keys
+ and neighbor-dict values. The neighbor-dict is keyed by neighbor
+ to the edgekey-dict. So `G.adj[3][2][0]['color'] = 'blue'` sets
+ the color of the edge `(3, 2, 0)` to `"blue"`.
+
+ Iterating over G.adj behaves like a dict. Useful idioms include
+ `for nbr, datadict in G.adj[n].items():`.
+
+ The neighbor information is also provided by subscripting the graph.
+ So `for nbr, foovalue in G[node].data('foo', default=1):` works.
+
+ For directed graphs, `G.succ` is identical to `G.adj`.
+ """
+ return MultiAdjacencyView(self._succ)
+
+ @cached_property
+ def pred(self):
+ """Graph adjacency object holding the predecessors of each node.
+
+ This object is a read-only dict-like structure with node keys
+ and neighbor-dict values. The neighbor-dict is keyed by neighbor
+ to the edgekey-dict. So `G.adj[3][2][0]['color'] = 'blue'` sets
+ the color of the edge `(3, 2, 0)` to `"blue"`.
+
+ Iterating over G.adj behaves like a dict. Useful idioms include
+ `for nbr, datadict in G.adj[n].items():`.
+ """
+ return MultiAdjacencyView(self._pred)
+
+ def add_edge(self, u_for_edge, v_for_edge, key=None, **attr):
+ """Add an edge between u and v.
+
+ The nodes u and v will be automatically added if they are
+ not already in the graph.
+
+ Edge attributes can be specified with keywords or by directly
+ accessing the edge's attribute dictionary. See examples below.
+
+ Parameters
+ ----------
+ u_for_edge, v_for_edge : nodes
+ Nodes can be, for example, strings or numbers.
+ Nodes must be hashable (and not None) Python objects.
+ key : hashable identifier, optional (default=lowest unused integer)
+ Used to distinguish multiedges between a pair of nodes.
+ attr : keyword arguments, optional
+ Edge data (or labels or objects) can be assigned using
+ keyword arguments.
+
+ Returns
+ -------
+ The edge key assigned to the edge.
+
+ See Also
+ --------
+ add_edges_from : add a collection of edges
+
+ Notes
+ -----
+ To replace/update edge data, use the optional key argument
+ to identify a unique edge. Otherwise a new edge will be created.
+
+ NetworkX algorithms designed for weighted graphs cannot use
+ multigraphs directly because it is not clear how to handle
+ multiedge weights. Convert to Graph using edge attribute
+ 'weight' to enable weighted graph algorithms.
+
+ Default keys are generated using the method `new_edge_key()`.
+ This method can be overridden by subclassing the base class and
+ providing a custom `new_edge_key()` method.
+
+ Examples
+ --------
+ The following all add the edge e=(1, 2) to graph G:
+
+ >>> G = nx.MultiDiGraph()
+ >>> e = (1, 2)
+ >>> key = G.add_edge(1, 2) # explicit two-node form
+ >>> G.add_edge(*e) # single edge as tuple of two nodes
+ 1
+ >>> G.add_edges_from([(1, 2)]) # add edges from iterable container
+ [2]
+
+ Associate data to edges using keywords:
+
+ >>> key = G.add_edge(1, 2, weight=3)
+ >>> key = G.add_edge(1, 2, key=0, weight=4) # update data for key=0
+ >>> key = G.add_edge(1, 3, weight=7, capacity=15, length=342.7)
+
+ For non-string attribute keys, use subscript notation.
+
+ >>> ekey = G.add_edge(1, 2)
+ >>> G[1][2][0].update({0: 5})
+ >>> G.edges[1, 2, 0].update({0: 5})
+ """
+ u, v = u_for_edge, v_for_edge
+ # add nodes
+ if u not in self._succ:
+ if u is None:
+ raise ValueError("None cannot be a node")
+ self._succ[u] = self.adjlist_inner_dict_factory()
+ self._pred[u] = self.adjlist_inner_dict_factory()
+ self._node[u] = self.node_attr_dict_factory()
+ if v not in self._succ:
+ if v is None:
+ raise ValueError("None cannot be a node")
+ self._succ[v] = self.adjlist_inner_dict_factory()
+ self._pred[v] = self.adjlist_inner_dict_factory()
+ self._node[v] = self.node_attr_dict_factory()
+ if key is None:
+ key = self.new_edge_key(u, v)
+ if v in self._succ[u]:
+ keydict = self._adj[u][v]
+ datadict = keydict.get(key, self.edge_attr_dict_factory())
+ datadict.update(attr)
+ keydict[key] = datadict
+ else:
+ # selfloops work this way without special treatment
+ datadict = self.edge_attr_dict_factory()
+ datadict.update(attr)
+ keydict = self.edge_key_dict_factory()
+ keydict[key] = datadict
+ self._succ[u][v] = keydict
+ self._pred[v][u] = keydict
+ nx._clear_cache(self)
+ return key
+
+ def remove_edge(self, u, v, key=None):
+ """Remove an edge between u and v.
+
+ Parameters
+ ----------
+ u, v : nodes
+ Remove an edge between nodes u and v.
+ key : hashable identifier, optional (default=None)
+ Used to distinguish multiple edges between a pair of nodes.
+ If None, remove a single edge between u and v. If there are
+ multiple edges, removes the last edge added in terms of
+ insertion order.
+
+ Raises
+ ------
+ NetworkXError
+ If there is not an edge between u and v, or
+ if there is no edge with the specified key.
+
+ See Also
+ --------
+ remove_edges_from : remove a collection of edges
+
+ Examples
+ --------
+ >>> G = nx.MultiDiGraph()
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.remove_edge(0, 1)
+ >>> e = (1, 2)
+ >>> G.remove_edge(*e) # unpacks e from an edge tuple
+
+ For multiple edges
+
+ >>> G = nx.MultiDiGraph()
+ >>> G.add_edges_from([(1, 2), (1, 2), (1, 2)]) # key_list returned
+ [0, 1, 2]
+
+ When ``key=None`` (the default), edges are removed in the opposite
+ order that they were added:
+
+ >>> G.remove_edge(1, 2)
+ >>> G.edges(keys=True)
+ OutMultiEdgeView([(1, 2, 0), (1, 2, 1)])
+
+ For edges with keys
+
+ >>> G = nx.MultiDiGraph()
+ >>> G.add_edge(1, 2, key="first")
+ 'first'
+ >>> G.add_edge(1, 2, key="second")
+ 'second'
+ >>> G.remove_edge(1, 2, key="first")
+ >>> G.edges(keys=True)
+ OutMultiEdgeView([(1, 2, 'second')])
+
+ """
+ try:
+ d = self._adj[u][v]
+ except KeyError as err:
+ raise NetworkXError(f"The edge {u}-{v} is not in the graph.") from err
+ # remove the edge with specified data
+ if key is None:
+ d.popitem()
+ else:
+ try:
+ del d[key]
+ except KeyError as err:
+ msg = f"The edge {u}-{v} with key {key} is not in the graph."
+ raise NetworkXError(msg) from err
+ if len(d) == 0:
+ # remove the key entries if last edge
+ del self._succ[u][v]
+ del self._pred[v][u]
+ nx._clear_cache(self)
+
+ @cached_property
+ def edges(self):
+ """An OutMultiEdgeView of the Graph as G.edges or G.edges().
+
+ edges(self, nbunch=None, data=False, keys=False, default=None)
+
+ The OutMultiEdgeView provides set-like operations on the edge-tuples
+ as well as edge attribute lookup. When called, it also provides
+ an EdgeDataView object which allows control of access to edge
+ attributes (but does not provide set-like operations).
+ Hence, ``G.edges[u, v, k]['color']`` provides the value of the color
+ attribute for the edge from ``u`` to ``v`` with key ``k`` while
+ ``for (u, v, k, c) in G.edges(data='color', default='red', keys=True):``
+ iterates through all the edges yielding the color attribute with
+ default `'red'` if no color attribute exists.
+
+ Edges are returned as tuples with optional data and keys
+ in the order (node, neighbor, key, data). If ``keys=True`` is not
+ provided, the tuples will just be (node, neighbor, data), but
+ multiple tuples with the same node and neighbor will be
+ generated when multiple edges between two nodes exist.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges from these nodes.
+ data : string or bool, optional (default=False)
+ The edge attribute returned in 3-tuple (u, v, ddict[data]).
+ If True, return edge attribute dict in 3-tuple (u, v, ddict).
+ If False, return 2-tuple (u, v).
+ keys : bool, optional (default=False)
+ If True, return edge keys with each edge, creating (u, v, k,
+ d) tuples when data is also requested (the default) and (u,
+ v, k) tuples when data is not requested.
+ default : value, optional (default=None)
+ Value used for edges that don't have the requested attribute.
+ Only relevant if data is not True or False.
+
+ Returns
+ -------
+ edges : OutMultiEdgeView
+ A view of edge attributes, usually it iterates over (u, v)
+ (u, v, k) or (u, v, k, d) tuples of edges, but can also be
+ used for attribute lookup as ``edges[u, v, k]['foo']``.
+
+ Notes
+ -----
+ Nodes in nbunch that are not in the graph will be (quietly) ignored.
+ For directed graphs this returns the out-edges.
+
+ Examples
+ --------
+ >>> G = nx.MultiDiGraph()
+ >>> nx.add_path(G, [0, 1, 2])
+ >>> key = G.add_edge(2, 3, weight=5)
+ >>> key2 = G.add_edge(1, 2) # second edge between these nodes
+ >>> [e for e in G.edges()]
+ [(0, 1), (1, 2), (1, 2), (2, 3)]
+ >>> list(G.edges(data=True)) # default data is {} (empty dict)
+ [(0, 1, {}), (1, 2, {}), (1, 2, {}), (2, 3, {'weight': 5})]
+ >>> list(G.edges(data="weight", default=1))
+ [(0, 1, 1), (1, 2, 1), (1, 2, 1), (2, 3, 5)]
+ >>> list(G.edges(keys=True)) # default keys are integers
+ [(0, 1, 0), (1, 2, 0), (1, 2, 1), (2, 3, 0)]
+ >>> list(G.edges(data=True, keys=True))
+ [(0, 1, 0, {}), (1, 2, 0, {}), (1, 2, 1, {}), (2, 3, 0, {'weight': 5})]
+ >>> list(G.edges(data="weight", default=1, keys=True))
+ [(0, 1, 0, 1), (1, 2, 0, 1), (1, 2, 1, 1), (2, 3, 0, 5)]
+ >>> list(G.edges([0, 2]))
+ [(0, 1), (2, 3)]
+ >>> list(G.edges(0))
+ [(0, 1)]
+ >>> list(G.edges(1))
+ [(1, 2), (1, 2)]
+
+ See Also
+ --------
+ in_edges, out_edges
+ """
+ return OutMultiEdgeView(self)
+
+ # alias out_edges to edges
+ @cached_property
+ def out_edges(self):
+ return OutMultiEdgeView(self)
+
+ out_edges.__doc__ = edges.__doc__
+
+ @cached_property
+ def in_edges(self):
+ """A view of the in edges of the graph as G.in_edges or G.in_edges().
+
+ in_edges(self, nbunch=None, data=False, keys=False, default=None)
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+ data : string or bool, optional (default=False)
+ The edge attribute returned in 3-tuple (u, v, ddict[data]).
+ If True, return edge attribute dict in 3-tuple (u, v, ddict).
+ If False, return 2-tuple (u, v).
+ keys : bool, optional (default=False)
+ If True, return edge keys with each edge, creating 3-tuples
+ (u, v, k) or with data, 4-tuples (u, v, k, d).
+ default : value, optional (default=None)
+ Value used for edges that don't have the requested attribute.
+ Only relevant if data is not True or False.
+
+ Returns
+ -------
+ in_edges : InMultiEdgeView or InMultiEdgeDataView
+ A view of edge attributes, usually it iterates over (u, v)
+ or (u, v, k) or (u, v, k, d) tuples of edges, but can also be
+ used for attribute lookup as `edges[u, v, k]['foo']`.
+
+ See Also
+ --------
+ edges
+ """
+ return InMultiEdgeView(self)
+
+ @cached_property
+ def degree(self):
+ """A DegreeView for the Graph as G.degree or G.degree().
+
+ The node degree is the number of edges adjacent to the node.
+ The weighted node degree is the sum of the edge weights for
+ edges incident to that node.
+
+ This object provides an iterator for (node, degree) as well as
+ lookup for the degree for a single node.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+
+ weight : string or None, optional (default=None)
+ The name of an edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ Returns
+ -------
+ DiMultiDegreeView or int
+ If multiple nodes are requested (the default), returns a `DiMultiDegreeView`
+ mapping nodes to their degree.
+ If a single node is requested, returns the degree of the node as an integer.
+
+ See Also
+ --------
+ out_degree, in_degree
+
+ Examples
+ --------
+ >>> G = nx.MultiDiGraph()
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.degree(0) # node 0 with degree 1
+ 1
+ >>> list(G.degree([0, 1, 2]))
+ [(0, 1), (1, 2), (2, 2)]
+ >>> G.add_edge(0, 1) # parallel edge
+ 1
+ >>> list(G.degree([0, 1, 2])) # parallel edges are counted
+ [(0, 2), (1, 3), (2, 2)]
+
+ """
+ return DiMultiDegreeView(self)
+
+ @cached_property
+ def in_degree(self):
+ """A DegreeView for (node, in_degree) or in_degree for single node.
+
+ The node in-degree is the number of edges pointing into the node.
+ The weighted node degree is the sum of the edge weights for
+ edges incident to that node.
+
+ This object provides an iterator for (node, degree) as well as
+ lookup for the degree for a single node.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+
+ weight : string or None, optional (default=None)
+ The edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ Returns
+ -------
+ If a single node is requested
+ deg : int
+ Degree of the node
+
+ OR if multiple nodes are requested
+ nd_iter : iterator
+ The iterator returns two-tuples of (node, in-degree).
+
+ See Also
+ --------
+ degree, out_degree
+
+ Examples
+ --------
+ >>> G = nx.MultiDiGraph()
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.in_degree(0) # node 0 with degree 0
+ 0
+ >>> list(G.in_degree([0, 1, 2]))
+ [(0, 0), (1, 1), (2, 1)]
+ >>> G.add_edge(0, 1) # parallel edge
+ 1
+ >>> list(G.in_degree([0, 1, 2])) # parallel edges counted
+ [(0, 0), (1, 2), (2, 1)]
+
+ """
+ return InMultiDegreeView(self)
+
+ @cached_property
+ def out_degree(self):
+ """Returns an iterator for (node, out-degree) or out-degree for single node.
+
+ out_degree(self, nbunch=None, weight=None)
+
+ The node out-degree is the number of edges pointing out of the node.
+ This function returns the out-degree for a single node or an iterator
+ for a bunch of nodes or if nothing is passed as argument.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+
+ weight : string or None, optional (default=None)
+ The edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights.
+
+ Returns
+ -------
+ If a single node is requested
+ deg : int
+ Degree of the node
+
+ OR if multiple nodes are requested
+ nd_iter : iterator
+ The iterator returns two-tuples of (node, out-degree).
+
+ See Also
+ --------
+ degree, in_degree
+
+ Examples
+ --------
+ >>> G = nx.MultiDiGraph()
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.out_degree(0) # node 0 with degree 1
+ 1
+ >>> list(G.out_degree([0, 1, 2]))
+ [(0, 1), (1, 1), (2, 1)]
+ >>> G.add_edge(0, 1) # parallel edge
+ 1
+ >>> list(G.out_degree([0, 1, 2])) # counts parallel edges
+ [(0, 2), (1, 1), (2, 1)]
+
+ """
+ return OutMultiDegreeView(self)
+
+ def is_multigraph(self):
+ """Returns True if graph is a multigraph, False otherwise."""
+ return True
+
+ def is_directed(self):
+ """Returns True if graph is directed, False otherwise."""
+ return True
+
+ def to_undirected(self, reciprocal=False, as_view=False):
+ """Returns an undirected representation of the digraph.
+
+ Parameters
+ ----------
+ reciprocal : bool (optional)
+ If True only keep edges that appear in both directions
+ in the original digraph.
+ as_view : bool (optional, default=False)
+ If True return an undirected view of the original directed graph.
+
+ Returns
+ -------
+ G : MultiGraph
+ An undirected graph with the same name and nodes and
+ with edge (u, v, data) if either (u, v, data) or (v, u, data)
+ is in the digraph. If both edges exist in digraph and
+ their edge data is different, only one edge is created
+ with an arbitrary choice of which edge data to use.
+ You must check and correct for this manually if desired.
+
+ See Also
+ --------
+ MultiGraph, copy, add_edge, add_edges_from
+
+ Notes
+ -----
+ This returns a "deepcopy" of the edge, node, and
+ graph attributes which attempts to completely copy
+ all of the data and references.
+
+ This is in contrast to the similar D=MultiDiGraph(G) which
+ returns a shallow copy of the data.
+
+ See the Python copy module for more information on shallow
+ and deep copies, https://docs.python.org/3/library/copy.html.
+
+ Warning: If you have subclassed MultiDiGraph to use dict-like
+ objects in the data structure, those changes do not transfer
+ to the MultiGraph created by this method.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(2) # or MultiGraph, etc
+ >>> H = G.to_directed()
+ >>> list(H.edges)
+ [(0, 1), (1, 0)]
+ >>> G2 = H.to_undirected()
+ >>> list(G2.edges)
+ [(0, 1)]
+ """
+ graph_class = self.to_undirected_class()
+ if as_view is True:
+ return nx.graphviews.generic_graph_view(self, graph_class)
+ # deepcopy when not a view
+ G = graph_class()
+ G.graph.update(deepcopy(self.graph))
+ G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
+ if reciprocal is True:
+ G.add_edges_from(
+ (u, v, key, deepcopy(data))
+ for u, nbrs in self._adj.items()
+ for v, keydict in nbrs.items()
+ for key, data in keydict.items()
+ if v in self._pred[u] and key in self._pred[u][v]
+ )
+ else:
+ G.add_edges_from(
+ (u, v, key, deepcopy(data))
+ for u, nbrs in self._adj.items()
+ for v, keydict in nbrs.items()
+ for key, data in keydict.items()
+ )
+ return G
+
+ def reverse(self, copy=True):
+ """Returns the reverse of the graph.
+
+ The reverse is a graph with the same nodes and edges
+ but with the directions of the edges reversed.
+
+ Parameters
+ ----------
+ copy : bool optional (default=True)
+ If True, return a new DiGraph holding the reversed edges.
+ If False, the reverse graph is created using a view of
+ the original graph.
+ """
+ if copy:
+ H = self.__class__()
+ H.graph.update(deepcopy(self.graph))
+ H.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
+ H.add_edges_from(
+ (v, u, k, deepcopy(d))
+ for u, v, k, d in self.edges(keys=True, data=True)
+ )
+ return H
+ return nx.reverse_view(self)
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/multigraph.py b/.venv/lib/python3.12/site-packages/networkx/classes/multigraph.py
new file mode 100644
index 00000000..0e3f1aec
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/multigraph.py
@@ -0,0 +1,1283 @@
+"""Base class for MultiGraph."""
+
+from copy import deepcopy
+from functools import cached_property
+
+import networkx as nx
+from networkx import NetworkXError, convert
+from networkx.classes.coreviews import MultiAdjacencyView
+from networkx.classes.graph import Graph
+from networkx.classes.reportviews import MultiDegreeView, MultiEdgeView
+
+__all__ = ["MultiGraph"]
+
+
+class MultiGraph(Graph):
+ """
+ An undirected graph class that can store multiedges.
+
+ Multiedges are multiple edges between two nodes. Each edge
+ can hold optional data or attributes.
+
+ A MultiGraph holds undirected edges. Self loops are allowed.
+
+ Nodes can be arbitrary (hashable) Python objects with optional
+ key/value attributes. By convention `None` is not used as a node.
+
+ Edges are represented as links between nodes with optional
+ key/value attributes, in a MultiGraph each edge has a key to
+ distinguish between multiple edges that have the same source and
+ destination nodes.
+
+ Parameters
+ ----------
+ incoming_graph_data : input graph (optional, default: None)
+ Data to initialize graph. If None (default) an empty
+ graph is created. The data can be any format that is supported
+ by the to_networkx_graph() function, currently including edge list,
+ dict of dicts, dict of lists, NetworkX graph, 2D NumPy array,
+ SciPy sparse array, 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.
+
+ See Also
+ --------
+ Graph
+ DiGraph
+ MultiDiGraph
+
+ Examples
+ --------
+ Create an empty graph structure (a "null graph") with no nodes and
+ no edges.
+
+ >>> G = nx.MultiGraph()
+
+ G can be grown in several ways.
+
+ **Nodes:**
+
+ Add one node at a time:
+
+ >>> G.add_node(1)
+
+ Add the nodes from any container (a list, dict, set or
+ even the lines from a file or the nodes from another graph).
+
+ >>> G.add_nodes_from([2, 3])
+ >>> G.add_nodes_from(range(100, 110))
+ >>> H = nx.path_graph(10)
+ >>> G.add_nodes_from(H)
+
+ In addition to strings and integers any hashable Python object
+ (except None) can represent a node, e.g. a customized node object,
+ or even another Graph.
+
+ >>> G.add_node(H)
+
+ **Edges:**
+
+ G can also be grown by adding edges.
+
+ Add one edge,
+
+ >>> key = G.add_edge(1, 2)
+
+ a list of edges,
+
+ >>> keys = G.add_edges_from([(1, 2), (1, 3)])
+
+ or a collection of edges,
+
+ >>> keys = G.add_edges_from(H.edges)
+
+ If some edges connect nodes not yet in the graph, the nodes
+ are added automatically. If an edge already exists, an additional
+ edge is created and stored using a key to identify the edge.
+ By default the key is the lowest unused integer.
+
+ >>> keys = G.add_edges_from([(4, 5, {"route": 28}), (4, 5, {"route": 37})])
+ >>> G[4]
+ AdjacencyView({3: {0: {}}, 5: {0: {}, 1: {'route': 28}, 2: {'route': 37}}})
+
+ **Attributes:**
+
+ Each graph, node, and edge can hold key/value attribute pairs
+ in an associated attribute dictionary (the keys must be hashable).
+ By default these are empty, but can be added or changed using
+ add_edge, add_node or direct manipulation of the attribute
+ dictionaries named graph, node and edge respectively.
+
+ >>> G = nx.MultiGraph(day="Friday")
+ >>> G.graph
+ {'day': 'Friday'}
+
+ Add node attributes using add_node(), add_nodes_from() or G.nodes
+
+ >>> G.add_node(1, time="5pm")
+ >>> G.add_nodes_from([3], time="2pm")
+ >>> G.nodes[1]
+ {'time': '5pm'}
+ >>> G.nodes[1]["room"] = 714
+ >>> del G.nodes[1]["room"] # remove attribute
+ >>> list(G.nodes(data=True))
+ [(1, {'time': '5pm'}), (3, {'time': '2pm'})]
+
+ Add edge attributes using add_edge(), add_edges_from(), subscript
+ notation, or G.edges.
+
+ >>> key = G.add_edge(1, 2, weight=4.7)
+ >>> keys = G.add_edges_from([(3, 4), (4, 5)], color="red")
+ >>> keys = G.add_edges_from([(1, 2, {"color": "blue"}), (2, 3, {"weight": 8})])
+ >>> G[1][2][0]["weight"] = 4.7
+ >>> G.edges[1, 2, 0]["weight"] = 4
+
+ Warning: we protect the graph data structure by making `G.edges[1,
+ 2, 0]` a read-only dict-like structure. However, you can assign to
+ attributes in e.g. `G.edges[1, 2, 0]`. Thus, use 2 sets of brackets
+ to add/change data attributes: `G.edges[1, 2, 0]['weight'] = 4`.
+
+ **Shortcuts:**
+
+ Many common graph features allow python syntax to speed reporting.
+
+ >>> 1 in G # check if node in graph
+ True
+ >>> [n for n in G if n < 3] # iterate through nodes
+ [1, 2]
+ >>> len(G) # number of nodes in graph
+ 5
+ >>> G[1] # adjacency dict-like view mapping neighbor -> edge key -> edge attributes
+ AdjacencyView({2: {0: {'weight': 4}, 1: {'color': 'blue'}}})
+
+ Often the best way to traverse all edges of a graph is via the neighbors.
+ The neighbors are reported as an adjacency-dict `G.adj` or `G.adjacency()`.
+
+ >>> for n, nbrsdict in G.adjacency():
+ ... for nbr, keydict in nbrsdict.items():
+ ... for key, eattr in keydict.items():
+ ... if "weight" in eattr:
+ ... # Do something useful with the edges
+ ... pass
+
+ But the edges() method is often more convenient:
+
+ >>> for u, v, keys, weight in G.edges(data="weight", keys=True):
+ ... if weight is not None:
+ ... # Do something useful with the edges
+ ... pass
+
+ **Reporting:**
+
+ Simple graph information is obtained using methods and object-attributes.
+ Reporting usually provides views instead of containers to reduce memory
+ usage. The views update as the graph is updated similarly to dict-views.
+ The objects `nodes`, `edges` and `adj` provide access to data attributes
+ via lookup (e.g. `nodes[n]`, `edges[u, v, k]`, `adj[u][v]`) and iteration
+ (e.g. `nodes.items()`, `nodes.data('color')`,
+ `nodes.data('color', default='blue')` and similarly for `edges`)
+ Views exist for `nodes`, `edges`, `neighbors()`/`adj` and `degree`.
+
+ For details on these and other miscellaneous methods, see below.
+
+ **Subclasses (Advanced):**
+
+ The MultiGraph class uses a dict-of-dict-of-dict-of-dict data structure.
+ The outer dict (node_dict) holds adjacency information keyed by node.
+ The next dict (adjlist_dict) represents the adjacency information
+ and holds edge_key dicts keyed by neighbor. The edge_key dict holds
+ each edge_attr dict keyed by edge key. The inner dict
+ (edge_attr_dict) represents the edge data and holds edge attribute
+ values keyed by attribute names.
+
+ Each of these four dicts in the dict-of-dict-of-dict-of-dict
+ structure can be replaced by a user defined dict-like object.
+ In general, the dict-like features should be maintained but
+ extra features can be added. To replace one of the dicts create
+ a new graph class by changing the class(!) variable holding the
+ factory for that dict-like structure. The variable names are
+ node_dict_factory, node_attr_dict_factory, adjlist_inner_dict_factory,
+ adjlist_outer_dict_factory, edge_key_dict_factory, edge_attr_dict_factory
+ and graph_attr_dict_factory.
+
+ node_dict_factory : function, (default: dict)
+ Factory function to be used to create the dict containing node
+ attributes, keyed by node id.
+ It should require no arguments and return a dict-like object
+
+ node_attr_dict_factory: function, (default: dict)
+ Factory function to be used to create the node attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object
+
+ adjlist_outer_dict_factory : function, (default: dict)
+ Factory function to be used to create the outer-most dict
+ in the data structure that holds adjacency info keyed by node.
+ It should require no arguments and return a dict-like object.
+
+ adjlist_inner_dict_factory : function, (default: dict)
+ Factory function to be used to create the adjacency list
+ dict which holds multiedge key dicts keyed by neighbor.
+ It should require no arguments and return a dict-like object.
+
+ edge_key_dict_factory : function, (default: dict)
+ Factory function to be used to create the edge key dict
+ which holds edge data keyed by edge key.
+ It should require no arguments and return a dict-like object.
+
+ edge_attr_dict_factory : function, (default: dict)
+ Factory function to be used to create the edge attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object.
+
+ graph_attr_dict_factory : function, (default: dict)
+ Factory function to be used to create the graph attribute
+ dict which holds attribute values keyed by attribute name.
+ It should require no arguments and return a dict-like object.
+
+ Typically, if your extension doesn't impact the data structure all
+ methods will inherited without issue except: `to_directed/to_undirected`.
+ By default these methods create a DiGraph/Graph class and you probably
+ want them to create your extension of a DiGraph/Graph. To facilitate
+ this we define two class variables that you can set in your subclass.
+
+ to_directed_class : callable, (default: DiGraph or MultiDiGraph)
+ Class to create a new graph structure in the `to_directed` method.
+ If `None`, a NetworkX class (DiGraph or MultiDiGraph) is used.
+
+ to_undirected_class : callable, (default: Graph or MultiGraph)
+ Class to create a new graph structure in the `to_undirected` method.
+ If `None`, a NetworkX class (Graph or MultiGraph) is used.
+
+ **Subclassing Example**
+
+ Create a low memory graph class that effectively disallows edge
+ attributes by using a single attribute dict for all edges.
+ This reduces the memory used, but you lose edge attributes.
+
+ >>> class ThinGraph(nx.Graph):
+ ... all_edge_dict = {"weight": 1}
+ ...
+ ... def single_edge_dict(self):
+ ... return self.all_edge_dict
+ ...
+ ... edge_attr_dict_factory = single_edge_dict
+ >>> G = ThinGraph()
+ >>> G.add_edge(2, 1)
+ >>> G[2][1]
+ {'weight': 1}
+ >>> G.add_edge(2, 2)
+ >>> G[2][1] is G[2][2]
+ True
+ """
+
+ # node_dict_factory = dict # already assigned in Graph
+ # adjlist_outer_dict_factory = dict
+ # adjlist_inner_dict_factory = dict
+ edge_key_dict_factory = dict
+ # edge_attr_dict_factory = dict
+
+ def to_directed_class(self):
+ """Returns the class to use for empty directed copies.
+
+ If you subclass the base classes, use this to designate
+ what directed class to use for `to_directed()` copies.
+ """
+ return nx.MultiDiGraph
+
+ def to_undirected_class(self):
+ """Returns the class to use for empty undirected copies.
+
+ If you subclass the base classes, use this to designate
+ what directed class to use for `to_directed()` copies.
+ """
+ return MultiGraph
+
+ def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr):
+ """Initialize a graph with edges, name, or graph attributes.
+
+ Parameters
+ ----------
+ incoming_graph_data : input graph
+ Data to initialize graph. If incoming_graph_data=None (default)
+ an empty graph is created. The data can be an edge list, or any
+ NetworkX graph object. If the corresponding optional Python
+ packages are installed the data can also be a 2D NumPy array, a
+ SciPy sparse array, 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.
+
+ See Also
+ --------
+ convert
+
+ Examples
+ --------
+ >>> G = nx.MultiGraph()
+ >>> G = nx.MultiGraph(name="my graph")
+ >>> e = [(1, 2), (1, 2), (2, 3), (3, 4)] # list of edges
+ >>> G = nx.MultiGraph(e)
+
+ Arbitrary graph attribute pairs (key=value) may be assigned
+
+ >>> G = nx.MultiGraph(e, day="Friday")
+ >>> G.graph
+ {'day': 'Friday'}
+
+ """
+ # 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 err:
+ if multigraph_input is True:
+ raise nx.NetworkXError(
+ f"converting multigraph_input raised:\n{type(err)}: {err}"
+ )
+ Graph.__init__(self, incoming_graph_data, **attr)
+ else:
+ Graph.__init__(self, incoming_graph_data, **attr)
+
+ @cached_property
+ def adj(self):
+ """Graph adjacency object holding the neighbors of each node.
+
+ This object is a read-only dict-like structure with node keys
+ and neighbor-dict values. The neighbor-dict is keyed by neighbor
+ to the edgekey-data-dict. So `G.adj[3][2][0]['color'] = 'blue'` sets
+ the color of the edge `(3, 2, 0)` to `"blue"`.
+
+ Iterating over G.adj behaves like a dict. Useful idioms include
+ `for nbr, edgesdict in G.adj[n].items():`.
+
+ The neighbor information is also provided by subscripting the graph.
+
+ Examples
+ --------
+ >>> e = [(1, 2), (1, 2), (1, 3), (3, 4)] # list of edges
+ >>> G = nx.MultiGraph(e)
+ >>> G.edges[1, 2, 0]["weight"] = 3
+ >>> result = set()
+ >>> for edgekey, data in G[1][2].items():
+ ... result.add(data.get("weight", 1))
+ >>> result
+ {1, 3}
+
+ For directed graphs, `G.adj` holds outgoing (successor) info.
+ """
+ return MultiAdjacencyView(self._adj)
+
+ def new_edge_key(self, u, v):
+ """Returns an unused key for edges between nodes `u` and `v`.
+
+ The nodes `u` and `v` do not need to be already in the graph.
+
+ Notes
+ -----
+ In the standard MultiGraph class the new key is the number of existing
+ edges between `u` and `v` (increased if necessary to ensure unused).
+ The first edge will have key 0, then 1, etc. If an edge is removed
+ further new_edge_keys may not be in this order.
+
+ Parameters
+ ----------
+ u, v : nodes
+
+ Returns
+ -------
+ key : int
+ """
+ try:
+ keydict = self._adj[u][v]
+ except KeyError:
+ return 0
+ key = len(keydict)
+ while key in keydict:
+ key += 1
+ return key
+
+ def add_edge(self, u_for_edge, v_for_edge, key=None, **attr):
+ """Add an edge between u and v.
+
+ The nodes u and v will be automatically added if they are
+ not already in the graph.
+
+ Edge attributes can be specified with keywords or by directly
+ accessing the edge's attribute dictionary. See examples below.
+
+ Parameters
+ ----------
+ u_for_edge, v_for_edge : nodes
+ Nodes can be, for example, strings or numbers.
+ Nodes must be hashable (and not None) Python objects.
+ key : hashable identifier, optional (default=lowest unused integer)
+ Used to distinguish multiedges between a pair of nodes.
+ attr : keyword arguments, optional
+ Edge data (or labels or objects) can be assigned using
+ keyword arguments.
+
+ Returns
+ -------
+ The edge key assigned to the edge.
+
+ See Also
+ --------
+ add_edges_from : add a collection of edges
+
+ Notes
+ -----
+ To replace/update edge data, use the optional key argument
+ to identify a unique edge. Otherwise a new edge will be created.
+
+ NetworkX algorithms designed for weighted graphs cannot use
+ multigraphs directly because it is not clear how to handle
+ multiedge weights. Convert to Graph using edge attribute
+ 'weight' to enable weighted graph algorithms.
+
+ Default keys are generated using the method `new_edge_key()`.
+ This method can be overridden by subclassing the base class and
+ providing a custom `new_edge_key()` method.
+
+ Examples
+ --------
+ The following each add an additional edge e=(1, 2) to graph G:
+
+ >>> G = nx.MultiGraph()
+ >>> e = (1, 2)
+ >>> ekey = G.add_edge(1, 2) # explicit two-node form
+ >>> G.add_edge(*e) # single edge as tuple of two nodes
+ 1
+ >>> G.add_edges_from([(1, 2)]) # add edges from iterable container
+ [2]
+
+ Associate data to edges using keywords:
+
+ >>> ekey = G.add_edge(1, 2, weight=3)
+ >>> ekey = G.add_edge(1, 2, key=0, weight=4) # update data for key=0
+ >>> ekey = G.add_edge(1, 3, weight=7, capacity=15, length=342.7)
+
+ For non-string attribute keys, use subscript notation.
+
+ >>> ekey = G.add_edge(1, 2)
+ >>> G[1][2][0].update({0: 5})
+ >>> G.edges[1, 2, 0].update({0: 5})
+ """
+ u, v = u_for_edge, v_for_edge
+ # add nodes
+ if u not in self._adj:
+ if u is None:
+ raise ValueError("None cannot be a node")
+ self._adj[u] = self.adjlist_inner_dict_factory()
+ self._node[u] = self.node_attr_dict_factory()
+ if v not in self._adj:
+ if v is None:
+ raise ValueError("None cannot be a node")
+ self._adj[v] = self.adjlist_inner_dict_factory()
+ self._node[v] = self.node_attr_dict_factory()
+ if key is None:
+ key = self.new_edge_key(u, v)
+ if v in self._adj[u]:
+ keydict = self._adj[u][v]
+ datadict = keydict.get(key, self.edge_attr_dict_factory())
+ datadict.update(attr)
+ keydict[key] = datadict
+ else:
+ # selfloops work this way without special treatment
+ datadict = self.edge_attr_dict_factory()
+ datadict.update(attr)
+ keydict = self.edge_key_dict_factory()
+ keydict[key] = datadict
+ self._adj[u][v] = keydict
+ self._adj[v][u] = keydict
+ nx._clear_cache(self)
+ return key
+
+ def add_edges_from(self, ebunch_to_add, **attr):
+ """Add all the edges in ebunch_to_add.
+
+ Parameters
+ ----------
+ ebunch_to_add : container of edges
+ Each edge given in the container will be added to the
+ graph. The edges can be:
+
+ - 2-tuples (u, v) or
+ - 3-tuples (u, v, d) for an edge data dict d, or
+ - 3-tuples (u, v, k) for not iterable key k, or
+ - 4-tuples (u, v, k, d) for an edge with data and key k
+
+ attr : keyword arguments, optional
+ Edge data (or labels or objects) can be assigned using
+ keyword arguments.
+
+ Returns
+ -------
+ A list of edge keys assigned to the edges in `ebunch`.
+
+ See Also
+ --------
+ add_edge : add a single edge
+ add_weighted_edges_from : convenient way to add weighted edges
+
+ Notes
+ -----
+ Adding the same edge twice has no effect but any edge data
+ will be updated when each duplicate edge is added.
+
+ Edge attributes specified in an ebunch take precedence over
+ attributes specified via keyword arguments.
+
+ Default keys are generated using the method ``new_edge_key()``.
+ This method can be overridden by subclassing the base class and
+ providing a custom ``new_edge_key()`` method.
+
+ When adding edges from an iterator over the graph you are changing,
+ a `RuntimeError` can be raised with message:
+ `RuntimeError: dictionary changed size during iteration`. This
+ happens when the graph's underlying dictionary is modified during
+ iteration. To avoid this error, evaluate the iterator into a separate
+ object, e.g. by using `list(iterator_of_edges)`, and pass this
+ object to `G.add_edges_from`.
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> G.add_edges_from([(0, 1), (1, 2)]) # using a list of edge tuples
+ >>> e = zip(range(0, 3), range(1, 4))
+ >>> G.add_edges_from(e) # Add the path graph 0-1-2-3
+
+ Associate data to edges
+
+ >>> G.add_edges_from([(1, 2), (2, 3)], weight=3)
+ >>> G.add_edges_from([(3, 4), (1, 4)], label="WN2898")
+
+ Evaluate an iterator over a graph if using it to modify the same graph
+
+ >>> G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)])
+ >>> # Grow graph by one new node, adding edges to all existing nodes.
+ >>> # wrong way - will raise RuntimeError
+ >>> # G.add_edges_from(((5, n) for n in G.nodes))
+ >>> # right way - note that there will be no self-edge for node 5
+ >>> assigned_keys = G.add_edges_from(list((5, n) for n in G.nodes))
+ """
+ keylist = []
+ for e in ebunch_to_add:
+ ne = len(e)
+ if ne == 4:
+ u, v, key, dd = e
+ elif ne == 3:
+ u, v, dd = e
+ key = None
+ elif ne == 2:
+ u, v = e
+ dd = {}
+ key = None
+ else:
+ msg = f"Edge tuple {e} must be a 2-tuple, 3-tuple or 4-tuple."
+ raise NetworkXError(msg)
+ ddd = {}
+ ddd.update(attr)
+ try:
+ ddd.update(dd)
+ except (TypeError, ValueError):
+ if ne != 3:
+ raise
+ key = dd # ne == 3 with 3rd value not dict, must be a key
+ key = self.add_edge(u, v, key)
+ self[u][v][key].update(ddd)
+ keylist.append(key)
+ nx._clear_cache(self)
+ return keylist
+
+ def remove_edge(self, u, v, key=None):
+ """Remove an edge between u and v.
+
+ Parameters
+ ----------
+ u, v : nodes
+ Remove an edge between nodes u and v.
+ key : hashable identifier, optional (default=None)
+ Used to distinguish multiple edges between a pair of nodes.
+ If None, remove a single edge between u and v. If there are
+ multiple edges, removes the last edge added in terms of
+ insertion order.
+
+ Raises
+ ------
+ NetworkXError
+ If there is not an edge between u and v, or
+ if there is no edge with the specified key.
+
+ See Also
+ --------
+ remove_edges_from : remove a collection of edges
+
+ Examples
+ --------
+ >>> G = nx.MultiGraph()
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.remove_edge(0, 1)
+ >>> e = (1, 2)
+ >>> G.remove_edge(*e) # unpacks e from an edge tuple
+
+ For multiple edges
+
+ >>> G = nx.MultiGraph() # or MultiDiGraph, etc
+ >>> G.add_edges_from([(1, 2), (1, 2), (1, 2)]) # key_list returned
+ [0, 1, 2]
+
+ When ``key=None`` (the default), edges are removed in the opposite
+ order that they were added:
+
+ >>> G.remove_edge(1, 2)
+ >>> G.edges(keys=True)
+ MultiEdgeView([(1, 2, 0), (1, 2, 1)])
+ >>> G.remove_edge(2, 1) # edges are not directed
+ >>> G.edges(keys=True)
+ MultiEdgeView([(1, 2, 0)])
+
+ For edges with keys
+
+ >>> G = nx.MultiGraph()
+ >>> G.add_edge(1, 2, key="first")
+ 'first'
+ >>> G.add_edge(1, 2, key="second")
+ 'second'
+ >>> G.remove_edge(1, 2, key="first")
+ >>> G.edges(keys=True)
+ MultiEdgeView([(1, 2, 'second')])
+
+ """
+ try:
+ d = self._adj[u][v]
+ except KeyError as err:
+ raise NetworkXError(f"The edge {u}-{v} is not in the graph.") from err
+ # remove the edge with specified data
+ if key is None:
+ d.popitem()
+ else:
+ try:
+ del d[key]
+ except KeyError as err:
+ msg = f"The edge {u}-{v} with key {key} is not in the graph."
+ raise NetworkXError(msg) from err
+ if len(d) == 0:
+ # remove the key entries if last edge
+ del self._adj[u][v]
+ if u != v: # check for selfloop
+ del self._adj[v][u]
+ nx._clear_cache(self)
+
+ def remove_edges_from(self, ebunch):
+ """Remove all edges specified in ebunch.
+
+ Parameters
+ ----------
+ ebunch: list or container of edge tuples
+ Each edge given in the list or container will be removed
+ from the graph. The edges can be:
+
+ - 2-tuples (u, v) A single edge between u and v is removed.
+ - 3-tuples (u, v, key) The edge identified by key is removed.
+ - 4-tuples (u, v, key, data) where data is ignored.
+
+ See Also
+ --------
+ remove_edge : remove a single edge
+
+ Notes
+ -----
+ Will fail silently if an edge in ebunch is not in the graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> ebunch = [(1, 2), (2, 3)]
+ >>> G.remove_edges_from(ebunch)
+
+ Removing multiple copies of edges
+
+ >>> G = nx.MultiGraph()
+ >>> keys = G.add_edges_from([(1, 2), (1, 2), (1, 2)])
+ >>> G.remove_edges_from([(1, 2), (2, 1)]) # edges aren't directed
+ >>> list(G.edges())
+ [(1, 2)]
+ >>> G.remove_edges_from([(1, 2), (1, 2)]) # silently ignore extra copy
+ >>> list(G.edges) # now empty graph
+ []
+
+ When the edge is a 2-tuple ``(u, v)`` but there are multiple edges between
+ u and v in the graph, the most recent edge (in terms of insertion
+ order) is removed.
+
+ >>> G = nx.MultiGraph()
+ >>> for key in ("x", "y", "a"):
+ ... k = G.add_edge(0, 1, key=key)
+ >>> G.edges(keys=True)
+ MultiEdgeView([(0, 1, 'x'), (0, 1, 'y'), (0, 1, 'a')])
+ >>> G.remove_edges_from([(0, 1)])
+ >>> G.edges(keys=True)
+ MultiEdgeView([(0, 1, 'x'), (0, 1, 'y')])
+
+ """
+ for e in ebunch:
+ try:
+ self.remove_edge(*e[:3])
+ except NetworkXError:
+ pass
+ nx._clear_cache(self)
+
+ def has_edge(self, u, v, key=None):
+ """Returns True if the graph has an edge between nodes u and v.
+
+ This is the same as `v in G[u] or key in G[u][v]`
+ without KeyError exceptions.
+
+ Parameters
+ ----------
+ u, v : nodes
+ Nodes can be, for example, strings or numbers.
+
+ key : hashable identifier, optional (default=None)
+ If specified return True only if the edge with
+ key is found.
+
+ Returns
+ -------
+ edge_ind : bool
+ True if edge is in the graph, False otherwise.
+
+ Examples
+ --------
+ Can be called either using two nodes u, v, an edge tuple (u, v),
+ or an edge tuple (u, v, key).
+
+ >>> G = nx.MultiGraph() # or MultiDiGraph
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.has_edge(0, 1) # using two nodes
+ True
+ >>> e = (0, 1)
+ >>> G.has_edge(*e) # e is a 2-tuple (u, v)
+ True
+ >>> G.add_edge(0, 1, key="a")
+ 'a'
+ >>> G.has_edge(0, 1, key="a") # specify key
+ True
+ >>> G.has_edge(1, 0, key="a") # edges aren't directed
+ True
+ >>> e = (0, 1, "a")
+ >>> G.has_edge(*e) # e is a 3-tuple (u, v, 'a')
+ True
+
+ The following syntax are equivalent:
+
+ >>> G.has_edge(0, 1)
+ True
+ >>> 1 in G[0] # though this gives :exc:`KeyError` if 0 not in G
+ True
+ >>> 0 in G[1] # other order; also gives :exc:`KeyError` if 0 not in G
+ True
+
+ """
+ try:
+ if key is None:
+ return v in self._adj[u]
+ else:
+ return key in self._adj[u][v]
+ except KeyError:
+ return False
+
+ @cached_property
+ def edges(self):
+ """Returns an iterator over the edges.
+
+ edges(self, nbunch=None, data=False, keys=False, default=None)
+
+ The MultiEdgeView provides set-like operations on the edge-tuples
+ as well as edge attribute lookup. When called, it also provides
+ an EdgeDataView object which allows control of access to edge
+ attributes (but does not provide set-like operations).
+ Hence, ``G.edges[u, v, k]['color']`` provides the value of the color
+ attribute for the edge from ``u`` to ``v`` with key ``k`` while
+ ``for (u, v, k, c) in G.edges(data='color', keys=True, default="red"):``
+ iterates through all the edges yielding the color attribute with
+ default `'red'` if no color attribute exists.
+
+ Edges are returned as tuples with optional data and keys
+ in the order (node, neighbor, key, data). If ``keys=True`` is not
+ provided, the tuples will just be (node, neighbor, data), but
+ multiple tuples with the same node and neighbor will be generated
+ when multiple edges exist between two nodes.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges from these nodes.
+ data : string or bool, optional (default=False)
+ The edge attribute returned in 3-tuple (u, v, ddict[data]).
+ If True, return edge attribute dict in 3-tuple (u, v, ddict).
+ If False, return 2-tuple (u, v).
+ keys : bool, optional (default=False)
+ If True, return edge keys with each edge, creating (u, v, k)
+ tuples or (u, v, k, d) tuples if data is also requested.
+ default : value, optional (default=None)
+ Value used for edges that don't have the requested attribute.
+ Only relevant if data is not True or False.
+
+ Returns
+ -------
+ edges : MultiEdgeView
+ A view of edge attributes, usually it iterates over (u, v)
+ (u, v, k) or (u, v, k, d) tuples of edges, but can also be
+ used for attribute lookup as ``edges[u, v, k]['foo']``.
+
+ Notes
+ -----
+ Nodes in nbunch that are not in the graph will be (quietly) ignored.
+ For directed graphs this returns the out-edges.
+
+ Examples
+ --------
+ >>> G = nx.MultiGraph()
+ >>> nx.add_path(G, [0, 1, 2])
+ >>> key = G.add_edge(2, 3, weight=5)
+ >>> key2 = G.add_edge(2, 1, weight=2) # multi-edge
+ >>> [e for e in G.edges()]
+ [(0, 1), (1, 2), (1, 2), (2, 3)]
+ >>> G.edges.data() # default data is {} (empty dict)
+ MultiEdgeDataView([(0, 1, {}), (1, 2, {}), (1, 2, {'weight': 2}), (2, 3, {'weight': 5})])
+ >>> G.edges.data("weight", default=1)
+ MultiEdgeDataView([(0, 1, 1), (1, 2, 1), (1, 2, 2), (2, 3, 5)])
+ >>> G.edges(keys=True) # default keys are integers
+ MultiEdgeView([(0, 1, 0), (1, 2, 0), (1, 2, 1), (2, 3, 0)])
+ >>> G.edges.data(keys=True)
+ MultiEdgeDataView([(0, 1, 0, {}), (1, 2, 0, {}), (1, 2, 1, {'weight': 2}), (2, 3, 0, {'weight': 5})])
+ >>> G.edges.data("weight", default=1, keys=True)
+ MultiEdgeDataView([(0, 1, 0, 1), (1, 2, 0, 1), (1, 2, 1, 2), (2, 3, 0, 5)])
+ >>> G.edges([0, 3]) # Note ordering of tuples from listed sources
+ MultiEdgeDataView([(0, 1), (3, 2)])
+ >>> G.edges([0, 3, 2, 1]) # Note ordering of tuples
+ MultiEdgeDataView([(0, 1), (3, 2), (2, 1), (2, 1)])
+ >>> G.edges(0)
+ MultiEdgeDataView([(0, 1)])
+ """
+ return MultiEdgeView(self)
+
+ def get_edge_data(self, u, v, key=None, default=None):
+ """Returns the attribute dictionary associated with edge (u, v,
+ key).
+
+ If a key is not provided, returns a dictionary mapping edge keys
+ to attribute dictionaries for each edge between u and v.
+
+ This is identical to `G[u][v][key]` except the default is returned
+ instead of an exception is the edge doesn't exist.
+
+ Parameters
+ ----------
+ u, v : nodes
+
+ default : any Python object (default=None)
+ Value to return if the specific edge (u, v, key) is not
+ found, OR if there are no edges between u and v and no key
+ is specified.
+
+ key : hashable identifier, optional (default=None)
+ Return data only for the edge with specified key, as an
+ attribute dictionary (rather than a dictionary mapping keys
+ to attribute dictionaries).
+
+ Returns
+ -------
+ edge_dict : dictionary
+ The edge attribute dictionary, OR a dictionary mapping edge
+ keys to attribute dictionaries for each of those edges if no
+ specific key is provided (even if there's only one edge
+ between u and v).
+
+ Examples
+ --------
+ >>> G = nx.MultiGraph() # or MultiDiGraph
+ >>> key = G.add_edge(0, 1, key="a", weight=7)
+ >>> G[0][1]["a"] # key='a'
+ {'weight': 7}
+ >>> G.edges[0, 1, "a"] # key='a'
+ {'weight': 7}
+
+ Warning: we protect the graph data structure by making
+ `G.edges` and `G[1][2]` read-only dict-like structures.
+ However, you can assign values to attributes in e.g.
+ `G.edges[1, 2, 'a']` or `G[1][2]['a']` using an additional
+ bracket as shown next. You need to specify all edge info
+ to assign to the edge data associated with an edge.
+
+ >>> G[0][1]["a"]["weight"] = 10
+ >>> G.edges[0, 1, "a"]["weight"] = 10
+ >>> G[0][1]["a"]["weight"]
+ 10
+ >>> G.edges[1, 0, "a"]["weight"]
+ 10
+
+ >>> G = nx.MultiGraph() # or MultiDiGraph
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.edges[0, 1, 0]["weight"] = 5
+ >>> G.get_edge_data(0, 1)
+ {0: {'weight': 5}}
+ >>> e = (0, 1)
+ >>> G.get_edge_data(*e) # tuple form
+ {0: {'weight': 5}}
+ >>> G.get_edge_data(3, 0) # edge not in graph, returns None
+ >>> G.get_edge_data(3, 0, default=0) # edge not in graph, return default
+ 0
+ >>> G.get_edge_data(1, 0, 0) # specific key gives back
+ {'weight': 5}
+ """
+ try:
+ if key is None:
+ return self._adj[u][v]
+ else:
+ return self._adj[u][v][key]
+ except KeyError:
+ return default
+
+ @cached_property
+ def degree(self):
+ """A DegreeView for the Graph as G.degree or G.degree().
+
+ The node degree is the number of edges adjacent to the node.
+ The weighted node degree is the sum of the edge weights for
+ edges incident to that node.
+
+ This object provides an iterator for (node, degree) as well as
+ lookup for the degree for a single node.
+
+ Parameters
+ ----------
+ nbunch : single node, container, or all nodes (default= all nodes)
+ The view will only report edges incident to these nodes.
+
+ weight : string or None, optional (default=None)
+ The name of an edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ Returns
+ -------
+ MultiDegreeView or int
+ If multiple nodes are requested (the default), returns a `MultiDegreeView`
+ mapping nodes to their degree.
+ If a single node is requested, returns the degree of the node as an integer.
+
+ Examples
+ --------
+ >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> nx.add_path(G, [0, 1, 2, 3])
+ >>> G.degree(0) # node 0 with degree 1
+ 1
+ >>> list(G.degree([0, 1]))
+ [(0, 1), (1, 2)]
+
+ """
+ return MultiDegreeView(self)
+
+ def is_multigraph(self):
+ """Returns True if graph is a multigraph, False otherwise."""
+ return True
+
+ def is_directed(self):
+ """Returns True if graph is directed, False otherwise."""
+ return False
+
+ def copy(self, as_view=False):
+ """Returns a copy of the graph.
+
+ The copy method by default returns an independent shallow copy
+ of the graph and attributes. That is, if an attribute is a
+ container, that container is shared by the original an the copy.
+ Use Python's `copy.deepcopy` for new containers.
+
+ If `as_view` is True then a view is returned instead of a copy.
+
+ Notes
+ -----
+ All copies reproduce the graph structure, but data attributes
+ may be handled in different ways. There are four types of copies
+ of a graph that people might want.
+
+ Deepcopy -- A "deepcopy" copies the graph structure as well as
+ all data attributes and any objects they might contain.
+ The entire graph object is new so that changes in the copy
+ do not affect the original object. (see Python's copy.deepcopy)
+
+ Data Reference (Shallow) -- For a shallow copy the graph structure
+ is copied but the edge, node and graph attribute dicts are
+ references to those in the original graph. This saves
+ time and memory but could cause confusion if you change an attribute
+ in one graph and it changes the attribute in the other.
+ NetworkX does not provide this level of shallow copy.
+
+ Independent Shallow -- This copy creates new independent attribute
+ dicts and then does a shallow copy of the attributes. That is, any
+ attributes that are containers are shared between the new graph
+ and the original. This is exactly what `dict.copy()` provides.
+ You can obtain this style copy using:
+
+ >>> G = nx.path_graph(5)
+ >>> H = G.copy()
+ >>> H = G.copy(as_view=False)
+ >>> H = nx.Graph(G)
+ >>> H = G.__class__(G)
+
+ Fresh Data -- For fresh data, the graph structure is copied while
+ new empty data attribute dicts are created. The resulting graph
+ is independent of the original and it has no edge, node or graph
+ attributes. Fresh copies are not enabled. Instead use:
+
+ >>> H = G.__class__()
+ >>> H.add_nodes_from(G)
+ >>> H.add_edges_from(G.edges)
+
+ View -- Inspired by dict-views, graph-views act like read-only
+ versions of the original graph, providing a copy of the original
+ structure without requiring any memory for copying the information.
+
+ See the Python copy module for more information on shallow
+ and deep copies, https://docs.python.org/3/library/copy.html.
+
+ Parameters
+ ----------
+ as_view : bool, optional (default=False)
+ If True, the returned graph-view provides a read-only view
+ of the original graph without actually copying any data.
+
+ Returns
+ -------
+ G : Graph
+ A copy of the graph.
+
+ See Also
+ --------
+ to_directed: return a directed copy of the graph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
+ >>> H = G.copy()
+
+ """
+ if as_view is True:
+ return nx.graphviews.generic_graph_view(self)
+ G = self.__class__()
+ G.graph.update(self.graph)
+ G.add_nodes_from((n, d.copy()) for n, d in self._node.items())
+ G.add_edges_from(
+ (u, v, key, datadict.copy())
+ for u, nbrs in self._adj.items()
+ for v, keydict in nbrs.items()
+ for key, datadict in keydict.items()
+ )
+ return G
+
+ def to_directed(self, as_view=False):
+ """Returns a directed representation of the graph.
+
+ Returns
+ -------
+ G : MultiDiGraph
+ A directed graph with the same name, same nodes, and with
+ each edge (u, v, k, data) replaced by two directed edges
+ (u, v, k, data) and (v, u, k, data).
+
+ Notes
+ -----
+ This returns a "deepcopy" of the edge, node, and
+ graph attributes which attempts to completely copy
+ all of the data and references.
+
+ This is in contrast to the similar D=MultiDiGraph(G) which
+ returns a shallow copy of the data.
+
+ See the Python copy module for more information on shallow
+ and deep copies, https://docs.python.org/3/library/copy.html.
+
+ Warning: If you have subclassed MultiGraph to use dict-like objects
+ in the data structure, those changes do not transfer to the
+ MultiDiGraph created by this method.
+
+ Examples
+ --------
+ >>> G = nx.MultiGraph()
+ >>> G.add_edge(0, 1)
+ 0
+ >>> G.add_edge(0, 1)
+ 1
+ >>> H = G.to_directed()
+ >>> list(H.edges)
+ [(0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1)]
+
+ If already directed, return a (deep) copy
+
+ >>> G = nx.MultiDiGraph()
+ >>> G.add_edge(0, 1)
+ 0
+ >>> H = G.to_directed()
+ >>> list(H.edges)
+ [(0, 1, 0)]
+ """
+ graph_class = self.to_directed_class()
+ if as_view is True:
+ return nx.graphviews.generic_graph_view(self, graph_class)
+ # deepcopy when not a view
+ G = graph_class()
+ G.graph.update(deepcopy(self.graph))
+ G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
+ G.add_edges_from(
+ (u, v, key, deepcopy(datadict))
+ for u, nbrs in self.adj.items()
+ for v, keydict in nbrs.items()
+ for key, datadict in keydict.items()
+ )
+ return G
+
+ def to_undirected(self, as_view=False):
+ """Returns an undirected copy of the graph.
+
+ Returns
+ -------
+ G : Graph/MultiGraph
+ A deepcopy of the graph.
+
+ See Also
+ --------
+ copy, add_edge, add_edges_from
+
+ Notes
+ -----
+ This returns a "deepcopy" of the edge, node, and
+ graph attributes which attempts to completely copy
+ all of the data and references.
+
+ This is in contrast to the similar `G = nx.MultiGraph(D)`
+ which returns a shallow copy of the data.
+
+ See the Python copy module for more information on shallow
+ and deep copies, https://docs.python.org/3/library/copy.html.
+
+ Warning: If you have subclassed MultiGraph to use dict-like
+ objects in the data structure, those changes do not transfer
+ to the MultiGraph created by this method.
+
+ Examples
+ --------
+ >>> G = nx.MultiGraph([(0, 1), (0, 1), (1, 2)])
+ >>> H = G.to_directed()
+ >>> list(H.edges)
+ [(0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 2, 0), (2, 1, 0)]
+ >>> G2 = H.to_undirected()
+ >>> list(G2.edges)
+ [(0, 1, 0), (0, 1, 1), (1, 2, 0)]
+ """
+ graph_class = self.to_undirected_class()
+ if as_view is True:
+ return nx.graphviews.generic_graph_view(self, graph_class)
+ # deepcopy when not a view
+ G = graph_class()
+ G.graph.update(deepcopy(self.graph))
+ G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
+ G.add_edges_from(
+ (u, v, key, deepcopy(datadict))
+ for u, nbrs in self._adj.items()
+ for v, keydict in nbrs.items()
+ for key, datadict in keydict.items()
+ )
+ return G
+
+ def number_of_edges(self, u=None, v=None):
+ """Returns the number of edges between two nodes.
+
+ Parameters
+ ----------
+ u, v : nodes, optional (Default=all edges)
+ If u and v are specified, return the number of edges between
+ u and v. Otherwise return the total number of all edges.
+
+ Returns
+ -------
+ nedges : int
+ The number of edges in the graph. If nodes `u` and `v` are
+ specified return the number of edges between those nodes. If
+ the graph is directed, this only returns the number of edges
+ from `u` to `v`.
+
+ See Also
+ --------
+ size
+
+ Examples
+ --------
+ For undirected multigraphs, this method counts the total number
+ of edges in the graph::
+
+ >>> G = nx.MultiGraph()
+ >>> G.add_edges_from([(0, 1), (0, 1), (1, 2)])
+ [0, 1, 0]
+ >>> G.number_of_edges()
+ 3
+
+ If you specify two nodes, this counts the total number of edges
+ joining the two nodes::
+
+ >>> G.number_of_edges(0, 1)
+ 2
+
+ For directed multigraphs, this method can count the total number
+ of directed edges from `u` to `v`::
+
+ >>> G = nx.MultiDiGraph()
+ >>> G.add_edges_from([(0, 1), (0, 1), (1, 0)])
+ [0, 1, 0]
+ >>> G.number_of_edges(0, 1)
+ 2
+ >>> G.number_of_edges(1, 0)
+ 1
+
+ """
+ if u is None:
+ return self.size()
+ try:
+ edgedata = self._adj[u][v]
+ except KeyError:
+ return 0 # no such edge
+ return len(edgedata)
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/reportviews.py b/.venv/lib/python3.12/site-packages/networkx/classes/reportviews.py
new file mode 100644
index 00000000..789662de
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/reportviews.py
@@ -0,0 +1,1447 @@
+"""
+View Classes provide node, edge and degree "views" of a graph.
+
+Views for nodes, edges and degree are provided for all base graph classes.
+A view means a read-only object that is quick to create, automatically
+updated when the graph changes, and provides basic access like `n in V`,
+`for n in V`, `V[n]` and sometimes set operations.
+
+The views are read-only iterable containers that are updated as the
+graph is updated. As with dicts, the graph should not be updated
+while iterating through the view. Views can be iterated multiple times.
+
+Edge and Node views also allow data attribute lookup.
+The resulting attribute dict is writable as `G.edges[3, 4]['color']='red'`
+Degree views allow lookup of degree values for single nodes.
+Weighted degree is supported with the `weight` argument.
+
+NodeView
+========
+
+ `V = G.nodes` (or `V = G.nodes()`) allows `len(V)`, `n in V`, set
+ operations e.g. "G.nodes & H.nodes", and `dd = G.nodes[n]`, where
+ `dd` is the node data dict. Iteration is over the nodes by default.
+
+NodeDataView
+============
+
+ To iterate over (node, data) pairs, use arguments to `G.nodes()`
+ to create a DataView e.g. `DV = G.nodes(data='color', default='red')`.
+ The DataView iterates as `for n, color in DV` and allows
+ `(n, 'red') in DV`. Using `DV = G.nodes(data=True)`, the DataViews
+ use the full datadict in writeable form also allowing contain testing as
+ `(n, {'color': 'red'}) in VD`. DataViews allow set operations when
+ data attributes are hashable.
+
+DegreeView
+==========
+
+ `V = G.degree` allows iteration over (node, degree) pairs as well
+ as lookup: `deg=V[n]`. There are many flavors of DegreeView
+ for In/Out/Directed/Multi. For Directed Graphs, `G.degree`
+ counts both in and out going edges. `G.out_degree` and
+ `G.in_degree` count only specific directions.
+ Weighted degree using edge data attributes is provide via
+ `V = G.degree(weight='attr_name')` where any string with the
+ attribute name can be used. `weight=None` is the default.
+ No set operations are implemented for degrees, use NodeView.
+
+ The argument `nbunch` restricts iteration to nodes in nbunch.
+ The DegreeView can still lookup any node even if nbunch is specified.
+
+EdgeView
+========
+
+ `V = G.edges` or `V = G.edges()` allows iteration over edges as well as
+ `e in V`, set operations and edge data lookup `dd = G.edges[2, 3]`.
+ Iteration is over 2-tuples `(u, v)` for Graph/DiGraph. For multigraphs
+ edges 3-tuples `(u, v, key)` are the default but 2-tuples can be obtained
+ via `V = G.edges(keys=False)`.
+
+ Set operations for directed graphs treat the edges as a set of 2-tuples.
+ For undirected graphs, 2-tuples are not a unique representation of edges.
+ So long as the set being compared to contains unique representations
+ of its edges, the set operations will act as expected. If the other
+ set contains both `(0, 1)` and `(1, 0)` however, the result of set
+ operations may contain both representations of the same edge.
+
+EdgeDataView
+============
+
+ Edge data can be reported using an EdgeDataView typically created
+ by calling an EdgeView: `DV = G.edges(data='weight', default=1)`.
+ The EdgeDataView allows iteration over edge tuples, membership checking
+ but no set operations.
+
+ Iteration depends on `data` and `default` and for multigraph `keys`
+ If `data is False` (the default) then iterate over 2-tuples `(u, v)`.
+ If `data is True` iterate over 3-tuples `(u, v, datadict)`.
+ Otherwise iterate over `(u, v, datadict.get(data, default))`.
+ For Multigraphs, if `keys is True`, replace `u, v` with `u, v, key`
+ to create 3-tuples and 4-tuples.
+
+ The argument `nbunch` restricts edges to those incident to nodes in nbunch.
+"""
+
+from abc import ABC
+from collections.abc import Mapping, Set
+
+import networkx as nx
+
+__all__ = [
+ "NodeView",
+ "NodeDataView",
+ "EdgeView",
+ "OutEdgeView",
+ "InEdgeView",
+ "EdgeDataView",
+ "OutEdgeDataView",
+ "InEdgeDataView",
+ "MultiEdgeView",
+ "OutMultiEdgeView",
+ "InMultiEdgeView",
+ "MultiEdgeDataView",
+ "OutMultiEdgeDataView",
+ "InMultiEdgeDataView",
+ "DegreeView",
+ "DiDegreeView",
+ "InDegreeView",
+ "OutDegreeView",
+ "MultiDegreeView",
+ "DiMultiDegreeView",
+ "InMultiDegreeView",
+ "OutMultiDegreeView",
+]
+
+
+# NodeViews
+class NodeView(Mapping, Set):
+ """A NodeView class to act as G.nodes for a NetworkX Graph
+
+ Set operations act on the nodes without considering data.
+ Iteration is over nodes. Node data can be looked up like a dict.
+ Use NodeDataView to iterate over node data or to specify a data
+ attribute for lookup. NodeDataView is created by calling the NodeView.
+
+ Parameters
+ ----------
+ graph : NetworkX graph-like class
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3)
+ >>> NV = G.nodes()
+ >>> 2 in NV
+ True
+ >>> for n in NV:
+ ... print(n)
+ 0
+ 1
+ 2
+ >>> assert NV & {1, 2, 3} == {1, 2}
+
+ >>> G.add_node(2, color="blue")
+ >>> NV[2]
+ {'color': 'blue'}
+ >>> G.add_node(8, color="red")
+ >>> NDV = G.nodes(data=True)
+ >>> (2, NV[2]) in NDV
+ True
+ >>> for n, dd in NDV:
+ ... print((n, dd.get("color", "aqua")))
+ (0, 'aqua')
+ (1, 'aqua')
+ (2, 'blue')
+ (8, 'red')
+ >>> NDV[2] == NV[2]
+ True
+
+ >>> NVdata = G.nodes(data="color", default="aqua")
+ >>> (2, NVdata[2]) in NVdata
+ True
+ >>> for n, dd in NVdata:
+ ... print((n, dd))
+ (0, 'aqua')
+ (1, 'aqua')
+ (2, 'blue')
+ (8, 'red')
+ >>> NVdata[2] == NV[2] # NVdata gets 'color', NV gets datadict
+ False
+ """
+
+ __slots__ = ("_nodes",)
+
+ def __getstate__(self):
+ return {"_nodes": self._nodes}
+
+ def __setstate__(self, state):
+ self._nodes = state["_nodes"]
+
+ def __init__(self, graph):
+ self._nodes = graph._node
+
+ # Mapping methods
+ def __len__(self):
+ return len(self._nodes)
+
+ def __iter__(self):
+ return iter(self._nodes)
+
+ def __getitem__(self, n):
+ if isinstance(n, slice):
+ raise nx.NetworkXError(
+ f"{type(self).__name__} does not support slicing, "
+ f"try list(G.nodes)[{n.start}:{n.stop}:{n.step}]"
+ )
+ return self._nodes[n]
+
+ # Set methods
+ def __contains__(self, n):
+ return n in self._nodes
+
+ @classmethod
+ def _from_iterable(cls, it):
+ return set(it)
+
+ # DataView method
+ def __call__(self, data=False, default=None):
+ if data is False:
+ return self
+ return NodeDataView(self._nodes, data, default)
+
+ def data(self, data=True, default=None):
+ """
+ Return a read-only view of node data.
+
+ Parameters
+ ----------
+ data : bool or node data key, default=True
+ If ``data=True`` (the default), return a `NodeDataView` object that
+ maps each node to *all* of its attributes. `data` may also be an
+ arbitrary key, in which case the `NodeDataView` maps each node to
+ the value for the keyed attribute. In this case, if a node does
+ not have the `data` attribute, the `default` value is used.
+ default : object, default=None
+ The value used when a node does not have a specific attribute.
+
+ Returns
+ -------
+ NodeDataView
+ The layout of the returned NodeDataView depends on the value of the
+ `data` parameter.
+
+ Notes
+ -----
+ If ``data=False``, returns a `NodeView` object without data.
+
+ See Also
+ --------
+ NodeDataView
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> G.add_nodes_from(
+ ... [
+ ... (0, {"color": "red", "weight": 10}),
+ ... (1, {"color": "blue"}),
+ ... (2, {"color": "yellow", "weight": 2}),
+ ... ]
+ ... )
+
+ Accessing node data with ``data=True`` (the default) returns a
+ NodeDataView mapping each node to all of its attributes:
+
+ >>> G.nodes.data()
+ NodeDataView({0: {'color': 'red', 'weight': 10}, 1: {'color': 'blue'}, 2: {'color': 'yellow', 'weight': 2}})
+
+ If `data` represents a key in the node attribute dict, a NodeDataView mapping
+ the nodes to the value for that specific key is returned:
+
+ >>> G.nodes.data("color")
+ NodeDataView({0: 'red', 1: 'blue', 2: 'yellow'}, data='color')
+
+ If a specific key is not found in an attribute dict, the value specified
+ by `default` is returned:
+
+ >>> G.nodes.data("weight", default=-999)
+ NodeDataView({0: 10, 1: -999, 2: 2}, data='weight')
+
+ Note that there is no check that the `data` key is in any of the
+ node attribute dictionaries:
+
+ >>> G.nodes.data("height")
+ NodeDataView({0: None, 1: None, 2: None}, data='height')
+ """
+ if data is False:
+ return self
+ return NodeDataView(self._nodes, data, default)
+
+ def __str__(self):
+ return str(list(self))
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({tuple(self)})"
+
+
+class NodeDataView(Set):
+ """A DataView class for nodes of a NetworkX Graph
+
+ The main use for this class is to iterate through node-data pairs.
+ The data can be the entire data-dictionary for each node, or it
+ can be a specific attribute (with default) for each node.
+ Set operations are enabled with NodeDataView, but don't work in
+ cases where the data is not hashable. Use with caution.
+ Typically, set operations on nodes use NodeView, not NodeDataView.
+ That is, they use `G.nodes` instead of `G.nodes(data='foo')`.
+
+ Parameters
+ ==========
+ graph : NetworkX graph-like class
+ data : bool or string (default=False)
+ default : object (default=None)
+ """
+
+ __slots__ = ("_nodes", "_data", "_default")
+
+ def __getstate__(self):
+ return {"_nodes": self._nodes, "_data": self._data, "_default": self._default}
+
+ def __setstate__(self, state):
+ self._nodes = state["_nodes"]
+ self._data = state["_data"]
+ self._default = state["_default"]
+
+ def __init__(self, nodedict, data=False, default=None):
+ self._nodes = nodedict
+ self._data = data
+ self._default = default
+
+ @classmethod
+ def _from_iterable(cls, it):
+ try:
+ return set(it)
+ except TypeError as err:
+ if "unhashable" in str(err):
+ msg = " : Could be b/c data=True or your values are unhashable"
+ raise TypeError(str(err) + msg) from err
+ raise
+
+ def __len__(self):
+ return len(self._nodes)
+
+ def __iter__(self):
+ data = self._data
+ if data is False:
+ return iter(self._nodes)
+ if data is True:
+ return iter(self._nodes.items())
+ return (
+ (n, dd[data] if data in dd else self._default)
+ for n, dd in self._nodes.items()
+ )
+
+ def __contains__(self, n):
+ try:
+ node_in = n in self._nodes
+ except TypeError:
+ n, d = n
+ return n in self._nodes and self[n] == d
+ if node_in is True:
+ return node_in
+ try:
+ n, d = n
+ except (TypeError, ValueError):
+ return False
+ return n in self._nodes and self[n] == d
+
+ def __getitem__(self, n):
+ if isinstance(n, slice):
+ raise nx.NetworkXError(
+ f"{type(self).__name__} does not support slicing, "
+ f"try list(G.nodes.data())[{n.start}:{n.stop}:{n.step}]"
+ )
+ ddict = self._nodes[n]
+ data = self._data
+ if data is False or data is True:
+ return ddict
+ return ddict[data] if data in ddict else self._default
+
+ def __str__(self):
+ return str(list(self))
+
+ def __repr__(self):
+ name = self.__class__.__name__
+ if self._data is False:
+ return f"{name}({tuple(self)})"
+ if self._data is True:
+ return f"{name}({dict(self)})"
+ return f"{name}({dict(self)}, data={self._data!r})"
+
+
+# DegreeViews
+class DiDegreeView:
+ """A View class for degree of nodes in a NetworkX Graph
+
+ The functionality is like dict.items() with (node, degree) pairs.
+ Additional functionality includes read-only lookup of node degree,
+ and calling with optional features nbunch (for only a subset of nodes)
+ and weight (use edge weights to compute degree).
+
+ Parameters
+ ==========
+ graph : NetworkX graph-like class
+ nbunch : node, container of nodes, or None meaning all nodes (default=None)
+ weight : bool or string (default=None)
+
+ Notes
+ -----
+ DegreeView can still lookup any node even if nbunch is specified.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3)
+ >>> DV = G.degree()
+ >>> assert DV[2] == 1
+ >>> assert sum(deg for n, deg in DV) == 4
+
+ >>> DVweight = G.degree(weight="span")
+ >>> G.add_edge(1, 2, span=34)
+ >>> DVweight[2]
+ 34
+ >>> DVweight[0] # default edge weight is 1
+ 1
+ >>> sum(span for n, span in DVweight) # sum weighted degrees
+ 70
+
+ >>> DVnbunch = G.degree(nbunch=(1, 2))
+ >>> assert len(list(DVnbunch)) == 2 # iteration over nbunch only
+ """
+
+ def __init__(self, G, nbunch=None, weight=None):
+ self._graph = G
+ self._succ = G._succ if hasattr(G, "_succ") else G._adj
+ self._pred = G._pred if hasattr(G, "_pred") else G._adj
+ self._nodes = self._succ if nbunch is None else list(G.nbunch_iter(nbunch))
+ self._weight = weight
+
+ def __call__(self, nbunch=None, weight=None):
+ if nbunch is None:
+ if weight == self._weight:
+ return self
+ return self.__class__(self._graph, None, weight)
+ try:
+ if nbunch in self._nodes:
+ if weight == self._weight:
+ return self[nbunch]
+ return self.__class__(self._graph, None, weight)[nbunch]
+ except TypeError:
+ pass
+ return self.__class__(self._graph, nbunch, weight)
+
+ def __getitem__(self, n):
+ weight = self._weight
+ succs = self._succ[n]
+ preds = self._pred[n]
+ if weight is None:
+ return len(succs) + len(preds)
+ return sum(dd.get(weight, 1) for dd in succs.values()) + sum(
+ dd.get(weight, 1) for dd in preds.values()
+ )
+
+ def __iter__(self):
+ weight = self._weight
+ if weight is None:
+ for n in self._nodes:
+ succs = self._succ[n]
+ preds = self._pred[n]
+ yield (n, len(succs) + len(preds))
+ else:
+ for n in self._nodes:
+ succs = self._succ[n]
+ preds = self._pred[n]
+ deg = sum(dd.get(weight, 1) for dd in succs.values()) + sum(
+ dd.get(weight, 1) for dd in preds.values()
+ )
+ yield (n, deg)
+
+ def __len__(self):
+ return len(self._nodes)
+
+ def __str__(self):
+ return str(list(self))
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({dict(self)})"
+
+
+class DegreeView(DiDegreeView):
+ """A DegreeView class to act as G.degree for a NetworkX Graph
+
+ Typical usage focuses on iteration over `(node, degree)` pairs.
+ The degree is by default the number of edges incident to the node.
+ Optional argument `weight` enables weighted degree using the edge
+ attribute named in the `weight` argument. Reporting and iteration
+ can also be restricted to a subset of nodes using `nbunch`.
+
+ Additional functionality include node lookup so that `G.degree[n]`
+ reported the (possibly weighted) degree of node `n`. Calling the
+ view creates a view with different arguments `nbunch` or `weight`.
+
+ Parameters
+ ==========
+ graph : NetworkX graph-like class
+ nbunch : node, container of nodes, or None meaning all nodes (default=None)
+ weight : string or None (default=None)
+
+ Notes
+ -----
+ DegreeView can still lookup any node even if nbunch is specified.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3)
+ >>> DV = G.degree()
+ >>> assert DV[2] == 1
+ >>> assert G.degree[2] == 1
+ >>> assert sum(deg for n, deg in DV) == 4
+
+ >>> DVweight = G.degree(weight="span")
+ >>> G.add_edge(1, 2, span=34)
+ >>> DVweight[2]
+ 34
+ >>> DVweight[0] # default edge weight is 1
+ 1
+ >>> sum(span for n, span in DVweight) # sum weighted degrees
+ 70
+
+ >>> DVnbunch = G.degree(nbunch=(1, 2))
+ >>> assert len(list(DVnbunch)) == 2 # iteration over nbunch only
+ """
+
+ def __getitem__(self, n):
+ weight = self._weight
+ nbrs = self._succ[n]
+ if weight is None:
+ return len(nbrs) + (n in nbrs)
+ return sum(dd.get(weight, 1) for dd in nbrs.values()) + (
+ n in nbrs and nbrs[n].get(weight, 1)
+ )
+
+ def __iter__(self):
+ weight = self._weight
+ if weight is None:
+ for n in self._nodes:
+ nbrs = self._succ[n]
+ yield (n, len(nbrs) + (n in nbrs))
+ else:
+ for n in self._nodes:
+ nbrs = self._succ[n]
+ deg = sum(dd.get(weight, 1) for dd in nbrs.values()) + (
+ n in nbrs and nbrs[n].get(weight, 1)
+ )
+ yield (n, deg)
+
+
+class OutDegreeView(DiDegreeView):
+ """A DegreeView class to report out_degree for a DiGraph; See DegreeView"""
+
+ def __getitem__(self, n):
+ weight = self._weight
+ nbrs = self._succ[n]
+ if self._weight is None:
+ return len(nbrs)
+ return sum(dd.get(self._weight, 1) for dd in nbrs.values())
+
+ def __iter__(self):
+ weight = self._weight
+ if weight is None:
+ for n in self._nodes:
+ succs = self._succ[n]
+ yield (n, len(succs))
+ else:
+ for n in self._nodes:
+ succs = self._succ[n]
+ deg = sum(dd.get(weight, 1) for dd in succs.values())
+ yield (n, deg)
+
+
+class InDegreeView(DiDegreeView):
+ """A DegreeView class to report in_degree for a DiGraph; See DegreeView"""
+
+ def __getitem__(self, n):
+ weight = self._weight
+ nbrs = self._pred[n]
+ if weight is None:
+ return len(nbrs)
+ return sum(dd.get(weight, 1) for dd in nbrs.values())
+
+ def __iter__(self):
+ weight = self._weight
+ if weight is None:
+ for n in self._nodes:
+ preds = self._pred[n]
+ yield (n, len(preds))
+ else:
+ for n in self._nodes:
+ preds = self._pred[n]
+ deg = sum(dd.get(weight, 1) for dd in preds.values())
+ yield (n, deg)
+
+
+class MultiDegreeView(DiDegreeView):
+ """A DegreeView class for undirected multigraphs; See DegreeView"""
+
+ def __getitem__(self, n):
+ weight = self._weight
+ nbrs = self._succ[n]
+ if weight is None:
+ return sum(len(keys) for keys in nbrs.values()) + (
+ n in nbrs and len(nbrs[n])
+ )
+ # edge weighted graph - degree is sum of nbr edge weights
+ deg = sum(
+ d.get(weight, 1) for key_dict in nbrs.values() for d in key_dict.values()
+ )
+ if n in nbrs:
+ deg += sum(d.get(weight, 1) for d in nbrs[n].values())
+ return deg
+
+ def __iter__(self):
+ weight = self._weight
+ if weight is None:
+ for n in self._nodes:
+ nbrs = self._succ[n]
+ deg = sum(len(keys) for keys in nbrs.values()) + (
+ n in nbrs and len(nbrs[n])
+ )
+ yield (n, deg)
+ else:
+ for n in self._nodes:
+ nbrs = self._succ[n]
+ deg = sum(
+ d.get(weight, 1)
+ for key_dict in nbrs.values()
+ for d in key_dict.values()
+ )
+ if n in nbrs:
+ deg += sum(d.get(weight, 1) for d in nbrs[n].values())
+ yield (n, deg)
+
+
+class DiMultiDegreeView(DiDegreeView):
+ """A DegreeView class for MultiDiGraph; See DegreeView"""
+
+ def __getitem__(self, n):
+ weight = self._weight
+ succs = self._succ[n]
+ preds = self._pred[n]
+ if weight is None:
+ return sum(len(keys) for keys in succs.values()) + sum(
+ len(keys) for keys in preds.values()
+ )
+ # edge weighted graph - degree is sum of nbr edge weights
+ deg = sum(
+ d.get(weight, 1) for key_dict in succs.values() for d in key_dict.values()
+ ) + sum(
+ d.get(weight, 1) for key_dict in preds.values() for d in key_dict.values()
+ )
+ return deg
+
+ def __iter__(self):
+ weight = self._weight
+ if weight is None:
+ for n in self._nodes:
+ succs = self._succ[n]
+ preds = self._pred[n]
+ deg = sum(len(keys) for keys in succs.values()) + sum(
+ len(keys) for keys in preds.values()
+ )
+ yield (n, deg)
+ else:
+ for n in self._nodes:
+ succs = self._succ[n]
+ preds = self._pred[n]
+ deg = sum(
+ d.get(weight, 1)
+ for key_dict in succs.values()
+ for d in key_dict.values()
+ ) + sum(
+ d.get(weight, 1)
+ for key_dict in preds.values()
+ for d in key_dict.values()
+ )
+ yield (n, deg)
+
+
+class InMultiDegreeView(DiDegreeView):
+ """A DegreeView class for inward degree of MultiDiGraph; See DegreeView"""
+
+ def __getitem__(self, n):
+ weight = self._weight
+ nbrs = self._pred[n]
+ if weight is None:
+ return sum(len(data) for data in nbrs.values())
+ # edge weighted graph - degree is sum of nbr edge weights
+ return sum(
+ d.get(weight, 1) for key_dict in nbrs.values() for d in key_dict.values()
+ )
+
+ def __iter__(self):
+ weight = self._weight
+ if weight is None:
+ for n in self._nodes:
+ nbrs = self._pred[n]
+ deg = sum(len(data) for data in nbrs.values())
+ yield (n, deg)
+ else:
+ for n in self._nodes:
+ nbrs = self._pred[n]
+ deg = sum(
+ d.get(weight, 1)
+ for key_dict in nbrs.values()
+ for d in key_dict.values()
+ )
+ yield (n, deg)
+
+
+class OutMultiDegreeView(DiDegreeView):
+ """A DegreeView class for outward degree of MultiDiGraph; See DegreeView"""
+
+ def __getitem__(self, n):
+ weight = self._weight
+ nbrs = self._succ[n]
+ if weight is None:
+ return sum(len(data) for data in nbrs.values())
+ # edge weighted graph - degree is sum of nbr edge weights
+ return sum(
+ d.get(weight, 1) for key_dict in nbrs.values() for d in key_dict.values()
+ )
+
+ def __iter__(self):
+ weight = self._weight
+ if weight is None:
+ for n in self._nodes:
+ nbrs = self._succ[n]
+ deg = sum(len(data) for data in nbrs.values())
+ yield (n, deg)
+ else:
+ for n in self._nodes:
+ nbrs = self._succ[n]
+ deg = sum(
+ d.get(weight, 1)
+ for key_dict in nbrs.values()
+ for d in key_dict.values()
+ )
+ yield (n, deg)
+
+
+# A base class for all edge views. Ensures all edge view and edge data view
+# objects/classes are captured by `isinstance(obj, EdgeViewABC)` and
+# `issubclass(cls, EdgeViewABC)` respectively
+class EdgeViewABC(ABC):
+ pass
+
+
+# EdgeDataViews
+class OutEdgeDataView(EdgeViewABC):
+ """EdgeDataView for outward edges of DiGraph; See EdgeDataView"""
+
+ __slots__ = (
+ "_viewer",
+ "_nbunch",
+ "_data",
+ "_default",
+ "_adjdict",
+ "_nodes_nbrs",
+ "_report",
+ )
+
+ def __getstate__(self):
+ return {
+ "viewer": self._viewer,
+ "nbunch": self._nbunch,
+ "data": self._data,
+ "default": self._default,
+ }
+
+ def __setstate__(self, state):
+ self.__init__(**state)
+
+ def __init__(self, viewer, nbunch=None, data=False, *, default=None):
+ self._viewer = viewer
+ adjdict = self._adjdict = viewer._adjdict
+ if nbunch is None:
+ self._nodes_nbrs = adjdict.items
+ else:
+ # dict retains order of nodes but acts like a set
+ nbunch = dict.fromkeys(viewer._graph.nbunch_iter(nbunch))
+ self._nodes_nbrs = lambda: [(n, adjdict[n]) for n in nbunch]
+ self._nbunch = nbunch
+ self._data = data
+ self._default = default
+ # Set _report based on data and default
+ if data is True:
+ self._report = lambda n, nbr, dd: (n, nbr, dd)
+ elif data is False:
+ self._report = lambda n, nbr, dd: (n, nbr)
+ else: # data is attribute name
+ self._report = (
+ lambda n, nbr, dd: (n, nbr, dd[data])
+ if data in dd
+ else (n, nbr, default)
+ )
+
+ def __len__(self):
+ return sum(len(nbrs) for n, nbrs in self._nodes_nbrs())
+
+ def __iter__(self):
+ return (
+ self._report(n, nbr, dd)
+ for n, nbrs in self._nodes_nbrs()
+ for nbr, dd in nbrs.items()
+ )
+
+ def __contains__(self, e):
+ u, v = e[:2]
+ if self._nbunch is not None and u not in self._nbunch:
+ return False # this edge doesn't start in nbunch
+ try:
+ ddict = self._adjdict[u][v]
+ except KeyError:
+ return False
+ return e == self._report(u, v, ddict)
+
+ def __str__(self):
+ return str(list(self))
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({list(self)})"
+
+
+class EdgeDataView(OutEdgeDataView):
+ """A EdgeDataView class for edges of Graph
+
+ This view is primarily used to iterate over the edges reporting
+ edges as node-tuples with edge data optionally reported. The
+ argument `nbunch` allows restriction to edges incident to nodes
+ in that container/singleton. The default (nbunch=None)
+ reports all edges. The arguments `data` and `default` control
+ what edge data is reported. The default `data is False` reports
+ only node-tuples for each edge. If `data is True` the entire edge
+ data dict is returned. Otherwise `data` is assumed to hold the name
+ of the edge attribute to report with default `default` if that
+ edge attribute is not present.
+
+ Parameters
+ ----------
+ nbunch : container of nodes, node or None (default None)
+ data : False, True or string (default False)
+ default : default value (default None)
+
+ Examples
+ --------
+ >>> G = nx.path_graph(3)
+ >>> G.add_edge(1, 2, foo="bar")
+ >>> list(G.edges(data="foo", default="biz"))
+ [(0, 1, 'biz'), (1, 2, 'bar')]
+ >>> assert (0, 1, "biz") in G.edges(data="foo", default="biz")
+ """
+
+ __slots__ = ()
+
+ def __len__(self):
+ return sum(1 for e in self)
+
+ def __iter__(self):
+ seen = {}
+ for n, nbrs in self._nodes_nbrs():
+ for nbr, dd in nbrs.items():
+ if nbr not in seen:
+ yield self._report(n, nbr, dd)
+ seen[n] = 1
+ del seen
+
+ def __contains__(self, e):
+ u, v = e[:2]
+ if self._nbunch is not None and u not in self._nbunch and v not in self._nbunch:
+ return False # this edge doesn't start and it doesn't end in nbunch
+ try:
+ ddict = self._adjdict[u][v]
+ except KeyError:
+ return False
+ return e == self._report(u, v, ddict)
+
+
+class InEdgeDataView(OutEdgeDataView):
+ """An EdgeDataView class for outward edges of DiGraph; See EdgeDataView"""
+
+ __slots__ = ()
+
+ def __iter__(self):
+ return (
+ self._report(nbr, n, dd)
+ for n, nbrs in self._nodes_nbrs()
+ for nbr, dd in nbrs.items()
+ )
+
+ def __contains__(self, e):
+ u, v = e[:2]
+ if self._nbunch is not None and v not in self._nbunch:
+ return False # this edge doesn't end in nbunch
+ try:
+ ddict = self._adjdict[v][u]
+ except KeyError:
+ return False
+ return e == self._report(u, v, ddict)
+
+
+class OutMultiEdgeDataView(OutEdgeDataView):
+ """An EdgeDataView for outward edges of MultiDiGraph; See EdgeDataView"""
+
+ __slots__ = ("keys",)
+
+ def __getstate__(self):
+ return {
+ "viewer": self._viewer,
+ "nbunch": self._nbunch,
+ "keys": self.keys,
+ "data": self._data,
+ "default": self._default,
+ }
+
+ def __setstate__(self, state):
+ self.__init__(**state)
+
+ def __init__(self, viewer, nbunch=None, data=False, *, default=None, keys=False):
+ self._viewer = viewer
+ adjdict = self._adjdict = viewer._adjdict
+ self.keys = keys
+ if nbunch is None:
+ self._nodes_nbrs = adjdict.items
+ else:
+ # dict retains order of nodes but acts like a set
+ nbunch = dict.fromkeys(viewer._graph.nbunch_iter(nbunch))
+ self._nodes_nbrs = lambda: [(n, adjdict[n]) for n in nbunch]
+ self._nbunch = nbunch
+ self._data = data
+ self._default = default
+ # Set _report based on data and default
+ if data is True:
+ if keys is True:
+ self._report = lambda n, nbr, k, dd: (n, nbr, k, dd)
+ else:
+ self._report = lambda n, nbr, k, dd: (n, nbr, dd)
+ elif data is False:
+ if keys is True:
+ self._report = lambda n, nbr, k, dd: (n, nbr, k)
+ else:
+ self._report = lambda n, nbr, k, dd: (n, nbr)
+ else: # data is attribute name
+ if keys is True:
+ self._report = (
+ lambda n, nbr, k, dd: (n, nbr, k, dd[data])
+ if data in dd
+ else (n, nbr, k, default)
+ )
+ else:
+ self._report = (
+ lambda n, nbr, k, dd: (n, nbr, dd[data])
+ if data in dd
+ else (n, nbr, default)
+ )
+
+ def __len__(self):
+ return sum(1 for e in self)
+
+ def __iter__(self):
+ return (
+ self._report(n, nbr, k, dd)
+ for n, nbrs in self._nodes_nbrs()
+ for nbr, kd in nbrs.items()
+ for k, dd in kd.items()
+ )
+
+ def __contains__(self, e):
+ u, v = e[:2]
+ if self._nbunch is not None and u not in self._nbunch:
+ return False # this edge doesn't start in nbunch
+ try:
+ kdict = self._adjdict[u][v]
+ except KeyError:
+ return False
+ if self.keys is True:
+ k = e[2]
+ try:
+ dd = kdict[k]
+ except KeyError:
+ return False
+ return e == self._report(u, v, k, dd)
+ return any(e == self._report(u, v, k, dd) for k, dd in kdict.items())
+
+
+class MultiEdgeDataView(OutMultiEdgeDataView):
+ """An EdgeDataView class for edges of MultiGraph; See EdgeDataView"""
+
+ __slots__ = ()
+
+ def __iter__(self):
+ seen = {}
+ for n, nbrs in self._nodes_nbrs():
+ for nbr, kd in nbrs.items():
+ if nbr not in seen:
+ for k, dd in kd.items():
+ yield self._report(n, nbr, k, dd)
+ seen[n] = 1
+ del seen
+
+ def __contains__(self, e):
+ u, v = e[:2]
+ if self._nbunch is not None and u not in self._nbunch and v not in self._nbunch:
+ return False # this edge doesn't start and doesn't end in nbunch
+ try:
+ kdict = self._adjdict[u][v]
+ except KeyError:
+ try:
+ kdict = self._adjdict[v][u]
+ except KeyError:
+ return False
+ if self.keys is True:
+ k = e[2]
+ try:
+ dd = kdict[k]
+ except KeyError:
+ return False
+ return e == self._report(u, v, k, dd)
+ return any(e == self._report(u, v, k, dd) for k, dd in kdict.items())
+
+
+class InMultiEdgeDataView(OutMultiEdgeDataView):
+ """An EdgeDataView for inward edges of MultiDiGraph; See EdgeDataView"""
+
+ __slots__ = ()
+
+ def __iter__(self):
+ return (
+ self._report(nbr, n, k, dd)
+ for n, nbrs in self._nodes_nbrs()
+ for nbr, kd in nbrs.items()
+ for k, dd in kd.items()
+ )
+
+ def __contains__(self, e):
+ u, v = e[:2]
+ if self._nbunch is not None and v not in self._nbunch:
+ return False # this edge doesn't end in nbunch
+ try:
+ kdict = self._adjdict[v][u]
+ except KeyError:
+ return False
+ if self.keys is True:
+ k = e[2]
+ dd = kdict[k]
+ return e == self._report(u, v, k, dd)
+ return any(e == self._report(u, v, k, dd) for k, dd in kdict.items())
+
+
+# EdgeViews have set operations and no data reported
+class OutEdgeView(Set, Mapping, EdgeViewABC):
+ """A EdgeView class for outward edges of a DiGraph"""
+
+ __slots__ = ("_adjdict", "_graph", "_nodes_nbrs")
+
+ def __getstate__(self):
+ return {"_graph": self._graph, "_adjdict": self._adjdict}
+
+ def __setstate__(self, state):
+ self._graph = state["_graph"]
+ self._adjdict = state["_adjdict"]
+ self._nodes_nbrs = self._adjdict.items
+
+ @classmethod
+ def _from_iterable(cls, it):
+ return set(it)
+
+ dataview = OutEdgeDataView
+
+ def __init__(self, G):
+ self._graph = G
+ self._adjdict = G._succ if hasattr(G, "succ") else G._adj
+ self._nodes_nbrs = self._adjdict.items
+
+ # Set methods
+ def __len__(self):
+ return sum(len(nbrs) for n, nbrs in self._nodes_nbrs())
+
+ def __iter__(self):
+ for n, nbrs in self._nodes_nbrs():
+ for nbr in nbrs:
+ yield (n, nbr)
+
+ def __contains__(self, e):
+ try:
+ u, v = e
+ return v in self._adjdict[u]
+ except KeyError:
+ return False
+
+ # Mapping Methods
+ def __getitem__(self, e):
+ if isinstance(e, slice):
+ raise nx.NetworkXError(
+ f"{type(self).__name__} does not support slicing, "
+ f"try list(G.edges)[{e.start}:{e.stop}:{e.step}]"
+ )
+ u, v = e
+ try:
+ return self._adjdict[u][v]
+ except KeyError as ex: # Customize msg to indicate exception origin
+ raise KeyError(f"The edge {e} is not in the graph.")
+
+ # EdgeDataView methods
+ def __call__(self, nbunch=None, data=False, *, default=None):
+ if nbunch is None and data is False:
+ return self
+ return self.dataview(self, nbunch, data, default=default)
+
+ def data(self, data=True, default=None, nbunch=None):
+ """
+ Return a read-only view of edge data.
+
+ Parameters
+ ----------
+ data : bool or edge attribute key
+ If ``data=True``, then the data view maps each edge to a dictionary
+ containing all of its attributes. If `data` is a key in the edge
+ dictionary, then the data view maps each edge to its value for
+ the keyed attribute. In this case, if the edge doesn't have the
+ attribute, the `default` value is returned.
+ default : object, default=None
+ The value used when an edge does not have a specific attribute
+ nbunch : container of nodes, optional (default=None)
+ Allows restriction to edges only involving certain nodes. All edges
+ are considered by default.
+
+ Returns
+ -------
+ dataview
+ Returns an `EdgeDataView` for undirected Graphs, `OutEdgeDataView`
+ for DiGraphs, `MultiEdgeDataView` for MultiGraphs and
+ `OutMultiEdgeDataView` for MultiDiGraphs.
+
+ Notes
+ -----
+ If ``data=False``, returns an `EdgeView` without any edge data.
+
+ See Also
+ --------
+ EdgeDataView
+ OutEdgeDataView
+ MultiEdgeDataView
+ OutMultiEdgeDataView
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> G.add_edges_from(
+ ... [
+ ... (0, 1, {"dist": 3, "capacity": 20}),
+ ... (1, 2, {"dist": 4}),
+ ... (2, 0, {"dist": 5}),
+ ... ]
+ ... )
+
+ Accessing edge data with ``data=True`` (the default) returns an
+ edge data view object listing each edge with all of its attributes:
+
+ >>> G.edges.data()
+ EdgeDataView([(0, 1, {'dist': 3, 'capacity': 20}), (0, 2, {'dist': 5}), (1, 2, {'dist': 4})])
+
+ If `data` represents a key in the edge attribute dict, a dataview listing
+ each edge with its value for that specific key is returned:
+
+ >>> G.edges.data("dist")
+ EdgeDataView([(0, 1, 3), (0, 2, 5), (1, 2, 4)])
+
+ `nbunch` can be used to limit the edges:
+
+ >>> G.edges.data("dist", nbunch=[0])
+ EdgeDataView([(0, 1, 3), (0, 2, 5)])
+
+ If a specific key is not found in an edge attribute dict, the value
+ specified by `default` is used:
+
+ >>> G.edges.data("capacity")
+ EdgeDataView([(0, 1, 20), (0, 2, None), (1, 2, None)])
+
+ Note that there is no check that the `data` key is present in any of
+ the edge attribute dictionaries:
+
+ >>> G.edges.data("speed")
+ EdgeDataView([(0, 1, None), (0, 2, None), (1, 2, None)])
+ """
+ if nbunch is None and data is False:
+ return self
+ return self.dataview(self, nbunch, data, default=default)
+
+ # String Methods
+ def __str__(self):
+ return str(list(self))
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({list(self)})"
+
+
+class EdgeView(OutEdgeView):
+ """A EdgeView class for edges of a Graph
+
+ This densely packed View allows iteration over edges, data lookup
+ like a dict and set operations on edges represented by node-tuples.
+ In addition, edge data can be controlled by calling this object
+ possibly creating an EdgeDataView. Typically edges are iterated over
+ and reported as `(u, v)` node tuples or `(u, v, key)` node/key tuples
+ for multigraphs. Those edge representations can also be using to
+ lookup the data dict for any edge. Set operations also are available
+ where those tuples are the elements of the set.
+ Calling this object with optional arguments `data`, `default` and `keys`
+ controls the form of the tuple (see EdgeDataView). Optional argument
+ `nbunch` allows restriction to edges only involving certain nodes.
+
+ If `data is False` (the default) then iterate over 2-tuples `(u, v)`.
+ If `data is True` iterate over 3-tuples `(u, v, datadict)`.
+ Otherwise iterate over `(u, v, datadict.get(data, default))`.
+ For Multigraphs, if `keys is True`, replace `u, v` with `u, v, key` above.
+
+ Parameters
+ ==========
+ graph : NetworkX graph-like class
+ nbunch : (default= all nodes in graph) only report edges with these nodes
+ keys : (only for MultiGraph. default=False) report edge key in tuple
+ data : bool or string (default=False) see above
+ default : object (default=None)
+
+ Examples
+ ========
+ >>> G = nx.path_graph(4)
+ >>> EV = G.edges()
+ >>> (2, 3) in EV
+ True
+ >>> for u, v in EV:
+ ... print((u, v))
+ (0, 1)
+ (1, 2)
+ (2, 3)
+ >>> assert EV & {(1, 2), (3, 4)} == {(1, 2)}
+
+ >>> EVdata = G.edges(data="color", default="aqua")
+ >>> G.add_edge(2, 3, color="blue")
+ >>> assert (2, 3, "blue") in EVdata
+ >>> for u, v, c in EVdata:
+ ... print(f"({u}, {v}) has color: {c}")
+ (0, 1) has color: aqua
+ (1, 2) has color: aqua
+ (2, 3) has color: blue
+
+ >>> EVnbunch = G.edges(nbunch=2)
+ >>> assert (2, 3) in EVnbunch
+ >>> assert (0, 1) not in EVnbunch
+ >>> for u, v in EVnbunch:
+ ... assert u == 2 or v == 2
+
+ >>> MG = nx.path_graph(4, create_using=nx.MultiGraph)
+ >>> EVmulti = MG.edges(keys=True)
+ >>> (2, 3, 0) in EVmulti
+ True
+ >>> (2, 3) in EVmulti # 2-tuples work even when keys is True
+ True
+ >>> key = MG.add_edge(2, 3)
+ >>> for u, v, k in EVmulti:
+ ... print((u, v, k))
+ (0, 1, 0)
+ (1, 2, 0)
+ (2, 3, 0)
+ (2, 3, 1)
+ """
+
+ __slots__ = ()
+
+ dataview = EdgeDataView
+
+ def __len__(self):
+ num_nbrs = (len(nbrs) + (n in nbrs) for n, nbrs in self._nodes_nbrs())
+ return sum(num_nbrs) // 2
+
+ def __iter__(self):
+ seen = {}
+ for n, nbrs in self._nodes_nbrs():
+ for nbr in list(nbrs):
+ if nbr not in seen:
+ yield (n, nbr)
+ seen[n] = 1
+ del seen
+
+ def __contains__(self, e):
+ try:
+ u, v = e[:2]
+ return v in self._adjdict[u] or u in self._adjdict[v]
+ except (KeyError, ValueError):
+ return False
+
+
+class InEdgeView(OutEdgeView):
+ """A EdgeView class for inward edges of a DiGraph"""
+
+ __slots__ = ()
+
+ def __setstate__(self, state):
+ self._graph = state["_graph"]
+ self._adjdict = state["_adjdict"]
+ self._nodes_nbrs = self._adjdict.items
+
+ dataview = InEdgeDataView
+
+ def __init__(self, G):
+ self._graph = G
+ self._adjdict = G._pred if hasattr(G, "pred") else G._adj
+ self._nodes_nbrs = self._adjdict.items
+
+ def __iter__(self):
+ for n, nbrs in self._nodes_nbrs():
+ for nbr in nbrs:
+ yield (nbr, n)
+
+ def __contains__(self, e):
+ try:
+ u, v = e
+ return u in self._adjdict[v]
+ except KeyError:
+ return False
+
+ def __getitem__(self, e):
+ if isinstance(e, slice):
+ raise nx.NetworkXError(
+ f"{type(self).__name__} does not support slicing, "
+ f"try list(G.in_edges)[{e.start}:{e.stop}:{e.step}]"
+ )
+ u, v = e
+ return self._adjdict[v][u]
+
+
+class OutMultiEdgeView(OutEdgeView):
+ """A EdgeView class for outward edges of a MultiDiGraph"""
+
+ __slots__ = ()
+
+ dataview = OutMultiEdgeDataView
+
+ def __len__(self):
+ return sum(
+ len(kdict) for n, nbrs in self._nodes_nbrs() for nbr, kdict in nbrs.items()
+ )
+
+ def __iter__(self):
+ for n, nbrs in self._nodes_nbrs():
+ for nbr, kdict in nbrs.items():
+ for key in kdict:
+ yield (n, nbr, key)
+
+ def __contains__(self, e):
+ N = len(e)
+ if N == 3:
+ u, v, k = e
+ elif N == 2:
+ u, v = e
+ k = 0
+ else:
+ raise ValueError("MultiEdge must have length 2 or 3")
+ try:
+ return k in self._adjdict[u][v]
+ except KeyError:
+ return False
+
+ def __getitem__(self, e):
+ if isinstance(e, slice):
+ raise nx.NetworkXError(
+ f"{type(self).__name__} does not support slicing, "
+ f"try list(G.edges)[{e.start}:{e.stop}:{e.step}]"
+ )
+ u, v, k = e
+ return self._adjdict[u][v][k]
+
+ def __call__(self, nbunch=None, data=False, *, default=None, keys=False):
+ if nbunch is None and data is False and keys is True:
+ return self
+ return self.dataview(self, nbunch, data, default=default, keys=keys)
+
+ def data(self, data=True, default=None, nbunch=None, keys=False):
+ if nbunch is None and data is False and keys is True:
+ return self
+ return self.dataview(self, nbunch, data, default=default, keys=keys)
+
+
+class MultiEdgeView(OutMultiEdgeView):
+ """A EdgeView class for edges of a MultiGraph"""
+
+ __slots__ = ()
+
+ dataview = MultiEdgeDataView
+
+ def __len__(self):
+ return sum(1 for e in self)
+
+ def __iter__(self):
+ seen = {}
+ for n, nbrs in self._nodes_nbrs():
+ for nbr, kd in nbrs.items():
+ if nbr not in seen:
+ for k, dd in kd.items():
+ yield (n, nbr, k)
+ seen[n] = 1
+ del seen
+
+
+class InMultiEdgeView(OutMultiEdgeView):
+ """A EdgeView class for inward edges of a MultiDiGraph"""
+
+ __slots__ = ()
+
+ def __setstate__(self, state):
+ self._graph = state["_graph"]
+ self._adjdict = state["_adjdict"]
+ self._nodes_nbrs = self._adjdict.items
+
+ dataview = InMultiEdgeDataView
+
+ def __init__(self, G):
+ self._graph = G
+ self._adjdict = G._pred if hasattr(G, "pred") else G._adj
+ self._nodes_nbrs = self._adjdict.items
+
+ def __iter__(self):
+ for n, nbrs in self._nodes_nbrs():
+ for nbr, kdict in nbrs.items():
+ for key in kdict:
+ yield (nbr, n, key)
+
+ def __contains__(self, e):
+ N = len(e)
+ if N == 3:
+ u, v, k = e
+ elif N == 2:
+ u, v = e
+ k = 0
+ else:
+ raise ValueError("MultiEdge must have length 2 or 3")
+ try:
+ return k in self._adjdict[v][u]
+ except KeyError:
+ return False
+
+ def __getitem__(self, e):
+ if isinstance(e, slice):
+ raise nx.NetworkXError(
+ f"{type(self).__name__} does not support slicing, "
+ f"try list(G.in_edges)[{e.start}:{e.stop}:{e.step}]"
+ )
+ u, v, k = e
+ return self._adjdict[v][u][k]
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/dispatch_interface.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/dispatch_interface.py
new file mode 100644
index 00000000..5cc908d7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/dispatch_interface.py
@@ -0,0 +1,185 @@
+# This file contains utilities for testing the dispatching feature
+
+# A full test of all dispatchable algorithms is performed by
+# modifying the pytest invocation and setting an environment variable
+# NETWORKX_TEST_BACKEND=nx_loopback pytest
+# This is comprehensive, but only tests the `test_override_dispatch`
+# function in networkx.classes.backends.
+
+# To test the `_dispatchable` function directly, several tests scattered throughout
+# NetworkX have been augmented to test normal and dispatch mode.
+# Searching for `dispatch_interface` should locate the specific tests.
+
+import networkx as nx
+from networkx import DiGraph, Graph, MultiDiGraph, MultiGraph, PlanarEmbedding
+from networkx.classes.reportviews import NodeView
+
+
+class LoopbackGraph(Graph):
+ __networkx_backend__ = "nx_loopback"
+
+
+class LoopbackDiGraph(DiGraph):
+ __networkx_backend__ = "nx_loopback"
+
+
+class LoopbackMultiGraph(MultiGraph):
+ __networkx_backend__ = "nx_loopback"
+
+
+class LoopbackMultiDiGraph(MultiDiGraph):
+ __networkx_backend__ = "nx_loopback"
+
+
+class LoopbackPlanarEmbedding(PlanarEmbedding):
+ __networkx_backend__ = "nx_loopback"
+
+
+def convert(graph):
+ if isinstance(graph, PlanarEmbedding):
+ return LoopbackPlanarEmbedding(graph)
+ if isinstance(graph, MultiDiGraph):
+ return LoopbackMultiDiGraph(graph)
+ if isinstance(graph, MultiGraph):
+ return LoopbackMultiGraph(graph)
+ if isinstance(graph, DiGraph):
+ return LoopbackDiGraph(graph)
+ if isinstance(graph, Graph):
+ return LoopbackGraph(graph)
+ raise TypeError(f"Unsupported type of graph: {type(graph)}")
+
+
+class LoopbackBackendInterface:
+ def __getattr__(self, item):
+ try:
+ return nx.utils.backends._registered_algorithms[item].orig_func
+ except KeyError:
+ raise AttributeError(item) from None
+
+ @staticmethod
+ def convert_from_nx(
+ graph,
+ *,
+ edge_attrs=None,
+ node_attrs=None,
+ preserve_edge_attrs=None,
+ preserve_node_attrs=None,
+ preserve_graph_attrs=None,
+ name=None,
+ graph_name=None,
+ ):
+ if name in {
+ # Raise if input graph changes. See test_dag.py::test_topological_sort6
+ "lexicographical_topological_sort",
+ "topological_generations",
+ "topological_sort",
+ # Would be nice to some day avoid these cutoffs of full testing
+ }:
+ return graph
+ if isinstance(graph, NodeView):
+ # Convert to a Graph with only nodes (no edges)
+ new_graph = Graph()
+ new_graph.add_nodes_from(graph.items())
+ graph = new_graph
+ G = LoopbackGraph()
+ elif not isinstance(graph, Graph):
+ raise TypeError(
+ f"Bad type for graph argument {graph_name} in {name}: {type(graph)}"
+ )
+ elif graph.__class__ in {Graph, LoopbackGraph}:
+ G = LoopbackGraph()
+ elif graph.__class__ in {DiGraph, LoopbackDiGraph}:
+ G = LoopbackDiGraph()
+ elif graph.__class__ in {MultiGraph, LoopbackMultiGraph}:
+ G = LoopbackMultiGraph()
+ elif graph.__class__ in {MultiDiGraph, LoopbackMultiDiGraph}:
+ G = LoopbackMultiDiGraph()
+ elif graph.__class__ in {PlanarEmbedding, LoopbackPlanarEmbedding}:
+ G = LoopbackDiGraph() # or LoopbackPlanarEmbedding
+ else:
+ # Would be nice to handle these better some day
+ # nx.algorithms.approximation.kcomponents._AntiGraph
+ # nx.classes.tests.test_multidigraph.MultiDiGraphSubClass
+ # nx.classes.tests.test_multigraph.MultiGraphSubClass
+ G = graph.__class__()
+
+ if preserve_graph_attrs:
+ G.graph.update(graph.graph)
+
+ # add nodes
+ G.add_nodes_from(graph)
+ if preserve_node_attrs:
+ for n, dd in G._node.items():
+ dd.update(graph.nodes[n])
+ elif node_attrs:
+ for n, dd in G._node.items():
+ dd.update(
+ (attr, graph._node[n].get(attr, default))
+ for attr, default in node_attrs.items()
+ if default is not None or attr in graph._node[n]
+ )
+
+ # tools to build datadict and keydict
+ if preserve_edge_attrs:
+
+ def G_new_datadict(old_dd):
+ return G.edge_attr_dict_factory(old_dd)
+ elif edge_attrs:
+
+ def G_new_datadict(old_dd):
+ return G.edge_attr_dict_factory(
+ (attr, old_dd.get(attr, default))
+ for attr, default in edge_attrs.items()
+ if default is not None or attr in old_dd
+ )
+ else:
+
+ def G_new_datadict(old_dd):
+ return G.edge_attr_dict_factory()
+
+ if G.is_multigraph():
+
+ def G_new_inner(keydict):
+ kd = G.adjlist_inner_dict_factory(
+ (k, G_new_datadict(dd)) for k, dd in keydict.items()
+ )
+ return kd
+ else:
+ G_new_inner = G_new_datadict
+
+ # add edges keeping the same order in _adj and _pred
+ G_adj = G._adj
+ if G.is_directed():
+ for n, nbrs in graph._adj.items():
+ G_adj[n].update((nbr, G_new_inner(dd)) for nbr, dd in nbrs.items())
+ # ensure same datadict for pred and adj; and pred order of graph._pred
+ G_pred = G._pred
+ for n, nbrs in graph._pred.items():
+ G_pred[n].update((nbr, G_adj[nbr][n]) for nbr in nbrs)
+ else: # undirected
+ for n, nbrs in graph._adj.items():
+ # ensure same datadict for both ways; and adj order of graph._adj
+ G_adj[n].update(
+ (nbr, G_adj[nbr][n] if n in G_adj[nbr] else G_new_inner(dd))
+ for nbr, dd in nbrs.items()
+ )
+
+ return G
+
+ @staticmethod
+ def convert_to_nx(obj, *, name=None):
+ return obj
+
+ @staticmethod
+ def on_start_tests(items):
+ # Verify that items can be xfailed
+ for item in items:
+ assert hasattr(item, "add_marker")
+
+ def can_run(self, name, args, kwargs):
+ # It is unnecessary to define this function if algorithms are fully supported.
+ # We include it for illustration purposes.
+ return hasattr(self, name)
+
+
+backend_interface = LoopbackBackendInterface()
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/historical_tests.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/historical_tests.py
new file mode 100644
index 00000000..9dad24e2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/historical_tests.py
@@ -0,0 +1,475 @@
+"""Original NetworkX graph tests"""
+
+import pytest
+
+import networkx as nx
+from networkx import convert_node_labels_to_integers as cnlti
+from networkx.utils import edges_equal, nodes_equal
+
+
+class HistoricalTests:
+ @classmethod
+ def setup_class(cls):
+ cls.null = nx.null_graph()
+ cls.P1 = cnlti(nx.path_graph(1), first_label=1)
+ cls.P3 = cnlti(nx.path_graph(3), first_label=1)
+ cls.P10 = cnlti(nx.path_graph(10), first_label=1)
+ cls.K1 = cnlti(nx.complete_graph(1), first_label=1)
+ cls.K3 = cnlti(nx.complete_graph(3), first_label=1)
+ cls.K4 = cnlti(nx.complete_graph(4), first_label=1)
+ cls.K5 = cnlti(nx.complete_graph(5), first_label=1)
+ cls.K10 = cnlti(nx.complete_graph(10), first_label=1)
+ cls.G = nx.Graph
+
+ def test_name(self):
+ G = self.G(name="test")
+ assert G.name == "test"
+ H = self.G()
+ assert H.name == ""
+
+ # Nodes
+
+ def test_add_remove_node(self):
+ G = self.G()
+ G.add_node("A")
+ assert G.has_node("A")
+ G.remove_node("A")
+ assert not G.has_node("A")
+
+ def test_nonhashable_node(self):
+ # Test if a non-hashable object is in the Graph. A python dict will
+ # raise a TypeError, but for a Graph class a simple False should be
+ # returned (see Graph __contains__). If it cannot be a node then it is
+ # not a node.
+ G = self.G()
+ assert not G.has_node(["A"])
+ assert not G.has_node({"A": 1})
+
+ def test_add_nodes_from(self):
+ G = self.G()
+ G.add_nodes_from(list("ABCDEFGHIJKL"))
+ assert G.has_node("L")
+ G.remove_nodes_from(["H", "I", "J", "K", "L"])
+ G.add_nodes_from([1, 2, 3, 4])
+ assert sorted(G.nodes(), key=str) == [
+ 1,
+ 2,
+ 3,
+ 4,
+ "A",
+ "B",
+ "C",
+ "D",
+ "E",
+ "F",
+ "G",
+ ]
+ # test __iter__
+ assert sorted(G, key=str) == [1, 2, 3, 4, "A", "B", "C", "D", "E", "F", "G"]
+
+ def test_contains(self):
+ G = self.G()
+ G.add_node("A")
+ assert "A" in G
+ assert [] not in G # never raise a Key or TypeError in this test
+ assert {1: 1} not in G
+
+ def test_add_remove(self):
+ # Test add_node and remove_node acting for various nbunch
+ G = self.G()
+ G.add_node("m")
+ assert G.has_node("m")
+ G.add_node("m") # no complaints
+ pytest.raises(nx.NetworkXError, G.remove_node, "j")
+ G.remove_node("m")
+ assert list(G) == []
+
+ def test_nbunch_is_list(self):
+ G = self.G()
+ G.add_nodes_from(list("ABCD"))
+ G.add_nodes_from(self.P3) # add nbunch of nodes (nbunch=Graph)
+ assert sorted(G.nodes(), key=str) == [1, 2, 3, "A", "B", "C", "D"]
+ G.remove_nodes_from(self.P3) # remove nbunch of nodes (nbunch=Graph)
+ assert sorted(G.nodes(), key=str) == ["A", "B", "C", "D"]
+
+ def test_nbunch_is_set(self):
+ G = self.G()
+ nbunch = set("ABCDEFGHIJKL")
+ G.add_nodes_from(nbunch)
+ assert G.has_node("L")
+
+ def test_nbunch_dict(self):
+ # nbunch is a dict with nodes as keys
+ G = self.G()
+ nbunch = set("ABCDEFGHIJKL")
+ G.add_nodes_from(nbunch)
+ nbunch = {"I": "foo", "J": 2, "K": True, "L": "spam"}
+ G.remove_nodes_from(nbunch)
+ assert sorted(G.nodes(), key=str), ["A", "B", "C", "D", "E", "F", "G", "H"]
+
+ def test_nbunch_iterator(self):
+ G = self.G()
+ G.add_nodes_from(["A", "B", "C", "D", "E", "F", "G", "H"])
+ n_iter = self.P3.nodes()
+ G.add_nodes_from(n_iter)
+ assert sorted(G.nodes(), key=str) == [
+ 1,
+ 2,
+ 3,
+ "A",
+ "B",
+ "C",
+ "D",
+ "E",
+ "F",
+ "G",
+ "H",
+ ]
+ n_iter = self.P3.nodes() # rebuild same iterator
+ G.remove_nodes_from(n_iter) # remove nbunch of nodes (nbunch=iterator)
+ assert sorted(G.nodes(), key=str) == ["A", "B", "C", "D", "E", "F", "G", "H"]
+
+ def test_nbunch_graph(self):
+ G = self.G()
+ G.add_nodes_from(["A", "B", "C", "D", "E", "F", "G", "H"])
+ nbunch = self.K3
+ G.add_nodes_from(nbunch)
+ assert sorted(G.nodes(), key=str), [
+ 1,
+ 2,
+ 3,
+ "A",
+ "B",
+ "C",
+ "D",
+ "E",
+ "F",
+ "G",
+ "H",
+ ]
+
+ # Edges
+
+ def test_add_edge(self):
+ G = self.G()
+ pytest.raises(TypeError, G.add_edge, "A")
+
+ G.add_edge("A", "B") # testing add_edge()
+ G.add_edge("A", "B") # should fail silently
+ assert G.has_edge("A", "B")
+ assert not G.has_edge("A", "C")
+ assert G.has_edge(*("A", "B"))
+ if G.is_directed():
+ assert not G.has_edge("B", "A")
+ else:
+ # G is undirected, so B->A is an edge
+ assert G.has_edge("B", "A")
+
+ G.add_edge("A", "C") # test directedness
+ G.add_edge("C", "A")
+ G.remove_edge("C", "A")
+ if G.is_directed():
+ assert G.has_edge("A", "C")
+ else:
+ assert not G.has_edge("A", "C")
+ assert not G.has_edge("C", "A")
+
+ def test_self_loop(self):
+ G = self.G()
+ G.add_edge("A", "A") # test self loops
+ assert G.has_edge("A", "A")
+ G.remove_edge("A", "A")
+ G.add_edge("X", "X")
+ assert G.has_node("X")
+ G.remove_node("X")
+ G.add_edge("A", "Z") # should add the node silently
+ assert G.has_node("Z")
+
+ def test_add_edges_from(self):
+ G = self.G()
+ G.add_edges_from([("B", "C")]) # test add_edges_from()
+ assert G.has_edge("B", "C")
+ if G.is_directed():
+ assert not G.has_edge("C", "B")
+ else:
+ assert G.has_edge("C", "B") # undirected
+
+ G.add_edges_from([("D", "F"), ("B", "D")])
+ assert G.has_edge("D", "F")
+ assert G.has_edge("B", "D")
+
+ if G.is_directed():
+ assert not G.has_edge("D", "B")
+ else:
+ assert G.has_edge("D", "B") # undirected
+
+ def test_add_edges_from2(self):
+ G = self.G()
+ # after failing silently, should add 2nd edge
+ G.add_edges_from([tuple("IJ"), list("KK"), tuple("JK")])
+ assert G.has_edge(*("I", "J"))
+ assert G.has_edge(*("K", "K"))
+ assert G.has_edge(*("J", "K"))
+ if G.is_directed():
+ assert not G.has_edge(*("K", "J"))
+ else:
+ assert G.has_edge(*("K", "J"))
+
+ def test_add_edges_from3(self):
+ G = self.G()
+ G.add_edges_from(zip(list("ACD"), list("CDE")))
+ assert G.has_edge("D", "E")
+ assert not G.has_edge("E", "C")
+
+ def test_remove_edge(self):
+ G = self.G()
+ G.add_nodes_from([1, 2, 3, "A", "B", "C", "D", "E", "F", "G", "H"])
+
+ G.add_edges_from(zip(list("MNOP"), list("NOPM")))
+ assert G.has_edge("O", "P")
+ assert G.has_edge("P", "M")
+ G.remove_node("P") # tests remove_node()'s handling of edges.
+ assert not G.has_edge("P", "M")
+ pytest.raises(TypeError, G.remove_edge, "M")
+
+ G.add_edge("N", "M")
+ assert G.has_edge("M", "N")
+ G.remove_edge("M", "N")
+ assert not G.has_edge("M", "N")
+
+ # self loop fails silently
+ G.remove_edges_from([list("HI"), list("DF"), tuple("KK"), tuple("JK")])
+ assert not G.has_edge("H", "I")
+ assert not G.has_edge("J", "K")
+ G.remove_edges_from([list("IJ"), list("KK"), list("JK")])
+ assert not G.has_edge("I", "J")
+ G.remove_nodes_from(set("ZEFHIMNO"))
+ G.add_edge("J", "K")
+
+ def test_edges_nbunch(self):
+ # Test G.edges(nbunch) with various forms of nbunch
+ G = self.G()
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")])
+ # node not in nbunch should be quietly ignored
+ pytest.raises(nx.NetworkXError, G.edges, 6)
+ assert list(G.edges("Z")) == [] # iterable non-node
+ # nbunch can be an empty list
+ assert list(G.edges([])) == []
+ if G.is_directed():
+ elist = [("A", "B"), ("A", "C"), ("B", "D")]
+ else:
+ elist = [("A", "B"), ("A", "C"), ("B", "C"), ("B", "D")]
+ # nbunch can be a list
+ assert edges_equal(list(G.edges(["A", "B"])), elist)
+ # nbunch can be a set
+ assert edges_equal(G.edges({"A", "B"}), elist)
+ # nbunch can be a graph
+ G1 = self.G()
+ G1.add_nodes_from("AB")
+ assert edges_equal(G.edges(G1), elist)
+ # nbunch can be a dict with nodes as keys
+ ndict = {"A": "thing1", "B": "thing2"}
+ assert edges_equal(G.edges(ndict), elist)
+ # nbunch can be a single node
+ assert edges_equal(list(G.edges("A")), [("A", "B"), ("A", "C")])
+ assert nodes_equal(sorted(G), ["A", "B", "C", "D"])
+
+ # nbunch can be nothing (whole graph)
+ assert edges_equal(
+ list(G.edges()),
+ [("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")],
+ )
+
+ def test_degree(self):
+ G = self.G()
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")])
+ assert G.degree("A") == 2
+
+ # degree of single node in iterable container must return dict
+ assert list(G.degree(["A"])) == [("A", 2)]
+ assert sorted(d for n, d in G.degree(["A", "B"])) == [2, 3]
+ assert sorted(d for n, d in G.degree()) == [2, 2, 3, 3]
+
+ def test_degree2(self):
+ H = self.G()
+ H.add_edges_from([(1, 24), (1, 2)])
+ assert sorted(d for n, d in H.degree([1, 24])) == [1, 2]
+
+ def test_degree_graph(self):
+ P3 = nx.path_graph(3)
+ P5 = nx.path_graph(5)
+ # silently ignore nodes not in P3
+ assert dict(d for n, d in P3.degree(["A", "B"])) == {}
+ # nbunch can be a graph
+ assert sorted(d for n, d in P5.degree(P3)) == [1, 2, 2]
+ # nbunch can be a graph that's way too big
+ assert sorted(d for n, d in P3.degree(P5)) == [1, 1, 2]
+ assert list(P5.degree([])) == []
+ assert dict(P5.degree([])) == {}
+
+ def test_null(self):
+ null = nx.null_graph()
+ assert list(null.degree()) == []
+ assert dict(null.degree()) == {}
+
+ def test_order_size(self):
+ G = self.G()
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")])
+ assert G.order() == 4
+ assert G.size() == 5
+ assert G.number_of_edges() == 5
+ assert G.number_of_edges("A", "B") == 1
+ assert G.number_of_edges("A", "D") == 0
+
+ def test_copy(self):
+ G = self.G()
+ H = G.copy() # copy
+ assert H.adj == G.adj
+ assert H.name == G.name
+ assert H is not G
+
+ def test_subgraph(self):
+ G = self.G()
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")])
+ SG = G.subgraph(["A", "B", "D"])
+ assert nodes_equal(list(SG), ["A", "B", "D"])
+ assert edges_equal(list(SG.edges()), [("A", "B"), ("B", "D")])
+
+ def test_to_directed(self):
+ G = self.G()
+ if not G.is_directed():
+ G.add_edges_from(
+ [("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")]
+ )
+
+ DG = G.to_directed()
+ assert DG is not G # directed copy or copy
+
+ assert DG.is_directed()
+ assert DG.name == G.name
+ assert DG.adj == G.adj
+ assert sorted(DG.out_edges(list("AB"))) == [
+ ("A", "B"),
+ ("A", "C"),
+ ("B", "A"),
+ ("B", "C"),
+ ("B", "D"),
+ ]
+ DG.remove_edge("A", "B")
+ assert DG.has_edge("B", "A") # this removes B-A but not A-B
+ assert not DG.has_edge("A", "B")
+
+ def test_to_undirected(self):
+ G = self.G()
+ if G.is_directed():
+ G.add_edges_from(
+ [("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")]
+ )
+ UG = G.to_undirected() # to_undirected
+ assert UG is not G
+ assert not UG.is_directed()
+ assert G.is_directed()
+ assert UG.name == G.name
+ assert UG.adj != G.adj
+ assert sorted(UG.edges(list("AB"))) == [
+ ("A", "B"),
+ ("A", "C"),
+ ("B", "C"),
+ ("B", "D"),
+ ]
+ assert sorted(UG.edges(["A", "B"])) == [
+ ("A", "B"),
+ ("A", "C"),
+ ("B", "C"),
+ ("B", "D"),
+ ]
+ UG.remove_edge("A", "B")
+ assert not UG.has_edge("B", "A")
+ assert not UG.has_edge("A", "B")
+
+ def test_neighbors(self):
+ G = self.G()
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")])
+ G.add_nodes_from("GJK")
+ assert sorted(G["A"]) == ["B", "C"]
+ assert sorted(G.neighbors("A")) == ["B", "C"]
+ assert sorted(G.neighbors("A")) == ["B", "C"]
+ assert sorted(G.neighbors("G")) == []
+ pytest.raises(nx.NetworkXError, G.neighbors, "j")
+
+ def test_iterators(self):
+ G = self.G()
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")])
+ G.add_nodes_from("GJK")
+ assert sorted(G.nodes()) == ["A", "B", "C", "D", "G", "J", "K"]
+ assert edges_equal(
+ G.edges(), [("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")]
+ )
+
+ assert sorted(v for k, v in G.degree()) == [0, 0, 0, 2, 2, 3, 3]
+ assert sorted(G.degree(), key=str) == [
+ ("A", 2),
+ ("B", 3),
+ ("C", 3),
+ ("D", 2),
+ ("G", 0),
+ ("J", 0),
+ ("K", 0),
+ ]
+ assert sorted(G.neighbors("A")) == ["B", "C"]
+ pytest.raises(nx.NetworkXError, G.neighbors, "X")
+ G.clear()
+ assert nx.number_of_nodes(G) == 0
+ assert nx.number_of_edges(G) == 0
+
+ def test_null_subgraph(self):
+ # Subgraph of a null graph is a null graph
+ nullgraph = nx.null_graph()
+ G = nx.null_graph()
+ H = G.subgraph([])
+ assert nx.is_isomorphic(H, nullgraph)
+
+ def test_empty_subgraph(self):
+ # Subgraph of an empty graph is an empty graph. test 1
+ nullgraph = nx.null_graph()
+ E5 = nx.empty_graph(5)
+ E10 = nx.empty_graph(10)
+ H = E10.subgraph([])
+ assert nx.is_isomorphic(H, nullgraph)
+ H = E10.subgraph([1, 2, 3, 4, 5])
+ assert nx.is_isomorphic(H, E5)
+
+ def test_complete_subgraph(self):
+ # Subgraph of a complete graph is a complete graph
+ K1 = nx.complete_graph(1)
+ K3 = nx.complete_graph(3)
+ K5 = nx.complete_graph(5)
+ H = K5.subgraph([1, 2, 3])
+ assert nx.is_isomorphic(H, K3)
+
+ def test_subgraph_nbunch(self):
+ nullgraph = nx.null_graph()
+ K1 = nx.complete_graph(1)
+ K3 = nx.complete_graph(3)
+ K5 = nx.complete_graph(5)
+ # Test G.subgraph(nbunch), where nbunch is a single node
+ H = K5.subgraph(1)
+ assert nx.is_isomorphic(H, K1)
+ # Test G.subgraph(nbunch), where nbunch is a set
+ H = K5.subgraph({1})
+ assert nx.is_isomorphic(H, K1)
+ # Test G.subgraph(nbunch), where nbunch is an iterator
+ H = K5.subgraph(iter(K3))
+ assert nx.is_isomorphic(H, K3)
+ # Test G.subgraph(nbunch), where nbunch is another graph
+ H = K5.subgraph(K3)
+ assert nx.is_isomorphic(H, K3)
+ H = K5.subgraph([9])
+ assert nx.is_isomorphic(H, nullgraph)
+
+ def test_node_tuple_issue(self):
+ H = self.G()
+ # Test error handling of tuple as a node
+ pytest.raises(nx.NetworkXError, H.remove_node, (1, 2))
+ H.remove_nodes_from([(1, 2)]) # no error
+ pytest.raises(nx.NetworkXError, H.neighbors, (1, 2))
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_coreviews.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_coreviews.py
new file mode 100644
index 00000000..24de7f2f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_coreviews.py
@@ -0,0 +1,362 @@
+import pickle
+
+import pytest
+
+import networkx as nx
+
+
+class TestAtlasView:
+ # node->data
+ def setup_method(self):
+ self.d = {0: {"color": "blue", "weight": 1.2}, 1: {}, 2: {"color": 1}}
+ self.av = nx.classes.coreviews.AtlasView(self.d)
+
+ def test_pickle(self):
+ view = self.av
+ pview = pickle.loads(pickle.dumps(view, -1))
+ assert view == pview
+ assert view.__slots__ == pview.__slots__
+ pview = pickle.loads(pickle.dumps(view))
+ assert view == pview
+ assert view.__slots__ == pview.__slots__
+
+ def test_len(self):
+ assert len(self.av) == len(self.d)
+
+ def test_iter(self):
+ assert list(self.av) == list(self.d)
+
+ def test_getitem(self):
+ assert self.av[1] is self.d[1]
+ assert self.av[2]["color"] == 1
+ pytest.raises(KeyError, self.av.__getitem__, 3)
+
+ def test_copy(self):
+ avcopy = self.av.copy()
+ assert avcopy[0] == self.av[0]
+ assert avcopy == self.av
+ assert avcopy[0] is not self.av[0]
+ assert avcopy is not self.av
+ avcopy[5] = {}
+ assert avcopy != self.av
+
+ avcopy[0]["ht"] = 4
+ assert avcopy[0] != self.av[0]
+ self.av[0]["ht"] = 4
+ assert avcopy[0] == self.av[0]
+ del self.av[0]["ht"]
+
+ assert not hasattr(self.av, "__setitem__")
+
+ def test_items(self):
+ assert sorted(self.av.items()) == sorted(self.d.items())
+
+ def test_str(self):
+ out = str(self.d)
+ assert str(self.av) == out
+
+ def test_repr(self):
+ out = "AtlasView(" + str(self.d) + ")"
+ assert repr(self.av) == out
+
+
+class TestAdjacencyView:
+ # node->nbr->data
+ def setup_method(self):
+ dd = {"color": "blue", "weight": 1.2}
+ self.nd = {0: dd, 1: {}, 2: {"color": 1}}
+ self.adj = {3: self.nd, 0: {3: dd}, 1: {}, 2: {3: {"color": 1}}}
+ self.adjview = nx.classes.coreviews.AdjacencyView(self.adj)
+
+ def test_pickle(self):
+ view = self.adjview
+ pview = pickle.loads(pickle.dumps(view, -1))
+ assert view == pview
+ assert view.__slots__ == pview.__slots__
+
+ def test_len(self):
+ assert len(self.adjview) == len(self.adj)
+
+ def test_iter(self):
+ assert list(self.adjview) == list(self.adj)
+
+ def test_getitem(self):
+ assert self.adjview[1] is not self.adj[1]
+ assert self.adjview[3][0] is self.adjview[0][3]
+ assert self.adjview[2][3]["color"] == 1
+ pytest.raises(KeyError, self.adjview.__getitem__, 4)
+
+ def test_copy(self):
+ avcopy = self.adjview.copy()
+ assert avcopy[0] == self.adjview[0]
+ assert avcopy[0] is not self.adjview[0]
+
+ avcopy[2][3]["ht"] = 4
+ assert avcopy[2] != self.adjview[2]
+ self.adjview[2][3]["ht"] = 4
+ assert avcopy[2] == self.adjview[2]
+ del self.adjview[2][3]["ht"]
+
+ assert not hasattr(self.adjview, "__setitem__")
+
+ def test_items(self):
+ view_items = sorted((n, dict(d)) for n, d in self.adjview.items())
+ assert view_items == sorted(self.adj.items())
+
+ def test_str(self):
+ out = str(dict(self.adj))
+ assert str(self.adjview) == out
+
+ def test_repr(self):
+ out = self.adjview.__class__.__name__ + "(" + str(self.adj) + ")"
+ assert repr(self.adjview) == out
+
+
+class TestMultiAdjacencyView(TestAdjacencyView):
+ # node->nbr->key->data
+ def setup_method(self):
+ dd = {"color": "blue", "weight": 1.2}
+ self.kd = {0: dd, 1: {}, 2: {"color": 1}}
+ self.nd = {3: self.kd, 0: {3: dd}, 1: {0: {}}, 2: {3: {"color": 1}}}
+ self.adj = {3: self.nd, 0: {3: {3: dd}}, 1: {}, 2: {3: {8: {}}}}
+ self.adjview = nx.classes.coreviews.MultiAdjacencyView(self.adj)
+
+ def test_getitem(self):
+ assert self.adjview[1] is not self.adj[1]
+ assert self.adjview[3][0][3] is self.adjview[0][3][3]
+ assert self.adjview[3][2][3]["color"] == 1
+ pytest.raises(KeyError, self.adjview.__getitem__, 4)
+
+ def test_copy(self):
+ avcopy = self.adjview.copy()
+ assert avcopy[0] == self.adjview[0]
+ assert avcopy[0] is not self.adjview[0]
+
+ avcopy[2][3][8]["ht"] = 4
+ assert avcopy[2] != self.adjview[2]
+ self.adjview[2][3][8]["ht"] = 4
+ assert avcopy[2] == self.adjview[2]
+ del self.adjview[2][3][8]["ht"]
+
+ assert not hasattr(self.adjview, "__setitem__")
+
+
+class TestUnionAtlas:
+ # node->data
+ def setup_method(self):
+ self.s = {0: {"color": "blue", "weight": 1.2}, 1: {}, 2: {"color": 1}}
+ self.p = {3: {"color": "blue", "weight": 1.2}, 4: {}, 2: {"watch": 2}}
+ self.av = nx.classes.coreviews.UnionAtlas(self.s, self.p)
+
+ def test_pickle(self):
+ view = self.av
+ pview = pickle.loads(pickle.dumps(view, -1))
+ assert view == pview
+ assert view.__slots__ == pview.__slots__
+
+ def test_len(self):
+ assert len(self.av) == len(self.s.keys() | self.p.keys()) == 5
+
+ def test_iter(self):
+ assert set(self.av) == set(self.s) | set(self.p)
+
+ def test_getitem(self):
+ assert self.av[0] is self.s[0]
+ assert self.av[4] is self.p[4]
+ assert self.av[2]["color"] == 1
+ pytest.raises(KeyError, self.av[2].__getitem__, "watch")
+ pytest.raises(KeyError, self.av.__getitem__, 8)
+
+ def test_copy(self):
+ avcopy = self.av.copy()
+ assert avcopy[0] == self.av[0]
+ assert avcopy[0] is not self.av[0]
+ assert avcopy is not self.av
+ avcopy[5] = {}
+ assert avcopy != self.av
+
+ avcopy[0]["ht"] = 4
+ assert avcopy[0] != self.av[0]
+ self.av[0]["ht"] = 4
+ assert avcopy[0] == self.av[0]
+ del self.av[0]["ht"]
+
+ assert not hasattr(self.av, "__setitem__")
+
+ def test_items(self):
+ expected = dict(self.p.items())
+ expected.update(self.s)
+ assert sorted(self.av.items()) == sorted(expected.items())
+
+ def test_str(self):
+ out = str(dict(self.av))
+ assert str(self.av) == out
+
+ def test_repr(self):
+ out = f"{self.av.__class__.__name__}({self.s}, {self.p})"
+ assert repr(self.av) == out
+
+
+class TestUnionAdjacency:
+ # node->nbr->data
+ def setup_method(self):
+ dd = {"color": "blue", "weight": 1.2}
+ self.nd = {0: dd, 1: {}, 2: {"color": 1}}
+ self.s = {3: self.nd, 0: {}, 1: {}, 2: {3: {"color": 1}}}
+ self.p = {3: {}, 0: {3: dd}, 1: {0: {}}, 2: {1: {"color": 1}}}
+ self.adjview = nx.classes.coreviews.UnionAdjacency(self.s, self.p)
+
+ def test_pickle(self):
+ view = self.adjview
+ pview = pickle.loads(pickle.dumps(view, -1))
+ assert view == pview
+ assert view.__slots__ == pview.__slots__
+
+ def test_len(self):
+ assert len(self.adjview) == len(self.s)
+
+ def test_iter(self):
+ assert sorted(self.adjview) == sorted(self.s)
+
+ def test_getitem(self):
+ assert self.adjview[1] is not self.s[1]
+ assert self.adjview[3][0] is self.adjview[0][3]
+ assert self.adjview[2][3]["color"] == 1
+ pytest.raises(KeyError, self.adjview.__getitem__, 4)
+
+ def test_copy(self):
+ avcopy = self.adjview.copy()
+ assert avcopy[0] == self.adjview[0]
+ assert avcopy[0] is not self.adjview[0]
+
+ avcopy[2][3]["ht"] = 4
+ assert avcopy[2] != self.adjview[2]
+ self.adjview[2][3]["ht"] = 4
+ assert avcopy[2] == self.adjview[2]
+ del self.adjview[2][3]["ht"]
+
+ assert not hasattr(self.adjview, "__setitem__")
+
+ def test_str(self):
+ out = str(dict(self.adjview))
+ assert str(self.adjview) == out
+
+ def test_repr(self):
+ clsname = self.adjview.__class__.__name__
+ out = f"{clsname}({self.s}, {self.p})"
+ assert repr(self.adjview) == out
+
+
+class TestUnionMultiInner(TestUnionAdjacency):
+ # nbr->key->data
+ def setup_method(self):
+ dd = {"color": "blue", "weight": 1.2}
+ self.kd = {7: {}, "ekey": {}, 9: {"color": 1}}
+ self.s = {3: self.kd, 0: {7: dd}, 1: {}, 2: {"key": {"color": 1}}}
+ self.p = {3: {}, 0: {3: dd}, 1: {}, 2: {1: {"span": 2}}}
+ self.adjview = nx.classes.coreviews.UnionMultiInner(self.s, self.p)
+
+ def test_len(self):
+ assert len(self.adjview) == len(self.s.keys() | self.p.keys()) == 4
+
+ def test_getitem(self):
+ assert self.adjview[1] is not self.s[1]
+ assert self.adjview[0][7] is self.adjview[0][3]
+ assert self.adjview[2]["key"]["color"] == 1
+ assert self.adjview[2][1]["span"] == 2
+ pytest.raises(KeyError, self.adjview.__getitem__, 4)
+ pytest.raises(KeyError, self.adjview[1].__getitem__, "key")
+
+ def test_copy(self):
+ avcopy = self.adjview.copy()
+ assert avcopy[0] == self.adjview[0]
+ assert avcopy[0] is not self.adjview[0]
+
+ avcopy[2][1]["width"] = 8
+ assert avcopy[2] != self.adjview[2]
+ self.adjview[2][1]["width"] = 8
+ assert avcopy[2] == self.adjview[2]
+ del self.adjview[2][1]["width"]
+
+ assert not hasattr(self.adjview, "__setitem__")
+ assert hasattr(avcopy, "__setitem__")
+
+
+class TestUnionMultiAdjacency(TestUnionAdjacency):
+ # node->nbr->key->data
+ def setup_method(self):
+ dd = {"color": "blue", "weight": 1.2}
+ self.kd = {7: {}, 8: {}, 9: {"color": 1}}
+ self.nd = {3: self.kd, 0: {9: dd}, 1: {8: {}}, 2: {9: {"color": 1}}}
+ self.s = {3: self.nd, 0: {3: {7: dd}}, 1: {}, 2: {3: {8: {}}}}
+ self.p = {3: {}, 0: {3: {9: dd}}, 1: {}, 2: {1: {8: {}}}}
+ self.adjview = nx.classes.coreviews.UnionMultiAdjacency(self.s, self.p)
+
+ def test_getitem(self):
+ assert self.adjview[1] is not self.s[1]
+ assert self.adjview[3][0][9] is self.adjview[0][3][9]
+ assert self.adjview[3][2][9]["color"] == 1
+ pytest.raises(KeyError, self.adjview.__getitem__, 4)
+
+ def test_copy(self):
+ avcopy = self.adjview.copy()
+ assert avcopy[0] == self.adjview[0]
+ assert avcopy[0] is not self.adjview[0]
+
+ avcopy[2][3][8]["ht"] = 4
+ assert avcopy[2] != self.adjview[2]
+ self.adjview[2][3][8]["ht"] = 4
+ assert avcopy[2] == self.adjview[2]
+ del self.adjview[2][3][8]["ht"]
+
+ assert not hasattr(self.adjview, "__setitem__")
+ assert hasattr(avcopy, "__setitem__")
+
+
+class TestFilteredGraphs:
+ def setup_method(self):
+ self.Graphs = [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
+
+ def test_hide_show_nodes(self):
+ SubGraph = nx.subgraph_view
+ for Graph in self.Graphs:
+ G = nx.path_graph(4, Graph)
+ SG = G.subgraph([2, 3])
+ RG = SubGraph(G, filter_node=nx.filters.hide_nodes([0, 1]))
+ assert SG.nodes == RG.nodes
+ assert SG.edges == RG.edges
+ SGC = SG.copy()
+ RGC = RG.copy()
+ assert SGC.nodes == RGC.nodes
+ assert SGC.edges == RGC.edges
+
+ def test_str_repr(self):
+ SubGraph = nx.subgraph_view
+ for Graph in self.Graphs:
+ G = nx.path_graph(4, Graph)
+ SG = G.subgraph([2, 3])
+ RG = SubGraph(G, filter_node=nx.filters.hide_nodes([0, 1]))
+ str(SG.adj)
+ str(RG.adj)
+ repr(SG.adj)
+ repr(RG.adj)
+ str(SG.adj[2])
+ str(RG.adj[2])
+ repr(SG.adj[2])
+ repr(RG.adj[2])
+
+ def test_copy(self):
+ SubGraph = nx.subgraph_view
+ for Graph in self.Graphs:
+ G = nx.path_graph(4, Graph)
+ SG = G.subgraph([2, 3])
+ RG = SubGraph(G, filter_node=nx.filters.hide_nodes([0, 1]))
+ RsG = SubGraph(G, filter_node=nx.filters.show_nodes([2, 3]))
+ assert G.adj.copy() == G.adj
+ assert G.adj[2].copy() == G.adj[2]
+ assert SG.adj.copy() == SG.adj
+ assert SG.adj[2].copy() == SG.adj[2]
+ assert RG.adj.copy() == RG.adj
+ assert RG.adj[2].copy() == RG.adj[2]
+ assert RsG.adj.copy() == RsG.adj
+ assert RsG.adj[2].copy() == RsG.adj[2]
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_digraph.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_digraph.py
new file mode 100644
index 00000000..b9972f9a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_digraph.py
@@ -0,0 +1,331 @@
+import pytest
+
+import networkx as nx
+from networkx.utils import nodes_equal
+
+from .test_graph import BaseAttrGraphTester, BaseGraphTester
+from .test_graph import TestEdgeSubgraph as _TestGraphEdgeSubgraph
+from .test_graph import TestGraph as _TestGraph
+
+
+class BaseDiGraphTester(BaseGraphTester):
+ def test_has_successor(self):
+ G = self.K3
+ assert G.has_successor(0, 1)
+ assert not G.has_successor(0, -1)
+
+ def test_successors(self):
+ G = self.K3
+ assert sorted(G.successors(0)) == [1, 2]
+ with pytest.raises(nx.NetworkXError):
+ G.successors(-1)
+
+ def test_has_predecessor(self):
+ G = self.K3
+ assert G.has_predecessor(0, 1)
+ assert not G.has_predecessor(0, -1)
+
+ def test_predecessors(self):
+ G = self.K3
+ assert sorted(G.predecessors(0)) == [1, 2]
+ with pytest.raises(nx.NetworkXError):
+ G.predecessors(-1)
+
+ def test_edges(self):
+ G = self.K3
+ assert sorted(G.edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
+ assert sorted(G.edges(0)) == [(0, 1), (0, 2)]
+ assert sorted(G.edges([0, 1])) == [(0, 1), (0, 2), (1, 0), (1, 2)]
+ with pytest.raises(nx.NetworkXError):
+ G.edges(-1)
+
+ def test_out_edges(self):
+ G = self.K3
+ assert sorted(G.out_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
+ assert sorted(G.out_edges(0)) == [(0, 1), (0, 2)]
+ with pytest.raises(nx.NetworkXError):
+ G.out_edges(-1)
+
+ def test_out_edges_dir(self):
+ G = self.P3
+ assert sorted(G.out_edges()) == [(0, 1), (1, 2)]
+ assert sorted(G.out_edges(0)) == [(0, 1)]
+ assert sorted(G.out_edges(2)) == []
+
+ def test_out_edges_data(self):
+ G = nx.DiGraph([(0, 1, {"data": 0}), (1, 0, {})])
+ assert sorted(G.out_edges(data=True)) == [(0, 1, {"data": 0}), (1, 0, {})]
+ assert sorted(G.out_edges(0, data=True)) == [(0, 1, {"data": 0})]
+ assert sorted(G.out_edges(data="data")) == [(0, 1, 0), (1, 0, None)]
+ assert sorted(G.out_edges(0, data="data")) == [(0, 1, 0)]
+
+ def test_in_edges_dir(self):
+ G = self.P3
+ assert sorted(G.in_edges()) == [(0, 1), (1, 2)]
+ assert sorted(G.in_edges(0)) == []
+ assert sorted(G.in_edges(2)) == [(1, 2)]
+
+ def test_in_edges_data(self):
+ G = nx.DiGraph([(0, 1, {"data": 0}), (1, 0, {})])
+ assert sorted(G.in_edges(data=True)) == [(0, 1, {"data": 0}), (1, 0, {})]
+ assert sorted(G.in_edges(1, data=True)) == [(0, 1, {"data": 0})]
+ assert sorted(G.in_edges(data="data")) == [(0, 1, 0), (1, 0, None)]
+ assert sorted(G.in_edges(1, data="data")) == [(0, 1, 0)]
+
+ def test_degree(self):
+ G = self.K3
+ assert sorted(G.degree()) == [(0, 4), (1, 4), (2, 4)]
+ assert dict(G.degree()) == {0: 4, 1: 4, 2: 4}
+ assert G.degree(0) == 4
+ assert list(G.degree(iter([0]))) == [(0, 4)] # run through iterator
+
+ def test_in_degree(self):
+ G = self.K3
+ assert sorted(G.in_degree()) == [(0, 2), (1, 2), (2, 2)]
+ assert dict(G.in_degree()) == {0: 2, 1: 2, 2: 2}
+ assert G.in_degree(0) == 2
+ assert list(G.in_degree(iter([0]))) == [(0, 2)] # run through iterator
+
+ def test_out_degree(self):
+ G = self.K3
+ assert sorted(G.out_degree()) == [(0, 2), (1, 2), (2, 2)]
+ assert dict(G.out_degree()) == {0: 2, 1: 2, 2: 2}
+ assert G.out_degree(0) == 2
+ assert list(G.out_degree(iter([0]))) == [(0, 2)]
+
+ def test_size(self):
+ G = self.K3
+ assert G.size() == 6
+ assert G.number_of_edges() == 6
+
+ def test_to_undirected_reciprocal(self):
+ G = self.Graph()
+ G.add_edge(1, 2)
+ assert G.to_undirected().has_edge(1, 2)
+ assert not G.to_undirected(reciprocal=True).has_edge(1, 2)
+ G.add_edge(2, 1)
+ assert G.to_undirected(reciprocal=True).has_edge(1, 2)
+
+ def test_reverse_copy(self):
+ G = nx.DiGraph([(0, 1), (1, 2)])
+ R = G.reverse()
+ assert sorted(R.edges()) == [(1, 0), (2, 1)]
+ R.remove_edge(1, 0)
+ assert sorted(R.edges()) == [(2, 1)]
+ assert sorted(G.edges()) == [(0, 1), (1, 2)]
+
+ def test_reverse_nocopy(self):
+ G = nx.DiGraph([(0, 1), (1, 2)])
+ R = G.reverse(copy=False)
+ assert sorted(R.edges()) == [(1, 0), (2, 1)]
+ with pytest.raises(nx.NetworkXError):
+ R.remove_edge(1, 0)
+
+ def test_reverse_hashable(self):
+ class Foo:
+ pass
+
+ x = Foo()
+ y = Foo()
+ G = nx.DiGraph()
+ G.add_edge(x, y)
+ assert nodes_equal(G.nodes(), G.reverse().nodes())
+ assert [(y, x)] == list(G.reverse().edges())
+
+ def test_di_cache_reset(self):
+ G = self.K3.copy()
+ old_succ = G.succ
+ assert id(G.succ) == id(old_succ)
+ old_adj = G.adj
+ assert id(G.adj) == id(old_adj)
+
+ G._succ = {}
+ assert id(G.succ) != id(old_succ)
+ assert id(G.adj) != id(old_adj)
+
+ old_pred = G.pred
+ assert id(G.pred) == id(old_pred)
+ G._pred = {}
+ assert id(G.pred) != id(old_pred)
+
+ def test_di_attributes_cached(self):
+ G = self.K3.copy()
+ assert id(G.in_edges) == id(G.in_edges)
+ assert id(G.out_edges) == id(G.out_edges)
+ assert id(G.in_degree) == id(G.in_degree)
+ assert id(G.out_degree) == id(G.out_degree)
+ assert id(G.succ) == id(G.succ)
+ assert id(G.pred) == id(G.pred)
+
+
+class BaseAttrDiGraphTester(BaseDiGraphTester, BaseAttrGraphTester):
+ def test_edges_data(self):
+ G = self.K3
+ all_edges = [
+ (0, 1, {}),
+ (0, 2, {}),
+ (1, 0, {}),
+ (1, 2, {}),
+ (2, 0, {}),
+ (2, 1, {}),
+ ]
+ assert sorted(G.edges(data=True)) == all_edges
+ assert sorted(G.edges(0, data=True)) == all_edges[:2]
+ assert sorted(G.edges([0, 1], data=True)) == all_edges[:4]
+ with pytest.raises(nx.NetworkXError):
+ G.edges(-1, True)
+
+ def test_in_degree_weighted(self):
+ G = self.K3.copy()
+ G.add_edge(0, 1, weight=0.3, other=1.2)
+ assert sorted(G.in_degree(weight="weight")) == [(0, 2), (1, 1.3), (2, 2)]
+ assert dict(G.in_degree(weight="weight")) == {0: 2, 1: 1.3, 2: 2}
+ assert G.in_degree(1, weight="weight") == 1.3
+ assert sorted(G.in_degree(weight="other")) == [(0, 2), (1, 2.2), (2, 2)]
+ assert dict(G.in_degree(weight="other")) == {0: 2, 1: 2.2, 2: 2}
+ assert G.in_degree(1, weight="other") == 2.2
+ assert list(G.in_degree(iter([1]), weight="other")) == [(1, 2.2)]
+
+ def test_out_degree_weighted(self):
+ G = self.K3.copy()
+ G.add_edge(0, 1, weight=0.3, other=1.2)
+ assert sorted(G.out_degree(weight="weight")) == [(0, 1.3), (1, 2), (2, 2)]
+ assert dict(G.out_degree(weight="weight")) == {0: 1.3, 1: 2, 2: 2}
+ assert G.out_degree(0, weight="weight") == 1.3
+ assert sorted(G.out_degree(weight="other")) == [(0, 2.2), (1, 2), (2, 2)]
+ assert dict(G.out_degree(weight="other")) == {0: 2.2, 1: 2, 2: 2}
+ assert G.out_degree(0, weight="other") == 2.2
+ assert list(G.out_degree(iter([0]), weight="other")) == [(0, 2.2)]
+
+
+class TestDiGraph(BaseAttrDiGraphTester, _TestGraph):
+ """Tests specific to dict-of-dict-of-dict digraph data structure"""
+
+ def setup_method(self):
+ self.Graph = nx.DiGraph
+ # build dict-of-dict-of-dict K3
+ ed1, ed2, ed3, ed4, ed5, ed6 = ({}, {}, {}, {}, {}, {})
+ self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed3, 2: ed4}, 2: {0: ed5, 1: ed6}}
+ self.k3edges = [(0, 1), (0, 2), (1, 2)]
+ self.k3nodes = [0, 1, 2]
+ self.K3 = self.Graph()
+ self.K3._succ = self.k3adj # K3._adj is synced with K3._succ
+ self.K3._pred = {0: {1: ed3, 2: ed5}, 1: {0: ed1, 2: ed6}, 2: {0: ed2, 1: ed4}}
+ self.K3._node = {}
+ self.K3._node[0] = {}
+ self.K3._node[1] = {}
+ self.K3._node[2] = {}
+
+ ed1, ed2 = ({}, {})
+ self.P3 = self.Graph()
+ self.P3._succ = {0: {1: ed1}, 1: {2: ed2}, 2: {}}
+ self.P3._pred = {0: {}, 1: {0: ed1}, 2: {1: ed2}}
+ # P3._adj is synced with P3._succ
+ self.P3._node = {}
+ self.P3._node[0] = {}
+ self.P3._node[1] = {}
+ self.P3._node[2] = {}
+
+ 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: {}})]
+ assert sorted(G.succ.items()) == [(1, {2: {}}), (2, {1: {}})]
+ assert sorted(G.pred.items()) == [(1, {2: {}}), (2, {1: {}})]
+
+ def test_add_edge(self):
+ G = self.Graph()
+ G.add_edge(0, 1)
+ assert G.adj == {0: {1: {}}, 1: {}}
+ assert G.succ == {0: {1: {}}, 1: {}}
+ assert G.pred == {0: {}, 1: {0: {}}}
+ G = self.Graph()
+ G.add_edge(*(0, 1))
+ assert G.adj == {0: {1: {}}, 1: {}}
+ assert G.succ == {0: {1: {}}, 1: {}}
+ assert G.pred == {0: {}, 1: {0: {}}}
+ with pytest.raises(ValueError, match="None cannot be a node"):
+ G.add_edge(None, 3)
+
+ def test_add_edges_from(self):
+ G = self.Graph()
+ G.add_edges_from([(0, 1), (0, 2, {"data": 3})], data=2)
+ assert G.adj == {0: {1: {"data": 2}, 2: {"data": 3}}, 1: {}, 2: {}}
+ assert G.succ == {0: {1: {"data": 2}, 2: {"data": 3}}, 1: {}, 2: {}}
+ assert G.pred == {0: {}, 1: {0: {"data": 2}}, 2: {0: {"data": 3}}}
+
+ with pytest.raises(nx.NetworkXError):
+ G.add_edges_from([(0,)]) # too few in tuple
+ with pytest.raises(nx.NetworkXError):
+ G.add_edges_from([(0, 1, 2, 3)]) # too many in tuple
+ with pytest.raises(TypeError):
+ G.add_edges_from([0]) # not a tuple
+ with pytest.raises(ValueError, match="None cannot be a node"):
+ G.add_edges_from([(None, 3), (3, 2)])
+
+ def test_remove_edge(self):
+ G = self.K3.copy()
+ G.remove_edge(0, 1)
+ assert G.succ == {0: {2: {}}, 1: {0: {}, 2: {}}, 2: {0: {}, 1: {}}}
+ assert G.pred == {0: {1: {}, 2: {}}, 1: {2: {}}, 2: {0: {}, 1: {}}}
+ with pytest.raises(nx.NetworkXError):
+ G.remove_edge(-1, 0)
+
+ def test_remove_edges_from(self):
+ G = self.K3.copy()
+ G.remove_edges_from([(0, 1)])
+ assert G.succ == {0: {2: {}}, 1: {0: {}, 2: {}}, 2: {0: {}, 1: {}}}
+ assert G.pred == {0: {1: {}, 2: {}}, 1: {2: {}}, 2: {0: {}, 1: {}}}
+ G.remove_edges_from([(0, 0)]) # silent fail
+
+ def test_clear(self):
+ G = self.K3
+ G.graph["name"] = "K3"
+ G.clear()
+ assert list(G.nodes) == []
+ assert G.succ == {}
+ assert G.pred == {}
+ assert G.graph == {}
+
+ def test_clear_edges(self):
+ G = self.K3
+ G.graph["name"] = "K3"
+ nodes = list(G.nodes)
+ G.clear_edges()
+ assert list(G.nodes) == nodes
+ expected = {0: {}, 1: {}, 2: {}}
+ assert G.succ == expected
+ assert G.pred == expected
+ assert list(G.edges) == []
+ assert G.graph["name"] == "K3"
+
+
+class TestEdgeSubgraph(_TestGraphEdgeSubgraph):
+ """Unit tests for the :meth:`DiGraph.edge_subgraph` method."""
+
+ def setup_method(self):
+ # Create a doubly-linked path graph on five nodes.
+ G = nx.DiGraph(nx.path_graph(5))
+ # Add some node, edge, and graph attributes.
+ for i in range(5):
+ G.nodes[i]["name"] = f"node{i}"
+ G.edges[0, 1]["name"] = "edge01"
+ G.edges[3, 4]["name"] = "edge34"
+ G.graph["name"] = "graph"
+ # Get the subgraph induced by the first and last edges.
+ self.G = G
+ self.H = G.edge_subgraph([(0, 1), (3, 4)])
+
+ def test_pred_succ(self):
+ """Test that nodes are added to predecessors and successors.
+
+ For more information, see GitHub issue #2370.
+
+ """
+ G = nx.DiGraph()
+ G.add_edge(0, 1)
+ H = G.edge_subgraph([(0, 1)])
+ assert list(H.predecessors(0)) == []
+ assert list(H.successors(0)) == [1]
+ assert list(H.predecessors(1)) == [0]
+ assert list(H.successors(1)) == []
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_digraph_historical.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_digraph_historical.py
new file mode 100644
index 00000000..4f2b1da9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_digraph_historical.py
@@ -0,0 +1,111 @@
+"""Original NetworkX graph tests"""
+
+import pytest
+
+import networkx
+import networkx as nx
+
+from .historical_tests import HistoricalTests
+
+
+class TestDiGraphHistorical(HistoricalTests):
+ @classmethod
+ def setup_class(cls):
+ HistoricalTests.setup_class()
+ cls.G = nx.DiGraph
+
+ def test_in_degree(self):
+ G = self.G()
+ G.add_nodes_from("GJK")
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")])
+
+ assert sorted(d for n, d in G.in_degree()) == [0, 0, 0, 0, 1, 2, 2]
+ assert dict(G.in_degree()) == {
+ "A": 0,
+ "C": 2,
+ "B": 1,
+ "D": 2,
+ "G": 0,
+ "K": 0,
+ "J": 0,
+ }
+
+ def test_out_degree(self):
+ G = self.G()
+ G.add_nodes_from("GJK")
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")])
+ assert sorted(v for k, v in G.in_degree()) == [0, 0, 0, 0, 1, 2, 2]
+ assert dict(G.out_degree()) == {
+ "A": 2,
+ "C": 1,
+ "B": 2,
+ "D": 0,
+ "G": 0,
+ "K": 0,
+ "J": 0,
+ }
+
+ def test_degree_digraph(self):
+ H = nx.DiGraph()
+ H.add_edges_from([(1, 24), (1, 2)])
+ assert sorted(d for n, d in H.in_degree([1, 24])) == [0, 1]
+ assert sorted(d for n, d in H.out_degree([1, 24])) == [0, 2]
+ assert sorted(d for n, d in H.degree([1, 24])) == [1, 2]
+
+ def test_neighbors(self):
+ G = self.G()
+ G.add_nodes_from("GJK")
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")])
+
+ assert sorted(G.neighbors("C")) == ["D"]
+ assert sorted(G["C"]) == ["D"]
+ assert sorted(G.neighbors("A")) == ["B", "C"]
+ pytest.raises(nx.NetworkXError, G.neighbors, "j")
+ pytest.raises(nx.NetworkXError, G.neighbors, "j")
+
+ def test_successors(self):
+ G = self.G()
+ G.add_nodes_from("GJK")
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")])
+ assert sorted(G.successors("A")) == ["B", "C"]
+ assert sorted(G.successors("A")) == ["B", "C"]
+ assert sorted(G.successors("G")) == []
+ assert sorted(G.successors("D")) == []
+ assert sorted(G.successors("G")) == []
+ pytest.raises(nx.NetworkXError, G.successors, "j")
+ pytest.raises(nx.NetworkXError, G.successors, "j")
+
+ def test_predecessors(self):
+ G = self.G()
+ G.add_nodes_from("GJK")
+ G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")])
+ assert sorted(G.predecessors("C")) == ["A", "B"]
+ assert sorted(G.predecessors("C")) == ["A", "B"]
+ assert sorted(G.predecessors("G")) == []
+ assert sorted(G.predecessors("A")) == []
+ assert sorted(G.predecessors("G")) == []
+ assert sorted(G.predecessors("A")) == []
+ assert sorted(G.successors("D")) == []
+
+ pytest.raises(nx.NetworkXError, G.predecessors, "j")
+ pytest.raises(nx.NetworkXError, G.predecessors, "j")
+
+ def test_reverse(self):
+ G = nx.complete_graph(10)
+ H = G.to_directed()
+ HR = H.reverse()
+ assert nx.is_isomorphic(H, HR)
+ assert sorted(H.edges()) == sorted(HR.edges())
+
+ def test_reverse2(self):
+ H = nx.DiGraph()
+ foo = [H.add_edge(u, u + 1) for u in range(5)]
+ HR = H.reverse()
+ for u in range(5):
+ assert HR.has_edge(u + 1, u)
+
+ def test_reverse3(self):
+ H = nx.DiGraph()
+ H.add_nodes_from([1, 2, 3, 4])
+ HR = H.reverse()
+ assert sorted(HR.nodes()) == [1, 2, 3, 4]
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_filters.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_filters.py
new file mode 100644
index 00000000..2da59117
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_filters.py
@@ -0,0 +1,177 @@
+import pytest
+
+import networkx as nx
+
+
+class TestFilterFactory:
+ def test_no_filter(self):
+ nf = nx.filters.no_filter
+ assert nf()
+ assert nf(1)
+ assert nf(2, 1)
+
+ def test_hide_nodes(self):
+ f = nx.classes.filters.hide_nodes([1, 2, 3])
+ assert not f(1)
+ assert not f(2)
+ assert not f(3)
+ assert f(4)
+ assert f(0)
+ assert f("a")
+ pytest.raises(TypeError, f, 1, 2)
+ pytest.raises(TypeError, f)
+
+ def test_show_nodes(self):
+ f = nx.classes.filters.show_nodes([1, 2, 3])
+ assert f(1)
+ assert f(2)
+ assert f(3)
+ assert not f(4)
+ assert not f(0)
+ assert not f("a")
+ pytest.raises(TypeError, f, 1, 2)
+ pytest.raises(TypeError, f)
+
+ def test_hide_edges(self):
+ factory = nx.classes.filters.hide_edges
+ f = factory([(1, 2), (3, 4)])
+ assert not f(1, 2)
+ assert not f(3, 4)
+ assert not f(4, 3)
+ assert f(2, 3)
+ assert f(0, -1)
+ assert f("a", "b")
+ pytest.raises(TypeError, f, 1, 2, 3)
+ pytest.raises(TypeError, f, 1)
+ pytest.raises(TypeError, f)
+ pytest.raises(TypeError, factory, [1, 2, 3])
+ pytest.raises(ValueError, factory, [(1, 2, 3)])
+
+ def test_show_edges(self):
+ factory = nx.classes.filters.show_edges
+ f = factory([(1, 2), (3, 4)])
+ assert f(1, 2)
+ assert f(3, 4)
+ assert f(4, 3)
+ assert not f(2, 3)
+ assert not f(0, -1)
+ assert not f("a", "b")
+ pytest.raises(TypeError, f, 1, 2, 3)
+ pytest.raises(TypeError, f, 1)
+ pytest.raises(TypeError, f)
+ pytest.raises(TypeError, factory, [1, 2, 3])
+ pytest.raises(ValueError, factory, [(1, 2, 3)])
+
+ def test_hide_diedges(self):
+ factory = nx.classes.filters.hide_diedges
+ f = factory([(1, 2), (3, 4)])
+ assert not f(1, 2)
+ assert not f(3, 4)
+ assert f(4, 3)
+ assert f(2, 3)
+ assert f(0, -1)
+ assert f("a", "b")
+ pytest.raises(TypeError, f, 1, 2, 3)
+ pytest.raises(TypeError, f, 1)
+ pytest.raises(TypeError, f)
+ pytest.raises(TypeError, factory, [1, 2, 3])
+ pytest.raises(ValueError, factory, [(1, 2, 3)])
+
+ def test_show_diedges(self):
+ factory = nx.classes.filters.show_diedges
+ f = factory([(1, 2), (3, 4)])
+ assert f(1, 2)
+ assert f(3, 4)
+ assert not f(4, 3)
+ assert not f(2, 3)
+ assert not f(0, -1)
+ assert not f("a", "b")
+ pytest.raises(TypeError, f, 1, 2, 3)
+ pytest.raises(TypeError, f, 1)
+ pytest.raises(TypeError, f)
+ pytest.raises(TypeError, factory, [1, 2, 3])
+ pytest.raises(ValueError, factory, [(1, 2, 3)])
+
+ def test_hide_multiedges(self):
+ factory = nx.classes.filters.hide_multiedges
+ f = factory([(1, 2, 0), (3, 4, 1), (1, 2, 1)])
+ assert not f(1, 2, 0)
+ assert not f(1, 2, 1)
+ assert f(1, 2, 2)
+ assert f(3, 4, 0)
+ assert not f(3, 4, 1)
+ assert not f(4, 3, 1)
+ assert f(4, 3, 0)
+ assert f(2, 3, 0)
+ assert f(0, -1, 0)
+ assert f("a", "b", 0)
+ pytest.raises(TypeError, f, 1, 2, 3, 4)
+ pytest.raises(TypeError, f, 1, 2)
+ pytest.raises(TypeError, f, 1)
+ pytest.raises(TypeError, f)
+ pytest.raises(TypeError, factory, [1, 2, 3])
+ pytest.raises(ValueError, factory, [(1, 2)])
+ pytest.raises(ValueError, factory, [(1, 2, 3, 4)])
+
+ def test_show_multiedges(self):
+ factory = nx.classes.filters.show_multiedges
+ f = factory([(1, 2, 0), (3, 4, 1), (1, 2, 1)])
+ assert f(1, 2, 0)
+ assert f(1, 2, 1)
+ assert not f(1, 2, 2)
+ assert not f(3, 4, 0)
+ assert f(3, 4, 1)
+ assert f(4, 3, 1)
+ assert not f(4, 3, 0)
+ assert not f(2, 3, 0)
+ assert not f(0, -1, 0)
+ assert not f("a", "b", 0)
+ pytest.raises(TypeError, f, 1, 2, 3, 4)
+ pytest.raises(TypeError, f, 1, 2)
+ pytest.raises(TypeError, f, 1)
+ pytest.raises(TypeError, f)
+ pytest.raises(TypeError, factory, [1, 2, 3])
+ pytest.raises(ValueError, factory, [(1, 2)])
+ pytest.raises(ValueError, factory, [(1, 2, 3, 4)])
+
+ def test_hide_multidiedges(self):
+ factory = nx.classes.filters.hide_multidiedges
+ f = factory([(1, 2, 0), (3, 4, 1), (1, 2, 1)])
+ assert not f(1, 2, 0)
+ assert not f(1, 2, 1)
+ assert f(1, 2, 2)
+ assert f(3, 4, 0)
+ assert not f(3, 4, 1)
+ assert f(4, 3, 1)
+ assert f(4, 3, 0)
+ assert f(2, 3, 0)
+ assert f(0, -1, 0)
+ assert f("a", "b", 0)
+ pytest.raises(TypeError, f, 1, 2, 3, 4)
+ pytest.raises(TypeError, f, 1, 2)
+ pytest.raises(TypeError, f, 1)
+ pytest.raises(TypeError, f)
+ pytest.raises(TypeError, factory, [1, 2, 3])
+ pytest.raises(ValueError, factory, [(1, 2)])
+ pytest.raises(ValueError, factory, [(1, 2, 3, 4)])
+
+ def test_show_multidiedges(self):
+ factory = nx.classes.filters.show_multidiedges
+ f = factory([(1, 2, 0), (3, 4, 1), (1, 2, 1)])
+ assert f(1, 2, 0)
+ assert f(1, 2, 1)
+ assert not f(1, 2, 2)
+ assert not f(3, 4, 0)
+ assert f(3, 4, 1)
+ assert not f(4, 3, 1)
+ assert not f(4, 3, 0)
+ assert not f(2, 3, 0)
+ assert not f(0, -1, 0)
+ assert not f("a", "b", 0)
+ pytest.raises(TypeError, f, 1, 2, 3, 4)
+ pytest.raises(TypeError, f, 1, 2)
+ pytest.raises(TypeError, f, 1)
+ pytest.raises(TypeError, f)
+ pytest.raises(TypeError, factory, [1, 2, 3])
+ pytest.raises(ValueError, factory, [(1, 2)])
+ pytest.raises(ValueError, factory, [(1, 2, 3, 4)])
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_function.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_function.py
new file mode 100644
index 00000000..f86890dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_function.py
@@ -0,0 +1,1035 @@
+import random
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal, nodes_equal
+
+
+def test_degree_histogram_empty():
+ G = nx.Graph()
+ assert nx.degree_histogram(G) == []
+
+
+class TestFunction:
+ def setup_method(self):
+ self.G = nx.Graph({0: [1, 2, 3], 1: [1, 2, 0], 4: []}, name="Test")
+ self.Gdegree = {0: 3, 1: 2, 2: 2, 3: 1, 4: 0}
+ self.Gnodes = list(range(5))
+ self.Gedges = [(0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2)]
+ self.DG = nx.DiGraph({0: [1, 2, 3], 1: [1, 2, 0], 4: []})
+ self.DGin_degree = {0: 1, 1: 2, 2: 2, 3: 1, 4: 0}
+ self.DGout_degree = {0: 3, 1: 3, 2: 0, 3: 0, 4: 0}
+ self.DGnodes = list(range(5))
+ self.DGedges = [(0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2)]
+
+ def test_nodes(self):
+ assert nodes_equal(self.G.nodes(), list(nx.nodes(self.G)))
+ assert nodes_equal(self.DG.nodes(), list(nx.nodes(self.DG)))
+
+ def test_edges(self):
+ assert edges_equal(self.G.edges(), list(nx.edges(self.G)))
+ assert sorted(self.DG.edges()) == sorted(nx.edges(self.DG))
+ assert edges_equal(
+ self.G.edges(nbunch=[0, 1, 3]), list(nx.edges(self.G, nbunch=[0, 1, 3]))
+ )
+ assert sorted(self.DG.edges(nbunch=[0, 1, 3])) == sorted(
+ nx.edges(self.DG, nbunch=[0, 1, 3])
+ )
+
+ def test_degree(self):
+ assert edges_equal(self.G.degree(), list(nx.degree(self.G)))
+ assert sorted(self.DG.degree()) == sorted(nx.degree(self.DG))
+ assert edges_equal(
+ self.G.degree(nbunch=[0, 1]), list(nx.degree(self.G, nbunch=[0, 1]))
+ )
+ assert sorted(self.DG.degree(nbunch=[0, 1])) == sorted(
+ nx.degree(self.DG, nbunch=[0, 1])
+ )
+ assert edges_equal(
+ self.G.degree(weight="weight"), list(nx.degree(self.G, weight="weight"))
+ )
+ assert sorted(self.DG.degree(weight="weight")) == sorted(
+ nx.degree(self.DG, weight="weight")
+ )
+
+ def test_neighbors(self):
+ assert list(self.G.neighbors(1)) == list(nx.neighbors(self.G, 1))
+ assert list(self.DG.neighbors(1)) == list(nx.neighbors(self.DG, 1))
+
+ def test_number_of_nodes(self):
+ assert self.G.number_of_nodes() == nx.number_of_nodes(self.G)
+ assert self.DG.number_of_nodes() == nx.number_of_nodes(self.DG)
+
+ def test_number_of_edges(self):
+ assert self.G.number_of_edges() == nx.number_of_edges(self.G)
+ assert self.DG.number_of_edges() == nx.number_of_edges(self.DG)
+
+ def test_is_directed(self):
+ assert self.G.is_directed() == nx.is_directed(self.G)
+ assert self.DG.is_directed() == nx.is_directed(self.DG)
+
+ def test_add_star(self):
+ G = self.G.copy()
+ nlist = [12, 13, 14, 15]
+ nx.add_star(G, nlist)
+ assert edges_equal(G.edges(nlist), [(12, 13), (12, 14), (12, 15)])
+
+ G = self.G.copy()
+ nx.add_star(G, nlist, weight=2.0)
+ assert edges_equal(
+ G.edges(nlist, data=True),
+ [
+ (12, 13, {"weight": 2.0}),
+ (12, 14, {"weight": 2.0}),
+ (12, 15, {"weight": 2.0}),
+ ],
+ )
+
+ G = self.G.copy()
+ nlist = [12]
+ nx.add_star(G, nlist)
+ assert nodes_equal(G, list(self.G) + nlist)
+
+ G = self.G.copy()
+ nlist = []
+ nx.add_star(G, nlist)
+ assert nodes_equal(G.nodes, self.Gnodes)
+ assert edges_equal(G.edges, self.G.edges)
+
+ def test_add_path(self):
+ G = self.G.copy()
+ nlist = [12, 13, 14, 15]
+ nx.add_path(G, nlist)
+ assert edges_equal(G.edges(nlist), [(12, 13), (13, 14), (14, 15)])
+ G = self.G.copy()
+ nx.add_path(G, nlist, weight=2.0)
+ assert edges_equal(
+ G.edges(nlist, data=True),
+ [
+ (12, 13, {"weight": 2.0}),
+ (13, 14, {"weight": 2.0}),
+ (14, 15, {"weight": 2.0}),
+ ],
+ )
+
+ G = self.G.copy()
+ nlist = ["node"]
+ nx.add_path(G, nlist)
+ assert edges_equal(G.edges(nlist), [])
+ assert nodes_equal(G, list(self.G) + ["node"])
+
+ G = self.G.copy()
+ nlist = iter(["node"])
+ nx.add_path(G, nlist)
+ assert edges_equal(G.edges(["node"]), [])
+ assert nodes_equal(G, list(self.G) + ["node"])
+
+ G = self.G.copy()
+ nlist = [12]
+ nx.add_path(G, nlist)
+ assert edges_equal(G.edges(nlist), [])
+ assert nodes_equal(G, list(self.G) + [12])
+
+ G = self.G.copy()
+ nlist = iter([12])
+ nx.add_path(G, nlist)
+ assert edges_equal(G.edges([12]), [])
+ assert nodes_equal(G, list(self.G) + [12])
+
+ G = self.G.copy()
+ nlist = []
+ nx.add_path(G, nlist)
+ assert edges_equal(G.edges, self.G.edges)
+ assert nodes_equal(G, list(self.G))
+
+ G = self.G.copy()
+ nlist = iter([])
+ nx.add_path(G, nlist)
+ assert edges_equal(G.edges, self.G.edges)
+ assert nodes_equal(G, list(self.G))
+
+ def test_add_cycle(self):
+ G = self.G.copy()
+ nlist = [12, 13, 14, 15]
+ oklists = [
+ [(12, 13), (12, 15), (13, 14), (14, 15)],
+ [(12, 13), (13, 14), (14, 15), (15, 12)],
+ ]
+ nx.add_cycle(G, nlist)
+ assert sorted(G.edges(nlist)) in oklists
+ G = self.G.copy()
+ oklists = [
+ [
+ (12, 13, {"weight": 1.0}),
+ (12, 15, {"weight": 1.0}),
+ (13, 14, {"weight": 1.0}),
+ (14, 15, {"weight": 1.0}),
+ ],
+ [
+ (12, 13, {"weight": 1.0}),
+ (13, 14, {"weight": 1.0}),
+ (14, 15, {"weight": 1.0}),
+ (15, 12, {"weight": 1.0}),
+ ],
+ ]
+ nx.add_cycle(G, nlist, weight=1.0)
+ assert sorted(G.edges(nlist, data=True)) in oklists
+
+ G = self.G.copy()
+ nlist = [12]
+ nx.add_cycle(G, nlist)
+ assert nodes_equal(G, list(self.G) + nlist)
+
+ G = self.G.copy()
+ nlist = []
+ nx.add_cycle(G, nlist)
+ assert nodes_equal(G.nodes, self.Gnodes)
+ assert edges_equal(G.edges, self.G.edges)
+
+ def test_subgraph(self):
+ assert (
+ self.G.subgraph([0, 1, 2, 4]).adj == nx.subgraph(self.G, [0, 1, 2, 4]).adj
+ )
+ assert (
+ self.DG.subgraph([0, 1, 2, 4]).adj == nx.subgraph(self.DG, [0, 1, 2, 4]).adj
+ )
+ assert (
+ self.G.subgraph([0, 1, 2, 4]).adj
+ == nx.induced_subgraph(self.G, [0, 1, 2, 4]).adj
+ )
+ assert (
+ self.DG.subgraph([0, 1, 2, 4]).adj
+ == nx.induced_subgraph(self.DG, [0, 1, 2, 4]).adj
+ )
+ # subgraph-subgraph chain is allowed in function interface
+ H = nx.induced_subgraph(self.G.subgraph([0, 1, 2, 4]), [0, 1, 4])
+ assert H._graph is not self.G
+ assert H.adj == self.G.subgraph([0, 1, 4]).adj
+
+ def test_edge_subgraph(self):
+ assert (
+ self.G.edge_subgraph([(1, 2), (0, 3)]).adj
+ == nx.edge_subgraph(self.G, [(1, 2), (0, 3)]).adj
+ )
+ assert (
+ self.DG.edge_subgraph([(1, 2), (0, 3)]).adj
+ == nx.edge_subgraph(self.DG, [(1, 2), (0, 3)]).adj
+ )
+
+ def test_create_empty_copy(self):
+ G = nx.create_empty_copy(self.G, with_data=False)
+ assert nodes_equal(G, list(self.G))
+ assert G.graph == {}
+ assert G._node == {}.fromkeys(self.G.nodes(), {})
+ assert G._adj == {}.fromkeys(self.G.nodes(), {})
+ G = nx.create_empty_copy(self.G)
+ assert nodes_equal(G, list(self.G))
+ assert G.graph == self.G.graph
+ assert G._node == self.G._node
+ assert G._adj == {}.fromkeys(self.G.nodes(), {})
+
+ def test_degree_histogram(self):
+ assert nx.degree_histogram(self.G) == [1, 1, 1, 1, 1]
+
+ def test_density(self):
+ assert nx.density(self.G) == 0.5
+ assert nx.density(self.DG) == 0.3
+ G = nx.Graph()
+ G.add_node(1)
+ assert nx.density(G) == 0.0
+
+ def test_density_selfloop(self):
+ G = nx.Graph()
+ G.add_edge(1, 1)
+ assert nx.density(G) == 0.0
+ G.add_edge(1, 2)
+ assert nx.density(G) == 2.0
+
+ def test_freeze(self):
+ G = nx.freeze(self.G)
+ assert G.frozen
+ pytest.raises(nx.NetworkXError, G.add_node, 1)
+ pytest.raises(nx.NetworkXError, G.add_nodes_from, [1])
+ pytest.raises(nx.NetworkXError, G.remove_node, 1)
+ pytest.raises(nx.NetworkXError, G.remove_nodes_from, [1])
+ pytest.raises(nx.NetworkXError, G.add_edge, 1, 2)
+ pytest.raises(nx.NetworkXError, G.add_edges_from, [(1, 2)])
+ pytest.raises(nx.NetworkXError, G.remove_edge, 1, 2)
+ pytest.raises(nx.NetworkXError, G.remove_edges_from, [(1, 2)])
+ pytest.raises(nx.NetworkXError, G.clear_edges)
+ pytest.raises(nx.NetworkXError, G.clear)
+
+ def test_is_frozen(self):
+ assert not nx.is_frozen(self.G)
+ G = nx.freeze(self.G)
+ assert G.frozen == nx.is_frozen(self.G)
+ assert G.frozen
+
+ def test_node_attributes_are_still_mutable_on_frozen_graph(self):
+ G = nx.freeze(nx.path_graph(3))
+ node = G.nodes[0]
+ node["node_attribute"] = True
+ assert node["node_attribute"] == True
+
+ def test_edge_attributes_are_still_mutable_on_frozen_graph(self):
+ G = nx.freeze(nx.path_graph(3))
+ edge = G.edges[(0, 1)]
+ edge["edge_attribute"] = True
+ assert edge["edge_attribute"] == True
+
+ def test_neighbors_complete_graph(self):
+ graph = nx.complete_graph(100)
+ pop = random.sample(list(graph), 1)
+ nbors = list(nx.neighbors(graph, pop[0]))
+ # should be all the other vertices in the graph
+ assert len(nbors) == len(graph) - 1
+
+ graph = nx.path_graph(100)
+ node = random.sample(list(graph), 1)[0]
+ nbors = list(nx.neighbors(graph, node))
+ # should be all the other vertices in the graph
+ if node != 0 and node != 99:
+ assert len(nbors) == 2
+ else:
+ assert len(nbors) == 1
+
+ # create a star graph with 99 outer nodes
+ graph = nx.star_graph(99)
+ nbors = list(nx.neighbors(graph, 0))
+ assert len(nbors) == 99
+
+ def test_non_neighbors(self):
+ graph = nx.complete_graph(100)
+ pop = random.sample(list(graph), 1)
+ nbors = nx.non_neighbors(graph, pop[0])
+ # should be all the other vertices in the graph
+ assert len(nbors) == 0
+
+ graph = nx.path_graph(100)
+ node = random.sample(list(graph), 1)[0]
+ nbors = nx.non_neighbors(graph, node)
+ # should be all the other vertices in the graph
+ if node != 0 and node != 99:
+ assert len(nbors) == 97
+ else:
+ assert len(nbors) == 98
+
+ # create a star graph with 99 outer nodes
+ graph = nx.star_graph(99)
+ nbors = nx.non_neighbors(graph, 0)
+ assert len(nbors) == 0
+
+ # disconnected graph
+ graph = nx.Graph()
+ graph.add_nodes_from(range(10))
+ nbors = nx.non_neighbors(graph, 0)
+ assert len(nbors) == 9
+
+ def test_non_edges(self):
+ # All possible edges exist
+ graph = nx.complete_graph(5)
+ nedges = list(nx.non_edges(graph))
+ assert len(nedges) == 0
+
+ graph = nx.path_graph(4)
+ expected = [(0, 2), (0, 3), (1, 3)]
+ nedges = list(nx.non_edges(graph))
+ for u, v in expected:
+ assert (u, v) in nedges or (v, u) in nedges
+
+ graph = nx.star_graph(4)
+ expected = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+ nedges = list(nx.non_edges(graph))
+ for u, v in expected:
+ assert (u, v) in nedges or (v, u) in nedges
+
+ # Directed graphs
+ graph = nx.DiGraph()
+ graph.add_edges_from([(0, 2), (2, 0), (2, 1)])
+ expected = [(0, 1), (1, 0), (1, 2)]
+ nedges = list(nx.non_edges(graph))
+ for e in expected:
+ assert e in nedges
+
+ def test_is_weighted(self):
+ G = nx.Graph()
+ assert not nx.is_weighted(G)
+
+ G = nx.path_graph(4)
+ assert not nx.is_weighted(G)
+ assert not nx.is_weighted(G, (2, 3))
+
+ G.add_node(4)
+ G.add_edge(3, 4, weight=4)
+ assert not nx.is_weighted(G)
+ assert nx.is_weighted(G, (3, 4))
+
+ G = nx.DiGraph()
+ G.add_weighted_edges_from(
+ [
+ ("0", "3", 3),
+ ("0", "1", -5),
+ ("1", "0", -5),
+ ("0", "2", 2),
+ ("1", "2", 4),
+ ("2", "3", 1),
+ ]
+ )
+ assert nx.is_weighted(G)
+ assert nx.is_weighted(G, ("1", "0"))
+
+ G = G.to_undirected()
+ assert nx.is_weighted(G)
+ assert nx.is_weighted(G, ("1", "0"))
+
+ pytest.raises(nx.NetworkXError, nx.is_weighted, G, (1, 2))
+
+ def test_is_negatively_weighted(self):
+ G = nx.Graph()
+ assert not nx.is_negatively_weighted(G)
+
+ G.add_node(1)
+ G.add_nodes_from([2, 3, 4, 5])
+ assert not nx.is_negatively_weighted(G)
+
+ G.add_edge(1, 2, weight=4)
+ assert not nx.is_negatively_weighted(G, (1, 2))
+
+ G.add_edges_from([(1, 3), (2, 4), (2, 6)])
+ G[1][3]["color"] = "blue"
+ assert not nx.is_negatively_weighted(G)
+ assert not nx.is_negatively_weighted(G, (1, 3))
+
+ G[2][4]["weight"] = -2
+ assert nx.is_negatively_weighted(G, (2, 4))
+ assert nx.is_negatively_weighted(G)
+
+ G = nx.DiGraph()
+ G.add_weighted_edges_from(
+ [
+ ("0", "3", 3),
+ ("0", "1", -5),
+ ("1", "0", -2),
+ ("0", "2", 2),
+ ("1", "2", -3),
+ ("2", "3", 1),
+ ]
+ )
+ assert nx.is_negatively_weighted(G)
+ assert not nx.is_negatively_weighted(G, ("0", "3"))
+ assert nx.is_negatively_weighted(G, ("1", "0"))
+
+ pytest.raises(nx.NetworkXError, nx.is_negatively_weighted, G, (1, 4))
+
+
+class TestCommonNeighbors:
+ @classmethod
+ def setup_class(cls):
+ cls.func = staticmethod(nx.common_neighbors)
+
+ def test_func(G, u, v, expected):
+ result = sorted(cls.func(G, u, v))
+ assert result == expected
+
+ cls.test = staticmethod(test_func)
+
+ def test_K5(self):
+ G = nx.complete_graph(5)
+ self.test(G, 0, 1, [2, 3, 4])
+
+ def test_P3(self):
+ G = nx.path_graph(3)
+ self.test(G, 0, 2, [1])
+
+ def test_S4(self):
+ G = nx.star_graph(4)
+ self.test(G, 1, 2, [0])
+
+ def test_digraph(self):
+ with pytest.raises(nx.NetworkXNotImplemented):
+ G = nx.DiGraph()
+ G.add_edges_from([(0, 1), (1, 2)])
+ self.func(G, 0, 2)
+
+ def test_nonexistent_nodes(self):
+ G = nx.complete_graph(5)
+ pytest.raises(nx.NetworkXError, nx.common_neighbors, G, 5, 4)
+ pytest.raises(nx.NetworkXError, nx.common_neighbors, G, 4, 5)
+ pytest.raises(nx.NetworkXError, nx.common_neighbors, G, 5, 6)
+
+ def test_custom1(self):
+ """Case of no common neighbors."""
+ G = nx.Graph()
+ G.add_nodes_from([0, 1])
+ self.test(G, 0, 1, [])
+
+ def test_custom2(self):
+ """Case of equal nodes."""
+ G = nx.complete_graph(4)
+ self.test(G, 0, 0, [1, 2, 3])
+
+
+@pytest.mark.parametrize(
+ "graph_type", (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
+)
+def test_set_node_attributes(graph_type):
+ # Test single value
+ G = nx.path_graph(3, create_using=graph_type)
+ vals = 100
+ attr = "hello"
+ nx.set_node_attributes(G, vals, attr)
+ assert G.nodes[0][attr] == vals
+ assert G.nodes[1][attr] == vals
+ assert G.nodes[2][attr] == vals
+
+ # Test dictionary
+ G = nx.path_graph(3, create_using=graph_type)
+ vals = dict(zip(sorted(G.nodes()), range(len(G))))
+ attr = "hi"
+ nx.set_node_attributes(G, vals, attr)
+ assert G.nodes[0][attr] == 0
+ assert G.nodes[1][attr] == 1
+ assert G.nodes[2][attr] == 2
+
+ # Test dictionary of dictionaries
+ G = nx.path_graph(3, create_using=graph_type)
+ d = {"hi": 0, "hello": 200}
+ vals = dict.fromkeys(G.nodes(), d)
+ vals.pop(0)
+ nx.set_node_attributes(G, vals)
+ assert G.nodes[0] == {}
+ assert G.nodes[1]["hi"] == 0
+ assert G.nodes[2]["hello"] == 200
+
+
+@pytest.mark.parametrize(
+ ("values", "name"),
+ (
+ ({0: "red", 1: "blue"}, "color"), # values dictionary
+ ({0: {"color": "red"}, 1: {"color": "blue"}}, None), # dict-of-dict
+ ),
+)
+def test_set_node_attributes_ignores_extra_nodes(values, name):
+ """
+ When `values` is a dict or dict-of-dict keyed by nodes, ensure that keys
+ that correspond to nodes not in G are ignored.
+ """
+ G = nx.Graph()
+ G.add_node(0)
+ nx.set_node_attributes(G, values, name)
+ assert G.nodes[0]["color"] == "red"
+ assert 1 not in G.nodes
+
+
+@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph))
+def test_set_edge_attributes(graph_type):
+ # Test single value
+ G = nx.path_graph(3, create_using=graph_type)
+ attr = "hello"
+ vals = 3
+ nx.set_edge_attributes(G, vals, attr)
+ assert G[0][1][attr] == vals
+ assert G[1][2][attr] == vals
+
+ # Test multiple values
+ G = nx.path_graph(3, create_using=graph_type)
+ attr = "hi"
+ edges = [(0, 1), (1, 2)]
+ vals = dict(zip(edges, range(len(edges))))
+ nx.set_edge_attributes(G, vals, attr)
+ assert G[0][1][attr] == 0
+ assert G[1][2][attr] == 1
+
+ # Test dictionary of dictionaries
+ G = nx.path_graph(3, create_using=graph_type)
+ d = {"hi": 0, "hello": 200}
+ edges = [(0, 1)]
+ vals = dict.fromkeys(edges, d)
+ nx.set_edge_attributes(G, vals)
+ assert G[0][1]["hi"] == 0
+ assert G[0][1]["hello"] == 200
+ assert G[1][2] == {}
+
+
+@pytest.mark.parametrize(
+ ("values", "name"),
+ (
+ ({(0, 1): 1.0, (0, 2): 2.0}, "weight"), # values dict
+ ({(0, 1): {"weight": 1.0}, (0, 2): {"weight": 2.0}}, None), # values dod
+ ),
+)
+def test_set_edge_attributes_ignores_extra_edges(values, name):
+ """If `values` is a dict or dict-of-dicts containing edges that are not in
+ G, data associate with these edges should be ignored.
+ """
+ G = nx.Graph([(0, 1)])
+ nx.set_edge_attributes(G, values, name)
+ assert G[0][1]["weight"] == 1.0
+ assert (0, 2) not in G.edges
+
+
+@pytest.mark.parametrize("graph_type", (nx.MultiGraph, nx.MultiDiGraph))
+def test_set_edge_attributes_multi(graph_type):
+ # Test single value
+ G = nx.path_graph(3, create_using=graph_type)
+ attr = "hello"
+ vals = 3
+ nx.set_edge_attributes(G, vals, attr)
+ assert G[0][1][0][attr] == vals
+ assert G[1][2][0][attr] == vals
+
+ # Test multiple values
+ G = nx.path_graph(3, create_using=graph_type)
+ attr = "hi"
+ edges = [(0, 1, 0), (1, 2, 0)]
+ vals = dict(zip(edges, range(len(edges))))
+ nx.set_edge_attributes(G, vals, attr)
+ assert G[0][1][0][attr] == 0
+ assert G[1][2][0][attr] == 1
+
+ # Test dictionary of dictionaries
+ G = nx.path_graph(3, create_using=graph_type)
+ d = {"hi": 0, "hello": 200}
+ edges = [(0, 1, 0)]
+ vals = dict.fromkeys(edges, d)
+ nx.set_edge_attributes(G, vals)
+ assert G[0][1][0]["hi"] == 0
+ assert G[0][1][0]["hello"] == 200
+ assert G[1][2][0] == {}
+
+
+@pytest.mark.parametrize(
+ ("values", "name"),
+ (
+ ({(0, 1, 0): 1.0, (0, 2, 0): 2.0}, "weight"), # values dict
+ ({(0, 1, 0): {"weight": 1.0}, (0, 2, 0): {"weight": 2.0}}, None), # values dod
+ ),
+)
+def test_set_edge_attributes_multi_ignores_extra_edges(values, name):
+ """If `values` is a dict or dict-of-dicts containing edges that are not in
+ G, data associate with these edges should be ignored.
+ """
+ G = nx.MultiGraph([(0, 1, 0), (0, 1, 1)])
+ nx.set_edge_attributes(G, values, name)
+ assert G[0][1][0]["weight"] == 1.0
+ assert G[0][1][1] == {}
+ assert (0, 2) not in G.edges()
+
+
+def test_get_node_attributes():
+ graphs = [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()]
+ for G in graphs:
+ G = nx.path_graph(3, create_using=G)
+ attr = "hello"
+ vals = 100
+ nx.set_node_attributes(G, vals, attr)
+ attrs = nx.get_node_attributes(G, attr)
+ assert attrs[0] == vals
+ assert attrs[1] == vals
+ assert attrs[2] == vals
+ default_val = 1
+ G.add_node(4)
+ attrs = nx.get_node_attributes(G, attr, default=default_val)
+ assert attrs[4] == default_val
+
+
+def test_get_edge_attributes():
+ graphs = [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()]
+ for G in graphs:
+ G = nx.path_graph(3, create_using=G)
+ attr = "hello"
+ vals = 100
+ nx.set_edge_attributes(G, vals, attr)
+ attrs = nx.get_edge_attributes(G, attr)
+ assert len(attrs) == 2
+
+ for edge in G.edges:
+ assert attrs[edge] == vals
+
+ default_val = vals
+ G.add_edge(4, 5)
+ deafult_attrs = nx.get_edge_attributes(G, attr, default=default_val)
+ assert len(deafult_attrs) == 3
+
+ for edge in G.edges:
+ assert deafult_attrs[edge] == vals
+
+
+@pytest.mark.parametrize(
+ "graph_type", (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
+)
+def test_remove_node_attributes(graph_type):
+ # Test removing single attribute
+ G = nx.path_graph(3, create_using=graph_type)
+ vals = 100
+ attr = "hello"
+ nx.set_node_attributes(G, vals, attr)
+ nx.remove_node_attributes(G, attr)
+ assert attr not in G.nodes[0]
+ assert attr not in G.nodes[1]
+ assert attr not in G.nodes[2]
+
+ # Test removing single attribute when multiple present
+ G = nx.path_graph(3, create_using=graph_type)
+ other_vals = 200
+ other_attr = "other"
+ nx.set_node_attributes(G, vals, attr)
+ nx.set_node_attributes(G, other_vals, other_attr)
+ nx.remove_node_attributes(G, attr)
+ assert attr not in G.nodes[0]
+ assert G.nodes[0][other_attr] == other_vals
+ assert attr not in G.nodes[1]
+ assert G.nodes[1][other_attr] == other_vals
+ assert attr not in G.nodes[2]
+ assert G.nodes[2][other_attr] == other_vals
+
+ # Test removing multiple attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ nx.set_node_attributes(G, vals, attr)
+ nx.set_node_attributes(G, other_vals, other_attr)
+ nx.remove_node_attributes(G, attr, other_attr)
+ assert attr not in G.nodes[0] and other_attr not in G.nodes[0]
+ assert attr not in G.nodes[1] and other_attr not in G.nodes[1]
+ assert attr not in G.nodes[2] and other_attr not in G.nodes[2]
+
+ # Test removing multiple (but not all) attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ third_vals = 300
+ third_attr = "three"
+ nx.set_node_attributes(
+ G,
+ {
+ n: {attr: vals, other_attr: other_vals, third_attr: third_vals}
+ for n in G.nodes()
+ },
+ )
+ nx.remove_node_attributes(G, other_attr, third_attr)
+ assert other_attr not in G.nodes[0] and third_attr not in G.nodes[0]
+ assert other_attr not in G.nodes[1] and third_attr not in G.nodes[1]
+ assert other_attr not in G.nodes[2] and third_attr not in G.nodes[2]
+ assert G.nodes[0][attr] == vals
+ assert G.nodes[1][attr] == vals
+ assert G.nodes[2][attr] == vals
+
+ # Test incomplete node attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ nx.set_node_attributes(
+ G,
+ {
+ 1: {attr: vals, other_attr: other_vals},
+ 2: {attr: vals, other_attr: other_vals},
+ },
+ )
+ nx.remove_node_attributes(G, attr)
+ assert attr not in G.nodes[0]
+ assert attr not in G.nodes[1]
+ assert attr not in G.nodes[2]
+ assert G.nodes[1][other_attr] == other_vals
+ assert G.nodes[2][other_attr] == other_vals
+
+ # Test removing on a subset of nodes
+ G = nx.path_graph(3, create_using=graph_type)
+ nx.set_node_attributes(
+ G,
+ {
+ n: {attr: vals, other_attr: other_vals, third_attr: third_vals}
+ for n in G.nodes()
+ },
+ )
+ nx.remove_node_attributes(G, attr, other_attr, nbunch=[0, 1])
+ assert attr not in G.nodes[0] and other_attr not in G.nodes[0]
+ assert attr not in G.nodes[1] and other_attr not in G.nodes[1]
+ assert attr in G.nodes[2] and other_attr in G.nodes[2]
+ assert third_attr in G.nodes[0] and G.nodes[0][third_attr] == third_vals
+ assert third_attr in G.nodes[1] and G.nodes[1][third_attr] == third_vals
+
+
+@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph))
+def test_remove_edge_attributes(graph_type):
+ # Test removing single attribute
+ G = nx.path_graph(3, create_using=graph_type)
+ attr = "hello"
+ vals = 100
+ nx.set_edge_attributes(G, vals, attr)
+ nx.remove_edge_attributes(G, attr)
+ assert len(nx.get_edge_attributes(G, attr)) == 0
+
+ # Test removing only some attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ other_attr = "other"
+ other_vals = 200
+ nx.set_edge_attributes(G, vals, attr)
+ nx.set_edge_attributes(G, other_vals, other_attr)
+ nx.remove_edge_attributes(G, attr)
+
+ assert attr not in G[0][1]
+ assert attr not in G[1][2]
+ assert G[0][1][other_attr] == 200
+ assert G[1][2][other_attr] == 200
+
+ # Test removing multiple attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ nx.set_edge_attributes(G, vals, attr)
+ nx.set_edge_attributes(G, other_vals, other_attr)
+ nx.remove_edge_attributes(G, attr, other_attr)
+ assert attr not in G[0][1] and other_attr not in G[0][1]
+ assert attr not in G[1][2] and other_attr not in G[1][2]
+
+ # Test removing multiple (not all) attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ third_attr = "third"
+ third_vals = 300
+ nx.set_edge_attributes(
+ G,
+ {
+ (u, v): {attr: vals, other_attr: other_vals, third_attr: third_vals}
+ for u, v in G.edges()
+ },
+ )
+ nx.remove_edge_attributes(G, other_attr, third_attr)
+ assert other_attr not in G[0][1] and third_attr not in G[0][1]
+ assert other_attr not in G[1][2] and third_attr not in G[1][2]
+ assert G[0][1][attr] == vals
+ assert G[1][2][attr] == vals
+
+ # Test removing incomplete edge attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ nx.set_edge_attributes(G, {(0, 1): {attr: vals, other_attr: other_vals}})
+ nx.remove_edge_attributes(G, other_attr)
+ assert other_attr not in G[0][1] and G[0][1][attr] == vals
+ assert other_attr not in G[1][2]
+
+ # Test removing subset of edge attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ nx.set_edge_attributes(
+ G,
+ {
+ (u, v): {attr: vals, other_attr: other_vals, third_attr: third_vals}
+ for u, v in G.edges()
+ },
+ )
+ nx.remove_edge_attributes(G, other_attr, third_attr, ebunch=[(0, 1)])
+ assert other_attr not in G[0][1] and third_attr not in G[0][1]
+ assert other_attr in G[1][2] and third_attr in G[1][2]
+
+
+@pytest.mark.parametrize("graph_type", (nx.MultiGraph, nx.MultiDiGraph))
+def test_remove_multi_edge_attributes(graph_type):
+ # Test removing single attribute
+ G = nx.path_graph(3, create_using=graph_type)
+ G.add_edge(1, 2)
+ attr = "hello"
+ vals = 100
+ nx.set_edge_attributes(G, vals, attr)
+ nx.remove_edge_attributes(G, attr)
+ assert attr not in G[0][1][0]
+ assert attr not in G[1][2][0]
+ assert attr not in G[1][2][1]
+
+ # Test removing only some attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ G.add_edge(1, 2)
+ other_attr = "other"
+ other_vals = 200
+ nx.set_edge_attributes(G, vals, attr)
+ nx.set_edge_attributes(G, other_vals, other_attr)
+ nx.remove_edge_attributes(G, attr)
+ assert attr not in G[0][1][0]
+ assert attr not in G[1][2][0]
+ assert attr not in G[1][2][1]
+ assert G[0][1][0][other_attr] == other_vals
+ assert G[1][2][0][other_attr] == other_vals
+ assert G[1][2][1][other_attr] == other_vals
+
+ # Test removing multiple attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ G.add_edge(1, 2)
+ nx.set_edge_attributes(G, vals, attr)
+ nx.set_edge_attributes(G, other_vals, other_attr)
+ nx.remove_edge_attributes(G, attr, other_attr)
+ assert attr not in G[0][1][0] and other_attr not in G[0][1][0]
+ assert attr not in G[1][2][0] and other_attr not in G[1][2][0]
+ assert attr not in G[1][2][1] and other_attr not in G[1][2][1]
+
+ # Test removing multiple (not all) attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ G.add_edge(1, 2)
+ third_attr = "third"
+ third_vals = 300
+ nx.set_edge_attributes(
+ G,
+ {
+ (u, v, k): {attr: vals, other_attr: other_vals, third_attr: third_vals}
+ for u, v, k in G.edges(keys=True)
+ },
+ )
+ nx.remove_edge_attributes(G, other_attr, third_attr)
+ assert other_attr not in G[0][1][0] and third_attr not in G[0][1][0]
+ assert other_attr not in G[1][2][0] and other_attr not in G[1][2][0]
+ assert other_attr not in G[1][2][1] and other_attr not in G[1][2][1]
+ assert G[0][1][0][attr] == vals
+ assert G[1][2][0][attr] == vals
+ assert G[1][2][1][attr] == vals
+
+ # Test removing incomplete edge attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ G.add_edge(1, 2)
+ nx.set_edge_attributes(
+ G,
+ {
+ (0, 1, 0): {attr: vals, other_attr: other_vals},
+ (1, 2, 1): {attr: vals, other_attr: other_vals},
+ },
+ )
+ nx.remove_edge_attributes(G, other_attr)
+ assert other_attr not in G[0][1][0] and G[0][1][0][attr] == vals
+ assert other_attr not in G[1][2][0]
+ assert other_attr not in G[1][2][1]
+
+ # Test removing subset of edge attributes
+ G = nx.path_graph(3, create_using=graph_type)
+ G.add_edge(1, 2)
+ nx.set_edge_attributes(
+ G,
+ {
+ (0, 1, 0): {attr: vals, other_attr: other_vals},
+ (1, 2, 0): {attr: vals, other_attr: other_vals},
+ (1, 2, 1): {attr: vals, other_attr: other_vals},
+ },
+ )
+ nx.remove_edge_attributes(G, attr, ebunch=[(0, 1, 0), (1, 2, 0)])
+ assert attr not in G[0][1][0] and other_attr in G[0][1][0]
+ assert attr not in G[1][2][0] and other_attr in G[1][2][0]
+ assert attr in G[1][2][1] and other_attr in G[1][2][1]
+
+
+def test_is_empty():
+ graphs = [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()]
+ for G in graphs:
+ assert nx.is_empty(G)
+ G.add_nodes_from(range(5))
+ assert nx.is_empty(G)
+ G.add_edges_from([(1, 2), (3, 4)])
+ assert not nx.is_empty(G)
+
+
+@pytest.mark.parametrize(
+ "graph_type", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
+)
+def test_selfloops(graph_type):
+ G = nx.complete_graph(3, create_using=graph_type)
+ G.add_edge(0, 0)
+ assert nodes_equal(nx.nodes_with_selfloops(G), [0])
+ assert edges_equal(nx.selfloop_edges(G), [(0, 0)])
+ assert edges_equal(nx.selfloop_edges(G, data=True), [(0, 0, {})])
+ assert nx.number_of_selfloops(G) == 1
+
+
+@pytest.mark.parametrize(
+ "graph_type", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
+)
+def test_selfloop_edges_attr(graph_type):
+ G = nx.complete_graph(3, create_using=graph_type)
+ G.add_edge(0, 0)
+ G.add_edge(1, 1, weight=2)
+ assert edges_equal(
+ nx.selfloop_edges(G, data=True), [(0, 0, {}), (1, 1, {"weight": 2})]
+ )
+ assert edges_equal(nx.selfloop_edges(G, data="weight"), [(0, 0, None), (1, 1, 2)])
+
+
+def test_selfloop_edges_multi_with_data_and_keys():
+ G = nx.complete_graph(3, create_using=nx.MultiGraph)
+ G.add_edge(0, 0, weight=10)
+ G.add_edge(0, 0, weight=100)
+ assert edges_equal(
+ nx.selfloop_edges(G, data="weight", keys=True), [(0, 0, 0, 10), (0, 0, 1, 100)]
+ )
+
+
+@pytest.mark.parametrize("graph_type", [nx.Graph, nx.DiGraph])
+def test_selfloops_removal(graph_type):
+ G = nx.complete_graph(3, create_using=graph_type)
+ G.add_edge(0, 0)
+ G.remove_edges_from(nx.selfloop_edges(G, keys=True))
+ G.add_edge(0, 0)
+ G.remove_edges_from(nx.selfloop_edges(G, data=True))
+ G.add_edge(0, 0)
+ G.remove_edges_from(nx.selfloop_edges(G, keys=True, data=True))
+
+
+@pytest.mark.parametrize("graph_type", [nx.MultiGraph, nx.MultiDiGraph])
+def test_selfloops_removal_multi(graph_type):
+ """test removing selfloops behavior vis-a-vis altering a dict while iterating.
+ cf. gh-4068"""
+ G = nx.complete_graph(3, create_using=graph_type)
+ # Defaults - see gh-4080
+ G.add_edge(0, 0)
+ G.add_edge(0, 0)
+ G.remove_edges_from(nx.selfloop_edges(G))
+ assert (0, 0) not in G.edges()
+ # With keys
+ G.add_edge(0, 0)
+ G.add_edge(0, 0)
+ with pytest.raises(RuntimeError):
+ G.remove_edges_from(nx.selfloop_edges(G, keys=True))
+ # With data
+ G.add_edge(0, 0)
+ G.add_edge(0, 0)
+ with pytest.raises(TypeError):
+ G.remove_edges_from(nx.selfloop_edges(G, data=True))
+ # With keys and data
+ G.add_edge(0, 0)
+ G.add_edge(0, 0)
+ with pytest.raises(RuntimeError):
+ G.remove_edges_from(nx.selfloop_edges(G, data=True, keys=True))
+
+
+def test_pathweight():
+ valid_path = [1, 2, 3]
+ invalid_path = [1, 3, 2]
+ graphs = [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()]
+ edges = [
+ (1, 2, {"cost": 5, "dist": 6}),
+ (2, 3, {"cost": 3, "dist": 4}),
+ (1, 2, {"cost": 1, "dist": 2}),
+ ]
+ for graph in graphs:
+ graph.add_edges_from(edges)
+ assert nx.path_weight(graph, valid_path, "cost") == 4
+ assert nx.path_weight(graph, valid_path, "dist") == 6
+ pytest.raises(nx.NetworkXNoPath, nx.path_weight, graph, invalid_path, "cost")
+
+
+@pytest.mark.parametrize(
+ "G", (nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph())
+)
+def test_ispath(G):
+ G.add_edges_from([(1, 2), (2, 3), (1, 2), (3, 4)])
+ valid_path = [1, 2, 3, 4]
+ invalid_path = [1, 2, 4, 3] # wrong node order
+ another_invalid_path = [1, 2, 3, 4, 5] # contains node not in G
+ assert nx.is_path(G, valid_path)
+ assert not nx.is_path(G, invalid_path)
+ assert not nx.is_path(G, another_invalid_path)
+
+
+@pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph()))
+def test_restricted_view(G):
+ G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2)])
+ G.add_node(4)
+ H = nx.restricted_view(G, [0, 2, 5], [(1, 2), (3, 4)])
+ assert set(H.nodes()) == {1, 3, 4}
+ assert set(H.edges()) == {(1, 1)}
+
+
+@pytest.mark.parametrize("G", (nx.MultiGraph(), nx.MultiDiGraph()))
+def test_restricted_view_multi(G):
+ G.add_edges_from(
+ [(0, 1, 0), (0, 2, 0), (0, 3, 0), (0, 1, 1), (1, 0, 0), (1, 1, 0), (1, 2, 0)]
+ )
+ G.add_node(4)
+ H = nx.restricted_view(G, [0, 2, 5], [(1, 2, 0), (3, 4, 0)])
+ assert set(H.nodes()) == {1, 3, 4}
+ assert set(H.edges()) == {(1, 1)}
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graph.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graph.py
new file mode 100644
index 00000000..b0048a31
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graph.py
@@ -0,0 +1,920 @@
+import gc
+import pickle
+import platform
+import weakref
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal, graphs_equal, nodes_equal
+
+
+class BaseGraphTester:
+ """Tests for data-structure independent graph class features."""
+
+ def test_contains(self):
+ G = self.K3
+ assert 1 in G
+ assert 4 not in G
+ assert "b" not in G
+ assert [] not in G # no exception for nonhashable
+ assert {1: 1} not in G # no exception for nonhashable
+
+ def test_order(self):
+ G = self.K3
+ assert len(G) == 3
+ assert G.order() == 3
+ assert G.number_of_nodes() == 3
+
+ def test_nodes(self):
+ G = self.K3
+ assert isinstance(G._node, G.node_dict_factory)
+ assert isinstance(G._adj, G.adjlist_outer_dict_factory)
+ assert all(
+ isinstance(adj, G.adjlist_inner_dict_factory) for adj in G._adj.values()
+ )
+ assert sorted(G.nodes()) == self.k3nodes
+ assert sorted(G.nodes(data=True)) == [(0, {}), (1, {}), (2, {})]
+
+ def test_none_node(self):
+ G = self.Graph()
+ with pytest.raises(ValueError):
+ G.add_node(None)
+ with pytest.raises(ValueError):
+ G.add_nodes_from([None])
+ with pytest.raises(ValueError):
+ G.add_edge(0, None)
+ with pytest.raises(ValueError):
+ G.add_edges_from([(0, None)])
+
+ def test_has_node(self):
+ G = self.K3
+ assert G.has_node(1)
+ assert not G.has_node(4)
+ assert not G.has_node([]) # no exception for nonhashable
+ assert not G.has_node({1: 1}) # no exception for nonhashable
+
+ def test_has_edge(self):
+ G = self.K3
+ assert G.has_edge(0, 1)
+ assert not G.has_edge(0, -1)
+
+ def test_neighbors(self):
+ G = self.K3
+ assert sorted(G.neighbors(0)) == [1, 2]
+ with pytest.raises(nx.NetworkXError):
+ G.neighbors(-1)
+
+ @pytest.mark.skipif(
+ platform.python_implementation() == "PyPy", reason="PyPy gc is different"
+ )
+ def test_memory_leak(self):
+ G = self.Graph()
+
+ def count_objects_of_type(_type):
+ # Iterating over all objects tracked by gc can include weak references
+ # whose weakly-referenced objects may no longer exist. Calling `isinstance`
+ # on such a weak reference will raise ReferenceError. There are at least
+ # three workarounds for this: one is to compare type names instead of using
+ # `isinstance` such as `type(obj).__name__ == typename`, another is to use
+ # `type(obj) == _type`, and the last is to ignore ProxyTypes as we do below.
+ # NOTE: even if this safeguard is deemed unnecessary to pass NetworkX tests,
+ # we should still keep it for maximum safety for other NetworkX backends.
+ return sum(
+ 1
+ for obj in gc.get_objects()
+ if not isinstance(obj, weakref.ProxyTypes) and isinstance(obj, _type)
+ )
+
+ gc.collect()
+ before = count_objects_of_type(self.Graph)
+ G.copy()
+ gc.collect()
+ after = count_objects_of_type(self.Graph)
+ assert before == after
+
+ # test a subgraph of the base class
+ class MyGraph(self.Graph):
+ pass
+
+ gc.collect()
+ G = MyGraph()
+ before = count_objects_of_type(MyGraph)
+ G.copy()
+ gc.collect()
+ after = count_objects_of_type(MyGraph)
+ assert before == after
+
+ def test_edges(self):
+ G = self.K3
+ assert isinstance(G._adj, G.adjlist_outer_dict_factory)
+ assert edges_equal(G.edges(), [(0, 1), (0, 2), (1, 2)])
+ assert edges_equal(G.edges(0), [(0, 1), (0, 2)])
+ assert edges_equal(G.edges([0, 1]), [(0, 1), (0, 2), (1, 2)])
+ with pytest.raises(nx.NetworkXError):
+ G.edges(-1)
+
+ def test_degree(self):
+ G = self.K3
+ assert sorted(G.degree()) == [(0, 2), (1, 2), (2, 2)]
+ assert dict(G.degree()) == {0: 2, 1: 2, 2: 2}
+ assert G.degree(0) == 2
+ with pytest.raises(nx.NetworkXError):
+ G.degree(-1) # node not in graph
+
+ def test_size(self):
+ G = self.K3
+ assert G.size() == 3
+ assert G.number_of_edges() == 3
+
+ def test_nbunch_iter(self):
+ G = self.K3
+ assert nodes_equal(G.nbunch_iter(), self.k3nodes) # all nodes
+ assert nodes_equal(G.nbunch_iter(0), [0]) # single node
+ assert nodes_equal(G.nbunch_iter([0, 1]), [0, 1]) # sequence
+ # sequence with none in graph
+ assert nodes_equal(G.nbunch_iter([-1]), [])
+ # string sequence with none in graph
+ assert nodes_equal(G.nbunch_iter("foo"), [])
+ # node not in graph doesn't get caught upon creation of iterator
+ bunch = G.nbunch_iter(-1)
+ # but gets caught when iterator used
+ with pytest.raises(nx.NetworkXError, match="is not a node or a sequence"):
+ list(bunch)
+ # unhashable doesn't get caught upon creation of iterator
+ bunch = G.nbunch_iter([0, 1, 2, {}])
+ # but gets caught when iterator hits the unhashable
+ with pytest.raises(
+ nx.NetworkXError, match="in sequence nbunch is not a valid node"
+ ):
+ list(bunch)
+
+ def test_nbunch_iter_node_format_raise(self):
+ # Tests that a node that would have failed string formatting
+ # doesn't cause an error when attempting to raise a
+ # :exc:`nx.NetworkXError`.
+
+ # For more information, see pull request #1813.
+ G = self.Graph()
+ nbunch = [("x", set())]
+ with pytest.raises(nx.NetworkXError):
+ list(G.nbunch_iter(nbunch))
+
+ def test_selfloop_degree(self):
+ G = self.Graph()
+ G.add_edge(1, 1)
+ assert sorted(G.degree()) == [(1, 2)]
+ assert dict(G.degree()) == {1: 2}
+ assert G.degree(1) == 2
+ assert sorted(G.degree([1])) == [(1, 2)]
+ assert G.degree(1, weight="weight") == 2
+
+ def test_selfloops(self):
+ G = self.K3.copy()
+ G.add_edge(0, 0)
+ assert nodes_equal(nx.nodes_with_selfloops(G), [0])
+ assert edges_equal(nx.selfloop_edges(G), [(0, 0)])
+ assert nx.number_of_selfloops(G) == 1
+ G.remove_edge(0, 0)
+ G.add_edge(0, 0)
+ G.remove_edges_from([(0, 0)])
+ G.add_edge(1, 1)
+ G.remove_node(1)
+ G.add_edge(0, 0)
+ G.add_edge(1, 1)
+ G.remove_nodes_from([0, 1])
+
+ def test_cache_reset(self):
+ G = self.K3.copy()
+ old_adj = G.adj
+ assert id(G.adj) == id(old_adj)
+ G._adj = {}
+ assert id(G.adj) != id(old_adj)
+
+ old_nodes = G.nodes
+ assert id(G.nodes) == id(old_nodes)
+ G._node = {}
+ assert id(G.nodes) != id(old_nodes)
+
+ def test_attributes_cached(self):
+ G = self.K3.copy()
+ assert id(G.nodes) == id(G.nodes)
+ assert id(G.edges) == id(G.edges)
+ assert id(G.degree) == id(G.degree)
+ assert id(G.adj) == id(G.adj)
+
+
+class BaseAttrGraphTester(BaseGraphTester):
+ """Tests of graph class attribute features."""
+
+ def test_weighted_degree(self):
+ G = self.Graph()
+ G.add_edge(1, 2, weight=2, other=3)
+ G.add_edge(2, 3, weight=3, other=4)
+ assert sorted(d for n, d in G.degree(weight="weight")) == [2, 3, 5]
+ assert dict(G.degree(weight="weight")) == {1: 2, 2: 5, 3: 3}
+ assert G.degree(1, weight="weight") == 2
+ assert nodes_equal((G.degree([1], weight="weight")), [(1, 2)])
+
+ assert nodes_equal((d for n, d in G.degree(weight="other")), [3, 7, 4])
+ assert dict(G.degree(weight="other")) == {1: 3, 2: 7, 3: 4}
+ assert G.degree(1, weight="other") == 3
+ assert edges_equal((G.degree([1], weight="other")), [(1, 3)])
+
+ def add_attributes(self, G):
+ G.graph["foo"] = []
+ G.nodes[0]["foo"] = []
+ G.remove_edge(1, 2)
+ ll = []
+ G.add_edge(1, 2, foo=ll)
+ G.add_edge(2, 1, foo=ll)
+
+ def test_name(self):
+ G = self.Graph(name="")
+ assert G.name == ""
+ G = self.Graph(name="test")
+ assert G.name == "test"
+
+ def test_str_unnamed(self):
+ G = self.Graph()
+ G.add_edges_from([(1, 2), (2, 3)])
+ assert str(G) == f"{type(G).__name__} with 3 nodes and 2 edges"
+
+ def test_str_named(self):
+ G = self.Graph(name="foo")
+ G.add_edges_from([(1, 2), (2, 3)])
+ assert str(G) == f"{type(G).__name__} named 'foo' with 3 nodes and 2 edges"
+
+ def test_graph_chain(self):
+ G = self.Graph([(0, 1), (1, 2)])
+ DG = G.to_directed(as_view=True)
+ SDG = DG.subgraph([0, 1])
+ RSDG = SDG.reverse(copy=False)
+ assert G is DG._graph
+ assert DG is SDG._graph
+ assert SDG is RSDG._graph
+
+ def test_copy(self):
+ G = self.Graph()
+ G.add_node(0)
+ G.add_edge(1, 2)
+ self.add_attributes(G)
+ # copy edge datadict but any container attr are same
+ H = G.copy()
+ self.graphs_equal(H, G)
+ self.different_attrdict(H, G)
+ self.shallow_copy_attrdict(H, G)
+
+ def test_class_copy(self):
+ G = self.Graph()
+ G.add_node(0)
+ G.add_edge(1, 2)
+ self.add_attributes(G)
+ # copy edge datadict but any container attr are same
+ H = G.__class__(G)
+ self.graphs_equal(H, G)
+ self.different_attrdict(H, G)
+ self.shallow_copy_attrdict(H, G)
+
+ def test_fresh_copy(self):
+ G = self.Graph()
+ G.add_node(0)
+ G.add_edge(1, 2)
+ self.add_attributes(G)
+ # copy graph structure but use fresh datadict
+ H = G.__class__()
+ H.add_nodes_from(G)
+ H.add_edges_from(G.edges())
+ assert len(G.nodes[0]) == 1
+ ddict = G.adj[1][2][0] if G.is_multigraph() else G.adj[1][2]
+ assert len(ddict) == 1
+ assert len(H.nodes[0]) == 0
+ ddict = H.adj[1][2][0] if H.is_multigraph() else H.adj[1][2]
+ assert len(ddict) == 0
+
+ def is_deepcopy(self, H, G):
+ self.graphs_equal(H, G)
+ self.different_attrdict(H, G)
+ self.deep_copy_attrdict(H, G)
+
+ def deep_copy_attrdict(self, H, G):
+ self.deepcopy_graph_attr(H, G)
+ self.deepcopy_node_attr(H, G)
+ self.deepcopy_edge_attr(H, G)
+
+ def deepcopy_graph_attr(self, H, G):
+ assert G.graph["foo"] == H.graph["foo"]
+ G.graph["foo"].append(1)
+ assert G.graph["foo"] != H.graph["foo"]
+
+ def deepcopy_node_attr(self, H, G):
+ assert G.nodes[0]["foo"] == H.nodes[0]["foo"]
+ G.nodes[0]["foo"].append(1)
+ assert G.nodes[0]["foo"] != H.nodes[0]["foo"]
+
+ def deepcopy_edge_attr(self, H, G):
+ assert G[1][2]["foo"] == H[1][2]["foo"]
+ G[1][2]["foo"].append(1)
+ assert G[1][2]["foo"] != H[1][2]["foo"]
+
+ def is_shallow_copy(self, H, G):
+ self.graphs_equal(H, G)
+ self.shallow_copy_attrdict(H, G)
+
+ def shallow_copy_attrdict(self, H, G):
+ self.shallow_copy_graph_attr(H, G)
+ self.shallow_copy_node_attr(H, G)
+ self.shallow_copy_edge_attr(H, G)
+
+ def shallow_copy_graph_attr(self, H, G):
+ assert G.graph["foo"] == H.graph["foo"]
+ G.graph["foo"].append(1)
+ assert G.graph["foo"] == H.graph["foo"]
+
+ def shallow_copy_node_attr(self, H, G):
+ assert G.nodes[0]["foo"] == H.nodes[0]["foo"]
+ G.nodes[0]["foo"].append(1)
+ assert G.nodes[0]["foo"] == H.nodes[0]["foo"]
+
+ def shallow_copy_edge_attr(self, H, G):
+ assert G[1][2]["foo"] == H[1][2]["foo"]
+ G[1][2]["foo"].append(1)
+ assert G[1][2]["foo"] == H[1][2]["foo"]
+
+ def same_attrdict(self, H, G):
+ old_foo = H[1][2]["foo"]
+ H.adj[1][2]["foo"] = "baz"
+ assert G.edges == H.edges
+ H.adj[1][2]["foo"] = old_foo
+ assert G.edges == H.edges
+
+ old_foo = H.nodes[0]["foo"]
+ H.nodes[0]["foo"] = "baz"
+ assert G.nodes == H.nodes
+ H.nodes[0]["foo"] = old_foo
+ assert G.nodes == H.nodes
+
+ def different_attrdict(self, H, G):
+ old_foo = H[1][2]["foo"]
+ H.adj[1][2]["foo"] = "baz"
+ assert G._adj != H._adj
+ H.adj[1][2]["foo"] = old_foo
+ assert G._adj == H._adj
+
+ old_foo = H.nodes[0]["foo"]
+ H.nodes[0]["foo"] = "baz"
+ assert G._node != H._node
+ H.nodes[0]["foo"] = old_foo
+ assert G._node == H._node
+
+ def graphs_equal(self, H, G):
+ assert G._adj == H._adj
+ assert G._node == H._node
+ assert G.graph == H.graph
+ assert G.name == H.name
+ if not G.is_directed() and not H.is_directed():
+ assert H._adj[1][2] is H._adj[2][1]
+ assert G._adj[1][2] is G._adj[2][1]
+ else: # at least one is directed
+ if not G.is_directed():
+ G._pred = G._adj
+ G._succ = G._adj
+ if not H.is_directed():
+ H._pred = H._adj
+ H._succ = H._adj
+ assert G._pred == H._pred
+ assert G._succ == H._succ
+ assert H._succ[1][2] is H._pred[2][1]
+ assert G._succ[1][2] is G._pred[2][1]
+
+ def test_graph_attr(self):
+ G = self.K3.copy()
+ G.graph["foo"] = "bar"
+ assert isinstance(G.graph, G.graph_attr_dict_factory)
+ assert G.graph["foo"] == "bar"
+ del G.graph["foo"]
+ assert G.graph == {}
+ H = self.Graph(foo="bar")
+ assert H.graph["foo"] == "bar"
+
+ def test_node_attr(self):
+ G = self.K3.copy()
+ G.add_node(1, foo="bar")
+ assert all(
+ isinstance(d, G.node_attr_dict_factory) for u, d in G.nodes(data=True)
+ )
+ assert nodes_equal(G.nodes(), [0, 1, 2])
+ assert nodes_equal(G.nodes(data=True), [(0, {}), (1, {"foo": "bar"}), (2, {})])
+ G.nodes[1]["foo"] = "baz"
+ assert nodes_equal(G.nodes(data=True), [(0, {}), (1, {"foo": "baz"}), (2, {})])
+ assert nodes_equal(G.nodes(data="foo"), [(0, None), (1, "baz"), (2, None)])
+ assert nodes_equal(
+ G.nodes(data="foo", default="bar"), [(0, "bar"), (1, "baz"), (2, "bar")]
+ )
+
+ def test_node_attr2(self):
+ G = self.K3.copy()
+ a = {"foo": "bar"}
+ G.add_node(3, **a)
+ assert nodes_equal(G.nodes(), [0, 1, 2, 3])
+ assert nodes_equal(
+ G.nodes(data=True), [(0, {}), (1, {}), (2, {}), (3, {"foo": "bar"})]
+ )
+
+ def test_edge_lookup(self):
+ G = self.Graph()
+ G.add_edge(1, 2, foo="bar")
+ assert edges_equal(G.edges[1, 2], {"foo": "bar"})
+
+ def test_edge_attr(self):
+ G = self.Graph()
+ G.add_edge(1, 2, foo="bar")
+ assert all(
+ isinstance(d, G.edge_attr_dict_factory) for u, v, d in G.edges(data=True)
+ )
+ assert edges_equal(G.edges(data=True), [(1, 2, {"foo": "bar"})])
+ assert edges_equal(G.edges(data="foo"), [(1, 2, "bar")])
+
+ def test_edge_attr2(self):
+ G = self.Graph()
+ G.add_edges_from([(1, 2), (3, 4)], foo="foo")
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"foo": "foo"}), (3, 4, {"foo": "foo"})]
+ )
+ assert edges_equal(G.edges(data="foo"), [(1, 2, "foo"), (3, 4, "foo")])
+
+ def test_edge_attr3(self):
+ G = self.Graph()
+ G.add_edges_from([(1, 2, {"weight": 32}), (3, 4, {"weight": 64})], foo="foo")
+ assert edges_equal(
+ G.edges(data=True),
+ [
+ (1, 2, {"foo": "foo", "weight": 32}),
+ (3, 4, {"foo": "foo", "weight": 64}),
+ ],
+ )
+
+ G.remove_edges_from([(1, 2), (3, 4)])
+ G.add_edge(1, 2, data=7, spam="bar", bar="foo")
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"data": 7, "spam": "bar", "bar": "foo"})]
+ )
+
+ def test_edge_attr4(self):
+ G = self.Graph()
+ G.add_edge(1, 2, data=7, spam="bar", bar="foo")
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"data": 7, "spam": "bar", "bar": "foo"})]
+ )
+ G[1][2]["data"] = 10 # OK to set data like this
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"data": 10, "spam": "bar", "bar": "foo"})]
+ )
+
+ G.adj[1][2]["data"] = 20
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"data": 20, "spam": "bar", "bar": "foo"})]
+ )
+ G.edges[1, 2]["data"] = 21 # another spelling, "edge"
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"data": 21, "spam": "bar", "bar": "foo"})]
+ )
+ G.adj[1][2]["listdata"] = [20, 200]
+ G.adj[1][2]["weight"] = 20
+ dd = {
+ "data": 21,
+ "spam": "bar",
+ "bar": "foo",
+ "listdata": [20, 200],
+ "weight": 20,
+ }
+ assert edges_equal(G.edges(data=True), [(1, 2, dd)])
+
+ def test_to_undirected(self):
+ G = self.K3
+ self.add_attributes(G)
+ H = nx.Graph(G)
+ self.is_shallow_copy(H, G)
+ self.different_attrdict(H, G)
+ H = G.to_undirected()
+ self.is_deepcopy(H, G)
+
+ def test_to_directed_as_view(self):
+ H = nx.path_graph(2, create_using=self.Graph)
+ H2 = H.to_directed(as_view=True)
+ assert H is H2._graph
+ assert H2.has_edge(0, 1)
+ assert H2.has_edge(1, 0) or H.is_directed()
+ pytest.raises(nx.NetworkXError, H2.add_node, -1)
+ pytest.raises(nx.NetworkXError, H2.add_edge, 1, 2)
+ H.add_edge(1, 2)
+ assert H2.has_edge(1, 2)
+ assert H2.has_edge(2, 1) or H.is_directed()
+
+ def test_to_undirected_as_view(self):
+ H = nx.path_graph(2, create_using=self.Graph)
+ H2 = H.to_undirected(as_view=True)
+ assert H is H2._graph
+ assert H2.has_edge(0, 1)
+ assert H2.has_edge(1, 0)
+ pytest.raises(nx.NetworkXError, H2.add_node, -1)
+ pytest.raises(nx.NetworkXError, H2.add_edge, 1, 2)
+ H.add_edge(1, 2)
+ assert H2.has_edge(1, 2)
+ assert H2.has_edge(2, 1)
+
+ def test_directed_class(self):
+ G = self.Graph()
+
+ class newGraph(G.to_undirected_class()):
+ def to_directed_class(self):
+ return newDiGraph
+
+ def to_undirected_class(self):
+ return newGraph
+
+ class newDiGraph(G.to_directed_class()):
+ def to_directed_class(self):
+ return newDiGraph
+
+ def to_undirected_class(self):
+ return newGraph
+
+ G = newDiGraph() if G.is_directed() else newGraph()
+ H = G.to_directed()
+ assert isinstance(H, newDiGraph)
+ H = G.to_undirected()
+ assert isinstance(H, newGraph)
+
+ def test_to_directed(self):
+ G = self.K3
+ self.add_attributes(G)
+ H = nx.DiGraph(G)
+ self.is_shallow_copy(H, G)
+ self.different_attrdict(H, G)
+ H = G.to_directed()
+ self.is_deepcopy(H, G)
+
+ def test_subgraph(self):
+ G = self.K3
+ self.add_attributes(G)
+ H = G.subgraph([0, 1, 2, 5])
+ self.graphs_equal(H, G)
+ self.same_attrdict(H, G)
+ self.shallow_copy_attrdict(H, G)
+
+ H = G.subgraph(0)
+ assert H.adj == {0: {}}
+ H = G.subgraph([])
+ assert H.adj == {}
+ assert G.adj != {}
+
+ def test_selfloops_attr(self):
+ G = self.K3.copy()
+ G.add_edge(0, 0)
+ G.add_edge(1, 1, weight=2)
+ assert edges_equal(
+ nx.selfloop_edges(G, data=True), [(0, 0, {}), (1, 1, {"weight": 2})]
+ )
+ assert edges_equal(
+ nx.selfloop_edges(G, data="weight"), [(0, 0, None), (1, 1, 2)]
+ )
+
+
+class TestGraph(BaseAttrGraphTester):
+ """Tests specific to dict-of-dict-of-dict graph data structure"""
+
+ def setup_method(self):
+ self.Graph = nx.Graph
+ # build dict-of-dict-of-dict K3
+ ed1, ed2, ed3 = ({}, {}, {})
+ self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed1, 2: ed3}, 2: {0: ed2, 1: ed3}}
+ self.k3edges = [(0, 1), (0, 2), (1, 2)]
+ self.k3nodes = [0, 1, 2]
+ self.K3 = self.Graph()
+ self.K3._adj = self.k3adj
+ self.K3._node = {}
+ self.K3._node[0] = {}
+ self.K3._node[1] = {}
+ self.K3._node[2] = {}
+
+ def test_pickle(self):
+ G = self.K3
+ pg = pickle.loads(pickle.dumps(G, -1))
+ self.graphs_equal(pg, G)
+ pg = pickle.loads(pickle.dumps(G))
+ self.graphs_equal(pg, G)
+
+ 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: {}})]
+
+ def test_adjacency(self):
+ G = self.K3
+ assert dict(G.adjacency()) == {
+ 0: {1: {}, 2: {}},
+ 1: {0: {}, 2: {}},
+ 2: {0: {}, 1: {}},
+ }
+
+ def test_getitem(self):
+ G = self.K3
+ assert G.adj[0] == {1: {}, 2: {}}
+ assert G[0] == {1: {}, 2: {}}
+ with pytest.raises(KeyError):
+ G.__getitem__("j")
+ with pytest.raises(TypeError):
+ G.__getitem__(["A"])
+
+ def test_add_node(self):
+ G = self.Graph()
+ G.add_node(0)
+ assert G.adj == {0: {}}
+ # test add attributes
+ G.add_node(1, c="red")
+ G.add_node(2, c="blue")
+ G.add_node(3, c="red")
+ assert G.nodes[1]["c"] == "red"
+ assert G.nodes[2]["c"] == "blue"
+ assert G.nodes[3]["c"] == "red"
+ # test updating attributes
+ G.add_node(1, c="blue")
+ G.add_node(2, c="red")
+ G.add_node(3, c="blue")
+ assert G.nodes[1]["c"] == "blue"
+ assert G.nodes[2]["c"] == "red"
+ assert G.nodes[3]["c"] == "blue"
+
+ def test_add_nodes_from(self):
+ G = self.Graph()
+ G.add_nodes_from([0, 1, 2])
+ assert G.adj == {0: {}, 1: {}, 2: {}}
+ # test add attributes
+ G.add_nodes_from([0, 1, 2], c="red")
+ assert G.nodes[0]["c"] == "red"
+ assert G.nodes[2]["c"] == "red"
+ # test that attribute dicts are not the same
+ assert G.nodes[0] is not G.nodes[1]
+ # test updating attributes
+ G.add_nodes_from([0, 1, 2], c="blue")
+ assert G.nodes[0]["c"] == "blue"
+ assert G.nodes[2]["c"] == "blue"
+ assert G.nodes[0] is not G.nodes[1]
+ # test tuple input
+ H = self.Graph()
+ H.add_nodes_from(G.nodes(data=True))
+ assert H.nodes[0]["c"] == "blue"
+ assert H.nodes[2]["c"] == "blue"
+ assert H.nodes[0] is not H.nodes[1]
+ # specific overrides general
+ H.add_nodes_from([0, (1, {"c": "green"}), (3, {"c": "cyan"})], c="red")
+ assert H.nodes[0]["c"] == "red"
+ assert H.nodes[1]["c"] == "green"
+ assert H.nodes[2]["c"] == "blue"
+ assert H.nodes[3]["c"] == "cyan"
+
+ def test_remove_node(self):
+ G = self.K3.copy()
+ G.remove_node(0)
+ assert G.adj == {1: {2: {}}, 2: {1: {}}}
+ with pytest.raises(nx.NetworkXError):
+ G.remove_node(-1)
+
+ # generator here to implement list,set,string...
+
+ def test_remove_nodes_from(self):
+ G = self.K3.copy()
+ G.remove_nodes_from([0, 1])
+ assert G.adj == {2: {}}
+ G.remove_nodes_from([-1]) # silent fail
+
+ def test_add_edge(self):
+ G = self.Graph()
+ G.add_edge(0, 1)
+ assert G.adj == {0: {1: {}}, 1: {0: {}}}
+ G = self.Graph()
+ G.add_edge(*(0, 1))
+ assert G.adj == {0: {1: {}}, 1: {0: {}}}
+ G = self.Graph()
+ with pytest.raises(ValueError):
+ G.add_edge(None, "anything")
+
+ def test_add_edges_from(self):
+ G = self.Graph()
+ G.add_edges_from([(0, 1), (0, 2, {"weight": 3})])
+ assert G.adj == {
+ 0: {1: {}, 2: {"weight": 3}},
+ 1: {0: {}},
+ 2: {0: {"weight": 3}},
+ }
+ G = self.Graph()
+ G.add_edges_from([(0, 1), (0, 2, {"weight": 3}), (1, 2, {"data": 4})], data=2)
+ assert G.adj == {
+ 0: {1: {"data": 2}, 2: {"weight": 3, "data": 2}},
+ 1: {0: {"data": 2}, 2: {"data": 4}},
+ 2: {0: {"weight": 3, "data": 2}, 1: {"data": 4}},
+ }
+
+ with pytest.raises(nx.NetworkXError):
+ G.add_edges_from([(0,)]) # too few in tuple
+ with pytest.raises(nx.NetworkXError):
+ G.add_edges_from([(0, 1, 2, 3)]) # too many in tuple
+ with pytest.raises(TypeError):
+ G.add_edges_from([0]) # not a tuple
+ with pytest.raises(ValueError):
+ G.add_edges_from([(None, 3), (3, 2)]) # None cannot be a node
+
+ def test_remove_edge(self):
+ G = self.K3.copy()
+ G.remove_edge(0, 1)
+ assert G.adj == {0: {2: {}}, 1: {2: {}}, 2: {0: {}, 1: {}}}
+ with pytest.raises(nx.NetworkXError):
+ G.remove_edge(-1, 0)
+
+ def test_remove_edges_from(self):
+ G = self.K3.copy()
+ G.remove_edges_from([(0, 1)])
+ assert G.adj == {0: {2: {}}, 1: {2: {}}, 2: {0: {}, 1: {}}}
+ G.remove_edges_from([(0, 0)]) # silent fail
+
+ def test_clear(self):
+ G = self.K3.copy()
+ G.graph["name"] = "K3"
+ G.clear()
+ assert list(G.nodes) == []
+ assert G.adj == {}
+ assert G.graph == {}
+
+ def test_clear_edges(self):
+ G = self.K3.copy()
+ G.graph["name"] = "K3"
+ nodes = list(G.nodes)
+ G.clear_edges()
+ assert list(G.nodes) == nodes
+ assert G.adj == {0: {}, 1: {}, 2: {}}
+ assert list(G.edges) == []
+ assert G.graph["name"] == "K3"
+
+ def test_edges_data(self):
+ G = self.K3
+ all_edges = [(0, 1, {}), (0, 2, {}), (1, 2, {})]
+ assert edges_equal(G.edges(data=True), all_edges)
+ assert edges_equal(G.edges(0, data=True), [(0, 1, {}), (0, 2, {})])
+ assert edges_equal(G.edges([0, 1], data=True), all_edges)
+ with pytest.raises(nx.NetworkXError):
+ G.edges(-1, True)
+
+ def test_get_edge_data(self):
+ G = self.K3.copy()
+ assert G.get_edge_data(0, 1) == {}
+ assert G[0][1] == {}
+ assert G.get_edge_data(10, 20) is None
+ assert G.get_edge_data(-1, 0) is None
+ assert G.get_edge_data(-1, 0, default=1) == 1
+
+ def test_update(self):
+ # specify both edges and nodes
+ G = self.K3.copy()
+ G.update(nodes=[3, (4, {"size": 2})], edges=[(4, 5), (6, 7, {"weight": 2})])
+ nlist = [
+ (0, {}),
+ (1, {}),
+ (2, {}),
+ (3, {}),
+ (4, {"size": 2}),
+ (5, {}),
+ (6, {}),
+ (7, {}),
+ ]
+ assert sorted(G.nodes.data()) == nlist
+ if G.is_directed():
+ elist = [
+ (0, 1, {}),
+ (0, 2, {}),
+ (1, 0, {}),
+ (1, 2, {}),
+ (2, 0, {}),
+ (2, 1, {}),
+ (4, 5, {}),
+ (6, 7, {"weight": 2}),
+ ]
+ else:
+ elist = [
+ (0, 1, {}),
+ (0, 2, {}),
+ (1, 2, {}),
+ (4, 5, {}),
+ (6, 7, {"weight": 2}),
+ ]
+ assert sorted(G.edges.data()) == elist
+ assert G.graph == {}
+
+ # no keywords -- order is edges, nodes
+ G = self.K3.copy()
+ G.update([(4, 5), (6, 7, {"weight": 2})], [3, (4, {"size": 2})])
+ assert sorted(G.nodes.data()) == nlist
+ assert sorted(G.edges.data()) == elist
+ assert G.graph == {}
+
+ # update using only a graph
+ G = self.Graph()
+ G.graph["foo"] = "bar"
+ G.add_node(2, data=4)
+ G.add_edge(0, 1, weight=0.5)
+ GG = G.copy()
+ H = self.Graph()
+ GG.update(H)
+ assert graphs_equal(G, GG)
+ H.update(G)
+ assert graphs_equal(H, G)
+
+ # update nodes only
+ H = self.Graph()
+ H.update(nodes=[3, 4])
+ assert H.nodes ^ {3, 4} == set()
+ assert H.size() == 0
+
+ # update edges only
+ H = self.Graph()
+ H.update(edges=[(3, 4)])
+ assert sorted(H.edges.data()) == [(3, 4, {})]
+ assert H.size() == 1
+
+ # No inputs -> exception
+ with pytest.raises(nx.NetworkXError):
+ nx.Graph().update()
+
+
+class TestEdgeSubgraph:
+ """Unit tests for the :meth:`Graph.edge_subgraph` method."""
+
+ def setup_method(self):
+ # Create a path graph on five nodes.
+ G = nx.path_graph(5)
+ # Add some node, edge, and graph attributes.
+ for i in range(5):
+ G.nodes[i]["name"] = f"node{i}"
+ G.edges[0, 1]["name"] = "edge01"
+ G.edges[3, 4]["name"] = "edge34"
+ G.graph["name"] = "graph"
+ # Get the subgraph induced by the first and last edges.
+ self.G = G
+ self.H = G.edge_subgraph([(0, 1), (3, 4)])
+
+ def test_correct_nodes(self):
+ """Tests that the subgraph has the correct nodes."""
+ assert [0, 1, 3, 4] == sorted(self.H.nodes())
+
+ def test_correct_edges(self):
+ """Tests that the subgraph has the correct edges."""
+ assert [(0, 1, "edge01"), (3, 4, "edge34")] == sorted(self.H.edges(data="name"))
+
+ def test_add_node(self):
+ """Tests that adding a node to the original graph does not
+ affect the nodes of the subgraph.
+
+ """
+ self.G.add_node(5)
+ assert [0, 1, 3, 4] == sorted(self.H.nodes())
+
+ def test_remove_node(self):
+ """Tests that removing a node in the original graph does
+ affect the nodes of the subgraph.
+
+ """
+ self.G.remove_node(0)
+ assert [1, 3, 4] == sorted(self.H.nodes())
+
+ def test_node_attr_dict(self):
+ """Tests that the node attribute dictionary of the two graphs is
+ the same object.
+
+ """
+ for v in self.H:
+ assert self.G.nodes[v] == self.H.nodes[v]
+ # Making a change to G should make a change in H and vice versa.
+ self.G.nodes[0]["name"] = "foo"
+ assert self.G.nodes[0] == self.H.nodes[0]
+ self.H.nodes[1]["name"] = "bar"
+ assert self.G.nodes[1] == self.H.nodes[1]
+
+ def test_edge_attr_dict(self):
+ """Tests that the edge attribute dictionary of the two graphs is
+ the same object.
+
+ """
+ for u, v in self.H.edges():
+ assert self.G.edges[u, v] == self.H.edges[u, v]
+ # Making a change to G should make a change in H and vice versa.
+ self.G.edges[0, 1]["name"] = "foo"
+ assert self.G.edges[0, 1]["name"] == self.H.edges[0, 1]["name"]
+ self.H.edges[3, 4]["name"] = "bar"
+ assert self.G.edges[3, 4]["name"] == self.H.edges[3, 4]["name"]
+
+ def test_graph_attr_dict(self):
+ """Tests that the graph attribute dictionary of the two graphs
+ is the same object.
+
+ """
+ assert self.G.graph is self.H.graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graph_historical.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graph_historical.py
new file mode 100644
index 00000000..36aba710
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graph_historical.py
@@ -0,0 +1,13 @@
+"""Original NetworkX graph tests"""
+
+import networkx
+import networkx as nx
+
+from .historical_tests import HistoricalTests
+
+
+class TestGraphHistorical(HistoricalTests):
+ @classmethod
+ def setup_class(cls):
+ HistoricalTests.setup_class()
+ cls.G = nx.Graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graphviews.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graphviews.py
new file mode 100644
index 00000000..591c760c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_graphviews.py
@@ -0,0 +1,350 @@
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal, nodes_equal
+
+# Note: SubGraph views are not tested here. They have their own testing file
+
+
+class TestReverseView:
+ def setup_method(self):
+ self.G = nx.path_graph(9, create_using=nx.DiGraph())
+ self.rv = nx.reverse_view(self.G)
+
+ def test_pickle(self):
+ import pickle
+
+ rv = self.rv
+ prv = pickle.loads(pickle.dumps(rv, -1))
+ assert rv._node == prv._node
+ assert rv._adj == prv._adj
+ assert rv.graph == prv.graph
+
+ def test_contains(self):
+ assert (2, 3) in self.G.edges
+ assert (3, 2) not in self.G.edges
+ assert (2, 3) not in self.rv.edges
+ assert (3, 2) in self.rv.edges
+
+ def test_iter(self):
+ expected = sorted(tuple(reversed(e)) for e in self.G.edges)
+ assert sorted(self.rv.edges) == expected
+
+ def test_exceptions(self):
+ G = nx.Graph()
+ pytest.raises(nx.NetworkXNotImplemented, nx.reverse_view, G)
+
+ def test_subclass(self):
+ class MyGraph(nx.DiGraph):
+ def my_method(self):
+ return "me"
+
+ def to_directed_class(self):
+ return MyGraph()
+
+ M = MyGraph()
+ M.add_edge(1, 2)
+ RM = nx.reverse_view(M)
+ print("RM class", RM.__class__)
+ RMC = RM.copy()
+ print("RMC class", RMC.__class__)
+ print(RMC.edges)
+ assert RMC.has_edge(2, 1)
+ assert RMC.my_method() == "me"
+
+
+class TestMultiReverseView:
+ def setup_method(self):
+ self.G = nx.path_graph(9, create_using=nx.MultiDiGraph())
+ self.G.add_edge(4, 5)
+ self.rv = nx.reverse_view(self.G)
+
+ def test_pickle(self):
+ import pickle
+
+ rv = self.rv
+ prv = pickle.loads(pickle.dumps(rv, -1))
+ assert rv._node == prv._node
+ assert rv._adj == prv._adj
+ assert rv.graph == prv.graph
+
+ def test_contains(self):
+ assert (2, 3, 0) in self.G.edges
+ assert (3, 2, 0) not in self.G.edges
+ assert (2, 3, 0) not in self.rv.edges
+ assert (3, 2, 0) in self.rv.edges
+ assert (5, 4, 1) in self.rv.edges
+ assert (4, 5, 1) not in self.rv.edges
+
+ def test_iter(self):
+ expected = sorted((v, u, k) for u, v, k in self.G.edges)
+ assert sorted(self.rv.edges) == expected
+
+ def test_exceptions(self):
+ MG = nx.MultiGraph(self.G)
+ pytest.raises(nx.NetworkXNotImplemented, nx.reverse_view, MG)
+
+
+def test_generic_multitype():
+ nxg = nx.graphviews
+ G = nx.DiGraph([(1, 2)])
+ with pytest.raises(nx.NetworkXError):
+ nxg.generic_graph_view(G, create_using=nx.MultiGraph)
+ G = nx.MultiDiGraph([(1, 2)])
+ with pytest.raises(nx.NetworkXError):
+ nxg.generic_graph_view(G, create_using=nx.DiGraph)
+
+
+class TestToDirected:
+ def setup_method(self):
+ self.G = nx.path_graph(9)
+ self.dv = nx.to_directed(self.G)
+ self.MG = nx.path_graph(9, create_using=nx.MultiGraph())
+ self.Mdv = nx.to_directed(self.MG)
+
+ def test_directed(self):
+ assert not self.G.is_directed()
+ assert self.dv.is_directed()
+
+ def test_already_directed(self):
+ dd = nx.to_directed(self.dv)
+ Mdd = nx.to_directed(self.Mdv)
+ assert edges_equal(dd.edges, self.dv.edges)
+ assert edges_equal(Mdd.edges, self.Mdv.edges)
+
+ def test_pickle(self):
+ import pickle
+
+ dv = self.dv
+ pdv = pickle.loads(pickle.dumps(dv, -1))
+ assert dv._node == pdv._node
+ assert dv._succ == pdv._succ
+ assert dv._pred == pdv._pred
+ assert dv.graph == pdv.graph
+
+ def test_contains(self):
+ assert (2, 3) in self.G.edges
+ assert (3, 2) in self.G.edges
+ assert (2, 3) in self.dv.edges
+ assert (3, 2) in self.dv.edges
+
+ def test_iter(self):
+ revd = [tuple(reversed(e)) for e in self.G.edges]
+ expected = sorted(list(self.G.edges) + revd)
+ assert sorted(self.dv.edges) == expected
+
+
+class TestToUndirected:
+ def setup_method(self):
+ self.DG = nx.path_graph(9, create_using=nx.DiGraph())
+ self.uv = nx.to_undirected(self.DG)
+ self.MDG = nx.path_graph(9, create_using=nx.MultiDiGraph())
+ self.Muv = nx.to_undirected(self.MDG)
+
+ def test_directed(self):
+ assert self.DG.is_directed()
+ assert not self.uv.is_directed()
+
+ def test_already_directed(self):
+ uu = nx.to_undirected(self.uv)
+ Muu = nx.to_undirected(self.Muv)
+ assert edges_equal(uu.edges, self.uv.edges)
+ assert edges_equal(Muu.edges, self.Muv.edges)
+
+ def test_pickle(self):
+ import pickle
+
+ uv = self.uv
+ puv = pickle.loads(pickle.dumps(uv, -1))
+ assert uv._node == puv._node
+ assert uv._adj == puv._adj
+ assert uv.graph == puv.graph
+ assert hasattr(uv, "_graph")
+
+ def test_contains(self):
+ assert (2, 3) in self.DG.edges
+ assert (3, 2) not in self.DG.edges
+ assert (2, 3) in self.uv.edges
+ assert (3, 2) in self.uv.edges
+
+ def test_iter(self):
+ expected = sorted(self.DG.edges)
+ assert sorted(self.uv.edges) == expected
+
+
+class TestChainsOfViews:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9)
+ cls.DG = nx.path_graph(9, create_using=nx.DiGraph())
+ cls.MG = nx.path_graph(9, create_using=nx.MultiGraph())
+ cls.MDG = nx.path_graph(9, create_using=nx.MultiDiGraph())
+ cls.Gv = nx.to_undirected(cls.DG)
+ cls.DGv = nx.to_directed(cls.G)
+ cls.MGv = nx.to_undirected(cls.MDG)
+ cls.MDGv = nx.to_directed(cls.MG)
+ cls.Rv = cls.DG.reverse()
+ cls.MRv = cls.MDG.reverse()
+ cls.graphs = [
+ cls.G,
+ cls.DG,
+ cls.MG,
+ cls.MDG,
+ cls.Gv,
+ cls.DGv,
+ cls.MGv,
+ cls.MDGv,
+ cls.Rv,
+ cls.MRv,
+ ]
+ for G in cls.graphs:
+ G.edges, G.nodes, G.degree
+
+ def test_pickle(self):
+ import pickle
+
+ for G in self.graphs:
+ H = pickle.loads(pickle.dumps(G, -1))
+ assert edges_equal(H.edges, G.edges)
+ assert nodes_equal(H.nodes, G.nodes)
+
+ def test_subgraph_of_subgraph(self):
+ SGv = nx.subgraph(self.G, range(3, 7))
+ SDGv = nx.subgraph(self.DG, range(3, 7))
+ SMGv = nx.subgraph(self.MG, range(3, 7))
+ SMDGv = nx.subgraph(self.MDG, range(3, 7))
+ for G in self.graphs + [SGv, SDGv, SMGv, SMDGv]:
+ SG = nx.induced_subgraph(G, [4, 5, 6])
+ assert list(SG) == [4, 5, 6]
+ SSG = SG.subgraph([6, 7])
+ assert list(SSG) == [6]
+ # subgraph-subgraph chain is short-cut in base class method
+ assert SSG._graph is G
+
+ def test_restricted_induced_subgraph_chains(self):
+ """Test subgraph chains that both restrict and show nodes/edges.
+
+ A restricted_view subgraph should allow induced subgraphs using
+ G.subgraph that automagically without a chain (meaning the result
+ is a subgraph view of the original graph not a subgraph-of-subgraph.
+ """
+ hide_nodes = [3, 4, 5]
+ hide_edges = [(6, 7)]
+ RG = nx.restricted_view(self.G, hide_nodes, hide_edges)
+ nodes = [4, 5, 6, 7, 8]
+ SG = nx.induced_subgraph(RG, nodes)
+ SSG = RG.subgraph(nodes)
+ assert RG._graph is self.G
+ assert SSG._graph is self.G
+ assert SG._graph is RG
+ assert edges_equal(SG.edges, SSG.edges)
+ # should be same as morphing the graph
+ CG = self.G.copy()
+ CG.remove_nodes_from(hide_nodes)
+ CG.remove_edges_from(hide_edges)
+ assert edges_equal(CG.edges(nodes), SSG.edges)
+ CG.remove_nodes_from([0, 1, 2, 3])
+ assert edges_equal(CG.edges, SSG.edges)
+ # switch order: subgraph first, then restricted view
+ SSSG = self.G.subgraph(nodes)
+ RSG = nx.restricted_view(SSSG, hide_nodes, hide_edges)
+ assert RSG._graph is not self.G
+ assert edges_equal(RSG.edges, CG.edges)
+
+ def test_subgraph_copy(self):
+ for origG in self.graphs:
+ G = nx.Graph(origG)
+ SG = G.subgraph([4, 5, 6])
+ H = SG.copy()
+ assert type(G) == type(H)
+
+ def test_subgraph_todirected(self):
+ SG = nx.induced_subgraph(self.G, [4, 5, 6])
+ SSG = SG.to_directed()
+ assert sorted(SSG) == [4, 5, 6]
+ assert sorted(SSG.edges) == [(4, 5), (5, 4), (5, 6), (6, 5)]
+
+ def test_subgraph_toundirected(self):
+ SG = nx.induced_subgraph(self.G, [4, 5, 6])
+ SSG = SG.to_undirected()
+ assert list(SSG) == [4, 5, 6]
+ assert sorted(SSG.edges) == [(4, 5), (5, 6)]
+
+ def test_reverse_subgraph_toundirected(self):
+ G = self.DG.reverse(copy=False)
+ SG = G.subgraph([4, 5, 6])
+ SSG = SG.to_undirected()
+ assert list(SSG) == [4, 5, 6]
+ assert sorted(SSG.edges) == [(4, 5), (5, 6)]
+
+ def test_reverse_reverse_copy(self):
+ G = self.DG.reverse(copy=False)
+ H = G.reverse(copy=True)
+ assert H.nodes == self.DG.nodes
+ assert H.edges == self.DG.edges
+ G = self.MDG.reverse(copy=False)
+ H = G.reverse(copy=True)
+ assert H.nodes == self.MDG.nodes
+ assert H.edges == self.MDG.edges
+
+ def test_subgraph_edgesubgraph_toundirected(self):
+ G = self.G.copy()
+ SG = G.subgraph([4, 5, 6])
+ SSG = SG.edge_subgraph([(4, 5), (5, 4)])
+ USSG = SSG.to_undirected()
+ assert list(USSG) == [4, 5]
+ assert sorted(USSG.edges) == [(4, 5)]
+
+ def test_copy_subgraph(self):
+ G = self.G.copy()
+ SG = G.subgraph([4, 5, 6])
+ CSG = SG.copy(as_view=True)
+ DCSG = SG.copy(as_view=False)
+ assert hasattr(CSG, "_graph") # is a view
+ assert not hasattr(DCSG, "_graph") # not a view
+
+ def test_copy_disubgraph(self):
+ G = self.DG.copy()
+ SG = G.subgraph([4, 5, 6])
+ CSG = SG.copy(as_view=True)
+ DCSG = SG.copy(as_view=False)
+ assert hasattr(CSG, "_graph") # is a view
+ assert not hasattr(DCSG, "_graph") # not a view
+
+ def test_copy_multidisubgraph(self):
+ G = self.MDG.copy()
+ SG = G.subgraph([4, 5, 6])
+ CSG = SG.copy(as_view=True)
+ DCSG = SG.copy(as_view=False)
+ assert hasattr(CSG, "_graph") # is a view
+ assert not hasattr(DCSG, "_graph") # not a view
+
+ def test_copy_multisubgraph(self):
+ G = self.MG.copy()
+ SG = G.subgraph([4, 5, 6])
+ CSG = SG.copy(as_view=True)
+ DCSG = SG.copy(as_view=False)
+ assert hasattr(CSG, "_graph") # is a view
+ assert not hasattr(DCSG, "_graph") # not a view
+
+ def test_copy_of_view(self):
+ G = nx.MultiGraph(self.MGv)
+ assert G.__class__.__name__ == "MultiGraph"
+ G = G.copy(as_view=True)
+ assert G.__class__.__name__ == "MultiGraph"
+
+ def test_subclass(self):
+ class MyGraph(nx.DiGraph):
+ def my_method(self):
+ return "me"
+
+ def to_directed_class(self):
+ return MyGraph()
+
+ for origG in self.graphs:
+ G = MyGraph(origG)
+ SG = G.subgraph([4, 5, 6])
+ H = SG.copy()
+ assert SG.my_method() == "me"
+ assert H.my_method() == "me"
+ assert 3 not in H or 3 in SG
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_multidigraph.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_multidigraph.py
new file mode 100644
index 00000000..fc0bd546
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_multidigraph.py
@@ -0,0 +1,459 @@
+from collections import UserDict
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal
+
+from .test_multigraph import BaseMultiGraphTester
+from .test_multigraph import TestEdgeSubgraph as _TestMultiGraphEdgeSubgraph
+from .test_multigraph import TestMultiGraph as _TestMultiGraph
+
+
+class BaseMultiDiGraphTester(BaseMultiGraphTester):
+ def test_edges(self):
+ G = self.K3
+ edges = [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
+ assert sorted(G.edges()) == edges
+ assert sorted(G.edges(0)) == [(0, 1), (0, 2)]
+ pytest.raises((KeyError, nx.NetworkXError), G.edges, -1)
+
+ def test_edges_data(self):
+ G = self.K3
+ edges = [(0, 1, {}), (0, 2, {}), (1, 0, {}), (1, 2, {}), (2, 0, {}), (2, 1, {})]
+ assert sorted(G.edges(data=True)) == edges
+ assert sorted(G.edges(0, data=True)) == [(0, 1, {}), (0, 2, {})]
+ pytest.raises((KeyError, nx.NetworkXError), G.neighbors, -1)
+
+ def test_edges_multi(self):
+ G = self.K3
+ assert sorted(G.edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
+ assert sorted(G.edges(0)) == [(0, 1), (0, 2)]
+ G.add_edge(0, 1)
+ assert sorted(G.edges()) == [
+ (0, 1),
+ (0, 1),
+ (0, 2),
+ (1, 0),
+ (1, 2),
+ (2, 0),
+ (2, 1),
+ ]
+
+ def test_out_edges(self):
+ G = self.K3
+ assert sorted(G.out_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
+ assert sorted(G.out_edges(0)) == [(0, 1), (0, 2)]
+ pytest.raises((KeyError, nx.NetworkXError), G.out_edges, -1)
+ assert sorted(G.out_edges(0, keys=True)) == [(0, 1, 0), (0, 2, 0)]
+
+ def test_out_edges_multi(self):
+ G = self.K3
+ assert sorted(G.out_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
+ assert sorted(G.out_edges(0)) == [(0, 1), (0, 2)]
+ G.add_edge(0, 1, 2)
+ assert sorted(G.out_edges()) == [
+ (0, 1),
+ (0, 1),
+ (0, 2),
+ (1, 0),
+ (1, 2),
+ (2, 0),
+ (2, 1),
+ ]
+
+ def test_out_edges_data(self):
+ G = self.K3
+ assert sorted(G.edges(0, data=True)) == [(0, 1, {}), (0, 2, {})]
+ G.remove_edge(0, 1)
+ G.add_edge(0, 1, data=1)
+ assert sorted(G.edges(0, data=True)) == [(0, 1, {"data": 1}), (0, 2, {})]
+ assert sorted(G.edges(0, data="data")) == [(0, 1, 1), (0, 2, None)]
+ assert sorted(G.edges(0, data="data", default=-1)) == [(0, 1, 1), (0, 2, -1)]
+
+ def test_in_edges(self):
+ G = self.K3
+ assert sorted(G.in_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
+ assert sorted(G.in_edges(0)) == [(1, 0), (2, 0)]
+ pytest.raises((KeyError, nx.NetworkXError), G.in_edges, -1)
+ G.add_edge(0, 1, 2)
+ assert sorted(G.in_edges()) == [
+ (0, 1),
+ (0, 1),
+ (0, 2),
+ (1, 0),
+ (1, 2),
+ (2, 0),
+ (2, 1),
+ ]
+ assert sorted(G.in_edges(0, keys=True)) == [(1, 0, 0), (2, 0, 0)]
+
+ def test_in_edges_no_keys(self):
+ G = self.K3
+ assert sorted(G.in_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
+ assert sorted(G.in_edges(0)) == [(1, 0), (2, 0)]
+ G.add_edge(0, 1, 2)
+ assert sorted(G.in_edges()) == [
+ (0, 1),
+ (0, 1),
+ (0, 2),
+ (1, 0),
+ (1, 2),
+ (2, 0),
+ (2, 1),
+ ]
+
+ assert sorted(G.in_edges(data=True, keys=False)) == [
+ (0, 1, {}),
+ (0, 1, {}),
+ (0, 2, {}),
+ (1, 0, {}),
+ (1, 2, {}),
+ (2, 0, {}),
+ (2, 1, {}),
+ ]
+
+ def test_in_edges_data(self):
+ G = self.K3
+ assert sorted(G.in_edges(0, data=True)) == [(1, 0, {}), (2, 0, {})]
+ G.remove_edge(1, 0)
+ G.add_edge(1, 0, data=1)
+ assert sorted(G.in_edges(0, data=True)) == [(1, 0, {"data": 1}), (2, 0, {})]
+ assert sorted(G.in_edges(0, data="data")) == [(1, 0, 1), (2, 0, None)]
+ assert sorted(G.in_edges(0, data="data", default=-1)) == [(1, 0, 1), (2, 0, -1)]
+
+ def is_shallow(self, H, G):
+ # graph
+ assert G.graph["foo"] == H.graph["foo"]
+ G.graph["foo"].append(1)
+ assert G.graph["foo"] == H.graph["foo"]
+ # node
+ assert G.nodes[0]["foo"] == H.nodes[0]["foo"]
+ G.nodes[0]["foo"].append(1)
+ assert G.nodes[0]["foo"] == H.nodes[0]["foo"]
+ # edge
+ assert G[1][2][0]["foo"] == H[1][2][0]["foo"]
+ G[1][2][0]["foo"].append(1)
+ assert G[1][2][0]["foo"] == H[1][2][0]["foo"]
+
+ def is_deep(self, H, G):
+ # graph
+ assert G.graph["foo"] == H.graph["foo"]
+ G.graph["foo"].append(1)
+ assert G.graph["foo"] != H.graph["foo"]
+ # node
+ assert G.nodes[0]["foo"] == H.nodes[0]["foo"]
+ G.nodes[0]["foo"].append(1)
+ assert G.nodes[0]["foo"] != H.nodes[0]["foo"]
+ # edge
+ assert G[1][2][0]["foo"] == H[1][2][0]["foo"]
+ G[1][2][0]["foo"].append(1)
+ assert G[1][2][0]["foo"] != H[1][2][0]["foo"]
+
+ def test_to_undirected(self):
+ # MultiDiGraph -> MultiGraph changes number of edges so it is
+ # not a copy operation... use is_shallow, not is_shallow_copy
+ G = self.K3
+ self.add_attributes(G)
+ H = nx.MultiGraph(G)
+ # self.is_shallow(H,G)
+ # the result is traversal order dependent so we
+ # can't use the is_shallow() test here.
+ try:
+ assert edges_equal(H.edges(), [(0, 1), (1, 2), (2, 0)])
+ except AssertionError:
+ assert edges_equal(H.edges(), [(0, 1), (1, 2), (1, 2), (2, 0)])
+ H = G.to_undirected()
+ self.is_deep(H, G)
+
+ def test_has_successor(self):
+ G = self.K3
+ assert G.has_successor(0, 1)
+ assert not G.has_successor(0, -1)
+
+ def test_successors(self):
+ G = self.K3
+ assert sorted(G.successors(0)) == [1, 2]
+ pytest.raises((KeyError, nx.NetworkXError), G.successors, -1)
+
+ def test_has_predecessor(self):
+ G = self.K3
+ assert G.has_predecessor(0, 1)
+ assert not G.has_predecessor(0, -1)
+
+ def test_predecessors(self):
+ G = self.K3
+ assert sorted(G.predecessors(0)) == [1, 2]
+ pytest.raises((KeyError, nx.NetworkXError), G.predecessors, -1)
+
+ def test_degree(self):
+ G = self.K3
+ assert sorted(G.degree()) == [(0, 4), (1, 4), (2, 4)]
+ assert dict(G.degree()) == {0: 4, 1: 4, 2: 4}
+ assert G.degree(0) == 4
+ assert list(G.degree(iter([0]))) == [(0, 4)]
+ G.add_edge(0, 1, weight=0.3, other=1.2)
+ assert sorted(G.degree(weight="weight")) == [(0, 4.3), (1, 4.3), (2, 4)]
+ assert sorted(G.degree(weight="other")) == [(0, 5.2), (1, 5.2), (2, 4)]
+
+ def test_in_degree(self):
+ G = self.K3
+ assert sorted(G.in_degree()) == [(0, 2), (1, 2), (2, 2)]
+ assert dict(G.in_degree()) == {0: 2, 1: 2, 2: 2}
+ assert G.in_degree(0) == 2
+ assert list(G.in_degree(iter([0]))) == [(0, 2)]
+ assert G.in_degree(0, weight="weight") == 2
+
+ def test_out_degree(self):
+ G = self.K3
+ assert sorted(G.out_degree()) == [(0, 2), (1, 2), (2, 2)]
+ assert dict(G.out_degree()) == {0: 2, 1: 2, 2: 2}
+ assert G.out_degree(0) == 2
+ assert list(G.out_degree(iter([0]))) == [(0, 2)]
+ assert G.out_degree(0, weight="weight") == 2
+
+ def test_size(self):
+ G = self.K3
+ assert G.size() == 6
+ assert G.number_of_edges() == 6
+ G.add_edge(0, 1, weight=0.3, other=1.2)
+ assert round(G.size(weight="weight"), 2) == 6.3
+ assert round(G.size(weight="other"), 2) == 7.2
+
+ def test_to_undirected_reciprocal(self):
+ G = self.Graph()
+ G.add_edge(1, 2)
+ assert G.to_undirected().has_edge(1, 2)
+ assert not G.to_undirected(reciprocal=True).has_edge(1, 2)
+ G.add_edge(2, 1)
+ assert G.to_undirected(reciprocal=True).has_edge(1, 2)
+
+ def test_reverse_copy(self):
+ G = nx.MultiDiGraph([(0, 1), (0, 1)])
+ R = G.reverse()
+ assert sorted(R.edges()) == [(1, 0), (1, 0)]
+ R.remove_edge(1, 0)
+ assert sorted(R.edges()) == [(1, 0)]
+ assert sorted(G.edges()) == [(0, 1), (0, 1)]
+
+ def test_reverse_nocopy(self):
+ G = nx.MultiDiGraph([(0, 1), (0, 1)])
+ R = G.reverse(copy=False)
+ assert sorted(R.edges()) == [(1, 0), (1, 0)]
+ pytest.raises(nx.NetworkXError, R.remove_edge, 1, 0)
+
+ def test_di_attributes_cached(self):
+ G = self.K3.copy()
+ assert id(G.in_edges) == id(G.in_edges)
+ assert id(G.out_edges) == id(G.out_edges)
+ assert id(G.in_degree) == id(G.in_degree)
+ assert id(G.out_degree) == id(G.out_degree)
+ assert id(G.succ) == id(G.succ)
+ assert id(G.pred) == id(G.pred)
+
+
+class TestMultiDiGraph(BaseMultiDiGraphTester, _TestMultiGraph):
+ def setup_method(self):
+ self.Graph = nx.MultiDiGraph
+ # build K3
+ self.k3edges = [(0, 1), (0, 2), (1, 2)]
+ self.k3nodes = [0, 1, 2]
+ self.K3 = self.Graph()
+ self.K3._succ = {0: {}, 1: {}, 2: {}}
+ # K3._adj is synced with K3._succ
+ self.K3._pred = {0: {}, 1: {}, 2: {}}
+ for u in self.k3nodes:
+ for v in self.k3nodes:
+ if u == v:
+ continue
+ d = {0: {}}
+ self.K3._succ[u][v] = d
+ self.K3._pred[v][u] = d
+ self.K3._node = {}
+ self.K3._node[0] = {}
+ self.K3._node[1] = {}
+ self.K3._node[2] = {}
+
+ def test_add_edge(self):
+ G = self.Graph()
+ G.add_edge(0, 1)
+ assert G._adj == {0: {1: {0: {}}}, 1: {}}
+ assert G._succ == {0: {1: {0: {}}}, 1: {}}
+ assert G._pred == {0: {}, 1: {0: {0: {}}}}
+ G = self.Graph()
+ G.add_edge(*(0, 1))
+ assert G._adj == {0: {1: {0: {}}}, 1: {}}
+ assert G._succ == {0: {1: {0: {}}}, 1: {}}
+ assert G._pred == {0: {}, 1: {0: {0: {}}}}
+ with pytest.raises(ValueError, match="None cannot be a node"):
+ G.add_edge(None, 3)
+
+ def test_add_edges_from(self):
+ G = self.Graph()
+ G.add_edges_from([(0, 1), (0, 1, {"weight": 3})])
+ assert G._adj == {0: {1: {0: {}, 1: {"weight": 3}}}, 1: {}}
+ assert G._succ == {0: {1: {0: {}, 1: {"weight": 3}}}, 1: {}}
+ assert G._pred == {0: {}, 1: {0: {0: {}, 1: {"weight": 3}}}}
+
+ G.add_edges_from([(0, 1), (0, 1, {"weight": 3})], weight=2)
+ assert G._succ == {
+ 0: {1: {0: {}, 1: {"weight": 3}, 2: {"weight": 2}, 3: {"weight": 3}}},
+ 1: {},
+ }
+ assert G._pred == {
+ 0: {},
+ 1: {0: {0: {}, 1: {"weight": 3}, 2: {"weight": 2}, 3: {"weight": 3}}},
+ }
+
+ G = self.Graph()
+ edges = [
+ (0, 1, {"weight": 3}),
+ (0, 1, (("weight", 2),)),
+ (0, 1, 5),
+ (0, 1, "s"),
+ ]
+ G.add_edges_from(edges)
+ keydict = {0: {"weight": 3}, 1: {"weight": 2}, 5: {}, "s": {}}
+ assert G._succ == {0: {1: keydict}, 1: {}}
+ assert G._pred == {1: {0: keydict}, 0: {}}
+
+ # too few in tuple
+ pytest.raises(nx.NetworkXError, G.add_edges_from, [(0,)])
+ # too many in tuple
+ pytest.raises(nx.NetworkXError, G.add_edges_from, [(0, 1, 2, 3, 4)])
+ # not a tuple
+ pytest.raises(TypeError, G.add_edges_from, [0])
+ with pytest.raises(ValueError, match="None cannot be a node"):
+ G.add_edges_from([(None, 3), (3, 2)])
+
+ def test_remove_edge(self):
+ G = self.K3
+ G.remove_edge(0, 1)
+ assert G._succ == {
+ 0: {2: {0: {}}},
+ 1: {0: {0: {}}, 2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+ assert G._pred == {
+ 0: {1: {0: {}}, 2: {0: {}}},
+ 1: {2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+ pytest.raises((KeyError, nx.NetworkXError), G.remove_edge, -1, 0)
+ pytest.raises((KeyError, nx.NetworkXError), G.remove_edge, 0, 2, key=1)
+
+ def test_remove_multiedge(self):
+ G = self.K3
+ G.add_edge(0, 1, key="parallel edge")
+ G.remove_edge(0, 1, key="parallel edge")
+ assert G._adj == {
+ 0: {1: {0: {}}, 2: {0: {}}},
+ 1: {0: {0: {}}, 2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+
+ assert G._succ == {
+ 0: {1: {0: {}}, 2: {0: {}}},
+ 1: {0: {0: {}}, 2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+
+ assert G._pred == {
+ 0: {1: {0: {}}, 2: {0: {}}},
+ 1: {0: {0: {}}, 2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+ G.remove_edge(0, 1)
+ assert G._succ == {
+ 0: {2: {0: {}}},
+ 1: {0: {0: {}}, 2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+ assert G._pred == {
+ 0: {1: {0: {}}, 2: {0: {}}},
+ 1: {2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+ pytest.raises((KeyError, nx.NetworkXError), G.remove_edge, -1, 0)
+
+ def test_remove_edges_from(self):
+ G = self.K3
+ G.remove_edges_from([(0, 1)])
+ assert G._succ == {
+ 0: {2: {0: {}}},
+ 1: {0: {0: {}}, 2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+ assert G._pred == {
+ 0: {1: {0: {}}, 2: {0: {}}},
+ 1: {2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+ G.remove_edges_from([(0, 0)]) # silent fail
+
+
+class TestEdgeSubgraph(_TestMultiGraphEdgeSubgraph):
+ """Unit tests for the :meth:`MultiDiGraph.edge_subgraph` method."""
+
+ def setup_method(self):
+ # Create a quadruply-linked path graph on five nodes.
+ G = nx.MultiDiGraph()
+ nx.add_path(G, range(5))
+ nx.add_path(G, range(5))
+ nx.add_path(G, reversed(range(5)))
+ nx.add_path(G, reversed(range(5)))
+ # Add some node, edge, and graph attributes.
+ for i in range(5):
+ G.nodes[i]["name"] = f"node{i}"
+ G.adj[0][1][0]["name"] = "edge010"
+ G.adj[0][1][1]["name"] = "edge011"
+ G.adj[3][4][0]["name"] = "edge340"
+ G.adj[3][4][1]["name"] = "edge341"
+ G.graph["name"] = "graph"
+ # Get the subgraph induced by one of the first edges and one of
+ # the last edges.
+ self.G = G
+ self.H = G.edge_subgraph([(0, 1, 0), (3, 4, 1)])
+
+
+class CustomDictClass(UserDict):
+ pass
+
+
+class MultiDiGraphSubClass(nx.MultiDiGraph):
+ node_dict_factory = CustomDictClass # type: ignore[assignment]
+ node_attr_dict_factory = CustomDictClass # type: ignore[assignment]
+ adjlist_outer_dict_factory = CustomDictClass # type: ignore[assignment]
+ adjlist_inner_dict_factory = CustomDictClass # type: ignore[assignment]
+ edge_key_dict_factory = CustomDictClass # type: ignore[assignment]
+ edge_attr_dict_factory = CustomDictClass # type: ignore[assignment]
+ graph_attr_dict_factory = CustomDictClass # type: ignore[assignment]
+
+
+class TestMultiDiGraphSubclass(TestMultiDiGraph):
+ def setup_method(self):
+ self.Graph = MultiDiGraphSubClass
+ # build K3
+ self.k3edges = [(0, 1), (0, 2), (1, 2)]
+ self.k3nodes = [0, 1, 2]
+ self.K3 = self.Graph()
+ self.K3._succ = self.K3.adjlist_outer_dict_factory(
+ {
+ 0: self.K3.adjlist_inner_dict_factory(),
+ 1: self.K3.adjlist_inner_dict_factory(),
+ 2: self.K3.adjlist_inner_dict_factory(),
+ }
+ )
+ # K3._adj is synced with K3._succ
+ self.K3._pred = {0: {}, 1: {}, 2: {}}
+ for u in self.k3nodes:
+ for v in self.k3nodes:
+ if u == v:
+ continue
+ d = {0: {}}
+ self.K3._succ[u][v] = d
+ self.K3._pred[v][u] = d
+ self.K3._node = self.K3.node_dict_factory()
+ self.K3._node[0] = self.K3.node_attr_dict_factory()
+ self.K3._node[1] = self.K3.node_attr_dict_factory()
+ self.K3._node[2] = self.K3.node_attr_dict_factory()
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_multigraph.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_multigraph.py
new file mode 100644
index 00000000..cd912d1d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_multigraph.py
@@ -0,0 +1,528 @@
+from collections import UserDict
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal
+
+from .test_graph import BaseAttrGraphTester
+from .test_graph import TestGraph as _TestGraph
+
+
+class BaseMultiGraphTester(BaseAttrGraphTester):
+ def test_has_edge(self):
+ G = self.K3
+ assert G.has_edge(0, 1)
+ assert not G.has_edge(0, -1)
+ assert G.has_edge(0, 1, 0)
+ assert not G.has_edge(0, 1, 1)
+
+ def test_get_edge_data(self):
+ G = self.K3
+ assert G.get_edge_data(0, 1) == {0: {}}
+ assert G[0][1] == {0: {}}
+ assert G[0][1][0] == {}
+ assert G.get_edge_data(10, 20) is None
+ assert G.get_edge_data(0, 1, 0) == {}
+
+ def test_adjacency(self):
+ G = self.K3
+ assert dict(G.adjacency()) == {
+ 0: {1: {0: {}}, 2: {0: {}}},
+ 1: {0: {0: {}}, 2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+
+ def deepcopy_edge_attr(self, H, G):
+ assert G[1][2][0]["foo"] == H[1][2][0]["foo"]
+ G[1][2][0]["foo"].append(1)
+ assert G[1][2][0]["foo"] != H[1][2][0]["foo"]
+
+ def shallow_copy_edge_attr(self, H, G):
+ assert G[1][2][0]["foo"] == H[1][2][0]["foo"]
+ G[1][2][0]["foo"].append(1)
+ assert G[1][2][0]["foo"] == H[1][2][0]["foo"]
+
+ def graphs_equal(self, H, G):
+ assert G._adj == H._adj
+ assert G._node == H._node
+ assert G.graph == H.graph
+ assert G.name == H.name
+ if not G.is_directed() and not H.is_directed():
+ assert H._adj[1][2][0] is H._adj[2][1][0]
+ assert G._adj[1][2][0] is G._adj[2][1][0]
+ else: # at least one is directed
+ if not G.is_directed():
+ G._pred = G._adj
+ G._succ = G._adj
+ if not H.is_directed():
+ H._pred = H._adj
+ H._succ = H._adj
+ assert G._pred == H._pred
+ assert G._succ == H._succ
+ assert H._succ[1][2][0] is H._pred[2][1][0]
+ assert G._succ[1][2][0] is G._pred[2][1][0]
+
+ def same_attrdict(self, H, G):
+ # same attrdict in the edgedata
+ old_foo = H[1][2][0]["foo"]
+ H.adj[1][2][0]["foo"] = "baz"
+ assert G._adj == H._adj
+ H.adj[1][2][0]["foo"] = old_foo
+ assert G._adj == H._adj
+
+ old_foo = H.nodes[0]["foo"]
+ H.nodes[0]["foo"] = "baz"
+ assert G._node == H._node
+ H.nodes[0]["foo"] = old_foo
+ assert G._node == H._node
+
+ def different_attrdict(self, H, G):
+ # used by graph_equal_but_different
+ old_foo = H[1][2][0]["foo"]
+ H.adj[1][2][0]["foo"] = "baz"
+ assert G._adj != H._adj
+ H.adj[1][2][0]["foo"] = old_foo
+ assert G._adj == H._adj
+
+ old_foo = H.nodes[0]["foo"]
+ H.nodes[0]["foo"] = "baz"
+ assert G._node != H._node
+ H.nodes[0]["foo"] = old_foo
+ assert G._node == H._node
+
+ def test_to_undirected(self):
+ G = self.K3
+ self.add_attributes(G)
+ H = nx.MultiGraph(G)
+ self.is_shallow_copy(H, G)
+ H = G.to_undirected()
+ self.is_deepcopy(H, G)
+
+ def test_to_directed(self):
+ G = self.K3
+ self.add_attributes(G)
+ H = nx.MultiDiGraph(G)
+ self.is_shallow_copy(H, G)
+ H = G.to_directed()
+ self.is_deepcopy(H, G)
+
+ def test_number_of_edges_selfloops(self):
+ G = self.K3
+ G.add_edge(0, 0)
+ G.add_edge(0, 0)
+ G.add_edge(0, 0, key="parallel edge")
+ G.remove_edge(0, 0, key="parallel edge")
+ assert G.number_of_edges(0, 0) == 2
+ G.remove_edge(0, 0)
+ assert G.number_of_edges(0, 0) == 1
+
+ def test_edge_lookup(self):
+ G = self.Graph()
+ G.add_edge(1, 2, foo="bar")
+ G.add_edge(1, 2, "key", foo="biz")
+ assert edges_equal(G.edges[1, 2, 0], {"foo": "bar"})
+ assert edges_equal(G.edges[1, 2, "key"], {"foo": "biz"})
+
+ def test_edge_attr(self):
+ G = self.Graph()
+ G.add_edge(1, 2, key="k1", foo="bar")
+ G.add_edge(1, 2, key="k2", foo="baz")
+ assert isinstance(G.get_edge_data(1, 2), G.edge_key_dict_factory)
+ assert all(
+ isinstance(d, G.edge_attr_dict_factory) for u, v, d in G.edges(data=True)
+ )
+ assert edges_equal(
+ G.edges(keys=True, data=True),
+ [(1, 2, "k1", {"foo": "bar"}), (1, 2, "k2", {"foo": "baz"})],
+ )
+ assert edges_equal(
+ G.edges(keys=True, data="foo"), [(1, 2, "k1", "bar"), (1, 2, "k2", "baz")]
+ )
+
+ def test_edge_attr4(self):
+ G = self.Graph()
+ G.add_edge(1, 2, key=0, data=7, spam="bar", bar="foo")
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"data": 7, "spam": "bar", "bar": "foo"})]
+ )
+ G[1][2][0]["data"] = 10 # OK to set data like this
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"data": 10, "spam": "bar", "bar": "foo"})]
+ )
+
+ G.adj[1][2][0]["data"] = 20
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"data": 20, "spam": "bar", "bar": "foo"})]
+ )
+ G.edges[1, 2, 0]["data"] = 21 # another spelling, "edge"
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"data": 21, "spam": "bar", "bar": "foo"})]
+ )
+ G.adj[1][2][0]["listdata"] = [20, 200]
+ G.adj[1][2][0]["weight"] = 20
+ assert edges_equal(
+ G.edges(data=True),
+ [
+ (
+ 1,
+ 2,
+ {
+ "data": 21,
+ "spam": "bar",
+ "bar": "foo",
+ "listdata": [20, 200],
+ "weight": 20,
+ },
+ )
+ ],
+ )
+
+
+class TestMultiGraph(BaseMultiGraphTester, _TestGraph):
+ def setup_method(self):
+ self.Graph = nx.MultiGraph
+ # build K3
+ ed1, ed2, ed3 = ({0: {}}, {0: {}}, {0: {}})
+ self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed1, 2: ed3}, 2: {0: ed2, 1: ed3}}
+ self.k3edges = [(0, 1), (0, 2), (1, 2)]
+ self.k3nodes = [0, 1, 2]
+ self.K3 = self.Graph()
+ self.K3._adj = self.k3adj
+ self.K3._node = {}
+ self.K3._node[0] = {}
+ self.K3._node[1] = {}
+ self.K3._node[2] = {}
+
+ def test_data_input(self):
+ G = self.Graph({1: [2], 2: [1]}, name="test")
+ assert G.name == "test"
+ 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 = {"w": 200, "s": "foo"}
+ edata1 = {"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, {})] # type: ignore[var-annotated]
+ 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: {}}}
+ with pytest.raises(KeyError):
+ G.__getitem__("j")
+ with pytest.raises(TypeError):
+ G.__getitem__(["A"])
+
+ def test_remove_node(self):
+ G = self.K3
+ G.remove_node(0)
+ assert G.adj == {1: {2: {0: {}}}, 2: {1: {0: {}}}}
+ with pytest.raises(nx.NetworkXError):
+ G.remove_node(-1)
+
+ def test_add_edge(self):
+ G = self.Graph()
+ G.add_edge(0, 1)
+ assert G.adj == {0: {1: {0: {}}}, 1: {0: {0: {}}}}
+ G = self.Graph()
+ G.add_edge(*(0, 1))
+ assert G.adj == {0: {1: {0: {}}}, 1: {0: {0: {}}}}
+ G = self.Graph()
+ with pytest.raises(ValueError):
+ G.add_edge(None, "anything")
+
+ def test_add_edge_conflicting_key(self):
+ G = self.Graph()
+ G.add_edge(0, 1, key=1)
+ G.add_edge(0, 1)
+ assert G.number_of_edges() == 2
+ G = self.Graph()
+ G.add_edges_from([(0, 1, 1, {})])
+ G.add_edges_from([(0, 1)])
+ assert G.number_of_edges() == 2
+
+ def test_add_edges_from(self):
+ G = self.Graph()
+ G.add_edges_from([(0, 1), (0, 1, {"weight": 3})])
+ assert G.adj == {
+ 0: {1: {0: {}, 1: {"weight": 3}}},
+ 1: {0: {0: {}, 1: {"weight": 3}}},
+ }
+ G.add_edges_from([(0, 1), (0, 1, {"weight": 3})], weight=2)
+ assert G.adj == {
+ 0: {1: {0: {}, 1: {"weight": 3}, 2: {"weight": 2}, 3: {"weight": 3}}},
+ 1: {0: {0: {}, 1: {"weight": 3}, 2: {"weight": 2}, 3: {"weight": 3}}},
+ }
+ G = self.Graph()
+ edges = [
+ (0, 1, {"weight": 3}),
+ (0, 1, (("weight", 2),)),
+ (0, 1, 5),
+ (0, 1, "s"),
+ ]
+ G.add_edges_from(edges)
+ keydict = {0: {"weight": 3}, 1: {"weight": 2}, 5: {}, "s": {}}
+ assert G._adj == {0: {1: keydict}, 1: {0: keydict}}
+
+ # too few in tuple
+ with pytest.raises(nx.NetworkXError):
+ G.add_edges_from([(0,)])
+ # too many in tuple
+ with pytest.raises(nx.NetworkXError):
+ G.add_edges_from([(0, 1, 2, 3, 4)])
+ # not a tuple
+ with pytest.raises(TypeError):
+ G.add_edges_from([0])
+
+ def test_multigraph_add_edges_from_four_tuple_misordered(self):
+ """add_edges_from expects 4-tuples of the format (u, v, key, data_dict).
+
+ Ensure 4-tuples of form (u, v, data_dict, key) raise exception.
+ """
+ G = nx.MultiGraph()
+ with pytest.raises(TypeError):
+ # key/data values flipped in 4-tuple
+ G.add_edges_from([(0, 1, {"color": "red"}, 0)])
+
+ def test_remove_edge(self):
+ G = self.K3
+ G.remove_edge(0, 1)
+ assert G.adj == {0: {2: {0: {}}}, 1: {2: {0: {}}}, 2: {0: {0: {}}, 1: {0: {}}}}
+
+ with pytest.raises(nx.NetworkXError):
+ G.remove_edge(-1, 0)
+ with pytest.raises(nx.NetworkXError):
+ G.remove_edge(0, 2, key=1)
+
+ def test_remove_edges_from(self):
+ G = self.K3.copy()
+ G.remove_edges_from([(0, 1)])
+ kd = {0: {}}
+ assert G.adj == {0: {2: kd}, 1: {2: kd}, 2: {0: kd, 1: kd}}
+ G.remove_edges_from([(0, 0)]) # silent fail
+ self.K3.add_edge(0, 1)
+ G = self.K3.copy()
+ G.remove_edges_from(list(G.edges(data=True, keys=True)))
+ assert G.adj == {0: {}, 1: {}, 2: {}}
+ G = self.K3.copy()
+ G.remove_edges_from(list(G.edges(data=False, keys=True)))
+ assert G.adj == {0: {}, 1: {}, 2: {}}
+ G = self.K3.copy()
+ G.remove_edges_from(list(G.edges(data=False, keys=False)))
+ assert G.adj == {0: {}, 1: {}, 2: {}}
+ G = self.K3.copy()
+ G.remove_edges_from([(0, 1, 0), (0, 2, 0, {}), (1, 2)])
+ assert G.adj == {0: {1: {1: {}}}, 1: {0: {1: {}}}, 2: {}}
+
+ def test_remove_multiedge(self):
+ G = self.K3
+ G.add_edge(0, 1, key="parallel edge")
+ G.remove_edge(0, 1, key="parallel edge")
+ assert G.adj == {
+ 0: {1: {0: {}}, 2: {0: {}}},
+ 1: {0: {0: {}}, 2: {0: {}}},
+ 2: {0: {0: {}}, 1: {0: {}}},
+ }
+ G.remove_edge(0, 1)
+ kd = {0: {}}
+ assert G.adj == {0: {2: kd}, 1: {2: kd}, 2: {0: kd, 1: kd}}
+ with pytest.raises(nx.NetworkXError):
+ G.remove_edge(-1, 0)
+
+
+class TestEdgeSubgraph:
+ """Unit tests for the :meth:`MultiGraph.edge_subgraph` method."""
+
+ def setup_method(self):
+ # Create a doubly-linked path graph on five nodes.
+ G = nx.MultiGraph()
+ nx.add_path(G, range(5))
+ nx.add_path(G, range(5))
+ # Add some node, edge, and graph attributes.
+ for i in range(5):
+ G.nodes[i]["name"] = f"node{i}"
+ G.adj[0][1][0]["name"] = "edge010"
+ G.adj[0][1][1]["name"] = "edge011"
+ G.adj[3][4][0]["name"] = "edge340"
+ G.adj[3][4][1]["name"] = "edge341"
+ G.graph["name"] = "graph"
+ # Get the subgraph induced by one of the first edges and one of
+ # the last edges.
+ self.G = G
+ self.H = G.edge_subgraph([(0, 1, 0), (3, 4, 1)])
+
+ def test_correct_nodes(self):
+ """Tests that the subgraph has the correct nodes."""
+ assert [0, 1, 3, 4] == sorted(self.H.nodes())
+
+ def test_correct_edges(self):
+ """Tests that the subgraph has the correct edges."""
+ assert [(0, 1, 0, "edge010"), (3, 4, 1, "edge341")] == sorted(
+ self.H.edges(keys=True, data="name")
+ )
+
+ def test_add_node(self):
+ """Tests that adding a node to the original graph does not
+ affect the nodes of the subgraph.
+
+ """
+ self.G.add_node(5)
+ assert [0, 1, 3, 4] == sorted(self.H.nodes())
+
+ def test_remove_node(self):
+ """Tests that removing a node in the original graph does
+ affect the nodes of the subgraph.
+
+ """
+ self.G.remove_node(0)
+ assert [1, 3, 4] == sorted(self.H.nodes())
+
+ def test_node_attr_dict(self):
+ """Tests that the node attribute dictionary of the two graphs is
+ the same object.
+
+ """
+ for v in self.H:
+ assert self.G.nodes[v] == self.H.nodes[v]
+ # Making a change to G should make a change in H and vice versa.
+ self.G.nodes[0]["name"] = "foo"
+ assert self.G.nodes[0] == self.H.nodes[0]
+ self.H.nodes[1]["name"] = "bar"
+ assert self.G.nodes[1] == self.H.nodes[1]
+
+ def test_edge_attr_dict(self):
+ """Tests that the edge attribute dictionary of the two graphs is
+ the same object.
+
+ """
+ for u, v, k in self.H.edges(keys=True):
+ assert self.G._adj[u][v][k] == self.H._adj[u][v][k]
+ # Making a change to G should make a change in H and vice versa.
+ self.G._adj[0][1][0]["name"] = "foo"
+ assert self.G._adj[0][1][0]["name"] == self.H._adj[0][1][0]["name"]
+ self.H._adj[3][4][1]["name"] = "bar"
+ assert self.G._adj[3][4][1]["name"] == self.H._adj[3][4][1]["name"]
+
+ def test_graph_attr_dict(self):
+ """Tests that the graph attribute dictionary of the two graphs
+ is the same object.
+
+ """
+ assert self.G.graph is self.H.graph
+
+
+class CustomDictClass(UserDict):
+ pass
+
+
+class MultiGraphSubClass(nx.MultiGraph):
+ node_dict_factory = CustomDictClass # type: ignore[assignment]
+ node_attr_dict_factory = CustomDictClass # type: ignore[assignment]
+ adjlist_outer_dict_factory = CustomDictClass # type: ignore[assignment]
+ adjlist_inner_dict_factory = CustomDictClass # type: ignore[assignment]
+ edge_key_dict_factory = CustomDictClass # type: ignore[assignment]
+ edge_attr_dict_factory = CustomDictClass # type: ignore[assignment]
+ graph_attr_dict_factory = CustomDictClass # type: ignore[assignment]
+
+
+class TestMultiGraphSubclass(TestMultiGraph):
+ def setup_method(self):
+ self.Graph = MultiGraphSubClass
+ # build K3
+ self.k3edges = [(0, 1), (0, 2), (1, 2)]
+ self.k3nodes = [0, 1, 2]
+ self.K3 = self.Graph()
+ self.K3._adj = self.K3.adjlist_outer_dict_factory(
+ {
+ 0: self.K3.adjlist_inner_dict_factory(),
+ 1: self.K3.adjlist_inner_dict_factory(),
+ 2: self.K3.adjlist_inner_dict_factory(),
+ }
+ )
+ self.K3._pred = {0: {}, 1: {}, 2: {}}
+ for u in self.k3nodes:
+ for v in self.k3nodes:
+ if u != v:
+ d = {0: {}}
+ self.K3._adj[u][v] = d
+ self.K3._adj[v][u] = d
+ self.K3._node = self.K3.node_dict_factory()
+ self.K3._node[0] = self.K3.node_attr_dict_factory()
+ self.K3._node[1] = self.K3.node_attr_dict_factory()
+ self.K3._node[2] = self.K3.node_attr_dict_factory()
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_reportviews.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_reportviews.py
new file mode 100644
index 00000000..789c829f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_reportviews.py
@@ -0,0 +1,1435 @@
+import pickle
+from copy import deepcopy
+
+import pytest
+
+import networkx as nx
+from networkx.classes import reportviews as rv
+from networkx.classes.reportviews import NodeDataView
+
+
+# Nodes
+class TestNodeView:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9)
+ cls.nv = cls.G.nodes # NodeView(G)
+
+ def test_pickle(self):
+ import pickle
+
+ nv = self.nv
+ pnv = pickle.loads(pickle.dumps(nv, -1))
+ assert nv == pnv
+ assert nv.__slots__ == pnv.__slots__
+
+ def test_str(self):
+ assert str(self.nv) == "[0, 1, 2, 3, 4, 5, 6, 7, 8]"
+
+ def test_repr(self):
+ assert repr(self.nv) == "NodeView((0, 1, 2, 3, 4, 5, 6, 7, 8))"
+
+ def test_contains(self):
+ G = self.G.copy()
+ nv = G.nodes
+ assert 7 in nv
+ assert 9 not in nv
+ G.remove_node(7)
+ G.add_node(9)
+ assert 7 not in nv
+ assert 9 in nv
+
+ def test_getitem(self):
+ G = self.G.copy()
+ nv = G.nodes
+ G.nodes[3]["foo"] = "bar"
+ assert nv[7] == {}
+ assert nv[3] == {"foo": "bar"}
+ # slicing
+ with pytest.raises(nx.NetworkXError):
+ G.nodes[0:5]
+
+ def test_iter(self):
+ nv = self.nv
+ for i, n in enumerate(nv):
+ assert i == n
+ inv = iter(nv)
+ assert next(inv) == 0
+ assert iter(nv) != nv
+ assert iter(inv) == inv
+ inv2 = iter(nv)
+ next(inv2)
+ assert list(inv) == list(inv2)
+ # odd case where NodeView calls NodeDataView with data=False
+ nnv = nv(data=False)
+ for i, n in enumerate(nnv):
+ assert i == n
+
+ def test_call(self):
+ nodes = self.nv
+ assert nodes is nodes()
+ assert nodes is not nodes(data=True)
+ assert nodes is not nodes(data="weight")
+
+
+class TestNodeDataView:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9)
+ cls.nv = NodeDataView(cls.G)
+ cls.ndv = cls.G.nodes.data(True)
+ cls.nwv = cls.G.nodes.data("foo")
+
+ def test_viewtype(self):
+ nv = self.G.nodes
+ ndvfalse = nv.data(False)
+ assert nv is ndvfalse
+ assert nv is not self.ndv
+
+ def test_pickle(self):
+ import pickle
+
+ nv = self.nv
+ pnv = pickle.loads(pickle.dumps(nv, -1))
+ assert nv == pnv
+ assert nv.__slots__ == pnv.__slots__
+
+ def test_str(self):
+ msg = str([(n, {}) for n in range(9)])
+ assert str(self.ndv) == msg
+
+ def test_repr(self):
+ expected = "NodeDataView((0, 1, 2, 3, 4, 5, 6, 7, 8))"
+ assert repr(self.nv) == expected
+ expected = (
+ "NodeDataView({0: {}, 1: {}, 2: {}, 3: {}, "
+ + "4: {}, 5: {}, 6: {}, 7: {}, 8: {}})"
+ )
+ assert repr(self.ndv) == expected
+ expected = (
+ "NodeDataView({0: None, 1: None, 2: None, 3: None, 4: None, "
+ + "5: None, 6: None, 7: None, 8: None}, data='foo')"
+ )
+ assert repr(self.nwv) == expected
+
+ def test_contains(self):
+ G = self.G.copy()
+ nv = G.nodes.data()
+ nwv = G.nodes.data("foo")
+ G.nodes[3]["foo"] = "bar"
+ assert (7, {}) in nv
+ assert (3, {"foo": "bar"}) in nv
+ assert (3, "bar") in nwv
+ assert (7, None) in nwv
+ # default
+ nwv_def = G.nodes(data="foo", default="biz")
+ assert (7, "biz") in nwv_def
+ assert (3, "bar") in nwv_def
+
+ def test_getitem(self):
+ G = self.G.copy()
+ nv = G.nodes
+ G.nodes[3]["foo"] = "bar"
+ assert nv[3] == {"foo": "bar"}
+ # default
+ nwv_def = G.nodes(data="foo", default="biz")
+ assert nwv_def[7], "biz"
+ assert nwv_def[3] == "bar"
+ # slicing
+ with pytest.raises(nx.NetworkXError):
+ G.nodes.data()[0:5]
+
+ def test_iter(self):
+ G = self.G.copy()
+ nv = G.nodes.data()
+ ndv = G.nodes.data(True)
+ nwv = G.nodes.data("foo")
+ for i, (n, d) in enumerate(nv):
+ assert i == n
+ assert d == {}
+ inv = iter(nv)
+ assert next(inv) == (0, {})
+ G.nodes[3]["foo"] = "bar"
+ # default
+ for n, d in nv:
+ if n == 3:
+ assert d == {"foo": "bar"}
+ else:
+ assert d == {}
+ # data=True
+ for n, d in ndv:
+ if n == 3:
+ assert d == {"foo": "bar"}
+ else:
+ assert d == {}
+ # data='foo'
+ for n, d in nwv:
+ if n == 3:
+ assert d == "bar"
+ else:
+ assert d is None
+ # data='foo', default=1
+ for n, d in G.nodes.data("foo", default=1):
+ if n == 3:
+ assert d == "bar"
+ else:
+ assert d == 1
+
+
+def test_nodedataview_unhashable():
+ G = nx.path_graph(9)
+ G.nodes[3]["foo"] = "bar"
+ nvs = [G.nodes.data()]
+ nvs.append(G.nodes.data(True))
+ H = G.copy()
+ H.nodes[4]["foo"] = {1, 2, 3}
+ nvs.append(H.nodes.data(True))
+ # raise unhashable
+ for nv in nvs:
+ pytest.raises(TypeError, set, nv)
+ pytest.raises(TypeError, eval, "nv | nv", locals())
+ # no raise... hashable
+ Gn = G.nodes.data(False)
+ set(Gn)
+ Gn | Gn
+ Gn = G.nodes.data("foo")
+ set(Gn)
+ Gn | Gn
+
+
+class TestNodeViewSetOps:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9)
+ cls.G.nodes[3]["foo"] = "bar"
+ cls.nv = cls.G.nodes
+
+ def n_its(self, nodes):
+ return set(nodes)
+
+ def test_len(self):
+ G = self.G.copy()
+ nv = G.nodes
+ assert len(nv) == 9
+ G.remove_node(7)
+ assert len(nv) == 8
+ G.add_node(9)
+ assert len(nv) == 9
+
+ def test_and(self):
+ # print("G & H nodes:", gnv & hnv)
+ nv = self.nv
+ some_nodes = self.n_its(range(5, 12))
+ assert nv & some_nodes == self.n_its(range(5, 9))
+ assert some_nodes & nv == self.n_its(range(5, 9))
+
+ def test_or(self):
+ # print("G | H nodes:", gnv | hnv)
+ nv = self.nv
+ some_nodes = self.n_its(range(5, 12))
+ assert nv | some_nodes == self.n_its(range(12))
+ assert some_nodes | nv == self.n_its(range(12))
+
+ def test_xor(self):
+ # print("G ^ H nodes:", gnv ^ hnv)
+ nv = self.nv
+ some_nodes = self.n_its(range(5, 12))
+ nodes = {0, 1, 2, 3, 4, 9, 10, 11}
+ assert nv ^ some_nodes == self.n_its(nodes)
+ assert some_nodes ^ nv == self.n_its(nodes)
+
+ def test_sub(self):
+ # print("G - H nodes:", gnv - hnv)
+ nv = self.nv
+ some_nodes = self.n_its(range(5, 12))
+ assert nv - some_nodes == self.n_its(range(5))
+ assert some_nodes - nv == self.n_its(range(9, 12))
+
+
+class TestNodeDataViewSetOps(TestNodeViewSetOps):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9)
+ cls.G.nodes[3]["foo"] = "bar"
+ cls.nv = cls.G.nodes.data("foo")
+
+ def n_its(self, nodes):
+ return {(node, "bar" if node == 3 else None) for node in nodes}
+
+
+class TestNodeDataViewDefaultSetOps(TestNodeDataViewSetOps):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9)
+ cls.G.nodes[3]["foo"] = "bar"
+ cls.nv = cls.G.nodes.data("foo", default=1)
+
+ def n_its(self, nodes):
+ return {(node, "bar" if node == 3 else 1) for node in nodes}
+
+
+# Edges Data View
+class TestEdgeDataView:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9)
+ cls.eview = nx.reportviews.EdgeView
+
+ def test_pickle(self):
+ import pickle
+
+ ev = self.eview(self.G)(data=True)
+ pev = pickle.loads(pickle.dumps(ev, -1))
+ assert list(ev) == list(pev)
+ assert ev.__slots__ == pev.__slots__
+
+ def modify_edge(self, G, e, **kwds):
+ G._adj[e[0]][e[1]].update(kwds)
+
+ def test_str(self):
+ ev = self.eview(self.G)(data=True)
+ rep = str([(n, n + 1, {}) for n in range(8)])
+ assert str(ev) == rep
+
+ def test_repr(self):
+ ev = self.eview(self.G)(data=True)
+ rep = (
+ "EdgeDataView([(0, 1, {}), (1, 2, {}), "
+ + "(2, 3, {}), (3, 4, {}), "
+ + "(4, 5, {}), (5, 6, {}), "
+ + "(6, 7, {}), (7, 8, {})])"
+ )
+ assert repr(ev) == rep
+
+ def test_iterdata(self):
+ G = self.G.copy()
+ evr = self.eview(G)
+ ev = evr(data=True)
+ ev_def = evr(data="foo", default=1)
+
+ for u, v, d in ev:
+ pass
+ assert d == {}
+
+ for u, v, wt in ev_def:
+ pass
+ assert wt == 1
+
+ self.modify_edge(G, (2, 3), foo="bar")
+ for e in ev:
+ assert len(e) == 3
+ if set(e[:2]) == {2, 3}:
+ assert e[2] == {"foo": "bar"}
+ checked = True
+ else:
+ assert e[2] == {}
+ assert checked
+
+ for e in ev_def:
+ assert len(e) == 3
+ if set(e[:2]) == {2, 3}:
+ assert e[2] == "bar"
+ checked_wt = True
+ else:
+ assert e[2] == 1
+ assert checked_wt
+
+ def test_iter(self):
+ evr = self.eview(self.G)
+ ev = evr()
+ for u, v in ev:
+ pass
+ iev = iter(ev)
+ assert next(iev) == (0, 1)
+ assert iter(ev) != ev
+ assert iter(iev) == iev
+
+ def test_contains(self):
+ evr = self.eview(self.G)
+ ev = evr()
+ if self.G.is_directed():
+ assert (1, 2) in ev and (2, 1) not in ev
+ else:
+ assert (1, 2) in ev and (2, 1) in ev
+ assert (1, 4) not in ev
+ assert (1, 90) not in ev
+ assert (90, 1) not in ev
+
+ def test_contains_with_nbunch(self):
+ evr = self.eview(self.G)
+ ev = evr(nbunch=[0, 2])
+ if self.G.is_directed():
+ assert (0, 1) in ev
+ assert (1, 2) not in ev
+ assert (2, 3) in ev
+ else:
+ assert (0, 1) in ev
+ assert (1, 2) in ev
+ assert (2, 3) in ev
+ assert (3, 4) not in ev
+ assert (4, 5) not in ev
+ assert (5, 6) not in ev
+ assert (7, 8) not in ev
+ assert (8, 9) not in ev
+
+ def test_len(self):
+ evr = self.eview(self.G)
+ ev = evr(data="foo")
+ assert len(ev) == 8
+ assert len(evr(1)) == 2
+ assert len(evr([1, 2, 3])) == 4
+
+ assert len(self.G.edges(1)) == 2
+ assert len(self.G.edges()) == 8
+ assert len(self.G.edges) == 8
+
+ H = self.G.copy()
+ H.add_edge(1, 1)
+ assert len(H.edges(1)) == 3
+ assert len(H.edges()) == 9
+ assert len(H.edges) == 9
+
+
+class TestOutEdgeDataView(TestEdgeDataView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, create_using=nx.DiGraph())
+ cls.eview = nx.reportviews.OutEdgeView
+
+ def test_repr(self):
+ ev = self.eview(self.G)(data=True)
+ rep = (
+ "OutEdgeDataView([(0, 1, {}), (1, 2, {}), "
+ + "(2, 3, {}), (3, 4, {}), "
+ + "(4, 5, {}), (5, 6, {}), "
+ + "(6, 7, {}), (7, 8, {})])"
+ )
+ assert repr(ev) == rep
+
+ def test_len(self):
+ evr = self.eview(self.G)
+ ev = evr(data="foo")
+ assert len(ev) == 8
+ assert len(evr(1)) == 1
+ assert len(evr([1, 2, 3])) == 3
+
+ assert len(self.G.edges(1)) == 1
+ assert len(self.G.edges()) == 8
+ assert len(self.G.edges) == 8
+
+ H = self.G.copy()
+ H.add_edge(1, 1)
+ assert len(H.edges(1)) == 2
+ assert len(H.edges()) == 9
+ assert len(H.edges) == 9
+
+ def test_contains_with_nbunch(self):
+ evr = self.eview(self.G)
+ ev = evr(nbunch=[0, 2])
+ assert (0, 1) in ev
+ assert (1, 2) not in ev
+ assert (2, 3) in ev
+ assert (3, 4) not in ev
+ assert (4, 5) not in ev
+ assert (5, 6) not in ev
+ assert (7, 8) not in ev
+ assert (8, 9) not in ev
+
+
+class TestInEdgeDataView(TestOutEdgeDataView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, create_using=nx.DiGraph())
+ cls.eview = nx.reportviews.InEdgeView
+
+ def test_repr(self):
+ ev = self.eview(self.G)(data=True)
+ rep = (
+ "InEdgeDataView([(0, 1, {}), (1, 2, {}), "
+ + "(2, 3, {}), (3, 4, {}), "
+ + "(4, 5, {}), (5, 6, {}), "
+ + "(6, 7, {}), (7, 8, {})])"
+ )
+ assert repr(ev) == rep
+
+ def test_contains_with_nbunch(self):
+ evr = self.eview(self.G)
+ ev = evr(nbunch=[0, 2])
+ assert (0, 1) not in ev
+ assert (1, 2) in ev
+ assert (2, 3) not in ev
+ assert (3, 4) not in ev
+ assert (4, 5) not in ev
+ assert (5, 6) not in ev
+ assert (7, 8) not in ev
+ assert (8, 9) not in ev
+
+
+class TestMultiEdgeDataView(TestEdgeDataView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, create_using=nx.MultiGraph())
+ cls.eview = nx.reportviews.MultiEdgeView
+
+ def modify_edge(self, G, e, **kwds):
+ G._adj[e[0]][e[1]][0].update(kwds)
+
+ def test_repr(self):
+ ev = self.eview(self.G)(data=True)
+ rep = (
+ "MultiEdgeDataView([(0, 1, {}), (1, 2, {}), "
+ + "(2, 3, {}), (3, 4, {}), "
+ + "(4, 5, {}), (5, 6, {}), "
+ + "(6, 7, {}), (7, 8, {})])"
+ )
+ assert repr(ev) == rep
+
+ def test_contains_with_nbunch(self):
+ evr = self.eview(self.G)
+ ev = evr(nbunch=[0, 2])
+ assert (0, 1) in ev
+ assert (1, 2) in ev
+ assert (2, 3) in ev
+ assert (3, 4) not in ev
+ assert (4, 5) not in ev
+ assert (5, 6) not in ev
+ assert (7, 8) not in ev
+ assert (8, 9) not in ev
+
+
+class TestOutMultiEdgeDataView(TestOutEdgeDataView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, create_using=nx.MultiDiGraph())
+ cls.eview = nx.reportviews.OutMultiEdgeView
+
+ def modify_edge(self, G, e, **kwds):
+ G._adj[e[0]][e[1]][0].update(kwds)
+
+ def test_repr(self):
+ ev = self.eview(self.G)(data=True)
+ rep = (
+ "OutMultiEdgeDataView([(0, 1, {}), (1, 2, {}), "
+ + "(2, 3, {}), (3, 4, {}), "
+ + "(4, 5, {}), (5, 6, {}), "
+ + "(6, 7, {}), (7, 8, {})])"
+ )
+ assert repr(ev) == rep
+
+ def test_contains_with_nbunch(self):
+ evr = self.eview(self.G)
+ ev = evr(nbunch=[0, 2])
+ assert (0, 1) in ev
+ assert (1, 2) not in ev
+ assert (2, 3) in ev
+ assert (3, 4) not in ev
+ assert (4, 5) not in ev
+ assert (5, 6) not in ev
+ assert (7, 8) not in ev
+ assert (8, 9) not in ev
+
+
+class TestInMultiEdgeDataView(TestOutMultiEdgeDataView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, create_using=nx.MultiDiGraph())
+ cls.eview = nx.reportviews.InMultiEdgeView
+
+ def test_repr(self):
+ ev = self.eview(self.G)(data=True)
+ rep = (
+ "InMultiEdgeDataView([(0, 1, {}), (1, 2, {}), "
+ + "(2, 3, {}), (3, 4, {}), "
+ + "(4, 5, {}), (5, 6, {}), "
+ + "(6, 7, {}), (7, 8, {})])"
+ )
+ assert repr(ev) == rep
+
+ def test_contains_with_nbunch(self):
+ evr = self.eview(self.G)
+ ev = evr(nbunch=[0, 2])
+ assert (0, 1) not in ev
+ assert (1, 2) in ev
+ assert (2, 3) not in ev
+ assert (3, 4) not in ev
+ assert (4, 5) not in ev
+ assert (5, 6) not in ev
+ assert (7, 8) not in ev
+ assert (8, 9) not in ev
+
+
+# Edge Views
+class TestEdgeView:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9)
+ cls.eview = nx.reportviews.EdgeView
+
+ def test_pickle(self):
+ import pickle
+
+ ev = self.eview(self.G)
+ pev = pickle.loads(pickle.dumps(ev, -1))
+ assert ev == pev
+ assert ev.__slots__ == pev.__slots__
+
+ def modify_edge(self, G, e, **kwds):
+ G._adj[e[0]][e[1]].update(kwds)
+
+ def test_str(self):
+ ev = self.eview(self.G)
+ rep = str([(n, n + 1) for n in range(8)])
+ assert str(ev) == rep
+
+ def test_repr(self):
+ ev = self.eview(self.G)
+ rep = (
+ "EdgeView([(0, 1), (1, 2), (2, 3), (3, 4), "
+ + "(4, 5), (5, 6), (6, 7), (7, 8)])"
+ )
+ assert repr(ev) == rep
+
+ def test_getitem(self):
+ G = self.G.copy()
+ ev = G.edges
+ G.edges[0, 1]["foo"] = "bar"
+ assert ev[0, 1] == {"foo": "bar"}
+
+ # slicing
+ with pytest.raises(nx.NetworkXError, match=".*does not support slicing"):
+ G.edges[0:5]
+
+ # Invalid edge
+ with pytest.raises(KeyError, match=r".*edge.*is not in the graph."):
+ G.edges[0, 9]
+
+ def test_call(self):
+ ev = self.eview(self.G)
+ assert id(ev) == id(ev())
+ assert id(ev) == id(ev(data=False))
+ assert id(ev) != id(ev(data=True))
+ assert id(ev) != id(ev(nbunch=1))
+
+ def test_data(self):
+ ev = self.eview(self.G)
+ assert id(ev) != id(ev.data())
+ assert id(ev) == id(ev.data(data=False))
+ assert id(ev) != id(ev.data(data=True))
+ assert id(ev) != id(ev.data(nbunch=1))
+
+ def test_iter(self):
+ ev = self.eview(self.G)
+ for u, v in ev:
+ pass
+ iev = iter(ev)
+ assert next(iev) == (0, 1)
+ assert iter(ev) != ev
+ assert iter(iev) == iev
+
+ def test_contains(self):
+ ev = self.eview(self.G)
+ edv = ev()
+ if self.G.is_directed():
+ assert (1, 2) in ev and (2, 1) not in ev
+ assert (1, 2) in edv and (2, 1) not in edv
+ else:
+ assert (1, 2) in ev and (2, 1) in ev
+ assert (1, 2) in edv and (2, 1) in edv
+ assert (1, 4) not in ev
+ assert (1, 4) not in edv
+ # edge not in graph
+ assert (1, 90) not in ev
+ assert (90, 1) not in ev
+ assert (1, 90) not in edv
+ assert (90, 1) not in edv
+
+ def test_contains_with_nbunch(self):
+ ev = self.eview(self.G)
+ evn = ev(nbunch=[0, 2])
+ assert (0, 1) in evn
+ assert (1, 2) in evn
+ assert (2, 3) in evn
+ assert (3, 4) not in evn
+ assert (4, 5) not in evn
+ assert (5, 6) not in evn
+ assert (7, 8) not in evn
+ assert (8, 9) not in evn
+
+ def test_len(self):
+ ev = self.eview(self.G)
+ num_ed = 9 if self.G.is_multigraph() else 8
+ assert len(ev) == num_ed
+
+ H = self.G.copy()
+ H.add_edge(1, 1)
+ assert len(H.edges(1)) == 3 + H.is_multigraph() - H.is_directed()
+ assert len(H.edges()) == num_ed + 1
+ assert len(H.edges) == num_ed + 1
+
+ def test_and(self):
+ # print("G & H edges:", gnv & hnv)
+ ev = self.eview(self.G)
+ some_edges = {(0, 1), (1, 0), (0, 2)}
+ if self.G.is_directed():
+ assert some_edges & ev, {(0, 1)}
+ assert ev & some_edges, {(0, 1)}
+ else:
+ assert ev & some_edges == {(0, 1), (1, 0)}
+ assert some_edges & ev == {(0, 1), (1, 0)}
+ return
+
+ def test_or(self):
+ # print("G | H edges:", gnv | hnv)
+ ev = self.eview(self.G)
+ some_edges = {(0, 1), (1, 0), (0, 2)}
+ result1 = {(n, n + 1) for n in range(8)}
+ result1.update(some_edges)
+ result2 = {(n + 1, n) for n in range(8)}
+ result2.update(some_edges)
+ assert (ev | some_edges) in (result1, result2)
+ assert (some_edges | ev) in (result1, result2)
+
+ def test_xor(self):
+ # print("G ^ H edges:", gnv ^ hnv)
+ ev = self.eview(self.G)
+ some_edges = {(0, 1), (1, 0), (0, 2)}
+ if self.G.is_directed():
+ result = {(n, n + 1) for n in range(1, 8)}
+ result.update({(1, 0), (0, 2)})
+ assert ev ^ some_edges == result
+ else:
+ result = {(n, n + 1) for n in range(1, 8)}
+ result.update({(0, 2)})
+ assert ev ^ some_edges == result
+ return
+
+ def test_sub(self):
+ # print("G - H edges:", gnv - hnv)
+ ev = self.eview(self.G)
+ some_edges = {(0, 1), (1, 0), (0, 2)}
+ result = {(n, n + 1) for n in range(8)}
+ result.remove((0, 1))
+ assert ev - some_edges, result
+
+
+class TestOutEdgeView(TestEdgeView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, nx.DiGraph())
+ cls.eview = nx.reportviews.OutEdgeView
+
+ def test_repr(self):
+ ev = self.eview(self.G)
+ rep = (
+ "OutEdgeView([(0, 1), (1, 2), (2, 3), (3, 4), "
+ + "(4, 5), (5, 6), (6, 7), (7, 8)])"
+ )
+ assert repr(ev) == rep
+
+ def test_contains_with_nbunch(self):
+ ev = self.eview(self.G)
+ evn = ev(nbunch=[0, 2])
+ assert (0, 1) in evn
+ assert (1, 2) not in evn
+ assert (2, 3) in evn
+ assert (3, 4) not in evn
+ assert (4, 5) not in evn
+ assert (5, 6) not in evn
+ assert (7, 8) not in evn
+ assert (8, 9) not in evn
+
+
+class TestInEdgeView(TestEdgeView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, nx.DiGraph())
+ cls.eview = nx.reportviews.InEdgeView
+
+ def test_repr(self):
+ ev = self.eview(self.G)
+ rep = (
+ "InEdgeView([(0, 1), (1, 2), (2, 3), (3, 4), "
+ + "(4, 5), (5, 6), (6, 7), (7, 8)])"
+ )
+ assert repr(ev) == rep
+
+ def test_contains_with_nbunch(self):
+ ev = self.eview(self.G)
+ evn = ev(nbunch=[0, 2])
+ assert (0, 1) not in evn
+ assert (1, 2) in evn
+ assert (2, 3) not in evn
+ assert (3, 4) not in evn
+ assert (4, 5) not in evn
+ assert (5, 6) not in evn
+ assert (7, 8) not in evn
+ assert (8, 9) not in evn
+
+
+class TestMultiEdgeView(TestEdgeView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, nx.MultiGraph())
+ cls.G.add_edge(1, 2, key=3, foo="bar")
+ cls.eview = nx.reportviews.MultiEdgeView
+
+ def modify_edge(self, G, e, **kwds):
+ if len(e) == 2:
+ e = e + (0,)
+ G._adj[e[0]][e[1]][e[2]].update(kwds)
+
+ def test_str(self):
+ ev = self.eview(self.G)
+ replist = [(n, n + 1, 0) for n in range(8)]
+ replist.insert(2, (1, 2, 3))
+ rep = str(replist)
+ assert str(ev) == rep
+
+ def test_getitem(self):
+ G = self.G.copy()
+ ev = G.edges
+ G.edges[0, 1, 0]["foo"] = "bar"
+ assert ev[0, 1, 0] == {"foo": "bar"}
+
+ # slicing
+ with pytest.raises(nx.NetworkXError):
+ G.edges[0:5]
+
+ def test_repr(self):
+ ev = self.eview(self.G)
+ rep = (
+ "MultiEdgeView([(0, 1, 0), (1, 2, 0), (1, 2, 3), (2, 3, 0), "
+ + "(3, 4, 0), (4, 5, 0), (5, 6, 0), (6, 7, 0), (7, 8, 0)])"
+ )
+ assert repr(ev) == rep
+
+ def test_call(self):
+ ev = self.eview(self.G)
+ assert id(ev) == id(ev(keys=True))
+ assert id(ev) == id(ev(data=False, keys=True))
+ assert id(ev) != id(ev(keys=False))
+ assert id(ev) != id(ev(data=True))
+ assert id(ev) != id(ev(nbunch=1))
+
+ def test_data(self):
+ ev = self.eview(self.G)
+ assert id(ev) != id(ev.data())
+ assert id(ev) == id(ev.data(data=False, keys=True))
+ assert id(ev) != id(ev.data(keys=False))
+ assert id(ev) != id(ev.data(data=True))
+ assert id(ev) != id(ev.data(nbunch=1))
+
+ def test_iter(self):
+ ev = self.eview(self.G)
+ for u, v, k in ev:
+ pass
+ iev = iter(ev)
+ assert next(iev) == (0, 1, 0)
+ assert iter(ev) != ev
+ assert iter(iev) == iev
+
+ def test_iterkeys(self):
+ G = self.G
+ evr = self.eview(G)
+ ev = evr(keys=True)
+ for u, v, k in ev:
+ pass
+ assert k == 0
+ ev = evr(keys=True, data="foo", default=1)
+ for u, v, k, wt in ev:
+ pass
+ assert wt == 1
+
+ self.modify_edge(G, (2, 3, 0), foo="bar")
+ ev = evr(keys=True, data=True)
+ for e in ev:
+ assert len(e) == 4
+ print("edge:", e)
+ if set(e[:2]) == {2, 3}:
+ print(self.G._adj[2][3])
+ assert e[2] == 0
+ assert e[3] == {"foo": "bar"}
+ checked = True
+ elif set(e[:3]) == {1, 2, 3}:
+ assert e[2] == 3
+ assert e[3] == {"foo": "bar"}
+ checked_multi = True
+ else:
+ assert e[2] == 0
+ assert e[3] == {}
+ assert checked
+ assert checked_multi
+ ev = evr(keys=True, data="foo", default=1)
+ for e in ev:
+ if set(e[:2]) == {1, 2} and e[2] == 3:
+ assert e[3] == "bar"
+ if set(e[:2]) == {1, 2} and e[2] == 0:
+ assert e[3] == 1
+ if set(e[:2]) == {2, 3}:
+ assert e[2] == 0
+ assert e[3] == "bar"
+ assert len(e) == 4
+ checked_wt = True
+ assert checked_wt
+ ev = evr(keys=True)
+ for e in ev:
+ assert len(e) == 3
+ elist = sorted([(i, i + 1, 0) for i in range(8)] + [(1, 2, 3)])
+ assert sorted(ev) == elist
+ # test that the keyword arguments are passed correctly
+ ev = evr((1, 2), "foo", keys=True, default=1)
+ with pytest.raises(TypeError):
+ evr((1, 2), "foo", True, 1)
+ with pytest.raises(TypeError):
+ evr((1, 2), "foo", True, default=1)
+ for e in ev:
+ if set(e[:2]) == {1, 2}:
+ assert e[2] in {0, 3}
+ if e[2] == 3:
+ assert e[3] == "bar"
+ else: # e[2] == 0
+ assert e[3] == 1
+ if G.is_directed():
+ assert len(list(ev)) == 3
+ else:
+ assert len(list(ev)) == 4
+
+ def test_or(self):
+ # print("G | H edges:", gnv | hnv)
+ ev = self.eview(self.G)
+ some_edges = {(0, 1, 0), (1, 0, 0), (0, 2, 0)}
+ result = {(n, n + 1, 0) for n in range(8)}
+ result.update(some_edges)
+ result.update({(1, 2, 3)})
+ assert ev | some_edges == result
+ assert some_edges | ev == result
+
+ def test_sub(self):
+ # print("G - H edges:", gnv - hnv)
+ ev = self.eview(self.G)
+ some_edges = {(0, 1, 0), (1, 0, 0), (0, 2, 0)}
+ result = {(n, n + 1, 0) for n in range(8)}
+ result.remove((0, 1, 0))
+ result.update({(1, 2, 3)})
+ assert ev - some_edges, result
+ assert some_edges - ev, result
+
+ def test_xor(self):
+ # print("G ^ H edges:", gnv ^ hnv)
+ ev = self.eview(self.G)
+ some_edges = {(0, 1, 0), (1, 0, 0), (0, 2, 0)}
+ if self.G.is_directed():
+ result = {(n, n + 1, 0) for n in range(1, 8)}
+ result.update({(1, 0, 0), (0, 2, 0), (1, 2, 3)})
+ assert ev ^ some_edges == result
+ assert some_edges ^ ev == result
+ else:
+ result = {(n, n + 1, 0) for n in range(1, 8)}
+ result.update({(0, 2, 0), (1, 2, 3)})
+ assert ev ^ some_edges == result
+ assert some_edges ^ ev == result
+
+ def test_and(self):
+ # print("G & H edges:", gnv & hnv)
+ ev = self.eview(self.G)
+ some_edges = {(0, 1, 0), (1, 0, 0), (0, 2, 0)}
+ if self.G.is_directed():
+ assert ev & some_edges == {(0, 1, 0)}
+ assert some_edges & ev == {(0, 1, 0)}
+ else:
+ assert ev & some_edges == {(0, 1, 0), (1, 0, 0)}
+ assert some_edges & ev == {(0, 1, 0), (1, 0, 0)}
+
+ def test_contains_with_nbunch(self):
+ ev = self.eview(self.G)
+ evn = ev(nbunch=[0, 2])
+ assert (0, 1) in evn
+ assert (1, 2) in evn
+ assert (2, 3) in evn
+ assert (3, 4) not in evn
+ assert (4, 5) not in evn
+ assert (5, 6) not in evn
+ assert (7, 8) not in evn
+ assert (8, 9) not in evn
+
+
+class TestOutMultiEdgeView(TestMultiEdgeView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, nx.MultiDiGraph())
+ cls.G.add_edge(1, 2, key=3, foo="bar")
+ cls.eview = nx.reportviews.OutMultiEdgeView
+
+ def modify_edge(self, G, e, **kwds):
+ if len(e) == 2:
+ e = e + (0,)
+ G._adj[e[0]][e[1]][e[2]].update(kwds)
+
+ def test_repr(self):
+ ev = self.eview(self.G)
+ rep = (
+ "OutMultiEdgeView([(0, 1, 0), (1, 2, 0), (1, 2, 3), (2, 3, 0),"
+ + " (3, 4, 0), (4, 5, 0), (5, 6, 0), (6, 7, 0), (7, 8, 0)])"
+ )
+ assert repr(ev) == rep
+
+ def test_contains_with_nbunch(self):
+ ev = self.eview(self.G)
+ evn = ev(nbunch=[0, 2])
+ assert (0, 1) in evn
+ assert (1, 2) not in evn
+ assert (2, 3) in evn
+ assert (3, 4) not in evn
+ assert (4, 5) not in evn
+ assert (5, 6) not in evn
+ assert (7, 8) not in evn
+ assert (8, 9) not in evn
+
+
+class TestInMultiEdgeView(TestMultiEdgeView):
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, nx.MultiDiGraph())
+ cls.G.add_edge(1, 2, key=3, foo="bar")
+ cls.eview = nx.reportviews.InMultiEdgeView
+
+ def modify_edge(self, G, e, **kwds):
+ if len(e) == 2:
+ e = e + (0,)
+ G._adj[e[0]][e[1]][e[2]].update(kwds)
+
+ def test_repr(self):
+ ev = self.eview(self.G)
+ rep = (
+ "InMultiEdgeView([(0, 1, 0), (1, 2, 0), (1, 2, 3), (2, 3, 0), "
+ + "(3, 4, 0), (4, 5, 0), (5, 6, 0), (6, 7, 0), (7, 8, 0)])"
+ )
+ assert repr(ev) == rep
+
+ def test_contains_with_nbunch(self):
+ ev = self.eview(self.G)
+ evn = ev(nbunch=[0, 2])
+ assert (0, 1) not in evn
+ assert (1, 2) in evn
+ assert (2, 3) not in evn
+ assert (3, 4) not in evn
+ assert (4, 5) not in evn
+ assert (5, 6) not in evn
+ assert (7, 8) not in evn
+ assert (8, 9) not in evn
+
+
+# Degrees
+class TestDegreeView:
+ GRAPH = nx.Graph
+ dview = nx.reportviews.DegreeView
+
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(6, cls.GRAPH())
+ cls.G.add_edge(1, 3, foo=2)
+ cls.G.add_edge(1, 3, foo=3)
+
+ def test_pickle(self):
+ import pickle
+
+ deg = self.G.degree
+ pdeg = pickle.loads(pickle.dumps(deg, -1))
+ assert dict(deg) == dict(pdeg)
+
+ def test_str(self):
+ dv = self.dview(self.G)
+ rep = str([(0, 1), (1, 3), (2, 2), (3, 3), (4, 2), (5, 1)])
+ assert str(dv) == rep
+ dv = self.G.degree()
+ assert str(dv) == rep
+
+ def test_repr(self):
+ dv = self.dview(self.G)
+ rep = "DegreeView({0: 1, 1: 3, 2: 2, 3: 3, 4: 2, 5: 1})"
+ assert repr(dv) == rep
+
+ def test_iter(self):
+ dv = self.dview(self.G)
+ for n, d in dv:
+ pass
+ idv = iter(dv)
+ assert iter(dv) != dv
+ assert iter(idv) == idv
+ assert next(idv) == (0, dv[0])
+ assert next(idv) == (1, dv[1])
+ # weighted
+ dv = self.dview(self.G, weight="foo")
+ for n, d in dv:
+ pass
+ idv = iter(dv)
+ assert iter(dv) != dv
+ assert iter(idv) == idv
+ assert next(idv) == (0, dv[0])
+ assert next(idv) == (1, dv[1])
+
+ def test_nbunch(self):
+ dv = self.dview(self.G)
+ dvn = dv(0)
+ assert dvn == 1
+ dvn = dv([2, 3])
+ assert sorted(dvn) == [(2, 2), (3, 3)]
+
+ def test_getitem(self):
+ dv = self.dview(self.G)
+ assert dv[0] == 1
+ assert dv[1] == 3
+ assert dv[2] == 2
+ assert dv[3] == 3
+ dv = self.dview(self.G, weight="foo")
+ assert dv[0] == 1
+ assert dv[1] == 5
+ assert dv[2] == 2
+ assert dv[3] == 5
+
+ def test_weight(self):
+ dv = self.dview(self.G)
+ dvw = dv(0, weight="foo")
+ assert dvw == 1
+ dvw = dv(1, weight="foo")
+ assert dvw == 5
+ dvw = dv([2, 3], weight="foo")
+ assert sorted(dvw) == [(2, 2), (3, 5)]
+ dvd = dict(dv(weight="foo"))
+ assert dvd[0] == 1
+ assert dvd[1] == 5
+ assert dvd[2] == 2
+ assert dvd[3] == 5
+
+ def test_len(self):
+ dv = self.dview(self.G)
+ assert len(dv) == 6
+
+
+class TestDiDegreeView(TestDegreeView):
+ GRAPH = nx.DiGraph
+ dview = nx.reportviews.DiDegreeView
+
+ def test_repr(self):
+ dv = self.G.degree()
+ rep = "DiDegreeView({0: 1, 1: 3, 2: 2, 3: 3, 4: 2, 5: 1})"
+ assert repr(dv) == rep
+
+
+class TestOutDegreeView(TestDegreeView):
+ GRAPH = nx.DiGraph
+ dview = nx.reportviews.OutDegreeView
+
+ def test_str(self):
+ dv = self.dview(self.G)
+ rep = str([(0, 1), (1, 2), (2, 1), (3, 1), (4, 1), (5, 0)])
+ assert str(dv) == rep
+ dv = self.G.out_degree()
+ assert str(dv) == rep
+
+ def test_repr(self):
+ dv = self.G.out_degree()
+ rep = "OutDegreeView({0: 1, 1: 2, 2: 1, 3: 1, 4: 1, 5: 0})"
+ assert repr(dv) == rep
+
+ def test_nbunch(self):
+ dv = self.dview(self.G)
+ dvn = dv(0)
+ assert dvn == 1
+ dvn = dv([2, 3])
+ assert sorted(dvn) == [(2, 1), (3, 1)]
+
+ def test_getitem(self):
+ dv = self.dview(self.G)
+ assert dv[0] == 1
+ assert dv[1] == 2
+ assert dv[2] == 1
+ assert dv[3] == 1
+ dv = self.dview(self.G, weight="foo")
+ assert dv[0] == 1
+ assert dv[1] == 4
+ assert dv[2] == 1
+ assert dv[3] == 1
+
+ def test_weight(self):
+ dv = self.dview(self.G)
+ dvw = dv(0, weight="foo")
+ assert dvw == 1
+ dvw = dv(1, weight="foo")
+ assert dvw == 4
+ dvw = dv([2, 3], weight="foo")
+ assert sorted(dvw) == [(2, 1), (3, 1)]
+ dvd = dict(dv(weight="foo"))
+ assert dvd[0] == 1
+ assert dvd[1] == 4
+ assert dvd[2] == 1
+ assert dvd[3] == 1
+
+
+class TestInDegreeView(TestDegreeView):
+ GRAPH = nx.DiGraph
+ dview = nx.reportviews.InDegreeView
+
+ def test_str(self):
+ dv = self.dview(self.G)
+ rep = str([(0, 0), (1, 1), (2, 1), (3, 2), (4, 1), (5, 1)])
+ assert str(dv) == rep
+ dv = self.G.in_degree()
+ assert str(dv) == rep
+
+ def test_repr(self):
+ dv = self.G.in_degree()
+ rep = "InDegreeView({0: 0, 1: 1, 2: 1, 3: 2, 4: 1, 5: 1})"
+ assert repr(dv) == rep
+
+ def test_nbunch(self):
+ dv = self.dview(self.G)
+ dvn = dv(0)
+ assert dvn == 0
+ dvn = dv([2, 3])
+ assert sorted(dvn) == [(2, 1), (3, 2)]
+
+ def test_getitem(self):
+ dv = self.dview(self.G)
+ assert dv[0] == 0
+ assert dv[1] == 1
+ assert dv[2] == 1
+ assert dv[3] == 2
+ dv = self.dview(self.G, weight="foo")
+ assert dv[0] == 0
+ assert dv[1] == 1
+ assert dv[2] == 1
+ assert dv[3] == 4
+
+ def test_weight(self):
+ dv = self.dview(self.G)
+ dvw = dv(0, weight="foo")
+ assert dvw == 0
+ dvw = dv(1, weight="foo")
+ assert dvw == 1
+ dvw = dv([2, 3], weight="foo")
+ assert sorted(dvw) == [(2, 1), (3, 4)]
+ dvd = dict(dv(weight="foo"))
+ assert dvd[0] == 0
+ assert dvd[1] == 1
+ assert dvd[2] == 1
+ assert dvd[3] == 4
+
+
+class TestMultiDegreeView(TestDegreeView):
+ GRAPH = nx.MultiGraph
+ dview = nx.reportviews.MultiDegreeView
+
+ def test_str(self):
+ dv = self.dview(self.G)
+ rep = str([(0, 1), (1, 4), (2, 2), (3, 4), (4, 2), (5, 1)])
+ assert str(dv) == rep
+ dv = self.G.degree()
+ assert str(dv) == rep
+
+ def test_repr(self):
+ dv = self.G.degree()
+ rep = "MultiDegreeView({0: 1, 1: 4, 2: 2, 3: 4, 4: 2, 5: 1})"
+ assert repr(dv) == rep
+
+ def test_nbunch(self):
+ dv = self.dview(self.G)
+ dvn = dv(0)
+ assert dvn == 1
+ dvn = dv([2, 3])
+ assert sorted(dvn) == [(2, 2), (3, 4)]
+
+ def test_getitem(self):
+ dv = self.dview(self.G)
+ assert dv[0] == 1
+ assert dv[1] == 4
+ assert dv[2] == 2
+ assert dv[3] == 4
+ dv = self.dview(self.G, weight="foo")
+ assert dv[0] == 1
+ assert dv[1] == 7
+ assert dv[2] == 2
+ assert dv[3] == 7
+
+ def test_weight(self):
+ dv = self.dview(self.G)
+ dvw = dv(0, weight="foo")
+ assert dvw == 1
+ dvw = dv(1, weight="foo")
+ assert dvw == 7
+ dvw = dv([2, 3], weight="foo")
+ assert sorted(dvw) == [(2, 2), (3, 7)]
+ dvd = dict(dv(weight="foo"))
+ assert dvd[0] == 1
+ assert dvd[1] == 7
+ assert dvd[2] == 2
+ assert dvd[3] == 7
+
+
+class TestDiMultiDegreeView(TestMultiDegreeView):
+ GRAPH = nx.MultiDiGraph
+ dview = nx.reportviews.DiMultiDegreeView
+
+ def test_repr(self):
+ dv = self.G.degree()
+ rep = "DiMultiDegreeView({0: 1, 1: 4, 2: 2, 3: 4, 4: 2, 5: 1})"
+ assert repr(dv) == rep
+
+
+class TestOutMultiDegreeView(TestDegreeView):
+ GRAPH = nx.MultiDiGraph
+ dview = nx.reportviews.OutMultiDegreeView
+
+ def test_str(self):
+ dv = self.dview(self.G)
+ rep = str([(0, 1), (1, 3), (2, 1), (3, 1), (4, 1), (5, 0)])
+ assert str(dv) == rep
+ dv = self.G.out_degree()
+ assert str(dv) == rep
+
+ def test_repr(self):
+ dv = self.G.out_degree()
+ rep = "OutMultiDegreeView({0: 1, 1: 3, 2: 1, 3: 1, 4: 1, 5: 0})"
+ assert repr(dv) == rep
+
+ def test_nbunch(self):
+ dv = self.dview(self.G)
+ dvn = dv(0)
+ assert dvn == 1
+ dvn = dv([2, 3])
+ assert sorted(dvn) == [(2, 1), (3, 1)]
+
+ def test_getitem(self):
+ dv = self.dview(self.G)
+ assert dv[0] == 1
+ assert dv[1] == 3
+ assert dv[2] == 1
+ assert dv[3] == 1
+ dv = self.dview(self.G, weight="foo")
+ assert dv[0] == 1
+ assert dv[1] == 6
+ assert dv[2] == 1
+ assert dv[3] == 1
+
+ def test_weight(self):
+ dv = self.dview(self.G)
+ dvw = dv(0, weight="foo")
+ assert dvw == 1
+ dvw = dv(1, weight="foo")
+ assert dvw == 6
+ dvw = dv([2, 3], weight="foo")
+ assert sorted(dvw) == [(2, 1), (3, 1)]
+ dvd = dict(dv(weight="foo"))
+ assert dvd[0] == 1
+ assert dvd[1] == 6
+ assert dvd[2] == 1
+ assert dvd[3] == 1
+
+
+class TestInMultiDegreeView(TestDegreeView):
+ GRAPH = nx.MultiDiGraph
+ dview = nx.reportviews.InMultiDegreeView
+
+ def test_str(self):
+ dv = self.dview(self.G)
+ rep = str([(0, 0), (1, 1), (2, 1), (3, 3), (4, 1), (5, 1)])
+ assert str(dv) == rep
+ dv = self.G.in_degree()
+ assert str(dv) == rep
+
+ def test_repr(self):
+ dv = self.G.in_degree()
+ rep = "InMultiDegreeView({0: 0, 1: 1, 2: 1, 3: 3, 4: 1, 5: 1})"
+ assert repr(dv) == rep
+
+ def test_nbunch(self):
+ dv = self.dview(self.G)
+ dvn = dv(0)
+ assert dvn == 0
+ dvn = dv([2, 3])
+ assert sorted(dvn) == [(2, 1), (3, 3)]
+
+ def test_getitem(self):
+ dv = self.dview(self.G)
+ assert dv[0] == 0
+ assert dv[1] == 1
+ assert dv[2] == 1
+ assert dv[3] == 3
+ dv = self.dview(self.G, weight="foo")
+ assert dv[0] == 0
+ assert dv[1] == 1
+ assert dv[2] == 1
+ assert dv[3] == 6
+
+ def test_weight(self):
+ dv = self.dview(self.G)
+ dvw = dv(0, weight="foo")
+ assert dvw == 0
+ dvw = dv(1, weight="foo")
+ assert dvw == 1
+ dvw = dv([2, 3], weight="foo")
+ assert sorted(dvw) == [(2, 1), (3, 6)]
+ dvd = dict(dv(weight="foo"))
+ assert dvd[0] == 0
+ assert dvd[1] == 1
+ assert dvd[2] == 1
+ assert dvd[3] == 6
+
+
+@pytest.mark.parametrize(
+ ("reportview", "err_msg_terms"),
+ (
+ (rv.NodeView, "list(G.nodes"),
+ (rv.NodeDataView, "list(G.nodes.data"),
+ (rv.EdgeView, "list(G.edges"),
+ # Directed EdgeViews
+ (rv.InEdgeView, "list(G.in_edges"),
+ (rv.OutEdgeView, "list(G.edges"),
+ # Multi EdgeViews
+ (rv.MultiEdgeView, "list(G.edges"),
+ (rv.InMultiEdgeView, "list(G.in_edges"),
+ (rv.OutMultiEdgeView, "list(G.edges"),
+ ),
+)
+def test_slicing_reportviews(reportview, err_msg_terms):
+ G = nx.complete_graph(3)
+ view = reportview(G)
+ with pytest.raises(nx.NetworkXError) as exc:
+ view[0:2]
+ errmsg = str(exc.value)
+ assert type(view).__name__ in errmsg
+ assert err_msg_terms in errmsg
+
+
+@pytest.mark.parametrize(
+ "graph", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
+)
+def test_cache_dict_get_set_state(graph):
+ G = nx.path_graph(5, graph())
+ G.nodes, G.edges, G.adj, G.degree
+ if G.is_directed():
+ G.pred, G.succ, G.in_edges, G.out_edges, G.in_degree, G.out_degree
+ cached_dict = G.__dict__
+ assert "nodes" in cached_dict
+ assert "edges" in cached_dict
+ assert "adj" in cached_dict
+ assert "degree" in cached_dict
+ if G.is_directed():
+ assert "pred" in cached_dict
+ assert "succ" in cached_dict
+ assert "in_edges" in cached_dict
+ assert "out_edges" in cached_dict
+ assert "in_degree" in cached_dict
+ assert "out_degree" in cached_dict
+
+ # Raises error if the cached properties and views do not work
+ pickle.loads(pickle.dumps(G, -1))
+ deepcopy(G)
+
+
+def test_edge_views_inherit_from_EdgeViewABC():
+ all_edge_view_classes = (v for v in dir(nx.reportviews) if "Edge" in v)
+ for eview_class in all_edge_view_classes:
+ assert issubclass(
+ getattr(nx.reportviews, eview_class), nx.reportviews.EdgeViewABC
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_special.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_special.py
new file mode 100644
index 00000000..1fa79605
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_special.py
@@ -0,0 +1,131 @@
+import networkx as nx
+
+from .test_digraph import BaseDiGraphTester
+from .test_digraph import TestDiGraph as _TestDiGraph
+from .test_graph import BaseGraphTester
+from .test_graph import TestGraph as _TestGraph
+from .test_multidigraph import TestMultiDiGraph as _TestMultiDiGraph
+from .test_multigraph import TestMultiGraph as _TestMultiGraph
+
+
+def test_factories():
+ class mydict1(dict):
+ pass
+
+ class mydict2(dict):
+ pass
+
+ class mydict3(dict):
+ pass
+
+ class mydict4(dict):
+ pass
+
+ class mydict5(dict):
+ pass
+
+ for Graph in (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph):
+ # print("testing class: ", Graph.__name__)
+ class MyGraph(Graph):
+ node_dict_factory = mydict1
+ adjlist_outer_dict_factory = mydict2
+ adjlist_inner_dict_factory = mydict3
+ edge_key_dict_factory = mydict4
+ edge_attr_dict_factory = mydict5
+
+ G = MyGraph()
+ assert isinstance(G._node, mydict1)
+ assert isinstance(G._adj, mydict2)
+ G.add_node(1)
+ assert isinstance(G._adj[1], mydict3)
+ if G.is_directed():
+ assert isinstance(G._pred, mydict2)
+ assert isinstance(G._succ, mydict2)
+ assert isinstance(G._pred[1], mydict3)
+ G.add_edge(1, 2)
+ if G.is_multigraph():
+ assert isinstance(G._adj[1][2], mydict4)
+ assert isinstance(G._adj[1][2][0], mydict5)
+ else:
+ assert isinstance(G._adj[1][2], mydict5)
+
+
+class TestSpecialGraph(_TestGraph):
+ def setup_method(self):
+ _TestGraph.setup_method(self)
+ self.Graph = nx.Graph
+
+
+class TestThinGraph(BaseGraphTester):
+ def setup_method(self):
+ all_edge_dict = {"weight": 1}
+
+ class MyGraph(nx.Graph):
+ def edge_attr_dict_factory(self):
+ return all_edge_dict
+
+ self.Graph = MyGraph
+ # build dict-of-dict-of-dict K3
+ ed1, ed2, ed3 = (all_edge_dict, all_edge_dict, all_edge_dict)
+ self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed1, 2: ed3}, 2: {0: ed2, 1: ed3}}
+ self.k3edges = [(0, 1), (0, 2), (1, 2)]
+ self.k3nodes = [0, 1, 2]
+ self.K3 = self.Graph()
+ self.K3._adj = self.k3adj
+ self.K3._node = {}
+ self.K3._node[0] = {}
+ self.K3._node[1] = {}
+ self.K3._node[2] = {}
+
+
+class TestSpecialDiGraph(_TestDiGraph):
+ def setup_method(self):
+ _TestDiGraph.setup_method(self)
+ self.Graph = nx.DiGraph
+
+
+class TestThinDiGraph(BaseDiGraphTester):
+ def setup_method(self):
+ all_edge_dict = {"weight": 1}
+
+ class MyGraph(nx.DiGraph):
+ def edge_attr_dict_factory(self):
+ return all_edge_dict
+
+ self.Graph = MyGraph
+ # build dict-of-dict-of-dict K3
+ ed1, ed2, ed3 = (all_edge_dict, all_edge_dict, all_edge_dict)
+ ed4, ed5, ed6 = (all_edge_dict, all_edge_dict, all_edge_dict)
+ self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed3, 2: ed4}, 2: {0: ed5, 1: ed6}}
+ self.k3edges = [(0, 1), (0, 2), (1, 2)]
+ self.k3nodes = [0, 1, 2]
+ self.K3 = self.Graph()
+ self.K3._succ = self.k3adj
+ # K3._adj is synced with K3._succ
+ self.K3._pred = {0: {1: ed3, 2: ed5}, 1: {0: ed1, 2: ed6}, 2: {0: ed2, 1: ed4}}
+ self.K3._node = {}
+ self.K3._node[0] = {}
+ self.K3._node[1] = {}
+ self.K3._node[2] = {}
+
+ ed1, ed2 = (all_edge_dict, all_edge_dict)
+ self.P3 = self.Graph()
+ self.P3._succ = {0: {1: ed1}, 1: {2: ed2}, 2: {}}
+ # P3._adj is synced with P3._succ
+ self.P3._pred = {0: {}, 1: {0: ed1}, 2: {1: ed2}}
+ self.P3._node = {}
+ self.P3._node[0] = {}
+ self.P3._node[1] = {}
+ self.P3._node[2] = {}
+
+
+class TestSpecialMultiGraph(_TestMultiGraph):
+ def setup_method(self):
+ _TestMultiGraph.setup_method(self)
+ self.Graph = nx.MultiGraph
+
+
+class TestSpecialMultiDiGraph(_TestMultiDiGraph):
+ def setup_method(self):
+ _TestMultiDiGraph.setup_method(self)
+ self.Graph = nx.MultiDiGraph
diff --git a/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_subgraphviews.py b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_subgraphviews.py
new file mode 100644
index 00000000..73e0fdd2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/classes/tests/test_subgraphviews.py
@@ -0,0 +1,362 @@
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal
+
+
+class TestSubGraphView:
+ gview = staticmethod(nx.subgraph_view)
+ graph = nx.Graph
+ hide_edges_filter = staticmethod(nx.filters.hide_edges)
+ show_edges_filter = staticmethod(nx.filters.show_edges)
+
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, create_using=cls.graph())
+ cls.hide_edges_w_hide_nodes = {(3, 4), (4, 5), (5, 6)}
+
+ def test_hidden_nodes(self):
+ hide_nodes = [4, 5, 111]
+ nodes_gone = nx.filters.hide_nodes(hide_nodes)
+ gview = self.gview
+ G = gview(self.G, filter_node=nodes_gone)
+ assert self.G.nodes - G.nodes == {4, 5}
+ assert self.G.edges - G.edges == self.hide_edges_w_hide_nodes
+ if G.is_directed():
+ assert list(G[3]) == []
+ assert list(G[2]) == [3]
+ else:
+ assert list(G[3]) == [2]
+ assert set(G[2]) == {1, 3}
+ pytest.raises(KeyError, G.__getitem__, 4)
+ pytest.raises(KeyError, G.__getitem__, 112)
+ pytest.raises(KeyError, G.__getitem__, 111)
+ assert G.degree(3) == (3 if G.is_multigraph() else 1)
+ assert G.size() == (7 if G.is_multigraph() else 5)
+
+ def test_hidden_edges(self):
+ hide_edges = [(2, 3), (8, 7), (222, 223)]
+ edges_gone = self.hide_edges_filter(hide_edges)
+ gview = self.gview
+ G = gview(self.G, filter_edge=edges_gone)
+ assert self.G.nodes == G.nodes
+ if G.is_directed():
+ assert self.G.edges - G.edges == {(2, 3)}
+ assert list(G[2]) == []
+ assert list(G.pred[3]) == []
+ assert list(G.pred[2]) == [1]
+ assert G.size() == 7
+ else:
+ assert self.G.edges - G.edges == {(2, 3), (7, 8)}
+ assert list(G[2]) == [1]
+ assert G.size() == 6
+ assert list(G[3]) == [4]
+ pytest.raises(KeyError, G.__getitem__, 221)
+ pytest.raises(KeyError, G.__getitem__, 222)
+ assert G.degree(3) == 1
+
+ def test_shown_node(self):
+ induced_subgraph = nx.filters.show_nodes([2, 3, 111])
+ gview = self.gview
+ G = gview(self.G, filter_node=induced_subgraph)
+ assert set(G.nodes) == {2, 3}
+ if G.is_directed():
+ assert list(G[3]) == []
+ else:
+ assert list(G[3]) == [2]
+ assert list(G[2]) == [3]
+ pytest.raises(KeyError, G.__getitem__, 4)
+ pytest.raises(KeyError, G.__getitem__, 112)
+ pytest.raises(KeyError, G.__getitem__, 111)
+ assert G.degree(3) == (3 if G.is_multigraph() else 1)
+ assert G.size() == (3 if G.is_multigraph() else 1)
+
+ def test_shown_edges(self):
+ show_edges = [(2, 3), (8, 7), (222, 223)]
+ edge_subgraph = self.show_edges_filter(show_edges)
+ G = self.gview(self.G, filter_edge=edge_subgraph)
+ assert self.G.nodes == G.nodes
+ if G.is_directed():
+ assert G.edges == {(2, 3)}
+ assert list(G[3]) == []
+ assert list(G[2]) == [3]
+ assert list(G.pred[3]) == [2]
+ assert list(G.pred[2]) == []
+ assert G.size() == 1
+ else:
+ assert G.edges == {(2, 3), (7, 8)}
+ assert list(G[3]) == [2]
+ assert list(G[2]) == [3]
+ assert G.size() == 2
+ pytest.raises(KeyError, G.__getitem__, 221)
+ pytest.raises(KeyError, G.__getitem__, 222)
+ assert G.degree(3) == 1
+
+
+class TestSubDiGraphView(TestSubGraphView):
+ gview = staticmethod(nx.subgraph_view)
+ graph = nx.DiGraph
+ hide_edges_filter = staticmethod(nx.filters.hide_diedges)
+ show_edges_filter = staticmethod(nx.filters.show_diedges)
+ hide_edges = [(2, 3), (8, 7), (222, 223)]
+ excluded = {(2, 3), (3, 4), (4, 5), (5, 6)}
+
+ def test_inoutedges(self):
+ edges_gone = self.hide_edges_filter(self.hide_edges)
+ hide_nodes = [4, 5, 111]
+ nodes_gone = nx.filters.hide_nodes(hide_nodes)
+ G = self.gview(self.G, filter_node=nodes_gone, filter_edge=edges_gone)
+
+ assert self.G.in_edges - G.in_edges == self.excluded
+ assert self.G.out_edges - G.out_edges == self.excluded
+
+ def test_pred(self):
+ edges_gone = self.hide_edges_filter(self.hide_edges)
+ hide_nodes = [4, 5, 111]
+ nodes_gone = nx.filters.hide_nodes(hide_nodes)
+ G = self.gview(self.G, filter_node=nodes_gone, filter_edge=edges_gone)
+
+ assert list(G.pred[2]) == [1]
+ assert list(G.pred[6]) == []
+
+ def test_inout_degree(self):
+ edges_gone = self.hide_edges_filter(self.hide_edges)
+ hide_nodes = [4, 5, 111]
+ nodes_gone = nx.filters.hide_nodes(hide_nodes)
+ G = self.gview(self.G, filter_node=nodes_gone, filter_edge=edges_gone)
+
+ assert G.degree(2) == 1
+ assert G.out_degree(2) == 0
+ assert G.in_degree(2) == 1
+ assert G.size() == 4
+
+
+# multigraph
+class TestMultiGraphView(TestSubGraphView):
+ gview = staticmethod(nx.subgraph_view)
+ graph = nx.MultiGraph
+ hide_edges_filter = staticmethod(nx.filters.hide_multiedges)
+ show_edges_filter = staticmethod(nx.filters.show_multiedges)
+
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.path_graph(9, create_using=cls.graph())
+ multiedges = {(2, 3, 4), (2, 3, 5)}
+ cls.G.add_edges_from(multiedges)
+ cls.hide_edges_w_hide_nodes = {(3, 4, 0), (4, 5, 0), (5, 6, 0)}
+
+ def test_hidden_edges(self):
+ hide_edges = [(2, 3, 4), (2, 3, 3), (8, 7, 0), (222, 223, 0)]
+ edges_gone = self.hide_edges_filter(hide_edges)
+ G = self.gview(self.G, filter_edge=edges_gone)
+ assert self.G.nodes == G.nodes
+ if G.is_directed():
+ assert self.G.edges - G.edges == {(2, 3, 4)}
+ assert list(G[3]) == [4]
+ assert list(G[2]) == [3]
+ assert list(G.pred[3]) == [2] # only one 2 but two edges
+ assert list(G.pred[2]) == [1]
+ assert G.size() == 9
+ else:
+ assert self.G.edges - G.edges == {(2, 3, 4), (7, 8, 0)}
+ assert list(G[3]) == [2, 4]
+ assert list(G[2]) == [1, 3]
+ assert G.size() == 8
+ assert G.degree(3) == 3
+ pytest.raises(KeyError, G.__getitem__, 221)
+ pytest.raises(KeyError, G.__getitem__, 222)
+
+ def test_shown_edges(self):
+ show_edges = [(2, 3, 4), (2, 3, 3), (8, 7, 0), (222, 223, 0)]
+ edge_subgraph = self.show_edges_filter(show_edges)
+ G = self.gview(self.G, filter_edge=edge_subgraph)
+ assert self.G.nodes == G.nodes
+ if G.is_directed():
+ assert G.edges == {(2, 3, 4)}
+ assert list(G[3]) == []
+ assert list(G.pred[3]) == [2]
+ assert list(G.pred[2]) == []
+ assert G.size() == 1
+ else:
+ assert G.edges == {(2, 3, 4), (7, 8, 0)}
+ assert G.size() == 2
+ assert list(G[3]) == [2]
+ assert G.degree(3) == 1
+ assert list(G[2]) == [3]
+ pytest.raises(KeyError, G.__getitem__, 221)
+ pytest.raises(KeyError, G.__getitem__, 222)
+
+
+# multidigraph
+class TestMultiDiGraphView(TestMultiGraphView, TestSubDiGraphView):
+ gview = staticmethod(nx.subgraph_view)
+ graph = nx.MultiDiGraph
+ hide_edges_filter = staticmethod(nx.filters.hide_multidiedges)
+ show_edges_filter = staticmethod(nx.filters.show_multidiedges)
+ hide_edges = [(2, 3, 0), (8, 7, 0), (222, 223, 0)]
+ excluded = {(2, 3, 0), (3, 4, 0), (4, 5, 0), (5, 6, 0)}
+
+ def test_inout_degree(self):
+ edges_gone = self.hide_edges_filter(self.hide_edges)
+ hide_nodes = [4, 5, 111]
+ nodes_gone = nx.filters.hide_nodes(hide_nodes)
+ G = self.gview(self.G, filter_node=nodes_gone, filter_edge=edges_gone)
+
+ assert G.degree(2) == 3
+ assert G.out_degree(2) == 2
+ assert G.in_degree(2) == 1
+ assert G.size() == 6
+
+
+# induced_subgraph
+class TestInducedSubGraph:
+ @classmethod
+ def setup_class(cls):
+ cls.K3 = G = nx.complete_graph(3)
+ G.graph["foo"] = []
+ G.nodes[0]["foo"] = []
+ G.remove_edge(1, 2)
+ ll = []
+ G.add_edge(1, 2, foo=ll)
+ G.add_edge(2, 1, foo=ll)
+
+ def test_full_graph(self):
+ G = self.K3
+ H = nx.induced_subgraph(G, [0, 1, 2, 5])
+ assert H.name == G.name
+ self.graphs_equal(H, G)
+ self.same_attrdict(H, G)
+
+ def test_partial_subgraph(self):
+ G = self.K3
+ H = nx.induced_subgraph(G, 0)
+ assert dict(H.adj) == {0: {}}
+ assert dict(G.adj) != {0: {}}
+
+ H = nx.induced_subgraph(G, [0, 1])
+ assert dict(H.adj) == {0: {1: {}}, 1: {0: {}}}
+
+ def same_attrdict(self, H, G):
+ old_foo = H[1][2]["foo"]
+ H.edges[1, 2]["foo"] = "baz"
+ assert G.edges == H.edges
+ H.edges[1, 2]["foo"] = old_foo
+ assert G.edges == H.edges
+ old_foo = H.nodes[0]["foo"]
+ H.nodes[0]["foo"] = "baz"
+ assert G.nodes == H.nodes
+ H.nodes[0]["foo"] = old_foo
+ assert G.nodes == H.nodes
+
+ def graphs_equal(self, H, G):
+ assert G._adj == H._adj
+ assert G._node == H._node
+ assert G.graph == H.graph
+ assert G.name == H.name
+ if not G.is_directed() and not H.is_directed():
+ assert H._adj[1][2] is H._adj[2][1]
+ assert G._adj[1][2] is G._adj[2][1]
+ else: # at least one is directed
+ if not G.is_directed():
+ G._pred = G._adj
+ G._succ = G._adj
+ if not H.is_directed():
+ H._pred = H._adj
+ H._succ = H._adj
+ assert G._pred == H._pred
+ assert G._succ == H._succ
+ assert H._succ[1][2] is H._pred[2][1]
+ assert G._succ[1][2] is G._pred[2][1]
+
+
+# edge_subgraph
+class TestEdgeSubGraph:
+ @classmethod
+ def setup_class(cls):
+ # Create a path graph on five nodes.
+ cls.G = G = nx.path_graph(5)
+ # Add some node, edge, and graph attributes.
+ for i in range(5):
+ G.nodes[i]["name"] = f"node{i}"
+ G.edges[0, 1]["name"] = "edge01"
+ G.edges[3, 4]["name"] = "edge34"
+ G.graph["name"] = "graph"
+ # Get the subgraph induced by the first and last edges.
+ cls.H = nx.edge_subgraph(G, [(0, 1), (3, 4)])
+
+ def test_correct_nodes(self):
+ """Tests that the subgraph has the correct nodes."""
+ assert [(0, "node0"), (1, "node1"), (3, "node3"), (4, "node4")] == sorted(
+ self.H.nodes.data("name")
+ )
+
+ def test_correct_edges(self):
+ """Tests that the subgraph has the correct edges."""
+ assert edges_equal(
+ [(0, 1, "edge01"), (3, 4, "edge34")], self.H.edges.data("name")
+ )
+
+ def test_add_node(self):
+ """Tests that adding a node to the original graph does not
+ affect the nodes of the subgraph.
+
+ """
+ self.G.add_node(5)
+ assert [0, 1, 3, 4] == sorted(self.H.nodes)
+ self.G.remove_node(5)
+
+ def test_remove_node(self):
+ """Tests that removing a node in the original graph
+ removes the nodes of the subgraph.
+
+ """
+ self.G.remove_node(0)
+ assert [1, 3, 4] == sorted(self.H.nodes)
+ self.G.add_node(0, name="node0")
+ self.G.add_edge(0, 1, name="edge01")
+
+ def test_node_attr_dict(self):
+ """Tests that the node attribute dictionary of the two graphs is
+ the same object.
+
+ """
+ for v in self.H:
+ assert self.G.nodes[v] == self.H.nodes[v]
+ # Making a change to G should make a change in H and vice versa.
+ self.G.nodes[0]["name"] = "foo"
+ assert self.G.nodes[0] == self.H.nodes[0]
+ self.H.nodes[1]["name"] = "bar"
+ assert self.G.nodes[1] == self.H.nodes[1]
+ # Revert the change, so tests pass with pytest-randomly
+ self.G.nodes[0]["name"] = "node0"
+ self.H.nodes[1]["name"] = "node1"
+
+ def test_edge_attr_dict(self):
+ """Tests that the edge attribute dictionary of the two graphs is
+ the same object.
+
+ """
+ for u, v in self.H.edges():
+ assert self.G.edges[u, v] == self.H.edges[u, v]
+ # Making a change to G should make a change in H and vice versa.
+ self.G.edges[0, 1]["name"] = "foo"
+ assert self.G.edges[0, 1]["name"] == self.H.edges[0, 1]["name"]
+ self.H.edges[3, 4]["name"] = "bar"
+ assert self.G.edges[3, 4]["name"] == self.H.edges[3, 4]["name"]
+ # Revert the change, so tests pass with pytest-randomly
+ self.G.edges[0, 1]["name"] = "edge01"
+ self.H.edges[3, 4]["name"] = "edge34"
+
+ def test_graph_attr_dict(self):
+ """Tests that the graph attribute dictionary of the two graphs
+ is the same object.
+
+ """
+ assert self.G.graph is self.H.graph
+
+ def test_readonly(self):
+ """Tests that the subgraph cannot change the graph structure"""
+ pytest.raises(nx.NetworkXError, self.H.add_node, 5)
+ pytest.raises(nx.NetworkXError, self.H.remove_node, 0)
+ pytest.raises(nx.NetworkXError, self.H.add_edge, 5, 6)
+ pytest.raises(nx.NetworkXError, self.H.remove_edge, 0, 1)