11from copy import deepcopy
22from typing import TYPE_CHECKING
3+ from typing import List
34
45from .callbacks import CallbackGroup
56from .callbacks import CallbackPriority
@@ -17,7 +18,9 @@ class Transition:
1718
1819 Args:
1920 source (State): The origin state of the transition.
20- target (State): The target state of the transition.
21+ target: The target state(s) of the transition. Can be a single ``State``, a list of
22+ states (for multi-target transitions, e.g. SCXML parallel region entry), or ``None``
23+ (targetless transition).
2124 event (Optional[Union[str, List[str]]]): List of designators of events that trigger this
2225 transition. Can be either a list of strings, or a space-separated string list of event
2326 descriptors.
@@ -40,7 +43,7 @@ class Transition:
4043 def __init__ (
4144 self ,
4245 source : "State" ,
43- target : "State | None" = None ,
46+ target : "State | List[State] | None" = None ,
4447 event = None ,
4548 internal = False ,
4649 initial = False ,
@@ -52,18 +55,26 @@ def __init__(
5255 after = None ,
5356 ):
5457 self .source = source
55- self .target = target
58+ if isinstance (target , list ):
59+ self ._targets : "List[State]" = target
60+ elif target is not None :
61+ self ._targets = [target ]
62+ else :
63+ self ._targets = []
5664 self .internal = internal
5765 self .initial = initial
58- self .is_self = target is source
66+ first_target = self ._targets [0 ] if self ._targets else None
67+ self .is_self = first_target is source
5968 """Is the target state the same as the source state?"""
6069
61- if internal and not (self .is_self or (target and target .is_descendant (source ))):
70+ if internal and not (
71+ self .is_self or (first_target and first_target .is_descendant (source ))
72+ ):
6273 raise InvalidDefinition (
6374 _ (
6475 "Not a valid internal transition from source {source!r}, "
6576 "target {target!r} should be self or a descendant."
66- ).format (source = source , target = target )
77+ ).format (source = source , target = first_target )
6778 )
6879
6980 if initial and any ([cond , unless , event ]):
@@ -87,6 +98,23 @@ def __init__(
8798 .add (unless , priority = CallbackPriority .INLINE , expected_value = False )
8899 )
89100
101+ @property
102+ def target (self ) -> "State | None" :
103+ """Primary target state (first target for multi-target transitions)."""
104+ return self ._targets [0 ] if self ._targets else None
105+
106+ @target .setter
107+ def target (self , value : "State | None" ):
108+ if value is None :
109+ self ._targets = []
110+ else :
111+ self ._targets = [value ]
112+
113+ @property
114+ def targets (self ) -> "List[State]" :
115+ """All target states. For single-target transitions, returns a one-element list."""
116+ return self ._targets
117+
90118 def __repr__ (self ):
91119 return (
92120 f"{ type (self ).__name__ } ({ self .source .name !r} , { self .target and self .target .name !r} , "
@@ -147,7 +175,7 @@ def add_event(self, value):
147175
148176 def _copy_with_args (self , ** kwargs ):
149177 source = kwargs .pop ("source" , self .source )
150- target = kwargs .pop ("target" , self .target )
178+ target = kwargs .pop ("target" , list ( self ._targets ) if self . _targets else None )
151179 event = kwargs .pop ("event" , self .event )
152180 internal = kwargs .pop ("internal" , self .internal )
153181 new_transition = Transition (
0 commit comments