Repository: networkx/networkx

Issue:
Critical NetworkX 2.8.X bug with mutable cached_properties
### Current Behavior

The `nodes()` method of a Graph is decorated with `@cached_property`.<br>
This leads to the assumption that a Graph's `nodes()` method should return a static value.<br>
This assumption is incorrect.

Notably, the `@cached_property` decorator completely breaks `Graph.subgraph()`.<br>
Trace:

Graph.subgraph -> graphviews.subgraph_view -> [this line](https://github.com/networkx/networkx/blob/ead0e65bda59862e329f2e6f1da47919c6b07ca9/networkx/classes/graphviews.py#L149) prevents [this line](https://github.com/networkx/networkx/blob/ead0e65bda59862e329f2e6f1da47919c6b07ca9/networkx/classes/graphviews.py#L152) from functioning correctly

Subgraphs are shown as containing the wrong nodes as a result, until `del G.nodes` is manually run.<br>
This breaks things.

### Expected Behavior

`@cached_property` decorators should not break functionality.

### Steps to Reproduce

- Initialize a Graph, `G`
- Populate `G` with node objects that are complex enough that the following inequality assertion will pass: `newG = nx.freeze(G.__class__()); newG._graph = G; newG.graph = G.graph; assert(newG.nodes()!=G.nodes)`.
- Pick a subset of `G.nodes`, call this `subG_nodes`
- `subgraph = G.subgraph(subG_nodes)`
- In NetworkX 2.7, the nodes of `subgraph` will be a subset of the nodes of `G`. In NetworkX 2.8, the nodes of `subgraph` and the nodes of `G` will be a fully disjoint set.

### Environment
<!--- Please provide details about your local environment -->
Python version: 3.9.10
NetworkX version: 2.8.5 vs 2.7.0


Comments:

Patch:
diff --git a/networkx/classes/graph.py b/networkx/classes/graph.py
index ebbc8b535..47d5f81d9 100644
--- a/networkx/classes/graph.py
+++ b/networkx/classes/graph.py
@@ -41,6 +41,28 @@ class _CachedPropertyResetterAdj:
             del od["adj"]
 
 
+class _CachedPropertyResetterNode:
+    """Data Descriptor class for _node that resets ``nodes`` cached_property when needed
+
+    This assumes that the ``cached_property`` ``G.node`` should be reset whenever
+    ``G._node`` is set to a new value.
+
+    This object sits on a class and ensures that any instance of that
+    class clears its cached property "nodes" whenever the underlying
+    instance attribute "_node" is set to a new object. It only affects
+    the set process of the obj._adj attribute. All get/del operations
+    act as they normally would.
+
+    For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html
+    """
+
+    def __set__(self, obj, value):
+        od = obj.__dict__
+        od["_node"] = value
+        if "nodes" in od:
+            del od["nodes"]
+
+
 class Graph:
     """
     Base class for undirected graphs.
@@ -282,6 +304,7 @@ class Graph:
     """
 
     _adj = _CachedPropertyResetterAdj()
+    _node = _CachedPropertyResetterNode()
 
     node_dict_factory = dict
     node_attr_dict_factory = dict


Test patch:
diff --git a/networkx/algorithms/shortest_paths/unweighted.py b/networkx/algorithms/shortest_paths/unweighted.py
index 5363b24dc..9d1dff5b2 100644
--- a/networkx/algorithms/shortest_paths/unweighted.py
+++ b/networkx/algorithms/shortest_paths/unweighted.py
@@ -460,8 +460,7 @@ def all_pairs_shortest_path(G, cutoff=None):
 
 
 def predecessor(G, source, target=None, cutoff=None, return_seen=None):
-    """Returns dict of predecessors for the path from source to all nodes in G
-
+    """Returns dict of predecessors for the path from source to all nodes in G.
 
     Parameters
     ----------
@@ -477,12 +476,23 @@ def predecessor(G, source, target=None, cutoff=None, return_seen=None):
     cutoff : integer, optional
         Depth to stop the search. Only paths of length <= cutoff are returned.
 
+    return_seen : bool, optional (default=None)
+        Whether to return a dictionary, keyed by node, of the level (number of
+        hops) to reach the node (as seen during breadth-first-search).
 
     Returns
     -------
     pred : dictionary
         Dictionary, keyed by node, of predecessors in the shortest path.
 
+
+    (pred, seen): tuple of dictionaries
+        If `return_seen` argument is set to `True`, then a tuple of dictionaries
+        is returned. The first element is the dictionary, keyed by node, of
+        predecessors in the shortest path. The second element is the dictionary,
+        keyed by node, of the level (number of hops) to reach the node (as seen
+        during breadth-first-search).
+
     Examples
     --------
     >>> G = nx.path_graph(4)
@@ -490,6 +500,9 @@ def predecessor(G, source, target=None, cutoff=None, return_seen=None):
     [0, 1, 2, 3]
     >>> nx.predecessor(G, 0)
     {0: [], 1: [0], 2: [1], 3: [2]}
+    >>> nx.predecessor(G, 0, return_seen=True)
+    ({0: [], 1: [0], 2: [1], 3: [2]}, {0: 0, 1: 1, 2: 2, 3: 3})
+
 
     """
     if source not in G:
diff --git a/networkx/classes/tests/test_graph.py b/networkx/classes/tests/test_graph.py
index ebaa04bbd..2adb159bf 100644
--- a/networkx/classes/tests/test_graph.py
+++ b/networkx/classes/tests/test_graph.py
@@ -178,6 +178,11 @@ class BaseGraphTester:
         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)