diff --git a/sr/robot3/robot.py b/sr/robot3/robot.py index 6aa4adc6..6bb2233f 100644 --- a/sr/robot3/robot.py +++ b/sr/robot3/robot.py @@ -8,7 +8,7 @@ from pathlib import Path from socket import socket from types import MappingProxyType -from typing import Mapping, Optional +from typing import Mapping, Optional, final from . import game_specific, timeout from ._version import __version__ @@ -23,12 +23,15 @@ from .raw_serial import RawSerial from .servo_board import ServoBoard from .simulator.time_server import TimeServer -from .utils import IN_SIMULATOR, ensure_atexit_on_term, obtain_lock, singular +from .utils import ( + IN_SIMULATOR, FinalMeta, ensure_atexit_on_term, obtain_lock, singular, +) logger = logging.getLogger(__name__) -class Robot: +@final +class Robot(metaclass=FinalMeta): """ The main robot class that provides access to all the boards. diff --git a/sr/robot3/utils.py b/sr/robot3/utils.py index 87ee6cda..d99640b8 100644 --- a/sr/robot3/utils.py +++ b/sr/robot3/utils.py @@ -7,7 +7,7 @@ import socket from abc import ABC, abstractmethod from types import FrameType -from typing import Any, Mapping, NamedTuple, Optional, TypeVar +from typing import Any, Mapping, NamedTuple, Optional, Type, TypeVar from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -77,6 +77,18 @@ def identify(self) -> BoardIdentity: pass # pragma: no cover +class FinalMeta(type): + """ + A meta class which ensures a given class cannot be subclassed. + + It's recommended to additionally use typing.final. + """ + def __new__(cls, name: str, bases: tuple, classdict: dict) -> Type: + if bases: + raise TypeError(f"{name} cannot be sub-classed.") + return super().__new__(cls, name, bases, dict(classdict)) + + def map_to_int( x: float, in_min: float, diff --git a/tests/test_sr_robot.py b/tests/test_sr_robot.py index 9077620d..bbc67394 100644 --- a/tests/test_sr_robot.py +++ b/tests/test_sr_robot.py @@ -139,3 +139,9 @@ def test_robot_discovery() -> None: assert robot.power_board == robot.boards[power_asset_tag] assert robot.servo_board == robot.boards[servo_asset_tag] assert robot.motor_board == robot.boards[motor_asset_tag] + + +def test_cannot_subclass() -> None: + with pytest.raises(TypeError, match="Robot cannot be sub-classed"): + class MyRobot(Robot): + pass