Skip to content

Commit 7211235

Browse files
authored
fix: skip MachineMixin init for Django historical models (#551) (#561)
Django's apps.get_model() creates historical model classes with __module__ = '__fake__' that don't carry user-defined class attributes like state_machine_name. Detect this and skip state machine initialization instead of raising ValueError. Closes #551
1 parent 9697dea commit 7211235

2 files changed

Lines changed: 26 additions & 0 deletions

File tree

statemachine/mixins.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class MachineMixin:
2222
def __init__(self, *args, **kwargs):
2323
super().__init__(*args, **kwargs)
2424
if not self.state_machine_name:
25+
if self._is_django_historical_model():
26+
return
2527
raise ValueError(
2628
_("{!r} is not a valid state machine name.").format(self.state_machine_name)
2729
)
@@ -34,3 +36,12 @@ def __init__(self, *args, **kwargs):
3436
)
3537
if self.bind_events_as_methods:
3638
sm.bind_events_to(self)
39+
40+
@classmethod
41+
def _is_django_historical_model(cls) -> bool:
42+
"""Detect Django historical models created by ``apps.get_model()`` in migrations.
43+
44+
Django sets ``__module__ = '__fake__'`` on these dynamically-created classes,
45+
which lack the user-defined class attributes like ``state_machine_name``.
46+
"""
47+
return getattr(cls, "__module__", None) == "__fake__"

tests/test_mixins.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,18 @@ class MyModelWithoutMachineName(MachineMixin):
2121

2222
with pytest.raises(ValueError, match="None is not a valid state machine name"):
2323
MyModelWithoutMachineName()
24+
25+
26+
def test_mixin_should_skip_init_for_django_historical_models():
27+
"""Regression test for #551: MachineMixin fails in Django data migrations.
28+
29+
Django's ``apps.get_model()`` returns historical models with ``__module__ = '__fake__'``
30+
that don't carry user-defined class attributes like ``state_machine_name``.
31+
"""
32+
33+
# Simulate a Django historical model: __module__ is '__fake__' and
34+
# state_machine_name is not set (falls back to None from MachineMixin).
35+
HistoricalModel = type("HistoricalModel", (MachineMixin,), {"__module__": "__fake__"})
36+
37+
instance = HistoricalModel()
38+
assert not hasattr(instance, "statemachine")

0 commit comments

Comments
 (0)