From 3cd0f6cc4f15fc37d4dbcea162b0348759ef58c2 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 12 Oct 2026 07:49:31 +1000 Subject: [PATCH 1/2] test: adjacent() is canonical; neighbours/neighbors/isneighbour* are silent aliases Co-Authored-By: Claude Sonnet 5.5 --- tests/test_adjacent.py | 108 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/test_adjacent.py diff --git a/tests/test_adjacent.py b/tests/test_adjacent.py new file mode 100644 index 00000000..73a21f92 --- /dev/null +++ b/tests/test_adjacent.py @@ -0,0 +1,108 @@ +""" +``adjacent()`` is the canonical name for the vertices at the far end of a +vertex's edges, because "adjacent" is the graph-theory term and has one +spelling. ``neighbours`` and ``neighbors`` (and ``isneighbour`` / +``isneighbor``) remain as silent aliases: they are the same function, not a +second implementation, and they do not warn. +""" + +import unittest +import warnings + +from pgraph import DGraph, UGraph, DVertex, UVertex +from pgraph.PGraph import BaseVertex + + +class TestAliases(unittest.TestCase): + def test_aliases_are_the_same_function(self): + for cls in (BaseVertex, UVertex, DVertex): + with self.subTest(cls=cls.__name__): + self.assertIs(cls.neighbours, cls.adjacent) + self.assertIs(cls.neighbors, cls.adjacent) + self.assertIs(cls.isneighbour, cls.isadjacent) + self.assertIs(cls.isneighbor, cls.isadjacent) + + def test_no_warnings(self): + g = UGraph() + a, b = g.add_vertex(name="a"), g.add_vertex(name="b") + a.connect(b) + with warnings.catch_warnings(): + warnings.simplefilter("error") # any warning is an error + for name in ("adjacent", "neighbours", "neighbors"): + self.assertEqual(getattr(a, name)(), [b]) + for name in ("isadjacent", "isneighbour", "isneighbor"): + self.assertTrue(getattr(a, name)(b)) + + def test_docstring_names_the_aliases(self): + doc = BaseVertex.adjacent.__doc__ + for name in ("neighbours", "neighbors"): + self.assertIn(name, doc) + + +class _Adjacent: + Graph: type + + def setUp(self): + self.g = self.Graph() + self.a = self.g.add_vertex([0, 0], name="a") + self.b = self.g.add_vertex([1, 0], name="b") + self.c = self.g.add_vertex([2, 0], name="c") + + def test_simple(self): + self.a.connect(self.b) + self.assertEqual(self.a.adjacent(), [self.b]) + self.assertTrue(self.a.isadjacent(self.b)) + self.assertFalse(self.a.isadjacent(self.c)) + + def test_returns_a_new_list(self): + self.a.connect(self.b) + r = self.a.adjacent() + r.clear() + self.assertEqual(self.a.adjacent(), [self.b]) + + def test_parallel_edges_listed_once_per_edge(self): + for _ in range(3): + self.a.connect(self.b) + self.assertEqual(self.a.adjacent(), [self.b, self.b, self.b]) + + def test_matches_a_row_of_the_adjacency_matrix(self): + # the point of the name: row i of adjacency() is the vertices adjacent + # to vertex i, with multiplicity + self.a.connect(self.b) + self.a.connect(self.b) + self.a.connect(self.c) + A = self.g.adjacency() + for i, v in enumerate(self.g): + counts = {w: v.adjacent().count(w) for w in self.g} + for j, w in enumerate(self.g): + self.assertEqual(A[i, j], counts[w], (v.name, w.name)) + + +class TestUGraphAdjacent(_Adjacent, unittest.TestCase): + Graph = UGraph + + def test_both_ends(self): + self.a.connect(self.b) + self.assertEqual(self.b.adjacent(), [self.a]) + self.assertTrue(self.b.isadjacent(self.a)) + + def test_self_loop_listed_twice(self): + self.a.connect(self.a) + self.assertEqual(self.a.adjacent(), [self.a, self.a]) + + +class TestDGraphAdjacent(_Adjacent, unittest.TestCase): + Graph = DGraph + + def test_outgoing_only(self): + self.a.connect(self.b) + self.assertEqual(self.b.adjacent(), []) + self.assertFalse(self.b.isadjacent(self.a)) + + def test_self_loop_listed_once(self): + self.a.connect(self.a) + self.assertEqual(self.a.adjacent(), [self.a]) + + +if __name__ == "__main__": + unittest.main() From 53e0badebc45d04320bc60a87975caf20e822258 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 12 Oct 2026 07:49:59 +1000 Subject: [PATCH 2/2] refactor: adjacent() is the canonical name; neighbours/neighbors are aliases 'Adjacent' is the graph-theory term and has one spelling, which removes the UK/US split at the root: neighbours and neighbors were two separate methods with duplicated bodies and docstrings, isneighbour had no US form, and adjacent had been left deprecated. - adjacent() is the method; neighbours and neighbors are class attributes bound to it, so they are the same function and do not warn - isadjacent() likewise, with isneighbour and isneighbor as aliases - adjacent() is documented as the relation adjacency() tabulates: row i of the adjacency matrix is non-zero at the vertices g[i].adjacent() returns - the package's own calls use the canonical name No behaviour changes, and nothing that worked before stops working; the only visible difference is that adjacent() no longer emits a DeprecationWarning. Co-Authored-By: Claude Sonnet 5.5 --- src/pgraph/PGraph.py | 119 +++++++++++++++++++------------------------ tests/test_graph.py | 12 ----- 2 files changed, 53 insertions(+), 78 deletions(-) diff --git a/src/pgraph/PGraph.py b/src/pgraph/PGraph.py index d320faa8..a5f2dbf9 100644 --- a/src/pgraph/PGraph.py +++ b/src/pgraph/PGraph.py @@ -1238,7 +1238,7 @@ def plot( ax.text(vertex.x, vertex.y, f" {vertex.name}", **textopt) if colorcomponents: ax.plot(vertex.x, vertex.y, color=color[c, :], **vopt) - for v in vertex.neighbours(): + for v in vertex.adjacent(): ax.plot( [vertex.x, v.x], [vertex.y, v.y], @@ -1247,7 +1247,7 @@ def plot( ) else: ax.plot(vertex.x, vertex.y, **vopt) - for v in vertex.neighbours(): + for v in vertex.adjacent(): ax.plot([vertex.x, v.x], [vertex.y, v.y], **eopt) else: # 3D or higher plotting, just do (x, y, z) @@ -1267,7 +1267,7 @@ def plot( vertex.z, **{**dict(color=color[c, :]), **vopt}, ) - for v in vertex.neighbours(): + for v in vertex.adjacent(): ax.plot( [vertex.x, v.x], [vertex.y, v.y], @@ -1276,7 +1276,7 @@ def plot( ) else: ax.plot(vertex.x, vertex.y, vertex.z, **vopt) - for v in vertex.neighbours(): + for v in vertex.adjacent(): ax.plot( [vertex.x, v.x], [vertex.y, v.y], @@ -1669,7 +1669,7 @@ def iscyclic(self) -> bool: repeatedly remove vertices with no remaining incoming edges. If every vertex can eventually be removed, the graph is a DAG (acyclic); if some are never freed, they form a cycle. This is O(V+E), using - vertices' outgoing edges via :meth:`BaseVertex.neighbours` the same + vertices' outgoing edges via :meth:`BaseVertex.adjacent` the same way :meth:`path_BFS` and friends do. .. note:: A self-loop is a cycle of length one, so it makes either kind @@ -1713,7 +1713,7 @@ def iscyclic(self) -> bool: while frontier: vertex = frontier.pop() removed += 1 - for n in vertex.neighbours(): + for n in vertex.adjacent(): indegree[n] -= 1 if indegree[n] == 0: frontier.append(n) @@ -1950,7 +1950,7 @@ def adjacency(self) -> NDArray: A = np.zeros((self.n, self.n)) for vertex in self: - for n in vertex.neighbours(): + for n in vertex.adjacent(): A[vdict[vertex], vdict[n]] += 1 return A @@ -2185,7 +2185,7 @@ def path_BFS( print(" expand", x.name) # expand the vertex - for n in x.neighbours(): + for n in x.adjacent(): if n is x: continue # self-loop, never part of a shortest path if n is G: @@ -2526,7 +2526,7 @@ def _graphcolor(self) -> None: while len(q) > 0: v = q.pop() # expand v v.label = label - for n in v.neighbours(): + for n in v.adjacent(): if n.label is None: q.append(n) lastlabel = label @@ -2580,7 +2580,7 @@ def _graphcolor(self) -> int | None: for v in self: if v.label is None: # no label, try to inherit one from a neighbour - for n in v.neighbours(): + for n in v.adjacent(): if n.label is not None: # neighbour has a label v.label = n.label @@ -2595,7 +2595,7 @@ def _graphcolor(self) -> int | None: assert label is not None # now look for clashes - for n in v.neighbours(): + for n in v.adjacent(): if n.label is None: # neighbour has no label, give it this one n.label = label @@ -3029,17 +3029,28 @@ def copy(self, cls: type[_BaseGraph] | None = None) -> BaseVertex: else: return self.__class__(coord=self.coord, name=self.name) - def neighbours(self) -> list[BaseVertex]: + def adjacent(self) -> list[BaseVertex]: """ - Neighbours of a vertex + Vertices adjacent to this vertex + + :return: the vertices at the far end of this vertex's edges + :rtype: list of BaseVertex subclass - ``v.neighbours()`` is a list of neighbours of this vertex. + ``v.adjacent()`` is a list of the vertices that this vertex is joined + to by an edge. "Adjacent" is the graph-theory term, and it is the + relation that :meth:`_BaseGraph.adjacency` tabulates: row ``i`` of the + adjacency matrix is non-zero exactly at the columns of the vertices + returned by ``g[i].adjacent()``. - .. note:: For a directed graph the neighbours are those on edges leaving this vertex + .. note:: - .. note:: There is one entry per edge, so a neighbour joined by ``k`` - parallel edges appears ``k`` times. In an undirected graph a - self-loop lists the vertex itself twice, once for each end. + - For a directed graph these are the vertices at the head of the + edges leaving this vertex, the *successors*. + - There is one entry per edge, so a vertex joined by ``k`` parallel + edges appears ``k`` times. In an undirected graph a self-loop + lists the vertex itself twice, once for each end. + - ``neighbours`` and ``neighbors`` are aliases, the same method + under the British and American spellings, and are not deprecated. .. runblock:: pycon @@ -3050,60 +3061,31 @@ def neighbours(self) -> list[BaseVertex]: >>> v3 = g.add_vertex(coord=[2,2], name='v3') >>> g.add_edge(v1, v2) >>> g.add_edge(v1, v3) - >>> print(v1.neighbours()) + >>> print(v1.adjacent()) - :seealso: :meth:`neighbors` :meth:`incidences` + :seealso: :meth:`isadjacent` :meth:`incidences` :meth:`edges` + :meth:`_BaseGraph.adjacency` """ return [e.next(self) for e in self._edgelist] - def neighbors(self) -> list[BaseVertex]: - """ - Neighbors of a vertex - - ``v.neighbors()`` is a list of neighbors of this vertex. - - .. note:: For a directed graph the neighbours are those on edges leaving this vertex - - .. 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) - >>> print(v1.neighbors()) + #: alias of :meth:`adjacent` + neighbours = adjacent + #: alias of :meth:`adjacent` + neighbors = adjacent - :seealso: :meth:`neighbours` + def isadjacent(self, vertex: BaseVertex) -> bool: """ - return [e.next(self) for e in self._edgelist] - - def adjacent(self) -> list[BaseVertex]: - """ - Neighbours of a vertex (deprecated) - - :return: a list of neighbours of this vertex - :rtype: list of BaseVertex subclass - - .. deprecated:: use :meth:`neighbours` instead - """ - warnings.warn( - "adjacent() is deprecated, use neighbours() instead", - DeprecationWarning, - stacklevel=2, - ) - return self.neighbours() - - def isneighbour(self, vertex: BaseVertex) -> bool: - """ - Test if vertex is a neigbour + Test if a vertex is adjacent to this vertex :param vertex: vertex reference :type vertex: BaseVertex subclass - :return: true if a neighbour + :return: true if ``vertex`` is adjacent :rtype: bool - For a directed graph this is true only if the edge is from ``self`` to - ``vertex``. + For a directed graph this is true only if there is an edge from + ``self`` to ``vertex``. + + ``isneighbour`` and ``isneighbor`` are aliases. .. runblock:: pycon @@ -3113,19 +3095,24 @@ def isneighbour(self, vertex: BaseVertex) -> bool: >>> v2 = g.add_vertex(coord=[1,1], name='v2') >>> v3 = g.add_vertex(coord=[2,2], name='v3') >>> g.add_edge(v1, v2) - >>> print(v1.isneighbour(v2)) - >>> print(v1.isneighbour(v3)) + >>> print(v1.isadjacent(v2)) + >>> print(v1.isadjacent(v3)) - :seealso: :meth:`neighbours` + :seealso: :meth:`adjacent` """ return vertex in [e.next(self) for e in self._edgelist] + #: alias of :meth:`isadjacent` + isneighbour = isadjacent + #: alias of :meth:`isadjacent` + isneighbor = isadjacent + def incidences(self) -> list[tuple[BaseVertex, Edge]]: """ Neighbours and edges of a vertex ``v.incidences()`` is a generator that returns a list of incidences, - tuples of (vertex, edge) for all neighbours of the vertex ``v``. + tuples of (vertex, edge) for all vertices adjacent to the vertex ``v``. .. note:: For a directed graph the edges are those leaving this vertex @@ -3138,7 +3125,7 @@ def incidences(self) -> list[tuple[BaseVertex, Edge]]: >>> g.add_edge(v1, v2) >>> print(v1.incidences()) - :seealso: :meth:`neighbours` :meth:`edges` + :seealso: :meth:`adjacent` :meth:`edges` """ return [(e.next(self), e) for e in self._edgelist] diff --git a/tests/test_graph.py b/tests/test_graph.py index 6e06f6fe..f9f7f15f 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -153,18 +153,6 @@ def test_neighbours(self): self.assertFalse(v1.isneighbour(v3)) self.assertFalse(v3.isneighbour(v1)) - def test_adjacent_deprecated(self): - # adjacent() must still work (same result as neighbours()) but - # emit a DeprecationWarning, not a hard failure - g = UGraph() - v1 = g.add_vertex(name='v1') - v2 = g.add_vertex(name='v2') - v1.connect(v2) - - with self.assertWarns(DeprecationWarning): - n = v1.adjacent() - self.assertEqual(n, v1.neighbours()) - def test_getitem(self): g = UGraph() v1 = g.add_vertex(name='v1')