• Home
  • History
  • Annotate
Name Date Size #Lines LOC

..03-May-2022-

binder/H03-May-2022-52

examples/H07-Apr-2021-1,8891,886

tests/H07-Apr-2021-5,1474,055

transitions/H07-Apr-2021-4,9624,125

transitions.egg-info/H03-May-2022-1,8081,398

.coveragercH A D16-Mar-202179 54

.pylintrcH A D03-Apr-202114.8 KiB426292

Changelog.mdH A D07-Apr-202124.3 KiB403289

LICENSEH A D12-Nov-20201.1 KiB2217

MANIFEST.inH A D12-Nov-2020305 1412

PKG-INFOH A D07-Apr-202193.2 KiB1,8081,398

README.mdH A D07-Apr-202181.1 KiB1,8371,419

conftest.pyH A D03-Apr-2021337 1612

pytest.iniH A D16-Mar-2021131 76

setup.cfgH A D07-Apr-2021181 1712

setup.pyH A D30-Dec-20202.2 KiB6152

tox.iniH A D11-Mar-2021523 2420

README.md

1# <a name="transitions-module"></a> transitions
2[![Version](https://img.shields.io/badge/version-v0.8.8-orange.svg)](https://github.com/pytransitions/transitions)
3[![Build Status](https://github.com/pytransitions/transitions/actions/workflows/pytest.yml/badge.svg)](https://github.com/pytransitions/transitions/actions?query=workflow%3Apytest)
4[![Coverage Status](https://coveralls.io/repos/pytransitions/transitions/badge.svg?branch=master&service=github)](https://coveralls.io/github/pytransitions/transitions?branch=master)
5[![PyPi](https://img.shields.io/pypi/v/transitions.svg)](https://pypi.org/project/transitions)
6[![GitHub commits](https://img.shields.io/github/commits-since/pytransitions/transitions/0.8.7.svg)](https://github.com/pytransitions/transitions/compare/0.8.7...master)
7[![License](https://img.shields.io/github/license/pytransitions/transitions.svg)](LICENSE)
8[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pytransitions/transitions/master?filepath=examples%2FPlayground.ipynb)
9<!-- [![Pylint](https://img.shields.io/badge/pylint-9.71%2F10-green.svg)](https://github.com/pytransitions/transitions) -->
10<!--[![Name](Image)](Link)-->
11
12A lightweight, object-oriented state machine implementation in Python with many extensions. Compatible with Python 2.7+ and 3.0+.
13
14## Installation
15
16    pip install transitions
17
18... or clone the repo from GitHub and then:
19
20    python setup.py install
21
22
23## Table of Contents
24- [Quickstart](#quickstart)
25- [Non-Quickstart](#the-non-quickstart)
26    - [Basic initialization](#basic-initialization)
27    - [States](#states)
28        - [Callbacks](#state-callbacks)
29        - [Checking state](#checking-state)
30        - [Enumerations](#enum-state)
31    - [Transitions](#transitions)
32        - [Automatic transitions](#automatic-transitions-for-all-states)
33        - [Transitioning from multiple states](#transitioning-from-multiple-states)
34        - [Reflexive transitions from multiple states](#reflexive-from-multiple-states)
35        - [Internal transitions](#internal-transitions)
36        - [Ordered transitions](#ordered-transitions)
37        - [Queued transitions](#queued-transitions)
38        - [Conditional transitions](#conditional-transitions)
39        - [Callbacks](#transition-callbacks)
40    - [Callable resolution](#resolution)
41    - [Callback execution order](#execution-order)
42    - [Passing data](#passing-data)
43    - [Alternative initialization patterns](#alternative-initialization-patterns)
44    - [Logging](#logging)
45    - [(Re-)Storing machine instances](#restoring)
46    - [Extensions](#extensions)
47        - [Diagrams](#diagrams)
48        - [Hierarchical State Machine](#hsm)
49        - [Threading](#threading)
50        - [Async](#async)
51        - [State features](#state-features)
52        - [Django](#django-support)
53    - [Bug reports etc.](#bug-reports)
54
55
56## Quickstart
57
58They say [a good example is worth](https://www.google.com/webhp?ie=UTF-8#q=%22a+good+example+is+worth%22&start=20) 100 pages of API documentation, a million directives, or a thousand words.
59
60Well, "they" probably lie... but here's an example anyway:
61
62```python
63from transitions import Machine
64import random
65
66class NarcolepticSuperhero(object):
67
68    # Define some states. Most of the time, narcoleptic superheroes are just like
69    # everyone else. Except for...
70    states = ['asleep', 'hanging out', 'hungry', 'sweaty', 'saving the world']
71
72    def __init__(self, name):
73
74        # No anonymous superheroes on my watch! Every narcoleptic superhero gets
75        # a name. Any name at all. SleepyMan. SlumberGirl. You get the idea.
76        self.name = name
77
78        # What have we accomplished today?
79        self.kittens_rescued = 0
80
81        # Initialize the state machine
82        self.machine = Machine(model=self, states=NarcolepticSuperhero.states, initial='asleep')
83
84        # Add some transitions. We could also define these using a static list of
85        # dictionaries, as we did with states above, and then pass the list to
86        # the Machine initializer as the transitions= argument.
87
88        # At some point, every superhero must rise and shine.
89        self.machine.add_transition(trigger='wake_up', source='asleep', dest='hanging out')
90
91        # Superheroes need to keep in shape.
92        self.machine.add_transition('work_out', 'hanging out', 'hungry')
93
94        # Those calories won't replenish themselves!
95        self.machine.add_transition('eat', 'hungry', 'hanging out')
96
97        # Superheroes are always on call. ALWAYS. But they're not always
98        # dressed in work-appropriate clothing.
99        self.machine.add_transition('distress_call', '*', 'saving the world',
100                         before='change_into_super_secret_costume')
101
102        # When they get off work, they're all sweaty and disgusting. But before
103        # they do anything else, they have to meticulously log their latest
104        # escapades. Because the legal department says so.
105        self.machine.add_transition('complete_mission', 'saving the world', 'sweaty',
106                         after='update_journal')
107
108        # Sweat is a disorder that can be remedied with water.
109        # Unless you've had a particularly long day, in which case... bed time!
110        self.machine.add_transition('clean_up', 'sweaty', 'asleep', conditions=['is_exhausted'])
111        self.machine.add_transition('clean_up', 'sweaty', 'hanging out')
112
113        # Our NarcolepticSuperhero can fall asleep at pretty much any time.
114        self.machine.add_transition('nap', '*', 'asleep')
115
116    def update_journal(self):
117        """ Dear Diary, today I saved Mr. Whiskers. Again. """
118        self.kittens_rescued += 1
119
120    @property
121    def is_exhausted(self):
122        """ Basically a coin toss. """
123        return random.random() < 0.5
124
125    def change_into_super_secret_costume(self):
126        print("Beauty, eh?")
127```
128
129There, now you've baked a state machine into `NarcolepticSuperhero`. Let's take him/her/it out for a spin...
130
131```python
132>>> batman = NarcolepticSuperhero("Batman")
133>>> batman.state
134'asleep'
135
136>>> batman.wake_up()
137>>> batman.state
138'hanging out'
139
140>>> batman.nap()
141>>> batman.state
142'asleep'
143
144>>> batman.clean_up()
145MachineError: "Can't trigger event clean_up from state asleep!"
146
147>>> batman.wake_up()
148>>> batman.work_out()
149>>> batman.state
150'hungry'
151
152# Batman still hasn't done anything useful...
153>>> batman.kittens_rescued
1540
155
156# We now take you live to the scene of a horrific kitten entreement...
157>>> batman.distress_call()
158'Beauty, eh?'
159>>> batman.state
160'saving the world'
161
162# Back to the crib.
163>>> batman.complete_mission()
164>>> batman.state
165'sweaty'
166
167>>> batman.clean_up()
168>>> batman.state
169'asleep'   # Too tired to shower!
170
171# Another productive day, Alfred.
172>>> batman.kittens_rescued
1731
174```
175
176While we cannot read the mind of the actual batman, we surely can visualize the current state of our `NarcolepticSuperhero`.
177
178![batman diagram](https://user-images.githubusercontent.com/205986/104932302-c2f24580-59a7-11eb-8963-5dce738b9305.png)
179
180Have a look at the [Diagrams](#diagrams) extensions if you want to know how.
181
182## The non-quickstart
183
184### Basic initialization
185
186Getting a state machine up and running is pretty simple. Let's say you have the object `lump` (an instance of class `Matter`), and you want to manage its states:
187
188```python
189class Matter(object):
190    pass
191
192lump = Matter()
193```
194
195You can initialize a (_minimal_) working state machine bound to `lump` like this:
196
197```python
198from transitions import Machine
199machine = Machine(model=lump, states=['solid', 'liquid', 'gas', 'plasma'], initial='solid')
200
201# Lump now has state!
202lump.state
203>>> 'solid'
204```
205
206I say "minimal", because while this state machine is technically operational, it doesn't actually _do_ anything. It starts in the `'solid'` state, but won't ever move into another state, because no transitions are defined... yet!
207
208Let's try again.
209
210```python
211# The states
212states=['solid', 'liquid', 'gas', 'plasma']
213
214# And some transitions between states. We're lazy, so we'll leave out
215# the inverse phase transitions (freezing, condensation, etc.).
216transitions = [
217    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' },
218    { 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas' },
219    { 'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas' },
220    { 'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma' }
221]
222
223# Initialize
224machine = Machine(lump, states=states, transitions=transitions, initial='liquid')
225
226# Now lump maintains state...
227lump.state
228>>> 'liquid'
229
230# And that state can change...
231lump.evaporate()
232lump.state
233>>> 'gas'
234lump.trigger('ionize')
235lump.state
236>>> 'plasma'
237```
238
239Notice the shiny new methods attached to the `Matter` instance (`evaporate()`, `ionize()`, etc.). Each method triggers the corresponding transition. You don't have to explicitly define these methods anywhere; the name of each transition is bound to the model passed to the `Machine` initializer (in this case, `lump`).
240To be more precise, your model **should not** already contain methods with the same name as event triggers since `transitions` will only attach convenience methods to your model if the spot is not already taken.
241If you want to modify that behaviour, have a look at the [FAQ](examples/Frequently%20asked%20questions.ipynb).
242Furthermore, there is a method called `trigger` now attached to your model (if it hasn't been there before).
243This method lets you execute transitions by name in case dynamic triggering is required.
244
245### <a name="states"></a>States
246
247The soul of any good state machine (and of many bad ones, no doubt) is a set of states. Above, we defined the valid model states by passing a list of strings to the `Machine` initializer. But internally, states are actually represented as `State` objects.
248
249You can initialize and modify States in a number of ways. Specifically, you can:
250
251- pass a string to the `Machine` initializer giving the name(s) of the state(s), or
252- directly initialize each new `State` object, or
253- pass a dictionary with initialization arguments
254
255The following snippets illustrate several ways to achieve the same goal:
256
257```python
258# import Machine and State class
259from transitions import State
260
261# Create a list of 3 states to pass to the Machine
262# initializer. We can mix types; in this case, we
263# pass one State, one string, and one dict.
264states = [
265    State(name='solid'),
266    'liquid',
267    { 'name': 'gas'}
268    ]
269machine = Machine(lump, states)
270
271# This alternative example illustrates more explicit
272# addition of states and state callbacks, but the net
273# result is identical to the above.
274machine = Machine(lump)
275solid = State('solid')
276liquid = State('liquid')
277gas = State('gas')
278machine.add_states([solid, liquid, gas])
279```
280
281States are initialized *once* when added to the machine and will persist until they are removed from it. In other words: if you alter the attributes of a state object, this change will NOT be reset the next time you enter that state. Have a look at how to [extend state features](#state-features) in case you require some other behaviour.
282
283#### <a name="state-callbacks"></a>Callbacks
284A `State` can also be associated with a list of `enter` and `exit` callbacks, which are called whenever the state machine enters or leaves that state. You can specify callbacks during initialization by passing them to a `State` object constructor, in a state property dictionary, or add them later.
285
286For convenience, whenever a new `State` is added to a `Machine`, the methods `on_enter_«state name»` and `on_exit_«state name»` are dynamically created on the Machine (not on the model!), which allow you to dynamically add new enter and exit callbacks later if you need them.
287
288```python
289# Our old Matter class, now with  a couple of new methods we
290# can trigger when entering or exit states.
291class Matter(object):
292    def say_hello(self): print("hello, new state!")
293    def say_goodbye(self): print("goodbye, old state!")
294
295lump = Matter()
296
297# Same states as above, but now we give StateA an exit callback
298states = [
299    State(name='solid', on_exit=['say_goodbye']),
300    'liquid',
301    { 'name': 'gas', 'on_exit': ['say_goodbye']}
302    ]
303
304machine = Machine(lump, states=states)
305machine.add_transition('sublimate', 'solid', 'gas')
306
307# Callbacks can also be added after initialization using
308# the dynamically added on_enter_ and on_exit_ methods.
309# Note that the initial call to add the callback is made
310# on the Machine and not on the model.
311machine.on_enter_gas('say_hello')
312
313# Test out the callbacks...
314machine.set_state('solid')
315lump.sublimate()
316>>> 'goodbye, old state!'
317>>> 'hello, new state!'
318```
319
320Note that `on_enter_«state name»` callback will *not* fire when a Machine is first initialized. For example if you have an `on_enter_A()` callback defined, and initialize the `Machine` with `initial='A'`, `on_enter_A()` will not be fired until the next time you enter state `A`. (If you need to make sure `on_enter_A()` fires at initialization, you can simply create a dummy initial state and then explicitly call `to_A()` inside the `__init__` method.)
321
322In addition to passing in callbacks when initializing a `State`, or adding them dynamically, it's also possible to define callbacks in the model class itself, which may increase code clarity. For example:
323
324```python
325class Matter(object):
326    def say_hello(self): print("hello, new state!")
327    def say_goodbye(self): print("goodbye, old state!")
328    def on_enter_A(self): print("We've just entered state A!")
329
330lump = Matter()
331machine = Machine(lump, states=['A', 'B', 'C'])
332```
333
334Now, any time `lump` transitions to state `A`, the `on_enter_A()` method defined in the `Matter` class will fire.
335
336#### <a name="checking-state"></a>Checking state
337
338You can always check the current state of the model by either:
339
340- inspecting the `.state` attribute, or
341- calling `is_«state name»()`
342
343And if you want to retrieve the actual `State` object for the current state, you can do that through the `Machine` instance's `get_state()` method.
344
345```python
346lump.state
347>>> 'solid'
348lump.is_gas()
349>>> False
350lump.is_solid()
351>>> True
352machine.get_state(lump.state).name
353>>> 'solid'
354```
355
356If you'd like you can choose your own state attribute name by passing the `model_attribute` argument while initializing the `Machine`. This will also change the name of `is_«state name»()` to `is_«model_attribute»_«state name»()` though. Similarly, auto transitions will be named `to_«model_attribute»_«state name»()` instead of `to_«state name»()`. This is done to allow multiple machines to work on the same model with individual state attribute names.
357
358```python
359lump = Matter()
360machine = Machine(lump, states=['solid', 'liquid', 'gas'],  model_attribute='matter_state', initial='solid')
361lump.matter_state
362>>> 'solid'
363# with a custom 'model_attribute', states can also be checked like this:
364lump.is_matter_state_solid()
365>>> True
366lump.to_matter_state_gas()
367>>> True
368```
369
370#### <a name="enum-state"></a>Enumerations
371
372So far we have seen how we can give state names and use these names to work with our state machine.
373If you favour stricter typing and more IDE code completion (or you just can't type 'sesquipedalophobia' any longer because the word scares you) using [Enumerations](https://docs.python.org/3/library/enum.html) might be what you are looking for:
374
375```python
376import enum  # Python 2.7 users need to have 'enum34' installed
377from transitions import Machine
378
379class States(enum.Enum):
380    ERROR = 0
381    RED = 1
382    YELLOW = 2
383    GREEN = 3
384
385transitions = [['proceed', States.RED, States.YELLOW],
386               ['proceed', States.YELLOW, States.GREEN],
387               ['error', '*', States.ERROR]]
388
389m = Machine(states=States, transitions=transitions, initial=States.RED)
390assert m.is_RED()
391assert m.state is States.RED
392state = m.get_state(States.RED)  # get transitions.State object
393print(state.name)  # >>> RED
394m.proceed()
395m.proceed()
396assert m.is_GREEN()
397m.error()
398assert m.state is States.ERROR
399```
400
401You can mix enums and strings if you like (e.g. `[States.RED, 'ORANGE', States.YELLOW, States.GREEN]`) but note that internally, `transitions` will still handle states by name (`enum.Enum.name`).
402Thus, it is not possible to have the states `'GREEN'` and `States.GREEN` at the same time.
403
404### <a name="transitions"></a>Transitions
405Some of the above examples already illustrate the use of transitions in passing, but here we'll explore them in more detail.
406
407As with states, each transition is represented internally as its own object – an instance of class `Transition`. The quickest way to initialize a set of transitions is to pass a dictionary, or list of dictionaries, to the `Machine` initializer. We already saw this above:
408
409```python
410transitions = [
411    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' },
412    { 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas' },
413    { 'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas' },
414    { 'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma' }
415]
416machine = Machine(model=Matter(), states=states, transitions=transitions)
417```
418
419Defining transitions in dictionaries has the benefit of clarity, but can be cumbersome. If you're after brevity, you might choose to define transitions using lists. Just make sure that the elements in each list are in the same order as the positional arguments in the `Transition` initialization (i.e., `trigger`, `source`, `destination`, etc.).
420
421The following list-of-lists is functionally equivalent to the list-of-dictionaries above:
422
423```python
424transitions = [
425    ['melt', 'solid', 'liquid'],
426    ['evaporate', 'liquid', 'gas'],
427    ['sublimate', 'solid', 'gas'],
428    ['ionize', 'gas', 'plasma']
429]
430```
431
432Alternatively, you can add transitions to a `Machine` after initialization:
433
434```python
435machine = Machine(model=lump, states=states, initial='solid')
436machine.add_transition('melt', source='solid', dest='liquid')
437```
438
439The `trigger` argument defines the name of the new triggering method that gets attached to the base model. When this method is called, it will try to execute the transition:
440
441```python
442>>> lump.melt()
443>>> lump.state
444'liquid'
445```
446
447By default, calling an invalid trigger will raise an exception:
448
449```python
450>>> lump.to_gas()
451>>> # This won't work because only objects in a solid state can melt
452>>> lump.melt()
453transitions.core.MachineError: "Can't trigger event melt from state gas!"
454```
455
456This behavior is generally desirable, since it helps alert you to problems in your code. But in some cases, you might want to silently ignore invalid triggers. You can do this by setting `ignore_invalid_triggers=True` (either on a state-by-state basis, or globally for all states):
457
458```python
459>>> # Globally suppress invalid trigger exceptions
460>>> m = Machine(lump, states, initial='solid', ignore_invalid_triggers=True)
461>>> # ...or suppress for only one group of states
462>>> states = ['new_state1', 'new_state2']
463>>> m.add_states(states, ignore_invalid_triggers=True)
464>>> # ...or even just for a single state. Here, exceptions will only be suppressed when the current state is A.
465>>> states = [State('A', ignore_invalid_triggers=True), 'B', 'C']
466>>> m = Machine(lump, states)
467>>> # ...this can be inverted as well if just one state should raise an exception
468>>> # since the machine's global value is not applied to a previously initialized state.
469>>> states = ['A', 'B', State('C')] # the default value for 'ignore_invalid_triggers' is False
470>>> m = Machine(lump, states, ignore_invalid_triggers=True)
471```
472
473If you need to know which transitions are valid from a certain state, you can use `get_triggers`:
474
475```
476m.get_triggers('solid')
477>>> ['melt', 'sublimate']
478m.get_triggers('liquid')
479>>> ['evaporate']
480m.get_triggers('plasma')
481>>> []
482# you can also query several states at once
483m.get_triggers('solid', 'liquid', 'gas', 'plasma')
484>>> ['melt', 'evaporate', 'sublimate', 'ionize']
485```
486
487#### <a name="automatic-transitions-for-all-states"></a>Automatic transitions for all states
488In addition to any transitions added explicitly, a `to_«state»()` method is created automatically whenever a state is added to a `Machine` instance. This method transitions to the target state no matter which state the machine is currently in:
489
490```python
491lump.to_liquid()
492lump.state
493>>> 'liquid'
494lump.to_solid()
495lump.state
496>>> 'solid'
497```
498
499If you desire, you can disable this behavior by setting `auto_transitions=False` in the `Machine` initializer.
500
501#### <a name="transitioning-from-multiple-states"></a>Transitioning from multiple states
502A given trigger can be attached to multiple transitions, some of which can potentially begin or end in the same state. For example:
503
504```python
505machine.add_transition('transmogrify', ['solid', 'liquid', 'gas'], 'plasma')
506machine.add_transition('transmogrify', 'plasma', 'solid')
507# This next transition will never execute
508machine.add_transition('transmogrify', 'plasma', 'gas')
509```
510
511In this case, calling `transmogrify()` will set the model's state to `'solid'` if it's currently `'plasma'`, and set it to `'plasma'` otherwise. (Note that only the _first_ matching transition will execute; thus, the transition defined in the last line above won't do anything.)
512
513You can also make a trigger cause a transition from _all_ states to a particular destination by using the `'*'` wildcard:
514
515```python
516machine.add_transition('to_liquid', '*', 'liquid')
517```
518
519Note that wildcard transitions will only apply to states that exist at the time of the add_transition() call. Calling a wildcard-based transition when the model is in a state added after the transition was defined will elicit an invalid transition message, and will not transition to the target state.
520
521#### <a name="reflexive-from-multiple-states"></a>Reflexive transitions from multiple states
522A reflexive trigger (trigger that has the same state as source and destination) can easily be added specifying `=` as destination.
523This is handy if the same reflexive trigger should be added to multiple states.
524For example:
525
526```python
527machine.add_transition('touch', ['liquid', 'gas', 'plasma'], '=', after='change_shape')
528```
529
530This will add reflexive transitions for all three states with `touch()` as trigger and with `change_shape` executed after each trigger.
531
532#### <a name="internal-transitions"></a>Internal transitions
533In contrast to reflexive transitions, internal transitions will never actually leave the state.
534This means that transition-related callbacks such as `before` or `after` will be processed while state-related callbacks `exit` or `enter` will not.
535To define a transition to be internal, set the destination to `None`.
536
537```python
538machine.add_transition('internal', ['liquid', 'gas'], None, after='change_shape')
539```
540
541#### <a name="ordered-transitions"></a> Ordered transitions
542A 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).
543
544To facilitate this behavior, Transitions provides an `add_ordered_transitions()` method in the `Machine` class:
545
546```python
547states = ['A', 'B', 'C']
548 # See the "alternative initialization" section for an explanation of the 1st argument to init
549machine = Machine(states=states, initial='A')
550machine.add_ordered_transitions()
551machine.next_state()
552print(machine.state)
553>>> 'B'
554# We can also define a different order of transitions
555machine = Machine(states=states, initial='A')
556machine.add_ordered_transitions(['A', 'C', 'B'])
557machine.next_state()
558print(machine.state)
559>>> 'C'
560# Conditions can be passed to 'add_ordered_transitions' as well
561# If one condition is passed, it will be used for all transitions
562machine = Machine(states=states, initial='A')
563machine.add_ordered_transitions(conditions='check')
564# If a list is passed, it must contain exactly as many elements as the
565# machine contains states (A->B, ..., X->A)
566machine = Machine(states=states, initial='A')
567machine.add_ordered_transitions(conditions=['check_A2B', ..., 'check_X2A'])
568```
569
570#### <a name="queued-transitions"></a>Queued transitions
571
572The default behaviour in Transitions is to process events instantly. This means events within an `on_enter` method will be processed _before_ callbacks bound to `after` are called.
573
574```python
575def go_to_C():
576    global machine
577    machine.to_C()
578
579def after_advance():
580    print("I am in state B now!")
581
582def entering_C():
583    print("I am in state C now!")
584
585states = ['A', 'B', 'C']
586machine = Machine(states=states, initial='A')
587
588# we want a message when state transition to B has been completed
589machine.add_transition('advance', 'A', 'B', after=after_advance)
590
591# call transition from state B to state C
592machine.on_enter_B(go_to_C)
593
594# we also want a message when entering state C
595machine.on_enter_C(entering_C)
596machine.advance()
597>>> 'I am in state C now!'
598>>> 'I am in state B now!' # what?
599```
600
601The execution order of this example is
602```
603prepare -> before -> on_enter_B -> on_enter_C -> after.
604```
605If queued processing is enabled, a transition will be finished before the next transition is triggered:
606
607```python
608machine = Machine(states=states, queued=True, initial='A')
609...
610machine.advance()
611>>> 'I am in state B now!'
612>>> 'I am in state C now!' # That's better!
613```
614
615This results in
616```
617prepare -> before -> on_enter_B -> queue(to_C) -> after  -> on_enter_C.
618```
619**Important note:** when processing events in a queue, the trigger call will _always_ return `True`, since there is no way to determine at queuing time whether a transition involving queued calls will ultimately complete successfully. This is true even when only a single event is processed.
620
621```python
622machine.add_transition('jump', 'A', 'C', conditions='will_fail')
623...
624# queued=False
625machine.jump()
626>>> False
627# queued=True
628machine.jump()
629>>> True
630```
631
632When a model is removed from the machine, `transitions` will also remove all related events from the queue.
633
634```python
635class Model:
636    def on_enter_B(self):
637        self.to_C()  # add event to queue ...
638        self.machine.remove_model(self)  # aaaand it's gone
639```
640
641
642#### <a name="conditional-transitions"></a>Conditional transitions
643Sometimes you only want a particular transition to execute if a specific condition occurs. You can do this by passing a method, or list of methods, in the `conditions` argument:
644
645```python
646# Our Matter class, now with a bunch of methods that return booleans.
647class Matter(object):
648    def is_flammable(self): return False
649    def is_really_hot(self): return True
650
651machine.add_transition('heat', 'solid', 'gas', conditions='is_flammable')
652machine.add_transition('heat', 'solid', 'liquid', conditions=['is_really_hot'])
653```
654
655In the above example, calling `heat()` when the model is in state `'solid'` will transition to state `'gas'` if `is_flammable` returns `True`. Otherwise, it will transition to state `'liquid'` if `is_really_hot` returns `True`.
656
657For convenience, there's also an `'unless'` argument that behaves exactly like conditions, but inverted:
658
659```python
660machine.add_transition('heat', 'solid', 'gas', unless=['is_flammable', 'is_really_hot'])
661```
662
663In this case, the model would transition from solid to gas whenever `heat()` fires, provided that both `is_flammable()` and `is_really_hot()` return `False`.
664
665Note that condition-checking methods will passively receive optional arguments and/or data objects passed to triggering methods. For instance, the following call:
666
667```python
668lump.heat(temp=74)
669# equivalent to lump.trigger('heat', temp=74)
670```
671
672... would pass the `temp=74` optional kwarg to the `is_flammable()` check (possibly wrapped in an `EventData` instance). For more on this, see the [Passing data](#passing-data) section below.
673
674#### <a name="transition-callbacks"></a>Callbacks
675You can attach callbacks to transitions as well as states. Every transition has `'before'` and `'after'` attributes that contain a list of methods to call before and after the transition executes:
676
677```python
678class Matter(object):
679    def make_hissing_noises(self): print("HISSSSSSSSSSSSSSSS")
680    def disappear(self): print("where'd all the liquid go?")
681
682transitions = [
683    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid', 'before': 'make_hissing_noises'},
684    { 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas', 'after': 'disappear' }
685]
686
687lump = Matter()
688machine = Machine(lump, states, transitions=transitions, initial='solid')
689lump.melt()
690>>> "HISSSSSSSSSSSSSSSS"
691lump.evaporate()
692>>> "where'd all the liquid go?"
693```
694
695There is also a `'prepare'` callback that is executed as soon as a transition starts, before any `'conditions'` are checked or other callbacks are executed.
696
697```python
698class Matter(object):
699    heat = False
700    attempts = 0
701    def count_attempts(self): self.attempts += 1
702    def heat_up(self): self.heat = random.random() < 0.25
703    def stats(self): print('It took you %i attempts to melt the lump!' %self.attempts)
704
705    @property
706    def is_really_hot(self):
707        return self.heat
708
709
710states=['solid', 'liquid', 'gas', 'plasma']
711
712transitions = [
713    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid', 'prepare': ['heat_up', 'count_attempts'], 'conditions': 'is_really_hot', 'after': 'stats'},
714]
715
716lump = Matter()
717machine = Machine(lump, states, transitions=transitions, initial='solid')
718lump.melt()
719lump.melt()
720lump.melt()
721lump.melt()
722>>> "It took you 4 attempts to melt the lump!"
723```
724
725Note that `prepare` will not be called unless the current state is a valid source for the named transition.
726
727Default actions meant to be executed before or after *every* transition can be passed to `Machine` during initialization with
728`before_state_change` and `after_state_change` respectively:
729
730```python
731class Matter(object):
732    def make_hissing_noises(self): print("HISSSSSSSSSSSSSSSS")
733    def disappear(self): print("where'd all the liquid go?")
734
735states=['solid', 'liquid', 'gas', 'plasma']
736
737lump = Matter()
738m = Machine(lump, states, before_state_change='make_hissing_noises', after_state_change='disappear')
739lump.to_gas()
740>>> "HISSSSSSSSSSSSSSSS"
741>>> "where'd all the liquid go?"
742```
743
744There are also two keywords for callbacks which should be executed *independently* a) of how many transitions are possible,
745b) if any transition succeeds and c) even if an error is raised during the execution of some other callback.
746Callbacks passed to `Machine` with `prepare_event` will be executed *once* before processing possible transitions
747(and their individual `prepare` callbacks) takes place.
748Callbacks of `finalize_event` will be executed regardless of the success of the processed transitions.
749Note that if an error occurred it will be attached to `event_data` as `error` and can be retrieved with `send_event=True`.
750
751```python
752from transitions import Machine
753
754class Matter(object):
755    def raise_error(self, event): raise ValueError("Oh no")
756    def prepare(self, event): print("I am ready!")
757    def finalize(self, event): print("Result: ", type(event.error), event.error)
758
759states=['solid', 'liquid', 'gas', 'plasma']
760
761lump = Matter()
762m = Machine(lump, states, prepare_event='prepare', before_state_change='raise_error',
763            finalize_event='finalize', send_event=True)
764try:
765    lump.to_gas()
766except ValueError:
767    pass
768print(lump.state)
769
770# >>> I am ready!
771# >>> Result:  <class 'ValueError'> Oh no
772# >>> initial
773```
774
775Sometimes things just don't work out as intended and we need to handle exceptions and clean up the mess to keep things going.
776We can pass callbacks to `on_exception` to do this:
777
778```python
779from transitions import Machine
780
781class Matter(object):
782    def raise_error(self, event): raise ValueError("Oh no")
783    def handle_error(self, event):
784        print("Fixing things ...")
785        del event.error  # it did not happen if we cannot see it ...
786
787states=['solid', 'liquid', 'gas', 'plasma']
788
789lump = Matter()
790m = Machine(lump, states, before_state_change='raise_error', on_exception='handle_error', send_event=True)
791try:
792    lump.to_gas()
793except ValueError:
794    pass
795print(lump.state)
796
797# >>> Fixing things ...
798# >>> initial
799```
800
801
802### <a name="resolution"></a>Callable resolution
803
804As you have probably already realized, the standard way of passing callables to states, conditions and transitions is by name. When processing callbacks and conditions, `transitions` will use their name to retrieve the related callable from the model. 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. Alternatively, you can pass names of properties or attributes. They will be wrapped into functions but cannot receive event data for obvious reasons. You can also pass callables such as (bound) functions directly. As mentioned earlier, you can also pass lists/tuples of callables names to the callback parameters. Callbacks will be executed in the order they were added.
805
806```python
807from transitions import Machine
808from mod import imported_func
809
810import random
811
812
813class Model(object):
814
815    def a_callback(self):
816        imported_func()
817
818    @property
819    def a_property(self):
820        """ Basically a coin toss. """
821        return random.random() < 0.5
822
823    an_attribute = False
824
825
826model = Model()
827machine = Machine(model=model, states=['A'], initial='A')
828machine.add_transition('by_name', 'A', 'A', conditions='a_property', after='a_callback')
829machine.add_transition('by_reference', 'A', 'A', unless=['a_property', 'an_attribute'], after=model.a_callback)
830machine.add_transition('imported', 'A', 'A', after='mod.imported_func')
831
832model.by_name()
833model.by_reference()
834model.imported()
835```
836
837The callable resolution is done in `Machine.resolve_callable`.
838This method can be overridden in case more complex callable resolution strategies are required.
839
840
841**Example**
842```python
843class CustomMachine(Machine):
844    @staticmethod
845    def resolve_callable(func, event_data):
846        # manipulate arguments here and return func, or super() if no manipulation is done.
847        super(CustomMachine, CustomMachine).resolve_callable(func, event_data)
848```
849
850### <a name="execution-order"></a>Callback execution order
851
852In summary, there are currently three ways to trigger events. You can call a model's convenience functions like `lump.melt()`,
853execute triggers by name such as `lump.trigger("melt")` or dispatch events on multiple models with `machine.dispatch("melt")`
854(see section about multiple models in [alternative initialization patterns](#alternative-initialization-patterns)).
855Callbacks on transitions are then executed in the following order:
856
857|      Callback                  | Current State |               Comments                                      |
858|--------------------------------|:-------------:|-------------------------------------------------------------|
859| `'machine.prepare_event'`      | `source`      | executed *once* before individual transitions are processed |
860| `'transition.prepare'`         | `source`      | executed as soon as the transition starts                   |
861| `'transition.conditions'`      | `source`      | conditions *may* fail and halt the transition               |
862| `'transition.unless'`          | `source`      | conditions *may* fail and halt the transition               |
863| `'machine.before_state_change'`| `source`      | default callbacks declared on model                         |
864| `'transition.before'`          | `source`      |                                                             |
865| `'state.on_exit'`              | `source`      | callbacks declared on the source state                      |
866| `<STATE CHANGE>`               |               |                                                             |
867| `'state.on_enter'`             | `destination` | callbacks declared on the destination state                 |
868| `'transition.after'`           | `destination` |                                                             |
869| `'machine.after_state_change'` | `destination` | default callbacks declared on model                         |
870| `'machine.on_exception'`       | `source/destination` | callbacks will be executed when an exception has been raised |
871| `'machine.finalize_event'`     | `source/destination` | callbacks will be executed even if no transition took place or an exception has been raised |
872
873If any callback raises an exception, the processing of callbacks is not continued. This means that when an error occurs before the transition (in `state.on_exit` or earlier), it is halted. In case there is a raise after the transition has been conducted (in `state.on_enter` or later), the state change persists and no rollback is happening. Callbacks specified in `machine.finalize_event` will always be executed unless the exception is raised by a finalizing callback itself. Note that each callback sequence has to be finished before the next stage is executed. Blocking callbacks will halt the execution order and therefore block the `trigger` or `dispatch` call itself. If you want callbacks to be executed in parallel, you could have a look at the [extensions](#extensions) `AsyncMachine` for asynchronous processing or `LockedMachine` for threading.
874
875### <a name="passing-data"></a>Passing data
876Sometimes you need to pass the callback functions registered at machine initialization some data that reflects the model's current state.
877Transitions allows you to do this in two different ways.
878
879First (the default), you can pass any positional or keyword arguments directly to the trigger methods (created when you call `add_transition()`):
880
881```python
882class Matter(object):
883    def __init__(self): self.set_environment()
884    def set_environment(self, temp=0, pressure=101.325):
885        self.temp = temp
886        self.pressure = pressure
887    def print_temperature(self): print("Current temperature is %d degrees celsius." % self.temp)
888    def print_pressure(self): print("Current pressure is %.2f kPa." % self.pressure)
889
890lump = Matter()
891machine = Machine(lump, ['solid', 'liquid'], initial='solid')
892machine.add_transition('melt', 'solid', 'liquid', before='set_environment')
893
894lump.melt(45)  # positional arg;
895# equivalent to lump.trigger('melt', 45)
896lump.print_temperature()
897>>> 'Current temperature is 45 degrees celsius.'
898
899machine.set_state('solid')  # reset state so we can melt again
900lump.melt(pressure=300.23)  # keyword args also work
901lump.print_pressure()
902>>> 'Current pressure is 300.23 kPa.'
903
904```
905
906You can pass any number of arguments you like to the trigger.
907
908There is one important limitation to this approach: every callback function triggered by the state transition must be able to handle _all_ of the arguments. This may cause problems if the callbacks each expect somewhat different data.
909
910To get around this, Transitions supports an alternate method for sending data. If you set `send_event=True` at `Machine` initialization, all arguments to the triggers will be wrapped in an `EventData` instance and passed on to every callback. (The `EventData` object also maintains internal references to the source state, model, transition, machine, and trigger associated with the event, in case you need to access these for anything.)
911
912```python
913class Matter(object):
914
915    def __init__(self):
916        self.temp = 0
917        self.pressure = 101.325
918
919    # Note that the sole argument is now the EventData instance.
920    # This object stores positional arguments passed to the trigger method in the
921    # .args property, and stores keywords arguments in the .kwargs dictionary.
922    def set_environment(self, event):
923        self.temp = event.kwargs.get('temp', 0)
924        self.pressure = event.kwargs.get('pressure', 101.325)
925
926    def print_pressure(self): print("Current pressure is %.2f kPa." % self.pressure)
927
928lump = Matter()
929machine = Machine(lump, ['solid', 'liquid'], send_event=True, initial='solid')
930machine.add_transition('melt', 'solid', 'liquid', before='set_environment')
931
932lump.melt(temp=45, pressure=1853.68)  # keyword args
933lump.print_pressure()
934>>> 'Current pressure is 1853.68 kPa.'
935
936```
937
938### <a name="alternative-initialization-patterns"></a>Alternative initialization patterns
939
940In all of the examples so far, we've attached a new `Machine` instance to a separate model (`lump`, an instance of class `Matter`). While this separation keeps things tidy (because you don't have to monkey patch a whole bunch of new methods into the `Matter` class), it can also get annoying, since it requires you to keep track of which methods are called on the state machine, and which ones are called on the model that the state machine is bound to (e.g., `lump.on_enter_StateA()` vs. `machine.add_transition()`).
941
942Fortunately, Transitions is flexible, and supports two other initialization patterns.
943
944First, you can create a standalone state machine that doesn't require another model at all. Simply omit the model argument during initialization:
945
946```python
947machine = Machine(states=states, transitions=transitions, initial='solid')
948machine.melt()
949machine.state
950>>> 'liquid'
951```
952
953If you initialize the machine this way, you can then attach all triggering events (like `evaporate()`, `sublimate()`, etc.) and all callback functions directly to the `Machine` instance.
954
955This approach has the benefit of consolidating all of the state machine functionality in one place, but can feel a little bit unnatural if you think state logic should be contained within the model itself rather than in a separate controller.
956
957An alternative (potentially better) approach is to have the model inherit from the `Machine` class. Transitions is designed to support inheritance seamlessly. (just be sure to override class `Machine`'s `__init__` method!):
958
959```python
960class Matter(Machine):
961    def say_hello(self): print("hello, new state!")
962    def say_goodbye(self): print("goodbye, old state!")
963
964    def __init__(self):
965        states = ['solid', 'liquid', 'gas']
966        Machine.__init__(self, states=states, initial='solid')
967        self.add_transition('melt', 'solid', 'liquid')
968
969lump = Matter()
970lump.state
971>>> 'solid'
972lump.melt()
973lump.state
974>>> 'liquid'
975```
976
977Here you get to consolidate all state machine functionality into your existing model, which often feels more natural way than sticking all of the functionality we want in a separate standalone `Machine` instance.
978
979A machine can handle multiple models which can be passed as a list like `Machine(model=[model1, model2, ...])`.
980In cases where you want to add models *as well as* the machine instance itself, you can pass the string placeholder `'self'` during initialization like `Machine(model=['self', model1, ...])`.
981You can also create a standalone machine, and register models dynamically via `machine.add_model` by passing `model=None` to the constructor.
982Furthermore, you can use `machine.dispatch` to trigger events on all currently added models.
983Remember to call `machine.remove_model` if machine is long-lasting and your models are temporary and should be garbage collected:
984
985```python
986class Matter():
987    pass
988
989lump1 = Matter()
990lump2 = Matter()
991
992# setting 'model' to None or passing an empty list will initialize the machine without a model
993machine = Machine(model=None, states=states, transitions=transitions, initial='solid')
994
995machine.add_model(lump1)
996machine.add_model(lump2, initial='liquid')
997
998lump1.state
999>>> 'solid'
1000lump2.state
1001>>> 'liquid'
1002
1003# custom events as well as auto transitions can be dispatched to all models
1004machine.dispatch("to_plasma")
1005
1006lump1.state
1007>>> 'plasma'
1008assert lump1.state == lump2.state
1009
1010machine.remove_model([lump1, lump2])
1011del lump1  # lump1 is garbage collected
1012del lump2  # lump2 is garbage collected
1013```
1014
1015If you don't provide an initial state in the state machine constructor, `transitions` will create and add a default state called `'initial'`.
1016If you do not want a default initial state, you can pass `initial=None`.
1017However, in this case you need to pass an initial state every time you add a model.
1018
1019```python
1020machine = Machine(model=None, states=states, transitions=transitions, initial=None)
1021
1022machine.add_model(Matter())
1023>>> "MachineError: No initial state configured for machine, must specify when adding model."
1024machine.add_model(Matter(), initial='liquid')
1025```
1026
1027Models with multiple states could attach multiple machines using different `model_attribute` values. As mentioned in [Checking state](#checking-state), this will add custom `is/to_<model_attribute>_<state_name>` functions:
1028
1029```python
1030lump = Matter()
1031
1032matter_machine = Machine(lump, states=['solid', 'liquid', 'gas'], initial='solid')
1033# add a second machine to the same model but assign a different state attribute
1034shipment_machine = Machine(lump, states=['delivered', 'shipping'], initial='delivered', model_attribute='shipping_state')
1035
1036lump.state
1037>>> 'solid'
1038lump.is_solid()  # check the default field
1039>>> True
1040lump.shipping_state
1041>>> 'delivered'
1042lump.is_shipping_state_delivered()  # check the custom field.
1043>>> True
1044lump.to_shipping_state_shipping()
1045>>> True
1046lump.is_shipping_state_delivered()
1047>>> False
1048```
1049
1050### Logging
1051
1052Transitions includes very rudimentary logging capabilities. A number of events – namely, state changes, transition triggers, and conditional checks – are logged as INFO-level events using the standard Python `logging` module. This means you can easily configure logging to standard output in a script:
1053
1054```python
1055# Set up logging; The basic log level will be DEBUG
1056import logging
1057logging.basicConfig(level=logging.DEBUG)
1058# Set transitions' log level to INFO; DEBUG messages will be omitted
1059logging.getLogger('transitions').setLevel(logging.INFO)
1060
1061# Business as usual
1062machine = Machine(states=states, transitions=transitions, initial='solid')
1063...
1064```
1065
1066### <a name="restoring"></a>(Re-)Storing machine instances
1067
1068Machines are picklable and can be stored and loaded with `pickle`. For Python 3.3 and earlier `dill` is required.
1069
1070```python
1071import dill as pickle # only required for Python 3.3 and earlier
1072
1073m = Machine(states=['A', 'B', 'C'], initial='A')
1074m.to_B()
1075m.state
1076>>> B
1077
1078# store the machine
1079dump = pickle.dumps(m)
1080
1081# load the Machine instance again
1082m2 = pickle.loads(dump)
1083
1084m2.state
1085>>> B
1086
1087m2.states.keys()
1088>>> ['A', 'B', 'C']
1089```
1090
1091### <a name="extensions"></a> Extensions
1092
1093Even though the core of transitions is kept lightweight, there are a variety of MixIns to extend its functionality. Currently supported are:
1094
1095- **Diagrams** to visualize the current state of a machine
1096- **Hierarchical State Machines** for nesting and reuse
1097- **Threadsafe Locks** for parallel execution
1098- **Async callbacks** for asynchronous execution
1099- **Custom States** for extended state-related behaviour
1100
1101There are two mechanisms to retrieve a state machine instance with the desired features enabled.
1102The first approach makes use of the convenience `factory` with the four parameters `graph`, `nested`, `locked` or `asyncio` set to `True` if the feature is required:
1103
1104```python
1105from transitions.extensions import MachineFactory
1106
1107# create a machine with mixins
1108diagram_cls = MachineFactory.get_predefined(graph=True)
1109nested_locked_cls = MachineFactory.get_predefined(nested=True, locked=True)
1110async_machine_cls = MachineFactory.get_predefined(asyncio=True)
1111
1112# create instances from these classes
1113# instances can be used like simple machines
1114machine1 = diagram_cls(model, state, transitions)
1115machine2 = nested_locked_cls(model, state, transitions)
1116```
1117
1118This approach targets experimental use since in this case the underlying classes do not have to be known.
1119However, classes can also be directly imported from `transitions.extensions`. The naming scheme is as follows:
1120
1121|                                | Diagrams | Nested | Locked | Asyncio |
1122| -----------------------------: | :------: | :----: | :----: | :---: |
1123| Machine                        | ✘        | ✘      | ✘      | ✘ |
1124| GraphMachine                   | ✓        | ✘      | ✘      | ✘ |
1125| HierarchicalMachine            | ✘        | ✓      | ✘      | ✘ |
1126| LockedMachine                  | ✘        | ✘      | ✓      | ✘ |
1127| HierarchicalGraphMachine       | ✓        | ✓      | ✘      | ✘ |
1128| LockedGraphMachine             | ✓        | ✘      | ✓      | ✘ |
1129| LockedHierarchicalMachine      | ✘        | ✓      | ✓      | ✘ |
1130| LockedHierarchicalGraphMachine | ✓        | ✓      | ✓      | ✘ |
1131| AsyncMachine                   | ✘        | ✘      | ✘      | ✓ |
1132| AsyncGraphMachine              | ✓        | ✘      | ✘      | ✓ |
1133| HierarchicalAsyncMachine       | ✘        | ✓      | ✘      | ✓ |
1134| HierarchicalAsyncGraphMachine  | ✓        | ✓      | ✘      | ✓ |
1135
1136To use a feature-rich state machine, one could write:
1137
1138```python
1139from transitions.extensions import LockedHierarchicalGraphMachine as Machine
1140
1141machine = Machine(model, states, transitions)
1142```
1143
1144#### <a name="diagrams"></a> Diagrams
1145
1146Additional Keywords:
1147* `title` (optional): Sets the title of the generated image.
1148* `show_conditions` (default False): Shows conditions at transition edges
1149* `show_auto_transitions` (default False): Shows auto transitions in graph
1150* `show_state_attributes` (default False): Show callbacks (enter, exit), tags and timeouts in graph
1151
1152Transitions can generate basic state diagrams displaying all valid transitions between states. To use the graphing functionality, you'll need to have `graphviz` and/or `pygraphviz` installed:
1153To generate graphs with the package `graphviz`, you need to install [Graphviz](https://graphviz.org/) manually or via a package manager.
1154
1155    sudo apt-get install graphviz  # Ubuntu and Debian
1156    brew install graphviz  # MacOS
1157    conda install graphviz python-graphviz  # (Ana)conda
1158
1159Now you can install the actual Python packages
1160
1161    pip install graphviz pygraphviz # install graphviz and/or pygraphviz manually...
1162    pip install transitions[diagrams]  # ... or install transitions with 'diagrams' extras which currently depends on pygraphviz
1163
1164Currently, `GraphMachine` will use `pygraphviz` when available and fall back to `graphviz` when `pygraphviz` cannot be
1165found. This can be overridden by passing `use_pygraphviz=False` to the constructor. Note that this default might change
1166in the future and `pygraphviz` support may be dropped.
1167With `Model.get_graph()` you can get the current graph or the region of interest (roi) and draw it like this:
1168
1169```python
1170# import transitions
1171
1172from transitions.extensions import GraphMachine as Machine
1173m = Model()
1174# without further arguments pygraphviz will be used
1175machine = Machine(model=m, ...)
1176# when you want to use graphviz explicitly
1177machine = Machine(model=m, use_pygraphviz=False, ...)
1178# in cases where auto transitions should be visible
1179machine = Machine(model=m, show_auto_transitions=True, ...)
1180
1181# draw the whole graph ...
1182m.get_graph().draw('my_state_diagram.png', prog='dot')
1183# ... or just the region of interest
1184# (previous state, active state and all reachable states)
1185roi = m.get_graph(show_roi=True).draw('my_state_diagram.png', prog='dot')
1186```
1187
1188This produces something like this:
1189
1190![state diagram example](https://user-images.githubusercontent.com/205986/47524268-725c1280-d89a-11e8-812b-1d3b6e667b91.png)
1191
1192References and partials passed as callbacks will be resolved as good as possible:
1193
1194```python
1195from transitions.extensions import GraphMachine as Machine
1196from functools import partial
1197
1198
1199class Model:
1200
1201    def clear_state(self, deep=False, force=False):
1202        print("Clearing state ...")
1203        return True
1204
1205
1206model = Model()
1207machine = Machine(model=model, states=['A', 'B', 'C'],
1208                  transitions=[
1209                      {'trigger': 'clear', 'source': 'B', 'dest': 'A', 'conditions': model.clear_state},
1210                      {'trigger': 'clear', 'source': 'C', 'dest': 'A',
1211                       'conditions': partial(model.clear_state, False, force=True)},
1212                  ],
1213                  initial='A', show_conditions=True)
1214
1215model.get_graph().draw('my_state_diagram.png', prog='dot')
1216```
1217
1218This should produce something similar to this:
1219
1220![state diagram references_example](https://user-images.githubusercontent.com/205986/110783076-39087f80-8268-11eb-8fa1-fc7bac97f4cf.png)
1221
1222If the format of references does not suit your needs, you can override the static method `GraphMachine.format_references`. If you want to skip reference entirely, just let `GraphMachine.format_references` return `None`.
1223Also, have a look at our [example](./examples) IPython/Jupyter notebooks for a more detailed example about how to use and edit graphs.
1224
1225
1226### <a name="hsm"></a>Hierarchical State Machine (HSM)
1227
1228Transitions includes an extension module which allows to nest states.
1229This allows to create contexts and to model cases where states are related to certain subtasks in the state machine.
1230To create a nested state, either import `NestedState` from transitions or use a dictionary with the initialization arguments `name` and `children`.
1231Optionally, `initial` can be used to define a sub state to transit to, when the nested state is entered.
1232
1233```python
1234from transitions.extensions import HierarchicalMachine as Machine
1235
1236states = ['standing', 'walking', {'name': 'caffeinated', 'children':['dithering', 'running']}]
1237transitions = [
1238  ['walk', 'standing', 'walking'],
1239  ['stop', 'walking', 'standing'],
1240  ['drink', '*', 'caffeinated'],
1241  ['walk', ['caffeinated', 'caffeinated_dithering'], 'caffeinated_running'],
1242  ['relax', 'caffeinated', 'standing']
1243]
1244
1245machine = Machine(states=states, transitions=transitions, initial='standing', ignore_invalid_triggers=True)
1246
1247machine.walk() # Walking now
1248machine.stop() # let's stop for a moment
1249machine.drink() # coffee time
1250machine.state
1251>>> 'caffeinated'
1252machine.walk() # we have to go faster
1253machine.state
1254>>> 'caffeinated_running'
1255machine.stop() # can't stop moving!
1256machine.state
1257>>> 'caffeinated_running'
1258machine.relax() # leave nested state
1259machine.state # phew, what a ride
1260>>> 'standing'
1261# machine.on_enter_caffeinated_running('callback_method')
1262```
1263
1264A configuration making use of  `initial` could look like this:
1265
1266```python
1267# ...
1268states = ['standing', 'walking', {'name': 'caffeinated', 'initial': 'dithering', 'children': ['dithering', 'running']}]
1269transitions = [
1270  ['walk', 'standing', 'walking'],
1271  ['stop', 'walking', 'standing'],
1272  # this transition will end in 'caffeinated_dithering'...
1273  ['drink', '*', 'caffeinated'],
1274  # ... that is why we do not need do specify 'caffeinated' here anymore
1275  ['walk', 'caffeinated_dithering', 'caffeinated_running'],
1276  ['relax', 'caffeinated', 'standing']
1277]
1278# ...
1279```
1280The `initial` keyword of the `HierarchicalMachine` constructor accepts nested states (e.g. `initial='caffeinated_running'`) and a list of states which is considered to be a parallel state (e.g. `initial=['A', 'B']`) or the current state of another model (`initial=model.state`) which should be effectively one of the previous mentioned options. Note that when passing a string, `transition` will check the targeted state for `initial` substates and use this as an entry state. This will be done recursively until a substate does not mention an initial state. Parallel states or a state passed as a list will be used 'as is' and no further initial evaluation will be conducted.
1281
1282Note that your previously created state object *must be* a `NestedState` or a derived class of it.
1283The standard `State` class used in simple `Machine` instances lacks features required for nesting.
1284
1285```python
1286from transitions.extensions.nesting import HierarchicalMachine, NestedState
1287from transitions import  State
1288m = HierarchicalMachine(states=['A'], initial='initial')
1289m.add_state('B')  # fine
1290m.add_state({'name': 'C'})  # also fine
1291m.add_state(NestedState('D'))  # fine as well
1292m.add_state(State('E'))  # does not work!
1293```
1294
1295Some things that have to be considered when working with nested states: State *names are concatenated* with `NestedState.separator`.
1296Currently the separator is set to underscore ('_') and therefore behaves similar to the basic machine.
1297This means a substate `bar` from state `foo` will be known by `foo_bar`. A substate `baz` of `bar` will be referred to as `foo_bar_baz` and so on.
1298When entering a substate, `enter` will be called for all parent states. The same is true for exiting substates.
1299Third, nested states can overwrite transition behaviour of their parents.
1300If a transition is not known to the current state it will be delegated to its parent.
1301
1302**This means that in the standard configuration, state names in HSMs MUST NOT contain underscores.**
1303For `transitions` it's impossible to tell whether `machine.add_state('state_name')` should add a state named `state_name` or add a substate `name` to the state `state`.
1304In some cases this is not sufficient however.
1305For instance if state names consists of more than one word and you want/need to use underscore to separate them instead of `CamelCase`.
1306To deal with this, you can change the character used for separation quite easily.
1307You can even use fancy unicode characters if you use Python 3.
1308Setting the separator to something else than underscore changes some of the behaviour (auto_transition and setting callbacks) though:
1309
1310```python
1311from transitions.extensions import HierarchicalMachine
1312from transitions.extensions.nesting import NestedState
1313NestedState.separator = '↦'
1314states = ['A', 'B',
1315  {'name': 'C', 'children':['1', '2',
1316    {'name': '3', 'children': ['a', 'b', 'c']}
1317  ]}
1318]
1319
1320transitions = [
1321    ['reset', 'C', 'A'],
1322    ['reset', 'C↦2', 'C']  # overwriting parent reset
1323]
1324
1325# we rely on auto transitions
1326machine = HierarchicalMachine(states=states, transitions=transitions, initial='A')
1327machine.to_B()  # exit state A, enter state B
1328machine.to_C()  # exit B, enter C
1329machine.to_C.s3.a()  # enter C↦a; enter C↦3↦a;
1330machine.state
1331>>> 'C↦3↦a'
1332assert machine.is_C.s3.a()
1333machine.to('C↦2')  # not interactive; exit C↦3↦a, exit C↦3, enter C↦2
1334machine.reset()  # exit C↦2; reset C has been overwritten by C↦3
1335machine.state
1336>>> 'C'
1337machine.reset()  # exit C, enter A
1338machine.state
1339>>> 'A'
1340# s.on_enter('C↦3↦a', 'callback_method')
1341```
1342
1343Instead of `to_C_3_a()` auto transition is called as `to_C.s3.a()`. If your substate starts with a digit, transitions adds a prefix 's' ('3' becomes 's3') to the auto transition `FunctionWrapper` to comply with the attribute naming scheme of Python.
1344If interactive completion is not required, `to('C↦3↦a')` can be called directly. Additionally, `on_enter/exit_<<state name>>` is replaced with `on_enter/exit(state_name, callback)`. State checks can be conducted in a similar fashion. Instead of `is_C_3_a()`, the `FunctionWrapper` variant `is_C.s3.a()` can be used.
1345
1346To check whether the current state is a substate of a specific state, `is_state` supports the keyword `allow_substates`:
1347
1348```python
1349machine.state
1350>>> 'C.2.a'
1351machine.is_C() # checks for specific states
1352>>> False
1353machine.is_C(allow_substates=True)
1354>>> True
1355assert machine.is_C.s2() is False
1356assert machine.is_C.s2(allow_substates=True)  # FunctionWrapper support allow_substate as well
1357```
1358
1359*new in 0.8.0*
1360You can use enumerations in HSMs as well but keep in mind that `Enum` are compared by value.
1361If you have a value more than once in a state tree those states cannot be distinguished.
1362
1363```python
1364states = [States.RED, States.YELLOW, {'name': States.GREEN, 'children': ['tick', 'tock']}]
1365states = ['A', {'name': 'B', 'children': states, 'initial': States.GREEN}, States.GREEN]
1366machine = HierarchicalMachine(states=states)
1367machine.to_B()
1368machine.is_GREEN()  # returns True even though the actual state is B_GREEN
1369```
1370
1371*new in 0.8.0*
1372`HierarchicalMachine` has been rewritten from scratch to support parallel states and better isolation of nested states.
1373This involves some tweaks based on community feedback.
1374To get an idea of processing order and configuration have a look at the following example:
1375
1376```python
1377from transitions.extensions.nesting import HierarchicalMachine
1378import logging
1379states = ['A', 'B', {'name': 'C', 'parallel': [{'name': '1', 'children': ['a', 'b', 'c'], 'initial': 'a',
1380                                                'transitions': [['go', 'a', 'b']]},
1381                                               {'name': '2', 'children': ['x', 'y', 'z'], 'initial': 'z'}],
1382                      'transitions': [['go', '2_z', '2_x']]}]
1383
1384transitions = [['reset', 'C_1_b', 'B']]
1385logging.basicConfig(level=logging.INFO)
1386machine = HierarchicalMachine(states=states, transitions=transitions, initial='A')
1387machine.to_C()
1388# INFO:transitions.extensions.nesting:Exited state A
1389# INFO:transitions.extensions.nesting:Entered state C
1390# INFO:transitions.extensions.nesting:Entered state C_1
1391# INFO:transitions.extensions.nesting:Entered state C_2
1392# INFO:transitions.extensions.nesting:Entered state C_1_a
1393# INFO:transitions.extensions.nesting:Entered state C_2_z
1394machine.go()
1395# INFO:transitions.extensions.nesting:Exited state C_1_a
1396# INFO:transitions.extensions.nesting:Entered state C_1_b
1397# INFO:transitions.extensions.nesting:Exited state C_2_z
1398# INFO:transitions.extensions.nesting:Entered state C_2_x
1399machine.reset()
1400# INFO:transitions.extensions.nesting:Exited state C_1_b
1401# INFO:transitions.extensions.nesting:Exited state C_2_x
1402# INFO:transitions.extensions.nesting:Exited state C_1
1403# INFO:transitions.extensions.nesting:Exited state C_2
1404# INFO:transitions.extensions.nesting:Exited state C
1405# INFO:transitions.extensions.nesting:Entered state B
1406```
1407
1408When using `parallel` instead of `children`, `transitions` will enter all states of the passed list at the same time.
1409Which substate to enter is defined by `initial` which should *always* point to a direct substate.
1410A novel feature is to define local transitions by passing the `transitions` keyword in a state definition.
1411The above defined transition `['go', 'a', 'b']` is only valid in `C_1`.
1412While you can reference substates as done in `['go', '2_z', '2_x']` you cannot reference parent states directly in locally defined transitions.
1413When a parent state is exited, its children will also be exited.
1414In addition to the processing order of transitions known from `Machine` where transitions are considered in the order they were added, `HierarchicalMachine` considers hierarchy as well.
1415Transitions defined in substates will be evaluated first (e.g. `C_1_a` is left before `C_2_z`) and transitions defined with wildcard `*` will (for now) only add transitions to root states (in this example `A`, `B`, `C`)
1416Starting with *0.8.0* nested states can be added directly and will issue the creation of parent states on-the-fly:
1417
1418```python
1419m = HierarchicalMachine(states=['A'], initial='A')
1420m.add_state('B_1_a')
1421m.to_B_1()
1422assert m.is_B(allow_substates=True)
1423```
1424
1425
1426#### Reuse of previously created HSMs
1427
1428Besides semantic order, nested states are very handy if you want to specify state machines for specific tasks and plan to reuse them.
1429Before *0.8.0*, a `HierarchicalMachine` would not integrate the machine instance itself but the states and transitions by creating copies of them.
1430However, since *0.8.0* `(Nested)State` instances are just **referenced** which means changes in one machine's collection of states and events will influence the other machine instance. Models and their state will not be shared though.
1431Note that events and transitions are also copied by reference and will be shared by both instances if you do not use the `remap` keyword.
1432This change was done to be more in line with `Machine` which also uses passed `State` instances by reference.
1433
1434```python
1435count_states = ['1', '2', '3', 'done']
1436count_trans = [
1437    ['increase', '1', '2'],
1438    ['increase', '2', '3'],
1439    ['decrease', '3', '2'],
1440    ['decrease', '2', '1'],
1441    ['done', '3', 'done'],
1442    ['reset', '*', '1']
1443]
1444
1445counter = Machine(states=count_states, transitions=count_trans, initial='1')
1446
1447counter.increase() # love my counter
1448states = ['waiting', 'collecting', {'name': 'counting', 'children': counter}]
1449
1450transitions = [
1451    ['collect', '*', 'collecting'],
1452    ['wait', '*', 'waiting'],
1453    ['count', 'collecting', 'counting']
1454]
1455
1456collector = Machine(states=states, transitions=transitions, initial='waiting')
1457collector.collect()  # collecting
1458collector.count()  # let's see what we got; counting_1
1459collector.increase()  # counting_2
1460collector.increase()  # counting_3
1461collector.done()  # collector.state == counting_done
1462collector.wait()  # collector.state == waiting
1463```
1464
1465If a `HierarchicalMachine` is passed with the `children` keyword, the initial state of this machine will be assigned to the new parent state.
1466In the above example we see that entering `counting` will also enter `counting_1`.
1467If this is undesired behaviour and the machine should rather halt in the parent state, the user can pass `initial` as `False` like `{'name': 'counting', 'children': counter, 'initial': False}`.
1468
1469Sometimes you want such an embedded state collection to 'return' which means after it is done it should exit and transit to one of your super states.
1470To achieve this behaviour you can remap state transitions.
1471In the example above we would like the counter to return if the state `done` was reached.
1472This is done as follows:
1473
1474```python
1475states = ['waiting', 'collecting', {'name': 'counting', 'children': counter, 'remap': {'done': 'waiting'}}]
1476
1477... # same as above
1478
1479collector.increase() # counting_3
1480collector.done()
1481collector.state
1482>>> 'waiting' # be aware that 'counting_done' will be removed from the state machine
1483```
1484
1485As mentioned above, using `remap` will **copy** events and transitions since they could not be valid in the original state machine.
1486If a reused state machine does not have a final state, you can of course add the transitions manually.
1487If 'counter' had no 'done' state, we could just add `['done', 'counter_3', 'waiting']` to achieve the same behaviour.
1488
1489In cases where you want states and transitions to be copied by value rather than reference (for instance, if you want to keep the pre-0.8 behaviour) you can do so by creating a `NestedState` and assign deep copies of the machine's events and states to it.
1490
1491```python
1492from transitions.extensions.nesting import NestedState
1493from copy import deepcopy
1494
1495# ... configuring and creating counter
1496
1497counting_state = NestedState(name="counting", initial='1')
1498counting_state.states = deepcopy(counter.states)
1499counting_state.events = deepcopy(counter.events)
1500
1501states = ['waiting', 'collecting', counting_state]
1502```
1503
1504For complex state machines, sharing configurations rather than instantiated machines might be more feasible.
1505Especially since instantiated machines must be derived from `HierarchicalMachine`.
1506Such configurations can be stored and loaded easily via JSON or YAML (see the [FAQ](examples/Frequently%20asked%20questions.ipynb)).
1507`HierarchicalMachine` allows defining substates either with the keyword `children` or `states`.
1508If both are present, only `children` will be considered.
1509
1510```python
1511counter_conf = {
1512    'name': 'counting',
1513    'states': ['1', '2', '3', 'done'],
1514    'transitions': [
1515        ['increase', '1', '2'],
1516        ['increase', '2', '3'],
1517        ['decrease', '3', '2'],
1518        ['decrease', '2', '1'],
1519        ['done', '3', 'done'],
1520        ['reset', '*', '1']
1521    ],
1522    'initial': '1'
1523}
1524
1525collector_conf = {
1526    'name': 'collector',
1527    'states': ['waiting', 'collecting', counter_conf],
1528    'transitions': [
1529        ['collect', '*', 'collecting'],
1530        ['wait', '*', 'waiting'],
1531        ['count', 'collecting', 'counting']
1532    ],
1533    'initial': 'waiting'
1534}
1535
1536collector = Machine(**collector_conf)
1537collector.collect()
1538collector.count()
1539collector.increase()
1540assert collector.is_counting_2()
1541```
1542
1543#### <a name="threading"></a> Threadsafe(-ish) State Machine
1544
1545In cases where event dispatching is done in threads, one can use either `LockedMachine` or `LockedHierarchicalMachine` where **function access** (!sic) is secured with reentrant locks.
1546This does not save you from corrupting your machine by tinkering with member variables of your model or state machine.
1547
1548```python
1549from transitions.extensions import LockedMachine as Machine
1550from threading import Thread
1551import time
1552
1553states = ['A', 'B', 'C']
1554machine = Machine(states=states, initial='A')
1555
1556# let us assume that entering B will take some time
1557thread = Thread(target=machine.to_B)
1558thread.start()
1559time.sleep(0.01) # thread requires some time to start
1560machine.to_C() # synchronized access; won't execute before thread is done
1561# accessing attributes directly
1562thread = Thread(target=machine.to_B)
1563thread.start()
1564machine.new_attrib = 42 # not synchronized! will mess with execution order
1565```
1566
1567Any python context manager can be passed in via the `machine_context` keyword argument:
1568
1569```python
1570from transitions.extensions import LockedMachine as Machine
1571from threading import RLock
1572
1573states = ['A', 'B', 'C']
1574
1575lock1 = RLock()
1576lock2 = RLock()
1577
1578machine = Machine(states=states, initial='A', machine_context=[lock1, lock2])
1579```
1580
1581Any contexts via `machine_model` will be shared between all models registered with the `Machine`.
1582Per-model contexts can be added as well:
1583
1584```python
1585lock3 = RLock()
1586
1587machine.add_model(model, model_context=lock3)
1588```
1589
1590It's important that all user-provided context managers are re-entrant since the state machine will call them multiple
1591times, even in the context of a single trigger invocation.
1592
1593
1594#### <a name="async"></a> Using async callbacks
1595
1596If you are using Python 3.7 or later, you can use `AsyncMachine` to work with asynchronous callbacks.
1597You can mix synchronous and asynchronous callbacks if you like but this may have undesired side effects.
1598Note that events need to be awaited and the event loop must also be handled by you.
1599
1600```python
1601from transitions.extensions.asyncio import AsyncMachine
1602import asyncio
1603import time
1604
1605
1606class AsyncModel:
1607
1608    def prepare_model(self):
1609        print("I am synchronous.")
1610        self.start_time = time.time()
1611
1612    async def before_change(self):
1613        print("I am asynchronous and will block now for 100 milliseconds.")
1614        await asyncio.sleep(0.1)
1615        print("I am done waiting.")
1616
1617    def sync_before_change(self):
1618        print("I am synchronous and will block the event loop (what I probably shouldn't)")
1619        time.sleep(0.1)
1620        print("I am done waiting synchronously.")
1621
1622    def after_change(self):
1623        print(f"I am synchronous again. Execution took {int((time.time() - self.start_time) * 1000)} ms.")
1624
1625
1626transition = dict(trigger="start", source="Start", dest="Done", prepare="prepare_model",
1627                  before=["before_change"] * 5 + ["sync_before_change"],
1628                  after="after_change")  # execute before function in asynchronously 5 times
1629model = AsyncModel()
1630machine = AsyncMachine(model, states=["Start", "Done"], transitions=[transition], initial='Start')
1631
1632asyncio.get_event_loop().run_until_complete(model.start())
1633# >>> I am synchronous.
1634#     I am asynchronous and will block now for 100 milliseconds.
1635#     I am asynchronous and will block now for 100 milliseconds.
1636#     I am asynchronous and will block now for 100 milliseconds.
1637#     I am asynchronous and will block now for 100 milliseconds.
1638#     I am asynchronous and will block now for 100 milliseconds.
1639#     I am synchronous and will block the event loop (what I probably shouldn't)
1640#     I am done waiting synchronously.
1641#     I am done waiting.
1642#     I am done waiting.
1643#     I am done waiting.
1644#     I am done waiting.
1645#     I am done waiting.
1646#     I am synchronous again. Execution took 101 ms.
1647assert model.is_Done()
1648```
1649
1650So, why do you need to use Python 3.7 or later you may ask.
1651Async support has been introduced earlier.
1652`AsyncMachine` makes use of `contextvars` to handle running callbacks when new events arrive before a transition
1653has been finished:
1654
1655```python
1656async def await_never_return():
1657    await asyncio.sleep(100)
1658    raise ValueError("That took too long!")
1659
1660async def fix():
1661    await m2.fix()
1662
1663m1 = AsyncMachine(states=['A', 'B', 'C'], initial='A', name="m1")
1664m2 = AsyncMachine(states=['A', 'B', 'C'], initial='A', name="m2")
1665m2.add_transition(trigger='go', source='A', dest='B', before=await_never_return)
1666m2.add_transition(trigger='fix', source='A', dest='C')
1667m1.add_transition(trigger='go', source='A', dest='B', after='go')
1668m1.add_transition(trigger='go', source='B', dest='C', after=fix)
1669asyncio.get_event_loop().run_until_complete(asyncio.gather(m2.go(), m1.go()))
1670
1671assert m1.state == m2.state
1672```
1673
1674This example illustrates actually two things:
1675First, that 'go' called in m1's transition from `A` to be `B` is not cancelled and second, calling `m2.fix()` will
1676halt the transition attempt of m2 from `A` to `B` by executing 'fix' from `A` to `C`.
1677This separation would not be possible without `contextvars`.
1678Note that `prepare` and `conditions` are NOT treated as ongoing transitions.
1679This means that after `conditions` have been evaluated, a transition is executed even though another event already happened.
1680Tasks will only be cancelled when run as a `before` callback or later.
1681
1682`AsyncMachine` features a model-special queue mode which can be used when `queued='model'` is passed to the constructor.
1683With model-specific queue, events will only be queued when they belong to the same model.
1684Furthermore, a raised exception will only clear the event queue of the model that raised that exception.
1685For the sake of simplicity, let's assume that every event in `asyncion.gather` below is not triggered at the same time but slightly delayed:
1686
1687```python
1688asyncio.gather(model1.event1(), model1.event2(), model2.event1())
1689# execution order with AsyncMachine(queued=True)
1690# model1.event1 -> model1.event2 -> model2.event1
1691# execution order with AsyncMachine(queued='model')
1692# (model1.event1, model2.event1) -> model1.event2
1693
1694asyncio.gather(model1.event1(), model1.error(), model1.event3(), model2.event1(), model2.event2(), model2.event3())
1695# execution order with AsyncMachine(queued=True)
1696# model1.event1 -> model1.error
1697# execution order with AsyncMachine(queued='model')
1698# (model1.event1, model2.event1) -> (model1.error, model2.event2) -> model2.event3
1699```
1700
1701Note that queue modes must not be changed after machine construction.
1702
1703#### <a name="state-features"></a>Adding features to states
1704
1705If your superheroes need some custom behaviour, you can throw in some extra functionality by decorating machine states:
1706
1707```python
1708from time import sleep
1709from transitions import Machine
1710from transitions.extensions.states import add_state_features, Tags, Timeout
1711
1712
1713@add_state_features(Tags, Timeout)
1714class CustomStateMachine(Machine):
1715    pass
1716
1717
1718class SocialSuperhero(object):
1719    def __init__(self):
1720        self.entourage = 0
1721
1722    def on_enter_waiting(self):
1723        self.entourage += 1
1724
1725
1726states = [{'name': 'preparing', 'tags': ['home', 'busy']},
1727          {'name': 'waiting', 'timeout': 1, 'on_timeout': 'go'},
1728          {'name': 'away'}]  # The city needs us!
1729
1730transitions = [['done', 'preparing', 'waiting'],
1731               ['join', 'waiting', 'waiting'],  # Entering Waiting again will increase our entourage
1732               ['go', 'waiting', 'away']]  # Okay, let' move
1733
1734hero = SocialSuperhero()
1735machine = CustomStateMachine(model=hero, states=states, transitions=transitions, initial='preparing')
1736assert hero.state == 'preparing'  # Preparing for the night shift
1737assert machine.get_state(hero.state).is_busy  # We are at home and busy
1738hero.done()
1739assert hero.state == 'waiting'  # Waiting for fellow superheroes to join us
1740assert hero.entourage == 1  # It's just us so far
1741sleep(0.7)  # Waiting...
1742hero.join()  # Weeh, we got company
1743sleep(0.5)  # Waiting...
1744hero.join()  # Even more company \o/
1745sleep(2)  # Waiting...
1746assert hero.state == 'away'  # Impatient superhero already left the building
1747assert machine.get_state(hero.state).is_home is False  # Yupp, not at home anymore
1748assert hero.entourage == 3  # At least he is not alone
1749```
1750
1751Currently, transitions comes equipped with the following state features:
1752
1753* **Timeout** -- triggers an event after some time has passed
1754    - keyword: `timeout` (int, optional) -- if passed, an entered state will timeout after `timeout` seconds
1755    - keyword: `on_timeout` (string/callable, optional) -- will be called when timeout time has been reached
1756    - will raise an `AttributeError` when `timeout` is set but `on_timeout` is not
1757    - Note: A timeout is triggered in a thread. This implies several limitations (e.g. catching Exceptions raised in timeouts). Consider an event queue for more sophisticated applications.
1758
1759* **Tags** -- adds tags to states
1760    - keyword: `tags` (list, optional) -- assigns tags to a state
1761    - `State.is_<tag_name>` will return `True` when the state has been tagged with `tag_name`, else `False`
1762
1763* **Error** -- raises a `MachineError` when a state cannot be left
1764    - inherits from `Tags` (if you use `Error` do not use `Tags`)
1765    - keyword: `accepted` (bool, optional) -- marks a state as accepted
1766    - alternatively the keyword `tags` can be passed, containing 'accepted'
1767    - Note: Errors will only be raised if `auto_transitions` has been set to `False`. Otherwise every state can be exited with `to_<state>` methods.
1768
1769* **Volatile** -- initialises an object every time a state is entered
1770    - keyword: `volatile` (class, optional) -- every time the state is entered an object of type class will be assigned to the model. The attribute name is defined by `hook`. If omitted, an empty VolatileObject will be created instead
1771    - keyword: `hook` (string, default='scope') -- The model's attribute name fore the temporal object.
1772
1773You can write your own `State` extensions and add them the same way. Just note that `add_state_features` expects *Mixins*. This means your extension should always call the overridden methods `__init__`, `enter` and `exit`. Your extension may inherit from *State* but will also work without it.
1774Using `@add_state_features` has a drawback which is that decorated machines cannot be pickled (more precisely, the dynamically generated `CustomState` cannot be pickled).
1775This might be a reason to write a dedicated custom state class instead.
1776Depending on the chosen state machine, your custom state class may need to provide certain state features. For instance, `HierarchicalMachine` requires your custom state to be an instance of `NestedState` (`State` is not sufficient). To inject your states you can either assign them to your `Machine`'s class attribute `state_cls` or override `Machine.create_state` in case you need some specific procedures done whenever a state is created:
1777
1778```python
1779from transitions import Machine, State
1780
1781class MyState(State):
1782    pass
1783
1784class CustomMachine(Machine):
1785    # Use MyState as state class
1786    state_cls = MyState
1787
1788
1789class VerboseMachine(Machine):
1790
1791    # `Machine._create_state` is a class method but we can
1792    # override it to be an instance method
1793    def _create_state(self, *args, **kwargs):
1794        print("Creating a new state with machine '{0}'".format(self.name))
1795        return MyState(*args, **kwargs)
1796```
1797
1798If you want to avoid threads in your `AsyncMachine` entirely, you can replace the `Timeout` state feature with `AsyncTimeout` from the `asyncio` extension:
1799
1800```python
1801import asyncio
1802from transitions.extensions.states import add_state_features
1803from transitions.extensions.asyncio import AsyncTimeout, AsyncMachine
1804
1805@add_state_features(AsyncTimeout)
1806class TimeoutMachine(AsyncMachine):
1807    pass
1808
1809states = ['A', {'name': 'B', 'timeout': 0.2, 'on_timeout': 'to_C'}, 'C']
1810m = TimeoutMachine(states=states, initial='A', queued=True)  # see remark below
1811asyncio.run(asyncio.wait([m.to_B(), asyncio.sleep(0.1)]))
1812assert m.is_B()  # timeout shouldn't be triggered
1813asyncio.run(asyncio.wait([m.to_B(), asyncio.sleep(0.3)]))
1814assert m.is_C()   # now timeout should have been processed
1815```
1816
1817You should consider passing `queued=True` to the `TimeoutMachine` constructor. This will make sure that events are processed sequentially and avoid asynchronous [racing conditions](https://github.com/pytransitions/transitions/issues/459) that may appear when timeout and event happen in close proximity.
1818
1819#### <a name="django-support"></a> Using transitions together with Django
1820
1821You can have a look at the [FAQ](examples/Frequently%20asked%20questions.ipynb) for some inspiration or checkout `django-transitions`.
1822It has been developed by Christian Ledermann and is also hosted on [Github](https://github.com/PrimarySite/django-transitions).
1823[The documentation](https://django-transitions.readthedocs.io/en/latest/) contains some usage examples.
1824
1825
1826### <a name="bug-reports"></a>I have a [bug report/issue/question]...
1827
1828First, congratulations! You reached the end of the documentation!
1829If you want to try out `transitions` before you install it, you can do that in an interactive Jupyter notebook at mybinder.org.
1830Just click this button �� [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pytransitions/transitions/master?filepath=examples%2FPlayground.ipynb).
1831
1832For bug reports and other issues, please open an issue on GitHub.
1833
1834For usage questions, post on Stack Overflow, making sure to tag your question with the [`pytransitions` tag](https://stackoverflow.com/questions/tagged/pytransitions). Do not forget to have a look at the [extended examples](./examples)!
1835
1836For any other questions, solicitations, or large unrestricted monetary gifts, email [Tal Yarkoni](mailto:tyarkoni@gmail.com).
1837