Skip to content

Commit

Permalink
tests, readme, state machine without model
Browse files Browse the repository at this point in the history
  • Loading branch information
fgmacedo committed Mar 23, 2017
1 parent 6576d33 commit a193f4f
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 57 deletions.
12 changes: 6 additions & 6 deletions CONTRIBUTING.rst
Expand Up @@ -57,17 +57,17 @@ If you are proposing a feature:
Get Started!
------------

Ready to contribute? Here's how to set up `statemachine` for local development.
Ready to contribute? Here's how to set up `python-statemachine` for local development.

1. Fork the `statemachine` repo on GitHub.
1. Fork the `python-statemachine` repo on GitHub.
2. Clone your fork locally::

$ git clone git@github.com:your_name_here/statemachine.git
$ git clone git@github.com:your_name_here/python-statemachine.git

3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::

$ mkvirtualenv statemachine
$ cd statemachine/
$ mkvirtualenv python-statemachine
$ cd python-statemachine/
$ python setup.py develop

4. Create a branch for local development::
Expand Down Expand Up @@ -101,7 +101,7 @@ Before you submit a pull request, check that it meets these guidelines:
2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and add the
feature to the list in README.rst.
3. The pull request should work for Python 2.6, 2.7, 3.3, 3.4 and 3.5, and for PyPy. Check
3. The pull request should work for Python 2.7, 3.3, 3.4 and 3.5. Check
https://travis-ci.org/fgmacedo/python-statemachine/pull_requests
and make sure that the tests pass for all supported Python versions.

Expand Down
15 changes: 15 additions & 0 deletions HISTORY.rst
Expand Up @@ -2,6 +2,21 @@
History
=======

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)
------------------

Expand Down
126 changes: 122 additions & 4 deletions README.rst
Expand Up @@ -18,14 +18,132 @@ Python State Machine
:alt: Updates


Python Finite State Machine made easy.
Python finite-state machines made easy.


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


Features
--------
Getting started
===============

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

.. code-block:: console
$ pip install python-statemachine
Import the statemachine::

from statemachine import StateMachine, State


Define your state machine::


class TrafficLightMachine(StateMachine):
green = State('Green', initial=True)
yellow = State('Yellow')
red = State('Red')

slowdown = green.to(yellow)
stop = yellow.to(green)
go = red.to(green)


You can now create an instance::

>>> machine = TrafficLightMachine()

And inspect about the current state::

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

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

>>> machine.is_green
True
>>> machine.is_yellow
False
>>> machine.is_red
False

Query about metadata::

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

Call a transition::

>>> machine.slowdown()

And check for the current status::

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

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

>>> machine.is_yellow
True
>>> machine.slowdown()
Traceback (most recent call last):
...
LookupError: Can't slowdown when in Yellow.

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

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

A state machine can be instantiated with an initial value::

>>> machine = TrafficLightMachine(start_value='red')
>>> machine.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')
>>> machine = TrafficLightMachine(obj)
>>> machine.is_red
True
>>> obj.state
'red'
>>> obj.state = 'green'
>>> machine.is_green
True
>>> machine.slowdown()
>>> obj.state
'yellow'
>>> machine.is_yellow
True


Events
------

Docs needed.
12 changes: 6 additions & 6 deletions docs/installation.rst
Expand Up @@ -12,9 +12,9 @@ To install Python State Machine, run this command in your terminal:

.. code-block:: console
$ pip install statemachine
$ pip install python-statemachine
This is the preferred method to install Python State Machine, as it will always install the most recent stable release.
This is the preferred method to install Python State Machine, as it will always install the most recent stable release.

If you don't have `pip`_ installed, this `Python installation guide`_ can guide
you through the process.
Expand All @@ -32,13 +32,13 @@ You can either clone the public repository:

.. code-block:: console
$ git clone git://github.com/fgmacedo/statemachine
$ git clone git://github.com/fgmacedo/python-statemachine
Or download the `tarball`_:

.. code-block:: console
$ curl -OL https://github.com/fgmacedo/statemachine/tarball/master
$ curl -OL https://github.com/fgmacedo/python-statemachine/tarball/master
Once you have a copy of the source, you can install it with:

Expand All @@ -47,5 +47,5 @@ Once you have a copy of the source, you can install it with:
$ python setup.py install
.. _Github repo: https://github.com/fgmacedo/statemachine
.. _tarball: https://github.com/fgmacedo/statemachine/tarball/master
.. _Github repo: https://github.com/fgmacedo/python-statemachine
.. _tarball: https://github.com/fgmacedo/python-statemachine/tarball/master
70 changes: 46 additions & 24 deletions statemachine/statemachine.py
Expand Up @@ -53,22 +53,22 @@ def __call__(self, *args, **kwargs):

class Transition(object):

