Add end-to-end generics to the public API - #40
Conversation
d248ebe to
9fb3b63
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9fb3b633e8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| class MachineConfig[ContextT = Any, EventDataT = Any, OutputT = Any]( | ||
| StateNodeConfig[ContextT, EventDataT, OutputT], total=False | ||
| ): | ||
| context: ContextT |
There was a problem hiding this comment.
Require the machine id in MachineConfig
Mark id as required on the root MachineConfig. Because it inherits the entirely optional StateNodeConfig, mypy currently accepts a value such as cfg: MachineConfig[...] = {"context": ..., "states": {}}, but passing that supposedly valid typed config to Machine(cfg) immediately raises InvalidConfigError because the constructor requires id.
Useful? React with 👍 / 👎.
| @overload | ||
| def from_promise[InputT, OutputT]( | ||
| fn: Callable[[InputT], OutputT | Awaitable[OutputT]], | ||
| ) -> PromiseLogic[InputT, OutputT]: ... |
There was a problem hiding this comment.
Preserve keyword-only promise factories
Model the named input parameter rather than only a positional callable. A supported factory such as def load(*, input: int) -> str is invoked successfully by _call_with_supported_kwargs(self._fn, input=...), but this overload rejects it as incompatible with Callable[[int], str], creating a static compatibility regression for the runtime's keyword-only input convention.
Useful? React with 👍 / 👎.
| @overload | ||
| def from_observable[InputT, OutputT]( | ||
| fn: Callable[[InputT], AsyncIterable[OutputT]], | ||
| ) -> ObservableLogic[InputT, OutputT]: ... |
There was a problem hiding this comment.
Accept zero-argument observable factories
Add an overload for callable sources that take no input. The runtime deliberately supports async def values(): yield ... because _call_with_supported_kwargs invokes it without undeclared arguments, and the previous broad annotation accepted it, but neither public overload now matches that callable, so mypy reports an incompatible argument for a valid from_observable(values) call.
Useful? React with 👍 / 👎.
| def create_actor[InputT, OutputT]( | ||
| logic: PromiseLogic[InputT, OutputT], | ||
| *, | ||
| id: str | None = None, | ||
| clock: Clock | None = None, | ||
| system: ActorSystem | None = None, | ||
| input: InputT = ..., | ||
| snapshot: ActorSnapshot[OutputT] | None = None, |
There was a problem hiding this comment.
Require input for input-dependent promise actors
Do not make input optional when PromiseLogic has a non-optional InputT. Mypy currently accepts create_actor(from_promise(double)) for def double(input: int) -> int, but the backend then supplies the implementation default None, causing the actor to settle in the error state instead of producing its typed output; only zero-argument or explicitly optional-input logic should allow this argument to be omitted.
Useful? React with 👍 / 👎.
Summary
Adds backward-compatible, defaulted generics across the public machine, snapshot, handler, interpreter, setup, and actor APIs.
This is layer 2 of 2. PR #39 merged first as
50d200f2f296c68c6b639a1f08112219a12bf960. This branch is rebased onto that exactmasterresult, and the rebased tree matches the previously reviewed tree.Type model
Event[PayloadT = Any]HandlerArgs[ContextT = Any, EventDataT = Any, OutputT = Any]State[ContextT = Any, EventDataT = Any, OutputT = Any]Machine[ContextT = Any, EventDataT = Any, OutputT = Any]MachineSetup[ContextT = Any, EventDataT = Any, OutputT = Any]Interpreter[ContextT = Any, EventDataT = Any, OutputT = Any]AsyncInterpreter[ContextT = Any, EventDataT = Any, OutputT = Any]Actor[SendEventT = Any, SnapshotT = Any, OutputT = Any]EventDataTrepresents the complete external event mapping, including itstypediscriminator, in line with XState event objects.HandlerArgs.eventcan also carry machine or actor output and runtime exceptions for done and error events.Scope
MachineSnapshot.ContextAdaptercontext-preserving.TypedDicthierarchy generic, including recursive state-node and state-value types.xstate.create_actorandspawn.to_promiseoutput.ActorSystem.get()safely widened.Machine,setup, sync and async interpreters, subscriptions, actors, andto_promisewithTypedDict,Literal, andassert_type.Compatibility
Anydefault.Machine(config, ...), raw JSON dictionaries, string events, and event dictionaries remain valid.types=runtime object, validation framework, generated code, runtime dependency, version bump, or release action is introduced.py.typedmarker remains included and the isolated wheel smoke verifies it.Static contract coverage
The primary test suite runs three strict-mypy fixtures:
MachineSnapshot, canonical handlers, context adapters, setup, sync and async sends, subscriptions, machine and non-machine actors, spawn, heterogeneous lookup, andto_promise;Eventpayload;Machine, raw dictionaries, string and dictionary sends, and legacy(context, event)handlers.Validation
Local validation on Python 3.14.4 at exact rebased head
9fb3b633e805a2ae7d427b2850a1f6f0d8629ea9:poetry run python -m pytest tests/ --ignore=tests/test_scxml.py- 435 passedpoetry run python -m pytest tests/test_scxml.py- 57 passedpoetry run mypy src/xstate/- passedpoetry run mypy --strict src/xstate/algorithm.py- passedpoetry run ruff check src/ tests/ scripts/ docs/examples/- passedpoetry run ruff format --check src/ tests/ scripts/ docs/examples/- passedpoetry check --lock- passedpoetry build- passedpoetry run python scripts/validate_distribution.py- installed-wheel smoke andpy.typedcheck passedHosted validation at exact head
9fb3b633e805a2ae7d427b2850a1f6f0d8629ea9:test (3.13, ubuntu-latest)- passedtest (3.14, ubuntu-latest)- passedSCXML smoke- passedcode-quality (3.14, ubuntu-latest)- passedRisk and rollback
The primary risk is an annotation becoming narrower than an accepted legacy runtime form. Defaulted type parameters, explicit compatibility fallbacks, and the legacy strict-mypy fixture limit that risk. Dynamic actor backends are widened internally and cast only where overload-selected public types establish the relationship.
Rollback is the single commit unique to this branch after PR #39. There are no storage, dependency, deployment, or migration effects.
Review focus
EventDataTis the whole event mapping and every send boundary consumesEventInput[EventDataT].State, interpreter subscriptions, machine actors, andto_promisepreserve context, snapshot, and output types.TypedDictconfigs and handler protocols improve opt-in checking without rejecting raw JSON or legacy callables.Merge order
masterand retargeted tomaster.Merge was authorized after exact-head verification. No version bump, release publication, or new conformance claim is included.