Repository: networkx/networkx

Issue:
Update documentation surrounding `union`, `disjoint_union`, `compose` and `Graph.update`
This is an action item extracted from the discussion in #4208.

It's not always immediately clear to users how the various operators `union`, `disjoint_union`, and `compose` differ and for which use-cases each is appropriate. There is also the `update` method of the graph classes, which simply updates the nodes/edges of a graph in-place with those of another, which is related to the above operations. It would be an improvement to ensure that the docstrings for these functions all include a link to the `update` method.

It may also be worthwhile to update the `operators` module docstring with examples highlighting the use-cases and differences between all of these operations.

Discussion:

Patch:
diff --git a/networkx/algorithms/distance_measures.py b/networkx/algorithms/distance_measures.py
index 5edd39621..4cb08e8cd 100644
--- a/networkx/algorithms/distance_measures.py
+++ b/networkx/algorithms/distance_measures.py
@@ -14,7 +14,7 @@ __all__ = [
 ]
 
 
-def _extrema_bounding(G, compute="diameter"):
+def _extrema_bounding(G, compute="diameter", weight=None):
     """Compute requested extreme distance metric of undirected graph G
 
     Computation is based on smart lower and upper bounds, and in practice
@@ -33,6 +33,26 @@ def _extrema_bounding(G, compute="diameter"):
        "center" for the set of nodes with eccentricity equal to the radius,
        "eccentricities" for the maximum distance from each node to all other nodes in G
 
+    weight : string, function, or None
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        If this is None, every edge has weight/distance/cost 1.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     value : value of the requested metric
@@ -46,25 +66,26 @@ def _extrema_bounding(G, compute="diameter"):
         If the graph consists of multiple components
     ValueError
         If `compute` is not one of "diameter", "radius", "periphery", "center", or "eccentricities".
+
     Notes
     -----
-    This algorithm was proposed in the following papers:
+    This algorithm was proposed in [1]_ and discussed further in [2]_ and [3]_.
 
-    F.W. Takes and W.A. Kosters, Determining the Diameter of Small World
-    Networks, in Proceedings of the 20th ACM International Conference on
-    Information and Knowledge Management (CIKM 2011), pp. 1191-1196, 2011.
-    doi: https://doi.org/10.1145/2063576.2063748
-
-    F.W. Takes and W.A. Kosters, Computing the Eccentricity Distribution of
-    Large Graphs, Algorithms 6(1): 100-118, 2013.
-    doi: https://doi.org/10.3390/a6010100
-
-    M. Borassi, P. Crescenzi, M. Habib, W.A. Kosters, A. Marino and F.W. Takes,
-    Fast Graph Diameter and Radius BFS-Based Computation in (Weakly Connected)
-    Real-World Graphs, Theoretical Computer Science 586: 59-80, 2015.
-    doi: https://doi.org/10.1016/j.tcs.2015.02.033
+    References
+    ----------
+    .. [1] F. W. Takes, W. A. Kosters,
+       "Determining the diameter of small world networks."
+       Proceedings of the 20th ACM international conference on Information and knowledge management, 2011
+       https://dl.acm.org/doi/abs/10.1145/2063576.2063748
+    .. [2] F. W. Takes, W. A. Kosters,
+       "Computing the Eccentricity Distribution of Large Graphs."
+       Algorithms, 2013
+       https://www.mdpi.com/1999-4893/6/1/100
+    .. [3] M. Borassi, P. Crescenzi, M. Habib, W. A. Kosters, A. Marino, F. W. Takes,
+       "Fast diameter and radius BFS-based computation in (weakly connected) real-world graphs: With an application to the six degrees of separation games. "
+       Theoretical Computer Science, 2015
+       https://www.sciencedirect.com/science/article/pii/S0304397515001644
     """
-
     # init variables
     degrees = dict(G.degree())  # start with the highest degree node
     minlowernode = max(degrees, key=degrees.get)
@@ -91,7 +112,8 @@ def _extrema_bounding(G, compute="diameter"):
         high = not high
 
         # get distances from/to current node and derive eccentricity
