Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ regions reach a final state:
>>> from statemachine import StateChart, State

>>> class DeployPipeline(StateChart):
... validate_disconnected_states = False
... class deploy(State.Parallel):
... class build(State.Compound):
... compiling = State(initial=True)
Expand Down Expand Up @@ -219,7 +218,6 @@ of starting from the initial one:
>>> from statemachine import HistoryState, StateChart, State

>>> class EditorWithHistory(StateChart):
... validate_disconnected_states = False
... class editor(State.Compound):
... source = State(initial=True)
... visual = State()
Expand Down
11 changes: 0 additions & 11 deletions docs/releases/3.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ the parent and all descendants. See {ref}`statecharts` for full details.
>>> from statemachine import State, StateChart

>>> class WarOfTheRing(StateChart):
... validate_disconnected_states = False
... class war(State.Parallel):
... class frodos_quest(State.Compound):
... shire = State(initial=True)
Expand Down Expand Up @@ -160,7 +159,6 @@ Supports both shallow (`HistoryState()`) and deep (`HistoryState(deep=True)`) hi
>>> from statemachine import HistoryState, State, StateChart

>>> class GollumPersonality(StateChart):
... validate_disconnected_states = False
... class personality(State.Compound):
... smeagol = State(initial=True)
... gollum = State()
Expand Down Expand Up @@ -402,15 +400,6 @@ if sm.is_terminated:
print("State machine has finished.")
```


### Disable single graph component validation

Since SCXML don't require that all states should be reachable by transitions, we added a class-level
flag `validate_disconnected_states: bool = True` that can be used to disable this validation.

It's already disabled when parsing SCXML files.


### Typed models with `Generic[TModel]`

`StateChart` now supports a generic type parameter for the model, enabling full type
Expand Down
11 changes: 0 additions & 11 deletions docs/statecharts.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,6 @@ independently — events in one region don't affect others. Use `State.Parallel`
>>> from statemachine import State, StateChart

>>> class WarOfTheRing(StateChart):
... validate_disconnected_states = False
... class war(State.Parallel):
... class frodos_quest(State.Compound):
... shire = State(initial=True)
Expand Down Expand Up @@ -373,7 +372,6 @@ state have reached a final state:
>>> from statemachine import State, StateChart

>>> class WarWithDone(StateChart):
... validate_disconnected_states = False
... class war(State.Parallel):
... class quest(State.Compound):
... start_q = State(initial=True)
Expand All @@ -396,12 +394,6 @@ True
True

```

```{note}
Parallel states commonly require `validate_disconnected_states = False` because
regions may not be reachable from each other via transitions.
```

(history-states)=
## History pseudo-states

Expand All @@ -415,7 +407,6 @@ Import `HistoryState` and place it inside a `State.Compound`:
>>> from statemachine import HistoryState, State, StateChart

>>> class GollumPersonality(StateChart):
... validate_disconnected_states = False
... class personality(State.Compound):
... smeagol = State(initial=True)
... gollum = State()
Expand Down Expand Up @@ -454,7 +445,6 @@ state and restores the full hierarchy:
>>> from statemachine import HistoryState, State, StateChart

>>> class DeepMemoryOfMoria(StateChart):
... validate_disconnected_states = False
... class moria(State.Compound):
... class halls(State.Compound):
... entrance = State(initial=True)
Expand Down Expand Up @@ -677,7 +667,6 @@ currently active. This is especially useful for cross-region guards in parallel
>>> from statemachine import State, StateChart

>>> class CoordinatedAdvance(StateChart):
... validate_disconnected_states = False
... class forces(State.Parallel):
... class vanguard(State.Compound):
... waiting = State(initial=True)
Expand Down
2 changes: 0 additions & 2 deletions docs/states.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ independently. Define them using `State.Parallel`:
>>> from statemachine import State, StateChart

>>> class WarOfTheRing(StateChart):
... validate_disconnected_states = False
... class war(State.Parallel):
... class quest(State.Compound):
... start = State(initial=True)
Expand Down Expand Up @@ -267,7 +266,6 @@ Re-entering via the history state restores the previously active child. Import a
>>> from statemachine import HistoryState, State, StateChart

>>> class WithHistory(StateChart):
... validate_disconnected_states = False
... class mode(State.Compound):
... a = State(initial=True)
... b = State()
Expand Down
1 change: 0 additions & 1 deletion docs/transitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,6 @@ source and all target states.
>>> from statemachine import State, StateChart

