about summary refs log tree commit diff
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)