mycorrhizal 0.1.2__py3-none-any.whl → 0.2.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.
- mycorrhizal/_version.py +1 -1
- mycorrhizal/common/__init__.py +15 -3
- mycorrhizal/common/cache.py +114 -0
- mycorrhizal/common/compilation.py +263 -0
- mycorrhizal/common/interface_detection.py +159 -0
- mycorrhizal/common/interfaces.py +3 -50
- mycorrhizal/common/mermaid.py +124 -0
- mycorrhizal/common/wrappers.py +1 -1
- mycorrhizal/hypha/core/builder.py +11 -1
- mycorrhizal/hypha/core/runtime.py +242 -107
- mycorrhizal/mycelium/__init__.py +174 -0
- mycorrhizal/mycelium/core.py +619 -0
- mycorrhizal/mycelium/exceptions.py +30 -0
- mycorrhizal/mycelium/hypha_bridge.py +1143 -0
- mycorrhizal/mycelium/instance.py +440 -0
- mycorrhizal/mycelium/pn_context.py +276 -0
- mycorrhizal/mycelium/runner.py +165 -0
- mycorrhizal/mycelium/spores_integration.py +655 -0
- mycorrhizal/mycelium/tree_builder.py +102 -0
- mycorrhizal/mycelium/tree_spec.py +197 -0
- mycorrhizal/rhizomorph/README.md +82 -33
- mycorrhizal/rhizomorph/core.py +287 -119
- mycorrhizal/septum/TRANSITION_REFERENCE.md +385 -0
- mycorrhizal/{enoki → septum}/core.py +326 -100
- mycorrhizal/{enoki → septum}/testing_utils.py +7 -7
- mycorrhizal/{enoki → septum}/util.py +44 -21
- mycorrhizal/spores/__init__.py +3 -3
- mycorrhizal/spores/core.py +149 -28
- mycorrhizal/spores/dsl/__init__.py +8 -8
- mycorrhizal/spores/dsl/hypha.py +3 -15
- mycorrhizal/spores/dsl/rhizomorph.py +3 -11
- mycorrhizal/spores/dsl/{enoki.py → septum.py} +26 -77
- mycorrhizal/spores/encoder/json.py +21 -12
- mycorrhizal/spores/extraction.py +14 -11
- mycorrhizal/spores/models.py +53 -20
- mycorrhizal-0.2.0.dist-info/METADATA +335 -0
- mycorrhizal-0.2.0.dist-info/RECORD +54 -0
- mycorrhizal-0.1.2.dist-info/METADATA +0 -198
- mycorrhizal-0.1.2.dist-info/RECORD +0 -39
- /mycorrhizal/{enoki → septum}/__init__.py +0 -0
- {mycorrhizal-0.1.2.dist-info → mycorrhizal-0.2.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tree instance - runtime instance of a Mycelium tree.
|
|
4
|
+
|
|
5
|
+
This module provides the TreeInstance class which manages the runtime
|
|
6
|
+
behavior of a tree, including FSM instances for FSM-integrated actions
|
|
7
|
+
and execution of BT nodes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from ..septum.core import StateMachine, StateSpec
|
|
16
|
+
from ..rhizomorph.core import Status, Runner as BTRunner
|
|
17
|
+
from .tree_spec import MyceliumTreeSpec, FSMIntegration
|
|
18
|
+
from .exceptions import FSMInstantiationError
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FSMRunner:
|
|
25
|
+
"""
|
|
26
|
+
Wrapper around an FSM instance for use in FSM-integrated actions.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
fsm: The StateMachine instance
|
|
30
|
+
name: Action name this FSM is associated with
|
|
31
|
+
_initialized: Whether the FSM has been initialized
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, name: str, fsm: StateMachine):
|
|
35
|
+
self.name = name
|
|
36
|
+
self.fsm = fsm
|
|
37
|
+
self._initialized = False
|
|
38
|
+
|
|
39
|
+
async def _ensure_initialized(self) -> None:
|
|
40
|
+
"""Ensure the FSM is initialized (lazy initialization)."""
|
|
41
|
+
if not self._initialized:
|
|
42
|
+
await self.fsm.initialize()
|
|
43
|
+
self._initialized = True
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def current_state(self) -> Optional[StateSpec]:
|
|
47
|
+
"""Get the current state of the FSM."""
|
|
48
|
+
return self.fsm.current_state
|
|
49
|
+
|
|
50
|
+
def send_message(self, message: Any) -> None:
|
|
51
|
+
"""Send a message to the FSM."""
|
|
52
|
+
self.fsm.send_message(message)
|
|
53
|
+
|
|
54
|
+
async def tick(self, timeout: Optional[float] = None) -> Any:
|
|
55
|
+
"""Tick the FSM once."""
|
|
56
|
+
await self._ensure_initialized()
|
|
57
|
+
return await self.fsm.tick(timeout=timeout)
|
|
58
|
+
|
|
59
|
+
async def run(self, timeout: Optional[float] = None) -> Any:
|
|
60
|
+
"""Run the FSM to completion."""
|
|
61
|
+
await self._ensure_initialized()
|
|
62
|
+
return await self.fsm.run(timeout=timeout)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class FSMAccessor:
|
|
66
|
+
"""
|
|
67
|
+
Provides convenient access to FSM runners.
|
|
68
|
+
|
|
69
|
+
Allows both attribute-style and dict-style access:
|
|
70
|
+
accessor.robot
|
|
71
|
+
accessor['robot']
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, fsm_runners: Dict[str, "FSMRunner"]):
|
|
75
|
+
self._fsm_runners = fsm_runners
|
|
76
|
+
|
|
77
|
+
def __getattr__(self, name: str) -> "FSMRunner":
|
|
78
|
+
if name in self._fsm_runners:
|
|
79
|
+
return self._fsm_runners[name]
|
|
80
|
+
raise AttributeError(f"No FSM named '{name}'")
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, name: str) -> "FSMRunner":
|
|
83
|
+
if name not in self._fsm_runners:
|
|
84
|
+
raise KeyError(f"No FSM named '{name}'")
|
|
85
|
+
return self._fsm_runners[name]
|
|
86
|
+
|
|
87
|
+
def __contains__(self, name: str) -> bool:
|
|
88
|
+
return name in self._fsm_runners
|
|
89
|
+
|
|
90
|
+
def __iter__(self):
|
|
91
|
+
return iter(self._fsm_runners)
|
|
92
|
+
|
|
93
|
+
def __len__(self) -> int:
|
|
94
|
+
return len(self._fsm_runners)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TreeInstance:
|
|
98
|
+
"""
|
|
99
|
+
Runtime instance of a Mycelium tree.
|
|
100
|
+
|
|
101
|
+
Manages FSM instances for FSM-integrated actions, executes BT nodes,
|
|
102
|
+
and coordinates execution.
|
|
103
|
+
|
|
104
|
+
Attributes:
|
|
105
|
+
spec: The tree specification
|
|
106
|
+
bb: The blackboard (shared state)
|
|
107
|
+
tb: The timebase (shared timing)
|
|
108
|
+
bt_runner: The underlying BT runner
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
spec: MyceliumTreeSpec,
|
|
114
|
+
bb: BaseModel,
|
|
115
|
+
tb: Any,
|
|
116
|
+
):
|
|
117
|
+
self.spec = spec
|
|
118
|
+
self.bb = bb
|
|
119
|
+
self.tb = tb
|
|
120
|
+
self._fsm_runners: Dict[str, FSMRunner] = {}
|
|
121
|
+
self.bt_runner: Optional[BTRunner] = None
|
|
122
|
+
|
|
123
|
+
# Instantiate FSMs for FSM-integrated actions
|
|
124
|
+
self._instantiate_fsms()
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def fsms(self) -> Any:
|
|
128
|
+
"""
|
|
129
|
+
Access FSM instances.
|
|
130
|
+
|
|
131
|
+
Provides attribute and dict-style access:
|
|
132
|
+
tree_instance.fsms.robot
|
|
133
|
+
tree_instance.fsms['robot']
|
|
134
|
+
"""
|
|
135
|
+
return FSMAccessor(self._fsm_runners)
|
|
136
|
+
|
|
137
|
+
def _instantiate_fsms(self) -> None:
|
|
138
|
+
"""Instantiate FSMs for all FSM-integrated actions."""
|
|
139
|
+
for action_name, fsm_integration in self.spec.fsm_integrations.items():
|
|
140
|
+
try:
|
|
141
|
+
# Get the initial state spec
|
|
142
|
+
initial = fsm_integration.initial_state
|
|
143
|
+
|
|
144
|
+
# Handle callable states
|
|
145
|
+
if callable(initial) and not isinstance(initial, type):
|
|
146
|
+
initial = initial()
|
|
147
|
+
|
|
148
|
+
# Create common data from blackboard
|
|
149
|
+
common_data = self.bb.model_dump() if hasattr(self.bb, 'model_dump') else dict(self.bb)
|
|
150
|
+
|
|
151
|
+
# Create FSM instance
|
|
152
|
+
fsm = StateMachine(initial_state=initial, common_data=common_data) # type: ignore[arg-type]
|
|
153
|
+
|
|
154
|
+
# Store FSM runner (will be initialized lazily)
|
|
155
|
+
runner = FSMRunner(
|
|
156
|
+
name=action_name,
|
|
157
|
+
fsm=fsm,
|
|
158
|
+
)
|
|
159
|
+
self._fsm_runners[action_name] = runner
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise FSMInstantiationError(
|
|
163
|
+
f"Failed to instantiate FSM for action '{action_name}': {e}"
|
|
164
|
+
) from e
|
|
165
|
+
|
|
166
|
+
def get_fsm_runner(self, action_name: str) -> Optional[FSMRunner]:
|
|
167
|
+
"""Get FSM runner for an action."""
|
|
168
|
+
return self._fsm_runners.get(action_name)
|
|
169
|
+
|
|
170
|
+
async def tick(self) -> Status:
|
|
171
|
+
"""
|
|
172
|
+
Execute one tick of the tree.
|
|
173
|
+
|
|
174
|
+
This runs the BT tree. For FSM-integrated actions, the tree instance
|
|
175
|
+
is injected onto the blackboard so actions can access their FSM runners.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Status from the BT tick
|
|
179
|
+
"""
|
|
180
|
+
# Get BT namespace
|
|
181
|
+
tree_namespace = self.spec.bt_namespace
|
|
182
|
+
|
|
183
|
+
# Create BT runner if needed
|
|
184
|
+
if self.bt_runner is None:
|
|
185
|
+
from ..rhizomorph.core import Runner as BTRunner
|
|
186
|
+
if tree_namespace is None:
|
|
187
|
+
raise RuntimeError("BT namespace is None, cannot create BT runner")
|
|
188
|
+
self.bt_runner = BTRunner(tree_namespace, bb=self.bb, tb=self.tb)
|
|
189
|
+
|
|
190
|
+
# Inject tree instance onto blackboard for FSM-integrated actions
|
|
191
|
+
self.bb._mycelium_tree_instance = self # type: ignore[attr-defined]
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
# Run BT tree
|
|
195
|
+
result = await self.bt_runner.tick()
|
|
196
|
+
finally:
|
|
197
|
+
# Clean up
|
|
198
|
+
if hasattr(self.bb, '_mycelium_tree_instance'):
|
|
199
|
+
delattr(self.bb, '_mycelium_tree_instance')
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
def to_mermaid(self) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Generate a unified Mermaid diagram showing BT and FSM integrations.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Mermaid diagram code
|
|
209
|
+
"""
|
|
210
|
+
lines = []
|
|
211
|
+
lines.append("%% Mycelium Unified Diagram")
|
|
212
|
+
lines.append("%% Shows Behavior Tree (BT) and FSM integrations")
|
|
213
|
+
lines.append("graph TD")
|
|
214
|
+
|
|
215
|
+
# Add the BT tree structure and track node ID mappings
|
|
216
|
+
bt_diagram = self._generate_bt_mermaid()
|
|
217
|
+
node_id_to_action = {} # Map N1, N2, etc. to action names
|
|
218
|
+
|
|
219
|
+
if bt_diagram:
|
|
220
|
+
lines.append("")
|
|
221
|
+
lines.append(" %% Behavior Tree Structure")
|
|
222
|
+
# Extract just the node definitions and edges
|
|
223
|
+
for line in bt_diagram.split("\n"):
|
|
224
|
+
stripped = line.strip()
|
|
225
|
+
if stripped and not stripped.startswith("TD") and not stripped.startswith("LR"):
|
|
226
|
+
lines.append(f" {stripped}")
|
|
227
|
+
|
|
228
|
+
# Parse action nodes to map IDs to names
|
|
229
|
+
if "ACTION" in stripped and "<br/>" in stripped:
|
|
230
|
+
parts = stripped.split("((ACTION<br/>")
|
|
231
|
+
if len(parts) == 2:
|
|
232
|
+
node_id = parts[0].strip()
|
|
233
|
+
action_name = parts[1].rstrip("))").strip()
|
|
234
|
+
node_id_to_action[node_id] = action_name
|
|
235
|
+
|
|
236
|
+
# Add FSM integrations as subgraphs
|
|
237
|
+
if self.spec.fsm_integrations:
|
|
238
|
+
lines.append("")
|
|
239
|
+
lines.append(" %% FSM Integrations")
|
|
240
|
+
|
|
241
|
+
for action_name, fsm_integration in self.spec.fsm_integrations.items():
|
|
242
|
+
lines.append("")
|
|
243
|
+
lines.append(f" %% FSM for action: {action_name}")
|
|
244
|
+
lines.append(f" subgraph FSM_{action_name} [\"FSM: {action_name}\"]")
|
|
245
|
+
|
|
246
|
+
# Generate state diagram for this FSM
|
|
247
|
+
fsm_diagram = self._generate_fsm_mermaid(action_name, fsm_integration)
|
|
248
|
+
for line in fsm_diagram.split("\n"):
|
|
249
|
+
if line.strip():
|
|
250
|
+
lines.append(f" {line}")
|
|
251
|
+
|
|
252
|
+
lines.append(" end")
|
|
253
|
+
|
|
254
|
+
# Add connections between BT actions and their FSMs
|
|
255
|
+
if self.spec.fsm_integrations and node_id_to_action:
|
|
256
|
+
lines.append("")
|
|
257
|
+
lines.append(" %% BT-FSM Connections")
|
|
258
|
+
|
|
259
|
+
# Find which BT nodes have FSM integrations
|
|
260
|
+
action_to_node_id = {v: k for k, v in node_id_to_action.items()}
|
|
261
|
+
|
|
262
|
+
for action_name in self.spec.fsm_integrations.keys():
|
|
263
|
+
if action_name in action_to_node_id:
|
|
264
|
+
node_id = action_to_node_id[action_name]
|
|
265
|
+
lines.append(f" {node_id} -.->|ticks| FSM_{action_name}")
|
|
266
|
+
|
|
267
|
+
return "\n".join(lines)
|
|
268
|
+
|
|
269
|
+
def _generate_bt_mermaid(self) -> str:
|
|
270
|
+
"""Generate BT structure from the namespace."""
|
|
271
|
+
try:
|
|
272
|
+
# Use the BT's to_mermaid if available
|
|
273
|
+
from ..rhizomorph.util import to_mermaid as bt_to_mermaid
|
|
274
|
+
bt_namespace = self.spec.bt_namespace
|
|
275
|
+
if bt_namespace is None:
|
|
276
|
+
return " Root[\"No BT namespace\"]"
|
|
277
|
+
|
|
278
|
+
diagram = bt_to_mermaid(bt_namespace)
|
|
279
|
+
|
|
280
|
+
# Remove the ```mermaid wrapper if present
|
|
281
|
+
if diagram.startswith("```"):
|
|
282
|
+
lines = diagram.split("\n")
|
|
283
|
+
if lines[0].startswith("```"):
|
|
284
|
+
diagram = "\n".join(lines[1:-1])
|
|
285
|
+
|
|
286
|
+
# Replace flowchart with graph for consistency
|
|
287
|
+
diagram = diagram.replace("flowchart TD", "TD").replace("flowchart LR", "LR")
|
|
288
|
+
|
|
289
|
+
return diagram.strip()
|
|
290
|
+
except Exception:
|
|
291
|
+
# Fallback: simple representation
|
|
292
|
+
return f" Root[\"{self.spec.name}\"]"
|
|
293
|
+
|
|
294
|
+
def _generate_fsm_mermaid(self, action_name: str, fsm_integration: FSMIntegration) -> str:
|
|
295
|
+
"""Generate FSM state diagram using shared formatting utilities."""
|
|
296
|
+
from ..common.mermaid import format_state_node, format_transition
|
|
297
|
+
|
|
298
|
+
lines = []
|
|
299
|
+
|
|
300
|
+
# Get the initial state
|
|
301
|
+
initial = fsm_integration.initial_state
|
|
302
|
+
if callable(initial) and not isinstance(initial, type):
|
|
303
|
+
initial = initial()
|
|
304
|
+
|
|
305
|
+
# Try to discover states by following transitions
|
|
306
|
+
from ..septum.core import StateSpec
|
|
307
|
+
|
|
308
|
+
discovered: Dict[str, StateSpec] = {}
|
|
309
|
+
to_visit: List[Any] = [initial] # Can be StateSpec or callable returning StateSpec
|
|
310
|
+
visited: set[str] = set()
|
|
311
|
+
|
|
312
|
+
while to_visit:
|
|
313
|
+
state = to_visit.pop(0)
|
|
314
|
+
|
|
315
|
+
if callable(state) and not isinstance(state, type):
|
|
316
|
+
try:
|
|
317
|
+
state = state()
|
|
318
|
+
except Exception:
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
if not isinstance(state, StateSpec):
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
state_name = state.name
|
|
325
|
+
|
|
326
|
+
if state_name in visited:
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
visited.add(state_name)
|
|
330
|
+
discovered[state_name] = state
|
|
331
|
+
|
|
332
|
+
# Get transitions and add target states
|
|
333
|
+
try:
|
|
334
|
+
transitions = state.get_transitions()
|
|
335
|
+
for transition in transitions:
|
|
336
|
+
target_state = None
|
|
337
|
+
|
|
338
|
+
if hasattr(transition, 'transition'): # type: ignore[attr-defined]
|
|
339
|
+
target_state = transition.transition # type: ignore[attr-defined]
|
|
340
|
+
elif hasattr(transition, 'states'): # Push
|
|
341
|
+
target_state = transition # type: ignore[attr-defined]
|
|
342
|
+
else:
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
# Skip if no target state
|
|
346
|
+
if target_state is None:
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
# Skip special transitions
|
|
350
|
+
class_name = target_state.__class__.__name__
|
|
351
|
+
if class_name in ('Again', 'Unhandled', 'Retry', 'Restart', 'Repeat', 'Pop'):
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# Handle Push
|
|
355
|
+
if class_name == 'Push':
|
|
356
|
+
for push_state in target_state.states: # type: ignore[attr-defined]
|
|
357
|
+
# Get the name to check if visited
|
|
358
|
+
if hasattr(push_state, 'name'):
|
|
359
|
+
push_name = push_state.name # type: ignore[attr-defined]
|
|
360
|
+
elif callable(push_state):
|
|
361
|
+
# Will be resolved when popped from to_visit
|
|
362
|
+
to_visit.append(push_state)
|
|
363
|
+
continue
|
|
364
|
+
else:
|
|
365
|
+
continue
|
|
366
|
+
if push_name not in visited:
|
|
367
|
+
to_visit.append(push_state)
|
|
368
|
+
else:
|
|
369
|
+
# Regular state transition - only add if it's a StateSpec or callable
|
|
370
|
+
if isinstance(target_state, StateSpec) or callable(target_state):
|
|
371
|
+
# Get the name to check if visited
|
|
372
|
+
if hasattr(target_state, 'name'):
|
|
373
|
+
target_name = target_state.name # type: ignore[attr-defined]
|
|
374
|
+
elif callable(target_state):
|
|
375
|
+
# Will be resolved when popped from to_visit
|
|
376
|
+
to_visit.append(target_state)
|
|
377
|
+
continue
|
|
378
|
+
else:
|
|
379
|
+
continue
|
|
380
|
+
if target_name not in visited:
|
|
381
|
+
to_visit.append(target_state)
|
|
382
|
+
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
# Add states
|
|
387
|
+
if not discovered:
|
|
388
|
+
return f" {action_name}_Empty[\"No states discovered\"]"
|
|
389
|
+
|
|
390
|
+
added_states = set()
|
|
391
|
+
|
|
392
|
+
for state_name, state_spec in discovered.items():
|
|
393
|
+
short_name = state_name.split(".")[-1]
|
|
394
|
+
state_id = f"{action_name}_{short_name}"
|
|
395
|
+
|
|
396
|
+
if state_id not in added_states:
|
|
397
|
+
is_initial = state_spec == initial
|
|
398
|
+
lines.append(format_state_node(state_id, short_name, is_initial))
|
|
399
|
+
added_states.add(state_id)
|
|
400
|
+
|
|
401
|
+
# Get transitions from this state
|
|
402
|
+
try:
|
|
403
|
+
transitions = state_spec.get_transitions()
|
|
404
|
+
for transition in transitions:
|
|
405
|
+
target_state = None
|
|
406
|
+
label = ""
|
|
407
|
+
|
|
408
|
+
if hasattr(transition, 'transition'):
|
|
409
|
+
target_state = transition.transition # type: ignore[attr-defined]
|
|
410
|
+
if hasattr(transition, 'label'):
|
|
411
|
+
label = transition.label.name # type: ignore[attr-defined]
|
|
412
|
+
elif hasattr(transition, 'name'):
|
|
413
|
+
target_state = transition
|
|
414
|
+
label = "transition"
|
|
415
|
+
|
|
416
|
+
if target_state is None:
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
class_name = target_state.__class__.__name__
|
|
420
|
+
if class_name in ('Again', 'Unhandled', 'Retry', 'Restart', 'Repeat'):
|
|
421
|
+
continue
|
|
422
|
+
if class_name == 'Pop':
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
if class_name == 'Push':
|
|
426
|
+
for push_state in target_state.states: # type: ignore[attr-defined]
|
|
427
|
+
push_name = push_state.name if hasattr(push_state, 'name') else push_state.__name__
|
|
428
|
+
target_short = push_name.split(".")[-1]
|
|
429
|
+
target_id = f"{action_name}_{target_short}"
|
|
430
|
+
lines.append(format_transition(state_id, target_id, "Push"))
|
|
431
|
+
else:
|
|
432
|
+
target_name = target_state.name if hasattr(target_state, 'name') else str(target_state) # type: ignore[attr-defined]
|
|
433
|
+
target_short = target_name.split(".")[-1]
|
|
434
|
+
target_id = f"{action_name}_{target_short}"
|
|
435
|
+
lines.append(format_transition(state_id, target_id, label))
|
|
436
|
+
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
return "\n".join(lines)
|