-
Notifications
You must be signed in to change notification settings - Fork 0
feat: stateIn() guard (0.7.0) #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,30 @@ | ||
| """Composable guard combinators (0.6.0). | ||
| """Composable guard combinators (0.6.0) + ``stateIn`` guard (0.7.0). | ||
|
|
||
| ``and_``, ``or_``, and ``not_`` compose guards from smaller pieces without | ||
| writing wrapper lambdas. Sub-guards can be callables or strings that reference | ||
| other named guards in the machine's ``guards`` registry. | ||
| writing wrapper lambdas. Sub-guards can be callables, strings that reference | ||
| other named guards in the machine's ``guards`` registry, or other composable | ||
| guards (including :func:`stateIn`). | ||
|
|
||
| ``stateIn`` is a guard over the *current configuration* — it passes when the | ||
| machine is in the given state — and composes with the combinators above. | ||
|
|
||
| Usage:: | ||
|
|
||
| from xstate import Machine, and_, not_, or_ | ||
| from xstate import Machine, and_, not_, or_, stateIn | ||
|
|
||
| machine = Machine(config, guards={ | ||
| "isLoggedIn": lambda ctx, evt: ctx.get("logged_in"), | ||
| "hasPermission": lambda ctx, evt: ctx.get("permission"), | ||
| "canGo": and_("isLoggedIn", "hasPermission"), | ||
| "canGo": and_("isLoggedIn", "hasPermission", stateIn("#ready")), | ||
| }) | ||
|
|
||
| Or via the ``setup()`` builder (recommended):: | ||
|
|
||
| from xstate import setup, and_ | ||
| from xstate import setup, and_, stateIn | ||
|
|
||
| machine = setup(guards={ | ||
| "isLoggedIn": lambda ctx, evt: ctx.get("logged_in"), | ||
| "canGo": and_("isLoggedIn", lambda ctx, evt: ctx.get("value") > 0), | ||
| "canGo": and_("isLoggedIn", stateIn("ready")), | ||
| }).create_machine(config) | ||
|
|
||
| String sub-guard names are resolved lazily from the machine's ``guards`` | ||
|
|
@@ -33,13 +37,21 @@ | |
|
|
||
|
|
||
| class _ComposableGuard: | ||
| """Base class for ``and_``, ``or_``, and ``not_`` guard combinators. | ||
| """Base class for ``and_``, ``or_``, ``not_``, and ``stateIn`` guards. | ||
|
|
||
| Instances are callable (``guard(context, event)``) and can be registered | ||
| directly in the machine's ``guards`` dict. | ||
| directly in the machine's ``guards`` dict. Evaluation threads the active | ||
| ``configuration`` through so nested ``stateIn`` sub-guards can see it. | ||
| """ | ||
|
|
||
| def _eval(self, guard: Any, context: Any, event: Any, registry: dict) -> bool: | ||
| def _eval( | ||
| self, | ||
| guard: Any, | ||
| context: Any, | ||
| event: Any, | ||
| registry: dict, | ||
| configuration: Any = None, | ||
| ) -> bool: | ||
| if isinstance(guard, str): | ||
| fn = registry.get(guard) | ||
| if fn is None: | ||
|
|
@@ -49,17 +61,23 @@ def _eval(self, guard: Any, context: Any, event: Any, registry: dict) -> bool: | |
| ) | ||
| guard = fn | ||
| if isinstance(guard, _ComposableGuard): | ||
| return guard._call(context, event, registry) | ||
| return guard._call(context, event, registry, configuration) | ||
| from xstate.handlers import invoke_handler | ||
|
|
||
| return bool(invoke_handler(guard, context, event)) | ||
|
Comment on lines
63
to
67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a bug when a composable guard (like Because all registered guards are wrapped in a To fix this, we should unwrap the from xstate.handlers import HandlerAdapter, invoke_handler
inner = guard.fn if isinstance(guard, HandlerAdapter) else guard
if isinstance(inner, _ComposableGuard):
return inner._call(context, event, registry, configuration)
return bool(invoke_handler(guard, context, event)) |
||
|
|
||
| def _call(self, context: Any, event: Any, registry: dict) -> bool: | ||
| def _call( | ||
| self, | ||
| context: Any, | ||
| event: Any, | ||
| registry: dict, | ||
| configuration: Any = None, | ||
| ) -> bool: | ||
| raise NotImplementedError | ||
|
|
||
| def __call__(self, context: Any = None, event: Any = None) -> bool: | ||
| """Direct call (no registry — string sub-guards must not be used).""" | ||
| return self._call(context, event, {}) | ||
| """Direct call (no registry/configuration — for standalone use).""" | ||
| return self._call(context, event, {}, None) | ||
|
|
||
|
|
||
| class _AndGuard(_ComposableGuard): | ||
|
|
@@ -68,8 +86,13 @@ class _AndGuard(_ComposableGuard): | |
| def __init__(self, guards: tuple[Any, ...]): | ||
| self._guards = guards | ||
|
|
||
| def _call(self, context: Any, event: Any, registry: dict) -> bool: | ||
| return all(self._eval(g, context, event, registry) for g in self._guards) | ||
| def _call( | ||
| self, context: Any, event: Any, registry: dict, configuration: Any = None | ||
| ) -> bool: | ||
| return all( | ||
| self._eval(g, context, event, registry, configuration) | ||
| for g in self._guards | ||
| ) | ||
|
|
||
|
|
||
| class _OrGuard(_ComposableGuard): | ||
|
|
@@ -78,8 +101,13 @@ class _OrGuard(_ComposableGuard): | |
| def __init__(self, guards: tuple[Any, ...]): | ||
| self._guards = guards | ||
|
|
||
| def _call(self, context: Any, event: Any, registry: dict) -> bool: | ||
| return any(self._eval(g, context, event, registry) for g in self._guards) | ||
| def _call( | ||
| self, context: Any, event: Any, registry: dict, configuration: Any = None | ||
| ) -> bool: | ||
| return any( | ||
| self._eval(g, context, event, registry, configuration) | ||
| for g in self._guards | ||
| ) | ||
|
|
||
|
|
||
| class _NotGuard(_ComposableGuard): | ||
|
|
@@ -88,8 +116,32 @@ class _NotGuard(_ComposableGuard): | |
| def __init__(self, guard: Any): | ||
| self._guard = guard | ||
|
|
||
| def _call(self, context: Any, event: Any, registry: dict) -> bool: | ||
| return not self._eval(self._guard, context, event, registry) | ||
| def _call( | ||
| self, context: Any, event: Any, registry: dict, configuration: Any = None | ||
| ) -> bool: | ||
| return not self._eval(self._guard, context, event, registry, configuration) | ||
|
|
||
|
|
||
| class _StateInGuard(_ComposableGuard): | ||
| """Guard that passes when the machine is in the given state (v5 ``stateIn``). | ||
|
|
||
| The *spec* uses the same syntax as a transition ``in`` guard: ``"#id"`` for | ||
| an explicit id, a dotted ``"parent.child"`` path, or a ``{parent: child}`` | ||
| dict. Evaluation needs the active configuration; when called without one | ||
| (e.g. directly, outside a transition) it returns ``False``. | ||
| """ | ||
|
|
||
| def __init__(self, spec: Any): | ||
| self._spec = spec | ||
|
|
||
| def _call( | ||
| self, context: Any, event: Any, registry: dict, configuration: Any = None | ||
| ) -> bool: | ||
| if configuration is None: | ||
| return False | ||
| from xstate.algorithm import _matches_in_state | ||
|
|
||
| return _matches_in_state(self._spec, configuration) | ||
|
Comment on lines
+140
to
+144
|
||
|
|
||
|
|
||
| def and_(*guards: Any) -> _AndGuard: | ||
|
|
@@ -105,3 +157,12 @@ def or_(*guards: Any) -> _OrGuard: | |
| def not_(guard: Any) -> _NotGuard: | ||
| """Return a guard that passes when *guard* does NOT pass.""" | ||
| return _NotGuard(guard) | ||
|
|
||
|
|
||
| def stateIn(spec: Any) -> _StateInGuard: | ||
| """Return a guard that passes when the machine is currently in *spec*. | ||
|
|
||
| *spec* is ``"#id"``, a dotted ``"parent.child"`` path, or a ``{parent: child}`` | ||
| dict — the same shape accepted by a transition ``in`` guard. | ||
|
Comment on lines
+165
to
+166
|
||
| """ | ||
| return _StateInGuard(spec) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a composable guard references a named
stateInguard, the registry value has already been wrapped inHandlerAdapter, so thisisinstance(..., _ComposableGuard)check misses it and the code falls through toinvoke_handler. That callsstateIn.__call__without the currentconfiguration, makingand_("isReady"),or_("isReady", ...), ornot_("isReady")evaluate as if the machine is never in that state even whenguards={"isReady": stateIn("ready")}is registered. This breaks the advertised composition of namedstateInguards; unwrapHandlerAdapterhere likecondition_match()does before calling_call(..., configuration).Useful? React with 👍 / 👎.