python-statemachine 2.3.1__py3-none-any.whl → 2.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,159 +1,151 @@
1
- from collections import namedtuple
1
+ from dataclasses import dataclass
2
2
  from operator import attrgetter
3
+ from typing import TYPE_CHECKING
3
4
  from typing import Any
5
+ from typing import Callable
4
6
  from typing import Generator
7
+ from typing import Iterable
8
+ from typing import Set
5
9
  from typing import Tuple
6
10
 
11
+ from statemachine.callbacks import SpecReference
12
+
7
13
  from .signature import SignatureAdapter
8
14
 
15
+ if TYPE_CHECKING:
16
+ from .callbacks import CallbackSpec
17
+ from .callbacks import CallbackSpecList
18
+
9
19
 
10
- class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs resolver_id")):
11
- """Configuration for objects passed to resolver_factory.
20
+ @dataclass
21
+ class Listener:
22
+ """Object reference that provides attributes to be used as callbacks.
12
23
 
13
24
  Args:
14
25
  obj: Any object that will serve as lookup for attributes.
15
26
  skip_attrs: Protected attrs that will be ignored on the search.
16
27
  """
17
28
 
29
+ obj: object
30
+ all_attrs: Set[str]
31
+ resolver_id: str
32
+
18
33
  @classmethod
19
- def from_obj(cls, obj, skip_attrs=None) -> "ObjectConfig":
20
- if isinstance(obj, ObjectConfig):
34
+ def from_obj(cls, obj, skip_attrs=None) -> "Listener":
35
+ if isinstance(obj, Listener):
21
36
  return obj
22
37
  else:
23
- return cls(obj, skip_attrs or set(), str(id(obj)))
24
-
25
-
26
- class WrapSearchResult:
27
- def __init__(self, attribute, resolver_id) -> None:
28
- self.attribute = attribute
29
- self.resolver_id = resolver_id
30
- self._cache = None
31
- self.unique_key = f"{attribute}@{resolver_id}"
32
-
33
- def __repr__(self):
34
- return f"{type(self).__name__}({self.unique_key})"
35
-
36
- def wrap(self): # pragma: no cover
37
- pass
38
-
39
- async def __call__(self, *args: Any, **kwds: Any) -> Any:
40
- if self._cache is None:
41
- self._cache = self.wrap()
42
- assert self._cache
43
- return await self._cache(*args, **kwds)
44
-
45
-
46
- class CallableSearchResult(WrapSearchResult):
47
- def __init__(self, attribute, a_callable, resolver_id) -> None:
48
- self.a_callable = a_callable
49
- super().__init__(attribute, resolver_id)
50
-
51
- def wrap(self):
52
- return SignatureAdapter.wrap(self.a_callable)
53
-
54
-
55
- class AttributeCallableSearchResult(WrapSearchResult):
56
- def __init__(self, attribute, obj, resolver_id) -> None:
57
- self.obj = obj
58
- super().__init__(attribute, resolver_id)
59
-
60
- def wrap(self):
61
- # if `attr` is not callable, then it's an attribute or property,
62
- # so `func` contains it's current value.
63
- # we'll build a method that get's the fresh value for each call
64
- getter = attrgetter(self.attribute)
65
-
66
- async def wrapper(*args, **kwargs):
67
- return getter(self.obj)
68
-
69
- return wrapper
70
-
71
-
72
- class EventSearchResult(WrapSearchResult):
73
- def __init__(self, attribute, func, resolver_id) -> None:
74
- self.func = func
75
- super().__init__(attribute, resolver_id)
76
-
77
- def wrap(self):
78
- "Events already have the 'machine' parameter defined."
38
+ if skip_attrs is None:
39
+ skip_attrs = set()
40
+ all_attrs = set(dir(obj)) - skip_attrs
41
+ return cls(obj, all_attrs, str(id(obj)))
79
42
 
80
- async def wrapper(*args, **kwargs):
81
- kwargs.pop("machine", None)
82
- return await self.func(*args, **kwargs)
83
43
 
84
- return wrapper
44
+ @dataclass
45
+ class Listeners:
46
+ """Listeners that provides attributes to be used as callbacks."""
85
47
 
48
+ items: Tuple[Listener, ...]
49
+ all_attrs: Set[str]
86
50
 
87
- def _search_callable_attr_is_property(
88
- attr, configs: Tuple[ObjectConfig, ...]
89
- ) -> "WrapSearchResult | None":
90
- # if the attr is a property, we'll try to find the object that has the
91
- # property on the configs
92
- attr_name = attr.fget.__name__
93
- for obj, _skip_attrs, resolver_id in configs:
94
- func = getattr(type(obj), attr_name, None)
95
- if func is not None and func is attr:
96
- return AttributeCallableSearchResult(attr_name, obj, resolver_id)
97
- return None
98
-
99
-
100
- def _search_callable_attr_is_callable(attr, configs: Tuple[ObjectConfig, ...]) -> WrapSearchResult:
101
- # if the attr is an unbounded method, we'll try to find the bounded method
102
- # on the configs
103
- if not hasattr(attr, "__self__"):
104
- for obj, _skip_attrs, resolver_id in configs:
105
- func = getattr(obj, attr.__name__, None)
106
- if func is not None and func.__func__ is attr:
107
- return CallableSearchResult(attr.__name__, func, resolver_id)
108
-
109
- return CallableSearchResult(attr, attr, None)
110
-
111
-
112
- def _search_callable_in_configs(
113
- attr, configs: Tuple[ObjectConfig, ...]
114
- ) -> Generator[WrapSearchResult, None, None]:
115
- for obj, skip_attrs, resolver_id in configs:
116
- if attr in skip_attrs:
117
- continue
118
-
119
- if not hasattr(obj, attr):
120
- continue
121
-
122
- func = getattr(obj, attr)
123
- if not callable(func):
124
- yield AttributeCallableSearchResult(attr, obj, resolver_id)
125
-
126
- if getattr(func, "_is_sm_event", False):
127
- yield EventSearchResult(attr, func, resolver_id)
128
-
129
- yield CallableSearchResult(attr, func, resolver_id)
130
-
131
-
132
- def search_callable(
133
- attr, configs: Tuple[ObjectConfig, ...]
134
- ) -> Generator[WrapSearchResult, None, None]: # noqa: C901
135
- if isinstance(attr, property):
136
- result = _search_callable_attr_is_property(attr, configs)
137
- if result is not None:
138
- yield result
139
- return
140
-
141
- if callable(attr):
142
- yield _search_callable_attr_is_callable(attr, configs)
143
- return
144
-
145
- yield from _search_callable_in_configs(attr, configs)
146
-
147
-
148
- def resolver_factory(objects: Tuple[ObjectConfig, ...]):
149
- """Factory that returns a configured resolver."""
150
-
151
- def resolver(attr) -> Generator[WrapSearchResult, None, None]:
152
- yield from search_callable(attr, objects)
153
-
154
- return resolver
51
+ @classmethod
52
+ def from_listeners(cls, listeners: Iterable["Listener"]) -> "Listeners":
53
+ listeners = tuple(listeners)
54
+ all_attrs = set().union(*(listener.all_attrs for listener in listeners))
55
+ return cls(listeners, all_attrs)
56
+
57
+ def resolve(self, specs: "CallbackSpecList", registry):
58
+ found_convention_specs = specs.conventional_specs & self.all_attrs
59
+ filtered_specs = [
60
+ spec for spec in specs if not spec.is_convention or spec.func in found_convention_specs
61
+ ]
62
+ if not filtered_specs:
63
+ return
64
+
65
+ for spec in filtered_specs:
66
+ registry[spec.group.build_key(specs)]._add(spec, self)
67
+
68
+ def search(self, spec: "CallbackSpec") -> Generator["Callable", None, None]:
69
+ if spec.reference is SpecReference.NAME:
70
+ yield from self._search_name(spec.func)
71
+ return
72
+ elif spec.reference is SpecReference.CALLABLE:
73
+ yield self._search_callable(spec)
74
+ return
75
+ elif spec.reference is SpecReference.PROPERTY:
76
+ result = self._search_property(spec)
77
+ if result is not None:
78
+ yield result
79
+ return
80
+ else: # never reached here from tests but put an exception for safety. pragma: no cover
81
+ raise ValueError(f"Invalid reference {spec.reference}")
82
+
83
+ def _search_property(self, spec) -> "Callable | None":
84
+ # if the attr is a property, we'll try to find the object that has the
85
+ # property on the configs
86
+ attr_name = spec.attr_name
87
+ if attr_name not in self.all_attrs:
88
+ return None
89
+ for config in self.items:
90
+ func = getattr(type(config.obj), attr_name, None)
91
+ if func is not None and func is spec.func:
92
+ return attr_method(attr_name, config.obj, config.resolver_id)
93
+ return None
94
+
95
+ def _search_callable(self, spec) -> "Callable":
96
+ # if the attr is an unbounded method, we'll try to find the bounded method
97
+ # on the self
98
+ if not spec.is_bounded:
99
+ for config in self.items:
100
+ func = getattr(config.obj, spec.attr_name, None)
101
+ if func is not None and func.__func__ is spec.func:
102
+ return callable_method(spec.attr_name, func, config.resolver_id)
103
+
104
+ return callable_method(spec.func, spec.func, None)
105
+
106
+ def _search_name(self, name) -> Generator["Callable", None, None]:
107
+ for config in self.items:
108
+ if name not in config.all_attrs:
109
+ continue
110
+
111
+ func = getattr(config.obj, name)
112
+ if not callable(func):
113
+ yield attr_method(name, config.obj, config.resolver_id)
114
+ continue
115
+
116
+ if getattr(func, "_is_sm_event", False):
117
+ yield event_method(name, func, config.resolver_id)
118
+ continue
119
+
120
+ yield callable_method(name, func, config.resolver_id)
121
+
122
+
123
+ def callable_method(attribute, a_callable, resolver_id) -> Callable:
124
+ method = SignatureAdapter.wrap(a_callable)
125
+ method.unique_key = f"{attribute}@{resolver_id}" # type: ignore[attr-defined]
126
+ method.__name__ = a_callable.__name__
127
+ method.__doc__ = a_callable.__doc__
128
+ return method
129
+
130
+
131
+ def attr_method(attribute, obj, resolver_id) -> Callable:
132
+ getter = attrgetter(attribute)
133
+
134
+ def method(*args, **kwargs):
135
+ return getter(obj)
136
+
137
+ method.unique_key = f"{attribute}@{resolver_id}" # type: ignore[attr-defined]
138
+ return method
139
+
140
+
141
+ def event_method(attribute, func, resolver_id) -> Callable:
142
+ def method(*args, **kwargs):
143
+ kwargs.pop("machine", None)
144
+ return func(*args, **kwargs)
145
+
146
+ method.unique_key = f"{attribute}@{resolver_id}" # type: ignore[attr-defined]
147
+ return method
155
148
 
156
149
 
157
150
  def resolver_factory_from_objects(*objects: Tuple[Any, ...]):
158
- configs: Tuple[ObjectConfig, ...] = tuple(ObjectConfig.from_obj(o) for o in objects)
159
- return resolver_factory(configs)
151
+ return Listeners.from_listeners(Listener.from_obj(o) for o in objects)
File without changes
@@ -0,0 +1,136 @@
1
+ from threading import Lock
2
+ from typing import TYPE_CHECKING
3
+ from weakref import proxy
4
+
5
+ from ..event_data import EventData
6
+ from ..event_data import TriggerData
7
+ from ..exceptions import InvalidDefinition
8
+ from ..exceptions import TransitionNotAllowed
9
+ from ..i18n import _
10
+ from ..transition import Transition
11
+
12
+ if TYPE_CHECKING:
13
+ from ..statemachine import StateMachine
14
+
15
+
16
+ class AsyncEngine:
17
+ def __init__(self, sm: "StateMachine", rtc: bool = True):
18
+ self.sm = proxy(sm)
19
+ self._sentinel = object()
20
+ if not rtc:
21
+ raise InvalidDefinition(_("Only RTC is supported on async engine"))
22
+ self._processing = Lock()
23
+
24
+ async def activate_initial_state(self):
25
+ """
26
+ Activate the initial state.
27
+
28
+ Called automatically on state machine creation from sync code, but in
29
+ async code, the user must call this method explicitly.
30
+
31
+ Given how async works on python, there's no built-in way to activate the initial state that
32
+ may depend on async code from the StateMachine.__init__ method.
33
+ """
34
+ return await self._processing_loop()
35
+
36
+ async def _processing_loop(self):
37
+ """Process event triggers.
38
+
39
+ The simplest implementation is the non-RTC (synchronous),
40
+ where the trigger will be run immediately and the result collected as the return.
41
+
42
+ .. note::
43
+
44
+ While processing the trigger, if others events are generated, they
45
+ will also be processed immediately, so a "nested" behavior happens.
46
+
47
+ If the machine is on ``rtc`` model (queued), the event is put on a queue, and only the
48
+ first event will have the result collected.
49
+
50
+ .. note::
51
+ While processing the queue items, if others events are generated, they
52
+ will be processed sequentially (and not nested).
53
+
54
+ """
55
+ # We make sure that only the first event enters the processing critical section,
56
+ # next events will only be put on the queue and processed by the same loop.
57
+ if not self._processing.acquire(blocking=False):
58
+ return None
59
+
60
+ # We will collect the first result as the processing result to keep backwards compatibility
61
+ # so we need to use a sentinel object instead of `None` because the first result may
62
+ # be also `None`, and on this case the `first_result` may be overridden by another result.
63
+ first_result = self._sentinel
64
+ try:
65
+ # Execute the triggers in the queue in FIFO order until the queue is empty
66
+ while self.sm._external_queue:
67
+ trigger_data = self.sm._external_queue.popleft()
68
+ try:
69
+ result = await self._trigger(trigger_data)
70
+ if first_result is self._sentinel:
71
+ first_result = result
72
+ except Exception:
73
+ # Whe clear the queue as we don't have an expected behavior
74
+ # and cannot keep processing
75
+ self.sm._external_queue.clear()
76
+ raise
77
+ finally:
78
+ self._processing.release()
79
+ return first_result if first_result is not self._sentinel else None
80
+
81
+ async def _trigger(self, trigger_data: TriggerData):
82
+ event_data = None
83
+ if trigger_data.event == "__initial__":
84
+ transition = Transition(None, self.sm._get_initial_state(), event="__initial__")
85
+ transition._specs.clear()
86
+ event_data = EventData(trigger_data=trigger_data, transition=transition)
87
+ await self._activate(event_data)
88
+ return self._sentinel
89
+
90
+ state = self.sm.current_state
91
+ for transition in state.transitions:
92
+ if not transition.match(trigger_data.event):
93
+ continue
94
+
95
+ event_data = EventData(trigger_data=trigger_data, transition=transition)
96
+ args, kwargs = event_data.args, event_data.extended_kwargs
97
+ await self.sm._get_callbacks(transition.validators.key).async_call(*args, **kwargs)
98
+ if not await self.sm._get_callbacks(transition.cond.key).async_all(*args, **kwargs):
99
+ continue
100
+
101
+ result = await self._activate(event_data)
102
+ event_data.result = result
103
+ event_data.executed = True
104
+ break
105
+ else:
106
+ if not self.sm.allow_event_without_transition:
107
+ raise TransitionNotAllowed(trigger_data.event, state)
108
+
109
+ return event_data.result if event_data else None
110
+
111
+ async def _activate(self, event_data: EventData):
112
+ args, kwargs = event_data.args, event_data.extended_kwargs
113
+ transition = event_data.transition
114
+ source = event_data.state
115
+ target = transition.target
116
+
117
+ result = await self.sm._get_callbacks(transition.before.key).async_call(*args, **kwargs)
118
+ if source is not None and not transition.internal:
119
+ await self.sm._get_callbacks(source.exit.key).async_call(*args, **kwargs)
120
+
121
+ result += await self.sm._get_callbacks(transition.on.key).async_call(*args, **kwargs)
122
+
123
+ self.sm.current_state = target
124
+ event_data.state = target
125
+ kwargs["state"] = target
126
+
127
+ if not transition.internal:
128
+ await self.sm._get_callbacks(target.enter.key).async_call(*args, **kwargs)
129
+ await self.sm._get_callbacks(transition.after.key).async_call(*args, **kwargs)
130
+
131
+ if len(result) == 0:
132
+ result = None
133
+ elif len(result) == 1:
134
+ result = result[0]
135
+
136
+ return result
@@ -0,0 +1,139 @@
1
+ from threading import Lock
2
+ from typing import TYPE_CHECKING
3
+ from weakref import proxy
4
+
5
+ from ..event_data import EventData
6
+ from ..event_data import TriggerData
7
+ from ..exceptions import TransitionNotAllowed
8
+ from ..transition import Transition
9
+
10
+ if TYPE_CHECKING:
11
+ from ..statemachine import StateMachine
12
+
13
+
14
+ class SyncEngine:
15
+ def __init__(self, sm: "StateMachine", rtc: bool = True):
16
+ self.sm = proxy(sm)
17
+ self._sentinel = object()
18
+ self._rtc = rtc
19
+ self._processing = Lock()
20
+ self.activate_initial_state()
21
+
22
+ def activate_initial_state(self):
23
+ """
24
+ Activate the initial state.
25
+
26
+ Called automatically on state machine creation from sync code, but in
27
+ async code, the user must call this method explicitly.
28
+
29
+ Given how async works on python, there's no built-in way to activate the initial state that
30
+ may depend on async code from the StateMachine.__init__ method.
31
+ """
32
+ return self._processing_loop()
33
+
34
+ def _processing_loop(self):
35
+ """Process event triggers.
36
+
37
+ The simplest implementation is the non-RTC (synchronous),
38
+ where the trigger will be run immediately and the result collected as the return.
39
+
40
+ .. note::
41
+
42
+ While processing the trigger, if others events are generated, they
43
+ will also be processed immediately, so a "nested" behavior happens.
44
+
45
+ If the machine is on ``rtc`` model (queued), the event is put on a queue, and only the
46
+ first event will have the result collected.
47
+
48
+ .. note::
49
+ While processing the queue items, if others events are generated, they
50
+ will be processed sequentially (and not nested).
51
+
52
+ """
53
+ if not self._rtc:
54
+ # The machine is in "synchronous" mode
55
+ trigger_data = self.sm._external_queue.popleft()
56
+ return self._trigger(trigger_data)
57
+
58
+ # We make sure that only the first event enters the processing critical section,
59
+ # next events will only be put on the queue and processed by the same loop.
60
+ if not self._processing.acquire(blocking=False):
61
+ return None
62
+
63
+ # We will collect the first result as the processing result to keep backwards compatibility
64
+ # so we need to use a sentinel object instead of `None` because the first result may
65
+ # be also `None`, and on this case the `first_result` may be overridden by another result.
66
+ first_result = self._sentinel
67
+ try:
68
+ # Execute the triggers in the queue in FIFO order until the queue is empty
69
+ while self.sm._external_queue:
70
+ trigger_data = self.sm._external_queue.popleft()
71
+ try:
72
+ result = self._trigger(trigger_data)
73
+ if first_result is self._sentinel:
74
+ first_result = result
75
+ except Exception:
76
+ # Whe clear the queue as we don't have an expected behavior
77
+ # and cannot keep processing
78
+ self.sm._external_queue.clear()
79
+ raise
80
+ finally:
81
+ self._processing.release()
82
+ return first_result if first_result is not self._sentinel else None
83
+
84
+ def _trigger(self, trigger_data: TriggerData):
85
+ event_data = None
86
+ if trigger_data.event == "__initial__":
87
+ transition = Transition(None, self.sm._get_initial_state(), event="__initial__")
88
+ transition._specs.clear()
89
+ event_data = EventData(trigger_data=trigger_data, transition=transition)
90
+ self._activate(event_data)
91
+ return self._sentinel
92
+
93
+ state = self.sm.current_state
94
+ for transition in state.transitions:
95
+ if not transition.match(trigger_data.event):
96
+ continue
97
+
98
+ event_data = EventData(trigger_data=trigger_data, transition=transition)
99
+ args, kwargs = event_data.args, event_data.extended_kwargs
100
+ self.sm._get_callbacks(transition.validators.key).call(*args, **kwargs)
101
+ if not self.sm._get_callbacks(transition.cond.key).all(*args, **kwargs):
102
+ continue
103
+
104
+ result = self._activate(event_data)
105
+ event_data.result = result
106
+ event_data.executed = True
107
+ break
108
+ else:
109
+ if not self.sm.allow_event_without_transition:
110
+ raise TransitionNotAllowed(trigger_data.event, state)
111
+
112
+ return event_data.result if event_data else None
113
+
114
+ def _activate(self, event_data: EventData):
115
+ args, kwargs = event_data.args, event_data.extended_kwargs
116
+ transition = event_data.transition
117
+ source = event_data.state
118
+ target = transition.target
119
+
120
+ result = self.sm._get_callbacks(transition.before.key).call(*args, **kwargs)
121
+ if source is not None and not transition.internal:
122
+ self.sm._get_callbacks(source.exit.key).call(*args, **kwargs)
123
+
124
+ result += self.sm._get_callbacks(transition.on.key).call(*args, **kwargs)
125
+
126
+ self.sm.current_state = target
127
+ event_data.state = target
128
+ kwargs["state"] = target
129
+
130
+ if not transition.internal:
131
+ self.sm._get_callbacks(target.enter.key).call(*args, **kwargs)
132
+ self.sm._get_callbacks(transition.after.key).call(*args, **kwargs)
133
+
134
+ if len(result) == 0:
135
+ result = None
136
+ elif len(result) == 1:
137
+ result = result[0]
138
+
139
+ return result
statemachine/event.py CHANGED
@@ -1,11 +1,9 @@
1
- from functools import partial
1
+ from inspect import isawaitable
2
2
  from typing import TYPE_CHECKING
3
3
 
4
4
  from statemachine.utils import run_async_from_sync
5
5
 
6
- from .event_data import EventData
7
6
  from .event_data import TriggerData
8
- from .exceptions import TransitionNotAllowed
9
7
 
10
8
  if TYPE_CHECKING:
11
9
  from .statemachine import StateMachine
@@ -18,42 +16,25 @@ class Event:
18
16
  def __repr__(self):
19
17
  return f"{type(self).__name__}({self.name!r})"
20
18
 
21
- async def trigger(self, machine: "StateMachine", *args, **kwargs):
19
+ def trigger(self, machine: "StateMachine", *args, **kwargs):
22
20
  trigger_data = TriggerData(
23
21
  machine=machine,
24
22
  event=self.name,
25
23
  args=args,
26
24
  kwargs=kwargs,
27
25
  )
28
- trigger_wrapper = partial(self._trigger, trigger_data=trigger_data)
29
-
30
- return await machine._process(trigger_wrapper)
31
-
32
- async def _trigger(self, trigger_data: TriggerData):
33
- event_data = None
34
- await trigger_data.machine._ensure_is_initialized()
35
-
36
- state = trigger_data.machine.current_state
37
- for transition in state.transitions:
38
- if not transition.match(trigger_data.event):
39
- continue
40
-
41
- event_data = EventData(trigger_data=trigger_data, transition=transition)
42
- if await transition.execute(event_data):
43
- event_data.executed = True
44
- break
45
- else:
46
- if not trigger_data.machine.allow_event_without_transition:
47
- raise TransitionNotAllowed(trigger_data.event, state)
48
-
49
- return event_data.result if event_data else None
26
+ machine._put_nonblocking(trigger_data)
27
+ return machine._processing_loop()
50
28
 
51
29
 
52
30
  def trigger_event_factory(event_instance: Event):
53
31
  """Build a method that sends specific `event` to the machine"""
54
32
 
55
33
  def trigger_event(self, *args, **kwargs):
56
- return run_async_from_sync(event_instance.trigger(self, *args, **kwargs))
34
+ result = event_instance.trigger(self, *args, **kwargs)
35
+ if not isawaitable(result):
36
+ return result
37
+ return run_async_from_sync(result)
57
38
 
58
39
  trigger_event.name = event_instance.name # type: ignore[attr-defined]
59
40
  trigger_event.identifier = event_instance.name # type: ignore[attr-defined]
@@ -66,7 +47,7 @@ def same_event_cond_builder(expected_event: str):
66
47
  Builds a condition method that evaluates to ``True`` when the expected event is received.
67
48
  """
68
49
 
69
- def cond(event: str) -> bool:
50
+ def cond(*args, event: "str | None" = None, **kwargs) -> bool:
70
51
  return event == expected_event
71
52
 
72
53
  return cond
@@ -47,16 +47,14 @@ class EventData:
47
47
  """The destination :ref:`State` of the :ref:`transition`."""
48
48
 
49
49
  result: "Any | None" = None
50
+
50
51
  executed: bool = False
51
52
 
52
53
  def __post_init__(self):
53
54
  self.state = self.transition.source
54
55
  self.source = self.transition.source
55
56
  self.target = self.transition.target
56
-
57
- @property
58
- def machine(self):
59
- return self.trigger_data.machine
57
+ self.machine = self.trigger_data.machine
60
58
 
61
59
  @property
62
60
  def event(self):
statemachine/events.py CHANGED
@@ -27,5 +27,5 @@ class Events:
27
27
 
28
28
  return self
29
29
 
30
- def match(self, event):
31
- return any(t == event for t in self)
30
+ def match(self, event: str):
31
+ return any(e == event for e in self)