Skip to content

Commit a193f4f

Browse files
committed
tests, readme, state machine without model
1 parent 6576d33 commit a193f4f

6 files changed

Lines changed: 256 additions & 57 deletions

File tree

‎CONTRIBUTING.rst‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,17 @@ If you are proposing a feature:
5757
Get Started!
5858
------------
5959

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

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

65-
$ git clone [email protected]:your_name_here/statemachine.git
65+
$ git clone [email protected]:your_name_here/python-statemachine.git
6666

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

69-
$ mkvirtualenv statemachine
70-
$ cd statemachine/
69+
$ mkvirtualenv python-statemachine
70+
$ cd python-statemachine/
7171
$ python setup.py develop
7272

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

‎HISTORY.rst‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22
History
33
=======
44

5+
0.3.0 (2017-03-22)
6+
------------------
7+
8+
* README getting started section.
9+
* Tests to state machine without model.
10+
11+
12+
0.2.0 (2017-03-22)
13+
------------------
14+
15+
* ``State`` can hold a value that will be assigned to the model as the state value.
16+
* Travis-CI integration.
17+
* RTD integration.
18+
19+
520
0.1.0 (2017-03-21)
621
------------------
722

‎README.rst‎

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,132 @@ Python State Machine
1818
:alt: Updates
1919

2020

21-
Python Finite State Machine made easy.
21+
Python finite-state machines made easy.
2222

2323

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

2727

28-
Features
29-
--------
28+
Getting started
29+
===============
3030

31-
* TODO
31+
To install Python State Machine, run this command in your terminal:
32+
33+
.. code-block:: console
34+
35+
$ pip install python-statemachine
36+
37+
38+
Import the statemachine::
39+
40+
from statemachine import StateMachine, State
41+
42+
43+
Define your state machine::
44+
45+
46+
class TrafficLightMachine(StateMachine):
47+
green = State('Green', initial=True)
48+
yellow = State('Yellow')
49+
red = State('Red')
50+
51+
slowdown = green.to(yellow)
52+
stop = yellow.to(green)
53+
go = red.to(green)
54+
55+
56+
You can now create an instance::
57+
58+
>>> machine = TrafficLightMachine()
59+
60+
And inspect about the current state::
61+
62+
>>> machine.current_state
63+
State('Green', identifier='green', value='green', initial=True)
64+
>>> machine.current_state == TrafficLightMachine.green == machine.green
65+
True
66+
67+
For each state, there's a dinamically created property in the form ``is_<state.identifier>``, that
68+
returns ``True`` if the current status matches the query::
69+
70+
>>> machine.is_green
71+
True
72+
>>> machine.is_yellow
73+
False
74+
>>> machine.is_red
75+
False
76+
77+
Query about metadata::
78+
79+
>>> [s.identifier for s in m.states]
80+
['green', 'red', 'yellow']
81+
>>> [t.identifier for t in m.transitions]
82+
['go', 'slowdown', 'stop']
83+
84+
Call a transition::
85+
86+
>>> machine.slowdown()
87+
88+
And check for the current status::
89+
90+
>>> machine.current_state
91+
State('Yellow', identifier='yellow', value='yellow', initial=False)
92+
>>> machine.is_yellow
93+
True
94+
95+
You can't run a transition from an invalid state::
96+
97+
>>> machine.is_yellow
98+
True
99+
>>> machine.slowdown()
100+
Traceback (most recent call last):
101+
...
102+
LookupError: Can't slowdown when in Yellow.
103+
104+
You can also trigger events in an alternative way, calling the ``run(<transition.identificer>)`` method::
105+
106+
>>> machine.is_yellow
107+
True
108+
>>> machine.run('stop')
109+
>>> machine.is_red
110+
True
111+
112+
A state machine can be instantiated with an initial value::
113+
114+
>>> machine = TrafficLightMachine(start_value='red')
115+
>>> machine.is_red
116+
True
117+
118+
119+
Models
120+
------
121+
122+
If you need to persist the current state on another object, or you're using the
123+
state machine to control the flow of another object, you can pass this object
124+
to the ``StateMachine`` constructor::
125+
126+
>>> class MyModel(object):
127+
... def __init__(self, state):
128+
... self.state = state
129+
...
130+
>>> obj = MyModel(state='red')
131+
>>> machine = TrafficLightMachine(obj)
132+
>>> machine.is_red
133+
True
134+
>>> obj.state
135+
'red'
136+
>>> obj.state = 'green'
137+
>>> machine.is_green
138+
True
139+
>>> machine.slowdown()
140+
>>> obj.state
141+
'yellow'
142+
>>> machine.is_yellow
143+
True
144+
145+
146+
Events
147+
------
148+
149+
Docs needed.

‎docs/installation.rst‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ To install Python State Machine, run this command in your terminal:
1212

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

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

3333
.. code-block:: console
3434
35-
$ git clone git://github.com/fgmacedo/statemachine
35+
$ git clone git://github.com/fgmacedo/python-statemachine
3636
3737
Or download the `tarball`_:
3838

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

@@ -47,5 +47,5 @@ Once you have a copy of the source, you can install it with:
4747
$ python setup.py install
4848
4949
50-
.. _Github repo: https://github.com/fgmacedo/statemachine
51-
.. _tarball: https://github.com/fgmacedo/statemachine/tarball/master
50+
.. _Github repo: https://github.com/fgmacedo/python-statemachine
51+
.. _tarball: https://github.com/fgmacedo/python-statemachine/tarball/master

