From 3363fe2e2496a03252f3728b425e8dd72cad524a Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 3 Nov 2026 14:36:10 +1000 Subject: [PATCH] refactor(backends): Connector is an abstract class again, not a Protocol #788 made Connector a Protocol so that upstream Swift, which does not depend on RTB, could count as one. RTB's own Swift wrapper is RTB code and can inherit, and no backend loaded by name is the upstream class. An abstract base class lets a new flag get a default that every backend inherits, and a missing method is an error when the backend is made. The Swift wrapper's _add_teach_panel now has the signature of the base class. Co-Authored-By: Claude Sonnet 5.5 --- docs/source/arm_backend.rst | 2 +- src/roboticstoolbox/backends/Connector.py | 16 ++++----- .../backends/swift/__init__.py | 8 +++-- tests/test_pyplot_backend_fixes.py | 35 +++---------------- 4 files changed, 19 insertions(+), 42 deletions(-) diff --git a/docs/source/arm_backend.rst b/docs/source/arm_backend.rst index 88c6c0f6e..8c32a5184 100644 --- a/docs/source/arm_backend.rst +++ b/docs/source/arm_backend.rst @@ -72,7 +72,7 @@ The backend holds a *scene*: the robots and other objects that it has been asked The contract ------------ -This is written down as the :py:class:`~roboticstoolbox.backends.Connector.Connector` protocol. A backend is *any* object that has these methods and flags, and does not have to inherit from it: Swift does not, since it does not depend on the Toolbox. The PyPlot backends inherit from it, which supplies the default values of the flags and makes a missing method an error when the backend is made. The arguments differ from one backend to another, so the methods are declared with ``*args, **kwargs``, and the details are in each backend. +This is written down as the :py:class:`~roboticstoolbox.backends.Connector.Connector` abstract class. A backend inherits from it, which supplies the default values of the flags and makes a missing method an error when the backend is made. The arguments differ from one backend to another, so the methods are declared with ``*args, **kwargs``, and the details are in each backend. .. autosummary:: :nosignatures: diff --git a/src/roboticstoolbox/backends/Connector.py b/src/roboticstoolbox/backends/Connector.py index f6e810e62..1f457760d 100644 --- a/src/roboticstoolbox/backends/Connector.py +++ b/src/roboticstoolbox/backends/Connector.py @@ -5,20 +5,18 @@ from __future__ import annotations -from abc import abstractmethod -from typing import Any, Protocol, runtime_checkable +from abc import ABC, abstractmethod +from typing import Any -@runtime_checkable -class Connector(Protocol): +class Connector(ABC): """ What RTB needs of a graphical backend - A backend is any object that has these methods and flags: it does not have to - inherit from this class. Swift does not, since it does not depend on RTB. - The PyPlot backends inherit from it explicitly, which supplies the default - values of the flags and of :meth:`_add_teach_panel`, and makes a missing - method an error at construction. + A backend inherits from this class, which supplies the default values of the + flags and of :meth:`_add_teach_panel`, and makes a missing method an error + when the backend is made. A backend from outside the Toolbox inherits from + it too. The methods take different arguments in different backends, so they are declared with ``*args, **kwargs``; see the backend for the details. diff --git a/src/roboticstoolbox/backends/swift/__init__.py b/src/roboticstoolbox/backends/swift/__init__.py index 4017ba22e..0a39fc738 100644 --- a/src/roboticstoolbox/backends/swift/__init__.py +++ b/src/roboticstoolbox/backends/swift/__init__.py @@ -15,14 +15,18 @@ ) from swift.Swift import Swift as _SwiftBase +from roboticstoolbox.backends.Connector import Connector -class Swift(_SwiftBase): + +class Swift(_SwiftBase, Connector): """Swift backend with RTB capability flags.""" supports_teach: bool = True supports_ellipse: bool = False - def _add_teach_panel(self, robot: Any, q: Any, handle: Any, block: Any) -> Any: + def _add_teach_panel( + self, robot: Any, q: Any, handle: Any = None, block: bool = True + ) -> Any: """ Add a joint-slider teach panel plus a live end-effector pose readout, and wire the sliders to drive ``handle`` (the diff --git a/tests/test_pyplot_backend_fixes.py b/tests/test_pyplot_backend_fixes.py index 6724f5d90..c2be3b4d1 100644 --- a/tests/test_pyplot_backend_fixes.py +++ b/tests/test_pyplot_backend_fixes.py @@ -82,31 +82,16 @@ def test_animation_without_a_window_manager(self): self.assertIn("Robotics Toolbox", title.call_args.args[1]) -class TestConnectorProtocol(unittest.TestCase): +class TestConnector(unittest.TestCase): def test_backends_are_connectors(self): self.assertIsInstance(PyPlot(), Connector) self.assertIsInstance(PyPlot2(), Connector) - def test_a_class_need_not_inherit(self): - # Swift does not depend on RTB, so it cannot inherit from Connector + def test_a_class_that_does_not_inherit_is_not_a_connector(self): class Standalone: - supports_teach = False - supports_ellipse = False - def launch(self, *a, **k): ... - def step(self, *a, **k): ... - def reset(self, *a, **k): ... - def restart(self, *a, **k): ... - def close(self, *a, **k): ... - def add(self, *a, **k): ... - def remove(self, *a, **k): ... - def hold(self, *a, **k): ... - def _add_teach_panel(self, *a, **k): ... - - self.assertIsInstance(Standalone(), Connector) - def test_an_object_without_the_methods_is_not(self): - self.assertNotIsInstance(object(), Connector) + self.assertNotIsInstance(Standalone(), Connector) def test_inheriting_gets_the_defaults(self): class Minimal(Connector): @@ -132,23 +117,13 @@ def launch(self, *a, **k): ... with self.assertRaises(TypeError): Incomplete() - def test_swift_satisfies_the_protocol(self): + def test_swift_is_a_connector(self): swift = __import__("importlib").util.find_spec("swift") if swift is None: self.skipTest("swift is not installed") from roboticstoolbox.backends.swift import Swift - for name in ( - "launch", - "step", - "reset", - "restart", - "close", - "add", - "remove", - "hold", - ): - self.assertTrue(callable(getattr(Swift, name, None)), name) + self.assertTrue(issubclass(Swift, Connector)) class TestShapePlot(unittest.TestCase):