Skip to content

Subclassing a StateChart with callbacks in a State.Compound body raises AttributeError #646

Description

@fgmacedo

Subclassing any StateChart whose compound state body contains a callback raises
AttributeError: 'State' object has no attribute '_callbacks' at subclass creation time.

Reproduction

from statemachine import State, StateChart

class Journey(StateChart):
    class shire(State.Compound):
        bag_end = State(initial=True)
        green_dragon = State()

        visit_pub = bag_end.to(green_dragon)

        def on_enter_green_dragon(self):
            ...

    road = State(final=True)
    depart = shire.to(road)

class Child(Journey):  # AttributeError
    pass
Traceback (most recent call last):
  ...
  File "statemachine/factory.py", line 168, in _unpack_builders_callbacks
    if state._callbacks:
       ^^^^^^^^^^^^^^^^
AttributeError: 'State' object has no attribute '_callbacks'

Cause

NestedStateFactory builds the compound as a State instance carrying _callbacks, and
StateMachineMetaclass._unpack_builders_callbacks consumes it with del state._callbacks:

def _unpack_builders_callbacks(cls):
    callbacks = {}
    for state in iterate_states(cls.states):
        if state._callbacks:
            callbacks.update(state._callbacks)
            del state._callbacks

add_inherited reuses the same State objects in the subclass, so the second metaclass run
re-reads an attribute the first run deleted. Any callback in the body triggers it, including a
plain method or an on=/cond= decorator.

Notes

Only compound bodies are affected: a flat StateChart never populates _callbacks on a state.
A compound body with no callbacks at all also subclasses fine, since _callbacks stays None
and the attribute is never deleted.

Setting state._callbacks = None instead of deleting it, or guarding with
getattr(state, "_callbacks", None), both fix the crash. The subclass does not need the
setattr again, since it inherits the attributes the base class already received.

Reproduced on the current develop.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions