Skip to content

Add end-to-end generics to the public API - #40

Merged
JovaniPink merged 1 commit into
masterfrom
typed-public-api
Aug 23, 2026
Merged

Add end-to-end generics to the public API#40
JovaniPink merged 1 commit into
masterfrom
typed-public-api

Conversation

@JovaniPink

@JovaniPink JovaniPink commented Aug 23, 2026

Copy link
Copy Markdown
Owner

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 exact master result, 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]

EventDataT represents the complete external event mapping, including its type discriminator, in line with XState event objects. HandlerArgs.event can also carry machine or actor output and runtime exceptions for done and error events.

Scope

  • Carry context, external event, and output types through pure transitions and MachineSnapshot.
  • Type sync and async interpreter sends, state fields, and subscription callbacks.
  • Make ContextAdapter context-preserving.
  • Add canonical protocols for action, guard, delay, assignment, and output handlers.
  • Make the config TypedDict hierarchy generic, including recursive state-node and state-value types.
  • Export config, handler, event-input, runtime-event-payload, and state-value types from xstate.
  • Add typed machine, promise, callback, and observable actor overloads for create_actor and spawn.
  • Preserve exact machine-backed actor snapshots and to_promise output.
  • Keep heterogeneous ActorSystem.get() safely widened.
  • Document typed Machine, setup, sync and async interpreters, subscriptions, actors, and to_promise with TypedDict, Literal, and assert_type.

Compatibility

  • Every public generic has an Any default.
  • Machine(config, ...), raw JSON dictionaries, string events, and event dictionaries remain valid.
  • Legacy zero-, one-, two-, and keyword-argument handlers remain runtime-compatible.
  • No types= runtime object, validation framework, generated code, runtime dependency, version bump, or release action is introduced.
  • The existing py.typed marker remains included and the isolated wheel smoke verifies it.

Static contract coverage

The primary test suite runs three strict-mypy fixtures:

  • positive: context, full event objects, output, MachineSnapshot, canonical handlers, context adapters, setup, sync and async sends, subscriptions, machine and non-machine actors, spawn, heterogeneous lookup, and to_promise;
  • negative: invalid event payload, invalid interpreter send, invalid snapshot assignments, invalid guard and output returns, and invalid Event payload;
  • compatibility: unparameterized 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 passed
  • poetry run python -m pytest tests/test_scxml.py - 57 passed
  • poetry run mypy src/xstate/ - passed
  • poetry run mypy --strict src/xstate/algorithm.py - passed
  • strict positive, negative, and compatibility consumer fixtures - passed through the primary suite
  • poetry run ruff check src/ tests/ scripts/ docs/examples/ - passed
  • poetry run ruff format --check src/ tests/ scripts/ docs/examples/ - passed
  • poetry check --lock - passed
  • poetry build - passed
  • poetry run python scripts/validate_distribution.py - installed-wheel smoke and py.typed check passed

Hosted validation at exact head 9fb3b633e805a2ae7d427b2850a1f6f0d8629ea9:

  • test (3.13, ubuntu-latest) - passed
  • test (3.14, ubuntu-latest) - passed
  • SCXML smoke - passed
  • code-quality (3.14, ubuntu-latest) - passed

Risk 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

  1. Confirm EventDataT is the whole event mapping and every send boundary consumes EventInput[EventDataT].
  2. Confirm State, interpreter subscriptions, machine actors, and to_promise preserve context, snapshot, and output types.
  3. Confirm actor overloads stay precise for known logic while system lookup remains heterogeneous.
  4. Confirm generic TypedDict configs and handler protocols improve opt-in checking without rejecting raw JSON or legacy callables.
  5. Review the negative fixture for meaningful defect sensitivity rather than error-count-only assertions.

Merge order

  1. PR Fix SCXML macrostep ordering and type the execution core #39 merged first.
  2. This branch was rebased onto the resulting master and retargeted to master.
  3. Merge this PR only after all checks pass at the exact rebased head.

Merge was authorized after exact-head verification. No version bump, release publication, or new conformance claim is included.

@JovaniPink
JovaniPink changed the base branch from scxml-core-ordering-types to master August 23, 2026 18:20
@JovaniPink
JovaniPink marked this pull request as ready for review August 23, 2026 18:22
@JovaniPink JovaniPink closed this Aug 23, 2026
@JovaniPink JovaniPink reopened this Aug 23, 2026
@JovaniPink
JovaniPink merged commit c840369 into master Aug 23, 2026
4 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/xstate/schema.py
Comment on lines +126 to +129
class MachineConfig[ContextT = Any, EventDataT = Any, OutputT = Any](
StateNodeConfig[ContextT, EventDataT, OutputT], total=False
):
context: ContextT

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread src/xstate/actor.py
Comment on lines +126 to +129
@overload
def from_promise[InputT, OutputT](
fn: Callable[[InputT], OutputT | Awaitable[OutputT]],
) -> PromiseLogic[InputT, OutputT]: ...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread src/xstate/actor.py
Comment on lines +172 to +175
@overload
def from_observable[InputT, OutputT](
fn: Callable[[InputT], AsyncIterable[OutputT]],
) -> ObservableLogic[InputT, OutputT]: ...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread src/xstate/actor.py
Comment on lines +941 to +948
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

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.

1 participant