diff --git a/README.md b/README.md
index 063c7647..8f0c572b 100644
--- a/README.md
+++ b/README.md
@@ -298,7 +298,7 @@ There's a lot more to cover, please take a look at our docs:
https://python-statemachine.readthedocs.io.
-## Contributing
+## Contributing to the project
* Star this project
* Open an Issue
diff --git a/docs/actions.md b/docs/actions.md
index 7430a370..4f9d9f95 100644
--- a/docs/actions.md
+++ b/docs/actions.md
@@ -1,6 +1,6 @@
# Actions
-Action is the way a StateMachine can cause things to happen in the
+Action is the way a {ref}`StateMachine` can cause things to happen in the
outside world, and indeed they are the main reason why they exist at all.
The main point of introducing a state machine is for the
@@ -26,7 +26,7 @@ when something changes and are not bounded to a specific state or event:
- `after_transition()`
-The follow example can get you an overview of the "generic" callbacks available:
+The following example can get you an overview of the "generic" callbacks available:
```py
>>> from statemachine import StateMachine, State
@@ -162,14 +162,14 @@ It's also possible to use an event name as action.
## Transition actions
-For each {ref}`event`, you can register `before`, `on` and `after` callbacks.
+For each {ref}`event`, you can register `before`, `on`, and `after` callbacks.
### Declare transition actions by naming convention
The action will be registered for every {ref}`transition` associated with the event.
-Callbacks by naming convention will be searched on the StateMachine and on the
-model, using the patterns:
+Callbacks by naming convention will be searched on the StateMachine and the model,
+using the patterns:
- `before_()`
@@ -337,7 +337,7 @@ Actions and Guards will be executed in the following order:
(dynamic-dispatch)=
## Dynamic dispatch
-`python-statemachine` implements a custom dispatch mechanism on all those available Actions and
+{ref}`statemachine` implements a custom dispatch mechanism on all those available Actions and
Guards. This means that you can declare an arbitrary number of `*args` and `**kwargs`, and the
library will match your method signature of what's expected to receive with the provided arguments.
@@ -359,7 +359,7 @@ For your convenience, all these parameters are available for you on any Action o
: All keyword arguments provided on the {ref}`Event`.
`event_data`
-: A reference to `EventData` instance.
+: A reference to {ref}`EventData` instance.
`event`
: The {ref}`Event` that was triggered.
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 00000000..2bf93950
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,66 @@
+# API
+
+## StateMachine
+
+```{eval-rst}
+.. autoclass:: statemachine.statemachine.StateMachine
+ :members:
+ :undoc-members:
+```
+
+## State
+
+```{seealso}
+{ref}`States` reference.
+```
+
+
+```{eval-rst}
+.. autoclass:: statemachine.state.State
+ :members:
+```
+
+## Transition
+
+```{seealso}
+{ref}`Transitions` reference.
+```
+
+```{eval-rst}
+.. autoclass:: statemachine.transition.Transition
+ :members:
+```
+
+## TransitionList
+
+```{eval-rst}
+.. autoclass:: statemachine.transition_list.TransitionList
+ :members:
+```
+
+## Model
+
+```{seealso}
+{ref}`Domain models` reference.
+```
+
+
+```{eval-rst}
+.. autoclass:: statemachine.model.Model
+ :members:
+```
+
+## TriggerData
+
+
+```{eval-rst}
+.. autoclass:: statemachine.event_data.TriggerData
+ :members:
+```
+
+## EventData
+
+```{eval-rst}
+.. autoclass:: statemachine.event_data.EventData
+ :members:
+```
diff --git a/docs/diagram.md b/docs/diagram.md
index ebf9c390..1b5070b9 100644
--- a/docs/diagram.md
+++ b/docs/diagram.md
@@ -1,10 +1,10 @@
# Diagrams
-You can generate diagrams from your state machine.
+You can generate diagrams from your {ref}`StateMachine`.
```{note}
This functionality depends on [pydot](https://github.com/pydot/pydot), it means that you need to
-have pydot installed on your system. pydot is a Python library that allows you to create and
+have `pydot` installed on your system. pydot is a Python library that allows you to create and
manipulate graphs in [Graphviz](https://graphviz.org/)'s
[dot language](https://graphviz.org/doc/info/lang.html).
@@ -59,7 +59,7 @@ As this one:

-The current state is also highlighted:
+The current {ref}`state` is also highlighted:
``` py
diff --git a/docs/guards.md b/docs/guards.md
index 04aceab2..9160126b 100644
--- a/docs/guards.md
+++ b/docs/guards.md
@@ -1,10 +1,10 @@
(validators-and-guards)=
# Validators and guards
-Validations and Guards are checked before an transition is started. They are meant to stop a
+Validations and Guards are checked before a transition is started. They are meant to stop a
transition to occur.
-The main difference, is that {ref}`validators` raise exceptions to stop the flow, and {ref}`guards`
+The main difference is that {ref}`validators` raise exceptions to stop the flow, and {ref}`guards`
act like predicates that shall resolve to a ``boolean`` value.
```{seealso}
@@ -16,16 +16,16 @@ for all the available callbacks, being validators and guards or {ref}`actions`.
Also known as **Conditional transition**.
-A guard is a condition that may be checked when a statemachine wants to handle
-an {ref}`event`. A guard is declared on the {ref}`transition`, and when that transition
+A guard is a condition that may be checked when a {ref}`statemachine` wants to handle
+an {ref}`event`. A guard is declared on the {ref}`transition`, and when that {ref}`transition`
would trigger, then the guard (if any) is checked. If the guard is `True`
then the transition does happen. If the guard is `False`, the transition
is ignored.
-When transitions have guards, then it's possible to define two or more
-transitions for the same event from the same {ref}`state`. When the event happens, then
+When {ref}`transitions` have guards, then it's possible to define two or more
+transitions for the same {ref}`event` from the same {ref}`state`. When the {ref}`event` happens, then
the guarded transitions are checked, one by one, and the first transition
-whose guard is true will be used, the others will be ignored.
+whose guard is true will be used, and the others will be ignored.
A guard is generally a boolean function or boolean variable and must not have any side effects.
Side effects are reserved for {ref}`actions`.
@@ -66,7 +66,7 @@ So, a condition `s1.to(s2, cond=lambda: [])` will evaluate as `False`, as an emp
Are like {ref}`guards`, but instead of evaluating to boolean, they are expected to raise an
-exception to stop the flow. It may be useful for imperative style programming when you don't
+exception to stop the flow. It may be useful for imperative-style programming when you don't
want to continue evaluating other possible transitions and exit immediately.
* Single validator: `validators="validator"`
diff --git a/docs/index.md b/docs/index.md
index ee97c8f8..a3bc95cf 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -17,6 +17,7 @@ mixins
integrations
diagram
processing_model
+api
auto_examples/index
contributing
authors
diff --git a/docs/integrations.md b/docs/integrations.md
index b50dbdfa..4eee2519 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -3,7 +3,8 @@
## Django integration
-When used in a Django App, this library implements an auto-discovery hook similar to how Django's built-in **admin** [autodiscover](https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.autodiscover).
+When used in a Django App, this library implements an auto-discovery hook similar to how Django's
+built-in **admin** [autodiscover](https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.autodiscover).
> This library attempts to import an **statemachine** or **statemachines** module in each installed
> application. Such modules are expected to register `StateMachine` classes to be used with
@@ -11,11 +12,12 @@ When used in a Django App, this library implements an auto-discovery hook simila
```{hint}
-When using `python-statemachine` to control the state of a Django model, we advise keeping the ``StateMachine`` definitions on their modules.
+When using `python-statemachine` to control the state of a Django model, we advise keeping the
+{ref}`StateMachine` definitions on their own modules.
So as circular references may occur, and as a way to help you organize your
code, if you put state machines on modules named as mentioned above inside installed
-Django Apps, these `StateMachine` classes will be automatically
+Django Apps, these {ref}`StateMachine` classes will be automatically
imported and registered.
This is only an advice, nothing stops you do declare your state machine alongside your models.
diff --git a/docs/mixins.md b/docs/mixins.md
index 88742490..f2aaa90b 100644
--- a/docs/mixins.md
+++ b/docs/mixins.md
@@ -1,7 +1,7 @@
# Mixins
-Your model can be inherited from a custom mixin to auto-instantiate a state machine.
+Your {ref}`domain models` can be inherited from a custom mixin to auto-instantiate a {ref}`statemachine`.
## MachineMixin
diff --git a/docs/models.md b/docs/models.md
index cb3791fd..3936630a 100644
--- a/docs/models.md
+++ b/docs/models.md
@@ -13,16 +13,15 @@ If you don't pass an explicit model instance, this simple `Model` will be used:
:linenos:
```
-```{eval-rst}
-.. autoclass:: statemachine.model.Model
- :members:
-```
```{seealso}
-See the {ref}`sphx_glr_auto_examples_order_control_rich_model_machine.py` as example of using a domain object to hold attributes and methods to be used on the `StateMachine` definition.
+See the {ref}`sphx_glr_auto_examples_order_control_rich_model_machine.py` as example of using a
+domain object to hold attributes and methods to be used on the `StateMachine` definition.
```
```{hint}
-Domain models are registered as {ref}`observers`, so you can have the same level of functionalities provided to the built-in `StateMachine`, such as implementing all callbacks and guards on your domain model and keeping only the definition of states and transitions on
-the `StateMachine`.
+Domain models are registered as {ref}`observers`, so you can have the same level of functionalities
+provided to the built-in {ref}`StateMachine`, such as implementing all {ref}`actions` and
+{ref}`guards` on your domain model and keeping only the definition of {ref}`states` and
+{ref}`transitions` on the {ref}`StateMachine`.
```
diff --git a/docs/releases/1.0.1.md b/docs/releases/1.0.1.md
index 23e4ea99..09da5bf8 100644
--- a/docs/releases/1.0.1.md
+++ b/docs/releases/1.0.1.md
@@ -21,7 +21,7 @@ This is the last release to support Python 2.7, 3.5, and 3.6.
## What's new in 1.0
-### Validators and Guards
+### Added validators and Guards
Transitions now support `cond` and `unless` parameters, to restrict
the execution.
diff --git a/docs/releases/1.0.3.md b/docs/releases/1.0.3.md
index dbb17b9e..083b4d4e 100644
--- a/docs/releases/1.0.3.md
+++ b/docs/releases/1.0.3.md
@@ -8,7 +8,7 @@ references of callbacks when there were multiple concurrent instances of the sam
class.
-## Bugfixes
+## Bugfixes in 1.0.3
- [#334](https://github.com/fgmacedo/python-statemachine/issues/334): Fixed a shared reference
of callbacks when there were multiple concurrent instances of the same `StateMachine` class.
diff --git a/docs/releases/2.0.0.md b/docs/releases/2.0.0.md
index 86ac5e09..c7870b3a 100644
--- a/docs/releases/2.0.0.md
+++ b/docs/releases/2.0.0.md
@@ -32,12 +32,6 @@ Even if you trigger an event inside an action.
See {ref}`processing model` for more details.
```
-### Added support for translations (i18n)
-
-Now the library messages can be translated into any language.
-
-See {ref}`Add a translation` on how to contribute with translations.
-
### State names are now optional
{ref}`State` names are now by default derived from the class variable that they are assigned to.
@@ -47,7 +41,7 @@ when it is different than the one derived from its id.
```py
>>> from statemachine import StateMachine, State
->>> class SM(StateMachine):
+>>> class ApprovalMachine(StateMachine):
... pending = State(initial=True)
... waiting_approval = State()
... approved = State(final=True)
@@ -56,13 +50,13 @@ when it is different than the one derived from its id.
... approve = waiting_approval.to(approved)
...
->>> SM.pending.name
+>>> ApprovalMachine.pending.name
'Pending'
->>> SM.waiting_approval.name
+>>> ApprovalMachine.waiting_approval.name
'Waiting approval'
->>> SM.approved.name
+>>> ApprovalMachine.approved.name
'Approved'
```
@@ -86,6 +80,41 @@ are ever executed as a result of an internal transition.
See {ref}`internal transition` for more details.
```
+### Added option to ignore unknown events
+
+You can now instantiate a {ref}`StateMachine` with `allow_event_without_transition=True`,
+so the state machine will allow triggering events that may not lead to a state {ref}`transition`,
+including tolerance to unknown {ref}`event` triggers.
+
+The default value is ``False``, that keeps the backward compatible behavior of when an
+event does not result in a {ref}`transition`, an exception ``TransitionNotAllowed`` will be raised.
+
+```py
+>>> sm = ApprovalMachine(allow_event_without_transition=True)
+
+>>> sm.send("unknow_event_name")
+
+>>> sm.pending.is_active
+True
+
+>>> sm.send("approve")
+
+>>> sm.pending.is_active
+True
+
+>>> sm.send("start")
+
+>>> sm.waiting_approval.is_active
+True
+
+```
+
+### Added support for translations (i18n)
+
+Now the library messages can be translated into any language.
+
+See {ref}`Add a translation` on how to contribute with translations.
+
## Minor features in 2.0
diff --git a/docs/states.md b/docs/states.md
index b7a50edd..e0910390 100644
--- a/docs/states.md
+++ b/docs/states.md
@@ -1,19 +1,12 @@
# States
-State, as the name says, holds the representation of a state in a statemachine.
+{ref}`State`, as the name says, holds the representation of a state in a {ref}`StateMachine`.
-## State
+## Initial state
-```{eval-rst}
-.. autoclass:: statemachine.state.State
- :members:
-```
-
-### Initial state
-
-A StateMachine should have one and only one `initial` state.
+A {ref}`StateMachine` should have one and only one `initial` {ref}`state`.
The initial {ref}`state` is entered when the machine starts and the corresponding entering
@@ -21,7 +14,7 @@ state {ref}`actions` are called if defined.
(final-state)=
-### Final state
+## Final state
You can explicitly set final states.
diff --git a/docs/transitions.md b/docs/transitions.md
index 978e6464..c4d5c24d 100644
--- a/docs/transitions.md
+++ b/docs/transitions.md
@@ -43,9 +43,9 @@ In line 18, you can say that this code defines three transitions:
And these transitions are assigned to the {ref}`event` `cycle` defined at the class level.
-## Transition
+## Transitions
-In an executing state machine, a transition is a transfer from one state to another. In a state machine, a transition tells us what happens when an {ref}`event` occurs.
+In an executing state machine, a {ref}`transition` is a transfer from one state to another. In a {ref}`statemachine`, a {ref}`transition` tells us what happens when an {ref}`event` occurs.
```{tip}
@@ -59,11 +59,6 @@ An action associated with an event (before, on, after), will be assigned to all
bounded that uses the event as trigger.
```
-```{eval-rst}
-.. autoclass:: statemachine.transition.Transition
- :members:
-```
-
```{hint}
Usually you don't need to import and use a {ref}`transition` class directly in your code,
one of the most powerful features of this library is how transitions and events can be expressed
diff --git a/statemachine/event.py b/statemachine/event.py
index aa1f5bc5..3bdb1386 100644
--- a/statemachine/event.py
+++ b/statemachine/event.py
@@ -29,6 +29,7 @@ def trigger_wrapper():
return machine._process(trigger_wrapper)
def _trigger(self, trigger_data: TriggerData):
+ event_data = None
state = trigger_data.machine.current_state
for transition in state.transitions:
if not transition.match(trigger_data.event):
@@ -39,9 +40,10 @@ def _trigger(self, trigger_data: TriggerData):
event_data.executed = True
break
else:
- raise TransitionNotAllowed(trigger_data.event, state)
+ if not trigger_data.machine.allow_event_without_transition:
+ raise TransitionNotAllowed(trigger_data.event, state)
- return event_data.result
+ return event_data.result if event_data else None
def trigger_event_factory(event):
diff --git a/statemachine/event_data.py b/statemachine/event_data.py
index 429366b0..d985db40 100644
--- a/statemachine/event_data.py
+++ b/statemachine/event_data.py
@@ -12,17 +12,18 @@
@dataclass
class TriggerData:
machine: "StateMachine"
+
event: str
"""The Event that was triggered."""
model: Any = field(init=False)
- """A reference to the underlying model that holds the current State."""
+ """A reference to the underlying model that holds the current :ref:`State`."""
args: tuple = field(default_factory=tuple)
- """All positional arguments provided on the Event."""
+ """All positional arguments provided on the :ref:`Event`."""
kwargs: dict = field(default_factory=dict)
- """All keyword arguments provided on the Event."""
+ """All keyword arguments provided on the :ref:`Event`."""
def __post_init__(self):
self.model = self.machine.model
@@ -31,17 +32,19 @@ def __post_init__(self):
@dataclass
class EventData:
trigger_data: TriggerData
+ """The :ref:`TriggerData` of the :ref:`event`."""
+
transition: "Transition"
- """The Transition instance that was activated by the Event."""
+ """The :ref:`Transition` instance that was activated by the :ref:`Event`."""
state: "State" = field(init=False)
- """The current State of the state machine."""
+ """The current :ref:`State` of the :ref:`statemachine`."""
source: "State" = field(init=False)
- """The State the state machine was in when the Event started."""
+ """The :ref:`State` which :ref:`statemachine` was in when the Event started."""
target: "State" = field(init=False)
- """The destination State of the transition."""
+ """The destination :ref:`State` of the :ref:`transition`."""
result: "Any | None" = None
executed: bool = False
diff --git a/statemachine/exceptions.py b/statemachine/exceptions.py
index 3a4887c4..f389b4f4 100644
--- a/statemachine/exceptions.py
+++ b/statemachine/exceptions.py
@@ -23,7 +23,7 @@ class AttrNotFound(InvalidDefinition):
class TransitionNotAllowed(StateMachineError):
- "The transition can't run from the current state."
+ "Raised when there's no transition that can run from the current :ref:`state`."
def __init__(self, event, state):
self.event = event
diff --git a/statemachine/state.py b/statemachine/state.py
index eea938f8..b0e4e7e0 100644
--- a/statemachine/state.py
+++ b/statemachine/state.py
@@ -10,7 +10,7 @@
class State:
"""
- A State in a state machine describes a particular behavior of the machine.
+ A State in a :ref:`StateMachine` describes a particular behavior of the machine.
When we say that a machine is “in” a state, it means that the machine behaves
in the way that state describes.
diff --git a/statemachine/statemachine.py b/statemachine/statemachine.py
index 8c7542ab..b5c66856 100644
--- a/statemachine/statemachine.py
+++ b/statemachine/statemachine.py
@@ -22,12 +22,45 @@
class StateMachine(metaclass=StateMachineMetaclass):
+ """
- TransitionNotAllowed = TransitionNotAllowed # shortcut for handling exceptions
+ Args:
+ model: An optional external object to store state. See :ref:`domain models`.
+
+ state_field (str): The model's field which stores the current state.
+ Default: ``state``.
+
+ start_value: An optional start state value if there's no current state assigned
+ on the :ref:`domain models`. Default: ``None``.
+
+ rtc (bool): Controls the :ref:`processing model`. Defaults to ``True``
+ that corresponds to a **run-to-completion** (RTC) model.
+
+ allow_event_without_transition: If ``False`` when an event does not result in a transition,
+ an exception ``TransitionNotAllowed`` will be raised.
+ If ``True`` the state machine allows triggering events that may not lead to a state
+ :ref:`transition`, including tolerance to unknown :ref:`event` triggers.
+ Default: ``False``.
+
+ """
+
+ TransitionNotAllowed = TransitionNotAllowed
+ """Shortcut for easy exception handling.
+
+ Example::
+
+ try:
+ sm.send("an-inexistent-event")
+ except sm.TransitionNotAllowed:
+ pass
+ """
_events: Dict[Any, Any] = {}
states: List["State"] = []
+ """List of all state machine :ref:`states`."""
+
states_map: Dict[Any, "State"] = {}
+ """Map of ``state.value`` to the corresponding :ref:`state`."""
def __init__(
self,
@@ -35,10 +68,12 @@ def __init__(
state_field: str = "state",
start_value: Any = None,
rtc: bool = True,
+ allow_event_without_transition: bool = False,
):
self.model = model if model else Model()
self.state_field = state_field
self.start_value = start_value
+ self.allow_event_without_transition = allow_event_without_transition
self.__rtc = rtc
self.__processing: bool = False
self._external_queue: deque = deque()
@@ -54,7 +89,7 @@ def __init__(
self._activate_initial_state(initial_transition)
def __repr__(self):
- current_state_id = self.current_state.id if self.current_state else None
+ current_state_id = self.current_state.id if self.current_state_value else None
return (
f"{type(self).__name__}(model={self.model!r}, state_field={self.state_field!r}, "
f"current_state={current_state_id!r})"
@@ -126,6 +161,16 @@ def _setup(self, initial_transition):
self.add_observer(machine, model)
def add_observer(self, *observers):
+ """Add an observer.
+
+ Observers are a way to generically add behavior to a :ref:`StateMachine` without changing
+ its internal implementation.
+
+ .. seealso::
+
+ :ref:`observers`.
+ """
+
resolvers = [resolver_factory(ObjectConfig.from_obj(o)) for o in observers]
self._visit_states_and_transitions(lambda x: x._add_observer(*resolvers))
return self
@@ -143,6 +188,11 @@ def _graph(self):
@property
def current_state_value(self):
+ """Get/Set the current :ref:`state` value.
+
+ This is a low level API, that can be used to assign any valid state value
+ completely bypassing all the hooks and validations.
+ """
value = getattr(self.model, self.state_field, None)
return value
@@ -153,8 +203,13 @@ def current_state_value(self, value):
setattr(self.model, self.state_field, value)
@property
- def current_state(self):
- return self.states_map.get(self.current_state_value, None)
+ def current_state(self) -> "State":
+ """Get/Set the current :ref:`state`.
+
+ This is a low level API, that can be to assign any valid state
+ completely bypassing all the hooks and validations.
+ """
+ return self.states_map[self.current_state_value]
@current_state.setter
def current_state(self, value):
@@ -166,7 +221,7 @@ def events(self):
@property
def allowed_events(self):
- "get the callable proxy of the current allowed events"
+ """List of the current allowed events."""
return [
getattr(self, event)
for event in self.current_state.transitions.unique_events
@@ -257,5 +312,12 @@ def _activate(self, event_data: EventData):
return result
def send(self, event, *args, **kwargs):
+ """Send an :ref:`Event` to the state machine.
+
+ .. seealso::
+
+ See: :ref:`triggering events`.
+
+ """
event = Event(event)
return event.trigger(self, *args, **kwargs)
diff --git a/tests/examples/order_control_rich_model_machine.py b/tests/examples/order_control_rich_model_machine.py
index 167bb5c0..47a040e2 100644
--- a/tests/examples/order_control_rich_model_machine.py
+++ b/tests/examples/order_control_rich_model_machine.py
@@ -49,8 +49,8 @@ class OrderControl(StateMachine):
# %%
-# Testing
-# -------
+# Testing OrderControl
+# --------------------
#
# Let's first try to create a statemachine instance, using the default dummy model that doesn't
# have the needed methods to complete the state machine. Since the required methods will not be
diff --git a/tests/examples/persistent_model_machine.py b/tests/examples/persistent_model_machine.py
index 77f47285..ddf86f2f 100644
--- a/tests/examples/persistent_model_machine.py
+++ b/tests/examples/persistent_model_machine.py
@@ -81,7 +81,7 @@ def _write_state(self, value):
# %%
# FilePersistentModel: Concrete model strategy
-# -----------------------
+# --------------------------------------------
#
# A concrete implementation of the generic storage protocol above, that reads and writes to a file
# in plain text.
diff --git a/tests/test_transitions.py b/tests/test_transitions.py
index 1ac7b97f..e553f900 100644
--- a/tests/test_transitions.py
+++ b/tests/test_transitions.py
@@ -231,3 +231,19 @@ class TestStateMachine(StateMachine):
final = State(final=True)
execute = initial.to(initial, final, internal=True)
+
+
+class TestAllowEventWithoutTransition:
+ def test_send_unknown_event(self, classic_traffic_light_machine):
+ sm = classic_traffic_light_machine(allow_event_without_transition=True)
+ assert sm.green.is_active
+ sm.send("unknow_event")
+ assert sm.green.is_active
+
+ def test_send_not_valid_for_the_current_state_event(
+ self, classic_traffic_light_machine
+ ):
+ sm = classic_traffic_light_machine(allow_event_without_transition=True)
+ assert sm.green.is_active
+ sm.stop()
+ assert sm.green.is_active