python-statemachine 3.2.1__py3-none-any.whl → 3.2.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.
- {python_statemachine-3.2.1.dist-info → python_statemachine-3.2.2.dist-info}/METADATA +7 -6
- {python_statemachine-3.2.1.dist-info → python_statemachine-3.2.2.dist-info}/RECORD +28 -27
- {python_statemachine-3.2.1.dist-info → python_statemachine-3.2.2.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +11 -0
- statemachine/contrib/diagram/__init__.py +15 -6
- statemachine/contrib/diagram/formatter.py +4 -2
- statemachine/contrib/diagram/graphviz.py +66 -0
- statemachine/contrib/diagram/sphinx_ext.py +28 -7
- statemachine/dispatcher.py +3 -2
- statemachine/engines/async_.py +209 -177
- statemachine/engines/base.py +245 -244
- statemachine/engines/sync.py +224 -108
- statemachine/event.py +29 -10
- statemachine/event_data.py +19 -3
- statemachine/events.py +5 -2
- statemachine/factory.py +126 -29
- statemachine/io/actions.py +4 -2
- statemachine/io/schemas/statechart.schema.json +8 -1
- statemachine/locale/en/LC_MESSAGES/statemachine.po +58 -17
- statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +66 -17
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +66 -17
- statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +60 -17
- statemachine/registry.py +6 -1
- statemachine/spec_parser.py +95 -19
- statemachine/state.py +45 -6
- statemachine/statemachine.py +42 -19
- {python_statemachine-3.2.1.dist-info → python_statemachine-3.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,7 @@ msgid ""
|
|
|
5
5
|
msgstr ""
|
|
6
6
|
"Project-Id-Version: 3.1.1\n"
|
|
7
7
|
"Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
|
|
8
|
-
"POT-Creation-Date: 2026-
|
|
8
|
+
"POT-Creation-Date: 2026-10-13 21:26-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
11
|
"Language: pt_BR\n"
|
|
@@ -16,7 +16,7 @@ msgstr ""
|
|
|
16
16
|
"Content-Transfer-Encoding: 8bit\n"
|
|
17
17
|
"Generated-By: Babel 2.18.0\n"
|
|
18
18
|
|
|
19
|
-
#: statemachine/callbacks.py:
|
|
19
|
+
#: statemachine/callbacks.py:409 statemachine/callbacks.py:414
|
|
20
20
|
#, python-brace-format
|
|
21
21
|
msgid "Did not found name '{}' from model or statemachine"
|
|
22
22
|
msgstr "Nome '{}' não encontrado no modelo ou na máquina de estados"
|
|
@@ -29,12 +29,24 @@ msgstr ""
|
|
|
29
29
|
"Nenhum estado atual definido. Em código assíncrono, você ativou o estado "
|
|
30
30
|
"inicial? (ex.: `await sm.activate_initial_state()`)"
|
|
31
31
|
|
|
32
|
-
#: statemachine/dispatcher.py:
|
|
32
|
+
#: statemachine/dispatcher.py:127
|
|
33
33
|
#, python-brace-format
|
|
34
34
|
msgid "Failed to parse boolean expression '{}'"
|
|
35
35
|
msgstr "Falha ao interpretar a expressão booleana '{}'"
|
|
36
36
|
|
|
37
|
-
#: statemachine/event.py:
|
|
37
|
+
#: statemachine/event.py:56
|
|
38
|
+
#, python-brace-format
|
|
39
|
+
msgid ""
|
|
40
|
+
"Event {id!r} cannot be both delayed and internal: the internal queue runs"
|
|
41
|
+
" within the current macrostep, which takes no time. Drop 'internal' to "
|
|
42
|
+
"deliver it later on the external queue, or drop 'delay' to run it now."
|
|
43
|
+
msgstr ""
|
|
44
|
+
"O evento {id!r} não pode ser atrasado e interno ao mesmo tempo: a fila "
|
|
45
|
+
"interna roda dentro do macrostep atual, que não leva tempo. Remova "
|
|
46
|
+
"'internal' para entregá-lo depois pela fila externa, ou remova 'delay' "
|
|
47
|
+
"para executá-lo agora."
|
|
48
|
+
|
|
49
|
+
#: statemachine/event.py:108
|
|
38
50
|
#, python-brace-format
|
|
39
51
|
msgid ""
|
|
40
52
|
"Event() received a non-string 'id' ({cls_name}). To combine multiple "
|
|
@@ -43,12 +55,12 @@ msgstr ""
|
|
|
43
55
|
"Event() recebeu um 'id' que não é uma string ({cls_name}). Para combinar "
|
|
44
56
|
"múltiplas transições em um único evento, use o operador |: t1 | t2."
|
|
45
57
|
|
|
46
|
-
#: statemachine/event.py:
|
|
58
|
+
#: statemachine/event.py:147
|
|
47
59
|
#, python-brace-format
|
|
48
60
|
msgid "Cannot add callback '{}' to an event with no transitions."
|
|
49
61
|
msgstr "Não é possível adicionar callback '{}' a um evento sem transições."
|
|
50
62
|
|
|
51
|
-
#: statemachine/event.py:
|
|
63
|
+
#: statemachine/event.py:182
|
|
52
64
|
#, python-brace-format
|
|
53
65
|
msgid "Event {} cannot be called without a SM instance"
|
|
54
66
|
msgstr "O evento {} não pode ser chamado sem uma instância de SM"
|
|
@@ -63,7 +75,7 @@ msgstr "{!r} não é um valor de estado válido."
|
|
|
63
75
|
msgid "Can't {} when in {}."
|
|
64
76
|
msgstr "Não é possível {} quando em {}."
|
|
65
77
|
|
|
66
|
-
#: statemachine/factory.py:
|
|
78
|
+
#: statemachine/factory.py:91
|
|
67
79
|
#, python-brace-format
|
|
68
80
|
msgid ""
|
|
69
81
|
"There should be one and only one initial state. Your currently have "
|
|
@@ -72,7 +84,36 @@ msgstr ""
|
|
|
72
84
|
"Deve haver um e apenas um estado inicial. Atualmente, você possui estes: "
|
|
73
85
|
"{0}"
|
|
74
86
|
|
|
75
|
-
#: statemachine/factory.py:
|
|
87
|
+
#: statemachine/factory.py:249
|
|
88
|
+
#, python-brace-format
|
|
89
|
+
msgid "is declared in both the '{}' and the '{}' state bodies"
|
|
90
|
+
msgstr "está declarado nos corpos dos estados '{}' e '{}'"
|
|
91
|
+
|
|
92
|
+
#: statemachine/factory.py:253
|
|
93
|
+
#, python-brace-format
|
|
94
|
+
msgid "in the '{}' state body clashes with the event '{}'"
|
|
95
|
+
msgstr "no corpo do estado '{}' conflita com o evento '{}'"
|
|
96
|
+
|
|
97
|
+
#: statemachine/factory.py:257
|
|
98
|
+
#, python-brace-format
|
|
99
|
+
msgid "is declared in both the '{}' state body and the '{}' class body"
|
|
100
|
+
msgstr "está declarado no corpo do estado '{}' e no corpo da classe '{}'"
|
|
101
|
+
|
|
102
|
+
#: statemachine/factory.py:261
|
|
103
|
+
#, python-brace-format
|
|
104
|
+
msgid "in the '{}' state body clashes with the '{}' attribute of '{}'"
|
|
105
|
+
msgstr "no corpo do estado '{}' conflita com o atributo '{}' de '{}'"
|
|
106
|
+
|
|
107
|
+
#: statemachine/factory.py:267
|
|
108
|
+
#, python-brace-format
|
|
109
|
+
msgid ""
|
|
110
|
+
"Callback '{}' {}. Nested state bodies share the class namespace. Rename "
|
|
111
|
+
"one of them."
|
|
112
|
+
msgstr ""
|
|
113
|
+
"Callback '{}' {}. Corpos de estados aninhados compartilham o namespace da"
|
|
114
|
+
" classe. Renomeie um deles."
|
|
115
|
+
|
|
116
|
+
#: statemachine/factory.py:291
|
|
76
117
|
#, python-brace-format
|
|
77
118
|
msgid ""
|
|
78
119
|
"There should be one and only one initial state. You currently have these:"
|
|
@@ -81,14 +122,14 @@ msgstr ""
|
|
|
81
122
|
"Deve haver um e apenas um estado inicial. Atualmente, você possui estes: "
|
|
82
123
|
"{!r}"
|
|
83
124
|
|
|
84
|
-
#: statemachine/factory.py:
|
|
125
|
+
#: statemachine/factory.py:306
|
|
85
126
|
#, python-brace-format
|
|
86
127
|
msgid "Cannot declare transitions from final state. Invalid state(s): {}"
|
|
87
128
|
msgstr ""
|
|
88
129
|
"Não é possível declarar transições a partir de um estado final. Estado(s)"
|
|
89
130
|
" inválido(s): {}"
|
|
90
131
|
|
|
91
|
-
#: statemachine/factory.py:
|
|
132
|
+
#: statemachine/factory.py:318
|
|
92
133
|
#, python-brace-format
|
|
93
134
|
msgid ""
|
|
94
135
|
"All non-final states should have at least one outgoing transition. These "
|
|
@@ -97,7 +138,7 @@ msgstr ""
|
|
|
97
138
|
"Todos os estados não finais devem ter pelo menos uma transição de saída. "
|
|
98
139
|
"Estes estados não possuem transição de saída: {!r}"
|
|
99
140
|
|
|
100
|
-
#: statemachine/factory.py:
|
|
141
|
+
#: statemachine/factory.py:332
|
|
101
142
|
#, python-brace-format
|
|
102
143
|
msgid ""
|
|
103
144
|
"All non-final states should have at least one path to a final state. "
|
|
@@ -107,7 +148,7 @@ msgstr ""
|
|
|
107
148
|
"estado final. Estes estados não possuem caminho para um estado final: "
|
|
108
149
|
"{!r}"
|
|
109
150
|
|
|
110
|
-
#: statemachine/factory.py:
|
|
151
|
+
#: statemachine/factory.py:345
|
|
111
152
|
#, python-brace-format
|
|
112
153
|
msgid ""
|
|
113
154
|
"There are unreachable states. The statemachine graph should have a single"
|
|
@@ -116,7 +157,7 @@ msgstr ""
|
|
|
116
157
|
"Há estados inacessíveis. O grafo da máquina de estados deve ter um único "
|
|
117
158
|
"componente. Estados desconectados: {}"
|
|
118
159
|
|
|
119
|
-
#: statemachine/factory.py:
|
|
160
|
+
#: statemachine/factory.py:382
|
|
120
161
|
#, python-brace-format
|
|
121
162
|
msgid ""
|
|
122
163
|
"Invalid entry in 'listeners': {!r}. Expected a class, callable, or "
|
|
@@ -125,7 +166,7 @@ msgstr ""
|
|
|
125
166
|
"Entrada inválida em 'listeners': {!r}. Esperado uma classe, callable ou "
|
|
126
167
|
"instância de listener."
|
|
127
168
|
|
|
128
|
-
#: statemachine/factory.py:
|
|
169
|
+
#: statemachine/factory.py:481
|
|
129
170
|
#, python-brace-format
|
|
130
171
|
msgid "An event in the '{}' has no id."
|
|
131
172
|
msgstr "Um evento em '{}' não possui id."
|
|
@@ -135,15 +176,15 @@ msgstr "Um evento em '{}' não possui id."
|
|
|
135
176
|
msgid "{!r} is not a valid state machine name."
|
|
136
177
|
msgstr "{!r} não é um nome de máquina de estados válido."
|
|
137
178
|
|
|
138
|
-
#: statemachine/state.py:
|
|
179
|
+
#: statemachine/state.py:273
|
|
139
180
|
msgid "'donedata' can only be specified on final states."
|
|
140
181
|
msgstr "'donedata' só pode ser especificado em estados finais."
|
|
141
182
|
|
|
142
|
-
#: statemachine/statemachine.py:
|
|
183
|
+
#: statemachine/statemachine.py:168
|
|
143
184
|
msgid "There are no states or transitions."
|
|
144
185
|
msgstr "Não há estados ou transições."
|
|
145
186
|
|
|
146
|
-
#: statemachine/statemachine.py:
|
|
187
|
+
#: statemachine/statemachine.py:242
|
|
147
188
|
#, python-brace-format
|
|
148
189
|
msgid "State overriding is not allowed. Trying to add '{}' to {}"
|
|
149
190
|
msgstr "Sobrescrever estados não é permitido. Tentando adicionar '{}' a {}"
|
|
@@ -162,6 +203,14 @@ msgstr ""
|
|
|
162
203
|
msgid "{} only supports the decorator syntax to register callbacks."
|
|
163
204
|
msgstr "{} suporta apenas a sintaxe de decorator para registrar callbacks."
|
|
164
205
|
|
|
206
|
+
#: statemachine/engines/async_.py:50
|
|
207
|
+
msgid ""
|
|
208
|
+
"The task running the processing loop was cancelled while it processed "
|
|
209
|
+
"this event."
|
|
210
|
+
msgstr ""
|
|
211
|
+
"A tarefa que executava o loop de processamento foi cancelada enquanto "
|
|
212
|
+
"processava este evento."
|
|
213
|
+
|
|
165
214
|
#~ msgid "There are no states."
|
|
166
215
|
#~ msgstr "Não há estados."
|
|
167
216
|
|
|
@@ -5,7 +5,7 @@ msgid ""
|
|
|
5
5
|
msgstr ""
|
|
6
6
|
"Project-Id-Version: 3.1.1\n"
|
|
7
7
|
"Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
|
|
8
|
-
"POT-Creation-Date: 2026-
|
|
8
|
+
"POT-Creation-Date: 2026-10-13 21:26-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
11
|
"Language: zh_CN\n"
|
|
@@ -16,7 +16,7 @@ msgstr ""
|
|
|
16
16
|
"Content-Transfer-Encoding: 8bit\n"
|
|
17
17
|
"Generated-By: Babel 2.18.0\n"
|
|
18
18
|
|
|
19
|
-
#: statemachine/callbacks.py:
|
|
19
|
+
#: statemachine/callbacks.py:409 statemachine/callbacks.py:414
|
|
20
20
|
#, python-brace-format
|
|
21
21
|
msgid "Did not found name '{}' from model or statemachine"
|
|
22
22
|
msgstr "在模型或状态机中未找到名称 '{}'"
|
|
@@ -27,24 +27,34 @@ msgid ""
|
|
|
27
27
|
" state? (e.g., `await sm.activate_initial_state()`)"
|
|
28
28
|
msgstr "没有设置当前状态。在异步代码中,您是否激活了初始状态?(例如,`await sm.activate_initial_state()`)"
|
|
29
29
|
|
|
30
|
-
#: statemachine/dispatcher.py:
|
|
30
|
+
#: statemachine/dispatcher.py:127
|
|
31
31
|
#, python-brace-format
|
|
32
32
|
msgid "Failed to parse boolean expression '{}'"
|
|
33
33
|
msgstr "无法解析布尔表达式 '{}'"
|
|
34
34
|
|
|
35
|
-
#: statemachine/event.py:
|
|
35
|
+
#: statemachine/event.py:56
|
|
36
|
+
#, python-brace-format
|
|
37
|
+
msgid ""
|
|
38
|
+
"Event {id!r} cannot be both delayed and internal: the internal queue runs"
|
|
39
|
+
" within the current macrostep, which takes no time. Drop 'internal' to "
|
|
40
|
+
"deliver it later on the external queue, or drop 'delay' to run it now."
|
|
41
|
+
msgstr ""
|
|
42
|
+
"事件 {id!r} 不能同时是延迟的和内部的:内部队列在当前宏步骤内运行,不占用时间。去掉 'internal' 以便稍后通过外部队列投递,或去掉"
|
|
43
|
+
" 'delay' 以立即执行。"
|
|
44
|
+
|
|
45
|
+
#: statemachine/event.py:108
|
|
36
46
|
#, python-brace-format
|
|
37
47
|
msgid ""
|
|
38
48
|
"Event() received a non-string 'id' ({cls_name}). To combine multiple "
|
|
39
49
|
"transitions under one event, use the | operator: t1 | t2."
|
|
40
50
|
msgstr "Event() 接收到的 'id' 不是字符串 ({cls_name})。要将多个转换组合在同一个事件下,请使用 | 运算符:t1 | t2。"
|
|
41
51
|
|
|
42
|
-
#: statemachine/event.py:
|
|
52
|
+
#: statemachine/event.py:147
|
|
43
53
|
#, python-brace-format
|
|
44
54
|
msgid "Cannot add callback '{}' to an event with no transitions."
|
|
45
55
|
msgstr "无法将回调 '{}' 添加到没有转换的事件。"
|
|
46
56
|
|
|
47
|
-
#: statemachine/event.py:
|
|
57
|
+
#: statemachine/event.py:182
|
|
48
58
|
#, python-brace-format
|
|
49
59
|
msgid "Event {} cannot be called without a SM instance"
|
|
50
60
|
msgstr "事件 {} 不能在没有 SM 实例的情况下调用"
|
|
@@ -59,54 +69,81 @@ msgstr "{!r} 不是有效的状态值。"
|
|
|
59
69
|
msgid "Can't {} when in {}."
|
|
60
70
|
msgstr "在 {} 时无法 {}。"
|
|
61
71
|
|
|
62
|
-
#: statemachine/factory.py:
|
|
72
|
+
#: statemachine/factory.py:91
|
|
63
73
|
#, python-brace-format
|
|
64
74
|
msgid ""
|
|
65
75
|
"There should be one and only one initial state. Your currently have "
|
|
66
76
|
"these: {0}"
|
|
67
77
|
msgstr "应有且仅有一个初始状态。当前您有这些:{0}"
|
|
68
78
|
|
|
69
|
-
#: statemachine/factory.py:
|
|
79
|
+
#: statemachine/factory.py:249
|
|
80
|
+
#, python-brace-format
|
|
81
|
+
msgid "is declared in both the '{}' and the '{}' state bodies"
|
|
82
|
+
msgstr "同时声明在 '{}' 和 '{}' 状态体中"
|
|
83
|
+
|
|
84
|
+
#: statemachine/factory.py:253
|
|
85
|
+
#, python-brace-format
|
|
86
|
+
msgid "in the '{}' state body clashes with the event '{}'"
|
|
87
|
+
msgstr "在 '{}' 状态体中与事件 '{}' 冲突"
|
|
88
|
+
|
|
89
|
+
#: statemachine/factory.py:257
|
|
90
|
+
#, python-brace-format
|
|
91
|
+
msgid "is declared in both the '{}' state body and the '{}' class body"
|
|
92
|
+
msgstr "同时声明在 '{}' 状态体和 '{}' 类体中"
|
|
93
|
+
|
|
94
|
+
#: statemachine/factory.py:261
|
|
95
|
+
#, python-brace-format
|
|
96
|
+
msgid "in the '{}' state body clashes with the '{}' attribute of '{}'"
|
|
97
|
+
msgstr "在 '{0}' 状态体中与 '{2}' 的属性 '{1}' 冲突"
|
|
98
|
+
|
|
99
|
+
#: statemachine/factory.py:267
|
|
100
|
+
#, python-brace-format
|
|
101
|
+
msgid ""
|
|
102
|
+
"Callback '{}' {}. Nested state bodies share the class namespace. Rename "
|
|
103
|
+
"one of them."
|
|
104
|
+
msgstr "回调 '{}' {}。嵌套状态体共享类的命名空间。请重命名其中一个。"
|
|
105
|
+
|
|
106
|
+
#: statemachine/factory.py:291
|
|
70
107
|
#, python-brace-format
|
|
71
108
|
msgid ""
|
|
72
109
|
"There should be one and only one initial state. You currently have these:"
|
|
73
110
|
" {!r}"
|
|
74
111
|
msgstr "应有且仅有一个初始状态。当前您有这些:{!r}"
|
|
75
112
|
|
|
76
|
-
#: statemachine/factory.py:
|
|
113
|
+
#: statemachine/factory.py:306
|
|
77
114
|
#, python-brace-format
|
|
78
115
|
msgid "Cannot declare transitions from final state. Invalid state(s): {}"
|
|
79
116
|
msgstr "无法从终止状态声明转换。无效状态:{}"
|
|
80
117
|
|
|
81
|
-
#: statemachine/factory.py:
|
|
118
|
+
#: statemachine/factory.py:318
|
|
82
119
|
#, python-brace-format
|
|
83
120
|
msgid ""
|
|
84
121
|
"All non-final states should have at least one outgoing transition. These "
|
|
85
122
|
"states have no outgoing transition: {!r}"
|
|
86
123
|
msgstr "所有非终止状态都应至少有一个外部转换。这些状态没有外部转换:{!r}"
|
|
87
124
|
|
|
88
|
-
#: statemachine/factory.py:
|
|
125
|
+
#: statemachine/factory.py:332
|
|
89
126
|
#, python-brace-format
|
|
90
127
|
msgid ""
|
|
91
128
|
"All non-final states should have at least one path to a final state. "
|
|
92
129
|
"These states have no path to a final state: {!r}"
|
|
93
130
|
msgstr "所有非终止状态应至少有一个到终止状态的路径。这些状态没有到终止状态的路径:{!r}"
|
|
94
131
|
|
|
95
|
-
#: statemachine/factory.py:
|
|
132
|
+
#: statemachine/factory.py:345
|
|
96
133
|
#, python-brace-format
|
|
97
134
|
msgid ""
|
|
98
135
|
"There are unreachable states. The statemachine graph should have a single"
|
|
99
136
|
" component. Disconnected states: {}"
|
|
100
137
|
msgstr "存在不可到达的状态。状态机图应具有单个组件。断开的状态:{}"
|
|
101
138
|
|
|
102
|
-
#: statemachine/factory.py:
|
|
139
|
+
#: statemachine/factory.py:382
|
|
103
140
|
#, python-brace-format
|
|
104
141
|
msgid ""
|
|
105
142
|
"Invalid entry in 'listeners': {!r}. Expected a class, callable, or "
|
|
106
143
|
"listener instance."
|
|
107
144
|
msgstr "'listeners' 中的条目无效:{!r}。期望为类、可调用对象或监听器实例。"
|
|
108
145
|
|
|
109
|
-
#: statemachine/factory.py:
|
|
146
|
+
#: statemachine/factory.py:481
|
|
110
147
|
#, python-brace-format
|
|
111
148
|
msgid "An event in the '{}' has no id."
|
|
112
149
|
msgstr "'{}' 中的事件没有 ID。"
|
|
@@ -116,15 +153,15 @@ msgstr "'{}' 中的事件没有 ID。"
|
|
|
116
153
|
msgid "{!r} is not a valid state machine name."
|
|
117
154
|
msgstr "{!r} 不是有效的状态机名称。"
|
|
118
155
|
|
|
119
|
-
#: statemachine/state.py:
|
|
156
|
+
#: statemachine/state.py:273
|
|
120
157
|
msgid "'donedata' can only be specified on final states."
|
|
121
158
|
msgstr "'donedata' 只能在终止状态上指定。"
|
|
122
159
|
|
|
123
|
-
#: statemachine/statemachine.py:
|
|
160
|
+
#: statemachine/statemachine.py:168
|
|
124
161
|
msgid "There are no states or transitions."
|
|
125
162
|
msgstr "没有状态或转换。"
|
|
126
163
|
|
|
127
|
-
#: statemachine/statemachine.py:
|
|
164
|
+
#: statemachine/statemachine.py:242
|
|
128
165
|
#, python-brace-format
|
|
129
166
|
msgid "State overriding is not allowed. Trying to add '{}' to {}"
|
|
130
167
|
msgstr "不允许覆盖状态。尝试将 '{}' 添加到 {}"
|
|
@@ -141,6 +178,12 @@ msgstr "从源 {source!r} 的内部转换无效,目标 {target!r} 应为自身
|
|
|
141
178
|
msgid "{} only supports the decorator syntax to register callbacks."
|
|
142
179
|
msgstr "{} 仅支持使用装饰器语法注册回调。"
|
|
143
180
|
|
|
181
|
+
#: statemachine/engines/async_.py:50
|
|
182
|
+
msgid ""
|
|
183
|
+
"The task running the processing loop was cancelled while it processed "
|
|
184
|
+
"this event."
|
|
185
|
+
msgstr "运行处理循环的任务在处理此事件时被取消。"
|
|
186
|
+
|
|
144
187
|
#~ msgid "There are no states."
|
|
145
188
|
#~ msgstr "没有状态。"
|
|
146
189
|
|
statemachine/registry.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
1
3
|
from .utils import qualname
|
|
2
4
|
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .statemachine import StateMachine
|
|
7
|
+
|
|
3
8
|
try:
|
|
4
9
|
from django.utils.module_loading import autodiscover_modules
|
|
5
10
|
except ImportError: # pragma: no cover
|
|
@@ -8,7 +13,7 @@ except ImportError: # pragma: no cover
|
|
|
8
13
|
pass
|
|
9
14
|
|
|
10
15
|
|
|
11
|
-
_REGISTRY = {}
|
|
16
|
+
_REGISTRY: "dict[str, type[StateMachine]]" = {}
|
|
12
17
|
_initialized = False
|
|
13
18
|
|
|
14
19
|
|
statemachine/spec_parser.py
CHANGED
|
@@ -204,7 +204,9 @@ def build_unaryop(op_fn, operand: Callable) -> Callable:
|
|
|
204
204
|
|
|
205
205
|
def build_collection(factory, item_exprs: "list[Callable]") -> Callable:
|
|
206
206
|
def decorated(*args, **kwargs):
|
|
207
|
-
|
|
207
|
+
items = [item(*args, **kwargs) for item in item_exprs]
|
|
208
|
+
_check_value_size(items, "collection")
|
|
209
|
+
return factory(items)
|
|
208
210
|
|
|
209
211
|
decorated.__name__ = f"{factory.__name__}(...)"
|
|
210
212
|
return decorated
|
|
@@ -212,10 +214,12 @@ def build_collection(factory, item_exprs: "list[Callable]") -> Callable:
|
|
|
212
214
|
|
|
213
215
|
def build_dict(key_exprs: "list[Callable]", value_exprs: "list[Callable]") -> Callable:
|
|
214
216
|
def decorated(*args, **kwargs):
|
|
215
|
-
|
|
216
|
-
key(*args, **kwargs)
|
|
217
|
+
items = [
|
|
218
|
+
(key(*args, **kwargs), value(*args, **kwargs))
|
|
217
219
|
for key, value in zip(key_exprs, value_exprs, strict=True)
|
|
218
|
-
|
|
220
|
+
]
|
|
221
|
+
_check_value_size(items, "dict")
|
|
222
|
+
return dict(items)
|
|
219
223
|
|
|
220
224
|
decorated.__name__ = "dict(...)"
|
|
221
225
|
return decorated
|
|
@@ -289,7 +293,7 @@ def build_expression( # noqa: C901
|
|
|
289
293
|
op_type = type(node.op)
|
|
290
294
|
if op_type not in binary_operators:
|
|
291
295
|
# e.g. bitwise ``^``/``|``/``<<`` are outside the allowlist. (``**`` and ``*``
|
|
292
|
-
# are allowed but magnitude-capped
|
|
296
|
+
# are allowed but magnitude-capped, see ``binary_operators``.)
|
|
293
297
|
raise ValueError(f"Binary operator '{op_type.__name__}' is not allowed")
|
|
294
298
|
return build_binop(binary_operators[op_type], recurse(node.left), recurse(node.right))
|
|
295
299
|
case ast.List(elts=elts) if allow_value_nodes:
|
|
@@ -339,6 +343,15 @@ def parse_boolean_expr(expr, variable_hook, operator_mapping):
|
|
|
339
343
|
return build_expression(tree.body, variable_hook, operator_mapping)
|
|
340
344
|
|
|
341
345
|
|
|
346
|
+
def names_in_boolean_expr(expr: str) -> "set[str]":
|
|
347
|
+
"""Variable names that :func:`parse_boolean_expr` would resolve in ``expr``."""
|
|
348
|
+
try:
|
|
349
|
+
tree = ast.parse(replace_operators(expr), mode="eval")
|
|
350
|
+
except SyntaxError:
|
|
351
|
+
return set()
|
|
352
|
+
return {node.id for node in ast.walk(tree) if isinstance(node, ast.Name)}
|
|
353
|
+
|
|
354
|
+
|
|
342
355
|
def parse_expr(expr: str, variable_hook: Callable) -> Callable:
|
|
343
356
|
"""Parse a value expression into a callable using the restricted AST allowlist.
|
|
344
357
|
|
|
@@ -374,11 +387,51 @@ operator_mapping = {
|
|
|
374
387
|
ast.NotEq: build_custom_operator(operator.ne),
|
|
375
388
|
}
|
|
376
389
|
|
|
377
|
-
# Result-size caps for the
|
|
378
|
-
#
|
|
379
|
-
#
|
|
390
|
+
# Result-size caps for the values an expression can build cheaply. Ordinary arithmetic and
|
|
391
|
+
# collections are well under these; the caps only reject the denial-of-service forms (giant
|
|
392
|
+
# bignums, giant or deeply shared sequences and collections).
|
|
380
393
|
_MAX_POW_RESULT_BITS = 4096
|
|
381
|
-
|
|
394
|
+
_MAX_VALUE_SIZE = 1_000_000
|
|
395
|
+
|
|
396
|
+
_TEXT_TYPES = (str, bytes, bytearray)
|
|
397
|
+
_SEQUENCE_TYPES = (*_TEXT_TYPES, list, tuple)
|
|
398
|
+
_CONTAINER_TYPES = (list, tuple, set, frozenset)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _value_size(value) -> int:
|
|
402
|
+
"""Size of ``value`` counting a shared reference every time it appears.
|
|
403
|
+
|
|
404
|
+
``[[0] * 1000] * 1000`` holds a thousand references to one list, so it is cheap to build,
|
|
405
|
+
but comparing, hashing or printing it walks a million elements. That walked size is what
|
|
406
|
+
is measured here, in elements, characters or bytes, memoized by identity so the
|
|
407
|
+
measurement itself stays cheap.
|
|
408
|
+
"""
|
|
409
|
+
memo: "dict[int, int]" = {}
|
|
410
|
+
|
|
411
|
+
def size(item) -> int:
|
|
412
|
+
key = id(item)
|
|
413
|
+
if key in memo:
|
|
414
|
+
return memo[key]
|
|
415
|
+
memo[key] = 1
|
|
416
|
+
if isinstance(item, _TEXT_TYPES):
|
|
417
|
+
total = len(item)
|
|
418
|
+
elif isinstance(item, int):
|
|
419
|
+
total = item.bit_length() // 8
|
|
420
|
+
elif isinstance(item, dict):
|
|
421
|
+
total = sum(size(k) + size(v) for k, v in item.items())
|
|
422
|
+
elif isinstance(item, _CONTAINER_TYPES):
|
|
423
|
+
total = sum(size(element) for element in item)
|
|
424
|
+
else:
|
|
425
|
+
total = 1
|
|
426
|
+
memo[key] = max(total, 1)
|
|
427
|
+
return memo[key]
|
|
428
|
+
|
|
429
|
+
return size(value)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _check_value_size(value, what: str):
|
|
433
|
+
if _value_size(value) > _MAX_VALUE_SIZE:
|
|
434
|
+
raise ValueError(f"{what} is too large for the restricted evaluator")
|
|
382
435
|
|
|
383
436
|
|
|
384
437
|
def _guarded_pow(base, exp):
|
|
@@ -396,29 +449,52 @@ def _guarded_pow(base, exp):
|
|
|
396
449
|
|
|
397
450
|
|
|
398
451
|
def _guarded_mul(a, b):
|
|
399
|
-
"""``*`` with a
|
|
452
|
+
"""``*`` with a result-size cap (GHSA-r8gj-366q-cgvj).
|
|
400
453
|
|
|
401
454
|
``seq * n`` (list/str/bytes/tuple times an int) can allocate an enormous object from a
|
|
402
|
-
12-character expression (``[0]*20000000``)
|
|
403
|
-
|
|
455
|
+
12-character expression (``[0]*20000000``), and repeatedly squaring an ``int`` doubles its
|
|
456
|
+
size every time. The result size is checked *before* allocating. Other operands (floats)
|
|
457
|
+
are passed straight through.
|
|
404
458
|
"""
|
|
459
|
+
if isinstance(a, int) and isinstance(b, int):
|
|
460
|
+
if a.bit_length() + b.bit_length() > _MAX_POW_RESULT_BITS:
|
|
461
|
+
raise ValueError("'*' result is too large for the restricted evaluator")
|
|
405
462
|
for seq, n in ((a, b), (b, a)):
|
|
406
|
-
if isinstance(seq,
|
|
407
|
-
if n > 0 and n *
|
|
463
|
+
if isinstance(seq, _SEQUENCE_TYPES) and isinstance(n, int):
|
|
464
|
+
if n > 0 and n * _value_size(seq) > _MAX_VALUE_SIZE:
|
|
408
465
|
raise ValueError("'*' repetition is too large for the restricted evaluator")
|
|
409
466
|
return operator.mul(a, b)
|
|
410
467
|
|
|
411
468
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
469
|
+
def _guarded_add(a, b):
|
|
470
|
+
"""``+`` with a sequence-length cap: repeated ``x + x`` doubles a sequence every time."""
|
|
471
|
+
sizes = [_value_size(x) for x in (a, b) if isinstance(x, _SEQUENCE_TYPES)]
|
|
472
|
+
if len(sizes) == 2 and sum(sizes) > _MAX_VALUE_SIZE:
|
|
473
|
+
raise ValueError("'+' result is too large for the restricted evaluator")
|
|
474
|
+
return operator.add(a, b)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _guarded_mod(a, b):
|
|
478
|
+
"""``%`` restricted to numbers.
|
|
479
|
+
|
|
480
|
+
printf-style formatting (``'%2000000000d' % 0``) allocates whatever field width the
|
|
481
|
+
expression asks for, so it is rejected on ``str``/``bytes`` operands.
|
|
482
|
+
"""
|
|
483
|
+
if isinstance(a, _TEXT_TYPES):
|
|
484
|
+
raise ValueError("'%' string formatting is not allowed by the restricted evaluator")
|
|
485
|
+
return operator.mod(a, b)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# ``**``, ``*``, ``+`` and ``%`` stay available for ordinary arithmetic but are wrapped so a
|
|
489
|
+
# tiny untrusted expression cannot exhaust CPU/memory (GHSA-r8gj-366q-cgvj). ``trusted=True``
|
|
490
|
+
# uses the full Python evaluator instead, without these caps.
|
|
415
491
|
binary_operators = {
|
|
416
|
-
ast.Add:
|
|
492
|
+
ast.Add: _guarded_add,
|
|
417
493
|
ast.Sub: operator.sub,
|
|
418
494
|
ast.Mult: _guarded_mul,
|
|
419
495
|
ast.Div: operator.truediv,
|
|
420
496
|
ast.FloorDiv: operator.floordiv,
|
|
421
|
-
ast.Mod:
|
|
497
|
+
ast.Mod: _guarded_mod,
|
|
422
498
|
ast.Pow: _guarded_pow,
|
|
423
499
|
}
|
|
424
500
|
|
statemachine/state.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
|
+
from collections.abc import Mapping
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
from typing import Any
|
|
@@ -8,6 +9,7 @@ from weakref import ref
|
|
|
8
9
|
from .callbacks import CallbackGroup
|
|
9
10
|
from .callbacks import CallbackPriority
|
|
10
11
|
from .callbacks import CallbackSpecList
|
|
12
|
+
from .event import Event
|
|
11
13
|
from .event import _expand_event_id
|
|
12
14
|
from .exceptions import InvalidDefinition
|
|
13
15
|
from .i18n import _
|
|
@@ -56,7 +58,7 @@ class _FromState(_TransitionBuilder):
|
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
class NestedStateFactory(type):
|
|
59
|
-
def __new__( # type: ignore [misc]
|
|
61
|
+
def __new__( # type: ignore [misc] # noqa: C901
|
|
60
62
|
cls, classname, bases, attrs, name="", **kwargs
|
|
61
63
|
) -> "State":
|
|
62
64
|
if not bases:
|
|
@@ -76,6 +78,8 @@ class NestedStateFactory(type):
|
|
|
76
78
|
states = []
|
|
77
79
|
history = []
|
|
78
80
|
callbacks = {}
|
|
81
|
+
# Order is significant: a ``HistoryState`` is a ``State``, and an ``Event`` is a
|
|
82
|
+
# callable ``str``, so both would be captured by a later branch.
|
|
79
83
|
for key, value in attrs.items():
|
|
80
84
|
if isinstance(value, States):
|
|
81
85
|
for state_id, state in value.items():
|
|
@@ -88,7 +92,27 @@ class NestedStateFactory(type):
|
|
|
88
92
|
value._set_id(key)
|
|
89
93
|
states.append(value)
|
|
90
94
|
elif isinstance(value, TransitionList):
|
|
91
|
-
|
|
95
|
+
for event_id in _expand_event_id(key):
|
|
96
|
+
value.add_event(event_id)
|
|
97
|
+
elif isinstance(value, Event):
|
|
98
|
+
event_ids = [value.id] if value._has_real_id else _expand_event_id(key)
|
|
99
|
+
events = [
|
|
100
|
+
Event(
|
|
101
|
+
id=event_id,
|
|
102
|
+
name=value.name,
|
|
103
|
+
delay=value.delay,
|
|
104
|
+
internal=value.internal,
|
|
105
|
+
)
|
|
106
|
+
for event_id in event_ids
|
|
107
|
+
]
|
|
108
|
+
if value._transitions is not None:
|
|
109
|
+
for event in events:
|
|
110
|
+
value._transitions.add_event(event)
|
|
111
|
+
elif getattr(value, "attr_name", None):
|
|
112
|
+
if value.is_event:
|
|
113
|
+
for event_id in _expand_event_id(key):
|
|
114
|
+
value._transitions.add_event(event_id)
|
|
115
|
+
callbacks[value.attr_name] = value
|
|
92
116
|
elif callable(value):
|
|
93
117
|
callbacks[key] = value
|
|
94
118
|
|
|
@@ -366,14 +390,18 @@ class InstanceState(State):
|
|
|
366
390
|
self,
|
|
367
391
|
state: State,
|
|
368
392
|
machine: "StateChart",
|
|
393
|
+
instance_states: "Mapping[Any, State]",
|
|
369
394
|
):
|
|
370
395
|
self._state = state
|
|
371
396
|
self._machine = ref(machine)
|
|
397
|
+
self._instance_states = instance_states
|
|
372
398
|
self._hash = hash(state)
|
|
373
|
-
|
|
399
|
+
# No _init_states(): it would rebind the shared children's ``parent`` to this proxy.
|
|
374
400
|
|
|
375
401
|
def __getattr__(self, name: str):
|
|
376
402
|
value = getattr(self._state, name)
|
|
403
|
+
if isinstance(value, State):
|
|
404
|
+
value = self._instance_states.get(value.value, value)
|
|
377
405
|
self.__dict__[name] = value
|
|
378
406
|
return value
|
|
379
407
|
|
|
@@ -416,13 +444,24 @@ class AnyState(State):
|
|
|
416
444
|
until the state machine class is evaluated.
|
|
417
445
|
"""
|
|
418
446
|
|
|
447
|
+
def __init__(self, *args, **kwargs):
|
|
448
|
+
super().__init__(*args, **kwargs)
|
|
449
|
+
self._copies: dict[tuple[int, int], Transition] = {}
|
|
450
|
+
"""Transition copied onto each state, keyed by ``(id(transition), id(state))``, so an
|
|
451
|
+
attribute that declares several ids adds them all to one copy."""
|
|
452
|
+
|
|
419
453
|
def _on_event_defined(self, event: str, transition: Transition, states: list[State]):
|
|
420
454
|
for state in states:
|
|
421
455
|
if state.final:
|
|
422
456
|
continue
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
457
|
+
key = (id(transition), id(state))
|
|
458
|
+
copy = self._copies.get(key)
|
|
459
|
+
if copy is None:
|
|
460
|
+
copy = transition._copy_with_args(source=state, event=event)
|
|
461
|
+
self._copies[key] = copy
|
|
462
|
+
state.transitions.add_transitions(copy)
|
|
463
|
+
else:
|
|
464
|
+
copy.add_event(event)
|
|
426
465
|
|
|
427
466
|
|
|
428
467
|
class HistoryType(str, Enum):
|