-        dist = dict(nx.single_source_shortest_path_length(G, current))
+        dist = nx.shortest_path_length(G, source=current, weight=weight)
+
         if len(dist) != N:
             msg = "Cannot compute metric because graph is not connected."
             raise nx.NetworkXError(msg)
@@ -200,20 +222,20 @@ def _extrema_bounding(G, compute="diameter"):
     # return the correct value of the requested metric
     if compute == "diameter":
         return maxlower
-    elif compute == "radius":
+    if compute == "radius":
         return minupper
-    elif compute == "periphery":
+    if compute == "periphery":
         p = [v for v in G if ecc_lower[v] == maxlower]
         return p
-    elif compute == "center":
+    if compute == "center":
         c = [v for v in G if ecc_upper[v] == minupper]
         return c
-    elif compute == "eccentricities":
+    if compute == "eccentricities":
         return ecc_lower
     return None
 
 
-def eccentricity(G, v=None, sp=None):
+def eccentricity(G, v=None, sp=None, weight=None):
     """Returns the eccentricity of nodes in G.
 
     The eccentricity of a node v is the maximum distance from v to
@@ -230,6 +252,26 @@ def eccentricity(G, v=None, sp=None):
     sp : dict of dicts, optional
        All pairs shortest path lengths as a dictionary of dictionaries
 
+    weight : string, function, or None (default=None)
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        If this is None, every edge has weight/distance/cost 1.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     ecc : dictionary
@@ -252,11 +294,11 @@ def eccentricity(G, v=None, sp=None):
     #    else:                      # assume v is a container of nodes
     #        nodes=v
     order = G.order()
-
     e = {}
     for n in G.nbunch_iter(v):
         if sp is None:
-            length = nx.single_source_shortest_path_length(G, n)
+            length = nx.shortest_path_length(G, source=n, weight=weight)
+
             L = len(length)
         else:
             try:
@@ -278,11 +320,10 @@ def eccentricity(G, v=None, sp=None):
 
     if v in G:
         return e[v]  # return single value
-    else:
-        return e
+    return e
 
 
-def diameter(G, e=None, usebounds=False):
+def diameter(G, e=None, usebounds=False, weight=None):
     """Returns the diameter of the graph G.
 
     The diameter is the maximum eccentricity.
@@ -295,6 +336,26 @@ def diameter(G, e=None, usebounds=False):
     e : eccentricity dictionary, optional
       A precomputed dictionary of eccentricities.
 
+    weight : string, function, or None
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        If this is None, every edge has weight/distance/cost 1.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     d : integer
@@ -311,13 +372,13 @@ def diameter(G, e=None, usebounds=False):
     eccentricity
     """
     if usebounds is True and e is None and not G.is_directed():
-        return _extrema_bounding(G, compute="diameter")
+        return _extrema_bounding(G, compute="diameter", weight=weight)
     if e is None:
-        e = eccentricity(G)
+        e = eccentricity(G, weight=weight)
     return max(e.values())
 
 
-def periphery(G, e=None, usebounds=False):
+def periphery(G, e=None, usebounds=False, weight=None):
     """Returns the periphery of the graph G.
 
     The periphery is the set of nodes with eccentricity equal to the diameter.
@@ -330,6 +391,26 @@ def periphery(G, e=None, usebounds=False):
     e : eccentricity dictionary, optional
       A precomputed dictionary of eccentricities.
 
+    weight : string, function, or None
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        If this is None, every edge has weight/distance/cost 1.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     p : list
@@ -347,15 +428,15 @@ def periphery(G, e=None, usebounds=False):
     center
     """
     if usebounds is True and e is None and not G.is_directed():
-        return _extrema_bounding(G, compute="periphery")
+        return _extrema_bounding(G, compute="periphery", weight=weight)
     if e is None:
-        e = eccentricity(G)
+        e = eccentricity(G, weight=weight)
     diameter = max(e.values())
     p = [v for v in e if e[v] == diameter]
     return p
 
 
-def radius(G, e=None, usebounds=False):
+def radius(G, e=None, usebounds=False, weight=None):
     """Returns the radius of the graph G.
 
     The radius is the minimum eccentricity.
