python-statemachine 2.4.0__py3-none-any.whl → 2.5.0__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,19 +1,17 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: python-statemachine
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Python Finite State Machines made easy.
5
- Home-page: https://github.com/fgmacedo/python-statemachine
6
- License: MIT
7
- Author: Fernando Macedo
8
- Author-email: fgmacedo@gmail.com
9
- Maintainer: Fernando Macedo
10
- Maintainer-email: fgmacedo@gmail.com
11
- Requires-Python: >=3.7
5
+ Project-URL: homepage, https://github.com/fgmacedo/python-statemachine
6
+ Author-email: Fernando Macedo <fgmacedo@gmail.com>
7
+ Maintainer-email: Fernando Macedo <fgmacedo@gmail.com>
8
+ License: MIT License
9
+ Classifier: Development Status :: 5 - Production/Stable
12
10
  Classifier: Framework :: AsyncIO
11
+ Classifier: Framework :: Django
13
12
  Classifier: Intended Audience :: Developers
14
13
  Classifier: License :: OSI Approved :: MIT License
15
14
  Classifier: Natural Language :: English
16
- Classifier: Programming Language :: Python :: 3
17
15
  Classifier: Programming Language :: Python :: 3.7
18
16
  Classifier: Programming Language :: Python :: 3.8
19
17
  Classifier: Programming Language :: Python :: 3.9
@@ -21,9 +19,11 @@ Classifier: Programming Language :: Python :: 3.10
21
19
  Classifier: Programming Language :: Python :: 3.11
22
20
  Classifier: Programming Language :: Python :: 3.12
23
21
  Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Home Automation
24
23
  Classifier: Topic :: Software Development :: Libraries
24
+ Requires-Python: >=3.7
25
25
  Provides-Extra: diagrams
26
- Requires-Dist: pydot (>=2.0.0) ; (python_full_version > "3.8.0") and (extra == "diagrams")
26
+ Requires-Dist: pydot>=2.0.0; extra == 'diagrams'
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  # Python StateMachine
@@ -196,19 +196,19 @@ Easily iterate over all states:
196
196
 
197
197
  ```py
198
198
  >>> [s.id for s in sm.states]
199
- ['green', 'red', 'yellow']
199
+ ['green', 'yellow', 'red']
200
200
 
201
201
  ```
202
202
 
203
203
  Or over events:
204
204
 
205
205
  ```py
206
- >>> [t.name for t in sm.events]
206
+ >>> [t.id for t in sm.events]
207
207
  ['cycle']
208
208
 
209
209
  ```
210
210
 
211
- Call an event by its name:
211
+ Call an event by its id:
212
212
 
213
213
  ```py
214
214
  >>> sm.cycle()
@@ -216,7 +216,7 @@ Don't move.
216
216
  'Running cycle from yellow to red'
217
217
 
218
218
  ```
219
- Or send an event with the event name:
219
+ Or send an event with the event id:
220
220
 
