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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,6 @@ target/
docs/auto_examples/sg_execution_times.*
docs/auto_examples/*.pickle
docs/sg_execution_times.rst

# Temporary files
tmp/
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ Generate a diagram or get a text representation with f-strings:
>>> print(f"{sm:md}")
| State | Event | Guard | Target |
| ------ | ----- | ----- | ------ |
| Green | cycle | | Yellow |
| Yellow | cycle | | Red |
| Red | cycle | | Green |
| Green | Cycle | | Yellow |
| Yellow | Cycle | | Red |
| Red | Cycle | | Green |
<BLANKLINE>

```

Expand Down
30 changes: 15 additions & 15 deletions docs/diagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ stateDiagram-v2
state "Yellow" as yellow
state "Red" as red
[*] --> green
green --> yellow : cycle
yellow --> red : cycle
red --> green : cycle
green --> yellow : Cycle
yellow --> red : Cycle
red --> green : Cycle
<BLANKLINE>
classDef active fill:#40E0D0,stroke:#333
green:::active
Expand All @@ -137,9 +137,9 @@ stateDiagram-v2
>>> print(f"{sm:md}")
| State | Event | Guard | Target |
| ------ | ----- | ----- | ------ |
| Green | cycle | | Yellow |
| Yellow | cycle | | Red |
| Red | cycle | | Green |
| Green | Cycle | | Yellow |
| Yellow | Cycle | | Red |
| Red | Cycle | | Green |
<BLANKLINE>

```
Expand All @@ -154,9 +154,9 @@ stateDiagram-v2
state "Yellow" as yellow
state "Red" as red
[*] --> green
green --> yellow : cycle
yellow --> red : cycle
red --> green : cycle
green --> yellow : Cycle
yellow --> red : Cycle
red --> green : Cycle
<BLANKLINE>

```
Expand Down Expand Up @@ -191,9 +191,9 @@ stateDiagram-v2
state "Yellow" as yellow
state "Red" as red
[*] --> green
green --> yellow : cycle
yellow --> red : cycle
red --> green : cycle
green --> yellow : Cycle
yellow --> red : Cycle
red --> green : Cycle
<BLANKLINE>

>>> formatter.supported_formats()
Expand Down Expand Up @@ -294,9 +294,9 @@ A traffic light.
<BLANKLINE>
| State | Event | Guard | Target |
| ------ | ----- | ----- | ------ |
| Green | cycle | | Yellow |
| Yellow | cycle | | Red |
| Red | cycle | | Green |
| Green | Cycle | | Yellow |
| Yellow | Cycle | | Red |
| Red | Cycle | | Green |
<BLANKLINE>
<BLANKLINE>

Expand Down
18 changes: 14 additions & 4 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,31 @@ Every event has two string properties:

- **`id`** — the programmatic identifier, derived from the class attribute name.
Use this in `send()`, guards, and comparisons.
- **`name`** — a human-readable label for display purposes. Defaults to the `id`
when not explicitly set.
- **`name`** — a human-readable label for display purposes. Auto-generated from
the `id` by replacing `_` and `.` with spaces and capitalizing the first word.
You can override the automatic name by passing `name=` explicitly when
declaring the event:

```py
>>> TrafficLight.cycle.id
'cycle'

>>> TrafficLight.cycle.name
'cycle'
'Cycle'

>>> class Example(StateChart):
... on = State(initial=True)
... off = State(final=True)
... shut_down = Event(on.to(off), name="Shut the system down")

>>> Example.shut_down.name
'Shut the system down'

```

```{tip}
Always use `event.id` for programmatic checks. The `name` property is intended
for UI display and may change format in future versions.
for UI display and may differ from the `id`.
```


Expand Down
Binary file modified docs/images/readme_trafficlightmachine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/releases/3.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,11 @@ machine instance concurrently. This is now documented in the
interpreted as the event `id`, leaving the extra transitions eventless (auto-firing).
[#588](https://github.com/fgmacedo/python-statemachine/pull/588).

- `Event.name` is now auto-humanized from the `id` (e.g., `cycle` → `Cycle`,
`pick_up` → `Pick up`). Diagrams, Mermaid output, and text tables all display
the human-readable name. Explicit `name=` values are preserved. The same
`humanize_id()` helper is now shared by `Event` and `State`.
[#601](https://github.com/fgmacedo/python-statemachine/pull/601),
fixes [#600](https://github.com/fgmacedo/python-statemachine/issues/600).

## Misc in 3.1.0
12 changes: 6 additions & 6 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,9 @@ You can also get text representations of any state machine using Python's built-
>>> print(f"{CoffeeOrder:md}")
| State | Event | Guard | Target |
| --------- | ------- | ----- | --------- |
| Pending | start | | Preparing |
| Preparing | finish | | Ready |
| Ready | pick_up | | Picked up |
| Pending | Start | | Preparing |
| Preparing | Finish | | Ready |
| Ready | Pick up | | Picked up |

```

Expand All @@ -394,9 +394,9 @@ stateDiagram-v2
state "Picked up" as picked_up
[*] --> pending
picked_up --> [*]
pending --> preparing : start
preparing --> ready : finish
ready --> picked_up : pick_up
pending --> preparing : Start
preparing --> ready : Finish
ready --> picked_up : Pick up
<BLANKLINE>

```
Expand Down
6 changes: 4 additions & 2 deletions statemachine/contrib/diagram/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,17 @@ def _format_event_names(transition: "Transition") -> str:

all_ids = {str(e) for e in events}

seen_ids: Set[str] = set()
display: List[str] = []
for event in events:
eid = str(event)
# Skip dot-form aliases (e.g. "done.invoke.X") when the underscore
# form ("done_invoke_X") is also registered on this transition.
if "." in eid and eid.replace(".", "_") in all_ids:
continue
if eid not in display: # pragma: no branch
display.append(eid)
if eid not in seen_ids: # pragma: no branch
seen_ids.add(eid)
display.append(event.name if event.name else eid)

return " ".join(display)

Expand Down
3 changes: 2 additions & 1 deletion statemachine/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .exceptions import InvalidDefinition
from .i18n import _
from .transition_mixin import AddCallbacksMixin
from .utils import humanize_id

if TYPE_CHECKING:
from .statemachine import StateChart
Expand Down Expand Up @@ -107,7 +108,7 @@ def __new__(
if name:
instance.name = name
elif _has_real_id:
instance.name = str(id).replace("_", " ").capitalize()
instance.name = humanize_id(id)
else:
instance.name = ""
if transitions:
Expand Down
2 changes: 1 addition & 1 deletion statemachine/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def add(self, events):
if isinstance(event, Event):
self._items.append(event)
else:
self._items.append(Event(id=event, name=event))
self._items.append(Event(id=event))

return self

Expand Down
4 changes: 2 additions & 2 deletions statemachine/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def add_from_attributes(cls, attrs): # noqa: C901
cls.add_state(key, value)
elif isinstance(value, (Transition, TransitionList)):
event_id = _expand_event_id(key)
cls.add_event(event=Event(transitions=value, id=event_id, name=key))
cls.add_event(event=Event(transitions=value, id=event_id))
elif isinstance(value, (Event,)):
if value._has_real_id:
event_id = value.id
Expand Down Expand Up @@ -338,7 +338,7 @@ def _add_unbounded_callback(cls, attr_name, func):
# machinery that is stored at ``func.attr_name``
setattr(cls, func.attr_name, func)
if func.is_event:
cls.add_event(event=Event(func._transitions, id=attr_name, name=attr_name))
cls.add_event(event=Event(func._transitions, id=attr_name))

def add_state(cls, id, state: State):
state._set_id(id)
Expand Down
4 changes: 2 additions & 2 deletions statemachine/io/scxml/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ def create_raise_action_callable(action: RaiseAction) -> Callable:
def raise_action(*args, **kwargs):
machine: StateChart = kwargs["machine"]

Event(id=action.event, name=action.event, internal=True, _sm=machine).put()
Event(id=action.event, internal=True, _sm=machine).put()

raise_action.action = action # type: ignore[attr-defined]
return raise_action
Expand Down Expand Up @@ -492,7 +492,7 @@ def send_action(*args, **kwargs): # noqa: C901
continue
params_values[param.name] = _eval(param.expr, **kwargs)

Event(id=event, name=event, delay=delay, internal=internal, _sm=machine).put(
Event(id=event, delay=delay, internal=internal, _sm=machine).put(
*content,
send_id=send_id,
**params_values,
Expand Down
9 changes: 4 additions & 5 deletions statemachine/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .invoke import normalize_invoke_callbacks
from .transition import Transition
from .transition_list import TransitionList
from .utils import humanize_id

if TYPE_CHECKING:
from .statemachine import StateChart
Expand Down Expand Up @@ -116,10 +117,8 @@ class State:

Args:
name: A human-readable representation of the state. Default is derived
from the name of the variable assigned to the state machine class.
The name is derived from the id using this logic::

name = id.replace("_", " ").capitalize()
from the name of the variable assigned to the state machine class,
by replacing ``_`` and ``.`` with spaces and capitalizing the first word.

value: A specific value to the storage and retrieval of states.
If specified, you can use It to map a more friendly representation to a low-level
Expand Down Expand Up @@ -302,7 +301,7 @@ def _set_id(self, id: str) -> "State":
if self.value is None:
self.value = id
if not self.name:
self.name = self._id.replace("_", " ").capitalize()
self.name = humanize_id(self._id)
self._hash = hash((self.name, self._id))

return self
Expand Down
18 changes: 18 additions & 0 deletions statemachine/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import asyncio
import re
import threading
from typing import Any

_SEPARATOR_RE = re.compile(r"[_.]")

_cached_loop = threading.local()
"""Loop that will be used when the SM is running in a synchronous context. One loop per thread."""

Expand All @@ -26,6 +29,21 @@ def ensure_iterable(obj):
return [obj]


def humanize_id(id: str) -> str:
"""Convert a machine identifier to a human-readable name.

Splits on ``_`` and ``.`` separators and capitalizes the first word.

>>> humanize_id("go")
'Go'
>>> humanize_id("done_state_parent")
'Done state parent'
>>> humanize_id("error.execution")
'Error execution'
"""
return _SEPARATOR_RE.sub(" ", id).strip().capitalize()


def run_async_from_sync(coroutine: "Any") -> "Any":
"""
Compatibility layer to run an async coroutine from a synchronous context.
Expand Down
2 changes: 1 addition & 1 deletion tests/test_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def on_start(self):
def test_issue_417_cannot_start(self, model_class, sm_class, mock_calls):
model = model_class(0)
sm = sm_class(model, 0)
with pytest.raises(sm.TransitionNotAllowed, match="Can't start when in Created"):
with pytest.raises(sm.TransitionNotAllowed, match="Can't Start when in Created"):
sm.start()

mock_calls.assert_not_called()
Expand Down
Loading
Loading