@@ -368,6 +449,26 @@ def radius(G, e=None, usebounds=False):
     e : eccentricity dictionary, optional
       A precomputed dictionary of eccentricities.
 
+    weight : string, function, or None
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        If this is None, every edge has weight/distance/cost 1.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     r : integer
@@ -381,13 +482,13 @@ def radius(G, e=None, usebounds=False):
 
     """
     if usebounds is True and e is None and not G.is_directed():
-        return _extrema_bounding(G, compute="radius")
+        return _extrema_bounding(G, compute="radius", weight=weight)
     if e is None:
-        e = eccentricity(G)
+        e = eccentricity(G, weight=weight)
     return min(e.values())
 
 
-def center(G, e=None, usebounds=False):
+def center(G, e=None, usebounds=False, weight=None):
     """Returns the center of the graph G.
 
     The center is the set of nodes with eccentricity equal to radius.
@@ -400,6 +501,26 @@ def center(G, e=None, usebounds=False):
     e : eccentricity dictionary, optional
       A precomputed dictionary of eccentricities.
 
+    weight : string, function, or None
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        If this is None, every edge has weight/distance/cost 1.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     c : list
@@ -417,9 +538,9 @@ def center(G, e=None, usebounds=False):
     periphery
     """
     if usebounds is True and e is None and not G.is_directed():
-        return _extrema_bounding(G, compute="center")
+        return _extrema_bounding(G, compute="center", weight=weight)
     if e is None:
-        e = eccentricity(G)
+        e = eccentricity(G, weight=weight)
     radius = min(e.values())
     p = [v for v in e if e[v] == radius]
     return p
@@ -560,14 +681,23 @@ def resistance_distance(G, nodeA, nodeB, weight=None, invert_weight=True):
 
     Notes
     -----
-    Overview discussion:
-    * https://en.wikipedia.org/wiki/Resistance_distance
-    * http://mathworld.wolfram.com/ResistanceDistance.html
-
-    Additional details:
-    Vaya Sapobi Samui Vos, “Methods for determining the effective resistance,” M.S.,
-    Mathematisch Instituut, Universiteit Leiden, Leiden, Netherlands, 2016
-    Available: `Link to thesis <https://www.universiteitleiden.nl/binaries/content/assets/science/mi/scripties/master/vos_vaya_master.pdf>`_
+    Overviews are provided in [1]_ and [2]_. Additional details on computational
+    methods, proofs of properties, and corresponding MATLAB codes are provided
+    in [3]_.
+
+    References
+    ----------
+    .. [1] Wikipedia
+       "Resistance distance."
+       https://en.wikipedia.org/wiki/Resistance_distance
+    .. [2] E. W. Weisstein
+       "Resistance Distance."
+       MathWorld--A Wolfram Web Resource
+       https://mathworld.wolfram.com/ResistanceDistance.html
+    .. [3] V. S. S. Vos,
+       "Methods for determining the effective resistance."
+       Mestrado, Mathematisch Instituut Universiteit Leiden, 2016
+       https://www.universiteitleiden.nl/binaries/content/assets/science/mi/scripties/master/vos_vaya_master.pdf
     """
     import numpy as np
     import scipy as sp
