python-statemachine 2.5.0__py3-none-any.whl → 2.6.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,11 +1,12 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: python-statemachine
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: Python Finite State Machines made easy.
5
5
  Project-URL: homepage, https://github.com/fgmacedo/python-statemachine
6
6
  Author-email: Fernando Macedo <fgmacedo@gmail.com>
7
7
  Maintainer-email: Fernando Macedo <fgmacedo@gmail.com>
8
8
  License: MIT License
9
+ License-File: LICENSE
9
10
  Classifier: Development Status :: 5 - Production/Stable
10
11
  Classifier: Framework :: AsyncIO
11
12
  Classifier: Framework :: Django
@@ -19,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
19
20
  Classifier: Programming Language :: Python :: 3.11
20
21
  Classifier: Programming Language :: Python :: 3.12
21
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
22
24
  Classifier: Topic :: Home Automation
23
25
  Classifier: Topic :: Software Development :: Libraries
24
26
  Requires-Python: >=3.7
@@ -127,6 +129,8 @@ You can now create an instance:
127
129
  This state machine can be represented graphically as follows:
128
130
 
129
131
  ```py
132
+ >>> # This example will only run on automated tests if dot is present
133
+ >>> getfixture("requires_dot_installed")
130
134
  >>> img_path = "docs/images/readme_trafficlightmachine.png"
131
135
  >>> sm._graph().write_png(img_path)
132
136
 
@@ -1,37 +1,37 @@
1
- statemachine/__init__.py,sha256=JsTxT_XFohxEg-P6qmBYcxNTwzW-A2a9zLIk7wpRzaM,226
1
+ statemachine/__init__.py,sha256=srtcRn5wkZna0Qd1bV8rTm6SF1JM-tg47_O3kcTkBwM,226
2
2
  statemachine/callbacks.py,sha256=UUOuMotQDV3ZkpqjWp3CIL3UrA70Bh_xYDf4Ugjlou8,11475
3
3
  statemachine/dispatcher.py,sha256=Ai8i79Lo5HdUJ-toOVg4NDUzAy88sryBHDbYdl_7sWE,7785
4
- statemachine/event.py,sha256=DhAVtoPGCHv95r3QOuL-Tn93UMWXPhiuODFcc9FPUPA,4586
4
+ statemachine/event.py,sha256=PceLkT5oQ4LRXHJGMqNyA43-y-CqJL9rUVCX94qaBL0,4583
5
5
  statemachine/event_data.py,sha256=H9lp_XnvHSK9YErUOCvMK3ZBjWhC-xDSOJ2gZxtmrq8,2261
6
6
  statemachine/events.py,sha256=UTYJu8te_bxiORTQpoXY5tB_x-ymVPWDOREcxCyhExA,1018
7
7
  statemachine/exceptions.py,sha256=vHVQPTTMMkVvySNbN6XZPciBryvpY608LDe3MCnmxFU,1124
8
8
  statemachine/factory.py,sha256=crL2FPfzdku3STlbxaF9N3oV9L1BFWoCAT9K3zEnBYo,9219
9
9
  statemachine/graph.py,sha256=KtwB1CYckaLjTgQD9tEeuaEzJje9q3fPVpBViW5TgSk,487
10
10
  statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
11
- statemachine/mixins.py,sha256=Y1fa52Cj20JaGkyNk3P7Gpqkt4cGTjJ0YyV_VQyCl0M,1231
11
+ statemachine/mixins.py,sha256=8qxZZfBwVdFcr3oPFVWHGzgmAubH6VgQus5x3c6VEE0,1706
12
12
  statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
13
13
  statemachine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
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
15
+ statemachine/signature.py,sha256=eoSCQkgLo4o68461WrF2_sUrlhS4gR-m8mGs6Xb-CFQ,9866
16
+ statemachine/spec_parser.py,sha256=S2vsxIhSHUKrNhtwv_gXhc8H38n2ZCv-6Tk-18A8uR0,7298
17
17
  statemachine/state.py,sha256=a2uJZyn8sM8HdnVN_FesCgcjoeDE8gfXCAChbLm4p9g,9504
18
- statemachine/statemachine.py,sha256=mznQ4NpiysrHm1Ef3JPGRQ07gSXzAD9bER3r_ou9d40,10881
18
+ statemachine/statemachine.py,sha256=leIYvPcEyotblUSnZZZHDkUYMsrxJZPFzYzFJCpa9Kk,11584
19
19
  statemachine/states.py,sha256=pPROZwIyE3_tlBGL3DJXnM3gr1pWsWICEMMo2c742RY,4889
20
20
  statemachine/transition.py,sha256=YDpI6NuCipW4cF4GVIt3o8ACE6VhWAQWHaZKjmjSQRA,5335
21
21
  statemachine/transition_list.py,sha256=I9viQ0zr6E8oL3WEESkst9DwQeTNJ6nwDAQXsWeo3_c,4179
22
- statemachine/transition_mixin.py,sha256=OGKF-hMyTEZDtlK_XxGaa5OqxYjmUtThMac9tDQUKUY,2615
22
+ statemachine/transition_mixin.py,sha256=wUJ3alWMMM7hch-IJuzUaonmcvLr0rGnvWo962NYLt4,2894
23
23
  statemachine/utils.py,sha256=DpcrGqlbrnT-ogh-BogG0L07EG3KirHOsKORHlspDlI,1041
24
24
  statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  statemachine/contrib/diagram.py,sha256=CVNIhBTedik_02b-4KUua_3_HtiO1TYQNWOg3ulkqiE,7159
26
26
  statemachine/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- statemachine/engines/async_.py,sha256=HA9EtS7vYS5tulvdwH2NnTMkpUddcDZPXVNckSQFAak,5212
27
+ statemachine/engines/async_.py,sha256=aiNv6NsfEJVm-p16FNnRVZgtwNy9iFRN3tWX01uX6LI,6322
28
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,,
29
+ statemachine/engines/sync.py,sha256=HmHBMpgTtQJov1pbSPOkmWTEYkunfGa8alSXTNSDknw,6081
30
+ statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=TuhNII8U73A-Gge-gxUKEb73g2DkQBSFHDVs3Eh1DRc,2837
31
+ statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po,sha256=OebfrHo04S65Y_cw_ZmdaPDpX-YitaGvNgrRp3v8VbU,5824
32
+ statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=7eWBeXqJz0XV8wTthCFeWtKnUJSbys-oBX73xXaQfLA,4211
33
+ statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po,sha256=lnCRgxeIvyDkOZVdxcGqRTwlSm9BVHjJmycumFDz0YU,3858
34
+ python_statemachine-2.6.0.dist-info/METADATA,sha256=xBVFYCnyHePX5PMrfdLzw8tyXsx65P6njeLj35mATF8,14252
35
+ python_statemachine-2.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
+ python_statemachine-2.6.0.dist-info/licenses/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
37
+ python_statemachine-2.6.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.26.3
2
+ Generator: hatchling 1.28.0
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.5.0"
7
+ __version__ = "2.6.0"
8
8
 
9
9
  __all__ = ["StateMachine", "State", "Event"]
@@ -97,6 +97,34 @@ class AsyncEngine(BaseEngine):
97
97
 
98
98
  return result if executed else None
99
99
 
100
+ async def enabled_events(self, *args, **kwargs):
101
+ sm = self.sm
102
+ enabled = {}
103
+ for transition in sm.current_state.transitions:
104
+ for event in transition.events:
105
+ if event in enabled:
106
+ continue
107
+ extended_kwargs = kwargs.copy()
108
+ extended_kwargs.update(
109
+ {
110
+ "machine": sm,
111
+ "model": sm.model,
112
+ "event": getattr(sm, event),
113
+ "source": transition.source,
114
+ "target": transition.target,
115
+ "state": sm.current_state,
116
+ "transition": transition,
117
+ }
118
+ )
119
+ try:
120
+ if await sm._callbacks.async_all(
121
+ transition.cond.key, *args, **extended_kwargs
122
+ ):
123
+ enabled[event] = getattr(sm, event)
124
+ except Exception:
125
+ enabled[event] = getattr(sm, event)
126
+ return list(enabled.values())
127
+
100
128
  async def _activate(self, trigger_data: TriggerData, transition: "Transition"):
101
129
  event_data = EventData(trigger_data=trigger_data, transition=transition)
102
130
  args, kwargs = event_data.args, event_data.extended_kwargs
@@ -99,6 +99,32 @@ class SyncEngine(BaseEngine):
99
99
 
100
100
  return result if executed else None
101
101
 
102
+ def enabled_events(self, *args, **kwargs):
103
+ sm = self.sm
104
+ enabled = {}
105
+ for transition in sm.current_state.transitions:
106
+ for event in transition.events:
107
+ if event in enabled:
108
+ continue
109
+ extended_kwargs = kwargs.copy()
110
+ extended_kwargs.update(
111
+ {
112
+ "machine": sm,
113
+ "model": sm.model,
114
+ "event": getattr(sm, event),
115
+ "source": transition.source,
116
+ "target": transition.target,
117
+ "state": sm.current_state,
118
+ "transition": transition,
119
+ }
120
+ )
121
+ try:
122
+ if sm._callbacks.all(transition.cond.key, *args, **extended_kwargs):
123
+ enabled[event] = getattr(sm, event)
124
+ except Exception:
125
+ enabled[event] = getattr(sm, event)
126
+ return list(enabled.values())
127
+
102
128
  def _activate(self, trigger_data: TriggerData, transition: "Transition"):
103
129
  event_data = EventData(trigger_data=trigger_data, transition=transition)
104
130
  args, kwargs = event_data.args, event_data.extended_kwargs
statemachine/event.py CHANGED
@@ -28,9 +28,9 @@ _event_data_kwargs = {
28
28
 
29
29
 
30
30
  class Event(AddCallbacksMixin, str):
31
- """An event is triggers a signal that something has happened.
31
+ """An event triggers a signal that something has happened.
32
32
 
33
- They are send to a state machine and allow the state machine to react.
33
+ They are sent to a state machine and allow the state machine to react.
34
34
 
35
35
  An event starts a :ref:`Transition`, which can be thought of as a “cause” that initiates a
36
36
  change in the state of the system.
@@ -1,24 +1,35 @@
1
- # This file is distributed under the same license as the PROJECT project.
1
+ # This file is distributed under the same license as the project.
2
2
  # Fernando Macedo <fgmacedo@gmail.com>, 2024.
3
3
  #
4
4
  msgid ""
5
5
  msgstr ""
6
- "Project-Id-Version: 2.4.0\n"
6
+ "Project-Id-Version: 2.4.0\n"
7
7
  "Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
8
- "POT-Creation-Date: 2023-03-04 16:10-0300\n"
8
+ "POT-Creation-Date: 2026-02-13 18:28-0300\n"
9
9
  "PO-Revision-Date: 2024-06-07 17:41-0300\n"
10
10
  "Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
11
+ "Language: en\n"
12
+ "Language-Team: en <LL@li.org>\n"
13
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
11
14
  "MIME-Version: 1.0\n"
12
15
  "Content-Type: text/plain; charset=utf-8\n"
13
16
  "Content-Transfer-Encoding: 8bit\n"
14
- "Generated-By: Babel 2.12.1\n"
17
+ "Generated-By: Babel 2.16.0\n"
15
18
 
16
- #: statemachine/callbacks.py:165
19
+ #: statemachine/callbacks.py:349 statemachine/callbacks.py:354
20
+ msgid "Did not found name '{}' from model or statemachine"
21
+ msgstr ""
22
+
23
+ #: statemachine/dispatcher.py:126
17
24
  msgid "Failed to parse boolean expression '{}'"
18
25
  msgstr ""
19
26
 
20
- #: statemachine/callbacks.py:407 statemachine/callbacks.py:412
21
- msgid "Did not found name '{}' from model or statemachine"
27
+ #: statemachine/event.py:90
28
+ msgid "Cannot add callback '{}' to an event with no transitions."
29
+ msgstr ""
30
+
31
+ #: statemachine/event.py:123
32
+ msgid "Event {} cannot be called without a SM instance"
22
33
  msgstr ""
23
34
 
24
35
  #: statemachine/exceptions.py:24
@@ -29,64 +40,68 @@ msgstr ""
29
40
  msgid "Can't {} when in {}."
30
41
  msgstr ""
31
42
 
32
- #: statemachine/factory.py:74
43
+ #: statemachine/factory.py:73
33
44
  msgid "There are no states."
34
45
  msgstr ""
35
46
 
36
- #: statemachine/factory.py:77
47
+ #: statemachine/factory.py:76
37
48
  msgid "There are no events."
38
49
  msgstr ""
39
50
 
40
- #: statemachine/factory.py:89
51
+ #: statemachine/factory.py:88
41
52
  msgid ""
42
53
  "There should be one and only one initial state. You currently have these:"
43
54
  " {!r}"
44
55
  msgstr ""
45
56
 
46
- #: statemachine/factory.py:102
57
+ #: statemachine/factory.py:101
47
58
  msgid "Cannot declare transitions from final state. Invalid state(s): {}"
48
59
  msgstr ""
49
60
 
50
- #: statemachine/factory.py:110
61
+ #: statemachine/factory.py:109
51
62
  msgid ""
52
63
  "All non-final states should have at least one outgoing transition. These "
53
64
  "states have no outgoing transition: {!r}"
54
65
  msgstr ""
55
66
 
56
- #: statemachine/factory.py:124
67
+ #: statemachine/factory.py:123
57
68
  msgid ""
58
69
  "All non-final states should have at least one path to a final state. "
59
70
  "These states have no path to a final state: {!r}"
60
71
  msgstr ""
61
72
 
62
- #: statemachine/factory.py:148
73
+ #: statemachine/factory.py:147
63
74
  msgid ""
64
75
  "There are unreachable states. The statemachine graph should have a single"
65
76
  " component. Disconnected states: {}"
66
77
  msgstr ""
67
78
 
68
- #: statemachine/factory.py:257
79
+ #: statemachine/factory.py:253
69
80
  msgid "An event in the '{}' has no id."
70
81
  msgstr ""
71
82
 
72
- #: statemachine/mixins.py:26
83
+ #: statemachine/mixins.py:28
73
84
  msgid "{!r} is not a valid state machine name."
74
85
  msgstr ""
75
86
 
76
- #: statemachine/state.py:155
87
+ #: statemachine/state.py:194
77
88
  msgid "State overriding is not allowed. Trying to add '{}' to {}"
78
89
  msgstr ""
79
90
 
80
- #: statemachine/statemachine.py:94
91
+ #: statemachine/statemachine.py:89
81
92
  msgid "There are no states or transitions."
82
93
  msgstr ""
83
94
 
84
- #: statemachine/statemachine.py:285
95
+ #: statemachine/statemachine.py:277
85
96
  msgid ""
86
97
  "There's no current state set. In async code, did you activate the initial"
87
98
  " state? (e.g., `await sm.activate_initial_state()`)"
88
99
  msgstr ""
89
100
 
90
- #: statemachine/engines/async_.py:22
101
+ #: statemachine/transition_mixin.py:15
102
+ msgid "{} only supports the decorator syntax to register callbacks."
103
+ msgstr ""
104
+
105
+ #: statemachine/engines/async_.py:18
91
106
  msgid "Only RTC is supported on async engine"
92
107
  msgstr ""
@@ -5,22 +5,32 @@ msgid ""
5
5
  msgstr ""
6
6
  "Project-Id-Version: 2.4.0\n"
7
7
  "Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
8
- "POT-Creation-Date: 2023-03-04 16:10-0300\n"
8
+ "POT-Creation-Date: 2026-02-13 18:28-0300\n"
9
9
  "PO-Revision-Date: 2024-06-07 17:41-0300\n"
10
10
  "Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
11
- "Language-Team: LANGUAGE <LL@li.org>\n"
11
+ "Language: hi_IN\n"
12
+ "Language-Team: hi_IN <LL@li.org>\n"
13
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
12
14
  "MIME-Version: 1.0\n"
13
15
  "Content-Type: text/plain; charset=utf-8\n"
14
16
  "Content-Transfer-Encoding: 8bit\n"
15
- "Generated-By: Babel 2.14.0\n"
17
+ "Generated-By: Babel 2.16.0\n"
16
18
 
17
- #: statemachine/callbacks.py:165
19
+ #: statemachine/callbacks.py:349 statemachine/callbacks.py:354
20
+ msgid "Did not found name '{}' from model or statemachine"
21
+ msgstr "मॉडल या स्टेटमशीन में नाम '{}' नहीं मिला"
22
+
23
+ #: statemachine/dispatcher.py:126
18
24
  msgid "Failed to parse boolean expression '{}'"
19
25
  msgstr "बूलियन अभिव्यक्ति '{}' को पार्स करने में विफल रहा"
20
26
 
21
- #: statemachine/callbacks.py:407 statemachine/callbacks.py:412
22
- msgid "Did not found name '{}' from model or statemachine"
23
- msgstr "मॉडल या स्टेटमशीन में नाम '{}' नहीं मिला"
27
+ #: statemachine/event.py:90
28
+ msgid "Cannot add callback '{}' to an event with no transitions."
29
+ msgstr "बिना ट्रांज़िशन वाले इवेंट में कॉलबैक '{}' नहीं जोड़ सकते।"
30
+
31
+ #: statemachine/event.py:123
32
+ msgid "Event {} cannot be called without a SM instance"
33
+ msgstr "इवेंट {} को SM इंस्टेंस के बिना कॉल नहीं किया जा सकता"
24
34
 
25
35
  #: statemachine/exceptions.py:24
26
36
  msgid "{!r} is not a valid state value."
@@ -30,64 +40,80 @@ msgstr "{!r} एक मान्य स्टेट मान नहीं ह
30
40
  msgid "Can't {} when in {}."
31
41
  msgstr "{} स्थिति में {} नहीं कर सकते।"
32
42
 
33
- #: statemachine/factory.py:74
43
+ #: statemachine/factory.py:73
34
44
  msgid "There are no states."
35
45
  msgstr "कोई स्टेट नहीं है।"
36
46
 
37
- #: statemachine/factory.py:77
47
+ #: statemachine/factory.py:76
38
48
  msgid "There are no events."
39
49
  msgstr "कोई इवेंट नहीं है।"
40
50
 
41
- #: statemachine/factory.py:89
51
+ #: statemachine/factory.py:88
42
52
  msgid ""
43
53
  "There should be one and only one initial state. You currently have these:"
44
54
  " {!r}"
45
- msgstr "एक और केवल एक प्रारंभिक स्टेट होना चाहिए। वर्तमान में आपके पास ये हैं: {!r}"
55
+ msgstr ""
56
+ "एक और केवल एक प्रारंभिक स्टेट होना चाहिए। वर्तमान में आपके पास ये हैं: "
57
+ "{!r}"
46
58
 
47
- #: statemachine/factory.py:102
59
+ #: statemachine/factory.py:101
48
60
  msgid "Cannot declare transitions from final state. Invalid state(s): {}"
49
61
  msgstr "अंतिम स्टेट से ट्रांज़िशन घोषित नहीं कर सकते। अमान्य स्टेट: {}"
50
62
 
51
- #: statemachine/factory.py:110
63
+ #: statemachine/factory.py:109
52
64
  msgid ""
53
65
  "All non-final states should have at least one outgoing transition. These "
54
66
  "states have no outgoing transition: {!r}"
55
- msgstr "सभी गैर-अंतिम स्टेट में कम से कम एक आउटगोइंग ट्रांज़िशन होना चाहिए। इन स्टेट में कोई आउटगोइंग ट्रांज़िशन नहीं है: {!r}"
67
+ msgstr ""
68
+ "सभी गैर-अंतिम स्टेट में कम से कम एक आउटगोइंग ट्रांज़िशन होना चाहिए। इन "
69
+ "स्टेट में कोई आउटगोइंग ट्रांज़िशन नहीं है: {!r}"
56
70
 
57
- #: statemachine/factory.py:124
71
+ #: statemachine/factory.py:123
58
72
  msgid ""
59
73
  "All non-final states should have at least one path to a final state. "
60
74
  "These states have no path to a final state: {!r}"
61
- msgstr "सभी गैर-अंतिम स्टेट में अंतिम स्टेट तक कम से कम एक पथ होना चाहिए। इन स्टेट में अंतिम स्टेट तक कोई पथ नहीं है: {!r}"
75
+ msgstr ""
76
+ "सभी गैर-अंतिम स्टेट में अंतिम स्टेट तक कम से कम एक पथ होना चाहिए। इन "
77
+ "स्टेट में अंतिम स्टेट तक कोई पथ नहीं है: {!r}"
62
78
 
63
- #: statemachine/factory.py:148
79
+ #: statemachine/factory.py:147
64
80
  msgid ""
65
81
  "There are unreachable states. The statemachine graph should have a single"
66
82
  " component. Disconnected states: {}"
67
- msgstr "कुछ स्टेट पहुंच योग्य नहीं हैं। स्टेटमशीन ग्राफ में एक ही घटक होना चाहिए। डिस्कनेक्टेड स्टेट: {}"
83
+ msgstr ""
84
+ "कुछ स्टेट पहुंच योग्य नहीं हैं। स्टेटमशीन ग्राफ में एक ही घटक होना चाहिए।"
85
+ " डिस्कनेक्टेड स्टेट: {}"
68
86
 
69
- #: statemachine/factory.py:257
87
+ #: statemachine/factory.py:253
70
88
  msgid "An event in the '{}' has no id."
71
89
  msgstr "'{}' में एक इवेंट का आईडी नहीं है।"
72
90
 
73
- #: statemachine/mixins.py:26
91
+ #: statemachine/mixins.py:28
74
92
  msgid "{!r} is not a valid state machine name."
75
93
  msgstr "{!r} एक मान्य स्टेटमशीन नाम नहीं है।"
76
94
 
77
- #: statemachine/state.py:155
95
+ #: statemachine/state.py:194
78
96
  msgid "State overriding is not allowed. Trying to add '{}' to {}"
79
- msgstr "स्टेट ओवरराइड करना अनुमति नहीं है। '{}' को {} में जोड़ने की कोशिश कर रहे हैं"
97
+ msgstr ""
98
+ "स्टेट ओवरराइड करना अनुमति नहीं है। '{}' को {} में जोड़ने की कोशिश कर रहे "
99
+ "हैं"
80
100
 
81
- #: statemachine/statemachine.py:94
101
+ #: statemachine/statemachine.py:89
82
102
  msgid "There are no states or transitions."
83
103
  msgstr "कोई स्टेट या ट्रांज़िशन नहीं हैं।"
84
104
 
85
- #: statemachine/statemachine.py:285
105
+ #: statemachine/statemachine.py:277
86
106
  msgid ""
87
107
  "There's no current state set. In async code, did you activate the initial"
88
108
  " state? (e.g., `await sm.activate_initial_state()`)"
89
- msgstr "कोई वर्तमान स्टेट सेट नहीं है। असिंक्रोनस कोड में, क्या आपने प्रारंभिक स्टेट को सक्रिय किया? (उदाहरण: `await sm.activate_initial_state()`)"
109
+ msgstr ""
110
+ "कोई वर्तमान स्टेट सेट नहीं है। असिंक्रोनस कोड में, क्या आपने प्रारंभिक "
111
+ "स्टेट को सक्रिय किया? (उदाहरण: `await sm.activate_initial_state()`)"
112
+
113
+ #: statemachine/transition_mixin.py:15
114
+ msgid "{} only supports the decorator syntax to register callbacks."
115
+ msgstr "{} केवल कॉलबैक रजिस्टर करने के लिए डेकोरेटर सिंटैक्स का समर्थन करता है।"
90
116
 
91
- #: statemachine/engines/async_.py:22
117
+ #: statemachine/engines/async_.py:18
92
118
  msgid "Only RTC is supported on async engine"
93
119
  msgstr "असिंक्रोनस इंजन पर केवल RTC समर्थित है"
@@ -5,22 +5,32 @@ msgid ""
5
5
  msgstr ""
6
6
  "Project-Id-Version: 2.4.0\n"
7
7
  "Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
8
- "POT-Creation-Date: 2023-03-04 16:10-0300\n"
8
+ "POT-Creation-Date: 2026-02-13 18:28-0300\n"
9
9
  "PO-Revision-Date: 2024-06-07 17:41-0300\n"
10
10
  "Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
11
- "Language-Team: LANGUAGE <LL@li.org>\n"
11
+ "Language: pt_BR\n"
12
+ "Language-Team: pt_BR <LL@li.org>\n"
13
+ "Plural-Forms: nplurals=2; plural=(n > 1);\n"
12
14
  "MIME-Version: 1.0\n"
13
15
  "Content-Type: text/plain; charset=utf-8\n"
14
16
  "Content-Transfer-Encoding: 8bit\n"
15
- "Generated-By: Babel 2.14.0\n"
17
+ "Generated-By: Babel 2.16.0\n"
16
18
 
17
- #: statemachine/callbacks.py:165
19
+ #: statemachine/callbacks.py:349 statemachine/callbacks.py:354
20
+ msgid "Did not found name '{}' from model or statemachine"
21
+ msgstr "Nome '{}' não encontrado no modelo ou na máquina de estados"
22
+
23
+ #: statemachine/dispatcher.py:126
18
24
  msgid "Failed to parse boolean expression '{}'"
19
25
  msgstr "Falha ao interpretar a expressão booleana '{}'"
20
26
 
21
- #: statemachine/callbacks.py:407 statemachine/callbacks.py:412
22
- msgid "Did not found name '{}' from model or statemachine"
23
- msgstr "Nome '{}' não encontrado no modelo ou na máquina de estados"
27
+ #: statemachine/event.py:90
28
+ msgid "Cannot add callback '{}' to an event with no transitions."
29
+ msgstr "Não é possível adicionar callback '{}' a um evento sem transições."
30
+
31
+ #: statemachine/event.py:123
32
+ msgid "Event {} cannot be called without a SM instance"
33
+ msgstr "O evento {} não pode ser chamado sem uma instância de SM"
24
34
 
25
35
  #: statemachine/exceptions.py:24
26
36
  msgid "{!r} is not a valid state value."
@@ -30,64 +40,81 @@ msgstr "{!r} não é um valor de estado válido."
30
40
  msgid "Can't {} when in {}."
31
41
  msgstr "Não é possível {} quando em {}."
32
42
 
33
- #: statemachine/factory.py:74
43
+ #: statemachine/factory.py:73
34
44
  msgid "There are no states."
35
45
  msgstr "Não há estados."
36
46
 
37
- #: statemachine/factory.py:77
47
+ #: statemachine/factory.py:76
38
48
  msgid "There are no events."
39
49
  msgstr "Não há eventos."
40
50
 
41
- #: statemachine/factory.py:89
51
+ #: statemachine/factory.py:88
42
52
  msgid ""
43
53
  "There should be one and only one initial state. You currently have these:"
44
54
  " {!r}"
45
- msgstr "Deve haver um e apenas um estado inicial. Atualmente, você possui estes: {!r}"
55
+ msgstr ""
56
+ "Deve haver um e apenas um estado inicial. Atualmente, você possui estes: "
57
+ "{!r}"
46
58
 
47
- #: statemachine/factory.py:102
59
+ #: statemachine/factory.py:101
48
60
  msgid "Cannot declare transitions from final state. Invalid state(s): {}"
49
- msgstr "Não é possível declarar transições a partir de um estado final. Estado(s) inválido(s): {}"
61
+ msgstr ""
62
+ "Não é possível declarar transições a partir de um estado final. Estado(s)"
63
+ " inválido(s): {}"
50
64
 
51
- #: statemachine/factory.py:110
65
+ #: statemachine/factory.py:109
52
66
  msgid ""
53
67
  "All non-final states should have at least one outgoing transition. These "
54
68
  "states have no outgoing transition: {!r}"
55
- msgstr "Todos os estados não finais devem ter pelo menos uma transição de saída. Estes estados não possuem transição de saída: {!r}"
69
+ msgstr ""
70
+ "Todos os estados não finais devem ter pelo menos uma transição de saída. "
71
+ "Estes estados não possuem transição de saída: {!r}"
56
72
 
57
- #: statemachine/factory.py:124
73
+ #: statemachine/factory.py:123
58
74
  msgid ""
59
75
  "All non-final states should have at least one path to a final state. "
60
76
  "These states have no path to a final state: {!r}"
61
- msgstr "Todos os estados não finais devem ter pelo menos um caminho para um estado final. Estes estados não possuem caminho para um estado final: {!r}"
77
+ msgstr ""
78
+ "Todos os estados não finais devem ter pelo menos um caminho para um "
79
+ "estado final. Estes estados não possuem caminho para um estado final: "
80
+ "{!r}"
62
81
 
63
- #: statemachine/factory.py:148
82
+ #: statemachine/factory.py:147
64
83
  msgid ""
65
84
  "There are unreachable states. The statemachine graph should have a single"
66
85
  " component. Disconnected states: {}"
67
- msgstr "Há estados inacessíveis. O grafo da máquina de estados deve ter um único componente. Estados desconectados: {}"
86
+ msgstr ""
87
+ "Há estados inacessíveis. O grafo da máquina de estados deve ter um único "
88
+ "componente. Estados desconectados: {}"
68
89
 
69
- #: statemachine/factory.py:257
90
+ #: statemachine/factory.py:253
70
91
  msgid "An event in the '{}' has no id."
71
92
  msgstr "Um evento em '{}' não possui id."
72
93
 
73
- #: statemachine/mixins.py:26
94
+ #: statemachine/mixins.py:28
74
95
  msgid "{!r} is not a valid state machine name."
75
96
  msgstr "{!r} não é um nome de máquina de estados válido."
76
97
 
77
- #: statemachine/state.py:155
98
+ #: statemachine/state.py:194
78
99
  msgid "State overriding is not allowed. Trying to add '{}' to {}"
79
100
  msgstr "Sobrescrever estados não é permitido. Tentando adicionar '{}' a {}"
80
101
 
81
- #: statemachine/statemachine.py:94
102
+ #: statemachine/statemachine.py:89
82
103
  msgid "There are no states or transitions."
83
104
  msgstr "Não há estados ou transições."
84
105
 
85
- #: statemachine/statemachine.py:285
106
+ #: statemachine/statemachine.py:277
86
107
  msgid ""
87
108
  "There's no current state set. In async code, did you activate the initial"
88
109
  " state? (e.g., `await sm.activate_initial_state()`)"
89
- msgstr "Nenhum estado atual definido. Em código assíncrono, você ativou o estado inicial? (ex.: `await sm.activate_initial_state()`)"
110
+ msgstr ""
111
+ "Nenhum estado atual definido. Em código assíncrono, você ativou o estado "
112
+ "inicial? (ex.: `await sm.activate_initial_state()`)"
113
+
114
+ #: statemachine/transition_mixin.py:15
115
+ msgid "{} only supports the decorator syntax to register callbacks."
116
+ msgstr "{} suporta apenas a sintaxe de decorator para registrar callbacks."
90
117
 
91
- #: statemachine/engines/async_.py:22
118
+ #: statemachine/engines/async_.py:18
92
119
  msgid "Only RTC is supported on async engine"
93
120
  msgstr "Apenas RTC é suportado no motor assíncrono"
@@ -5,22 +5,32 @@ msgid ""
5
5
  msgstr ""
6
6
  "Project-Id-Version: 2.4.0\n"
7
7
  "Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
8
- "POT-Creation-Date: 2023-03-04 16:10-0300\n"
8
+ "POT-Creation-Date: 2026-02-13 18:28-0300\n"
9
9
  "PO-Revision-Date: 2024-06-07 17:41-0300\n"
10
10
  "Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
11
- "Language-Team: LANGUAGE <LL@li.org>\n"
11
+ "Language: zh_CN\n"
12
+ "Language-Team: zh_CN <LL@li.org>\n"
13
+ "Plural-Forms: nplurals=1; plural=0;\n"
12
14
  "MIME-Version: 1.0\n"
13
15
  "Content-Type: text/plain; charset=utf-8\n"
14
16
  "Content-Transfer-Encoding: 8bit\n"
15
- "Generated-By: Babel 2.14.0\n"
17
+ "Generated-By: Babel 2.16.0\n"
16
18
 
17
- #: statemachine/callbacks.py:165
19
+ #: statemachine/callbacks.py:349 statemachine/callbacks.py:354
20
+ msgid "Did not found name '{}' from model or statemachine"
21
+ msgstr "在模型或状态机中未找到名称 '{}'"
22
+
23
+ #: statemachine/dispatcher.py:126
18
24
  msgid "Failed to parse boolean expression '{}'"
19
25
  msgstr "无法解析布尔表达式 '{}'"
20
26
 
21
- #: statemachine/callbacks.py:407 statemachine/callbacks.py:412
22
- msgid "Did not found name '{}' from model or statemachine"
23
- msgstr "在模型或状态机中未找到名称 '{}'"
27
+ #: statemachine/event.py:90
28
+ msgid "Cannot add callback '{}' to an event with no transitions."
29
+ msgstr "无法将回调 '{}' 添加到没有转换的事件。"
30
+
31
+ #: statemachine/event.py:123
32
+ msgid "Event {} cannot be called without a SM instance"
33
+ msgstr "事件 {} 不能在没有 SM 实例的情况下调用"
24
34
 
25
35
  #: statemachine/exceptions.py:24
26
36
  msgid "{!r} is not a valid state value."
@@ -30,64 +40,68 @@ msgstr "{!r} 不是有效的状态值。"
30
40
  msgid "Can't {} when in {}."
31
41
  msgstr "在 {} 时无法 {}。"
32
42
 
33
- #: statemachine/factory.py:74
43
+ #: statemachine/factory.py:73
34
44
  msgid "There are no states."
35
45
  msgstr "没有状态。"
36
46
 
37
- #: statemachine/factory.py:77
47
+ #: statemachine/factory.py:76
38
48
  msgid "There are no events."
39
49
  msgstr "没有事件。"
40
50
 
41
- #: statemachine/factory.py:89
51
+ #: statemachine/factory.py:88
42
52
  msgid ""
43
53
  "There should be one and only one initial state. You currently have these:"
44
54
  " {!r}"
45
55
  msgstr "应有且仅有一个初始状态。当前您有这些:{!r}"
46
56
 
47
- #: statemachine/factory.py:102
57
+ #: statemachine/factory.py:101
48
58
  msgid "Cannot declare transitions from final state. Invalid state(s): {}"
49
59
  msgstr "无法从终止状态声明转换。无效状态:{}"
50
60
 
51
- #: statemachine/factory.py:110
61
+ #: statemachine/factory.py:109
52
62
  msgid ""
53
63
  "All non-final states should have at least one outgoing transition. These "
54
64
  "states have no outgoing transition: {!r}"
55
65
  msgstr "所有非终止状态都应至少有一个外部转换。这些状态没有外部转换:{!r}"
56
66
 
57
- #: statemachine/factory.py:124
67
+ #: statemachine/factory.py:123
58
68
  msgid ""
59
69
  "All non-final states should have at least one path to a final state. "
60
70
  "These states have no path to a final state: {!r}"
61
71
  msgstr "所有非终止状态应至少有一个到终止状态的路径。这些状态没有到终止状态的路径:{!r}"
62
72
 
63
- #: statemachine/factory.py:148
73
+ #: statemachine/factory.py:147
64
74
  msgid ""
65
75
  "There are unreachable states. The statemachine graph should have a single"
66
76
  " component. Disconnected states: {}"
67
77
  msgstr "存在不可到达的状态。状态机图应具有单个组件。断开的状态:{}"
68
78
 
69
- #: statemachine/factory.py:257
79
+ #: statemachine/factory.py:253
70
80
  msgid "An event in the '{}' has no id."
71
81
  msgstr "'{}' 中的事件没有 ID。"
72
82
 
73
- #: statemachine/mixins.py:26
83
+ #: statemachine/mixins.py:28
74
84
  msgid "{!r} is not a valid state machine name."
75
85
  msgstr "{!r} 不是有效的状态机名称。"
76
86
 
77
- #: statemachine/state.py:155
87
+ #: statemachine/state.py:194
78
88
  msgid "State overriding is not allowed. Trying to add '{}' to {}"
79
89
  msgstr "不允许覆盖状态。尝试将 '{}' 添加到 {}"
80
90
 
81
- #: statemachine/statemachine.py:94
91
+ #: statemachine/statemachine.py:89
82
92
  msgid "There are no states or transitions."
83
93
  msgstr "没有状态或转换。"
84
94
 
85
- #: statemachine/statemachine.py:285
95
+ #: statemachine/statemachine.py:277
86
96
  msgid ""
87
97
  "There's no current state set. In async code, did you activate the initial"
88
98
  " state? (e.g., `await sm.activate_initial_state()`)"
89
99
  msgstr "没有设置当前状态。在异步代码中,您是否激活了初始状态?(例如,`await sm.activate_initial_state()`)"
90
100
 
91
- #: statemachine/engines/async_.py:22
101
+ #: statemachine/transition_mixin.py:15
102
+ msgid "{} only supports the decorator syntax to register callbacks."
103
+ msgstr "{} 仅支持使用装饰器语法注册回调。"
104
+
105
+ #: statemachine/engines/async_.py:18
92
106
  msgid "Only RTC is supported on async engine"
93
107
  msgstr "异步引擎仅支持 RTC"
statemachine/mixins.py CHANGED
@@ -22,6 +22,8 @@ class MachineMixin:
22
22
  def __init__(self, *args, **kwargs):
23
23
  super().__init__(*args, **kwargs)
24
24
  if not self.state_machine_name:
25
+ if self._is_django_historical_model():
26
+ return
25
27
  raise ValueError(
26
28
  _("{!r} is not a valid state machine name.").format(self.state_machine_name)
27
29
  )
@@ -34,3 +36,12 @@ class MachineMixin:
34
36
  )
35
37
  if self.bind_events_as_methods:
36
38
  sm.bind_events_to(self)
39
+
40
+ @classmethod
41
+ def _is_django_historical_model(cls) -> bool:
42
+ """Detect Django historical models created by ``apps.get_model()`` in migrations.
43
+
44
+ Django sets ``__module__ = '__fake__'`` on these dynamically-created classes,
45
+ which lack the user-defined class attributes like ``state_machine_name``.
46
+ """
47
+ return getattr(cls, "__module__", None) == "__fake__"
statemachine/signature.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from functools import partial
2
4
  from inspect import BoundArguments
3
5
  from inspect import Parameter
@@ -6,6 +8,12 @@ from inspect import iscoroutinefunction
6
8
  from itertools import chain
7
9
  from types import MethodType
8
10
  from typing import Any
11
+ from typing import FrozenSet
12
+ from typing import Optional
13
+ from typing import Tuple
14
+
15
+ BindCacheKey = Tuple[int, FrozenSet[str]]
16
+ BindTemplate = Tuple[Tuple[str, ...], Optional[str], Optional[str]] # noqa: UP007
9
17
 
10
18
 
11
19
  def _make_key(method):
@@ -44,6 +52,11 @@ def signature_cache(user_function):
44
52
 
45
53
  class SignatureAdapter(Signature):
46
54
  is_coroutine: bool = False
55
+ _bind_cache: dict[BindCacheKey, BindTemplate]
56
+
57
+ def __init__(self, *args, **kwargs):
58
+ super().__init__(*args, **kwargs)
59
+ self._bind_cache = {}
47
60
 
48
61
  @classmethod
49
62
  @signature_cache
@@ -60,19 +73,71 @@ class SignatureAdapter(Signature):
60
73
  adapter.is_coroutine = iscoroutinefunction(method)
61
74
  return adapter
62
75
 
63
- def bind_expected(self, *args: Any, **kwargs: Any) -> BoundArguments: # noqa: C901
76
+ def bind_expected(self, *args: Any, **kwargs: Any) -> BoundArguments:
77
+ cache_key: BindCacheKey = (len(args), frozenset(kwargs.keys()))
78
+ template = self._bind_cache.get(cache_key)
79
+
80
+ if template is not None:
81
+ return self._fast_bind(args, kwargs, template)
82
+
83
+ result = self._full_bind(cache_key, *args, **kwargs)
84
+ return result
85
+
86
+ def _fast_bind(
87
+ self,
88
+ args: tuple[Any, ...],
89
+ kwargs: dict[str, Any],
90
+ template: BindTemplate,
91
+ ) -> BoundArguments:
92
+ param_names, kwargs_param_name, var_positional_name = template
93
+ arguments: dict[str, Any] = {}
94
+ past_var_positional = False
95
+
96
+ for i, name in enumerate(param_names):
97
+ if name == var_positional_name:
98
+ # Collect all remaining positional args into a tuple
99
+ arguments[name] = args[i:]
100
+ past_var_positional = True
101
+ elif past_var_positional:
102
+ # After *args, remaining params are keyword-only
103
+ arguments[name] = kwargs.get(name)
104
+ elif i < len(args):
105
+ # Match _full_bind: if param is also in kwargs, kwargs wins
106
+ # (POSITIONAL_OR_KEYWORD params prefer kwargs over positional args)
107
+ if name in kwargs:
108
+ arguments[name] = kwargs[name]
109
+ else:
110
+ arguments[name] = args[i]
111
+ else:
112
+ arguments[name] = kwargs.get(name)
113
+
114
+ if kwargs_param_name is not None:
115
+ matched = set(param_names)
116
+ arguments[kwargs_param_name] = {k: v for k, v in kwargs.items() if k not in matched}
117
+
118
+ return BoundArguments(self, arguments) # type: ignore[arg-type]
119
+
120
+ def _full_bind( # noqa: C901
121
+ self,
122
+ cache_key: BindCacheKey,
123
+ *args: Any,
124
+ **kwargs: Any,
125
+ ) -> BoundArguments:
64
126
  """Get a BoundArguments object, that maps the passed `args`
65
127
  and `kwargs` to the function's signature. It avoids to raise `TypeError`
66
128
  trying to fill all the required arguments and ignoring the unknown ones.
67
129
 
68
130
  Adapted from the internal `inspect.Signature._bind`.
69
131
  """
70
- arguments = {}
132
+ arguments: dict[str, Any] = {}
133
+ param_names_used: list[str] = []
71
134
 
72
135
  parameters = iter(self.parameters.values())
73
136
  arg_vals = iter(args)
74
137
  parameters_ex: Any = ()
75
138
  kwargs_param = None
139
+ kwargs_param_name: str | None = None
140
+ var_positional_name: str | None = None
76
141
 
77
142
  while True:
78
143
  # Let's iterate through the positional arguments and corresponding
@@ -95,8 +160,7 @@ class SignatureAdapter(Signature):
95
160
  elif param.name in kwargs:
96
161
  if param.kind == Parameter.POSITIONAL_ONLY:
97
162
  msg = (
98
- "{arg!r} parameter is positional only, "
99
- "but was passed as a keyword"
163
+ "{arg!r} parameter is positional only, but was passed as a keyword"
100
164
  )
101
165
  msg = msg.format(arg=param.name)
102
166
  raise TypeError(msg) from None
@@ -141,12 +205,15 @@ class SignatureAdapter(Signature):
141
205
  values = [arg_val]
142
206
  values.extend(arg_vals)
143
207
  arguments[param.name] = tuple(values)
208
+ param_names_used.append(param.name)
209
+ var_positional_name = param.name
144
210
  break
145
211
 
146
212
  if param.name in kwargs and param.kind != Parameter.POSITIONAL_ONLY:
147
213
  arguments[param.name] = kwargs.pop(param.name)
148
214
  else:
149
215
  arguments[param.name] = arg_val
216
+ param_names_used.append(param.name)
150
217
 
151
218
  # Now, we iterate through the remaining parameters to process
152
219
  # keyword arguments
@@ -172,14 +239,19 @@ class SignatureAdapter(Signature):
172
239
  # arguments.
173
240
  pass
174
241
  else:
175
- arguments[param_name] = arg_val #
242
+ arguments[param_name] = arg_val
243
+ param_names_used.append(param_name)
176
244
 
177
245
  if kwargs:
178
246
  if kwargs_param is not None:
179
247
  # Process our '**kwargs'-like parameter
180
- arguments[kwargs_param.name] = kwargs # type: ignore [assignment]
248
+ arguments[kwargs_param.name] = kwargs # type: ignore[assignment]
249
+ kwargs_param_name = kwargs_param.name
181
250
  else:
182
251
  # 'ignoring we got an unexpected keyword argument'
183
252
  pass
184
253
 
185
- return BoundArguments(self, arguments) # type: ignore [arg-type]
254
+ template: BindTemplate = (tuple(param_names_used), kwargs_param_name, var_positional_name)
255
+ self._bind_cache[cache_key] = template
256
+
257
+ return BoundArguments(self, arguments) # type: ignore[arg-type]
@@ -2,6 +2,7 @@ import ast
2
2
  import operator
3
3
  import re
4
4
  from functools import reduce
5
+ from inspect import isawaitable
5
6
  from typing import Callable
6
7
 
7
8
  replacements = {"!": "not ", "^": " and ", "v": " or "}
@@ -33,8 +34,15 @@ def replace_operators(expr: str) -> str:
33
34
 
34
35
 
35
36
  def custom_not(predicate: Callable) -> Callable:
36
- def decorated(*args, **kwargs) -> bool:
37
- return not predicate(*args, **kwargs)
37
+ def decorated(*args, **kwargs):
38
+ result = predicate(*args, **kwargs)
39
+ if isawaitable(result):
40
+
41
+ async def _negate():
42
+ return not await result
43
+
44
+ return _negate()
45
+ return not result
38
46
 
39
47
  decorated.__name__ = f"not({predicate.__name__})"
40
48
  unique_key = getattr(predicate, "unique_key", "")
@@ -43,8 +51,26 @@ def custom_not(predicate: Callable) -> Callable:
43
51
 
44
52
 
45
53
  def custom_and(left: Callable, right: Callable) -> Callable:
46
- def decorated(*args, **kwargs) -> bool:
47
- return left(*args, **kwargs) and right(*args, **kwargs) # type: ignore[no-any-return]
54
+ def decorated(*args, **kwargs):
55
+ left_result = left(*args, **kwargs)
56
+ if isawaitable(left_result):
57
+
58
+ async def _async_and():
59
+ lr = await left_result
60
+ if not lr:
61
+ return lr
62
+ rr = right(*args, **kwargs)
63
+ if isawaitable(rr):
64
+ return await rr
65
+ return rr
66
+
67
+ return _async_and()
68
+ if not left_result:
69
+ return left_result
70
+ right_result = right(*args, **kwargs)
71
+ if isawaitable(right_result):
72
+ return right_result
73
+ return right_result
48
74
 
49
75
  decorated.__name__ = f"({left.__name__} and {right.__name__})"
50
76
  decorated.unique_key = _unique_key(left, right, "and") # type: ignore[attr-defined]
@@ -52,8 +78,26 @@ def custom_and(left: Callable, right: Callable) -> Callable:
52
78
 
53
79
 
54
80
  def custom_or(left: Callable, right: Callable) -> Callable:
55
- def decorated(*args, **kwargs) -> bool:
56
- return left(*args, **kwargs) or right(*args, **kwargs) # type: ignore[no-any-return]
81
+ def decorated(*args, **kwargs):
82
+ left_result = left(*args, **kwargs)
83
+ if isawaitable(left_result):
84
+
85
+ async def _async_or():
86
+ lr = await left_result
87
+ if lr:
88
+ return lr
89
+ rr = right(*args, **kwargs)
90
+ if isawaitable(rr):
91
+ return await rr
92
+ return rr
93
+
94
+ return _async_or()
95
+ if left_result:
96
+ return left_result
97
+ right_result = right(*args, **kwargs)
98
+ if isawaitable(right_result):
99
+ return right_result
100
+ return right_result
57
101
 
58
102
  decorated.__name__ = f"({left.__name__} or {right.__name__})"
59
103
  decorated.unique_key = _unique_key(left, right, "or") # type: ignore[attr-defined]
@@ -73,8 +117,18 @@ def build_custom_operator(operator) -> Callable:
73
117
  operator_repr = comparison_repr[operator]
74
118
 
75
119
  def custom_comparator(left: Callable, right: Callable) -> Callable:
76
- def decorated(*args, **kwargs) -> bool:
77
- return bool(operator(left(*args, **kwargs), right(*args, **kwargs)))
120
+ def decorated(*args, **kwargs):
121
+ left_result = left(*args, **kwargs)
122
+ right_result = right(*args, **kwargs)
123
+ if isawaitable(left_result) or isawaitable(right_result):
124
+
125
+ async def _async_compare():
126
+ lr = (await left_result) if isawaitable(left_result) else left_result
127
+ rr = (await right_result) if isawaitable(right_result) else right_result
128
+ return bool(operator(lr, rr))
129
+
130
+ return _async_compare()
131
+ return bool(operator(left_result, right_result))
78
132
 
79
133
  decorated.__name__ = f"({left.__name__} {operator_repr} {right.__name__})"
80
134
  decorated.unique_key = _unique_key(left, right, operator_repr) # type: ignore[attr-defined]
@@ -75,7 +75,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
75
75
  allow_event_without_transition: bool = False,
76
76
  listeners: "List[object] | None" = None,
77
77
  ):
78
- self.model = model if model else Model()
78
+ self.model = model if model is not None else Model()
79
79
  self.state_field = state_field
80
80
  self.start_value = start_value
81
81
  self.allow_event_without_transition = allow_event_without_transition
@@ -147,6 +147,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
147
147
  self._register_callbacks([])
148
148
  self.add_listener(*listeners.keys())
149
149
  self._engine = self._get_engine(rtc)
150
+ self._engine.start()
150
151
 
151
152
  def _get_initial_state(self):
152
153
  initial_state_value = self.start_value if self.start_value else self.initial_state.value
@@ -182,7 +183,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
182
183
  return self
183
184
 
184
185
  def _register_callbacks(self, listeners: List[object]):
185
- self._listeners.update({listener: None for listener in listeners})
186
+ self._listeners.update(dict.fromkeys(listeners))
186
187
  self._add_listener(
187
188
  Listeners.from_listeners(
188
189
  (
@@ -223,7 +224,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
223
224
 
224
225
  :ref:`listeners`.
225
226
  """
226
- self._listeners.update({o: None for o in listeners})
227
+ self._listeners.update(dict.fromkeys(listeners))
227
228
  return self._add_listener(
228
229
  Listeners.from_listeners(Listener.from_obj(o) for o in listeners),
229
230
  allowed_references=SPECS_SAFE,
@@ -294,6 +295,24 @@ class StateMachine(metaclass=StateMachineMetaclass):
294
295
  """List of the current allowed events."""
295
296
  return [getattr(self, event) for event in self.current_state.transitions.unique_events]
296
297
 
298
+ def enabled_events(self, *args, **kwargs):
299
+ """List of the current enabled events, considering guard conditions.
300
+
301
+ An event is **enabled** if at least one of its transitions from the current
302
+ state has all ``cond``/``unless`` guards satisfied.
303
+
304
+ Args:
305
+ *args: Positional arguments forwarded to condition callbacks.
306
+ **kwargs: Keyword arguments forwarded to condition callbacks.
307
+
308
+ Returns:
309
+ A list of enabled :ref:`Event` instances.
310
+ """
311
+ result = self._engine.enabled_events(*args, **kwargs)
312
+ if not isawaitable(result):
313
+ return result
314
+ return run_async_from_sync(result)
315
+
297
316
  def _put_nonblocking(self, trigger_data: TriggerData):
298
317
  """Put the trigger on the queue without blocking the caller."""
299
318
  self._engine.put(trigger_data)
@@ -1,14 +1,21 @@
1
1
  from typing import Callable
2
2
 
3
3
  from .callbacks import CallbackGroup
4
+ from .i18n import _
4
5
 
5
6
 
6
7
  class AddCallbacksMixin:
7
8
  def _add_callback(self, callback, grouper: CallbackGroup, is_event=False, **kwargs):
8
9
  raise NotImplementedError
9
10
 
10
- def __call__(self, f):
11
- return self._add_callback(f, CallbackGroup.ON, is_event=True)
11
+ def __call__(self, *args, **kwargs):
12
+ if len(args) == 1 and callable(args[0]) and not kwargs:
13
+ return self._add_callback(args[0], CallbackGroup.ON, is_event=True)
14
+ raise TypeError(
15
+ _("{} only supports the decorator syntax to register callbacks.").format(
16
+ type(self).__name__
17
+ )
18
+ )
12
19
 
13
20
  def before(self, f: Callable):
14
21
  """Adds a ``before`` :ref:`transition actions` callback to every :ref:`transition` in the