>>> class MiddleEarthJourney(StateChart):
... validate_disconnected_states = False
... class rivendell(State.Compound):
... council = State(initial=True)
... preparing = State()
Expand Down
10 changes: 10 additions & 0 deletions statemachine/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ def visit_connected_states(state: "State"):
already_visited.add(state)
yield state
visit.extend(t.target for t in state.transitions if t.target)
# Traverse the state hierarchy: entering a compound/parallel state
# implicitly enters its initial children (all children for parallel).
for child in state.states:
if child.initial:
visit.append(child)
for child in state.history:
visit.append(child)
# Being in a child state implies being in all ancestor states.
if state.parent:
visit.append(state.parent)


def disconnected_states(starting_state: "State", all_states: MutableSet["State"]):
Expand Down
2 changes: 1 addition & 1 deletion statemachine/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def create_machine_class_from_definition(
``history``, and transitions via ``on`` (event-triggered) or
``transitions`` (eventless).
**definition: Additional keyword arguments passed to the metaclass
(e.g., ``validate_disconnected_states=False``).
(e.g., ``validate_final_reachability=False``).

Returns:
A new StateChart subclass configured with the given states and transitions.
Expand Down
2 changes: 0 additions & 2 deletions tests/examples/statechart_compound_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ class QuestMachine(StateChart):
and ``rivendell`` (with council activities). A ``wilderness`` state connects them.
"""

validate_disconnected_states = False

class shire(State.Compound):
bag_end = State("Bag End", initial=True)
green_dragon = State("The Green Dragon")
Expand Down
4 changes: 0 additions & 4 deletions tests/examples/statechart_history_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ class PersonalityMachine(StateChart):
pseudo-state, the previously active personality is restored.
"""

validate_disconnected_states = False

class personality(State.Compound):
smeagol = State("Smeagol", initial=True)
gollum = State("Gollum")
Expand Down Expand Up @@ -89,8 +87,6 @@ class personality(State.Compound):
class DeepPersonalityMachine(StateChart):
"""A machine with nested compounds and deep history."""

validate_disconnected_states = False

class realm(State.Compound):
class inner(State.Compound):
entrance = State("Entrance", initial=True)
Expand Down
2 changes: 0 additions & 2 deletions tests/examples/statechart_in_condition_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ class FellowshipMachine(StateChart):
only follows Frodo to Mordor after Frodo has already arrived there.
"""

validate_disconnected_states = False

class quest(State.Parallel):
class frodo_path(State.Compound):
shire_f = State("Frodo in Shire", initial=True)
Expand Down
2 changes: 0 additions & 2 deletions tests/examples/statechart_parallel_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ class WarMachine(StateChart):
Gandalf's defense of the realms.
"""

validate_disconnected_states = False

class war(State.Parallel):
class frodos_quest(State.Compound):
shire = State("The Shire", initial=True)
Expand Down
8 changes: 0 additions & 8 deletions tests/test_contrib_diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,6 @@ def test_parallel_state_diagram():
"""Diagram renders parallel state with dashed style."""

class SM(StateChart):
validate_disconnected_states: bool = False

class p(State.Parallel, name="p"):
class r1(State.Compound, name="r1"):
a = State(initial=True)
Expand All @@ -167,8 +165,6 @@ def test_nested_compound_state_diagram():
"""Diagram renders nested compound states."""

class SM(StateChart):
validate_disconnected_states: bool = False

class outer(State.Compound, name="Outer"):
class inner(State.Compound, name="Inner"):
deep = State(initial=True)
Expand Down Expand Up @@ -296,8 +292,6 @@ def test_parallel_state_label_indicator():
"""Parallel subgraph label includes a visual indicator."""

class SM(StateChart):
validate_disconnected_states: bool = False

class p(State.Parallel, name="p"):
class r1(State.Compound, name="r1"):
a = State(initial=True)
Expand Down Expand Up @@ -353,8 +347,6 @@ def test_compound_and_parallel_mixed():
"""Full diagram with compound and parallel states renders without error."""

class SM(StateChart):
validate_disconnected_states: bool = False

class top(State.Compound, name="Top"):
class par(State.Parallel, name="Par"):
class region1(State.Compound, name="Region1"):
Expand Down
6 changes: 0 additions & 6 deletions tests/test_statechart_compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,6 @@ async def test_cross_compound_transition(self, sm_runner):
"""Transition from one compound to another removes old children."""

class MiddleEarthJourney(StateChart):
validate_disconnected_states = False

class rivendell(State.Compound):
council = State(initial=True)
preparing = State()
Expand Down Expand Up @@ -148,8 +146,6 @@ async def test_enter_compound_lands_on_initial(self, sm_runner):
"""Entering a compound from outside lands on the initial child."""

class MiddleEarthJourney(StateChart):
validate_disconnected_states = False

class rivendell(State.Compound):
council = State(initial=True)
preparing = State()
Expand Down Expand Up @@ -192,8 +188,6 @@ async def test_multiple_compound_sequential_traversal(self, sm_runner):
"""Traverse all three compounds sequentially."""

class MiddleEarthJourney(StateChart):
validate_disconnected_states = False

class rivendell(State.Compound):
council = State(initial=True)
preparing = State(final=True)
Expand Down
2 changes: 0 additions & 2 deletions tests/test_statechart_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ async def test_error_in_parallel_region_isolation(self, sm_runner):
"""Error in one parallel region; error.execution handles the exit."""

class ParallelError(StateChart):
validate_disconnected_states = False

class fronts(State.Parallel):
class battle_a(State.Compound):
fighting = State(initial=True)
Expand Down
2 changes: 0 additions & 2 deletions tests/test_statechart_eventless.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ async def test_eventless_with_in_condition(self, sm_runner):
"""Eventless transition guarded by In('state_id')."""

class CoordinatedAdvance(StateChart):
validate_disconnected_states = False

class forces(State.Parallel):
class vanguard(State.Compound):
waiting = State(initial=True)
Expand Down
16 changes: 0 additions & 16 deletions tests/test_statechart_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ async def test_shallow_history_remembers_last_child(self, sm_runner):
"""Exit compound, re-enter via history -> restores last active child."""

class GollumPersonality(StateChart):
validate_disconnected_states = False

class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
Expand Down Expand Up @@ -49,8 +47,6 @@ async def test_shallow_history_default_on_first_visit(self, sm_runner):
"""No prior visit -> history uses default transition target."""

class GollumPersonality(StateChart):
validate_disconnected_states = False

class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
Expand All @@ -73,8 +69,6 @@ async def test_deep_history_remembers_full_descendant(self, sm_runner):
"""Deep history restores the exact leaf in a nested compound."""

class DeepMemoryOfMoria(StateChart):
validate_disconnected_states = False

class moria(State.Compound):
class halls(State.Compound):
entrance = State(initial=True)
Expand Down Expand Up @@ -107,8 +101,6 @@ async def test_multiple_exits_and_reentries(self, sm_runner):
"""History updates each time we exit the compound."""

class GollumPersonality(StateChart):
validate_disconnected_states = False

class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
Expand Down Expand Up @@ -140,8 +132,6 @@ async def test_history_after_state_change(self, sm_runner):
"""Change state within compound, exit, re-enter -> new state restored."""

class GollumPersonality(StateChart):
validate_disconnected_states = False

class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
Expand All @@ -163,8 +153,6 @@ async def test_shallow_only_remembers_immediate_child(self, sm_runner):
"""Shallow history in nested compound restores direct child, not grandchild."""

class ShallowMoria(StateChart):
validate_disconnected_states = False

class moria(State.Compound):
class halls(State.Compound):
entrance = State(initial=True)
Expand Down Expand Up @@ -196,8 +184,6 @@ async def test_history_values_dict_populated(self, sm_runner):
"""sm.history_values[history_id] has saved states after exit."""

class GollumPersonality(StateChart):
validate_disconnected_states = False

class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
Expand All @@ -221,8 +207,6 @@ async def test_history_with_default_transition(self, sm_runner):
"""HistoryState with explicit default .to() transition."""

class GollumPersonality(StateChart):
validate_disconnected_states = False

class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
Expand Down
8 changes: 0 additions & 8 deletions tests/test_statechart_in_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ async def test_in_condition_true_enables_transition(self, sm_runner):
"""In('state_id') when state is active -> transition fires."""

class Fellowship(StateChart):
validate_disconnected_states = False

class positions(State.Parallel):
class frodo(State.Compound):
shire_f = State(initial=True)
Expand Down Expand Up @@ -62,8 +60,6 @@ async def test_in_with_parallel_regions(self, sm_runner):
"""Cross-region In() evaluation in parallel states."""

class FellowshipCoordination(StateChart):
validate_disconnected_states = False

class mission(State.Parallel):
class scouts(State.Compound):
scouting = State(initial=True)
Expand Down Expand Up @@ -117,8 +113,6 @@ async def test_in_combined_with_event(self, sm_runner):
"""Event + In() guard together."""

class CombinedGuard(StateChart):
validate_disconnected_states = False

class positions(State.Parallel):
class scout(State.Compound):
out = State(initial=True)
Expand All @@ -145,8 +139,6 @@ async def test_in_with_eventless_transition(self, sm_runner):
"""Eventless + In() guard."""

class EventlessIn(StateChart):
validate_disconnected_states = False

class coordination(State.Parallel):
class leader(State.Compound):
planning = State(initial=True)
Expand Down
Loading
Loading