From 9a3bfef555ded996490a8ed6808b7a04f032f8ce Mon Sep 17 00:00:00 2001 From: Fernando Macedo Date: Sat, 21 Mar 2026 12:59:29 -0300 Subject: [PATCH 1/3] chore: add tmp/ to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 22eb649a..ba144f94 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,6 @@ target/ docs/auto_examples/sg_execution_times.* docs/auto_examples/*.pickle docs/sg_execution_times.rst + +# Temporary files +tmp/ From bc401990da05ab24b1e76619ac67f17388548bb5 Mon Sep 17 00:00:00 2001 From: Fernando Macedo Date: Sat, 21 Mar 2026 13:00:35 -0300 Subject: [PATCH 2/3] fix: render human-readable Event name in diagrams (#600) Event.name is now auto-generated as a humanized form of the id (split by _ and . separators, first word capitalized) instead of echoing the raw identifier. Explicit name= passed by the user is preserved. - Add humanize_id() in utils.py (compiled regex, shared by Event and State) - Remove redundant name= from Event call sites in factory, events, and SCXML actions so auto-generation kicks in - Use event.name in diagram labels (_format_event_names) - Update docs and tests to reflect humanized names Closes #600 --- README.md | 7 ++-- docs/diagram.md | 30 +++++++------- docs/events.md | 18 +++++++-- docs/images/readme_trafficlightmachine.png | Bin 11895 -> 12504 bytes docs/tutorial.md | 12 +++--- statemachine/contrib/diagram/extract.py | 6 ++- statemachine/event.py | 3 +- statemachine/events.py | 2 +- statemachine/factory.py | 4 +- statemachine/io/scxml/actions.py | 4 +- statemachine/state.py | 9 ++--- statemachine/utils.py | 18 +++++++++ tests/test_callbacks.py | 2 +- tests/test_contrib_diagram.py | 44 ++++++++++++++------- tests/test_events.py | 8 ++-- tests/test_mermaid_renderer.py | 22 +++++------ tests/test_multiple_destinations.py | 2 +- tests/test_statemachine.py | 10 ++--- tests/test_transition_table.py | 4 +- tests/test_transitions.py | 2 +- 20 files changed, 127 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 43f141a6..b7393b09 100644 --- a/README.md +++ b/README.md @@ -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 | + ``` diff --git a/docs/diagram.md b/docs/diagram.md index ff3962df..d48443d3 100644 --- a/docs/diagram.md +++ b/docs/diagram.md @@ -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 classDef active fill:#40E0D0,stroke:#333 green:::active @@ -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 | ``` @@ -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 ``` @@ -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 >>> formatter.supported_formats() @@ -294,9 +294,9 @@ A traffic light. | State | Event | Guard | Target | | ------ | ----- | ----- | ------ | -| Green | cycle | | Yellow | -| Yellow | cycle | | Red | -| Red | cycle | | Green | +| Green | Cycle | | Yellow | +| Yellow | Cycle | | Red | +| Red | Cycle | | Green | diff --git a/docs/events.md b/docs/events.md index 6db4c195..57351820 100644 --- a/docs/events.md +++ b/docs/events.md @@ -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`. ``` diff --git a/docs/images/readme_trafficlightmachine.png b/docs/images/readme_trafficlightmachine.png index 5685ec100a85aaccb2bc8e59f59bb273831bdb42..f5179c05a3378e453ae7557555ef988a88ca0404 100644 GIT binary patch literal 12504 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 Mdn{Wn{WSRh0NuWx$^ZZW literal 11895 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 diff --git a/docs/tutorial.md b/docs/tutorial.md index d49526d7..3bfe1489 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -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 | ``` @@ -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 ``` diff --git a/statemachine/contrib/diagram/extract.py b/statemachine/contrib/diagram/extract.py index 15a1f2da..45caf9e5 100644 --- a/statemachine/contrib/diagram/extract.py +++ b/statemachine/contrib/diagram/extract.py @@ -116,6 +116,7 @@ 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) @@ -123,8 +124,9 @@ def _format_event_names(transition: "Transition") -> str: # 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) diff --git a/statemachine/event.py b/statemachine/event.py index 25a1de6c..f5986340 100644 --- a/statemachine/event.py +++ b/statemachine/event.py @@ -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 @@ -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: diff --git a/statemachine/events.py b/statemachine/events.py index 2fe2be01..16cd681e 100644 --- a/statemachine/events.py +++ b/statemachine/events.py @@ -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 diff --git a/statemachine/factory.py b/statemachine/factory.py index c29825f7..2e9b1c3b 100644 --- a/statemachine/factory.py +++ b/statemachine/factory.py @@ -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 @@ -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) diff --git a/statemachine/io/scxml/actions.py b/statemachine/io/scxml/actions.py index c4e46cca..b737cd2e 100644 --- a/statemachine/io/scxml/actions.py +++ b/statemachine/io/scxml/actions.py @@ -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 @@ -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, diff --git a/statemachine/state.py b/statemachine/state.py index e8aa572a..1d77bc9b 100644 --- a/statemachine/state.py +++ b/statemachine/state.py @@ -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 @@ -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 @@ -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 diff --git a/statemachine/utils.py b/statemachine/utils.py index d3661888..ac1ddbb1 100644 --- a/statemachine/utils.py +++ b/statemachine/utils.py @@ -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.""" @@ -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. diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index 048c3477..e593c75b 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -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() diff --git a/tests/test_contrib_diagram.py b/tests/test_contrib_diagram.py index 2da3cdd8..cb03dc9a 100644 --- a/tests/test_contrib_diagram.py +++ b/tests/test_contrib_diagram.py @@ -12,6 +12,7 @@ from statemachine.contrib.diagram.model import ActionType from statemachine.contrib.diagram.model import StateType from statemachine.contrib.diagram.renderers.dot import DotRenderer +from statemachine.event import Event from statemachine import State from statemachine import StateChart @@ -176,7 +177,7 @@ def test_format_mermaid(self, tmp_path): content = out.read_text() assert "stateDiagram-v2" in content - assert "green --> yellow : cycle" in content + assert "green --> yellow : Cycle" in content def test_format_md(self, tmp_path): out = tmp_path / "sm.md" @@ -192,7 +193,7 @@ def test_format_md(self, tmp_path): content = out.read_text() assert "| State" in content - assert "cycle" in content + assert "Cycle" in content def test_format_rst(self, tmp_path): out = tmp_path / "sm.rst" @@ -208,7 +209,7 @@ def test_format_rst(self, tmp_path): content = out.read_text() assert "+---" in content - assert "cycle" in content + assert "Cycle" in content def test_format_mermaid_stdout(self, capsys): main( @@ -702,8 +703,8 @@ def test_resolve_initial_fallback(self): class TestFormatEventNames: """Tests for _format_event_names — alias filtering for diagram display.""" - def test_simple_event_unchanged(self): - """A plain event with no aliases is returned as-is.""" + def test_simple_event_uses_name(self): + """A plain event displays its human-readable name.""" class SM(StateChart): s1 = State(initial=True) @@ -711,7 +712,7 @@ class SM(StateChart): go = s1.to(s2) t = SM.s1.transitions[0] - assert _format_event_names(t) == "go" + assert _format_event_names(t) == "Go" def test_done_state_alias_filtered(self): """done_state_X registers both underscore and dot forms; only underscore is shown.""" @@ -727,7 +728,7 @@ class parent(State.Compound): t = next(t for t in SM.parent.transitions if t.event and "done_state" in t.event) result = _format_event_names(t) - assert result == "done_state_parent" + assert result == "Done state parent" assert "done.state" not in result def test_done_invoke_alias_filtered(self): @@ -740,7 +741,7 @@ class SM(StateChart): t = SM.s1.transitions[0] result = _format_event_names(t) - assert result == "done_invoke_child" + assert result == "Done invoke child" assert "done.invoke" not in result def test_error_alias_filtered(self): @@ -753,7 +754,7 @@ class SM(StateChart): t = SM.s1.transitions[0] result = _format_event_names(t) - assert result == "error_execution" + assert result == "Error execution" assert "error.execution" not in result def test_multiple_distinct_events_preserved(self): @@ -769,8 +770,8 @@ class SM(StateChart): t = SM.s1.transitions[0] t.add_event("also") result = _format_event_names(t) - assert "go" in result - assert "also" in result + assert "Go" in result + assert "Also" in result def test_eventless_transition_returns_empty(self): """A transition with no events returns an empty string.""" @@ -798,7 +799,22 @@ class SM(StateChart): from statemachine.transition import Transition t = Transition(source=SM.s1, target=SM.s2, event="custom.event") - assert _format_event_names(t) == "custom.event" + assert _format_event_names(t) == "Custom event" + + def test_explicit_event_name_displayed(self): + """An Event with an explicit name= shows the human-readable name.""" + + class SM(StateChart): + active = State(initial=True) + suspended = State(final=True) + + suspend = Event( + active.to(suspended), + name="Human Suspend", + ) + + t = SM.active.transitions[0] + assert _format_event_names(t) == "Human Suspend" class TestDotRendererEdgeCases: @@ -1355,7 +1371,7 @@ def test_format_mermaid_class(self): result = f"{TrafficLightMachine:mermaid}" assert "stateDiagram-v2" in result - assert "green --> yellow : cycle" in result + assert "green --> yellow : Cycle" in result def test_format_md_instance(self): from tests.examples.traffic_light_machine import TrafficLightMachine @@ -1363,7 +1379,7 @@ def test_format_md_instance(self): sm = TrafficLightMachine() result = f"{sm:md}" assert "| State" in result - assert "cycle" in result + assert "Cycle" in result def test_format_md_class(self): from tests.examples.traffic_light_machine import TrafficLightMachine diff --git a/tests/test_events.py b/tests/test_events.py index 4a0cb015..b4ce48ab 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -82,8 +82,8 @@ class StartMachine(StateChart): assert list(StartMachine.events) == ["launch_the_machine"] assert [e.id for e in StartMachine.events] == ["launch_the_machine"] - assert [e.name for e in StartMachine.events] == ["launch_the_machine"] - assert StartMachine.launch_the_machine.name == "launch_the_machine" + assert [e.name for e in StartMachine.events] == ["Launch the machine"] + assert StartMachine.launch_the_machine.name == "Launch the machine" assert str(StartMachine.launch_the_machine) == "launch_the_machine" assert StartMachine.launch_the_machine == StartMachine.launch_the_machine.id @@ -201,8 +201,8 @@ def on_cycle(self, event_data, event: str): assert sm.send("cycle") == "Running cycle from red to green" assert sm.cycle.name == "Loop" assert sm.slow_down.name == "Slow down" - assert sm.stop.name == "stop" - assert sm.go.name == "go" + assert sm.stop.name == "Stop" + assert sm.go.name == "Go" def test_multiple_ids_from_the_same_event_will_be_converted_to_multiple_events(self): class TrafficLightMachine(StateChart): diff --git a/tests/test_mermaid_renderer.py b/tests/test_mermaid_renderer.py index 32cf518b..ea3a7bc8 100644 --- a/tests/test_mermaid_renderer.py +++ b/tests/test_mermaid_renderer.py @@ -247,10 +247,10 @@ class parent(State.Compound, name="Parent"): result = MermaidGraphMachine(SM).get_mermaid() assert 'state "Parent" as parent {' in result assert "[*] --> child1" in result - assert "child1 --> child2 : go" in result + assert "child1 --> child2 : Go" in result assert "child2 --> [*]" in result - assert "start --> parent : enter" in result - assert "parent --> end : finish" in result + assert "start --> parent : Enter" in result + assert "parent --> end : Finish" in result def test_compound_no_duplicate_transitions(self): """Transitions inside compound states must not also appear at top level.""" @@ -265,8 +265,8 @@ class parent(State.Compound, name="Parent"): enter = start.to(parent) result = MermaidGraphMachine(SM).get_mermaid() - # "child1 --> child2 : go" should appear exactly once (inside compound) - assert result.count("child1 --> child2 : go") == 1 + # "child1 --> child2 : Go" should appear exactly once (inside compound) + assert result.count("child1 --> child2 : Go") == 1 def test_parallel_state(self): class SM(StateChart): @@ -310,7 +310,7 @@ class region2(State.Compound, name="Region2"): result = MermaidGraphMachine(SM).get_mermaid() # Inside parallel: compound endpoint redirected to initial child - assert "idle --> working : start" in result + assert "idle --> working : Start" in result assert "idle --> inner" not in result def test_compound_outside_parallel_not_redirected(self): @@ -326,8 +326,8 @@ class parent(State.Compound, name="Parent"): leave = parent.to(end) result = MermaidGraphMachine(SM).get_mermaid() - assert "start --> parent : enter" in result - assert "parent --> end : leave" in result + assert "start --> parent : Enter" in result + assert "parent --> end : Leave" in result def test_nested_compound(self): class SM(StateChart): @@ -689,9 +689,9 @@ def test_traffic_light(self): from tests.examples.traffic_light_machine import TrafficLightMachine result = MermaidGraphMachine(TrafficLightMachine).get_mermaid() - assert "green --> yellow : cycle" in result - assert "yellow --> red : cycle" in result - assert "red --> green : cycle" in result + assert "green --> yellow : Cycle" in result + assert "yellow --> red : Cycle" in result + assert "red --> green : Cycle" in result def test_traffic_light_with_events(self): from tests.examples.traffic_light_machine import TrafficLightMachine diff --git a/tests/test_multiple_destinations.py b/tests/test_multiple_destinations.py index 59f38494..96e63953 100644 --- a/tests/test_multiple_destinations.py +++ b/tests/test_multiple_destinations.py @@ -163,7 +163,7 @@ def on_validate(self, previous_configuration): assert machine.is_terminated - with pytest.raises(exceptions.TransitionNotAllowed, match="Can't validate when in Completed."): + with pytest.raises(exceptions.TransitionNotAllowed, match="Can't Validate when in Completed."): assert machine.validate() diff --git a/tests/test_statemachine.py b/tests/test_statemachine.py index 5bc7f85c..1946a083 100644 --- a/tests/test_statemachine.py +++ b/tests/test_statemachine.py @@ -27,9 +27,9 @@ def test_machine_should_be_at_start_state(campaign_machine): "closed", ] assert [t.name for t in campaign_machine.events] == [ - "add_job", - "produce", - "deliver", + "Add job", + "Produce", + "Deliver", ] assert model.state == "draft" @@ -160,11 +160,11 @@ def test_machine_should_list_allowed_events_in_the_current_state(campaign_machin machine = campaign_machine(model) assert model.state == "draft" - assert [t.name for t in machine.allowed_events] == ["add_job", "produce"] + assert [t.name for t in machine.allowed_events] == ["Add job", "Produce"] machine.produce() assert model.state == "producing" - assert [t.name for t in machine.allowed_events] == ["add_job", "deliver"] + assert [t.name for t in machine.allowed_events] == ["Add job", "Deliver"] deliver = machine.allowed_events[1] diff --git a/tests/test_transition_table.py b/tests/test_transition_table.py index 198cf495..fadf363e 100644 --- a/tests/test_transition_table.py +++ b/tests/test_transition_table.py @@ -157,7 +157,7 @@ def test_traffic_light_md(self): assert "Green" in result assert "Yellow" in result assert "Red" in result - assert "cycle" in result + assert "Cycle" in result def test_traffic_light_rst(self): from tests.examples.traffic_light_machine import TrafficLightMachine @@ -165,7 +165,7 @@ def test_traffic_light_rst(self): ir = extract(TrafficLightMachine) result = TransitionTableRenderer().render(ir, fmt="rst") assert "Green" in result - assert "cycle" in result + assert "Cycle" in result assert "+---" in result def test_compound_state_names(self): diff --git a/tests/test_transitions.py b/tests/test_transitions.py index 4f4f47e4..b71497f0 100644 --- a/tests/test_transitions.py +++ b/tests/test_transitions.py @@ -19,7 +19,7 @@ def test_transition_representation(campaign_machine): def test_list_machine_events(classic_traffic_light_machine): machine = classic_traffic_light_machine() transitions = [t.name for t in machine.events] - assert transitions == ["slowdown", "stop", "go"] + assert transitions == ["Slowdown", "Stop", "Go"] def test_list_state_transitions(classic_traffic_light_machine): From 28a33080bee1e6f6091f78aaf623f790f4c4a04a Mon Sep 17 00:00:00 2001 From: Fernando Macedo Date: Sat, 21 Mar 2026 13:10:19 -0300 Subject: [PATCH 3/3] docs: add event humanized name to 3.1.0 release notes --- docs/releases/3.1.0.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releases/3.1.0.md b/docs/releases/3.1.0.md index e566ec62..ca9814ab 100644 --- a/docs/releases/3.1.0.md +++ b/docs/releases/3.1.0.md @@ -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