‎statemachine/statemachine.py‎

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,22 @@ def __call__(self, *args, **kwargs):
5353

5454
class Transition(object):
5555

56-
def __init__(self, source, destination, key=None, validators=None):
56+
def __init__(self, source, destination, identifier=None, validators=None):
5757
self.source = source
5858
self.destination = destination
59-
self.key = key
59+
self.identifier = identifier
6060
self.validators = validators or []
6161

6262
def __repr__(self):
63-
return "{}({!r}, {!r}, key={!r})".format(
64-
type(self).__name__, self.source, self.destination, self.key)
63+
return "{}({!r}, {!r}, identifier={!r})".format(
64+
type(self).__name__, self.source, self.destination, self.identifier)
6565

6666
def __or__(self, other):
67-
return CombinedTransition(self, other, key=self.key)
67+
return CombinedTransition(self, other, identifier=self.identifier)
6868

69-
def __contribute_to_class__(self, managed, key):
69+
def __contribute_to_class__(self, managed, identifier):
7070
self.managed = managed
71-
self.key = key
71+
self.identifier = identifier
7272

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

8686
def _run(self, instance, *args, **kwargs):
8787
if not self._can_run(instance):
88-
raise LookupError(_("Transition is not supported."))
88+
raise LookupError(
89+
_("Can't {} when in {}.").format(
90+
self.identifier,
91+
instance.current_state.name
92+
)
93+
)
8994

9095
self._validate(*args, **kwargs)
9196
return instance._activate(self, *args, **kwargs)
@@ -105,17 +110,22 @@ def _left(self):
105110
def _right(self):
106111
return self.destination
107112

108-
def __contribute_to_class__(self, managed, key):
109-
super(CombinedTransition, self).__contribute_to_class__(managed, key)
110-
self._left.__contribute_to_class__(managed, key)
111-
self._right.__contribute_to_class__(managed, key)
113+
def __contribute_to_class__(self, managed, identifier):
114+
super(CombinedTransition, self).__contribute_to_class__(managed, identifier)
115+
self._left.__contribute_to_class__(managed, identifier)
116+
self._right.__contribute_to_class__(managed, identifier)
112117

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

116121
def _run(self, instance, *args, **kwargs):
117122
if not self._can_run(instance):
118-
raise LookupError(_("Transition is not supported."))
123+
raise LookupError(
124+
_("Can't {} when in {}.").format(
125+
self.identifier,
126+
instance.current_state.name
127+
)
128+
)
119129

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

186196

197+
class Model(object):
198+
state = None
199+
200+
def __repr__(self):
201+
return 'Model(state={})'.format(self.state)
202+
203+
187204
class BaseStateMachine(object):
188205

189-
def __init__(self, model, state_field='state'):
190-
self.model = model
206+
def __init__(self, model=None, state_field='state', start_value=None):
207+
self.model = model if model else Model()
191208
self.state_field = state_field
209+
self.start_value = start_value
192210

193211
self.check()
194212

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

216234
if self.current_state_value is None:
217-
self.current_state_value = self.initial_state.value
235+
if self.start_value:
236+
self.current_state_value = self.start_value
237+
else:
238+
self.current_state_value = self.initial_state.value
218239

219240
@property
220241
def current_state_value(self):
@@ -223,7 +244,7 @@ def current_state_value(self):
223244
@current_state_value.setter
224245
def current_state_value(self, value):
225246
if value not in self.states_map:
226-
raise Exception(_("{!r} is not a valid state value.").format(value))
247+
raise ValueError(_("{!r} is not a valid state value.").format(value))
227248
setattr(self.model, self.state_field, value)
228249

229250
@property
@@ -234,7 +255,7 @@ def current_state(self):
234255
def allowed_transitions(self):
235256
"get the callable proxy of the current allowed transitions"
236257
return [
237-
getattr(self, t.key)
258+
getattr(self, t.identifier)
238259
for t in self.current_state.transitions if t._can_run(self)
239260
]
240261

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

245266
def _activate(self, transition, *args, **kwargs):
246-
on_event = getattr(self, 'on_{}'.format(transition.key), None)
267+
on_event = getattr(self, 'on_{}'.format(transition.identifier), None)
247268
result = on_event(*args, **kwargs) if callable(on_event) else None
248269
self.current_state = transition.destination
249270
return result
250271

251-
def get_transition(self, transition_key):
252-
transition = getattr(self, transition_key, None)
272+
def get_transition(self, transition_identifier):
273+
transition = getattr(self, transition_identifier, None)
253274
if not hasattr(transition, 'source') or not callable(transition):
254-
raise ValueError('{!r} is not a valid transition key'.format(transition_key))
275+
raise ValueError(
276+
'{!r} is not a valid transition identifier'.format(transition_identifier))
255277
return transition
256278

257-
def run(self, transition_key, *args, **kwargs):
258-
transition = self.get_transition(transition_key)
279+
def run(self, transition_identifier, *args, **kwargs):
280+
transition = self.get_transition(transition_identifier)
259281
return transition(*args, **kwargs)
260282

261283

0 commit comments

Comments
 (0)