221
221
  ```py
222
222
  >>> sm.send('cycle')
@@ -427,4 +427,3 @@ request. For more information on how to contribute, please see our [contributing
427
427
  - **Promote the project**: Help spread the word by sharing on social media,
428
428
  writing a blog post, or giving a talk about it. Tag me on Twitter
429
429
  [@fgmacedo](https://twitter.com/fgmacedo) so I can share it too!
430
-
@@ -0,0 +1,37 @@
1
+ statemachine/__init__.py,sha256=JsTxT_XFohxEg-P6qmBYcxNTwzW-A2a9zLIk7wpRzaM,226
2
+ statemachine/callbacks.py,sha256=UUOuMotQDV3ZkpqjWp3CIL3UrA70Bh_xYDf4Ugjlou8,11475
3
+ statemachine/dispatcher.py,sha256=Ai8i79Lo5HdUJ-toOVg4NDUzAy88sryBHDbYdl_7sWE,7785
4
+ statemachine/event.py,sha256=DhAVtoPGCHv95r3QOuL-Tn93UMWXPhiuODFcc9FPUPA,4586
5
+ statemachine/event_data.py,sha256=H9lp_XnvHSK9YErUOCvMK3ZBjWhC-xDSOJ2gZxtmrq8,2261
6
+ statemachine/events.py,sha256=UTYJu8te_bxiORTQpoXY5tB_x-ymVPWDOREcxCyhExA,1018
7
+ statemachine/exceptions.py,sha256=vHVQPTTMMkVvySNbN6XZPciBryvpY608LDe3MCnmxFU,1124
8
+ statemachine/factory.py,sha256=crL2FPfzdku3STlbxaF9N3oV9L1BFWoCAT9K3zEnBYo,9219
9
+ statemachine/graph.py,sha256=KtwB1CYckaLjTgQD9tEeuaEzJje9q3fPVpBViW5TgSk,487
10
+ statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
11
+ statemachine/mixins.py,sha256=Y1fa52Cj20JaGkyNk3P7Gpqkt4cGTjJ0YyV_VQyCl0M,1231
12
+ statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
13
+ statemachine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ statemachine/registry.py,sha256=HmV9sUGkYVrNUZxJYoZo-trSUis7dIun_WcGktblgc8,922
15
+ statemachine/signature.py,sha256=mZjYXjMAF2XNLxI_MfTYoReJxTuPBHExEm7H76yueWY,7117
16
+ statemachine/spec_parser.py,sha256=DRG-_6bWRTCTgU6qZHqRp5aOxFRZP0O_4VvVxyleItA,5686
17
+ statemachine/state.py,sha256=a2uJZyn8sM8HdnVN_FesCgcjoeDE8gfXCAChbLm4p9g,9504
18
+ statemachine/statemachine.py,sha256=mznQ4NpiysrHm1Ef3JPGRQ07gSXzAD9bER3r_ou9d40,10881
19
+ statemachine/states.py,sha256=pPROZwIyE3_tlBGL3DJXnM3gr1pWsWICEMMo2c742RY,4889
20
+ statemachine/transition.py,sha256=YDpI6NuCipW4cF4GVIt3o8ACE6VhWAQWHaZKjmjSQRA,5335
21
+ statemachine/transition_list.py,sha256=I9viQ0zr6E8oL3WEESkst9DwQeTNJ6nwDAQXsWeo3_c,4179
22
+ statemachine/transition_mixin.py,sha256=OGKF-hMyTEZDtlK_XxGaa5OqxYjmUtThMac9tDQUKUY,2615
23
+ statemachine/utils.py,sha256=DpcrGqlbrnT-ogh-BogG0L07EG3KirHOsKORHlspDlI,1041
24
+ statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ statemachine/contrib/diagram.py,sha256=CVNIhBTedik_02b-4KUua_3_HtiO1TYQNWOg3ulkqiE,7159
26
+ statemachine/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ statemachine/engines/async_.py,sha256=HA9EtS7vYS5tulvdwH2NnTMkpUddcDZPXVNckSQFAak,5212
28
+ statemachine/engines/base.py,sha256=d65JvngOrp8sE4RR8i2wrd72wzIci9-3InMLyGdxsZQ,1201
29
+ statemachine/engines/sync.py,sha256=Cys05fn7zQf3WowdIaVrdD8mAVQ-HEjbTMRngv_Z4cU,5035
30
+ statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=4Pk-h5nk7twOTHcRQ_Chanfdi5EtFi9aTAzGkVJuCo8,2424
31
+ statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po,sha256=Bs5bbIxDrYtODSNJKNl4FH8ZtjEJwPIidRwbAxMwu5E,4941
32
+ statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=gmnhc15-6YVDCLWYT0ZQL5jfXHgIOYq5p5rJLqNPaxE,3593
33
+ statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po,sha256=cDbRHDYpi3pwJkFmaSn79q5KmX9cGmHFHw1ndj37vRw,3325
34
+ python_statemachine-2.5.0.dist-info/METADATA,sha256=LzU6fCMlw6gANXQeCCgRpyVRorsAzvF8UMWERtnowng,14068
35
+ python_statemachine-2.5.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
36
+ python_statemachine-2.5.0.dist-info/licenses/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
37
+ python_statemachine-2.5.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.6.1
2
+ Generator: hatchling 1.26.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
statemachine/__init__.py CHANGED
@@ -4,6 +4,6 @@ from .statemachine import StateMachine
4
4
 
5
5
  __author__ = """Fernando Macedo"""
6
6
  __email__ = "fgmacedo@gmail.com"
7
- __version__ = "2.4.0"
7
+ __version__ = "2.5.0"
8
8
 
9
9
  __all__ = ["StateMachine", "State", "Event"]
statemachine/callbacks.py CHANGED
@@ -5,29 +5,22 @@ from collections import deque
5
5
  from enum import IntEnum
6
6
  from enum import IntFlag
7
7
  from enum import auto
8
- from functools import partial
9
- from functools import reduce
10
8
  from inspect import isawaitable
11
- from inspect import iscoroutinefunction
12
9
  from typing import TYPE_CHECKING
13
10
  from typing import Callable
14
11
  from typing import Dict
15
- from typing import Generator
16
- from typing import Iterable
17
12
  from typing import List
18
- from typing import Set
19
- from typing import Type
20
13
 
21
14
  from .exceptions import AttrNotFound
22
- from .exceptions import InvalidDefinition
23
15
  from .i18n import _
24
- from .spec_parser import custom_and
25
- from .spec_parser import operator_mapping
26
- from .spec_parser import parse_boolean_expr
27
16
  from .utils import ensure_iterable
28
17
 
29
18
  if TYPE_CHECKING:
30
- from statemachine.dispatcher import Listeners
19
+ from typing import Set
20
+
21
+
22
+ def allways_true(*args, **kwargs):
23
+ return True
31
24
 
32
25
 
33
26
  class CallbackPriority(IntEnum):
@@ -61,21 +54,6 @@ class CallbackGroup(IntEnum):
61
54
  return f"{self.name}@{id(specs)}"
62
55
 
63
56
 
64
- def allways_true(*args, **kwargs):
65
- return True
66
-
67
-
68
- def take_callback(name: str, resolver: "Listeners", not_found_handler: Callable) -> Callable:
69
- callbacks = list(resolver.search_name(name))
70
- if len(callbacks) == 0:
71
- not_found_handler(name)
72
- return allways_true
73
- elif len(callbacks) == 1:
74
- return callbacks[0]
75
- else:
76
- return reduce(custom_and, callbacks)
77
-
78
-
79
57
  class CallbackSpec:
80
58
  """Specs about callbacks.
81
59
 
@@ -85,11 +63,15 @@ class CallbackSpec:
85
63
  before any real call is performed.
86
64
  """
87
65
 
66
+ names_not_found: "Set[str] | None" = None
67
+ """List of names that were not found on the model or statemachine"""
68
+
88
69
  def __init__(
89
70
  self,
90
71
  func,
91
72
  group: CallbackGroup,
92
73
  is_convention=False,
74
+ is_event: bool = False,
93
75
  cond=None,
94
76
  priority: CallbackPriority = CallbackPriority.NAMING,
95
77
  expected_value=None,
@@ -97,6 +79,7 @@ class CallbackSpec:
97
79
  self.func = func
98
80
  self.group = group
99
81
  self.is_convention = is_convention
82
+ self.is_event = is_event
100
83
  self.cond = cond
101
84
  self.expected_value = expected_value
102
85
  self.priority = priority
@@ -107,11 +90,22 @@ class CallbackSpec:
107
90
  elif callable(func):
108
91
  self.reference = SpecReference.CALLABLE
109
92
  self.is_bounded = hasattr(func, "__self__")
110
- self.attr_name = func.__name__
93
+ self.attr_name = (
94
+ func.__name__ if not self.is_event or self.is_bounded else f"_{func.__name__}_"
95
+ )
96
+ if not self.is_bounded:
97
+ func.attr_name = self.attr_name
98
+ func.is_event = is_event
111
99
  else:
112
100
  self.reference = SpecReference.NAME
113
101
  self.attr_name = func
114
102
 
103
+ self.may_contain_boolean_expression = (
104
+ not self.is_convention
105
+ and self.group == CallbackGroup.COND
106
+ and self.reference == SpecReference.NAME
107
+ )
108
+
115
109
  def __repr__(self):
116
110
  return f"{type(self).__name__}({self.func!r}, is_convention={self.is_convention!r})"
117
111
 
@@ -127,68 +121,19 @@ class CallbackSpec:
127
121
  def __hash__(self):
128
122
  return id(self)
129
123
 
130
- def _update_func(self, func: Callable, attr_name: str):
131
- self.func = func
132
- self.reference = SpecReference.CALLABLE
133
- self.attr_name = attr_name
134
-
135
- def _wrap(self, callback):
136
- condition = self.cond if self.cond is not None else allways_true
137
- return CallbackWrapper(
138
- callback=callback,
139
- condition=condition,
140
- meta=self,
141
- unique_key=callback.unique_key,
142
- )
143
-
144
- def build(self, resolver: "Listeners") -> Generator["CallbackWrapper", None, None]:
145
- """
146
- Resolves the `func` into a usable callable.
147
-
148
- Args:
149
- resolver (callable): A method responsible to build and return a valid callable that
150
- can receive arbitrary parameters like `*args, **kwargs`.
151
- """
152
- if (
153
- not self.is_convention
154
- and self.group == CallbackGroup.COND
155
- and self.reference == SpecReference.NAME
156
- ):
157
- names_not_found: Set[str] = set()
158
- take_callback_partial = partial(
159
- take_callback, resolver=resolver, not_found_handler=names_not_found.add
160
- )
161
- try:
162
- expression = parse_boolean_expr(self.func, take_callback_partial, operator_mapping)
163
- except SyntaxError as err:
164
- raise InvalidDefinition(
165
- _("Failed to parse boolean expression '{}'").format(self.func)
166
- ) from err
167
- if not expression or names_not_found:
168
- self.names_not_found = names_not_found
169
- return
170
- yield self._wrap(expression)
171
- return
172
-
173
- for callback in resolver.search(self):
174
- yield self._wrap(callback)
175
-
176
124
 
177
125
  class SpecListGrouper:
178
- def __init__(
179
- self, list: "CallbackSpecList", group: CallbackGroup, factory=CallbackSpec
180
- ) -> None:
126
+ def __init__(self, list: "CallbackSpecList", group: CallbackGroup) -> None:
181
127
  self.list = list
182
128
  self.group = group
183
- self.factory = factory
184
129
  self.key = group.build_key(list)
185
130
 
186
131
  def add(self, callbacks, **kwargs):
187
- self.list.add(callbacks, group=self.group, factory=self.factory, **kwargs)
132
+ self.list.add(callbacks, group=self.group, **kwargs)
188
133
  return self
189
134
 
190
135
  def __call__(self, callback):
191
- return self.list._add_unbounded_callback(callback, group=self.group, factory=self.factory)
136
+ return self.list._add_unbounded_callback(callback, group=self.group)
192
137
 
193
138
  def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
194
139
  self.list._add_unbounded_callback(
@@ -196,7 +141,6 @@ class SpecListGrouper:
196
141
  is_event=is_event,
197
142
  transitions=transitions,
198
143
  group=self.group,
199
- factory=self.factory,
200
144
  **kwargs,
201
145
  )
202
146
 
@@ -210,12 +154,13 @@ class CallbackSpecList:
210
154
  def __init__(self, factory=CallbackSpec):
211
155
  self.items: List[CallbackSpec] = []
212
156
  self.conventional_specs = set()
157
+ self._groupers: Dict[CallbackGroup, SpecListGrouper] = {}
213
158
  self.factory = factory
214
159
 
215
160
  def __repr__(self):
216
161
  return f"{type(self).__name__}({self.items!r}, factory={self.factory!r})"
217
162
 
218
- def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
163
+ def _add_unbounded_callback(self, func, transitions=None, **kwargs):
219
164
  """This list was a target for adding a func using decorator
220
165
  `@<state|event>[.on|before|after|enter|exit]` syntax.
221
166
 
@@ -238,11 +183,7 @@ class CallbackSpecList:
238
183
  event.
239
184
 
240
185
  """
241
- spec = self._add(func, **kwargs)
242
- if not getattr(func, "_specs_to_update", None):
243
- func._specs_to_update = set()
244
- if is_event:
245
- func._specs_to_update.add(spec._update_func)
186
+ self._add(func, **kwargs)
246
187
  func._transitions = transitions
247
188
 
248
189
  return func
@@ -253,15 +194,16 @@ class CallbackSpecList:
253
194
  def clear(self):
254
195
  self.items = []
255
196
 
256
- def grouper(
257
- self, group: CallbackGroup, factory: Type[CallbackSpec] = CallbackSpec
258
- ) -> SpecListGrouper:
259
- return SpecListGrouper(self, group, factory=factory)
197
+ def grouper(self, group: CallbackGroup) -> SpecListGrouper:
198
+ if group not in self._groupers:
199
+ self._groupers[group] = SpecListGrouper(self, group)
200
+ return self._groupers[group]
260
201
 
261
- def _add(self, func, group: CallbackGroup, factory=None, **kwargs):
262
- if factory is None:
263
- factory = self.factory
264
- spec = factory(func, group, **kwargs)
202
+ def _add(self, func, group: CallbackGroup, **kwargs):
203
+ if isinstance(func, CallbackSpec):
204
+ spec = func
205
+ else:
206
+ spec = self.factory(func, group, **kwargs)
265
207
 
266
208
  if spec in self.items:
267
209
  return
@@ -291,7 +233,7 @@ class CallbackWrapper:
291
233
  unique_key: str,
292
234
  ) -> None:
293
235
  self._callback = callback
294
- self._iscoro = iscoroutinefunction(callback)
236
+ self._iscoro = getattr(callback, "is_coroutine", False)
295
237
  self.condition = condition
296
238
  self.meta = meta
297
239
  self.unique_key = unique_key
@@ -338,19 +280,21 @@ class CallbacksExecutor:
338
280
  def __str__(self):
339
281
  return ", ".join(str(c) for c in self)
340
282
 
341
- def _add(self, spec: CallbackSpec, resolver: "Listeners"):
342
- for callback in spec.build(resolver):
343
- if callback.unique_key in self.items_already_seen:
344
- continue
283
+ def add(self, key: str, spec: CallbackSpec, builder: Callable[[], Callable]):
284
+ if key in self.items_already_seen:
285
+ return
345
286
 
346
- self.items_already_seen.add(callback.unique_key)
347
- insort(self.items, callback)
287
+ self.items_already_seen.add(key)
348
288
 
349
- def add(self, items: Iterable[CallbackSpec], resolver: "Listeners"):
350
- """Validate configurations"""
351
- for item in items:
352
- self._add(item, resolver)
353
- return self
289
+ condition = spec.cond if spec.cond is not None else allways_true
290
+ wrapper = CallbackWrapper(
291
+ callback=builder(),
292
+ condition=condition,
293
+ meta=spec,
294
+ unique_key=key,
295
+ )
296
+
297
+ insort(self.items, wrapper)
354
298
 
355
299
  async def async_call(self, *args, **kwargs):
356
300
  return await asyncio.gather(
@@ -387,9 +331,6 @@ class CallbacksRegistry:
387
331
  self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
388
332
  self.has_async_callbacks: bool = False
389
333
 
390
- def clear(self):
391
- self._registry.clear()
392
-
393
334
  def __getitem__(self, key: str) -> CallbacksExecutor:
394
335
  return self._registry[key]
395
336
 
@@ -402,7 +343,8 @@ class CallbacksRegistry:
402
343
  callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
403
344
  ):
404
345
  continue
405
- if hasattr(meta, "names_not_found"):
346
+
347
+ if meta.names_not_found:
406
348
  raise AttrNotFound(
407
349
  _("Did not found name '{}' from model or statemachine").format(
408
350
  ", ".join(meta.names_not_found)
@@ -416,3 +358,24 @@ class CallbacksRegistry:
416
358
  self.has_async_callbacks = any(
417
359
  callback._iscoro for executor in self._registry.values() for callback in executor
418
360
  )
361
+
362
+ def call(self, key: str, *args, **kwargs):
363
+ if key not in self._registry:
364
+ return []
365
+ return self._registry[key].call(*args, **kwargs)
366
+
367
+ def async_call(self, key: str, *args, **kwargs):
368
+ return self._registry[key].async_call(*args, **kwargs)
369
+
370
+ def all(self, key: str, *args, **kwargs):
371
+ if key not in self._registry:
372
+ return True
373
+ return self._registry[key].all(*args, **kwargs)
374
+
375
+ def async_all(self, key: str, *args, **kwargs):
376
+ return self._registry[key].async_all(*args, **kwargs)
377
+
378
+ def str(self, key: str) -> str:
379
+ if key not in self._registry:
380
+ return ""
381
+ return str(self._registry[key])
@@ -29,7 +29,7 @@ class DotGraphMachine:
29
29
  transition_font_size = "9"
30
30
  """Transition font size in points"""
31
31
 
32
- def __init__(self, machine):
32
+ def __init__(self, machine: StateMachine):
33
33
  self.machine = machine
34
34
 
35
35
  def _get_graph(self):
@@ -69,11 +69,11 @@ class DotGraphMachine:
69
69
  def _actions_getter(self):
70
70
  if isinstance(self.machine, StateMachine):
71
71
 
72
- def getter(grouper):
73
- return self.machine._get_callbacks(grouper.key)
72
+ def getter(grouper) -> str:
73
+ return self.machine._callbacks.str(grouper.key)
74
74
  else:
75
75
 
76
- def getter(grouper):
76
+ def getter(grouper) -> str:
77
77
  all_names = set(dir(self.machine))
78
78
  return ", ".join(
79
79
  str(c) for c in grouper if not c.is_convention or c.func in all_names