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