Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions docs/releases/3.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,10 +468,9 @@ def on_validate(self, previous_configuration):
```


### `add_observer()` renamed to `add_listener()`
### `add_observer()` removed

The method `add_observer` has been renamed to `add_listener`. The old name still works but emits
a `DeprecationWarning`.
The method `add_observer`, deprecated since v2.3.2, has been removed. Use `add_listener` instead.


### `TransitionNotAllowed` exception changes
Expand All @@ -487,3 +486,20 @@ The `allow_event_without_transition` was previously configured as an init parame
attribute.

Defaults to `False` in `StateMachine` class to preserve maximum backwards compatibility.


### `States.from_enum` default `use_enum_instance=True`

The `use_enum_instance` parameter of `States.from_enum` now defaults to `True` (was `False` in 2.x).
This means state values are the enum instances themselves, not their raw values.

If your code relies on raw enum values (e.g., integers), pass `use_enum_instance=False` explicitly.


### Short registry names removed

State machine classes are now only registered by their fully-qualified name (`qualname`).
The short-name lookup (by `cls.__name__`) that was deprecated since v0.8 has been removed.

If you use `get_machine_cls()` (e.g., via `MachineMixin`), make sure you pass the fully-qualified
dotted path.
50 changes: 48 additions & 2 deletions docs/releases/upgrade_2x_to_3.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ defaults. Review this guide to understand what changed and adopt the new APIs at
5. Replace `sm.add_observer(...)` with `sm.add_listener(...)`.
6. Update code that catches `TransitionNotAllowed` and accesses `.state` → use `.configuration`.
7. Review `on` callbacks that query `is_active` or `current_state` during transitions.
8. If using `States.from_enum`, note that `use_enum_instance` now defaults to `True`.
9. If using `get_machine_cls()` with short names, switch to fully-qualified names.

---

Expand Down Expand Up @@ -159,8 +161,7 @@ while not sm.is_terminated:

## Replace `add_observer()` with `add_listener()`

The method `add_observer` has been renamed to `add_listener`. The old name still works but emits
a `DeprecationWarning`.
The method `add_observer` has been removed in v3.0. Use `add_listener` instead.

**Before (2.x):**

Expand All @@ -175,6 +176,51 @@ sm.add_listener(my_listener)
```


## `States.from_enum` default changed to `use_enum_instance=True`

In 2.x, `States.from_enum` defaulted to `use_enum_instance=False`, meaning state values were the
raw enum values (e.g., integers). In 3.0, the default is `True`, so state values are the enum
instances themselves.

**Before (2.x):**

```python
states = States.from_enum(MyEnum, initial=MyEnum.start)
# states.start.value == 1 (raw value)
```

**After (3.0):**

```python
states = States.from_enum(MyEnum, initial=MyEnum.start)
# states.start.value == MyEnum.start (enum instance)
```

If your code relies on raw enum values, pass `use_enum_instance=False` explicitly.


## Short registry names removed

In 2.x, state machine classes were registered both by their fully-qualified name and their short
class name. The short-name lookup was deprecated since v0.8 and has been removed in 3.0.

**Before (2.x):**

```python
from statemachine.registry import get_machine_cls

cls = get_machine_cls("MyMachine") # short name — worked with warning
```

**After (3.0):**

```python
from statemachine.registry import get_machine_cls

cls = get_machine_cls("myapp.machines.MyMachine") # fully-qualified name
```


## Update `TransitionNotAllowed` exception handling

The `TransitionNotAllowed` exception now stores a `configuration` attribute (a set of states)
Expand Down
9 changes: 0 additions & 9 deletions statemachine/registry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import warnings

from .utils import qualname

try:
Expand All @@ -16,18 +14,11 @@ def autodiscover_modules(module_name: str):

def register(cls):
_REGISTRY[qualname(cls)] = cls
_REGISTRY[cls.__name__] = cls
return cls


def get_machine_cls(name):
init_registry()
if "." not in name:
warnings.warn(
"""Use fully qualified names (<module>.<class>) for state machine mixins.""",
DeprecationWarning,
stacklevel=2,
)
return _REGISTRY[name]


Expand Down
9 changes: 0 additions & 9 deletions statemachine/statemachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,15 +243,6 @@ def _register_callbacks(self, listeners: List[object]):

self._callbacks.async_or_sync()

