python-statemachine 2.3.6__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.3.6
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) ; 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')
@@ -405,7 +405,7 @@ There's a lot more to cover, please take a look at our docs:
405
405
  https://python-statemachine.readthedocs.io.
406
406
 
407
407
 
408
- ## Contributing to the project
408
+ ## Contributing
409
409
 
410
410
  * <a class="github-button" href="https://github.com/fgmacedo/python-statemachine" data-icon="octicon-star" aria-label="Star fgmacedo/python-statemachine on GitHub">Star this project</a>
411
411
  * <a class="github-button" href="https://github.com/fgmacedo/python-statemachine/issues" data-icon="octicon-issue-opened" aria-label="Issue fgmacedo/python-statemachine on GitHub">Open an Issue</a>
@@ -413,18 +413,17 @@ https://python-statemachine.readthedocs.io.
413
413
 
414
414
  - If you found this project helpful, please consider giving it a star on GitHub.
415
415
 
416
- - **Contribute code**: If you would like to contribute code to this project, please submit a pull
416
+ - **Contribute code**: If you would like to contribute code, please submit a pull
417
417
  request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
418
418
 
419
- - **Report bugs**: If you find any bugs in this project, please report them by opening an issue
419
+ - **Report bugs**: If you find any bugs, please report them by opening an issue
420
420
  on our GitHub issue tracker.
421
421
 
422
- - **Suggest features**: If you have a great idea for a new feature, please let us know by opening
423
- an issue on our GitHub issue tracker.
422
+ - **Suggest features**: If you have an idea for a new feature, of feels something being harder than it should be,
423
+ please let us know by opening an issue on our GitHub issue tracker.
424
424
 
425
- - **Documentation**: Help improve this project's documentation by submitting pull requests.
425
+ - **Documentation**: Help improve documentation by submitting pull requests.
426
426
 
427
- - **Promote the project**: Help spread the word about this project by sharing it on social media,
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
@@ -1,8 +1,9 @@
1
+ from .event import Event
1
2
  from .state import State
2
3
  from .statemachine import StateMachine
3
4
 
4
5
  __author__ = """Fernando Macedo"""
5
6
  __email__ = "fgmacedo@gmail.com"
6
- __version__ = "2.3.6"
7
+ __version__ = "2.5.0"
7
8
 
8
- __all__ = ["StateMachine", "State"]
9
+ __all__ = ["StateMachine", "State", "Event"]
statemachine/callbacks.py CHANGED
@@ -6,18 +6,22 @@ from enum import IntEnum
6
6
  from enum import IntFlag
7
7
  from enum import auto
8
8
  from inspect import isawaitable
9
- from inspect import iscoroutinefunction
9
+ from typing import TYPE_CHECKING
10
10
  from typing import Callable
11
11
  from typing import Dict
12
- from typing import Generator
13
- from typing import Iterable
14
12
  from typing import List
15
- from typing import Type
16
13
 
17
14
  from .exceptions import AttrNotFound
18
15
  from .i18n import _
19
16
  from .utils import ensure_iterable
20
17
 
18
+ if TYPE_CHECKING:
19
+ from typing import Set
20
+
21
+
22
+ def allways_true(*args, **kwargs):
23
+ return True
24
+
21
25
 
22
26
  class CallbackPriority(IntEnum):
23
27
  GENERIC = 0
@@ -50,10 +54,6 @@ class CallbackGroup(IntEnum):
50
54
  return f"{self.name}@{id(specs)}"
51
55
 
52
56
 
53
- def allways_true(*args, **kwargs):
54
- return True
55
-
56
-
57
57
  class CallbackSpec:
58
58
  """Specs about callbacks.
59
59
 
@@ -63,11 +63,15 @@ class CallbackSpec:
63
63
  before any real call is performed.
64
64
  """
65
65
 
