Metadata-Version: 2.1
Name: python-statemachine
Version: 0.9.0
Summary: Python Finite State Machines made easy.
Home-page: https://github.com/fgmacedo/python-statemachine
Author: Fernando Macedo
Author-email: fgmacedo@gmail.com
License: MIT license
Keywords: statemachine
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Software Development :: Libraries

====================
Python State Machine
====================


.. image:: https://img.shields.io/pypi/v/python-statemachine.svg
        :target: https://pypi.python.org/pypi/python-statemachine

.. image:: https://img.shields.io/pypi/dm/python-statemachine.svg
        :target: https://pypi.python.org/pypi/python-statemachine

.. image:: https://travis-ci.org/fgmacedo/python-statemachine.svg?branch=develop
        :target: https://travis-ci.org/fgmacedo/python-statemachine
        :alt: Build status

.. image:: https://codecov.io/gh/fgmacedo/python-statemachine/branch/develop/graph/badge.svg
        :target: https://codecov.io/gh/fgmacedo/python-statemachine
        :alt: Coverage report

.. image:: https://readthedocs.org/projects/python-statemachine/badge/?version=latest
        :target: https://python-statemachine.readthedocs.io/en/latest/?badge=latest
        :alt: Documentation Status


Python `finite-state machines <https://en.wikipedia.org/wiki/Finite-state_machine>`_ made easy.


* Free software: MIT license
* Documentation: https://python-statemachine.readthedocs.io.


Getting started
===============

To install Python State Machine, run this command in your terminal:

.. code-block:: console

    $ pip install python-statemachine

Define your state machine:

>>> from statemachine import StateMachine, State

>>> class TrafficLightMachine(StateMachine):
...    green = State('Green', initial=True)
...    yellow = State('Yellow')
...    red = State('Red')
...
...    slowdown = green.to(yellow)
...    stop = yellow.to(red)
...    go = red.to(green)


You can now create an instance:

>>> traffic_light = TrafficLightMachine()

And inspect about the current state:

>>> traffic_light.current_state
State('Green', identifier='green', value='green', initial=True, final=False)
>>> traffic_light.current_state == TrafficLightMachine.green == traffic_light.green
True

For each state, there's a dynamically created property in the form ``is_<state.identifier>``, that
returns ``True`` if the current status matches the query:

>>> traffic_light.is_green
True
>>> traffic_light.is_yellow
False
>>> traffic_light.is_red
False

Query about metadata:

>>> [s.identifier for s in traffic_light.states]
['green', 'red', 'yellow']
>>> [t.identifier for t in traffic_light.transitions]
['go', 'slowdown', 'stop']

Call a transition:

>>> traffic_light.slowdown()

And check for the current status:

>>> traffic_light.current_state
State('Yellow', identifier='yellow', value='yellow', initial=False, final=False)
>>> traffic_light.is_yellow
True

You can't run a transition from an invalid state:

>>> traffic_light.is_yellow
True
>>> traffic_light.slowdown()
Traceback (most recent call last):
statemachine.exceptions.TransitionNotAllowed: Can't slowdown when in Yellow.

You can also trigger events in an alternative way, calling the ``run(<transition.identificer>)`` method:

>>> traffic_light.is_yellow
True
>>> traffic_light.run('stop')
>>> traffic_light.is_red
True

A state machine can be instantiated with an initial value:

>>> machine = TrafficLightMachine(start_value='red')
>>> traffic_light.is_red
True


Models
------

If you need to persist the current state on another object, or you're using the
state machine to control the flow of another object, you can pass this object
to the ``StateMachine`` constructor:

>>> class MyModel(object):
...     def __init__(self, state):
...         self.state = state
...
>>> obj = MyModel(state='red')
>>> traffic_light = TrafficLightMachine(obj)
>>> traffic_light.is_red
True
>>> obj.state
'red'
>>> obj.state = 'green'
>>> traffic_light.is_green
True
>>> traffic_light.slowdown()
>>> obj.state
'yellow'
>>> traffic_light.is_yellow
True


Callbacks
---------

Callbacks when running events:


>>> from statemachine import StateMachine, State

>>> class TrafficLightMachine(StateMachine):
...     "A traffic light machine"
...     green = State('Green', initial=True)
...     yellow = State('Yellow')
...     red = State('Red')
...
...     slowdown = green.to(yellow)
...     stop = yellow.to(red)
...     go = red.to(green)
...
...     def on_slowdown(self):
...         print('Calma, lá!')
...
...     def on_stop(self):
...         print('Parou.')
...
...     def on_go(self):
...         print('Valendo!')


>>> stm = TrafficLightMachine()
>>> stm.slowdown()
Calma, lá!
>>> stm.stop()
Parou.
>>> stm.go()
Valendo!


Or when entering/exiting states:

>>> from statemachine import StateMachine, State

>>> class TrafficLightMachine(StateMachine):
...    "A traffic light machine"
...    green = State('Green', initial=True)
...    yellow = State('Yellow')
...    red = State('Red')
...
...    cycle = green.to(yellow) | yellow.to(red) | red.to(green)
...
...    def on_enter_green(self):
...        print('Valendo!')
...
...    def on_enter_yellow(self):
...        print('Calma, lá!')
...
...    def on_enter_red(self):
...        print('Parou.')

