about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/classes/tests
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/networkx/classes/tests
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/classes/tests')
-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
16 files changed, 6874 insertions, 0 deletions
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)