def add_observer(self, *observers):
"""Add a listener."""
warnings.warn(
"""Method `add_observer` has been renamed to `add_listener`.""",
DeprecationWarning,
stacklevel=2,
)
return self.add_listener(*observers)

def add_listener(self, *listeners):
"""Add a listener.

Expand Down
36 changes: 18 additions & 18 deletions statemachine/states.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ class States:
"""
A class representing a collection of :ref:`State` objects.

Helps creating :ref:`StateMachine`'s :ref:`state` definitions from other
Helps creating :ref:`StateChart`'s :ref:`state` definitions from other
sources, like an ``Enum`` class, using :meth:`States.from_enum`.

>>> states_def = [('open', {'initial': True}), ('closed', {'final': True})]

>>> from statemachine import StateMachine
>>> class SM(StateMachine):
>>> from statemachine import StateChart
>>> class SM(StateChart):
...
... states = States({
... name: State(**params) for name, params in states_def
Expand All @@ -30,8 +30,8 @@ class States:

>>> sm = SM()
>>> sm.send("close")
>>> sm.current_state.id
'closed'
>>> sm.closed.is_active
True

"""

Expand Down Expand Up @@ -83,7 +83,7 @@ def items(self):
return self._states.items()

@classmethod
def from_enum(cls, enum_type: EnumType, initial, final=None, use_enum_instance: bool = False):
def from_enum(cls, enum_type: EnumType, initial, final=None, use_enum_instance: bool = True):
"""
Creates a new instance of the ``States`` class from an enumeration.

Expand All @@ -93,10 +93,10 @@ def from_enum(cls, enum_type: EnumType, initial, final=None, use_enum_instance:
... pending = 1
... completed = 2

A :ref:`StateMachine` that uses this enum can be declared as follows:
A :ref:`StateChart` that uses this enum can be declared as follows:

>>> from statemachine import StateMachine
>>> class ApprovalMachine(StateMachine):
>>> from statemachine import StateChart
>>> class ApprovalMachine(StateChart):
...
... _ = States.from_enum(Status, initial=Status.pending, final=Status.completed)
...
Expand All @@ -107,7 +107,7 @@ def from_enum(cls, enum_type: EnumType, initial, final=None, use_enum_instance:

.. tip::
When you assign the result of ``States.from_enum`` to a class-level variable in your
:ref:`StateMachine`, you're all set. You can use any name for this variable. In this
:ref:`StateChart`, you're all set. You can use any name for this variable. In this
example, we used ``_`` to show that the name doesn't matter. The metaclass will inspect
the variable of type :ref:`States (class)` and automatically assign the inner
:ref:`State` instances to the state machine.
Expand All @@ -128,25 +128,25 @@ def from_enum(cls, enum_type: EnumType, initial, final=None, use_enum_instance:
True

>>> sm.current_state_value
2
<Status.completed: 2>

If you need to use the enum instance as the state value, you can set the
``use_enum_instance=True``:
If you need to use the raw enum value instead of the enum instance, you can set
``use_enum_instance=False``:

>>> states = States.from_enum(Status, initial=Status.pending, use_enum_instance=True)
>>> states = States.from_enum(Status, initial=Status.pending, use_enum_instance=False)
>>> states.completed.value
<Status.completed: 2>
2

.. deprecated:: 2.3.3
.. versionchanged:: 3.0.0

On the next major release, ``use_enum_instance=True`` will be the default.
The default changed from ``False`` to ``True``.

Args:
enum_type: An enumeration containing the states of the machine.
initial: The initial state of the machine.
final: A set of final states of the machine.
use_enum_instance: If ``True``, the value of the state will be the enum item instance,
otherwise the enum item value. Defaults to ``False``.
otherwise the enum item value. Defaults to ``True``.

Returns:
A new instance of the :ref:`States (class)`.
Expand Down
36 changes: 18 additions & 18 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ def current_time():
def campaign_machine():
"Define a new class for each test"
from statemachine import State
from statemachine import StateMachine
from statemachine import StateChart

class CampaignMachine(StateMachine):
class CampaignMachine(StateChart):
"A workflow machine"

draft = State(initial=True)
Expand All @@ -47,11 +47,13 @@ class CampaignMachine(StateMachine):
def campaign_machine_with_validator():
"Define a new class for each test"
from statemachine import State
from statemachine import StateMachine
from statemachine import StateChart

class CampaignMachine(StateMachine):
class CampaignMachine(StateChart):
"A workflow machine"

error_on_execution = False

draft = State(initial=True)
producing = State("Being produced")
closed = State(final=True)
Expand All @@ -71,9 +73,9 @@ def can_produce(*args, **kwargs):
def campaign_machine_with_final_state():
"Define a new class for each test"
from statemachine import State
from statemachine import StateMachine
from statemachine import StateChart

class CampaignMachine(StateMachine):
class CampaignMachine(StateChart):
"A workflow machine"

draft = State(initial=True)
Expand All @@ -91,9 +93,9 @@ class CampaignMachine(StateMachine):
def campaign_machine_with_values():
"Define a new class for each test"
from statemachine import State
from statemachine import StateMachine
from statemachine import StateChart

class CampaignMachineWithKeys(StateMachine):
class CampaignMachineWithKeys(StateChart):
"A workflow machine"

draft = State(initial=True, value=1)
Expand Down Expand Up @@ -131,9 +133,9 @@ def AllActionsMachine():
@pytest.fixture()
def classic_traffic_light_machine(engine):
from statemachine import State
from statemachine import StateMachine
from statemachine import StateChart

class TrafficLightMachine(StateMachine):
class TrafficLightMachine(StateChart):
green = State(initial=True)
yellow = State()
red = State()
Expand All @@ -150,18 +152,16 @@ def _get_engine(self):

@pytest.fixture()
def classic_traffic_light_machine_allow_event(classic_traffic_light_machine):
class TrafficLightMachineAllowingEventWithoutTransition(classic_traffic_light_machine):
allow_event_without_transition = True

return TrafficLightMachineAllowingEventWithoutTransition
"""Already allow_event_without_transition=True (StateChart default)."""
return classic_traffic_light_machine


@pytest.fixture()
def reverse_traffic_light_machine():
from statemachine import State
from statemachine import StateMachine
from statemachine import StateChart

class ReverseTrafficLightMachine(StateMachine):
class ReverseTrafficLightMachine(StateChart):
"A traffic light machine"

green = State(initial=True)
Expand All @@ -177,9 +177,9 @@ class ReverseTrafficLightMachine(StateMachine):
@pytest.fixture()
def approval_machine(current_time): # noqa: C901
from statemachine import State
from statemachine import StateMachine
from statemachine import StateChart

class ApprovalMachine(StateMachine):
class ApprovalMachine(StateChart):
"A workflow machine"

requested = State(initial=True)
Expand Down
6 changes: 4 additions & 2 deletions tests/django_project/workflow/statemachines.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from statemachine.states import States

from statemachine import StateMachine
from statemachine import StateChart

from .models import WorkflowSteps


class WorfklowStateMachine(StateMachine):
class WorfklowStateMachine(StateChart):
allow_event_without_transition = False

_ = States.from_enum(WorkflowSteps, initial=WorkflowSteps.DRAFT, final=WorkflowSteps.PUBLISHED)

publish = _.DRAFT.to(_.PUBLISHED, cond="is_active")
Expand Down
1 change: 0 additions & 1 deletion tests/examples/enum_campaign_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class CampaignMachine(StateChart):
CampaignStatus,
initial=CampaignStatus.DRAFT,
final=CampaignStatus.CLOSED,
use_enum_instance=True,
)

add_job = states.DRAFT.to(states.DRAFT) | states.PRODUCING.to(states.PRODUCING)
Expand Down
10 changes: 5 additions & 5 deletions tests/examples/statechart_error_handling_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,16 @@ def on_enter_recovering(self, error=None, **kwargs):


# %%
# Comparison with StateMachine (error propagation)
# --------------------------------------------------
# Comparison with error_on_execution=False (error propagation)
# --------------------------------------------------------------
#
# With ``StateMachine`` (where ``error_on_execution=False``), the same error
# With ``error_on_execution=False``, the same error
# would propagate as an exception instead of being caught.

from statemachine import StateMachine # noqa: E402

class QuestNoCatch(StateChart):
error_on_execution = False

class QuestNoCatch(StateMachine):
safe = State("Safe", initial=True)
danger_zone = State("Danger Zone")

Expand Down
2 changes: 1 addition & 1 deletion tests/scxml/test_microwave.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_microwave_scxml():
processor.parse_scxml("microwave", MICROWAVE_SCXML)
sm = processor.start()

assert sm.current_state.id == "unplugged"
assert "unplugged" in sm.current_state_value
sm.send("plug-in")

assert "idle" in sm.current_state_value
Expand Down
Loading
Loading