Skip to content

fix: register one event per id a prefixed attribute expands to - #675

Merged
fgmacedo merged 1 commit into
mainfrom
fgmacedo/fix-654-expanded-event-ids
Oct 13, 2026
Merged

fgmacedo merged 1 commit into
mainfrom
fgmacedo/fix-654-expanded-event-ids

Conversation

@fgmacedo

@fgmacedo fgmacedo commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Why

_expand_event_id("error_foo") returned the glued string "error_foo error.foo" and the chart registered 1 event under it instead of 2, so sm.error_foo(), allowed_events and enabled_events() were broken for every error_ / done_state_ / done_invoke_ attribute (#654, question 1 of #644).

Fixes #654. Also fixes question 1 of #644 (left open for its question 2).

What changes

  • A prefixed attribute registers 2 events, the attribute name and its dotted form, each bound as a class attribute and callable as a method: sm.error_foo() runs the transition instead of raising TypeError (bare) or doing nothing (Event(...)).
  • allowed_events and enabled_events() list both ids instead of raising AttributeError on error.execution, on both engines.
  • name, delay and internal declared on an Event(...) inside a nested state body are kept on both ids instead of dropped.
  • The @source.to(target) decorator form expands too; it registered only the attribute name before.
  • from_.any() with a prefixed name copies its transition once per state carrying both ids, instead of once per id (one diagram edge per state, callbacks registered once).
  • A chart with an Event(...) attribute plus a "*" transition builds instead of crashing with ValueError: list.remove(x); placeholders are now found by membership, not by the prefix matcher.

Evidence

Before, on origin/main, with the ErrorLogger chart from #644:

events: ['process', 'error_execution error.execution']
enabled_events: AttributeError 'ErrorLogger' object has no attribute 'error.execution'
error_execution(): TypeError TransitionList only supports the decorator syntax to register callbacks.

After:

events: ['process', 'error_execution', 'error.execution']
enabled_events: ['process', 'error_execution', 'error.execution']
error_execution(): {'failed'}
$ timeout 120 uv run pytest -n 4 --cov=statemachine --cov-branch
TOTAL                                                 6093      0   1898      0   100%
2543 passed, 25 skipped, 44 xfailed, 9 warnings in 25.22s
$ uv run ruff check . && uv run mypy --namespace-packages --explicit-package-bases statemachine/ tests/
All checks passed!
mypy: No issues found

New tests: tests/test_expanded_event_ids.py (3 prefixes x 4 declaration forms x top-level/nested, sync and async, plus from_.any()), 2 cases in tests/test_events.py, and the inverted test_expanded_id_keeps_the_declared_arguments_on_both_ids. Run against the origin/main package, 109 of the new cases fail for the reported reasons.

Not verified

  • MachineMixin / Django models with prefixed events (no code path there touches registration, but not exercised).
  • An Event() placeholder referenced from inside a nested state body is still not replaced (_update_event_references walks only top-level states); pre-existing, skipped in the new tests with that reason.
  • on_error_execution naming convention still only fires for a trigger literally named error_execution, not for the engine-raised error.execution; pre-existing, left for a follow-up.

@fgmacedo fgmacedo added this to the 3.2.2 milestone Oct 12, 2026
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (b6bf8ca) to head (7ccf95f).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #675   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           52        52           
  Lines         5672      5685   +13     
  Branches       891       896    +5     
=========================================
+ Hits          5672      5685   +13     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

`_expand_event_id` turned `error_foo` into the string `"error_foo error.foo"`,
and the callers registered a single event under that glued id. The transition
carried both ids, so `send("error.foo")` worked, but `sm.error_foo()` raised
`TypeError` on a bare transition and was a no-op on an `Event(...)`, and
`allowed_events` / `enabled_events()` raised `AttributeError` looking up
`error.execution` on the instance. Inside a nested state body the glued id
was split by `Events.add`, which dropped the `name`, `delay` and `internal`
declared on the `Event`. The `@source.to(target)` decorator form never
expanded at all.

The expansion now returns the ids as a list and each call site, including the
decorator form, registers one event per id, binding each as a class attribute.
An `Event()` declared without transitions and referenced through `event=` maps
to every id it expands to, so `_events_to_update` keeps a list of replacements
per placeholder and looks the placeholder up by membership rather than by the
prefix matcher, which crashed on a `"*"` transition. `AnyState` adds every id
to a single copy per state instead of copying the transition once per id.

Fixes #654. Also fixes question 1 of #644.

Signed-off-by: Fernando Macedo <fgmacedo@gmail.com>
@fgmacedo
fgmacedo force-pushed the fgmacedo/fix-654-expanded-event-ids branch from 17c6d51 to 7ccf95f Compare October 13, 2026 13:12
@sonarqubecloud

Copy link
Copy Markdown

@fgmacedo
fgmacedo merged commit 98eaca5 into main Oct 13, 2026
13 checks passed
@fgmacedo
fgmacedo deleted the fgmacedo/fix-654-expanded-event-ids branch October 13, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expanded event ids travel inside a space-separated string, so error_ never worked as a method call

1 participant