Repository: networkx/networkx

Issue:
Update `all_pairs_lca` docstrings
Just a few potential improvements I noticed while reviewing #5736:

 - [ ] The `Returns` section should actually be a `Yields` as this is a generator. This should also simplify the return description
 - [ ] Add a `Raises` section to indicate when exceptions are raised (graph is undirected, graph isn't DAG, graph is empty, etc.)
 - [ ] Update example to show usage both with and without the `pairs` argument.

Note that these changes should be made to the `naive` fn docstring as well.

Discussion:

Patch:
diff --git a/doc/release/index.rst b/doc/release/index.rst
index 30316b6d5..a0b592be0 100644
--- a/doc/release/index.rst
+++ b/doc/release/index.rst
@@ -15,6 +15,7 @@ period.
    :maxdepth: 2
 
    release_dev
+   release_2.8.5
    release_2.8.4
    release_2.8.3
    release_2.8.2
diff --git a/doc/release/release_2.8.5.rst b/doc/release/release_2.8.5.rst
new file mode 100644
index 000000000..fc1792394
--- /dev/null
+++ b/doc/release/release_2.8.5.rst
@@ -0,0 +1,58 @@
+NetworkX 2.8.5
+==============
+
+Release date: 18 July 2022
+
+Supports Python 3.8, 3.9, and 3.10.
+
+NetworkX is a Python package for the creation, manipulation, and study of the
+structure, dynamics, and functions of complex networks.
+
+For more information, please visit our `website <https://networkx.org/>`_
+and our :ref:`gallery of examples <examples_gallery>`.
+Please send comments and questions to the `networkx-discuss mailing list
+<http://groups.google.com/group/networkx-discuss>`_.
+
+Highlights
+----------
+
+Minor documentation and bug fixes.
+
+Merged PRs
+----------
+
+- Bump release version
+- Check that nodes have "pos" attribute in geometric_edges (#5707)
+- Correct louvain formula, solve infinite loops (#5713)
+- Add more comprehensive tests for pydot (#5792)
+- Compute `is_strongly_connected` lazily (#5793)
+- Compute `is_weakly_connected` lazily (#5795)
+- Updated astar docstring (#5797)
+- Fix typo in bipartite closeness_centrality and thought-o in tests (#5800)
+- Fix pydot colon check node-to-str conversion (#5809)
+- Temporary fix for failing tests w/ scipy1.9. (#5816)
+- Update distance parameter description. (#5819)
+- Fix #5817 (#5822)
+- Attempt to reverse slowdown from hasattr  needed for cached_property (#5836)
+- Update tests in base class and simple rename in convert.py (#5848)
+- Move factory attributes to the class instead of instance. (#5850)
+- Point to the latest URL for the description. (#5852)
+- Gallery example: Morse code alphabet as a prefix tree (#5867)
+- make lazy_import private and remove its internal use (#5878)
+- Run CI against v2.8 branch
+- CI: add explicit path while installing pygraphviz wheels on macOS in GHA (#5805)
+- Deploy docs on v2.8 branch
+
+Contributors
+------------
+
+- Ross Barnowski
+- Shaked Brody
+- Lior
+- Jarrod Millman
+- Tomoya Nishide
+- Dimitrios Papageorgiou
+- Dan Schult
+- Matt Schwennesen
+- Mridul Seth
+- Matus Valo
diff --git a/networkx/__init__.py b/networkx/__init__.py
index a0b46a97c..5b56e346c 100644
--- a/networkx/__init__.py
+++ b/networkx/__init__.py
@@ -12,7 +12,7 @@ __version__ = "3.0b1.dev0"
 
 
 # These are imported in order as listed
-from networkx.lazy_imports import lazy_import
+from networkx.lazy_imports import _lazy_import
 
 from networkx.exception import *
 
diff --git a/networkx/algorithms/lowest_common_ancestors.py b/networkx/algorithms/lowest_common_ancestors.py
index 29592d7c2..a5158a899 100644
--- a/networkx/algorithms/lowest_common_ancestors.py
+++ b/networkx/algorithms/lowest_common_ancestors.py
@@ -28,12 +28,34 @@ def naive_all_pairs_lowest_common_ancestor(G, pairs=None):
         The pairs of nodes of interest.
         If None, will find the LCA of all pairs of nodes.
 
-    Returns
-    -------
-    An iterator over ((node1, node2), lca) where (node1, node2) are
-    the pairs specified and lca is a lowest common ancestor of the pair.
-    Note that for the default of all pairs in G, we consider
-    unordered pairs, e.g. you will not get both (b, a) and (a, b).
+    Yields
+    ------
+    ((node1, node2), lca) : 2-tuple
+        Where lca is least common ancestor of node1 and node2.
+        Note that for the default case, the order of the node pair is not considered,
+        e.g. you will not get both ``(a, b)`` and ``(b, a)``
+
+    Raises
+    ------
+    NetworkXPointlessConcept
+        If `G` is null.
+    NetworkXError
+        If `G` is not a DAG.
+
+    Examples
+    --------
+    The default behavior is to yield the lowest common ancestor for all
+    possible combinations of nodes in `G`, including self-pairings:
+
+    >>> G = nx.DiGraph([(0, 1), (0, 3), (1, 2)])
+    >>> dict(nx.naive_all_pairs_lowest_common_ancestor(G))
+    {(0, 0): 0, (0, 1): 0, (0, 3): 0, (0, 2): 0, (1, 1): 1, (1, 3): 0, (1, 2): 1, (3, 3): 3, (3, 2): 0, (2, 2): 2}
+
+    The pairs argument can be used to limit the output to only the
+    specified node pairings:
+
+    >>> dict(nx.naive_all_pairs_lowest_common_ancestor(G, pairs=[(1, 2), (2, 3)]))
+    {(1, 2): 1, (2, 3): 0}
 
     Notes
     -----
@@ -302,16 +324,32 @@ def all_pairs_lowest_common_ancestor(G, pairs=None):
         The pairs of nodes of interest.
         If None, will find the LCA of all pairs of nodes.
 
-    Returns
-    -------
-    An iterator over ((node1, node2), lca) where (node1, node2) are
-    the pairs specified and lca is a lowest common ancestor of the pair.
-    Note that for the default of all pairs in G, we consider
-    unordered pairs, e.g. you will not get both (b, a) and (a, b).
+    Yields
+    ------
+    ((node1, node2), lca) : 2-tuple
+        Where lca is least common ancestor of node1 and node2.
+        Note that for the default case, the order of the node pair is not considered,
+        e.g. you will not get both ``(a, b)`` and ``(b, a)``
+
+    Raises
+    ------
+    NetworkXPointlessConcept
+        If `G` is null.
+    NetworkXError
+        If `G` is not a DAG.
 
     Examples
     --------
+    The default behavior is to yield the lowest common ancestor for all
+    possible combinations of nodes in `G`, including self-pairings:
+
     >>> G = nx.DiGraph([(0, 1), (0, 3), (1, 2)])
+    >>> dict(nx.all_pairs_lowest_common_ancestor(G))
+    {(2, 2): 2, (1, 1): 1, (2, 1): 1, (1, 3): 0, (2, 3): 0, (3, 3): 3, (0, 0): 0, (1, 0): 0, (2, 0): 0, (3, 0): 0}
+
+    The `pairs` argument can be used to limit the output to only the
+    specified node pairings:
+
     >>> dict(nx.all_pairs_lowest_common_ancestor(G, pairs=[(1, 2), (2, 3)]))
     {(2, 3): 0, (1, 2): 1}
 
diff --git a/networkx/lazy_imports.py b/networkx/lazy_imports.py
index 6b4876c44..49344db8d 100644
--- a/networkx/lazy_imports.py
+++ b/networkx/lazy_imports.py
@@ -5,7 +5,7 @@ import os
 import sys
 import types
 
-__all__ = ["attach", "lazy_import"]
+__all__ = ["attach", "_lazy_import"]
 
 
 def attach(module_name, submodules=None, submod_attrs=None):
@@ -99,9 +99,27 @@ class DelayedImportErrorModule(types.ModuleType):
             )
 
 
-def lazy_import(fullname):
+def _lazy_import(fullname):
     """Return a lazily imported proxy for a module or library.
 
+    Warning
+    -------
+    Importing using this function can currently cause trouble
+    when the user tries to import from a subpackage of a module before
+    the package is fully imported. In particular, this idiom may not work:
+
+      np = lazy_import("numpy")
+      from numpy.lib import recfunctions
+
+    This is due to a difference in the way Python's LazyLoader handles
+    subpackage imports compared to the normal import process. Hopefully
+    we will get Python's LazyLoader to fix this, or find a workaround.
+    In the meantime, this is a potential problem.
+
+    The workaround is to import numpy before importing from the subpackage.
+
+    Notes
+    -----
     We often see the following pattern::
 
       def myfunc():
diff --git a/networkx/utils/misc.py b/networkx/utils/misc.py
index 2ebd76a7c..31189bf2b 100644
--- a/networkx/utils/misc.py
+++ b/networkx/utils/misc.py
@@ -20,8 +20,6 @@ from itertools import chain, tee
 
 import networkx as nx
 
-np = nx.lazy_import("numpy")
-
 __all__ = [
     "flatten",
     "make_list_of_ints",
@@ -109,6 +107,8 @@ def _dict_to_numpy_array2(d, mapping=None):
     with optional mapping.
 
     """
+    import numpy as np
+
     if mapping is None:
         s = set(d.keys())
         for k, v in d.items():
@@ -127,6 +127,8 @@ def _dict_to_numpy_array2(d, mapping=None):
 
 def _dict_to_numpy_array1(d, mapping=None):
     """Convert a dictionary of numbers to a 1d numpy array with optional mapping."""
+    import numpy as np
+
     if mapping is None:
         s = set(d.keys())
         mapping = dict(zip(s, range(len(s))))
@@ -252,6 +254,8 @@ def create_random_state(random_state=None):
         if None or numpy.random, return the global random number generator used
         by numpy.random.
     """
+    import numpy as np
+
     if random_state is None or random_state is np.random:
         return np.random.mtrand._rand
     if isinstance(random_state, np.random.RandomState):
@@ -287,6 +291,8 @@ class PythonRandomInterface:
         return a + (b - a) * self._rng.random()
 
     def randrange(self, a, b=None):
+        import numpy as np
+
         if isinstance(self._rng, np.random.Generator):
             return self._rng.integers(a, b)
         return self._rng.randint(a, b)
@@ -294,6 +300,8 @@ class PythonRandomInterface:
     # NOTE: the numpy implementations of `choice` don't support strings, so
     # this cannot be replaced with self._rng.choice
     def choice(self, seq):
+        import numpy as np
+
         if isinstance(self._rng, np.random.Generator):
             idx = self._rng.integers(0, len(seq))
         else:
@@ -313,6 +321,8 @@ class PythonRandomInterface:
         return self._rng.choice(list(seq), size=(k,), replace=False)
 
     def randint(self, a, b):
+        import numpy as np
+
         if isinstance(self._rng, np.random.Generator):
             return self._rng.integers(a, b + 1)
         return self._rng.randint(a, b + 1)


Test patch:
diff --git a/networkx/tests/test_lazy_imports.py b/networkx/tests/test_lazy_imports.py
index b5feffcd4..fe022c479 100644
--- a/networkx/tests/test_lazy_imports.py
+++ b/networkx/tests/test_lazy_imports.py
@@ -8,8 +8,8 @@ import networkx.lazy_imports as lazy
 
 
 def test_lazy_import_basics():
-    math = lazy.lazy_import("math")
-    anything_not_real = lazy.lazy_import("anything_not_real")
+    math = lazy._lazy_import("math")
+    anything_not_real = lazy._lazy_import("anything_not_real")
 
     # Now test that accessing attributes does what it should
     assert math.sin(math.pi) == pytest.approx(0, 1e-6)
@@ -29,8 +29,8 @@ def test_lazy_import_basics():
 
 
 def test_lazy_import_impact_on_sys_modules():
-    math = lazy.lazy_import("math")
-    anything_not_real = lazy.lazy_import("anything_not_real")
+    math = lazy._lazy_import("math")
+    anything_not_real = lazy._lazy_import("anything_not_real")
 
     assert type(math) == types.ModuleType
     assert "math" in sys.modules
@@ -39,7 +39,7 @@ def test_lazy_import_impact_on_sys_modules():
 
     # only do this if numpy is installed
     np_test = pytest.importorskip("numpy")
-    np = lazy.lazy_import("numpy")
+    np = lazy._lazy_import("numpy")
     assert type(np) == types.ModuleType
     assert "numpy" in sys.modules
 
@@ -50,8 +50,8 @@ def test_lazy_import_impact_on_sys_modules():
 
 
 def test_lazy_import_nonbuiltins():
-    sp = lazy.lazy_import("scipy")
-    np = lazy.lazy_import("numpy")
+    sp = lazy._lazy_import("scipy")
+    np = lazy._lazy_import("numpy")
     if isinstance(sp, lazy.DelayedImportErrorModule):
         try:
             sp.pi