diff --git a/statemachine/mixins.py b/statemachine/mixins.py index 161f758d..4d7b5e6c 100644 --- a/statemachine/mixins.py +++ b/statemachine/mixins.py @@ -22,6 +22,8 @@ class MachineMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.state_machine_name: + if self._is_django_historical_model(): + return raise ValueError( _("{!r} is not a valid state machine name.").format(self.state_machine_name) ) @@ -34,3 +36,12 @@ def __init__(self, *args, **kwargs): ) if self.bind_events_as_methods: sm.bind_events_to(self) + + @classmethod + def _is_django_historical_model(cls) -> bool: + """Detect Django historical models created by ``apps.get_model()`` in migrations. + + Django sets ``__module__ = '__fake__'`` on these dynamically-created classes, + which lack the user-defined class attributes like ``state_machine_name``. + """ + return getattr(cls, "__module__", None) == "__fake__" diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 227b2b67..85f25751 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -21,3 +21,18 @@ class MyModelWithoutMachineName(MachineMixin): with pytest.raises(ValueError, match="None is not a valid state machine name"): MyModelWithoutMachineName() + + +def test_mixin_should_skip_init_for_django_historical_models(): + """Regression test for #551: MachineMixin fails in Django data migrations. + + Django's ``apps.get_model()`` returns historical models with ``__module__ = '__fake__'`` + that don't carry user-defined class attributes like ``state_machine_name``. + """ + + # Simulate a Django historical model: __module__ is '__fake__' and + # state_machine_name is not set (falls back to None from MachineMixin). + HistoricalModel = type("HistoricalModel", (MachineMixin,), {"__module__": "__fake__"}) + + instance = HistoricalModel() + assert not hasattr(instance, "statemachine")