def __init__(self, source, destination, key=None, validators=None):
def __init__(self, source, destination, identifier=None, validators=None):
self.source = source
self.destination = destination
self.key = key
self.identifier = identifier
self.validators = validators or []

def __repr__(self):
return "{}({!r}, {!r}, key={!r})".format(
type(self).__name__, self.source, self.destination, self.key)
return "{}({!r}, {!r}, identifier={!r})".format(
type(self).__name__, self.source, self.destination, self.identifier)

def __or__(self, other):
return CombinedTransition(self, other, key=self.key)
return CombinedTransition(self, other, identifier=self.identifier)

def __contribute_to_class__(self, managed, key):
def __contribute_to_class__(self, managed, identifier):
self.managed = managed
self.key = key
self.identifier = identifier

def __get__(self, instance, owner):
def callable(*args, **kwargs):
Expand All @@ -85,7 +85,12 @@ def _can_run(self, instance):

def _run(self, instance, *args, **kwargs):
if not self._can_run(instance):
raise LookupError(_("Transition is not supported."))
raise LookupError(
_("Can't {} when in {}.").format(
self.identifier,
instance.current_state.name
)
)

self._validate(*args, **kwargs)
return instance._activate(self, *args, **kwargs)
Expand All @@ -105,17 +110,22 @@ def _left(self):
def _right(self):
return self.destination

def __contribute_to_class__(self, managed, key):
super(CombinedTransition, self).__contribute_to_class__(managed, key)
self._left.__contribute_to_class__(managed, key)
self._right.__contribute_to_class__(managed, key)
def __contribute_to_class__(self, managed, identifier):
super(CombinedTransition, self).__contribute_to_class__(managed, identifier)
self._left.__contribute_to_class__(managed, identifier)
self._right.__contribute_to_class__(managed, identifier)

def _can_run(self, instance):
return instance.current_state in [self._left.source, self._right.source]

def _run(self, instance, *args, **kwargs):
if not self._can_run(instance):
raise LookupError(_("Transition is not supported."))
raise LookupError(
_("Can't {} when in {}.").format(
self.identifier,
instance.current_state.name
)
)

self._validate(*args, **kwargs)
transition = self._left if instance.current_state == self._left.source else self._right
Expand Down Expand Up @@ -184,11 +194,19 @@ def __init__(cls, name, bases, attrs):
cls.states_map = {s.value: s for s in cls.states}


class Model(object):
state = None

def __repr__(self):
return 'Model(state={})'.format(self.state)


class BaseStateMachine(object):

def __init__(self, model, state_field='state'):
self.model = model
def __init__(self, model=None, state_field='state', start_value=None):
self.model = model if model else Model()
self.state_field = state_field
self.start_value = start_value

self.check()

Expand All @@ -214,7 +232,10 @@ def check(self):
self.initial_state = initials[0]

if self.current_state_value is None:
self.current_state_value = self.initial_state.value
if self.start_value:
self.current_state_value = self.start_value
else:
self.current_state_value = self.initial_state.value

@property
def current_state_value(self):
Expand All @@ -223,7 +244,7 @@ def current_state_value(self):
@current_state_value.setter
def current_state_value(self, value):
if value not in self.states_map:
raise Exception(_("{!r} is not a valid state value.").format(value))
raise ValueError(_("{!r} is not a valid state value.").format(value))
setattr(self.model, self.state_field, value)

@property
Expand All @@ -234,7 +255,7 @@ def current_state(self):
def allowed_transitions(self):
"get the callable proxy of the current allowed transitions"
return [
getattr(self, t.key)
getattr(self, t.identifier)
for t in self.current_state.transitions if t._can_run(self)
]

Expand All @@ -243,19 +264,20 @@ def current_state(self, value):
self.current_state_value = value.value

def _activate(self, transition, *args, **kwargs):
on_event = getattr(self, 'on_{}'.format(transition.key), None)
on_event = getattr(self, 'on_{}'.format(transition.identifier), None)
result = on_event(*args, **kwargs) if callable(on_event) else None
self.current_state = transition.destination
return result

def get_transition(self, transition_key):
transition = getattr(self, transition_key, None)
def get_transition(self, transition_identifier):
transition = getattr(self, transition_identifier, None)
if not hasattr(transition, 'source') or not callable(transition):
raise ValueError('{!r} is not a valid transition key'.format(transition_key))
raise ValueError(
'{!r} is not a valid transition identifier'.format(transition_identifier))
return transition

def run(self, transition_key, *args, **kwargs):
transition = self.get_transition(transition_key)
def run(self, transition_identifier, *args, **kwargs):
transition = self.get_transition(transition_identifier)
return transition(*args, **kwargs)


Expand Down

0 comments on commit a193f4f

Please sign in to comment.