Skip to content

methods: typed commands — positional arguments and a return value - #419

Open
coretl wants to merge 3 commits into
refactorfrom
refactor-issue-393
Open

methods: typed commands — positional arguments and a return value#419
coretl wants to merge 3 commits into
refactorfrom
refactor-issue-393

Conversation

@coretl

@coretl coretl commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Closes #393

Lifts the void/void restriction on Command. A @command may now take positional arguments and return a value, both of known types; the inspect.Signature that Method already captured stops being thrown away and becomes the public description transports read.

class Stage(Controller):
    @command()
    async def stop(self) -> None: ...                                # void, served everywhere

    @command()
    async def move_to(self, position: float, wait: bool) -> None: ...  # typed

    @command()
    async def measure(self) -> float: ...                              # typed

Scope

  • Command[P, T] is generic over its parameters and return type (ParamSpec + TypeVar), so Command[[float], None] is spellable as the ADR writes it. __call__ forwards *args/**kwargs instead of calling a bare zero-arg function, and UnboundCommand.bind carries that through.
  • Args and returns are validated independently at construction (ADR 0015): args are [] or [DT1, DT2, …], returns are None or a single DT. There is no Any middle case — every argument must be annotated and every type fully known, so a driver that knows nothing statically builds its commands at runtime rather than declaring a half-typed one.
  • Keyword-only arguments, *args and **kwargs are rejected with a message pointing at the keyword-argument spike (SPIKE (Opus/local): typed-command keyword-arg support via TYPE_CHECKING stub #403).
  • Method no longer forbids arguments or a return type. That restriction was in the shared base only because Command was the void case; it now lives on Scan, which is genuinely void/void, via two small named helpers.
  • New public surface on Command: signature, argument_types, return_datatype, is_void.

Transport capability, declared per transport

Transport Void Arguments Return value
REST ✅ any number, as a JSON body ✅ as {"value": …}, status 200 rather than 204
GraphQL ✅ any number, as mutation arguments ✅ the mutation result
Tango ⚠️ at most one, and not an enum ⚠️ not an enum
EPICS CA
EPICS PVA

A command a transport cannot serve is skipped with a warning at start-up and the rest of the controller is still served, per the ADR. The EPICS transports have no PV representation of a typed call; Tango commands carry at most one input value, which is a protocol limit rather than a choice.

Instructions to reviewer on how to test:

  1. uv run pytest tests/test_methods.py tests/test_typed_commands.py -v
  2. Declare a @command taking a float on a controller served over both REST and EPICS CA, and confirm the REST route takes it while the CA IOC logs a skip and still serves the rest.

Checks for reviewer

  • Would the PR title make sense to a user on a set of release notes
  • Argument and return types are the scalar DTypes (bool, int, float, str, Enum) — arrays and tables are deliberately excluded, in COMMAND_DTYPES. ADR 0015 says command serialisation should be shared with Attribute rather than duplicated, and that shared python-type + *Meta path arrives with Remove the DataType family — python types + *Meta typed dicts #413; serving arrays now would mean writing a second, command-specific array serialisation in every transport, which is what the ADR asks us not to do. An array-valued command has an attribute-shaped alternative today, and widening later is additive. Say if you would rather it waited for Remove the DataType family — python types + *Meta typed dicts #413 and served everything.
  • Tango's one-argument limit. A Tango command carries a single input value, so a two-argument command is skipped rather than packed into a spectrum or split — packing would lose the per-argument types that this issue exists to keep.
  • The EPICS GUI now honours enabled. Previously a command or attribute the CA IOC had skipped still got a widget; with typed commands being skipped routinely that becomes a button that does nothing, so extract_api_components skips disabled ones. This also fixes the pre-existing case of an attribute skipped for an over-long PV name.

Notes


Generated by Claude Code

Lifts the void/void restriction on `Command`. A `@command` may now take
positional arguments and return a value, both of known types, and the captured
`inspect.Signature` becomes the public description transports read.

- `Command[P, T]` is generic over its parameters and return type; `__call__`
  and `bind` forward arguments. `argument_types`, `return_datatype` and
  `is_void` are the shortcuts a transport needs.
- Arguments and returns are validated independently at construction: positional
  only, fully annotated, and one of bool/int/float/str/Enum. Keyword-only
  arguments are rejected, pending the spike in #403.
- `Method` no longer forbids arguments or a return type; `Scan` keeps that
  restriction for itself.
- Transport capability is declared, not assumed. REST and GraphQL round-trip a
  typed call; Tango carries at most one argument and no enum; EPICS CA and PVA
  are void-only. Anything a transport cannot serve is skipped with a warning at
  start-up rather than failing the whole controller, and the EPICS GUI now
  honours `enabled` so it does not draw a control for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zpFjvbhUisfVUq3NH8M1G
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e28ad9bb-c645-47df-9e72-6a59246943ff

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.33333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.21%. Comparing base (e73453b) to head (d04bb64).
⚠️ Report is 2 commits behind head on refactor.

Files with missing lines Patch % Lines
src/fastcs/methods/command.py 98.30% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           refactor     #419      +/-   ##
============================================
- Coverage     91.25%   91.21%   -0.04%     
============================================
  Files            72       72              
  Lines          2892     3040     +148     
============================================
+ Hits           2639     2773     +134     
- Misses          253      267      +14     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

claude added 2 commits August 14, 2026 23:00
`Command[P, T]` renders a reference to a bare `P`, which has no target of its
own - the same problem the existing TypeVar entries in nitpick_ignore cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zpFjvbhUisfVUq3NH8M1G
The PVA provider, the Tango command collection and the EPICS GUI all now have
a branch for a command they cannot serve, and none of them were exercised.
The PVA provider builds without a server as long as there is a running loop,
and the Tango collection without a device server, so both are reachable here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zpFjvbhUisfVUq3NH8M1G
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.

2 participants