>>> stm = TrafficLightMachine()
>>> stm.cycle()
Calma, lá!
>>> stm.cycle()
Parou.
>>> stm.cycle()
Valendo!

Mixins
------

Your model can inherited from a custom mixin to auto-instantiate a state machine.

>>> from statemachine.mixins import MachineMixin

>>> class CampaignMachineWithKeys(StateMachine):
...     "A workflow machine"
...     draft = State('Draft', initial=True, value=1)
...     producing = State('Being produced', value=2)
...     closed = State('Closed', value=3)
...     cancelled = State('Cancelled', value=4)
...
...     add_job = draft.to.itself() | producing.to.itself()
...     produce = draft.to(producing)
...     deliver = producing.to(closed)
...     cancel = cancelled.from_(draft, producing)


>>> class MyModel(MachineMixin):
...     state_machine_name = 'CampaignMachineWithKeys'
...
...     def __init__(self, **kwargs):
...         for k, v in kwargs.items():
...             setattr(self, k, v)
...         super(MyModel, self).__init__()
...
...     def __repr__(self):
...         return "{}({!r})".format(type(self).__name__, self.__dict__)

>>> model = MyModel(state=1)
>>> assert isinstance(model.statemachine, CampaignMachineWithKeys)
>>> assert model.state == 1
>>> assert model.statemachine.current_state == model.statemachine.draft
>>> model.statemachine.cancel()
>>> assert model.state == 4
>>> assert model.statemachine.current_state == model.statemachine.cancelled

Final States
------------

You can explicitly set final states.
Transitions from these states are not allowed and will raise exception.

>>> class CampaignMachine(StateMachine):
...     "A workflow machine"
...     draft = State('Draft', initial=True, value=1)
...     producing = State('Being produced', value=2)
...     closed = State('Closed', final=True, value=3)
...
...     add_job = draft.to.itself() | producing.to.itself() | closed.to(producing)
...     produce = draft.to(producing)
...     deliver = producing.to(closed)


>>> from statemachine.exceptions import InvalidDefinition
>>> try:
...     machine = CampaignMachine(model)
... except InvalidDefinition as err:
...     print(err)
Final state does not should have defined transitions starting from that state


You can retrieve all final states.

>>> class CampaignMachine(StateMachine):
...     "A workflow machine"
...     draft = State('Draft', initial=True, value=1)
...     producing = State('Being produced', value=2)
...     closed = State('Closed', final=True, value=3)
...
...     add_job = draft.to.itself() | producing.to.itself()
...     produce = draft.to(producing)
...     deliver = producing.to(closed)

>>> model = MyModel(state=3)
>>> machine = CampaignMachine(model)
>>> machine.final_states
[State('Closed', identifier='closed', value=3, initial=False, final=True)]



History
=======

0.9.0 (2022-12-21)
------------------

* This is the last release supporting Python2.X series.
* Doctesting all documentation including README (with issues on examples fixed).
* Fix state value misjudged when state value is an "boolean False value" (tks @the5fire)
* Fix returning dict as result of transitions callback.
* State machine declarations now with final states.
* Args and kwargs now are passed to bounded transitions.


0.8.0 (2020-01-23)
------------------

* Add support for Python 3.7 and 3.8 (adding to test matrix).
* Update development requirements.
* State machine names should now be fully qualified for mixins, simple names are deprecated and
  will no longer be supported on a future version.
* Development: Adding mypy linter.
* Add support for State machine inheritance. Thanks @rschrader.
* Add support for reverse transitions: ``transition = state_a.from_(state_b)``.
  Thanks @romulorosa.
* Fix current state equal to destination on enter events. Thanks @robnils and @joshuacc1.
* Check: StateMachine now validates if it's states/transitions graph has only one component.
  Thanks @rafaelrds.

Breaking changes:

* Drop official support for Python 3.4 (removing from test matrix, code may still work).


0.7.1 (2019-01-18)
------------------

* Fix Django integration for registry loading statemachine modules on Django1.7+.


0.7.0 (2018-04-01)
------------------

* New event callbacks: `on_enter_<state>` and `on_exit_<state>`.

0.6.2 (2017-08-25)
------------------

* Fix README.


0.6.1 (2017-08-25)
------------------

* Fix deploy issues.


0.6.0 (2017-08-25)
------------------

* Auto-discovering `statemachine`/`statemachines` under a Django project when
  they are requested using the mixin/registry feature.

0.5.1 (2017-07-24)
------------------

* Fix bug on ``CombinedTransition._can_run`` not allowing transitions to run if there are more than
  two transitions combined.

0.5.0 (2017-07-13)
------------------

* Custom exceptions.
* Duplicated definition of ``on_execute`` callback is not allowed.
* Fix bug on ``StateMachine.on_<transition.identifier>`` being called with extra ``self`` param.

0.4.2 (2017-07-10)
------------------

* Python 3.6 support.
* Drop official support for Python 3.3.
* `Transition` can be used as decorator for `on_execute` callback definition.
* `Transition` can point to multiple destination states.


0.3.0 (2017-03-22)
------------------

* README getting started section.
* Tests to state machine without model.


0.2.0 (2017-03-22)
------------------

* ``State`` can hold a value that will be assigned to the model as the state value.
* Travis-CI integration.
* RTD integration.


0.1.0 (2017-03-21)
------------------

* First release on PyPI.