66
+ names_not_found: "Set[str] | None" = None
67
+ """List of names that were not found on the model or statemachine"""
68
+
66
69
  def __init__(
67
70
  self,
68
71
  func,
69
72
  group: CallbackGroup,
70
73
  is_convention=False,
74
+ is_event: bool = False,
71
75
  cond=None,
72
76
  priority: CallbackPriority = CallbackPriority.NAMING,
73
77
  expected_value=None,
@@ -75,6 +79,7 @@ class CallbackSpec:
75
79
  self.func = func
76
80
  self.group = group
77
81
  self.is_convention = is_convention
82
+ self.is_event = is_event
78
83
  self.cond = cond
79
84
  self.expected_value = expected_value
80
85
  self.priority = priority
@@ -85,11 +90,22 @@ class CallbackSpec:
85
90
  elif callable(func):
86
91
  self.reference = SpecReference.CALLABLE
87
92
  self.is_bounded = hasattr(func, "__self__")
88
- 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
89
99
  else:
90
100
  self.reference = SpecReference.NAME
91
101
  self.attr_name = func
92
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
+
93
109
  def __repr__(self):
94
110
  return f"{type(self).__name__}({self.func!r}, is_convention={self.is_convention!r})"
95
111
 
@@ -105,44 +121,19 @@ class CallbackSpec:
105
121
  def __hash__(self):
106
122
  return id(self)
107
123
 
108
- def _update_func(self, func: Callable, attr_name: str):
109
- self.func = func
110
- self.reference = SpecReference.CALLABLE
111
- self.attr_name = attr_name
112
-
113
- def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
114
- """
115
- Resolves the `func` into a usable callable.
116
-
117
- Args:
118
- resolver (callable): A method responsible to build and return a valid callable that
119
- can receive arbitrary parameters like `*args, **kwargs`.
120
- """
121
- for callback in resolver.search(self):
122
- condition = self.cond if self.cond is not None else allways_true
123
- yield CallbackWrapper(
124
- callback=callback,
125
- condition=condition,
126
- meta=self,
127
- unique_key=callback.unique_key,
128
- )
129
-
130
124
 
131
125
  class SpecListGrouper:
132
- def __init__(
133
- self, list: "CallbackSpecList", group: CallbackGroup, factory=CallbackSpec
134
- ) -> None:
126
+ def __init__(self, list: "CallbackSpecList", group: CallbackGroup) -> None:
135
127
  self.list = list
136
128
  self.group = group
137
- self.factory = factory
138
129
  self.key = group.build_key(list)
139
130
 
140
131
  def add(self, callbacks, **kwargs):
141
- self.list.add(callbacks, group=self.group, factory=self.factory, **kwargs)
132
+ self.list.add(callbacks, group=self.group, **kwargs)
142
133
  return self
143
134
 
144
135
  def __call__(self, callback):
145
- return self.list._add_unbounded_callback(callback, group=self.group, factory=self.factory)
136
+ return self.list._add_unbounded_callback(callback, group=self.group)
146
137
 
147
138
  def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
148
139
  self.list._add_unbounded_callback(
@@ -150,7 +141,6 @@ class SpecListGrouper:
150
141
  is_event=is_event,
151
142
  transitions=transitions,
152
143
  group=self.group,
153
- factory=self.factory,
154
144
  **kwargs,
155
145
  )
156
146
 
@@ -164,12 +154,13 @@ class CallbackSpecList:
164
154
  def __init__(self, factory=CallbackSpec):
165
155
  self.items: List[CallbackSpec] = []
166
156
  self.conventional_specs = set()
157
+ self._groupers: Dict[CallbackGroup, SpecListGrouper] = {}
167
158
  self.factory = factory
168
159
 
169
160
  def __repr__(self):
170
161
  return f"{type(self).__name__}({self.items!r}, factory={self.factory!r})"
171
162
 
172
- def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
163
+ def _add_unbounded_callback(self, func, transitions=None, **kwargs):
173
164
  """This list was a target for adding a func using decorator
174
165
  `@<state|event>[.on|before|after|enter|exit]` syntax.
175
166
 
@@ -192,11 +183,7 @@ class CallbackSpecList:
192
183
  event.
193
184
 
194
185
  """
195
- spec = self._add(func, **kwargs)
196
- if not getattr(func, "_specs_to_update", None):
197
- func._specs_to_update = set()
198
- if is_event:
199
- func._specs_to_update.add(spec._update_func)
186
+ self._add(func, **kwargs)
200
187
  func._transitions = transitions
201
188
 
202
189
  return func
@@ -207,15 +194,16 @@ class CallbackSpecList:
207
194
  def clear(self):
208
195
  self.items = []
209
196
 
210
- def grouper(
211
- self, group: CallbackGroup, factory: Type[CallbackSpec] = CallbackSpec
212
- ) -> SpecListGrouper:
213
- 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]
214
201
 
