Skip to content

feat: class-level listener declarations with setup() protocol#570

Merged
fgmacedo merged 2 commits intodevelopfrom
feat/class-level-listeners
Feb 18, 2026
Merged

feat: class-level listener declarations with setup() protocol#570
fgmacedo merged 2 commits intodevelopfrom
feat/class-level-listeners

Conversation

@fgmacedo
Copy link
Owner

Summary

  • Add listeners class attribute to StateChart/StateMachine for declaring listeners at class definition time
  • Callable entries (classes, partial, lambdas) act as factories — called per SM instance to produce a fresh listener
  • Non-callable entries (pre-built objects) are shared across all SM instances
  • Inheritance supported via MRO: child listeners append after parent; listeners_inherit = False replaces entirely
  • setup(sm, **kwargs) protocol lets factory-created listeners receive runtime dependencies via SignatureAdapter
  • New active_listeners public property to inspect all attached listeners
  • Serialization (pickle/deepcopy) correctly preserves all listeners without duplication

Example

from functools import partial
from statemachine import State, StateChart

class AuditListener:
    def __init__(self):
        self.log = []

    def after_transition(self, event, source, target):
        self.log.append(f"{event}: {source.id} -> {target.id}")

class DBListener:
    def setup(self, sm, session=None, **kwargs):
        self.session = session

    def after_transition(self, event, source, target, **kwargs):
        if self.session:
            self.session.add({"event": event, "from": source.id, "to": target.id})

class OrderMachine(StateChart):
    listeners = [AuditListener, DBListener]

    draft = State(initial=True)
    confirmed = State(final=True)
    confirm = draft.to(confirmed)

# Runtime deps injected via kwargs, forwarded to setup()
sm = OrderMachine(session=db.session())
sm.send("confirm")

sm.active_listeners[0].log
# ['confirm: draft -> confirmed']

Allow listeners to be declared at class definition time via a `listeners`
attribute on StateChart/StateMachine. The list accepts callables (classes,
partial, lambdas) as per-instance factories and pre-built instances as
shared listeners.

- Metaclass collects `_class_listeners` from attrs and MRO
- `listeners_inherit = False` to replace instead of extend parent listeners
- `setup(sm, **kwargs)` protocol for runtime dependency injection
- `active_listeners` public property to inspect attached listeners
- Serialization correctly preserves all listeners through pickle/copy
@codecov
Copy link

codecov bot commented Feb 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.94%. Comparing base (7faf7fa) to head (8eda64a).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #570   +/-   ##
========================================
  Coverage    99.94%   99.94%           
========================================
  Files           32       32           
  Lines         3734     3768   +34     
  Branches       575      583    +8     
========================================
+ Hits          3732     3766   +34     
  Misses           1        1           
  Partials         1        1           
Flag Coverage Δ
unittests 99.94% <100.00%> (+<0.01%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

The `_class_listener_instances` attribute was redundant — `_listeners`
already tracks all listeners (class-level + runtime + add_listener).
Remove the internal attribute and migrate all tests to use the public
`active_listeners` property instead.
@sonarqubecloud
Copy link

@fgmacedo fgmacedo merged commit 3b5ef35 into develop Feb 18, 2026
14 checks passed
@fgmacedo fgmacedo deleted the feat/class-level-listeners branch February 18, 2026 17:30
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.

1 participant

Comments