Skip to content

Make every abstract base class inherit ABC - #1461

Open
coretl wants to merge 4 commits into
bluesky:mainfrom
coretl:abc-sweep
Open

coretl wants to merge 4 commits into
bluesky:mainfrom
coretl:abc-sweep

Conversation

@coretl

@coretl coretl commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

A class with @abstractmethod only has its abstract methods enforced when its metaclass is ABCMeta. Generic and @dataclass don't provide it, so on a plain base class neither pyright (reportAbstractUsage) nor the runtime notices a subclass that misses a method: the stub runs and returns None. The previous CLAUDE.md guidance ("No ABC base needed ... pyright enforces implementation statically") was wrong.

This PR makes every such base in src/ inherit ABC:

  • CommandBackend, ReadableDataProvider, StreamableDataProvider, FlyableLogic, MovableLogic, SettingsProvider, SignalBackend, SoftConverter and TangoProxy.

Protocols and classes that already used ABC (e.g. DetectorAcquireLogic) are unchanged.

Doing so exposed a real bug: tango's CommandProxy implemented none of TangoProxy's get, get_reading and has_subscription. get and get_reading now raise NotImplementedError, like its existing subscribe methods, and has_subscription returns False. TangoProxy.connect is an intentional empty hook, so it gets # noqa: B027.

The CLAUDE.md code convention is rewritten: every class with an @abstractmethod has ABCMeta as its metaclass, via ABC unless a base already brings it (Protocol, pydantic BaseModel).

Downstream subclasses that are missing an abstract method now fail with TypeError at construction, instead of silently returning None later.

AI Disclaimer

The code and this description were generated with AI and reviewed/tweaked by the author.

@coretl
coretl requested review from shihab-dls and a balanced review from Copilot October 20, 2026 11:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

New ABC inheritance introduces unsuppressed Ruff B024/B027 violations in movable and flyable logic.

2 open issues
What changed in this PR

This PR enforces abstract-method implementation across backend and device-logic base classes, making incomplete subclasses fail at construction.

Changes:

  • Add ABC inheritance to nine base classes.
  • Adapt Tango commands and unavailable EPICS transports for abstract enforcement.
  • Update contributor guidance and add Tango command tests.
File Description
tests/​system_tests/​tango/​core/​test_tango_transport.py Test unsupported command reads and subscription state.
src/​ophyd_async/​tango/​core/​_tango_transport.py Enforce abstraction and implement missing command methods.
src/​ophyd_async/​epics/​core/​_signal.py Preserve unavailable-transport errors through __new__.
src/​ophyd_async/​core/​_soft_signal_backend.py Make converters inherit ABC.
src/​ophyd_async/​core/​_signal_backend.py Enforce abstract signal backend methods.
src/​ophyd_async/​core/​_settings.py Enforce abstract settings provider methods.
src/​ophyd_async/​core/​_movable.py Add ABC to movable logic.
src/​ophyd_async/​core/​_flyable.py Enforce abstract flyable hooks.
src/​ophyd_async/​core/​_data_providers.py Enforce abstract data provider methods.
src/​ophyd_async/​core/​_command.py Enforce abstract command backend methods.
CLAUDE.md Revise abstract-class guidance.

Review effort: Balanced

Comment thread src/ophyd_async/core/_flyable.py Resolved
Comment thread src/ophyd_async/core/_movable.py Resolved

@shihab-dls shihab-dls left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this sweep covers everything, except for StandardMovable which I see still does not raise if movable_logic is not implemented. I ran this test which fails:

def test_movable_without_movable_logic_raises():
    class NoLogicMovable(StandardMovable[float]):
        pass

    with pytest.raises(TypeError, match="movable_logic"):
        NoLogicMovable()  # pyright: ignore[reportAbstractUsage]

Looks like this is because movable_logic is a cached_property, so ABCMeta only finds the cached property object. property works though, but then we'd rebuild the object each call, unless a subclass ignores the pyright flag....requesting changes so we can discuss this more.

Comment thread src/ophyd_async/core/_movable.py Resolved
@coretl

coretl commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

system tests failing because of an ubuntu base image update, investigating...

@coretl

coretl commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

system tests failing because of an ubuntu base image update, investigating...

#1470

@shihab-dls shihab-dls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! 2 comments, both minor, so approving.

Comment on lines +237 to 238
async def connect(self) -> None: # noqa: B027
"""Perform actions after proxy is connected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this is ignored instead of made an abstractmethod because connect() is an optional hook, right?

Comment on lines 144 to +153
@@ -152,6 +150,7 @@ def movable_logic(self) -> MovableLogic:
`readback` signals directly. Subclasses must implement this as a
`@cached_property` that returns a `MovableLogic` instance.
"""
raise NotImplementedError

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just use ...? As ABCMeta already catches inavlid construction now

A plain class with @AbstractMethod is enforced neither by pyright nor
at runtime: a subclass that misses a method silently runs the stub and
returns None. Give every such base ABCMeta by inheriting ABC, and
record the rule in CLAUDE.md.

This exposed tango's CommandProxy, which implemented none of
TangoProxy's get, get_reading and has_subscription: the first two now
raise NotImplementedError and has_subscription returns False.

Assisted-by: Claude Sonnet 5.5
TransportNotAvailable now inherits an ABC through EpicsSignalBackend, so
instantiating it fails the abstract-method check in object.__new__ with a
TypeError before __init__ can raise the intended NotImplementedError
("did you pip install ophyd_async[ca]?"). Raise it from __new__ instead.

Assisted-by: Claude Sonnet 5.5
Covers the get, get_reading and has_subscription methods added to
CommandProxy so that it implements the now-ABC TangoProxy.

Assisted-by: Claude Sonnet 5.5
Addresses shihab-dls's review of bluesky#1461: ABCMeta does not see an abstract
cached_property, so StandardMovable without movable_logic did not raise.
Adds abstract_cached_property and uses it for movable_logic. Also drops
ABC from MovableLogic, which has no abstract methods.

Assisted-by: Claude Opus 5

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants