Invalid Block Tag On Did You Forget To Register Or Load This Tag?
Quickstart
They say a good example is worth 100 pages of API documentation, a meg directives, or a thousand words.
Well, "they" probably prevarication... but here'south an instance anyhow:
from transitions import Auto import random class NarcolepticSuperhero ( object ): # Ascertain some states. Nearly of the time, narcoleptic superheroes are just similar # everyone else. Except for... states = [ 'comatose' , 'hanging out' , 'hungry' , 'sweaty' , 'saving the world' ] def __init__ ( self , name ): # No anonymous superheroes on my spotter! Every narcoleptic superhero gets # a proper name. Any name at all. SleepyMan. SlumberGirl. You go the idea. self . name = name # What have nosotros accomplished today? self . kittens_rescued = 0 # Initialize the state machine self . motorcar = Machine ( model = cocky , states = NarcolepticSuperhero . states , initial = 'asleep' ) # Add some transitions. Nosotros could likewise define these using a static listing of # dictionaries, as we did with states above, and then pass the list to # the Car initializer as the transitions= statement. # At some point, every superhero must rise and shine. cocky . machine . add_transition ( trigger = 'wake_up' , source = 'asleep' , dest = 'hanging out' ) # Superheroes need to go on in shape. cocky . auto . add_transition ( 'work_out' , 'hanging out' , 'hungry' ) # Those calories won't replenish themselves! self . machine . add_transition ( 'eat' , 'hungry' , 'hanging out' ) # Superheroes are always on call. E'er. But they're not ever # dressed in work-appropriate habiliment. self . machine . add_transition ( 'distress_call' , '*' , 'saving the earth' , before = 'change_into_super_secret_costume' ) # When they get off work, they're all sweaty and disgusting. But before # they do anything else, they take to meticulously log their latest # escapades. Because the legal department says so. self . machine . add_transition ( 'complete_mission' , 'saving the world' , 'sweaty' , afterward = 'update_journal' ) # Sweat is a disorder that can be remedied with water. # Unless y'all've had a particularly long twenty-four hours, in which case... bed time! self . machine . add_transition ( 'clean_up' , 'sweaty' , 'asleep' , conditions = [ 'is_exhausted' ]) self . machine . add_transition ( 'clean_up' , 'sweaty' , 'hanging out' ) # Our NarcolepticSuperhero tin fall comatose at pretty much whatsoever time. self . machine . add_transition ( 'nap' , '*' , 'asleep' ) def update_journal ( cocky ): """ Dear Diary, today I saved Mr. Whiskers. Again. """ cocky . kittens_rescued += 1 @belongings def is_exhausted ( cocky ): """ Basically a coin toss. """ return random . random () < 0.5 def change_into_super_secret_costume ( self ): print ( "Dazzler, eh?" )
There, now you lot've baked a land motorcar into NarcolepticSuperhero
. Allow'south have him/her/it out for a spin...
>>> batman = NarcolepticSuperhero ( "Batman" ) >>> batman . state 'asleep' >>> batman . wake_up () >>> batman . state 'hanging out' >>> batman . nap () >>> batman . state 'asleep' >>> batman . clean_up () MachineError : "Tin can't trigger event clean_up from land comatose!" >>> batman . wake_up () >>> batman . work_out () >>> batman . country 'hungry' # Batman withal hasn't done anything useful... >>> batman . kittens_rescued 0 # We now accept you alive to the scene of a horrific kitten entreement... >>> batman . distress_call () 'Dazzler, eh?' >>> batman . state 'saving the world' # Back to the crib. >>> batman . complete_mission () >>> batman . country 'sweaty' >>> batman . clean_up () >>> batman . state 'asleep' # Besides tired to shower! # Another productive day, Alfred. >>> batman . kittens_rescued ane
While we cannot read the heed of the actual batman, we surely can visualize the electric current land of our NarcolepticSuperhero
.
Have a wait at the Diagrams extensions if yous want to know how.
The not-quickstart
Basic initialization
Getting a state machine up and running is pretty uncomplicated. Allow's say yous have the object lump
(an instance of class Matter
), and you want to manage its states:
course Matter ( object ): laissez passer lump = Matter ()
You can initialize a (minimal) working state machine bound to lump
like this:
from transitions import Machine machine = Machine ( model = lump , states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ], initial = 'solid' ) # Lump at present has state! lump . land >>> 'solid'
I say "minimal", because while this state machine is technically operational, information technology doesn't actually do annihilation. It starts in the 'solid'
state, simply won't ever move into another state, because no transitions are defined... however!
Permit'due south try again.
# The states states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ] # And some transitions between states. We're lazy, and then we'll leave out # the changed phase transitions (freezing, condensation, etc.). transitions = [ { 'trigger' : 'melt' , 'source' : 'solid' , 'dest' : 'liquid' }, { 'trigger' : 'evaporate' , 'source' : 'liquid' , 'dest' : 'gas' }, { 'trigger' : 'sublimate' , 'source' : 'solid' , 'dest' : 'gas' }, { 'trigger' : 'ionize' , 'source' : 'gas' , 'dest' : 'plasma' } ] # Initialize car = Car ( lump , states = states , transitions = transitions , initial = 'liquid' ) # Now lump maintains state... lump . state >>> 'liquid' # And that country can change... lump . evaporate () lump . land >>> 'gas' lump . trigger ( 'ionize' ) lump . state >>> 'plasma'
Find the shiny new methods fastened to the Affair
instance (evaporate()
, ionize()
, etc.). Each method triggers the corresponding transition. You lot 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 example, lump
). To be more precise, your model should not already contain methods with the same name as event triggers since transitions
volition just attach convenience methods to your model if the spot is not already taken. If you want to modify that behaviour, have a look at the FAQ. Furthermore, in that location is a method called trigger
at present attached to your model (if it hasn't been there before). This method lets you execute transitions by name in example dynamic triggering is required.
States
The 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 past passing a list of strings to the Machine
initializer. But internally, states are really represented equally State
objects.
Yous tin can initialize and change States in a number of ways. Specifically, you can:
- pass a string to the
Car
initializer giving the name(s) of the land(s), or - directly initialize each new
State
object, or - pass a lexicon with initialization arguments
The following snippets illustrate several ways to achieve the same goal:
# import Machine and Land course from transitions import Machine , State # Create a list of three states to pass to the Automobile # initializer. We tin can mix types; in this case, nosotros # laissez passer one State, one string, and i dict. states = [ Land ( name = 'solid' ), 'liquid' , { 'proper name' : 'gas' } ] machine = Machine ( lump , states ) # This alternative example illustrates more explicit # improver of states and state callbacks, simply the net # issue is identical to the in a higher place. automobile = Machine ( lump ) solid = State ( 'solid' ) liquid = State ( 'liquid' ) gas = Land ( 'gas' ) car . add_states ([ solid , liquid , gas ])
States are initialized in one case when added to the car and will persist until they are removed from it. In other words: if you alter the attributes of a state object, this modify will NOT exist reset the next time yous enter that state. Take a expect at how to extend land features in instance you require some other behaviour.
Callbacks
A State
can also be associated with a list of enter
and get out
callbacks, which are called whenever the country car enters or leaves that land. You can specify callbacks during initialization by passing them to a State
object constructor, in a state property dictionary, or add together them afterward.
For convenience, whenever a new State
is added to a Machine
, the methods on_enter_«land proper name»
and on_exit_«land name»
are dynamically created on the Machine (not on the model!), which permit you to dynamically add together new enter and leave callbacks afterward if you need them.
# Our erstwhile Affair grade, now with a couple of new methods we # can trigger when entering or exit states. class Thing ( object ): def say_hello ( cocky ): print ( "hello, new state!" ) def say_goodbye ( cocky ): print ( "goodbye, old state!" ) lump = Thing () # Same states as above, but now we give StateA an exit callback states = [ State ( name = 'solid' , on_exit = [ 'say_goodbye' ]), 'liquid' , { 'name' : 'gas' , 'on_exit' : [ 'say_goodbye' ]} ] machine = Automobile ( lump , states = states ) automobile . add_transition ( 'sublimate' , 'solid' , 'gas' ) # Callbacks can also be added after initialization using # the dynamically added on_enter_ and on_exit_ methods. # Note that the initial call to add the callback is made # on the Machine and not on the model. machine . on_enter_gas ( 'say_hello' ) # Test out the callbacks... machine . set_state ( 'solid' ) lump . sublimate () >>> 'goodbye, former state!' >>> 'hullo, new state!'
Note that on_enter_«state name»
callback volition non fire when a Machine is first initialized. For example if y'all have an on_enter_A()
callback defined, and initialize the Machine
with initial='A'
, on_enter_A()
will non be fired until the side by side time you enter land A
. (If you demand to brand certain on_enter_A()
fires at initialization, you tin simply create a dummy initial state and then explicitly phone call to_A()
inside the __init__
method.)
In addition to passing in callbacks when initializing a Land
, or adding them dynamically, it's also possible to define callbacks in the model form itself, which may increase lawmaking clarity. For example:
course Matter ( object ): def say_hello ( cocky ): print ( "howdy, new country!" ) def say_goodbye ( self ): impress ( "goodbye, old state!" ) def on_enter_A ( self ): print ( "Nosotros've just entered state A!" ) lump = Matter () auto = Car ( lump , states = [ 'A' , 'B' , 'C' ])
Now, any time lump
transitions to land A
, the on_enter_A()
method divers in the Thing
class will fire.
Checking land
You can always check the current state of the model past either:
- inspecting the
.land
aspect, or - calling
is_«land name»()
And if you want to think the bodily Country
object for the electric current state, you can do that through the Machine
instance's get_state()
method.
lump . state >>> 'solid' lump . is_gas () >>> False lump . is_solid () >>> True auto . get_state ( lump . land ) . proper noun >>> 'solid'
If you'd similar you can choose your own land aspect name by passing the model_attribute
statement while initializing the Machine
. This volition also modify the name of is_«land name»()
to is_«model_attribute»_«country name»()
though. Similarly, auto transitions will be named to_«model_attribute»_«state name»()
instead of to_«state proper noun»()
. This is done to allow multiple machines to work on the same model with private state attribute names.
lump = Thing () motorcar = Machine ( lump , states = [ 'solid' , 'liquid' , 'gas' ], model_attribute = 'matter_state' , initial = 'solid' ) lump . matter_state >>> 'solid' # with a custom 'model_attribute', states can likewise be checked like this: lump . is_matter_state_solid () >>> True lump . to_matter_state_gas () >>> Truthful
Enumerations
And so far we have seen how we tin give land names and use these names to work with our state auto. If you lot favour stricter typing and more IDE code completion (or you just can't type 'sesquipedalophobia' whatsoever longer because the word scares you) using Enumerations might be what yous are looking for:
import enum # Python 2.7 users need to accept 'enum34' installed from transitions import Automobile class States ( enum . Enum ): ERROR = 0 Cerise = 1 YELLOW = two GREEN = 3 transitions = [[ 'proceed' , States . RED , States . Yellowish ], [ 'proceed' , States . Yellowish , States . GREEN ], [ 'mistake' , '*' , States . ERROR ]] m = Car ( states = States , transitions = transitions , initial = States . Cherry-red ) assert g . is_RED () affirm yard . country is States . RED state = m . get_state ( States . Blood-red ) # become transitions.Land object print ( state . name ) # >>> Scarlet grand . proceed () m . proceed () assert m . is_GREEN () thousand . error () assert m . state is States . ERROR
You can mix enums and strings if yous like (east.g. [States.RED, 'Orangish', States.Yellow, States.GREEN]
) but notation that internally, transitions
will still handle states by name (enum.Enum.proper name
). Thus, it is not possible to have the states 'GREEN'
and States.GREEN
at the same time.
Transitions
Some of the higher up examples already illustrate the apply of transitions in passing, but here we'll explore them in more detail.
As 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. Nosotros already saw this above:
transitions = [ { 'trigger' : 'melt' , 'source' : 'solid' , 'dest' : 'liquid' }, { 'trigger' : 'evaporate' , 'source' : 'liquid' , 'dest' : 'gas' }, { 'trigger' : 'sublimate' , 'source' : 'solid' , 'dest' : 'gas' }, { 'trigger' : 'ionize' , 'source' : 'gas' , 'dest' : 'plasma' } ] machine = Machine ( model = Matter (), states = states , transitions = transitions )
Defining transitions in dictionaries has the do good of clarity, just tin can exist cumbersome. If you're after brevity, you might choose to ascertain transitions using lists. Just brand sure that the elements in each list are in the same order as the positional arguments in the Transition
initialization (i.east., trigger
, source
, destination
, etc.).
The following list-of-lists is functionally equivalent to the list-of-dictionaries above:
transitions = [ [ 'melt' , 'solid' , 'liquid' ], [ 'evaporate' , 'liquid' , 'gas' ], [ 'sublimate' , 'solid' , 'gas' ], [ 'ionize' , 'gas' , 'plasma' ] ]
Alternatively, y'all can add together transitions to a Automobile
after initialization:
machine = Machine ( model = lump , states = states , initial = 'solid' ) motorcar . add_transition ( 'melt' , source = 'solid' , dest = 'liquid' )
The 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:
>>> lump . melt () >>> lump . state 'liquid'
Past default, calling an invalid trigger volition raise an exception:
>>> lump . to_gas () >>> # This won't work because but objects in a solid state tin can melt >>> lump . melt () transitions . core . MachineError : "Tin't trigger event melt from state gas!"
This behavior is more often than not desirable, since it helps alert you to bug in your code. Simply in some cases, you might desire to silently ignore invalid triggers. You can do this by setting ignore_invalid_triggers=Truthful
(either on a land-by-country basis, or globally for all states):
>>> # Globally suppress invalid trigger exceptions >>> m = Machine ( lump , states , initial = 'solid' , ignore_invalid_triggers = True ) >>> # ...or suppress for only one grouping of states >>> states = [ 'new_state1' , 'new_state2' ] >>> m . add_states ( states , ignore_invalid_triggers = True ) >>> # ...or fifty-fifty just for a single country. Hither, exceptions will merely exist suppressed when the electric current state is A. >>> states = [ State ( 'A' , ignore_invalid_triggers = Truthful ), 'B' , 'C' ] >>> thousand = Machine ( lump , states ) >>> # ...this can be inverted as well if but one country should raise an exception >>> # since the machine'south global value is non applied to a previously initialized country. >>> states = [ 'A' , 'B' , State ( 'C' )] # the default value for 'ignore_invalid_triggers' is False >>> m = Automobile ( lump , states , ignore_invalid_triggers = True )
If yous need to know which transitions are valid from a certain state, you lot can use get_triggers
:
m.get_triggers('solid') >>> ['melt', 'sublimate'] m.get_triggers('liquid') >>> ['evaporate'] yard.get_triggers('plasma') >>> [] # y'all tin also query several states at in one case m.get_triggers('solid', 'liquid', 'gas', 'plasma') >>> ['melt', 'evaporate', 'sublimate', 'ionize']
Automatic transitions for all states
In addition to whatsoever transitions added explicitly, a to_«land»()
method is created automatically whenever a state is added to a Machine
example. This method transitions to the target state no matter which state the machine is currently in:
lump . to_liquid () lump . state >>> 'liquid' lump . to_solid () lump . state >>> 'solid'
If y'all want, you tin can disable this beliefs by setting auto_transitions=Fake
in the Car
initializer.
Transitioning from multiple states
A given trigger can exist attached to multiple transitions, some of which can potentially begin or end in the aforementioned state. For case:
machine . add_transition ( 'transmogrify' , [ 'solid' , 'liquid' , 'gas' ], 'plasma' ) machine . add_transition ( 'transmogrify' , 'plasma' , 'solid' ) # This next transition will never execute machine . add_transition ( 'transmogrify' , 'plasma' , 'gas' )
In this case, calling transmogrify()
will set the model's state to 'solid'
if it'southward currently 'plasma'
, and set information technology to 'plasma'
otherwise. (Annotation that merely the beginning matching transition will execute; thus, the transition divers in the final line higher up won't do anything.)
You can also make a trigger cause a transition from all states to a particular destination past using the '*'
wildcard:
auto . add_transition ( 'to_liquid' , '*' , 'liquid' )
Note that wildcard transitions will only utilize to states that exist at the fourth dimension 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.
Reflexive transitions from multiple states
A reflexive trigger (trigger that has the aforementioned country every bit source and destination) tin can hands exist added specifying =
as destination. This is handy if the same reflexive trigger should exist added to multiple states. For instance:
automobile . add_transition ( 'touch' , [ 'liquid' , 'gas' , 'plasma' ], '=' , after = 'change_shape' )
This will add reflexive transitions for all three states with touch()
as trigger and with change_shape
executed after each trigger.
Internal transitions
In contrast to reflexive transitions, internal transitions will never really leave the state. This ways that transition-related callbacks such as earlier
or after
will be candy while state-related callbacks exit
or enter
volition non. To define a transition to be internal, set the destination to None
.
machine . add_transition ( 'internal' , [ 'liquid' , 'gas' ], None , later on = 'change_shape' )
Ordered transitions
A common desire is for country 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).
To facilitate this beliefs, Transitions provides an add_ordered_transitions()
method in the Machine
class:
states = [ 'A' , 'B' , 'C' ] # Encounter the "culling initialization" section for an explanation of the 1st argument to init machine = Machine ( states = states , initial = 'A' ) machine . add_ordered_transitions () car . next_state () print ( automobile . state ) >>> 'B' # We can also ascertain a different club of transitions car = Machine ( states = states , initial = 'A' ) machine . add_ordered_transitions ([ 'A' , 'C' , 'B' ]) machine . next_state () print ( machine . land ) >>> 'C' # Conditions can be passed to 'add_ordered_transitions' also # If one condition is passed, it will be used for all transitions automobile = Machine ( states = states , initial = 'A' ) machine . add_ordered_transitions ( weather = 'check' ) # If a list is passed, it must contain exactly as many elements as the # motorcar contains states (A->B, ..., X->A) machine = Machine ( states = states , initial = 'A' ) machine . add_ordered_transitions ( weather condition = [ 'check_A2B' , ... , 'check_X2A' ]) # Conditions are always applied starting from the initial state machine = Motorcar ( states = states , initial = 'B' ) motorcar . add_ordered_transitions ( weather condition = [ 'check_B2C' , ... , 'check_A2B' ]) # With `loop=Faux`, the transition from the last state to the first state will be omitted (east.g. C->A) # When you also pass atmospheric condition, you need to pass one condition less (len(states)-1) machine = Motorcar ( states = states , initial = 'A' ) automobile . add_ordered_transitions ( loop = Faux ) machine . next_state () machine . next_state () machine . next_state () # transitions.cadre.MachineError: "Tin can't trigger event next_state from country C!"
Queued transitions
The default behaviour in Transitions is to procedure events instantly. This ways events inside an on_enter
method will be processed before callbacks bound to after
are called.
def go_to_C (): global motorcar machine . to_C () def after_advance (): impress ( "I am in state B now!" ) def entering_C (): print ( "I am in state C now!" ) states = [ 'A' , 'B' , 'C' ] machine = Car ( states = states , initial = 'A' ) # we want a message when state transition to B has been completed car . add_transition ( 'advance' , 'A' , 'B' , after = after_advance ) # call transition from state B to state C car . on_enter_B ( go_to_C ) # we also want a message when entering state C machine . on_enter_C ( entering_C ) automobile . advance () >>> 'I am in state C now!' >>> 'I am in country B now!' # what?
The execution guild of this example is
fix -> before -> on_enter_B -> on_enter_C -> afterward.
If queued processing is enabled, a transition will be finished before the next transition is triggered:
auto = Car ( states = states , queued = True , initial = 'A' ) ... automobile . advance () >>> 'I am in state B now!' >>> 'I am in state C at present!' # That's better!
This results in
prepare -> earlier -> on_enter_B -> queue(to_C) -> after -> on_enter_C.
Of import note: when processing events in a queue, the trigger call will always return Truthful
, since there is no mode to determine at queuing time whether a transition involving queued calls will ultimately complete successfully. This is true fifty-fifty when merely a single event is processed.
machine . add_transition ( 'spring' , 'A' , 'C' , atmospheric condition = 'will_fail' ) ... # queued=False car . spring () >>> False # queued=True motorcar . jump () >>> Truthful
When a model is removed from the machine, transitions
will likewise remove all related events from the queue.
form Model : def on_enter_B ( self ): self . to_C () # add event to queue ... self . automobile . remove_model ( self ) # aaaand it's gone
Provisional transitions
Sometimes yous but desire a item transition to execute if a specific condition occurs. You can do this by passing a method, or listing of methods, in the weather
argument:
# Our Thing grade, now with a bunch of methods that render booleans. class Affair ( object ): def is_flammable ( self ): render Faux def is_really_hot ( self ): return True motorcar . add_transition ( 'oestrus' , 'solid' , 'gas' , conditions = 'is_flammable' ) machine . add_transition ( 'heat' , 'solid' , 'liquid' , conditions = [ 'is_really_hot' ])
In the to a higher place example, calling oestrus()
when the model is in country 'solid'
will transition to state 'gas'
if is_flammable
returns True
. Otherwise, it volition transition to state 'liquid'
if is_really_hot
returns True
.
For convenience, there's also an 'unless'
argument that behaves exactly similar atmospheric condition, just inverted:
machine . add_transition ( 'heat' , 'solid' , 'gas' , unless = [ 'is_flammable' , 'is_really_hot' ])
In this case, the model would transition from solid to gas whenever heat()
fires, provided that both is_flammable()
and is_really_hot()
render Simulated
.
Note that status-checking methods will passively receive optional arguments and/or information objects passed to triggering methods. For instance, the following call:
lump . oestrus ( temp = 74 ) # equivalent to lump.trigger('heat', temp=74)
... would pass the temp=74
optional kwarg to the is_flammable()
bank check (possibly wrapped in an EventData
instance). For more than on this, run into the Passing data department below.
Callbacks
Y'all tin can attach callbacks to transitions as well as states. Every transition has 'before'
and 'after'
attributes that comprise a listing of methods to call before and after the transition executes:
class Matter ( object ): def make_hissing_noises ( cocky ): print ( "HISSSSSSSSSSSSSSSS" ) def disappear ( self ): print ( "where'd all the liquid get?" ) transitions = [ { 'trigger' : 'cook' , 'source' : 'solid' , 'dest' : 'liquid' , 'before' : 'make_hissing_noises' }, { 'trigger' : 'evaporate' , 'source' : 'liquid' , 'dest' : 'gas' , 'subsequently' : 'disappear' } ] lump = Matter () machine = Machine ( lump , states , transitions = transitions , initial = 'solid' ) lump . cook () >>> "HISSSSSSSSSSSSSSSS" lump . evaporate () >>> "where'd all the liquid go?"
In that location is also a 'fix'
callback that is executed as presently as a transition starts, before whatsoever 'conditions'
are checked or other callbacks are executed.
form Matter ( object ): rut = Faux attempts = 0 def count_attempts ( self ): self . attempts += one def heat_up ( self ): cocky . oestrus = random . random () < 0.25 def stats ( self ): impress ( 'It took y'all %i attempts to melt the lump!' % self . attempts ) @property def is_really_hot ( self ): return self . heat states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ] transitions = [ { 'trigger' : 'melt' , 'source' : 'solid' , 'dest' : 'liquid' , 'prepare' : [ 'heat_up' , 'count_attempts' ], 'weather' : 'is_really_hot' , 'after' : 'stats' }, ] lump = Affair () auto = Machine ( lump , states , transitions = transitions , initial = 'solid' ) lump . melt () lump . melt () lump . melt () lump . melt () >>> "It took you 4 attempts to melt the lump!"
Note that prepare
volition not be called unless the current land is a valid source for the named transition.
Default actions meant to be executed earlier or later every transition can be passed to Machine
during initialization with before_state_change
and after_state_change
respectively:
class Matter ( object ): def make_hissing_noises ( self ): print ( "HISSSSSSSSSSSSSSSS" ) def disappear ( self ): print ( "where'd all the liquid become?" ) states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ] lump = Affair () m = Car ( lump , states , before_state_change = 'make_hissing_noises' , after_state_change = 'disappear' ) lump . to_gas () >>> "HISSSSSSSSSSSSSSSS" >>> "where'd all the liquid go?"
There are likewise two keywords for callbacks which should be executed independently a) of how many transitions are possible, b) if whatsoever transition succeeds and c) even if an fault is raised during the execution of some other callback. Callbacks passed to Machine
with prepare_event
volition be executed once before processing possible transitions (and their individual prepare
callbacks) takes place. Callbacks of finalize_event
will be executed regardless of the success of the candy transitions. Note that if an mistake occurred it will be fastened to event_data
as error
and tin can be retrieved with send_event=True
.
from transitions import Machine class Matter ( object ): def raise_error ( self , upshot ): raise ValueError ( "Oh no" ) def prepare ( self , event ): print ( "I am ready!" ) def finalize ( self , consequence ): print ( "Result: " , type ( outcome . mistake ), event . mistake ) states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ] lump = Matter () g = Machine ( lump , states , prepare_event = 'prepare' , before_state_change = 'raise_error' , finalize_event = 'finalize' , send_event = Truthful ) endeavour : lump . to_gas () except ValueError : pass impress ( lump . state ) # >>> I am ready! # >>> Result: <class 'ValueError'> Oh no # >>> initial
Sometimes things just don't work out as intended and nosotros need to handle exceptions and make clean upwards the mess to go on things going. We can pass callbacks to on_exception
to practice this:
from transitions import Machine grade Affair ( object ): def raise_error ( self , result ): raise ValueError ( "Oh no" ) def handle_error ( self , result ): print ( "Fixing things ..." ) del event . mistake # it did non happen if nosotros cannot see it ... states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ] lump = Affair () k = Machine ( lump , states , before_state_change = 'raise_error' , on_exception = 'handle_error' , send_event = True ) try : lump . to_gas () except ValueError : laissez passer print ( lump . state ) # >>> Fixing things ... # >>> initial
Callable resolution
As you have probably already realized, the standard mode of passing callables to states, conditions and transitions is by proper noun. When processing callbacks and conditions, transitions
will utilise their name to think the related callable from the model. If the method cannot be retrieved and it contains dots, transitions
will treat the proper noun as a path to a module office and try to import it. Alternatively, yous can pass names of properties or attributes. They will be wrapped into functions only cannot receive event data for obvious reasons. You tin also pass callables such as (bound) functions directly. As mentioned before, yous tin as well pass lists/tuples of callables names to the callback parameters. Callbacks volition be executed in the order they were added.
from transitions import Machine from modern import imported_func import random class Model ( object ): def a_callback ( self ): imported_func () @property def a_property ( cocky ): """ Basically a coin toss. """ return random . random () < 0.v an_attribute = Fake model = Model () car = Machine ( model = model , states = [ 'A' ], initial = 'A' ) car . add_transition ( 'by_name' , 'A' , 'A' , conditions = 'a_property' , after = 'a_callback' ) machine . add_transition ( 'by_reference' , 'A' , 'A' , unless = [ 'a_property' , 'an_attribute' ], after = model . a_callback ) machine . add_transition ( 'imported' , 'A' , 'A' , later = 'mod.imported_func' ) model . by_name () model . by_reference () model . imported ()
The callable resolution is done in Machine.resolve_callable
. This method tin be overridden in case more circuitous callable resolution strategies are required.
Case
class CustomMachine ( Automobile ): @staticmethod def resolve_callable ( func , event_data ): # dispense arguments here and return func, or super() if no manipulation is done. super ( CustomMachine , CustomMachine ) . resolve_callable ( func , event_data )
Callback execution order
In summary, there are currently three means to trigger events. You tin can call a model'due south convenience functions similar lump.melt()
, execute triggers by name such equally lump.trigger("melt")
or dispatch events on multiple models with machine.acceleration("melt")
(run across section about multiple models in alternative initialization patterns). Callbacks on transitions are and then executed in the following order:
Callback | Current State | Comments |
---|---|---|
'machine.prepare_event' | source | executed once before individual transitions are processed |
'transition.prepare' | source | executed equally shortly as the transition starts |
'transition.conditions' | source | weather may fail and halt the transition |
'transition.unless' | source | conditions may fail and halt the transition |
'machine.before_state_change' | source | default callbacks alleged on model |
'transition.earlier' | source | |
'state.on_exit' | source | callbacks declared on the source state |
<Country CHANGE> | ||
'state.on_enter' | destination | callbacks declared on the destination land |
'transition.after' | destination | |
'machine.after_state_change' | destination | default callbacks alleged on model |
'motorcar.on_exception' | source/destination | callbacks will be executed when an exception has been raised |
'machine.finalize_event' | source/destination | callbacks will be executed even if no transition took identify or an exception has been raised |
If any callback raises an exception, the processing of callbacks is not continued. This ways that when an error occurs earlier the transition (in land.on_exit
or before), information technology is halted. In instance there is a raise after the transition has been conducted (in state.on_enter
or later), the land modify persists and no rollback is happening. Callbacks specified in motorcar.finalize_event
will always be executed unless the exception is raised by a finalizing callback itself. Notation that each callback sequence has to be finished before the side by side stage is executed. Blocking callbacks volition halt the execution gild and therefore block the trigger
or dispatch
call itself. If you desire callbacks to be executed in parallel, you could have a look at the extensions AsyncMachine
for asynchronous processing or LockedMachine
for threading.
Passing data
Sometimes you need to laissez passer the callback functions registered at machine initialization some information that reflects the model's electric current state. Transitions allows you to do this in two dissimilar ways.
First (the default), you tin can pass any positional or keyword arguments direct to the trigger methods (created when you call add_transition()
):
class Matter ( object ): def __init__ ( self ): self . set_environment () def set_environment ( cocky , temp = 0 , pressure = 101.325 ): cocky . temp = temp self . pressure = pressure def print_temperature ( self ): print ( "Electric current temperature is %d degrees celsius." % cocky . temp ) def print_pressure ( cocky ): print ( "Current pressure is %.2f kPa." % self . pressure ) lump = Thing () automobile = Machine ( lump , [ 'solid' , 'liquid' ], initial = 'solid' ) machine . add_transition ( 'melt' , 'solid' , 'liquid' , before = 'set_environment' ) lump . melt ( 45 ) # positional arg; # equivalent to lump.trigger('cook', 45) lump . print_temperature () >>> 'Current temperature is 45 degrees celsius.' automobile . set_state ( 'solid' ) # reset state so we can melt again lump . melt ( pressure = 300.23 ) # keyword args also work lump . print_pressure () >>> 'Current pressure is 300.23 kPa.'
You can pass any number of arguments you lot like to the trigger.
There is one important limitation to this arroyo: every callback function triggered by the land transition must be able to handle all of the arguments. This may crusade problems if the callbacks each expect somewhat different data.
To go effectually this, Transitions supports an alternate method for sending information. If you gear up send_event=Truthful
at Auto
initialization, all arguments to the triggers volition 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 outcome, in case you need to access these for anything.)
class Matter ( object ): def __init__ ( cocky ): cocky . temp = 0 self . force per unit area = 101.325 # Notation that the sole statement is now the EventData case. # This object stores positional arguments passed to the trigger method in the # .args belongings, and stores keywords arguments in the .kwargs dictionary. def set_environment ( self , event ): self . temp = event . kwargs . get ( 'temp' , 0 ) cocky . pressure = event . kwargs . become ( 'pressure' , 101.325 ) def print_pressure ( self ): impress ( "Current pressure level is %.2f kPa." % self . force per unit area ) lump = Matter () machine = Machine ( lump , [ 'solid' , 'liquid' ], send_event = True , initial = 'solid' ) auto . add_transition ( 'melt' , 'solid' , 'liquid' , before = 'set_environment' ) lump . melt ( temp = 45 , pressure level = 1853.68 ) # keyword args lump . print_pressure () >>> 'Current pressure is 1853.68 kPa.'
Culling initialization patterns
In all of the examples so far, we've attached a new Machine
instance to a split 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 tin as well become abrasive, since it requires yous to keep track of which methods are called on the land auto, and which ones are called on the model that the state machine is leap to (e.thou., lump.on_enter_StateA()
vs. car.add_transition()
).
Fortunately, Transitions is flexible, and supports ii other initialization patterns.
Kickoff, you can create a standalone land machine that doesn't require some other model at all. But omit the model argument during initialization:
auto = Machine ( states = states , transitions = transitions , initial = 'solid' ) automobile . melt () car . land >>> 'liquid'
If you initialize the car this fashion, you tin can and so adhere all triggering events (like evaporate()
, sublimate()
, etc.) and all callback functions directly to the Machine
instance.
This approach has the benefit of consolidating all of the state car functionality in ane place, just can feel a niggling scrap unnatural if you remember country logic should be contained within the model itself rather than in a divide controller.
An alternative (potentially improve) approach is to have the model inherit from the Machine
form. Transitions is designed to support inheritance seamlessly. (but be sure to override class Machine
's __init__
method!):
class Matter ( Machine ): def say_hello ( cocky ): print ( "hello, new state!" ) def say_goodbye ( self ): print ( "goodbye, sometime state!" ) def __init__ ( self ): states = [ 'solid' , 'liquid' , 'gas' ] Machine . __init__ ( self , states = states , initial = 'solid' ) cocky . add_transition ( 'melt' , 'solid' , 'liquid' ) lump = Affair () lump . state >>> 'solid' lump . melt () lump . state >>> 'liquid'
Here you become to consolidate all state machine functionality into your existing model, which often feels more natural than sticking all of the functionality we want in a separate standalone Machine
instance.
A auto tin can handle multiple models which can exist passed as a listing similar Machine(model=[model1, model2, ...])
. In cases where you lot want to add together models also as the machine example itself, y'all can pass the class variable placeholder (string) Motorcar.self_literal
during initialization like Machine(model=[Auto.self_literal, model1, ...])
. You can as well create a standalone automobile, and register models dynamically via automobile.add_model
by passing model=None
to the constructor. Furthermore, you lot can utilize machine.dispatch
to trigger events on all currently added models. Remember to call machine.remove_model
if machine is long-lasting and your models are temporary and should exist garbage collected:
class Matter (): pass lump1 = Thing () lump2 = Matter () # setting 'model' to None or passing an empty listing will initialize the machine without a model machine = Machine ( model = None , states = states , transitions = transitions , initial = 'solid' ) automobile . add_model ( lump1 ) auto . add_model ( lump2 , initial = 'liquid' ) lump1 . state >>> 'solid' lump2 . country >>> 'liquid' # custom events equally well as auto transitions tin exist dispatched to all models machine . dispatch ( "to_plasma" ) lump1 . country >>> 'plasma' assert lump1 . state == lump2 . state automobile . remove_model ([ lump1 , lump2 ]) del lump1 # lump1 is garbage nerveless del lump2 # lump2 is garbage collected
If you don't provide an initial state in the state car constructor, transitions
will create and add a default state called 'initial'
. If yous do not desire a default initial country, y'all tin can pass initial=None
. Notwithstanding, in this case you need to pass an initial state every time you lot add a model.
automobile = Machine ( model = None , states = states , transitions = transitions , initial = None ) automobile . add_model ( Matter ()) >>> "MachineError: No initial state configured for machine, must specify when adding model." motorcar . add_model ( Matter (), initial = 'liquid' )
Models with multiple states could adhere multiple machines using unlike model_attribute
values. Every bit mentioned in Checking land, this will add custom is/to_<model_attribute>_<state_name>
functions:
lump = Thing () matter_machine = Car ( lump , states = [ 'solid' , 'liquid' , 'gas' ], initial = 'solid' ) # add a 2nd machine to the same model merely assign a different land aspect shipment_machine = Automobile ( lump , states = [ 'delivered' , 'shipping' ], initial = 'delivered' , model_attribute = 'shipping_state' ) lump . state >>> 'solid' lump . is_solid () # check the default field >>> True lump . shipping_state >>> 'delivered' lump . is_shipping_state_delivered () # bank check the custom field. >>> True lump . to_shipping_state_shipping () >>> True lump . is_shipping_state_delivered () >>> False
Logging
Transitions includes very rudimentary logging capabilities. A number of events – namely, country changes, transition triggers, and conditional checks – are logged equally INFO-level events using the standard Python logging
module. This means you lot tin can hands configure logging to standard output in a script:
# Fix logging; The bones log level volition be DEBUG import logging logging . basicConfig ( level = logging . DEBUG ) # Fix transitions' log level to INFO; DEBUG messages will be omitted logging . getLogger ( 'transitions' ) . setLevel ( logging . INFO ) # Business every bit usual machine = Machine ( states = states , transitions = transitions , initial = 'solid' ) ...
(Re-)Storing machine instances
Machines are picklable and can be stored and loaded with pickle
. For Python iii.iii and earlier dill
is required.
import dill equally pickle # just required for Python three.3 and earlier thou = Machine ( states = [ 'A' , 'B' , 'C' ], initial = 'A' ) thousand . to_B () chiliad . state >>> B # store the machine dump = pickle . dumps ( thou ) # load the Machine instance over again m2 = pickle . loads ( dump ) m2 . state >>> B m2 . states . keys () >>> [ 'A' , 'B' , 'C' ]
Extensions
Even though the cadre of transitions is kept lightweight, at that place are a multifariousness of MixIns to extend its functionality. Currently supported are:
- Diagrams to visualize the current land of a automobile
- Hierarchical Land Machines for nesting and reuse
- Threadsafe Locks for parallel execution
- Async callbacks for asynchronous execution
- Custom States for extended state-related behaviour
There are 2 mechanisms to retrieve a state automobile example with the desired features enabled. The first approach makes employ of the convenience factory
with the 4 parameters graph
, nested
, locked
or asyncio
ready to Truthful
if the feature is required:
from transitions.extensions import MachineFactory # create a machine with mixins diagram_cls = MachineFactory . get_predefined ( graph = Truthful ) nested_locked_cls = MachineFactory . get_predefined ( nested = True , locked = Truthful ) async_machine_cls = MachineFactory . get_predefined ( asyncio = True ) # create instances from these classes # instances tin be used like simple machines machine1 = diagram_cls ( model , state , transitions ) machine2 = nested_locked_cls ( model , state , transitions )
This approach targets experimental employ since in this case the underlying classes exercise not accept to be known. Notwithstanding, classes can too be directly imported from transitions.extensions
. The naming scheme is as follows:
Diagrams | Nested | Locked | Asyncio | |
---|---|---|---|---|
Car | ✘ | ✘ | ✘ | ✘ |
GraphMachine | ✓ | ✘ | ✘ | ✘ |
HierarchicalMachine | ✘ | ✓ | ✘ | ✘ |
LockedMachine | ✘ | ✘ | ✓ | ✘ |
HierarchicalGraphMachine | ✓ | ✓ | ✘ | ✘ |
LockedGraphMachine | ✓ | ✘ | ✓ | ✘ |
LockedHierarchicalMachine | ✘ | ✓ | ✓ | ✘ |
LockedHierarchicalGraphMachine | ✓ | ✓ | ✓ | ✘ |
AsyncMachine | ✘ | ✘ | ✘ | ✓ |
AsyncGraphMachine | ✓ | ✘ | ✘ | ✓ |
HierarchicalAsyncMachine | ✘ | ✓ | ✘ | ✓ |
HierarchicalAsyncGraphMachine | ✓ | ✓ | ✘ | ✓ |
To use a characteristic-rich state machine, one could write:
from transitions.extensions import LockedHierarchicalGraphMachine as LHGMachine motorcar = LHGMachine ( model , states , transitions )
Diagrams
Additional Keywords:
-
title
(optional): Sets the championship of the generated prototype. -
show_conditions
(default False): Shows weather condition at transition edges -
show_auto_transitions
(default False): Shows motorcar transitions in graph -
show_state_attributes
(default False): Evidence callbacks (enter, exit), tags and timeouts in graph
Transitions tin generate basic state diagrams displaying all valid transitions between states. To use the graphing functionality, you'll demand to have graphviz
and/or pygraphviz
installed:
To generate graphs with the package graphviz
, you need to install Graphviz manually or via a package manager.
sudo apt-get install graphviz graphviz-dev # Ubuntu and Debian mash install graphviz # MacOS conda install graphviz python-graphviz # (Ana)conda
Now you can install the actual Python packages
pip install graphviz pygraphviz # install graphviz and/or pygraphviz manually... pip install transitions[diagrams] # ... or install transitions with 'diagrams' extras which currently depends on pygraphviz
Currently, GraphMachine
volition utilise pygraphviz
when available and fall back to graphviz
when pygraphviz
cannot be institute. This can be overridden by passing use_pygraphviz=False
to the constructor. Annotation that this default might alter in the future and pygraphviz
support may be dropped. With Model.get_graph()
you can go the current graph or the region of interest (roi) and draw it like this:
# import transitions from transitions.extensions import GraphMachine m = Model () # without further arguments pygraphviz will be used machine = GraphMachine ( model = m , ... ) # when you want to use graphviz explicitly machine = GraphMachine ( model = m , use_pygraphviz = Faux , ... ) # in cases where auto transitions should exist visible machine = GraphMachine ( model = one thousand , show_auto_transitions = True , ... ) # draw the whole graph ... m . get_graph () . depict ( 'my_state_diagram.png' , prog = 'dot' ) # ... or just the region of interest # (previous state, active state and all reachable states) roi = m . get_graph ( show_roi = True ) . draw ( 'my_state_diagram.png' , prog = 'dot' )
This produces something similar this:
Independent of the backend you use, the draw function besides accepts a file descriptor or a binary stream as the first argument. If you set this parameter to None
, the byte stream will be returned:
import io with open ( 'a_graph.png' , 'bw' ) every bit f : # you need to pass the format when you lot pass objects instead of filenames. m . get_graph () . draw ( f , format = "png" , prog = 'dot' ) # you tin can pass a (binary) stream likewise b = io . BytesIO () yard . get_graph () . depict ( b , format = "png" , prog = 'dot' ) # or just handle the binary string yourself result = k . get_graph () . draw ( None , format = "png" , prog = 'dot' ) assert event == b . getvalue ()
References and partials passed equally callbacks will be resolved equally skillful as possible:
from transitions.extensions import GraphMachine from functools import partial grade Model : def clear_state ( self , deep = False , force = False ): print ( "Immigration state ..." ) return True model = Model () machine = GraphMachine ( model = model , states = [ 'A' , 'B' , 'C' ], transitions = [ { 'trigger' : 'clear' , 'source' : 'B' , 'dest' : 'A' , 'weather' : model . clear_state }, { 'trigger' : 'clear' , 'source' : 'C' , 'dest' : 'A' , 'weather condition' : partial ( model . clear_state , Faux , force = Truthful )}, ], initial = 'A' , show_conditions = True ) model . get_graph () . draw ( 'my_state_diagram.png' , prog = 'dot' )
This should produce something similar to this:
If the format of references does not arrange your needs, you can override the static method GraphMachine.format_references
. If y'all want to skip reference entirely, simply let GraphMachine.format_references
return None
. Likewise, accept a look at our example IPython/Jupyter notebooks for a more detailed example about how to utilise and edit graphs.
Hierarchical State Machine (HSM)
Transitions includes an extension module which allows nesting states. This allows us to create contexts and to model cases where states are related to certain subtasks in the state car. To create a nested land, either import NestedState
from transitions or use a dictionary with the initialization arguments name
and children
. Optionally, initial
can be used to ascertain a sub state to transit to, when the nested land is entered.
from transitions.extensions import HierarchicalMachine states = [ 'standing' , 'walking' , { 'proper name' : 'caffeinated' , 'children' :[ 'dithering' , 'running' ]}] transitions = [ [ 'walk' , 'standing' , 'walking' ], [ 'stop' , 'walking' , 'standing' ], [ 'drink' , '*' , 'caffeinated' ], [ 'walk' , [ 'caffeinated' , 'caffeinated_dithering' ], 'caffeinated_running' ], [ 'relax' , 'caffeinated' , 'standing' ] ] machine = HierarchicalMachine ( states = states , transitions = transitions , initial = 'standing' , ignore_invalid_triggers = True ) car . walk () # Walking now automobile . finish () # let's end for a moment automobile . drink () # java time automobile . country >>> 'caffeinated' machine . walk () # we have to get faster machine . state >>> 'caffeinated_running' machine . stop () # can't stop moving! car . state >>> 'caffeinated_running' car . relax () # go out nested state machine . state # phew, what a ride >>> 'standing' # car.on_enter_caffeinated_running('callback_method')
A configuration making utilise of initial
could expect like this:
# ... states = [ 'standing' , 'walking' , { 'proper noun' : 'caffeinated' , 'initial' : 'dithering' , 'children' : [ 'dithering' , 'running' ]}] transitions = [ [ 'walk' , 'standing' , 'walking' ], [ 'stop' , 'walking' , 'standing' ], # this transition will end in 'caffeinated_dithering'... [ 'drink' , '*' , 'caffeinated' ], # ... that is why we do non demand practise specify 'caffeinated' here anymore [ 'walk' , 'caffeinated_dithering' , 'caffeinated_running' ], [ 'relax' , 'caffeinated' , 'standing' ] ] # ...
The 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 country of another model (initial=model.state
) which should be effectively i of the previous mentioned options. Note that when passing a string, transition
will cheque the targeted state for initial
substates and use this as an entry land. This will be done recursively until a substate does non mention an initial country. Parallel states or a state passed equally a listing will be used 'equally is' and no farther initial evaluation will be conducted.
Notation that your previously created country object must be a NestedState
or a derived class of it. The standard State
form used in uncomplicated Machine
instances lacks features required for nesting.
from transitions.extensions.nesting import HierarchicalMachine , NestedState from transitions import State yard = HierarchicalMachine ( states = [ 'A' ], initial = 'initial' ) chiliad . add_state ( 'B' ) # fine thousand . add_state ({ 'name' : 'C' }) # likewise fine k . add_state ( NestedState ( 'D' )) # fine as well m . add_state ( Country ( 'E' )) # does not work!
Some things that have to be considered when working with nested states: Country names are concatenated with NestedState.separator
. Currently the separator is gear up to underscore ('_') and therefore behaves similar to the basic motorcar. This means a substate bar
from state foo
will be known past foo_bar
. A substate baz
of bar
will exist referred to equally foo_bar_baz
and then on. When entering a substate, enter
volition exist called for all parent states. The same is truthful for exiting substates. Third, nested states can overwrite transition behaviour of their parents. If a transition is not known to the current state information technology will be delegated to its parent.
This means that in the standard configuration, land names in HSMs MUST Non incorporate underscores. For transitions
it'southward incommunicable to tell whether machine.add_state('state_name')
should add a state named state_name
or add together a substate name
to the land state
. In some cases this is not sufficient however. For example if state names consist of more i word and you want/need to apply underscore to divide them instead of CamelCase
. To deal with this, you can alter the grapheme used for separation quite easily. You lot tin can even use fancy unicode characters if you lot use Python 3. Setting the separator to something else than underscore changes some of the behaviour (auto_transition and setting callbacks) though:
from transitions.extensions import HierarchicalMachine from transitions.extensions.nesting import NestedState NestedState . separator = '↦' states = [ 'A' , 'B' , { 'name' : 'C' , 'children' :[ '1' , '2' , { 'name' : '3' , 'children' : [ 'a' , 'b' , 'c' ]} ]} ] transitions = [ [ 'reset' , 'C' , 'A' ], [ 'reset' , 'C↦ii' , 'C' ] # overwriting parent reset ] # we rely on auto transitions machine = HierarchicalMachine ( states = states , transitions = transitions , initial = 'A' ) automobile . to_B () # exit country A, enter country B machine . to_C () # go out B, enter C machine . to_C . s3 . a () # enter C↦a; enter C↦3↦a; car . state >>> 'C↦3↦a' assert auto . is_C . s3 . a () machine . to ( 'C↦2' ) # non interactive; exit C↦iii↦a, exit C↦3, enter C↦2 automobile . reset () # exit C↦2; reset C has been overwritten by C↦3 motorcar . state >>> 'C' machine . reset () # exit C, enter A auto . country >>> 'A' # southward.on_enter('C↦3↦a', 'callback_method')
Instead of to_C_3_a()
auto transition is called equally to_C.s3.a()
. If your substate starts with a digit, transitions adds a prefix 's' ('3' becomes 's3') to the motorcar transition FunctionWrapper
to comply with the attribute naming scheme of Python. If interactive completion is not required, to('C↦3↦a')
tin be called directly. Additionally, on_enter/exit_<<land proper noun>>
is replaced with on_enter/exit(state_name, callback)
. Land checks can exist conducted in a like way. Instead of is_C_3_a()
, the FunctionWrapper
variant is_C.s3.a()
can be used.
To check whether the electric current land is a substate of a specific land, is_state
supports the keyword allow_substates
:
motorcar . state >>> 'C.2.a' machine . is_C () # checks for specific states >>> Imitation car . is_C ( allow_substates = True ) >>> True assert machine . is_C . s2 () is False assert machine . is_C . s2 ( allow_substates = True ) # FunctionWrapper support allow_substate also
new in 0.viii.0
You lot tin can employ enumerations in HSMs equally well but keep in heed that Enum
are compared past value. If you have a value more than than once in a state tree those states cannot be distinguished.
states = [ States . RED , States . Yellow , { 'name' : States . GREEN , 'children' : [ 'tick' , 'tock' ]}] states = [ 'A' , { 'name' : 'B' , 'children' : states , 'initial' : States . GREEN }, States . Greenish ] machine = HierarchicalMachine ( states = states ) machine . to_B () car . is_GREEN () # returns True even though the actual state is B_GREEN
new in 0.8.0
HierarchicalMachine
has been rewritten from scratch to support parallel states and better isolation of nested states. This involves some tweaks based on community feedback. To get an idea of processing order and configuration take a await at the post-obit example:
from transitions.extensions.nesting import HierarchicalMachine import logging states = [ 'A' , 'B' , { 'proper noun' : 'C' , 'parallel' : [{ 'name' : '1' , 'children' : [ 'a' , 'b' , 'c' ], 'initial' : 'a' , 'transitions' : [[ 'get' , 'a' , 'b' ]]}, { 'name' : '2' , 'children' : [ 'ten' , 'y' , 'z' ], 'initial' : 'z' }], 'transitions' : [[ 'go' , '2_z' , '2_x' ]]}] transitions = [[ 'reset' , 'C_1_b' , 'B' ]] logging . basicConfig ( level = logging . INFO ) machine = HierarchicalMachine ( states = states , transitions = transitions , initial = 'A' ) machine . to_C () # INFO:transitions.extensions.nesting:Exited state A # INFO:transitions.extensions.nesting:Entered land C # INFO:transitions.extensions.nesting:Entered land C_1 # INFO:transitions.extensions.nesting:Entered state C_2 # INFO:transitions.extensions.nesting:Entered state C_1_a # INFO:transitions.extensions.nesting:Entered state C_2_z auto . go () # INFO:transitions.extensions.nesting:Exited country C_1_a # INFO:transitions.extensions.nesting:Entered country C_1_b # INFO:transitions.extensions.nesting:Exited state C_2_z # INFO:transitions.extensions.nesting:Entered state C_2_x motorcar . reset () # INFO:transitions.extensions.nesting:Exited state C_1_b # INFO:transitions.extensions.nesting:Exited state C_2_x # INFO:transitions.extensions.nesting:Exited state C_1 # INFO:transitions.extensions.nesting:Exited state C_2 # INFO:transitions.extensions.nesting:Exited state C # INFO:transitions.extensions.nesting:Entered country B
When using parallel
instead of children
, transitions
will enter all states of the passed list at the same time. Which substate to enter is defined by initial
which should ever point to a direct substate. A novel feature is to ascertain local transitions by passing the transitions
keyword in a state definition. The above defined transition ['go', 'a', 'b']
is only valid in C_1
. While you can reference substates equally done in ['go', '2_z', '2_x']
you lot cannot reference parent states straight in locally divers transitions. When a parent state is exited, its children will as well be exited. In addition to the processing lodge of transitions known from Auto
where transitions are considered in the order they were added, HierarchicalMachine
considers hierarchy likewise. Transitions defined in substates will be evaluated first (east.1000. C_1_a
is left earlier C_2_z
) and transitions divers with wildcard *
will (for now) only add transitions to root states (in this example A
, B
, C
) Starting with 0.eight.0 nested states can be added directly and will issue the creation of parent states on-the-fly:
k = HierarchicalMachine ( states = [ 'A' ], initial = 'A' ) m . add_state ( 'B_1_a' ) m . to_B_1 () assert 1000 . is_B ( allow_substates = Truthful )
Reuse of previously created HSMs
Besides semantic order, nested states are very handy if you want to specify country machines for specific tasks and plan to reuse them. Before 0.8.0, a HierarchicalMachine
would non integrate the machine example itself but the states and transitions past creating copies of them. Yet, since 0.8.0 (Nested)Land
instances are just referenced which means changes in one machine's collection of states and events will influence the other machine example. Models and their state will non be shared though. Note that events and transitions are besides copied by reference and will be shared by both instances if you practise not use the remap
keyword. This modify was done to exist more in line with Car
which besides uses passed State
instances by reference.
count_states = [ '1' , '2' , '3' , 'done' ] count_trans = [ [ 'increase' , '1' , '2' ], [ 'increase' , '2' , '3' ], [ 'decrease' , 'iii' , '2' ], [ 'subtract' , 'two' , '1' ], [ 'done' , '3' , 'done' ], [ 'reset' , '*' , '1' ] ] counter = HierarchicalMachine ( states = count_states , transitions = count_trans , initial = '1' ) counter . increase () # love my counter states = [ 'waiting' , 'collecting' , { 'name' : 'counting' , 'children' : counter }] transitions = [ [ 'collect' , '*' , 'collecting' ], [ 'wait' , '*' , 'waiting' ], [ 'count' , 'collecting' , 'counting' ] ] collector = HierarchicalMachine ( states = states , transitions = transitions , initial = 'waiting' ) collector . collect () # collecting collector . count () # let'south come across what we got; counting_1 collector . increase () # counting_2 collector . increase () # counting_3 collector . done () # collector.state == counting_done collector . wait () # collector.state == waiting
If a HierarchicalMachine
is passed with the children
keyword, the initial state of this machine volition be assigned to the new parent state. In the in a higher place instance nosotros come across that entering counting
volition besides enter counting_1
. If this is undesired behaviour and the machine should rather halt in the parent state, the user can pass initial
as False
similar {'proper name': 'counting', 'children': counter, 'initial': Fake}
.
Sometimes you want such an embedded state collection to 'render' which means after it is washed it should exit and transit to ane of your super states. To accomplish this behaviour you tin remap state transitions. In the example to a higher place we would like the counter to return if the state done
was reached. This is done as follows:
states = [ 'waiting' , 'collecting' , { 'proper name' : 'counting' , 'children' : counter , 'remap' : { 'washed' : 'waiting' }}] ... # same equally above collector . increase () # counting_3 collector . done () collector . country >>> 'waiting' # be enlightened that 'counting_done' volition be removed from the state machine
As mentioned in a higher place, using remap
will copy events and transitions since they could non exist valid in the original state machine. If a reused country machine does non have a final state, you can of grade add the transitions manually. If 'counter' had no 'washed' state, we could simply add ['done', 'counter_3', 'waiting']
to achieve the aforementioned behaviour.
In cases where you want states and transitions to be copied by value rather than reference (for instance, if you want to go on the pre-0.eight behaviour) yous tin do so by creating a NestedState
and assigning deep copies of the machine's events and states to information technology.
from transitions.extensions.nesting import NestedState from copy import deepcopy # ... configuring and creating counter counting_state = NestedState ( name = "counting" , initial = 'ane' ) counting_state . states = deepcopy ( counter . states ) counting_state . events = deepcopy ( counter . events ) states = [ 'waiting' , 'collecting' , counting_state ]
For complex state machines, sharing configurations rather than instantiated machines might be more feasible. Especially since instantiated machines must be derived from HierarchicalMachine
. Such configurations can be stored and loaded easily via JSON or YAML (see the FAQ). HierarchicalMachine
allows defining substates either with the keyword children
or states
. If both are present, just children
volition exist considered.
counter_conf = { 'name' : 'counting' , 'states' : [ '1' , 'ii' , '3' , 'washed' ], 'transitions' : [ [ 'increment' , 'i' , 'two' ], [ 'increase' , '2' , '3' ], [ 'subtract' , 'iii' , '2' ], [ 'decrease' , 'ii' , 'ane' ], [ 'done' , '3' , 'done' ], [ 'reset' , '*' , '1' ] ], 'initial' : 'i' } collector_conf = { 'name' : 'collector' , 'states' : [ 'waiting' , 'collecting' , counter_conf ], 'transitions' : [ [ 'collect' , '*' , 'collecting' ], [ 'look' , '*' , 'waiting' ], [ 'count' , 'collecting' , 'counting' ] ], 'initial' : 'waiting' } collector = HierarchicalMachine ( ** collector_conf ) collector . collect () collector . count () collector . increase () assert collector . is_counting_2 ()
Threadsafe(-ish) State Machine
In cases where event dispatching is done in threads, i tin can utilize either LockedMachine
or LockedHierarchicalMachine
where function access (!sic) is secured with reentrant locks. This does non relieve you from corrupting your machine by tinkering with member variables of your model or country machine.
from transitions.extensions import LockedMachine from threading import Thread import time states = [ 'A' , 'B' , 'C' ] machine = LockedMachine ( states = states , initial = 'A' ) # let us assume that inbound B volition have some time thread = Thread ( target = car . to_B ) thread . showtime () time . sleep ( 0.01 ) # thread requires some time to start machine . to_C () # synchronized access; won't execute before thread is done # accessing attributes direct thread = Thread ( target = motorcar . to_B ) thread . start () motorcar . new_attrib = 42 # not synchronized! will mess with execution society
Any python context director can be passed in via the machine_context
keyword argument:
from transitions.extensions import LockedMachine from threading import RLock states = [ 'A' , 'B' , 'C' ] lock1 = RLock () lock2 = RLock () machine = LockedMachine ( states = states , initial = 'A' , machine_context = [ lock1 , lock2 ])
Any contexts via machine_model
will be shared betwixt all models registered with the Machine
. Per-model contexts can be added equally well:
lock3 = RLock () machine . add_model ( model , model_context = lock3 )
It's important that all user-provided context managers are re-entrant since the state motorcar volition call them multiple times, even in the context of a single trigger invocation.
Using async callbacks
If you are using Python three.7 or later on, you tin can use AsyncMachine
to piece of work with asynchronous callbacks. You lot tin mix synchronous and asynchronous callbacks if you similar just this may take undesired side effects. Annotation that events need to be awaited and the consequence loop must also be handled by you lot.
from transitions.extensions.asyncio import AsyncMachine import asyncio import time course AsyncModel : def prepare_model ( self ): print ( "I am synchronous." ) cocky . start_time = time . time () async def before_change ( cocky ): impress ( "I am asynchronous and volition block now for 100 milliseconds." ) expect asyncio . sleep ( 0.1 ) print ( "I am done waiting." ) def sync_before_change ( cocky ): print ( "I am synchronous and will block the effect loop (what I probably shouldn't)" ) fourth dimension . sleep ( 0.1 ) print ( "I am done waiting synchronously." ) def after_change ( self ): impress ( f "I am synchronous again. Execution took { int (( time . time () - cocky . start_time ) * 1000 ) } ms." ) transition = dict ( trigger = "start" , source = "Start" , dest = "Washed" , prepare = "prepare_model" , earlier = [ "before_change" ] * 5 + [ "sync_before_change" ], after = "after_change" ) # execute before function in asynchronously v times model = AsyncModel () machine = AsyncMachine ( model , states = [ "Starting time" , "Washed" ], transitions = [ transition ], initial = 'Get-go' ) asyncio . get_event_loop () . run_until_complete ( model . start ()) # >>> I am synchronous. # I am asynchronous and will cake now for 100 milliseconds. # I am asynchronous and will block now for 100 milliseconds. # I am asynchronous and volition block now for 100 milliseconds. # I am asynchronous and will block now for 100 milliseconds. # I am asynchronous and volition block now for 100 milliseconds. # I am synchronous and will cake the event loop (what I probably shouldn't) # I am washed waiting synchronously. # I am washed waiting. # I am done waiting. # I am washed waiting. # I am washed waiting. # I am done waiting. # I am synchronous again. Execution took 101 ms. assert model . is_Done ()
Then, why do you need to use Python 3.vii or later you lot may ask. Async support has been introduced earlier. AsyncMachine
makes utilize of contextvars
to handle running callbacks when new events arrive before a transition has been finished:
async def await_never_return (): await asyncio . sleep ( 100 ) raise ValueError ( "That took too long!" ) async def fix (): await m2 . ready () m1 = AsyncMachine ( states = [ 'A' , 'B' , 'C' ], initial = 'A' , proper noun = "m1" ) m2 = AsyncMachine ( states = [ 'A' , 'B' , 'C' ], initial = 'A' , name = "m2" ) m2 . add_transition ( trigger = 'become' , source = 'A' , dest = 'B' , before = await_never_return ) m2 . add_transition ( trigger = 'gear up' , source = 'A' , dest = 'C' ) m1 . add_transition ( trigger = 'go' , source = 'A' , dest = 'B' , after = 'go' ) m1 . add_transition ( trigger = 'go' , source = 'B' , dest = 'C' , subsequently = fix ) asyncio . get_event_loop () . run_until_complete ( asyncio . assemble ( m2 . get (), m1 . get ())) affirm m1 . state == m2 . state
This example actually illustrates two things: Kickoff, that 'get' called in m1's transition from A
to be B
is not cancelled and second, calling m2.fix()
will halt the transition try of m2 from A
to B
by executing 'prepare' from A
to C
. This separation would not be possible without contextvars
. Note that prepare
and weather
are NOT treated equally ongoing transitions. This ways that after weather
have been evaluated, a transition is executed fifty-fifty though another issue already happened. Tasks will simply be cancelled when run as a before
callback or later on.
AsyncMachine
features a model-special queue mode which can exist used when queued='model'
is passed to the constructor. With a model-specific queue, events will only be queued when they belong to the aforementioned model. Furthermore, a raised exception will only articulate the event queue of the model that raised that exception. For the sake of simplicity, permit's presume that every result in asyncion.gather
beneath is not triggered at the aforementioned time but slightly delayed:
asyncio . gather ( model1 . event1 (), model1 . event2 (), model2 . event1 ()) # execution social club with AsyncMachine(queued=Truthful) # model1.event1 -> model1.event2 -> model2.event1 # execution order with AsyncMachine(queued='model') # (model1.event1, model2.event1) -> model1.event2 asyncio . assemble ( model1 . event1 (), model1 . mistake (), model1 . event3 (), model2 . event1 (), model2 . event2 (), model2 . event3 ()) # execution order with AsyncMachine(queued=True) # model1.event1 -> model1.error # execution order with AsyncMachine(queued='model') # (model1.event1, model2.event1) -> (model1.fault, model2.event2) -> model2.event3
Note that queue modes must non exist changed after car construction.
Calculation features to states
If your superheroes demand some custom behaviour, yous can throw in some actress functionality by decorating machine states:
from time import slumber from transitions import Automobile from transitions.extensions.states import add_state_features , Tags , Timeout @add_state_features ( Tags , Timeout ) class CustomStateMachine ( Machine ): pass grade SocialSuperhero ( object ): def __init__ ( self ): self . entourage = 0 def on_enter_waiting ( self ): cocky . entourage += 1 states = [{ 'name' : 'preparing' , 'tags' : [ 'home' , 'busy' ]}, { 'proper name' : 'waiting' , 'timeout' : 1 , 'on_timeout' : 'get' }, { 'name' : 'away' }] # The city needs us! transitions = [[ 'done' , 'preparing' , 'waiting' ], [ 'bring together' , 'waiting' , 'waiting' ], # Entering Waiting over again will increase our entourage [ 'go' , 'waiting' , 'away' ]] # Okay, let' motion hero = SocialSuperhero () motorcar = CustomStateMachine ( model = hero , states = states , transitions = transitions , initial = 'preparing' ) assert hero . land == 'preparing' # Preparing for the night shift assert machine . get_state ( hero . state ) . is_busy # We are at home and busy hero . done () assert hero . state == 'waiting' # Waiting for fellow superheroes to join us assert hero . entourage == 1 # It's just u.s.a. so far sleep ( 0.7 ) # Waiting... hero . join () # Weeh, we got company sleep ( 0.five ) # Waiting... hero . join () # Even more company \o/ sleep ( 2 ) # Waiting... assert hero . country == 'away' # Impatient superhero already left the edifice assert machine . get_state ( hero . state ) . is_home is False # Yupp, not at dwelling anymore assert hero . entourage == 3 # At least he is not alone
Currently, transitions comes equipped with the following country features:
-
Timeout -- triggers an effect later some fourth dimension has passed
- keyword:
timeout
(int, optional) -- if passed, an entered state volition timeout aftertimeout
seconds - keyword:
on_timeout
(string/callable, optional) -- volition be called when timeout fourth dimension has been reached - will enhance an
AttributeError
whentimeout
is set merelyon_timeout
is not - Note: A timeout is triggered in a thread. This implies several limitations (e.thou. communicable Exceptions raised in timeouts). Consider an consequence queue for more sophisticated applications.
- keyword:
-
Tags -- adds tags to states
- keyword:
tags
(listing, optional) -- assigns tags to a state -
State.is_<tag_name>
will returnTrue
when the state has been tagged withtag_name
, elseFake
- keyword:
-
Mistake -- raises a
MachineError
when a state cannot be left- inherits from
Tags
(if you employError
practice non useTags
) - keyword:
accepted
(bool, optional) -- marks a state every bit accustomed - alternatively the keyword
tags
can exist passed, containing 'accepted' - Note: Errors will merely be raised if
auto_transitions
has been set toFalse
. Otherwise every state can be exited withto_<state>
methods.
- inherits from
-
Volatile -- initialises an object every time a state is entered
- keyword:
volatile
(class, optional) -- every time the land is entered an object of type course volition exist assigned to the model. The attribute name is defined byclaw
. If omitted, an empty VolatileObject volition exist created instead - keyword:
hook
(string, default='telescopic') -- The model's attribute proper noun for the temporal object.
- keyword:
You can write your own Land
extensions and add them the aforementioned way. Just annotation that add_state_features
expects Mixins. This ways your extension should always telephone call the overridden methods __init__
, enter
and exit
. Your extension may inherit from State simply volition too work without information technology. Using @add_state_features
has a drawback which is that decorated machines cannot be pickled (more precisely, the dynamically generated CustomState
cannot be pickled). This might exist a reason to write a defended custom country form instead. Depending on the chosen land car, your custom land 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 y'all tin can either assign them to your Motorcar
's course attribute state_cls
or override Machine.create_state
in case you need some specific procedures done whenever a state is created:
from transitions import Machine , State class MyState ( State ): pass class CustomMachine ( Machine ): # Utilize MyState as state class state_cls = MyState class VerboseMachine ( Machine ): # `Automobile._create_state` is a class method but we can # override it to be an case method def _create_state ( self , * args , ** kwargs ): impress ( "Creating a new country with auto ' {0} '" . format ( cocky . name )) render MyState ( * args , ** kwargs )
If you lot desire to avoid threads in your AsyncMachine
entirely, you lot tin replace the Timeout
state feature with AsyncTimeout
from the asyncio
extension:
import asyncio from transitions.extensions.states import add_state_features from transitions.extensions.asyncio import AsyncTimeout , AsyncMachine @add_state_features ( AsyncTimeout ) class TimeoutMachine ( AsyncMachine ): pass states = [ 'A' , { 'proper noun' : 'B' , 'timeout' : 0.2 , 'on_timeout' : 'to_C' }, 'C' ] g = TimeoutMachine ( states = states , initial = 'A' , queued = True ) # meet remark below asyncio . run ( asyncio . look ([ 1000 . to_B (), asyncio . slumber ( 0.1 )])) affirm m . is_B () # timeout shouldn't exist triggered asyncio . run ( asyncio . expect ([ m . to_B (), asyncio . sleep ( 0.three )])) affirm thousand . is_C () # now timeout should have been processed
You should consider passing queued=True
to the TimeoutMachine
constructor. This will make sure that events are processed sequentially and avoid asynchronous racing weather condition that may appear when timeout and event happen in close proximity.
Using transitions together with Django
Yous can have a look at the FAQ for some inspiration or checkout django-transitions
. It has been developed past Christian Ledermann and is also hosted on Github. The documentation contains some usage examples.
I have a [problems written report/consequence/question]...
Start, congratulations! You reached the end of the documentation! If you lot want to try out transitions
before y'all install it, you can do that in an interactive Jupyter notebook at mybinder.org. But click this push button 👉 .
For bug reports and other bug, please open up an upshot on GitHub.
For usage questions, mail on Stack Overflow, making sure to tag your question with the pytransitions
tag. Do non forget to take a look at the extended examples!
For any other questions, solicitations, or large unrestricted monetary gifts, email Tal Yarkoni (initial author) and/or Alexander Neumann (current maintainer).
Invalid Block Tag On Did You Forget To Register Or Load This Tag?,
Source: https://pypi.org/project/transitions/
Posted by: kramerdosed1999.blogspot.com
0 Response to "Invalid Block Tag On Did You Forget To Register Or Load This Tag?"
Post a Comment