Skip to content

Commit c886252

Browse files
committed
release 0.6.6
1 parent d6b5a26 commit c886252

File tree

6 files changed

+73
-21
lines changed

6 files changed

+73
-21
lines changed

Changelog.md

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
# Changelog
22

3-
## 0.6.6 ()
3+
## 0.6.6 (May, 2018)
44

5-
- `HierarchicalMachine` now considers the initial state of `NestedState` instances/names passed to `initial`.
5+
Release 0.6.6 is a minor release and contains several bugfixes and new features:
6+
7+
- Bugfix: `HierarchicalMachine` now considers the initial state of `NestedState` instances/names passed to `initial`.
68
- Bugfix: `HierarchicalMachine` used to ignore children when `NestedStates` were added to the machine.
7-
- Feature: Added `Machine.dispatch` to trigger events on all models assigned to `Machine`.
9+
- Bugfix #300: Fixed missing brackets in `TimeoutState` (thanks @Synss)
10+
- Feature #289: Introduced `Machine.resolve_callable(func, event_data)` to enable customization of callback definitions (thanks @ollamh and @paulbovbel)
11+
- Feature #299: Added support for internal transitions with `dest=None` (thanks @maueki)
12+
- Feature: Added `Machine.dispatch` to trigger events on all models assigned to `Machine`
813

914
## 0.6.5 (April, 2018)
1015

1116
Release 0.6.5 is a minor release and contains a new feature and a bugfix:
1217

1318
- Feature #287: Embedding `HierarchicalMachine` will now reuse the machine's `initial` state. Passing `initial: False` overrides this (thanks @mrjogo).
14-
- Bugfix #292: Models using `GraphMashine` were not picklable in the past due to `graph` property. Graphs for each model are now stored in `GraphMachine.model_graphs` (thanks @ansumanm)
19+
- Bugfix #292: Models using `GraphMashine` were not picklable in the past due to `graph` property. Graphs for each model are now stored in `GraphMachine.model_graphs` (thanks @ansumanm).
1520

1621
## 0.6.4 (January, 2018)
1722

README.md