215
- def _add(self, func, group: CallbackGroup, factory=None, **kwargs):
216
- if factory is None:
217
- factory = self.factory
218
- 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)
219
207
 
220
208
  if spec in self.items:
221
209
  return
@@ -245,7 +233,7 @@ class CallbackWrapper:
245
233
  unique_key: str,
246
234
  ) -> None:
247
235
  self._callback = callback
248
- self._iscoro = iscoroutinefunction(callback)
236
+ self._iscoro = getattr(callback, "is_coroutine", False)
249
237
  self.condition = condition
250
238
  self.meta = meta
251
239
  self.unique_key = unique_key
@@ -292,19 +280,21 @@ class CallbacksExecutor:
292
280
  def __str__(self):
293
281
  return ", ".join(str(c) for c in self)
294
282
 
295
- def _add(self, spec: CallbackSpec, resolver: Callable):
296
- for callback in spec.build(resolver):
297
- if callback.unique_key in self.items_already_seen:
298
- continue
283
+ def add(self, key: str, spec: CallbackSpec, builder: Callable[[], Callable]):
284
+ if key in self.items_already_seen:
285
+ return
299
286
 
300
- self.items_already_seen.add(callback.unique_key)
301
- insort(self.items, callback)
287
+ self.items_already_seen.add(key)
302
288
 
303
- def add(self, items: Iterable[CallbackSpec], resolver: Callable):
304
- """Validate configurations"""
305
- for item in items:
306
- self._add(item, resolver)
307
- 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)
308
298
 
309
299
  async def async_call(self, *args, **kwargs):
310
300
  return await asyncio.gather(
@@ -341,9 +331,6 @@ class CallbacksRegistry:
341
331
  self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
342
332
  self.has_async_callbacks: bool = False
343
333
 
344
- def clear(self):
345
- self._registry.clear()
346
-
347
334
  def __getitem__(self, key: str) -> CallbacksExecutor:
348
335
  return self._registry[key]
349
336
 
@@ -356,6 +343,13 @@ class CallbacksRegistry:
356
343
  callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
357
344
  ):
358
345
  continue
346
+
347
+ if meta.names_not_found:
348
+ raise AttrNotFound(
349
+ _("Did not found name '{}' from model or statemachine").format(
350
+ ", ".join(meta.names_not_found)
351
+ ),
352
+ )
359
353
  raise AttrNotFound(
360
354
  _("Did not found name '{}' from model or statemachine").format(meta.func)
361
355
  )
@@ -364,3 +358,24 @@ class CallbacksRegistry:
364
358
  self.has_async_callbacks = any(
365
359
  callback._iscoro for executor in self._registry.values() for callback in executor
366
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
@@ -166,10 +166,6 @@ def quickchart_write_svg(sm: StateMachine, path: str):
166
166
  >>> sm = OrderControl()
167
167
  >>> print(sm._graph().to_string())
168
168
  digraph list {
169
- fontname=Arial;
170
- fontsize=10;
171
- label=OrderControl;
172
- rankdir=LR;
173
169
  ...
174
170
 
175
171
  To give you an example, we included this method that will serialize the dot, request the graph