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.
@@ -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-06-16 23:00-0300\n"
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:398 statemachine/callbacks.py:403
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:123
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:94
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:130
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:163
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:77
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:194
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:209
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:221
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:235
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:248
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:285
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:384
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:249
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:167
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:237
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-06-16 23:00-0300\n"
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:398 statemachine/callbacks.py:403
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:123
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:94
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:130
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:163
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:77
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:194
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:209
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:221
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:235
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:248
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:285
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:384
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:249
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:167
160
+ #: statemachine/statemachine.py:168
124
161
  msgid "There are no states or transitions."
125
162
  msgstr "没有状态或转换。"
126
163
 
127
- #: statemachine/statemachine.py:237
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
 
@@ -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
- return factory(item(*args, **kwargs) for item in item_exprs)
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
- return {
216
- key(*args, **kwargs): value(*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 — see ``binary_operators``.)
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 two operators that can blow up cheaply. Ordinary scalar
378
- # arithmetic (``x * 2``, ``x ** 2``) is well under these; the caps only reject the
379
- # denial-of-service forms (``9**9**9`` bignum, ``[0]*20000000`` sequence replication).
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
- _MAX_SEQUENCE_LEN = 1_000_000
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 sequence-replication cap (GHSA-r8gj-366q-cgvj).
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``). The resulting length is checked *before*
403
- allocating. Scalar numeric multiplication is unaffected.
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, (str, bytes, bytearray, list, tuple)) and isinstance(n, int):
407
- if n > 0 and n * len(seq) > _MAX_SEQUENCE_LEN:
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
- # ``**`` and ``*`` stay available for ordinary arithmetic but are wrapped so a tiny untrusted
413
- # expression cannot exhaust CPU/memory (GHSA-r8gj-366q-cgvj). ``trusted=True`` uses the full
414
- # Python evaluator instead, without these caps.
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: operator.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: operator.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
- value.add_event(_expand_event_id(key))
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
- self._init_states()
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
- new_transition = transition._copy_with_args(source=state, event=event)
424
-
425
- state.transitions.add_transitions(new_transition)
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):