+42-2
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ A lightweight, object-oriented state machine implementation in Python. Compatibl
3030
- [Automatic transitions](#automatic-transitions-for-all-states)
3131
- [Transitioning from multiple states](#transitioning-from-multiple-states)
3232
- [Reflexive transitions from multiple states](#reflexive-from-multiple-states)
33+
- [Internal transitions](#internal-transitions)
3334
- [Ordered transitions](#ordered-transitions)
3435
- [Queued transitions](#queued-transitions)
3536
- [Conditional transitions](#conditional-transitions)
3637
- [Callbacks](#transition-callbacks)
37-
- [Execution order](#execution-order)
38+
- [Callback resolution and execution order](#execution-order)
3839
- [Passing data](#passing-data)
3940
- [Alternative initialization patterns](#alternative-initialization-patterns)
4041
- [Logging](#logging)
@@ -463,6 +464,15 @@ machine.add_transition('touch', ['liquid', 'gas', 'plasma'], '=', after='change_
463464

464465
This will add reflexive transitions for all three states with `touch()` as trigger and with `change_shape` executed after each trigger.
465466

467+
#### <a name="internal-transitions"></a>Internal transitions
468+
In contrast to reflexive transitions, internal transitions will never actually leave the state.
469+
This means that transition-related callbacks such as `before` or `after` will be processed while state-related callbacks `exit` or `enter` will not.
470+
To define a transition to be internal, set the destination to `None`.
471+
472+
```python
473+
machine.add_transition('internal', ['liquid', 'gas'], None, after='change_shape')
474+
```
475+
466476
#### <a name="ordered-transitions"></a> Ordered transitions
467477
A common desire is for state transitions to follow a strict linear sequence. For instance, given states `['A', 'B', 'C']`, you might want valid transitions for `A``B`, `B``C`, and `C``A` (but no other pairs).
468478

@@ -675,7 +685,37 @@ print(lump.state)
675685
>>> initial
676686
```
677687

678-
### <a name="execution-order"> Execution order
688+
### <a name="execution-order"> Callback resolution and execution order
689+
690+
As you have probably already realized, the standard way of passing callbacks to states and transitions is by name.
691+
When processing callbacks, Transitions will use the name to retrieve the related callback from the model.
692+
If the method cannot be retrieved and it contains dots, Transitions will treat the name as a path to a module function and try to import it.
693+
Alternatively, you can pass callables such as (bound) functions directly.
694+
695+
```python
696+
from transitions import Machine
697+
from mod import imported_func
698+
699+
700+
class Model(object):
701+
702+
def a_callback(self):
703+
imported_func()
704+
705+
706+
model = Model()
707+
machine = Machine(model=model, states=['A'], initial='A')
708+
machine.add_transition('by_name', 'A', 'A', after='a_callback')
709+
machine.add_transition('by_reference', 'A', 'A', after=model.a_callback)
710+
machine.add_transition('imported', 'A', 'A', after='mod.imported_func')
711+
712+
model.by_name()
713+
model.by_reference()
714+
model.imported()
715+
```
716+
The callback resolution is done in `Machine.resolve_callbacks`.
717+
This method can be overridden in case more complex callback resolution strategies are required.
718+
679719
In summary, callbacks on transitions are executed in the following order:
680720

681721
| Callback | Current State | Comments |

tests/test_graphing.py

+6
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ def test_to_method_filtering(self):
187187
self.assertEqual(len(m2.get_graph().get_edge('B', 'A')), 2)
188188
self.assertEqual(m2.get_graph().get_edge('A', 'B').attr['label'], 'to_B')
189189

190+
def test_loops(self):
191+
m = self.machine_cls(states=['A'], initial='A')
192+
m.add_transition('reflexive', 'A', '=')
193+
m.add_transition('fixed', 'A', None)
194+
g1 = m.get_graph()
195+
190196
def test_roi(self):
191197
m = self.machine_cls(states=['A', 'B', 'C', 'D', 'E', 'F'], initial='A')
192198
m.add_transition('to_state_A', 'B', 'A')

transitions/core.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -259,19 +259,19 @@ def execute(self, event_data):
259259
machine.callback(func, event_data)
260260
_LOGGER.debug("%sExecuted callback '%s' before transition.", event_data.machine.name, func)
261261

262-
self._change_state(event_data)
262+
if self.dest: # if self.dest is None this is an internal transition with no actual state change
263+
self._change_state(event_data)
263264

264265
for func in itertools.chain(self.after, machine.after_state_change):
265266
machine.callback(func, event_data)
266267
_LOGGER.debug("%sExecuted callback '%s' after transition.", event_data.machine.name, func)
267268
return True
268269

269270
def _change_state(self, event_data):
270-
if self.dest: # if self.dest is None this is an internal transition with no actual state change
271-
event_data.machine.get_state(self.source).exit(event_data)
272-
event_data.machine.set_state(self.dest, event_data.model)
273-
event_data.update(event_data.model)
274-
event_data.machine.get_state(self.dest).enter(event_data)
271+
event_data.machine.get_state(self.source).exit(event_data)
272+
event_data.machine.set_state(self.dest, event_data.model)
273+
event_data.update(event_data.model)
274+
event_data.machine.get_state(self.dest).enter(event_data)
275275

276276
def add_callback(self, trigger, func):
277277
""" Add a new before, after, or prepare callback.

transitions/extensions/diagrams.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,11 @@ def _add_edges(self, events, container):
124124
src = transitions[0]
125125
edge_attr = {}
126126
for trans in transitions[1]:
127-
dst = trans.dest
127+
if trans.dest is None:
128+
dst = src
129+
label += " [internal]"
130+
else:
131+
dst = trans.dest
128132
edge_attr['label'] = self._transition_label(label, trans)
129133
if container.has_edge(src, dst):
130134
edge = container.get_edge(src, dst)
@@ -241,12 +245,14 @@ def _add_edges(self, events, container):
241245
if trans in self.seen_transitions:
242246
continue
243247
if trans.dest is None:
248+
dst = src
249+
label += " [internal]"
250+
elif not container.has_node(trans.dest) and _get_subgraph(container, 'cluster_' + trans.dest) is None:
244251
continue
245-
if not container.has_node(trans.dest) and _get_subgraph(container, 'cluster_' + trans.dest) is None:
246-
continue
252+
else:
253+
dst = self.machine.get_state(trans.dest)
247254

248255
self.seen_transitions.append(trans)
249-
dst = self.machine.get_state(trans.dest)
250256
if dst.children:
251257
if not src.is_substate_of(dst.name):
252258
edge_attr['lhead'] = "cluster_" + dst.name
@@ -274,9 +280,6 @@ class TransitionGraphSupport(Transition):
274280
"""
275281

276282
def _change_state(self, event_data):
277-
if self.dest is None:
278-
return
279-
280283
machine = event_data.machine
281284
model = event_data.model
282285
dest = machine.get_state(self.dest)

transitions/extensions/nesting.py

-2
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,6 @@ def execute(self, event_data):
211211

212212
# The actual state change method 'execute' in Transition was restructured to allow overriding
213213
def _change_state(self, event_data):
214-
if self.dest is None:
215-
return
216214
machine = event_data.machine
217215
model = event_data.model
218216
dest_state = machine.get_state(self.dest)

0 commit comments

Comments
 (0)