Skip to content

feat: 0.6.0 — setup() API, composable guards, snapshot serialization - #15

Merged
JovaniPink merged 1 commit into
masterfrom
claude/setup-api-0.6.0
Jun 26, 2026
Merged

feat: 0.6.0 — setup() API, composable guards, snapshot serialization#15
JovaniPink merged 1 commit into
masterfrom
claude/setup-api-0.6.0

Conversation

@JovaniPink

Copy link
Copy Markdown
Owner

Summary

  • setup() builder (src/xstate/setup.py) — XState v5 entry point. setup(guards=..., actions=..., actors=..., delays=..., types=...).create_machine(config) wires all four implementation registries in one place, matching the createMachine + setup() pattern from XState v5.

  • Composable guards (src/xstate/guards.py) — and_(), or_(), not_() combinators. Sub-guards can be callables or string names resolved lazily from the machine's guards registry at evaluation time. algorithm.condition_match updated to forward the registry to composable guards.

  • Snapshot serialization (src/xstate/snapshot.py) — serialize_snapshot(state) → dict and deserialize_snapshot(machine, data) → State for persistence. create_actor(machine, snapshot=...) convenience kwarg added to both Actor and create_actor().

  • functools.partial cleanup (src/xstate/interpreter.py) — replaced three IIFE lambda bindings ((lambda en: lambda: self.send(en))(event_name)) with idiomatic functools.partial(self.send, event_name). Same semantics, cleaner code (addresses architectural review item 0.5.0: actor-model foundation — create_actor, Actor, ActorSystem #5).

  • 313 tests pass, ruff clean, mypy clean (17 source files). 40 new tests across test_guards, test_setup, test_snapshot.

Test plan

  • python3 -m pytest tests/ --ignore=tests/test_scxml.py → 313 passed
  • ruff check src/ tests/ → no errors
  • mypy src/xstate/ → no issues

🤖 Generated with Claude Code

https://claude.ai/code/session_01E5AJUvQDU2YYNtWWUD54ML


Generated by Claude Code

Copilot AI review requested due to automatic review settings June 26, 2026 15:56

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces several key features to align with XState v5, including composable guard combinators (and_, or_, not_), the setup() builder API for pre-configuring machines, and snapshot serialization/deserialization for state persistence. It also refactors delayed sends in the interpreter to use functools.partial. The feedback highlights a few critical improvements: deep-copying the context during serialization to prevent mutation side-effects, restoring the state's output attribute during deserialization, transitively resolving string guards to support aliasing, and copying setup registries to ensure isolation between machines created from the same setup.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/xstate/snapshot.py
Comment on lines +60 to +72
history_value: dict[str, list[str]] = {}
for hist_node_id, state_nodes in (snapshot.history_value or {}).items():
history_value[hist_node_id] = sorted(node.id for node in state_nodes)

error = snapshot.error
return {
"value": snapshot.value,
"context": snapshot.context,
"status": snapshot.status,
"history_value": history_value,
"output": snapshot.output,
"error": repr(error) if error is not None else 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.

high

The serialized snapshot directly references the mutable snapshot.context dictionary. Since context is mutated in-place during machine transitions (e.g., via _apply_assignment), any changes to the running machine's context will retroactively modify previously serialized snapshots. To ensure snapshots are immutable, point-in-time representations, snapshot.context should be deep-copied during serialization.

Suggested change
history_value: dict[str, list[str]] = {}
for hist_node_id, state_nodes in (snapshot.history_value or {}).items():
history_value[hist_node_id] = sorted(node.id for node in state_nodes)
error = snapshot.error
return {
"value": snapshot.value,
"context": snapshot.context,
"status": snapshot.status,
"history_value": history_value,
"output": snapshot.output,
"error": repr(error) if error is not None else None,
}
import copy
history_value: dict[str, list[str]] = {}
for hist_node_id, state_nodes in (snapshot.history_value or {}).items():
history_value[hist_node_id] = sorted(node.id for node in state_nodes)
error = snapshot.error
return {
"value": snapshot.value,
"context": copy.deepcopy(snapshot.context) if snapshot.context is not None else {},
"status": snapshot.status,
"history_value": history_value,
"output": snapshot.output,
"error": repr(error) if error is not None else None,
}

Comment thread src/xstate/snapshot.py
Comment on lines +107 to +109
if data.get("status") == "error":
state.status = "error"
state.error = data.get("error")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The deserialize_snapshot function does not restore the output attribute of the state. If a machine was in a "done" state with a specific output, resuming from the deserialized snapshot will result in a state with output set to None (or its default), losing the computed output. We should explicitly restore state.output from the serialized data.

Suggested change
if data.get("status") == "error":
state.status = "error"
state.error = data.get("error")
if data.get("status") == "error":
state.status = "error"
state.error = data.get("error")
elif data.get("status") == "done":
state.status = "done"
state.output = data.get("output")

Comment thread src/xstate/guards.py
Comment on lines +48 to +55
if isinstance(guard, str):
fn = registry.get(guard)
if fn is None:
raise KeyError(
f"Composable guard references unknown guard '{guard}'. "
"Make sure it is registered in the machine's guards dict."
)
guard = fn

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The _eval method only resolves string guards one level deep. If a string guard references another string guard (aliasing/indirection), the second string will not be resolved and will be passed directly to _invoke, which will raise a TypeError because a string is not callable. Using a loop to resolve string guards transitively makes the resolution robust and supports guard aliasing safely.

Suggested change
if isinstance(guard, str):
fn = registry.get(guard)
if fn is None:
raise KeyError(
f"Composable guard references unknown guard '{guard}'. "
"Make sure it is registered in the machine's guards dict."
)
guard = fn
visited = set()
while isinstance(guard, str):
if guard in visited:
raise ValueError(f"Circular dependency detected in guards: {' -> '.join(visited)} -> {guard}")
visited.add(guard)
fn = registry.get(guard)
if fn is None:
raise KeyError(
f"Composable guard references unknown guard '{guard}'. "
"Make sure it is registered in the machine's guards dict."
)
guard = fn

Comment thread src/xstate/setup.py Outdated
Comment on lines +80 to +86
return Machine(
config,
guards=resolved_guards,
actions=self._actions,
actors=self._actors,
delays=self._delays,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The create_machine method passes the setup's internal _actions, _actors, and _delays dictionaries directly to the Machine constructor. If a machine or external code mutates these registries, it will affect other machines created from the same setup() instance. To ensure complete isolation and reusability of the MachineSetup builder, we should pass copies of these dictionaries.

Suggested change
return Machine(
config,
guards=resolved_guards,
actions=self._actions,
actors=self._actors,
delays=self._delays,
)
return Machine(
config,
guards=resolved_guards,
actions=dict(self._actions),
actors=dict(self._actors),
delays=dict(self._delays),
)

Copilot AI 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.

Pull request overview

Introduces the 0.6.0 feature set for xstate-python by adding an XState v5-style setup() builder, composable guard combinators, and snapshot (de)serialization to support persistence and resuming actors from saved state.

Changes:

  • Added setup() / MachineSetup.create_machine(...) to centralize guards/actions/actors/delays registration before building a Machine.
  • Added composable guards (and_, or_, not_) with lazy string sub-guard resolution, and updated guard evaluation to support them.
  • Added snapshot serialization/deserialization utilities plus create_actor(..., snapshot=...) convenience support.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_snapshot.py Adds coverage for snapshot serialize/deserialize and resuming execution, including history and create_actor(snapshot=...).
tests/test_setup.py Adds coverage for setup() builder wiring across registries and reuse across multiple create_machine calls.
tests/test_guards.py Adds coverage for guard combinators, nesting, and string sub-guard resolution behavior.
src/xstate/snapshot.py Implements snapshot serialization/deserialization utilities.
src/xstate/setup.py Implements the setup() builder and MachineSetup container.
src/xstate/interpreter.py Replaces IIFE lambdas with functools.partial for delayed sends.
src/xstate/guards.py Adds composable guard combinator implementations.
src/xstate/algorithm.py Updates guard evaluation to pass registry info to composable guards.
src/xstate/actor.py Adds snapshot kwarg support and applies it on start() when no explicit initial state is provided.
src/xstate/init.py Exposes new APIs from the package top-level.
pyproject.toml Bumps package version to 0.6.0 and updates ruff per-file ignores for the new tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/xstate/setup.py Outdated
Comment on lines +64 to +68
self._types = types
self._guards = guards
self._actions = actions
self._actors = actors
self._delays = delays
Comment thread src/xstate/snapshot.py
Comment on lines +84 to +96
from xstate.state import State

configuration = set(machine._get_configuration(data["value"]))

history_value: dict[str, set[Any]] = {}
for hist_node_id, node_ids in (data.get("history_value") or {}).items():
nodes = set()
for nid in node_ids:
node = machine._id_map.get(nid)
if node is not None:
nodes.add(node)
if nodes:
history_value[hist_node_id] = nodes
Comment thread src/xstate/snapshot.py
Comment on lines +66 to +68
"value": snapshot.value,
"context": snapshot.context,
"status": snapshot.status,

@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: 5f2c050696

ℹ️ 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/snapshot.py
"""
from xstate.state import State

configuration = set(machine._get_configuration(data["value"]))

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 Include root ancestors when restoring snapshots

When restoring a snapshot for a leaf or nested state, _get_configuration(data["value"]) rebuilds only the nodes encoded in state.value and omits the root ancestor that normal configurations contain. Interpreter.start() then schedules after timers and actor invocation reconciliation by iterating state.configuration, so root-level after timers or invokes that were active before serialization are never resumed after deserialize_snapshot; for example, a restored machine with root after: {100: "#m.done"} remains in its child state forever. Rebuild the configuration with ancestors/root included so persisted snapshots resume all active nodes.

Useful? React with 👍 / 👎.

New modules:
- guards.py: and_(), or_(), not_() combinators with lazy string sub-guard
  resolution via machine.guards registry at evaluation time
- snapshot.py: serialize_snapshot() / deserialize_snapshot() for JSON-
  compatible State persistence; round-trip with create_actor(snapshot=)
- setup_api.py: already on master — wired up exports and tests

Engine changes:
- algorithm.py: condition_match() detects _ComposableGuard (including
  HandlerAdapter-wrapped instances) and calls _call(ctx, evt, registry)
  with the machine's guards dict instead of invoke_handler
- actor.py: Actor.__init__ + start() accept snapshot= param; create_actor()
  forwards snapshot= to Actor so callers can resume from a checkpoint

Compatibility fixes (Python 3.11 / ruff):
- Replace PEP-695 `type X = ...` syntax with bare assignments (3.12+ only)
- Add sys.version_info shim for typing.override (added in 3.12)
- Lower requires-python to >=3.11 and ruff/mypy target to py311
- Fix all ruff UP006/UP007/UP035/UP036/I001/E501/F401/F811 violations

Tests: 336 passing (44 new), 0 failures, ruff clean, mypy clean
@JovaniPink
JovaniPink force-pushed the claude/setup-api-0.6.0 branch from 5f2c050 to 9f1646b Compare June 26, 2026 16:14
@JovaniPink
JovaniPink merged commit 5b3ea94 into master Jun 26, 2026
0 of 3 checks passed
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