From 31fb30d48f1832337cbcf8bc342a811b9834cc29 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 12 Oct 2026 08:40:41 +1000 Subject: [PATCH 1/2] refactor: make graph constructor arguments keyword-only In 0.6.5 the first positional argument was accepted and ignored, so UGraph(6) looked as though it set a dimension but did nothing. With the current signature it would land in metric. Making every argument keyword-only turns UGraph(6) into a clear TypeError. Co-Authored-By: Claude Sonnet 5.5 --- src/pgraph/PGraph.py | 9 +++++++ tests/test_constructor_keywords.py | 40 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/test_constructor_keywords.py diff --git a/src/pgraph/PGraph.py b/src/pgraph/PGraph.py index cfc7255d..d44b815a 100644 --- a/src/pgraph/PGraph.py +++ b/src/pgraph/PGraph.py @@ -25,6 +25,7 @@ class BaseGraph(ABC): def __init__( self, + *, metric: Callable[[NDArray], float] | str | None = None, heuristic: Callable[[NDArray], float] | str | None = None, verbose: bool = False, @@ -60,6 +61,14 @@ def __init__( This is the common base class of :class:`UGraph` and :class:`DGraph` and should not be instantiated directly. + All arguments are keyword-only, so ``UGraph(dim=6)`` is correct and + ``UGraph(6)`` raises :exc:`TypeError`. + + .. versionchanged:: 1.0 + Arguments are keyword-only. In 0.6.5 the first positional + argument was accepted and ignored, so ``UGraph(6)`` silently did + nothing. + ``vertex_class`` and ``edge_class`` let user subclasses be built by the graph, so they never need to be constructed by hand:: diff --git a/tests/test_constructor_keywords.py b/tests/test_constructor_keywords.py new file mode 100644 index 00000000..92033a43 --- /dev/null +++ b/tests/test_constructor_keywords.py @@ -0,0 +1,40 @@ +"""Graph constructor arguments are keyword-only. + +In 0.6.5 the first positional argument was accepted and ignored, so +``UGraph(6)`` looked as though it set a dimension but did nothing. +""" + +import unittest + +from pgraph import DGraph, UGraph + + +class _Keywords: + cls = None + + def test_positional_rejected(self): + for arg in (6, "L2", None, True): + with self.subTest(arg=arg): + with self.assertRaises(TypeError): + self.cls(arg) + + def test_keywords_accepted(self): + g = self.cls(dim=6, metric="L1", heuristic="L2", verbose=False) + self.assertEqual(g.n, 0) + with self.assertRaises(ValueError): + g.add_vertex([1, 2]) + + def test_no_arguments(self): + self.assertEqual(self.cls().n, 0) + + +class TestUGraphKeywords(_Keywords, unittest.TestCase): + cls = UGraph + + +class TestDGraphKeywords(_Keywords, unittest.TestCase): + cls = DGraph + + +if __name__ == "__main__": + unittest.main() From 8c62dd25a4dc5fd0000fd5be0c7c3f2e1bbb1c31 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 12 Oct 2026 09:30:58 +1000 Subject: [PATCH 2/2] test: annotate the mixin's class attribute rather than assigning None Co-Authored-By: Claude Sonnet 5.5 --- tests/test_constructor_keywords.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_constructor_keywords.py b/tests/test_constructor_keywords.py index 92033a43..c8d042b3 100644 --- a/tests/test_constructor_keywords.py +++ b/tests/test_constructor_keywords.py @@ -10,7 +10,7 @@ class _Keywords: - cls = None + cls: type def test_positional_rejected(self): for arg in (6, "L2", None, True):