diff --git a/networkx/algorithms/operators/binary.py b/networkx/algorithms/operators/binary.py
index d0d3f77b4..fc4940c31 100644
--- a/networkx/algorithms/operators/binary.py
+++ b/networkx/algorithms/operators/binary.py
@@ -15,14 +15,16 @@ __all__ = [
 
 
 def union(G, H, rename=(None, None)):
-    """Return the union of graphs G and H.
+    """Combine graphs G and H. The names of nodes must be unique.
+
+    A name collision between the graphs will raise an exception.
+
+    A renaming facility is provided to avoid name collisions.
 
-    Graphs G and H must be disjoint after the renaming takes place,
-    otherwise an exception is raised.
 
     Parameters
     ----------
-    G,H : graph
+    G, H : graph
        A NetworkX graph
 
     rename : tuple , default=(None, None)
@@ -34,14 +36,23 @@ def union(G, H, rename=(None, None)):
     -------
     U : A union graph with the same type as G.
 
+    See Also
+    --------
+    compose
+    :func:`~networkx.Graph.update`
+    disjoint_union
+
     Notes
     -----
-    To force a disjoint union with node relabeling, use
-    disjoint_union(G,H) or convert_node_labels_to integers().
+    To combine graphs that have common nodes, consider compose(G, H)
+    or the method, Graph.update().
 
-    Graph, edge, and node attributes are propagated from G and H
-    to the union graph.  If a graph attribute is present in both
-    G and H the value from H is used.
+    disjoint_union() is similar to union() except that it avoids name clashes
+    by relabeling the nodes with sequential integers.
+
+    Edge and node attributes are propagated from G and H to the union graph.
+    Graph attributes are also propagated, but if they are present in both G and H,
+    then the value from H is used.
 
     Examples
     --------
@@ -53,17 +64,15 @@ def union(G, H, rename=(None, None)):
     >>> U.edges
     EdgeView([('G0', 'G1'), ('G0', 'G2'), ('G1', 'G2'), ('H0', 'H1'), ('H0', 'H3'), ('H1', 'H3'), ('H1', 'H2')])
 
-    See Also
-    --------
-    disjoint_union
+
     """
     return nx.union_all([G, H], rename)
 
 
 def disjoint_union(G, H):
-    """Return the disjoint union of graphs G and H.
+    """Combine graphs G and H. The nodes are assumed to be unique (disjoint).
 
-    This algorithm forces distinct integer node labels.
+    This algorithm automatically relabels nodes to avoid name collisions.
 
     Parameters
     ----------
@@ -74,6 +83,12 @@ def disjoint_union(G, H):
     -------
     U : A union graph with the same type as G.
 
+    See Also
+    --------
+    union
+    compose
+    :func:`~networkx.Graph.update`
+
     Notes
     -----
     A new graph is created, of the same class as G.  It is recommended
@@ -82,9 +97,15 @@ def disjoint_union(G, H):
     The nodes of G are relabeled 0 to len(G)-1, and the nodes of H are
     relabeled len(G) to len(G)+len(H)-1.
 
-    Graph, edge, and node attributes are propagated from G and H
-    to the union graph.  If a graph attribute is present in both
-    G and H the value from H is used.
+    Renumbering forces G and H to be disjoint, so no exception is ever raised for a name collision.
+    To preserve the check for common nodes, use union().
+
+    Edge and node attributes are propagated from G and H to the union graph.
+    Graph attributes are also propagated, but if they are present in both G and H,
+    then the value from H is used.
+
+    To combine graphs that have common nodes, consider compose(G, H)
+    or the method, Graph.update().
 
     Examples
     --------
@@ -262,10 +283,12 @@ def symmetric_difference(G, H):
 
 
 def compose(G, H):
-    """Returns a new graph of G composed with H.
+    """Compose graph G with H by combining nodes and edges into a single graph.
+
+    The node sets and edges sets do not need to be disjoint.
 
-    Composition is the simple union of the node sets and edge sets.
-    The node sets of G and H do not need to be disjoint.
+    Composing preserves the attributes of nodes and edges.
+    Attribute values from H take precedent over attribute values from G.
 
     Parameters
     ----------
@@ -274,17 +297,25 @@ def compose(G, H):
 
     Returns
     -------
-    C: A new graph  with the same type as G
+    C: A new graph with the same type as G
+
+    See Also
+    --------
+    :func:`~networkx.Graph.update`
+    union
+    disjoint_union
 
     Notes
     -----
     It is recommended that G and H be either both directed or both undirected.
-    Attributes from H take precedent over attributes from G.
 
     For MultiGraphs, the edges are identified by incident nodes AND edge-key.
     This can cause surprises (i.e., edge `(1, 2)` may or may not be the same
     in two graphs) if you use MultiGraph without keeping track of edge keys.
 
+    If combining the attributes of common nodes is not desired, consider union(),
+    which raises an exception for name collisions.
+
     Examples
     --------
     >>> G = nx.Graph([(0, 1), (0, 2)])
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
diff --git a/networkx/generators/lattice.py b/networkx/generators/lattice.py
index 47f12922a..38c4edf1e 100644
--- a/networkx/generators/lattice.py
+++ b/networkx/generators/lattice.py
@@ -295,7 +295,7 @@ def hexagonal_lattice_graph(
 
     periodic : bool
         Whether to make a periodic grid by joining the boundary vertices.
-        For this to work `n` must be odd and both `n > 1` and `m > 1`.
+        For this to work `n` must be even and both `n > 1` and `m > 1`.
         The periodic connections create another row and column of hexagons
         so these graphs have fewer nodes as boundary nodes are identified.
 
diff --git a/networkx/relabel.py b/networkx/relabel.py
index 35e71536a..65297d573 100644
--- a/networkx/relabel.py
+++ b/networkx/relabel.py
@@ -114,9 +114,13 @@ def relabel_nodes(G, mapping, copy=True):
     --------
     convert_node_labels_to_integers
     """
-    # you can pass a function f(old_label)->new_label
+    # you can pass a function f(old_label) -> new_label
+    # or a class e.g. str(old_label) -> new_label
     # but we'll just make a dictionary here regardless
-    if not hasattr(mapping, "__getitem__"):
+    # To allow classes, we check if __getitem__ is a bound method using __self__
+    if not (
+        hasattr(mapping, "__getitem__") and hasattr(mapping.__getitem__, "__self__")
+    ):
         m = {n: mapping(n) for n in G}
     else:
         m = mapping


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/algorithms/tests/test_distance_measures.py b/networkx/algorithms/tests/test_distance_measures.py
index 72f23fc50..a5066f662 100644
--- a/networkx/algorithms/tests/test_distance_measures.py
+++ b/networkx/algorithms/tests/test_distance_measures.py
@@ -97,6 +97,228 @@ class TestDistance:
             nx.eccentricity(DG)
 
 
+class TestWeightedDistance:
+    def setup_method(self):
+        G = nx.Graph()
+        G.add_edge(0, 1, weight=0.6, cost=0.6, high_cost=6)
+        G.add_edge(0, 2, weight=0.2, cost=0.2, high_cost=2)
+        G.add_edge(2, 3, weight=0.1, cost=0.1, high_cost=1)
+        G.add_edge(2, 4, weight=0.7, cost=0.7, high_cost=7)
+        G.add_edge(2, 5, weight=0.9, cost=0.9, high_cost=9)
+        G.add_edge(1, 5, weight=0.3, cost=0.3, high_cost=3)
+        self.G = G
+        self.weight_fn = lambda v, u, e: 2
+
+    def test_eccentricity_weight_None(self):
+        assert nx.eccentricity(self.G, 1, weight=None) == 3
+        e = nx.eccentricity(self.G, weight=None)
+        assert e[1] == 3
+
+        e = nx.eccentricity(self.G, v=1, weight=None)
+        assert e == 3
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1], weight=None)
+        assert e[1] == 3
+        e = nx.eccentricity(self.G, v=[1, 2], weight=None)
+        assert e[1] == 3
+
+    def test_eccentricity_weight_attr(self):
+        assert nx.eccentricity(self.G, 1, weight="weight") == 1.5
+        e = nx.eccentricity(self.G, weight="weight")
+        assert (
+            e
+            == nx.eccentricity(self.G, weight="cost")
+            != nx.eccentricity(self.G, weight="high_cost")
+        )
+        assert e[1] == 1.5
+
+        e = nx.eccentricity(self.G, v=1, weight="weight")
+        assert e == 1.5
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1], weight="weight")
+        assert e[1] == 1.5
+        e = nx.eccentricity(self.G, v=[1, 2], weight="weight")
+        assert e[1] == 1.5
+
+    def test_eccentricity_weight_fn(self):
+        assert nx.eccentricity(self.G, 1, weight=self.weight_fn) == 6
+        e = nx.eccentricity(self.G, weight=self.weight_fn)
+        assert e[1] == 6
+
+        e = nx.eccentricity(self.G, v=1, weight=self.weight_fn)
+        assert e == 6
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1], weight=self.weight_fn)
+        assert e[1] == 6
+        e = nx.eccentricity(self.G, v=[1, 2], weight=self.weight_fn)
+        assert e[1] == 6
+
+    def test_diameter_weight_None(self):
+        assert nx.diameter(self.G, weight=None) == 3
+
+    def test_diameter_weight_attr(self):
+        assert (
+            nx.diameter(self.G, weight="weight")
+            == nx.diameter(self.G, weight="cost")
+            == 1.6
+            != nx.diameter(self.G, weight="high_cost")
+        )
+
+    def test_diameter_weight_fn(self):
+        assert nx.diameter(self.G, weight=self.weight_fn) == 6
+
+    def test_radius_weight_None(self):
+        assert pytest.approx(nx.radius(self.G, weight=None)) == 2
+
+    def test_radius_weight_attr(self):
+        assert (
+            pytest.approx(nx.radius(self.G, weight="weight"))
+            == pytest.approx(nx.radius(self.G, weight="cost"))
+            == 0.9
+            != nx.radius(self.G, weight="high_cost")
+        )
+
+    def test_radius_weight_fn(self):
+        assert nx.radius(self.G, weight=self.weight_fn) == 4
+
+    def test_periphery_weight_None(self):
+        for v in set(nx.periphery(self.G, weight=None)):
+            assert nx.eccentricity(self.G, v, weight=None) == nx.diameter(
+                self.G, weight=None
+            )
+
+    def test_periphery_weight_attr(self):
+        periphery = set(nx.periphery(self.G, weight="weight"))
+        assert (
+            periphery
+            == set(nx.periphery(self.G, weight="cost"))
+            == set(nx.periphery(self.G, weight="high_cost"))
+        )
+        for v in periphery:
+            assert (
+                nx.eccentricity(self.G, v, weight="high_cost")
+                != nx.eccentricity(self.G, v, weight="weight")
+                == nx.eccentricity(self.G, v, weight="cost")
+                == nx.diameter(self.G, weight="weight")
+                == nx.diameter(self.G, weight="cost")
+                != nx.diameter(self.G, weight="high_cost")
+            )
+            assert nx.eccentricity(self.G, v, weight="high_cost") == nx.diameter(
+                self.G, weight="high_cost"
+            )
+
+    def test_periphery_weight_fn(self):
+        for v in set(nx.periphery(self.G, weight=self.weight_fn)):
+            assert nx.eccentricity(self.G, v, weight=self.weight_fn) == nx.diameter(
+                self.G, weight=self.weight_fn
+            )
+
+    def test_center_weight_None(self):
+        for v in set(nx.center(self.G, weight=None)):
+            assert pytest.approx(nx.eccentricity(self.G, v, weight=None)) == nx.radius(
+                self.G, weight=None
+            )
+
+    def test_center_weight_attr(self):
+        center = set(nx.center(self.G, weight="weight"))
+        assert (
+            center
+            == set(nx.center(self.G, weight="cost"))
+            != set(nx.center(self.G, weight="high_cost"))
+        )
+        for v in center:
+            assert (
+                nx.eccentricity(self.G, v, weight="high_cost")
+                != pytest.approx(nx.eccentricity(self.G, v, weight="weight"))
+                == pytest.approx(nx.eccentricity(self.G, v, weight="cost"))
+                == nx.radius(self.G, weight="weight")
+                == nx.radius(self.G, weight="cost")
+                != nx.radius(self.G, weight="high_cost")
+            )
+            assert nx.eccentricity(self.G, v, weight="high_cost") == nx.radius(
+                self.G, weight="high_cost"
+            )
+
+    def test_center_weight_fn(self):
+        for v in set(nx.center(self.G, weight=self.weight_fn)):
+            assert nx.eccentricity(self.G, v, weight=self.weight_fn) == nx.radius(
+                self.G, weight=self.weight_fn
+            )
+
+    def test_bound_diameter_weight_None(self):
+        assert nx.diameter(self.G, usebounds=True, weight=None) == 3
+
+    def test_bound_diameter_weight_attr(self):
+        assert (
+            nx.diameter(self.G, usebounds=True, weight="high_cost")
+            != nx.diameter(self.G, usebounds=True, weight="weight")
+            == nx.diameter(self.G, usebounds=True, weight="cost")
+            == 1.6
+            != nx.diameter(self.G, usebounds=True, weight="high_cost")
+        )
+        assert nx.diameter(self.G, usebounds=True, weight="high_cost") == nx.diameter(
+            self.G, usebounds=True, weight="high_cost"
+        )
+
+    def test_bound_diameter_weight_fn(self):
+        assert nx.diameter(self.G, usebounds=True, weight=self.weight_fn) == 6
+
+    def test_bound_radius_weight_None(self):
+        assert pytest.approx(nx.radius(self.G, usebounds=True, weight=None)) == 2
+
+    def test_bound_radius_weight_attr(self):
+        assert (
+            nx.radius(self.G, usebounds=True, weight="high_cost")
+            != pytest.approx(nx.radius(self.G, usebounds=True, weight="weight"))
+            == pytest.approx(nx.radius(self.G, usebounds=True, weight="cost"))
+            == 0.9
+            != nx.radius(self.G, usebounds=True, weight="high_cost")
+        )
+        assert nx.radius(self.G, usebounds=True, weight="high_cost") == nx.radius(
+            self.G, usebounds=True, weight="high_cost"
+        )
+
+    def test_bound_radius_weight_fn(self):
+        assert nx.radius(self.G, usebounds=True, weight=self.weight_fn) == 4
+
+    def test_bound_periphery_weight_None(self):
+        result = {1, 3, 4}
+        assert set(nx.periphery(self.G, usebounds=True, weight=None)) == result
+
+    def test_bound_periphery_weight_attr(self):
+        result = {4, 5}
+        assert (
+            set(nx.periphery(self.G, usebounds=True, weight="weight"))
+            == set(nx.periphery(self.G, usebounds=True, weight="cost"))
+            == result
+        )
+
+    def test_bound_periphery_weight_fn(self):
+        result = {1, 3, 4}
+        assert (
+            set(nx.periphery(self.G, usebounds=True, weight=self.weight_fn)) == result
+        )
+
+    def test_bound_center_weight_None(self):
+        result = {0, 2, 5}
+        assert set(nx.center(self.G, usebounds=True, weight=None)) == result
+
+    def test_bound_center_weight_attr(self):
+        result = {0}
+        assert (
+            set(nx.center(self.G, usebounds=True, weight="weight"))
+            == set(nx.center(self.G, usebounds=True, weight="cost"))
+            == result
+        )
+
+    def test_bound_center_weight_fn(self):
+        result = {0, 2, 5}
+        assert set(nx.center(self.G, usebounds=True, weight=self.weight_fn)) == result
+
+
 class TestResistanceDistance:
     @classmethod
     def setup_class(cls):
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)
diff --git a/networkx/tests/test_relabel.py b/networkx/tests/test_relabel.py
index 46b74482b..9a9db76eb 100644
--- a/networkx/tests/test_relabel.py
+++ b/networkx/tests/test_relabel.py
@@ -106,6 +106,12 @@ class TestRelabel:
         H = nx.relabel_nodes(G, mapping)
         assert nodes_equal(H.nodes(), [65, 66, 67, 68])
 
+    def test_relabel_nodes_classes(self):
+        G = nx.empty_graph()
+        G.add_edges_from([(0, 1), (0, 2), (1, 2), (2, 3)])
+        H = nx.relabel_nodes(G, str)
+        assert nodes_equal(H.nodes, ["0", "1", "2", "3"])
+
     def test_relabel_nodes_graph(self):
         G = nx.Graph([("A", "B"), ("A", "C"), ("B", "C"), ("C", "D")])
         mapping = {"A": "aardvark", "B": "bear", "C": "cat", "D": "dog"}