Repository: networkx/networkx

Issue:
Add more useful ```__str__``` function for Graph objects
Currently the ```__str___``` function prints the ```G.name``` property which is ```G.graph["name"]```.
That is pretty rarely useful in my experience (often a blank string).

There is useful information in nx.info(G) and much less of it than there once was. Perhaps it would be reasonable to look at making something like ```nx.info(G)``` show for ```__str__```.  Are there reasons to avoid doing this? What implications does it have for ```__repr__```?  Issue #3956 requested a useful ```__repr__``` and so is related to this.

Discussion:
pep8speaks: Hello @rossbar! Thanks for opening this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:





* In the file [`networkx/classes/tests/test_graph.py`](https://github.com/networkx/networkx/blob/8a81a3369f51ad0d9d310b729c7fd39f9cd71243/networkx/classes/tests/test_graph.py):

> [Line 195:80](https://github.com/networkx/networkx/blob/8a81a3369f51ad0d9d310b729c7fd39f9cd71243/networkx/classes/tests/test_graph.py#L195): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (83 > 79 characters)


AndrewEckart: Haha beat me to the punch! Just wrote nearly the exact same thing.
rossbar: > Haha beat me to the punch! Just wrote nearly the exact same thing.

Sorry, I should've pinged you!

> I think people working with many large graphs might be a little surprised when str(G) takes linear time, but hopefully this is a very small subset of users.

Agreed, though in terms of the challenges of working with large graphs, this is probably a pretty low-priority concern

> In any case it sounds like keeping track would require some type of distributed counter and significant engineering.

There are some minor changes that may improve this, but generally I agree. The improvements would come from changes to the underlying methods, which would better fit in separate PRs.
rossbar: FWIW the appveyor failure is unrelated (timeout when trying to clone) but I can't figure out how to relaunch the failed job. We can always close/reopen to retrigger all the CI processes, but that seems wasteful
MridulS: @rossbar `git commit --amend --no-edit && git push -f` my not so pretty way of re-trigging builds.
dschult: On my Appveyor page when I look at the list of all jobs run, there are 3 options in blue at the top right. The right one ("Re-run Incomplete") rebuilds only failed or cancelled jobs.  I selected the middle one ("Re-build PR") and it restarted all of them. *oops*.  Ah well...     At least its better than touching the PR branch to trigger the CI.
dschult: Is it better to have ```__str__``` call ```nx.info``` or to have ```nz.info``` call ```__str__```?
Two criteria I can think of: which is more efficient (shouldn't matter) and where will people look for the code (they'd probably look for ```__str__``` before looking for ```nx.info```... Are there other reasons/criteria?

Patch:
diff --git a/doc/developer/deprecations.rst b/doc/developer/deprecations.rst
index f501fa631..9d26d74f6 100644
--- a/doc/developer/deprecations.rst
+++ b/doc/developer/deprecations.rst
@@ -55,3 +55,4 @@ Version 3.0
 * In ``algorithms/centrality/betweenness.py`` remove ``edge_betweeness``.
 * In ``algorithms/community_modularity_max.py`` remove old name ``_naive_greedy_modularity_communities``.
 * In ``linalg/algebraicconnectivity.py`` remove ``_CholeskySolver`` and related code.
+* In ``convert_matrix.py`` remove ``to_numpy_matrix`` and ``from_numpy_matrix``.
diff --git a/doc/developer/nxeps/nxep-0002.rst b/doc/developer/nxeps/nxep-0002.rst
index 834e9ee0a..2fd77b811 100644
--- a/doc/developer/nxeps/nxep-0002.rst
+++ b/doc/developer/nxeps/nxep-0002.rst
@@ -1,3 +1,5 @@
+.. _NXEP2:
+
 ==================================
 NXEP 2 — API design of view slices
 ==================================
diff --git a/doc/developer/roadmap.rst b/doc/developer/roadmap.rst
index 295b8a0da..f0f1b1afd 100644
--- a/doc/developer/roadmap.rst
+++ b/doc/developer/roadmap.rst
@@ -2,3 +2,112 @@
 Roadmap
 =======
 
+The roadmap is intended for larger, fundamental changes to
+the project that are likely to take months or years of developer time.
+Smaller-scoped items will continue to be tracked on our issue tracker.
+
+The scope of these improvements means that these changes may be
+controversial, are likely to involve significant discussion
+among the core development team, and may require the creation
+of one or more :ref:`nxep`.
+
+Installation
+------------
+
+We aim to make NetworkX as easy to install as possible.
+Some of our dependencies (e.g., graphviz and gdal) can be tricky to install.
+Other of our dependencies are easy to install on the CPython platform, but
+may be more involved on other platforms such as PyPy.
+Addressing these installation issues may involve working with the external projects.
+
+Sustainability
+--------------
+
+We aim to reduce barriers to contribution, speed up pull request (PR) review,
+onboard new maintainers, and attract new developers to ensure the long-term
+sustainability of NetworkX.
+
+This includes:
+
+- improving continuous integration
+- making code base more approachable
+- creating new pathways beyond volunteer effort
+- growing maintainers and leadership
+- increasing diversity of developer community
+
+Performance
+-----------
+
+Speed improvements, lower memory usage, and the ability to parallelize
+algorithms are beneficial to most science domains and use cases.
+
+A first step may include implementing a benchmarking system using something
+like airspeed velocity (https://asv.readthedocs.io/en/stable/).
+It may also include review existing comparisons between NetworkX
+and other packages.
+
+Individual functions can be optimized for performance and memory use.
+We are also interested in exploring new technologies to accelerate
+code and reduce memory use.  Before adopting any new technologies
+we will need to careful consider its impact on code readability
+and difficulty of building and installating NetworkX.
+For more information, see our :ref:`mission_and_values`.
+
+Many functions can be trivially parallelized.
+But, we need to decide on an API and perhaps implement some
+helper code to make it consistent.
+
+Documentation
+-------------
+
+We’d like to improve the content, structure, and presentation of the NetworkX
+documentation. Some specific goals include:
+
+- longer gallery examples
+- domain-specific documentation (NetworkX for Geneticists,
+  NetworkX for Neuroscientists, etc.)
+- examples of how to use NetworkX with other packages
+
+Linear Algebra
+--------------
+
+We would like to improve our linear algebra based algorithms.
+The code is old and needs review and refactoring.
+This would include investigating SciPy's csgraph.
+It would also include deciding how to handle algorithms that
+have multiple implementations (e.g., some algorithms are implemented in Python,
+NumPy, and SciPy).
+
+NumPy has split its API from its execution engine with ``__array_function__`` and
+``__array_ufunc__``. This will enable parts of NumPy to accept distributed arrays
+(e.g. dask.array.Array) and GPU arrays (e.g. cupy.ndarray) that implement the
+ndarray interface. At the moment it is not yet clear which algorithms will work
+out of the box, and if there are significant performance gains when they do.
+
+Interoperability
+----------------
+
+We'd like to improve interoperability with the rest of the scientific Python
+ecosystem.
+This includes projects we depend on (e.g., NumPy, SciPy, Pandas, Matplotlib)
+as well as ones we don't (e.g., Geopandas).
+
+For example, we would also like to be able to seamlessly exchange graphs with
+other network analysis software.
+Another way to integrate with other scientific python ecosystem tools is to
+take on features from the other tools that are useful. And we should develop
+tools to ease use of NetworkX from within these other tools.
+Additional examples of interoperability improvements may include providing a more
+pandas-like interface for the ```__getitem__``` dunder function of node and
+edge views (:ref:`NXEP2`).
+Also developing a universal method to represent a graph as a single sequence of
+```nodes_and_edges``` objects that allow attribute dicts, nodes and edges as
+`discussed for graph generators
+<https://github.com/networkx/networkx/issues/3036>`_.
+
+Visualization
+-------------
+
+Visualization is not a focus on NetworkX, but it is a major feature for
+many users.
+We need to enhance the drawing tools for NetworkX.
diff --git a/doc/release/release_dev.rst b/doc/release/release_dev.rst
index 8e038434a..569e07d59 100644
--- a/doc/release/release_dev.rst
+++ b/doc/release/release_dev.rst
@@ -36,6 +36,9 @@ API Changes
 Deprecations
 ------------
 
+- [`#4238 <https://github.com/networkx/networkx/pull/4238>`_]
+  Deprecate `to_numpy_matrix` and `from_numpy_matrix`.
+
 Contributors to this release
 ----------------------------
 
diff --git a/networkx/classes/function.py b/networkx/classes/function.py
index d17e22f60..700cac158 100644
--- a/networkx/classes/function.py
+++ b/networkx/classes/function.py
@@ -576,23 +576,14 @@ def info(G, n=None):
 
     """
     if n is None:
-        n_nodes = G.number_of_nodes()
-        n_edges = G.number_of_edges()
-        return "".join(
-            [
-                type(G).__name__,
-                f" named '{G.name}'" if G.name else "",
-                f" with {n_nodes} nodes and {n_edges} edges",
-            ]
-        )
-    else:
-        if n not in G:
-            raise nx.NetworkXError(f"node {n} not in graph")
-        info = ""  # append this all to a string
-        info += f"Node {n} has the following properties:\n"
-        info += f"Degree: {G.degree(n)}\n"
-        info += "Neighbors: "
-        info += " ".join(str(nbr) for nbr in G.neighbors(n))
+        return str(G)
+    if n not in G:
+        raise nx.NetworkXError(f"node {n} not in graph")
+    info = ""  # append this all to a string
+    info += f"Node {n} has the following properties:\n"
+    info += f"Degree: {G.degree(n)}\n"
+    info += "Neighbors: "
+    info += " ".join(str(nbr) for nbr in G.neighbors(n))
     return info
 
 
diff --git a/networkx/classes/graph.py b/networkx/classes/graph.py
index ae34047bc..8629ee808 100644
--- a/networkx/classes/graph.py
+++ b/networkx/classes/graph.py
@@ -370,20 +370,31 @@ class Graph:
         self.graph["name"] = s
 
     def __str__(self):
-        """Returns the graph name.
+        """Returns a short summary of the graph.
 
         Returns
         -------
-        name : string
-            The name of the graph.
+        info : string
+            Graph information as provided by `nx.info`
 
         Examples
         --------
         >>> G = nx.Graph(name="foo")
         >>> str(G)
-        'foo'
+        "Graph named 'foo' with 0 nodes and 0 edges"
+
+        >>> G = nx.path_graph(3)
+        >>> str(G)
+        'Graph with 3 nodes and 2 edges'
+
         """
-        return self.name
+        return "".join(
+            [
+                type(self).__name__,
+                f" named '{self.name}'" if self.name else "",
+                f" with {self.number_of_nodes()} nodes and {self.number_of_edges()} edges",
+            ]
+        )
 
     def __iter__(self):
         """Iterate over the nodes. Use: 'for n in G'.
diff --git a/networkx/convert_matrix.py b/networkx/convert_matrix.py
index a334c6557..9c709b975 100644
--- a/networkx/convert_matrix.py
+++ b/networkx/convert_matrix.py
@@ -23,6 +23,7 @@ nx_agraph, nx_pydot
 """
 
 import itertools
+import warnings
 import networkx as nx
 from networkx.utils import not_implemented_for
 
@@ -487,7 +488,7 @@ def to_numpy_matrix(
 
     See Also
     --------
-    to_numpy_recarray, from_numpy_matrix
+    to_numpy_recarray
 
     Notes
     -----
@@ -534,6 +535,14 @@ def to_numpy_matrix(
             [0., 0., 4.]])
 
     """
+    warnings.warn(
+        (
+            "to_numpy_matrix is deprecated and will be removed in NetworkX 3.0.\n"
+            "Use to_numpy_array instead, e.g. np.asmatrix(to_numpy_array(G, **kwargs))"
+        ),
+        DeprecationWarning,
+    )
+
     import numpy as np
 
     A = to_numpy_array(
@@ -592,7 +601,7 @@ def from_numpy_matrix(A, parallel_edges=False, create_using=None):
 
     See Also
     --------
-    to_numpy_matrix, to_numpy_recarray
+    to_numpy_recarray
 
     Examples
     --------
@@ -634,81 +643,14 @@ def from_numpy_matrix(A, parallel_edges=False, create_using=None):
     1.0
 
     """
-    # This should never fail if you have created a numpy matrix with numpy...
-    import numpy as np
-
-    kind_to_python_type = {
-        "f": float,
-        "i": int,
-        "u": int,
-        "b": bool,
-        "c": complex,
-        "S": str,
-        "V": "void",
-    }
-    kind_to_python_type["U"] = str
-    G = nx.empty_graph(0, create_using)
-    n, m = A.shape
-    if n != m:
-        raise nx.NetworkXError(f"Adjacency matrix not square: nx,ny={A.shape}")
-    dt = A.dtype
-    try:
-        python_type = kind_to_python_type[dt.kind]
-    except Exception as e:
-        raise TypeError(f"Unknown numpy data type: {dt}") from e
-
-    # Make sure we get even the isolated nodes of the graph.
-    G.add_nodes_from(range(n))
-    # Get a list of all the entries in the matrix with nonzero entries. These
-    # coordinates become edges in the graph. (convert to int from np.int64)
-    edges = ((int(e[0]), int(e[1])) for e in zip(*np.asarray(A).nonzero()))
-    # handle numpy constructed data type
-    if python_type == "void":
-        # Sort the fields by their offset, then by dtype, then by name.
-        fields = sorted(
-            (offset, dtype, name) for name, (dtype, offset) in A.dtype.fields.items()
-        )
-        triples = (
-            (
-                u,
-                v,
-                {
-                    name: kind_to_python_type[dtype.kind](val)
-                    for (_, dtype, name), val in zip(fields, A[u, v])
-                },
-            )
-            for u, v in edges
-        )
-    # If the entries in the adjacency matrix are integers, the graph is a
-    # multigraph, and parallel_edges is True, then create parallel edges, each
-    # with weight 1, for each entry in the adjacency matrix. Otherwise, create
-    # one edge for each positive entry in the adjacency matrix and set the
-    # weight of that edge to be the entry in the matrix.
-    elif python_type is int and G.is_multigraph() and parallel_edges:
-        chain = itertools.chain.from_iterable
-        # The following line is equivalent to:
-        #
-        #     for (u, v) in edges:
-        #         for d in range(A[u, v]):
-        #             G.add_edge(u, v, weight=1)
-        #
-        triples = chain(
-            ((u, v, {"weight": 1}) for d in range(A[u, v])) for (u, v) in edges
-        )
-    else:  # basic data type
-        triples = ((u, v, dict(weight=python_type(A[u, v]))) for u, v in edges)
-    # If we are creating an undirected multigraph, only add the edges from the
-    # upper triangle of the matrix. Otherwise, add all the edges. This relies
-    # on the fact that the vertices created in the
-    # `_generated_weighted_edges()` function are actually the row/column
-    # indices for the matrix `A`.
-    #
-    # Without this check, we run into a problem where each edge is added twice
-    # when `G.add_edges_from()` is invoked below.
-    if G.is_multigraph() and not G.is_directed():
-        triples = ((u, v, d) for u, v, d in triples if u <= v)
-    G.add_edges_from(triples)
-    return G
+    warnings.warn(
+        (
+            "from_numpy_matrix is deprecated and will be removed in NetworkX 3.0.\n"
+            "Use from_numpy_array instead, e.g. from_numpy_array(A, **kwargs)"
+        ),
+        DeprecationWarning,
+    )
+    return from_numpy_array(A, parallel_edges=parallel_edges, create_using=create_using)
 
 
 @not_implemented_for("multigraph")
@@ -1276,13 +1218,13 @@ def to_numpy_array(
 
 
 def from_numpy_array(A, parallel_edges=False, create_using=None):
-    """Returns a graph from NumPy array.
+    """Returns a graph from a 2D NumPy array.
 
-    The NumPy array is interpreted as an adjacency matrix for the graph.
+    The 2D NumPy array is interpreted as an adjacency matrix for the graph.
 
     Parameters
     ----------
-    A : NumPy ndarray
+    A : a 2D numpy.ndarray
         An adjacency matrix representation of a graph
 
     parallel_edges : Boolean
@@ -1328,8 +1270,7 @@ def from_numpy_array(A, parallel_edges=False, create_using=None):
     >>> A = np.array([[1, 1], [2, 1]])
     >>> G = nx.from_numpy_array(A)
     >>> G.edges(data=True)
-    EdgeDataView([(0, 0, {'weight': 1}), (0, 1, {'weight': 2}), \
-(1, 1, {'weight': 1})])
+    EdgeDataView([(0, 0, {'weight': 1}), (0, 1, {'weight': 2}), (1, 1, {'weight': 1})])
 
     If `create_using` indicates a multigraph and the array has only integer
     entries and `parallel_edges` is False, then the entries will be treated
@@ -1363,6 +1304,77 @@ def from_numpy_array(A, parallel_edges=False, create_using=None):
     1.0
 
     """
-    return from_numpy_matrix(
-        A, parallel_edges=parallel_edges, create_using=create_using
-    )
+    kind_to_python_type = {
+        "f": float,
+        "i": int,
+        "u": int,
+        "b": bool,
+        "c": complex,
+        "S": str,
+        "U": str,
+        "V": "void",
+    }
+    G = nx.empty_graph(0, create_using)
+    if A.ndim != 2:
+        raise nx.NetworkXError(f"Input array must be 2D, not {A.ndim}")
+    n, m = A.shape
+    if n != m:
+        raise nx.NetworkXError(f"Adjacency matrix not square: nx,ny={A.shape}")
+    dt = A.dtype
+    try:
+        python_type = kind_to_python_type[dt.kind]
+    except Exception as e:
+        raise TypeError(f"Unknown numpy data type: {dt}") from e
+
+    # Make sure we get even the isolated nodes of the graph.
+    G.add_nodes_from(range(n))
+    # Get a list of all the entries in the array with nonzero entries. These
+    # coordinates become edges in the graph. (convert to int from np.int64)
+    edges = ((int(e[0]), int(e[1])) for e in zip(*A.nonzero()))
+    # handle numpy constructed data type
+    if python_type == "void":
+        # Sort the fields by their offset, then by dtype, then by name.
+        fields = sorted(
+            (offset, dtype, name) for name, (dtype, offset) in A.dtype.fields.items()
+        )
+        triples = (
+            (
+                u,
+                v,
+                {
+                    name: kind_to_python_type[dtype.kind](val)
+                    for (_, dtype, name), val in zip(fields, A[u, v])
+                },
+            )
+            for u, v in edges
+        )
+    # If the entries in the adjacency matrix are integers, the graph is a
+    # multigraph, and parallel_edges is True, then create parallel edges, each
+    # with weight 1, for each entry in the adjacency matrix. Otherwise, create
+    # one edge for each positive entry in the adjacency matrix and set the
+    # weight of that edge to be the entry in the matrix.
+    elif python_type is int and G.is_multigraph() and parallel_edges:
+        chain = itertools.chain.from_iterable
+        # The following line is equivalent to:
+        #
+        #     for (u, v) in edges:
+        #         for d in range(A[u, v]):
+        #             G.add_edge(u, v, weight=1)
+        #
+        triples = chain(
+            ((u, v, {"weight": 1}) for d in range(A[u, v])) for (u, v) in edges
+        )
+    else:  # basic data type
+        triples = ((u, v, dict(weight=python_type(A[u, v]))) for u, v in edges)
+    # If we are creating an undirected multigraph, only add the edges from the
+    # upper triangle of the matrix. Otherwise, add all the edges. This relies
+    # on the fact that the vertices created in the
+    # `_generated_weighted_edges()` function are actually the row/column
+    # indices for the matrix `A`.
+    #
+    # Without this check, we run into a problem where each edge is added twice
+    # when `G.add_edges_from()` is invoked below.
+    if G.is_multigraph() and not G.is_directed():
+        triples = ((u, v, d) for u, v, d in triples if u <= v)
+    G.add_edges_from(triples)
+    return G


Test patch:
diff --git a/networkx/classes/tests/historical_tests.py b/networkx/classes/tests/historical_tests.py
index 8f53c4c53..c19396add 100644
--- a/networkx/classes/tests/historical_tests.py
+++ b/networkx/classes/tests/historical_tests.py
@@ -21,7 +21,6 @@ class HistoricalTests:
 
     def test_name(self):
         G = self.G(name="test")
-        assert str(G) == "test"
         assert G.name == "test"
         H = self.G()
         assert H.name == ""
diff --git a/networkx/classes/tests/test_graph.py b/networkx/classes/tests/test_graph.py
index 03902b092..e90f99510 100644
--- a/networkx/classes/tests/test_graph.py
+++ b/networkx/classes/tests/test_graph.py
@@ -182,9 +182,18 @@ class BaseAttrGraphTester(BaseGraphTester):
         G = self.Graph(name="")
         assert G.name == ""
         G = self.Graph(name="test")
-        assert G.__str__() == "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)
diff --git a/networkx/conftest.py b/networkx/conftest.py
index cad9e46c6..9f0e53b7b 100644
--- a/networkx/conftest.py
+++ b/networkx/conftest.py
@@ -64,6 +64,12 @@ def set_warnings():
         category=PendingDeprecationWarning,
         message="the matrix subclass is not the recommended way*",
     )
+    warnings.filterwarnings(
+        "ignore", category=DeprecationWarning, message="to_numpy_matrix"
+    )
+    warnings.filterwarnings(
+        "ignore", category=DeprecationWarning, message="from_numpy_matrix"
+    )
 
 
 @pytest.fixture(autouse=True)
diff --git a/networkx/tests/test_convert_numpy.py b/networkx/tests/test_convert_numpy.py
index c5dbf2806..8e8d1ee06 100644
--- a/networkx/tests/test_convert_numpy.py
+++ b/networkx/tests/test_convert_numpy.py
@@ -8,7 +8,17 @@ from networkx.generators.classic import barbell_graph, cycle_graph, path_graph
 from networkx.testing.utils import assert_graphs_equal
 
 
-class TestConvertNumpy:
+def test_to_numpy_matrix_deprecation():
+    pytest.deprecated_call(nx.to_numpy_matrix, nx.Graph())
+
+
+def test_from_numpy_matrix_deprecation():
+    pytest.deprecated_call(nx.from_numpy_matrix, np.eye(2))
+
+
+class TestConvertNumpyMatrix:
+    # TODO: This entire class can be removed when to/from_numpy_matrix
+    # deprecation expires
     def setup_method(self):
         self.G1 = barbell_graph(10, 3)
         self.G2 = cycle_graph(10, create_using=nx.DiGraph)