diff --git a/src/pgraph/PGraph.py b/src/pgraph/PGraph.py index 72e1c5a..fc0f008 100644 --- a/src/pgraph/PGraph.py +++ b/src/pgraph/PGraph.py @@ -323,15 +323,38 @@ def __str__(self) -> str: >>> g.add_edge(v2, v3) >>> str(g) - :seealso: :meth:`show` + :seealso: :meth:`__repr__` :meth:`show` """ s = f"{self.__class__.__name__}: {self.n:,} {'vertex' if self.n==1 else 'vertices'}, {self.ne:,} edge{'s'[:self.ne^1]}, {self.nc:,} component{'s'[:self.nc^1]}" return s def __repr__(self) -> str: - # NOTE: this is shadowed by the __repr__ defined further below in - # this class, which is the one actually used -- kept as-is here to - # avoid changing behaviour as part of a typing-only pass. + """ + Representation of the graph, the same one-line summary as :meth:`__str__` + + :return: one-line summary of vertex/edge/component counts + :rtype: str + + The representation of a graph does not list its vertices, since that + would be as long as the graph, and a ``repr`` is what appears in a + notebook cell, a debugger, a log or an error message. Use + :meth:`show` to list them. + + .. runblock:: pycon + + >>> from pgraph import UGraph + >>> g = UGraph() + >>> v1 = g.add_vertex(coord=[0,0], name='v1') + >>> v2 = g.add_vertex(coord=[1,1], name='v2') + >>> g.add_edge(v1, v2) + >>> repr(g) + + .. versionchanged:: 1.1 + Was one line per vertex, with its name, coordinate and component; + that is now printed by :meth:`show`. + + :seealso: :meth:`__str__` :meth:`show` + """ return str(self) @classmethod @@ -1501,7 +1524,11 @@ def remove(self, x: Edge | Vertex) -> None: def show(self) -> None: """ - Print a summary of all vertices and edges to stdout + Print a listing of all vertices and edges to stdout + + Each vertex is shown with its name, coordinate and component number, + and each edge by its endpoints, cost and data. This is as long as the + graph, unlike :meth:`__str__` and :meth:`__repr__`, which are one line. .. runblock:: pycon @@ -1512,11 +1539,18 @@ def show(self) -> None: >>> g.add_edge(v1, v2) >>> g.show() - :seealso: :meth:`__str__` + .. versionchanged:: 1.1 + Each vertex is shown with its coordinate and component number, + which were what ``repr(g)`` listed before it became one line. + + :seealso: :meth:`__str__` :meth:`plot` """ print("vertices:") for v in self._vertexlist: - print(" " + str(v)) + line = f" {v.name} at {v.coord}" + if v.label is not None: + line += f" component={v.label}" + print(line) print("edges:") for e in self._edges: print(" " + str(e)) @@ -1808,33 +1842,6 @@ def heuristic(self, heuristic: Callable[[NDArray], float] | str) -> None: """ self._heuristic = self._metricfunc(heuristic) - def __repr__(self) -> str: # type: ignore[no-redef] - """ - Detailed representation of the graph, one line per vertex - - :return: one line per vertex showing its name, coordinate and component - :rtype: str - - .. runblock:: pycon - - >>> from pgraph import UGraph - >>> g = UGraph() - >>> v1 = g.add_vertex(coord=[0,0], name='v1') - >>> v2 = g.add_vertex(coord=[1,1], name='v2') - >>> v3 = g.add_vertex(coord=[2,2], name='v3') - >>> g.add_edge(v1, v2) - >>> g.add_edge(v2, v3) - >>> repr(g) - - """ - s = [f"{self.__class__.__name__}:"] - for vertex in self: - ss = f" {vertex.name} at {vertex.coord}" - if vertex.label is not None: - ss += f" component={vertex.label}" - s.append(ss) - return "\n".join(s) - def __getitem__(self, i: int | str | Vertex) -> Vertex: """ Get vertex (base class method) @@ -2528,7 +2535,7 @@ def plot( ... g.add_edge(a, b); >>> g.plot(block=None) - :seealso: :meth:`highlight_path` :meth:`highlight_edge` + :seealso: :meth:`highlight_path` :meth:`highlight_edge` :meth:`show` """ import matplotlib.pyplot as plt from matplotlib.patches import FancyArrowPatch diff --git a/tests/test_graph.py b/tests/test_graph.py index c10df00..a534e39 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -1,4 +1,6 @@ +import contextlib +import io import unittest import numpy as np import numpy.testing as nt @@ -45,11 +47,53 @@ def test_str(self): self.assertEqual(str(g), "DGraph: 2 vertices, 1 edge, 1 component") + # the repr is the one-line summary, not a listing of the vertices + self.assertEqual(repr(g), "DGraph: 2 vertices, 1 edge, 1 component") + + def test_repr_is_one_line_however_big_the_graph(self): + g = UGraph() + for i in range(500): + g.add_vertex([i, i], name=f"v{i}") s = repr(g) - self.assertIsInstance(s, str) - lines = s.split('\n') - self.assertEqual(len(lines), 3) # class-name header + one line per vertex - self.assertEqual(lines[0], "DGraph:") + self.assertNotIn("\n", s) + self.assertEqual(s, str(g)) + self.assertEqual(s, "UGraph: 500 vertices, 0 edges, 500 components") + + def test_show_lists_vertices_and_edges(self): + g = UGraph() + a = g.add_vertex([1, 2], name="a") + b = g.add_vertex([3, 4], name="b") + g.add_vertex([5, 6], name="c") + g.add_edge(a, b, cost=2.5) + + out = io.StringIO() + with contextlib.redirect_stdout(out): + g.show() + lines = out.getvalue().splitlines() + + # a vertex line has the name, the coordinate and the component number, + # which are what the repr used to list; c is a component of its own + self.assertEqual( + lines, + [ + "vertices:", + " a at [1 2] component=0", + " b at [3 4] component=0", + " c at [5 6] component=1", + "edges:", + " Edge{[a] -- [b], cost=2.5}", + ], + ) + + def test_show_a_vertex_without_a_coordinate(self): + g = UGraph() + g.add_vertex(name="x") + + out = io.StringIO() + with contextlib.redirect_stdout(out): + g.show() + + self.assertIn(" x at None", out.getvalue()) def test_str_has_thousands_separators(self):