pytransitions/transitions

repository·master·Indexed 27 days ago

https://github.com/pytransitions/transitions

A lightweight, object-oriented state machine implementation for Python. It allows developers to integrate state management into existing Python objects using a simple API, featuring support for callbacks (before, after, on_enter, on_exit), conditional transitions, wildcard and reflexive transitions, queued transitions, and ordered state sequences.

Tokens
13.4K
Snippets
30
Records
54
Agent score
42%

What's inside transitions

  1. Quickstart: Implement a state machine in a Python class

    master

    To use transitions, import Machine and initialize it within your model class. You define states as a list and add transitions using add_transition. Transitions can be added via method calls or passed as a list of dictionaries to the Machine constructor. You can also use before and after callbacks to execute logic during transitions, and conditions to restrict transitions based on method returns.

    from transitions import Machine
    import random
    
    class NarcolepticSuperhero(object):
        # Define states
        states = ['asleep', 'hanging out', 'hungry', 'sweaty', 'saving the world']
    
        def __init__(self, name):
            self.name = name
            self.kittens_rescued = 0
    
            # Initialize the state machine
            # 'model=self' attaches the state and transition methods to this instance
            self.machine = Machine(model=self, states=NarcolepticSuperhero.states, initial='asleep')
    
            # Add transitions
            # Using keyword arguments for trigger, source, and dest
            self.machine.add_transition(trigger='wake_up', source='asleep', dest='hanging out')
    
            # Using positional arguments for trigger, source, and dest
            self.machine.add_transition('work_out', 'hanging out', 'hungry')
    
            self.machine.add_transition('eat', 'hungry', 'hanging out')
    
            # Using '*' as a source allows triggering from any state
            self.machine.add_transition('distress_call', '*', 'saving the world',
                             before='change_into_super_secret_costume')
    
            # Using 'after' to trigger logic after a successful transition
            self.machine.add_transition('complete_mission', 'saving the world', 'sweaty',
                             after='update_journal')
    
            # Using 'conditions' to check a property/method before allowing transition
            self.machine.add_transition('clean_up', 'sweaty', 'asleep', conditions=['is_exhausted'])
            self.machine.add_transition('clean_up', 'sweaty', 'hanging out')
    
            self.machine.add_transition('nap', '*', 'asleep')
    
        def update_journal(self):
            self.kittens_rescued += 1
    
        @property
        def is_exhausted(self):
            return random.random() < 0.5
    
        def change_into_super_secret_costume(self):
            print("Beauty, eh?")
    
    # Usage
    batman = NarcolepticSuperhero("Batman")
    print(batman.state)  # 'asleep'
    
    batman.wake_up()
    print(batman.state)  # 'hanging out'
    
    batman.distress_call()
    print(batman.state)  # 'saving the world'
  2. Trigger transitions on a model

    master

    Once a Machine is attached to a model, you can trigger transitions in two ways:

    1. Automatic methods: transitions automatically attaches methods to your model named after each trigger. Note: If your model already has a method with the same name, transitions will not overwrite it.
    2. The trigger method: Use model.trigger('trigger_name') for dynamic triggering where the name is a string.

    Warning: Avoid naming your model's existing methods the same as your transition triggers to prevent conflicts.

  3. Configure logging for Transitions

    master

    Transitions uses the standard Python logging module. State changes, transition triggers, and conditional checks are logged at the INFO level. You can configure the log level for the transitions logger specifically.

    import logging
    
    # Basic setup to see DEBUG messages
    logging.basicConfig(level=logging.DEBUG)
    
    # Set transitions specifically to INFO to hide DEBUG noise
    logging.getLogger('transitions').setLevel(logging.INFO)
  4. Enable queued transitions

    master

    By default, transitions processes events instantly. If a transition is triggered inside an on_enter callback, it may execute before the after callback of the original transition, leading to unexpected execution orders.

    To ensure a transition is fully finished before the next one begins, enable queuing in the Machine initializer by setting queued=True.

    Execution Order Comparison:

    • Default: prepare $\rightarrow$ before $\rightarrow$ on_enter_B $\rightarrow$ on_enter_C $\rightarrow$ after (if C was triggered from B's enter).
    • Queued: prepare $\rightarrow$ before $\rightarrow$ on_enter_B $\rightarrow$ queue(to_C) $\rightarrow$ after $\rightarrow$ on_enter_C.

    Note: When using a queue, the trigger call will always return True because the success of a queued transition cannot be determined at the time of queuing.

  5. Trigger transitions via methods or dynamically

    master

    Once a Machine is bound to a model, you can trigger transitions in two ways:

    1. Directly via generated methods: If a transition has a trigger named 'melt', the model will have a method melt().
    2. Dynamically via trigger(): Call the trigger(name) method on the model, passing the trigger name as a string.

    Example transition definition:

    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' }
  6. Initialize the state machine using different patterns

    master

    Transitions supports three primary initialization patterns:

    1. Separate Machine and Model

    Attach a Machine instance to a separate model instance. This keeps state logic separate from the model class.

    lump = Matter()
    machine = Machine(lump, ['solid', 'liquid'], initial='solid')

    2. Standalone Machine

    Create a machine without a model by omitting the model argument. All triggers and callbacks are attached directly to the Machine instance.

    machine = Machine(states=states, transitions=transitions, initial='solid')

    3. Model Inheritance

    Have your model inherit from the Machine class. You must override the __init__ method and call Machine.__init__.

    class Matter(Machine):
        def __init__(self):
            states = ['solid', 'liquid', 'gas']
            Machine.__init__(self, states=states, initial='solid')
            self.add_transition('melt', 'solid', 'liquid')
    
    lump = Matter()
    # Standalone Machine
    machine = Machine(states=states, transitions=transitions, initial='solid')
    machine.melt()
    
    # Model Inheritance
    class Matter(Machine):
        def __init__(self):
            states = ['solid', 'liquid', 'gas']
            Machine.__init__(self, states=states, initial='solid')
            self.add_transition('melt', 'solid', 'liquid')
    
    lump = Matter()
    lump.melt()
  7. Add custom features to states using @add_state_features

    master

    You can extend state functionality by decorating a machine class with @add_state_features. This allows you to pass extra keywords in the states definition.

    To create your own extension, write a Mixin that overrides __init__, enter, and exit.

    Note: Decorated machines cannot be pickled because the dynamically generated CustomState is not picklable. For picklable machines, define a custom state class and assign it to the machine's state_cls attribute.

    from transitions import Machine
    from transitions.extensions.states import add_state_features, Tags, Timeout
    
    @add_state_features(Tags, Timeout)
    class CustomStateMachine(Machine):
        pass
    
    # States can now use 'tags' and 'timeout' keywords
    states = [
        {'name': 'preparing', 'tags': ['home', 'busy']},
        {'name': 'waiting', 'timeout': 1, 'on_timeout': 'go'}
    ]
  8. Use AsyncTimeout for non-threaded timeouts in AsyncMachine

    master

    To avoid using threads for timeouts in an AsyncMachine, use the AsyncTimeout extension from transitions.extensions.asyncio. It is highly recommended to pass queued=True to the constructor to prevent race conditions between timeouts and manual events.

    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', {'name': 'B', 'timeout': 0.2, 'on_timeout': 'to_C'}, 'C']
    m = TimeoutMachine(states=states, initial='A', queued=True)
  9. Use enter and exit callbacks for States

    master

    You can execute code automatically when entering or leaving a state using callbacks.

    Ways to define callbacks:

    1. In the State constructor: Pass a list of method names to on_enter or on_exit.
    2. On the Machine instance: Use dynamically generated methods machine.on_enter_<state_name>(callback_name) or machine.on_exit_<state_name>(callback_name).
    3. In the Model class: Define methods named on_enter_<state_name> or on_exit_<state_name> directly on your model class.

    Note: on_enter callbacks do not fire during the initial Machine setup if the machine starts in that state. To trigger them, you must transition into the state explicitly.

  10. Install dependencies for diagrams

    master

    To use advanced graphing features, you must install the system-level graphviz library and the corresponding Python packages.

    System Installation:

    • Ubuntu/Debian: sudo apt-get install graphviz graphviz-dev
    • MacOS: brew install graphviz
    • Conda: conda install graphviz python-graphviz

    Python Installation:

    • For pygraphviz support: pip install graphviz pygraphviz
    • For the diagrams extras: pip install transitions[diagrams]
    sudo apt-get install graphviz graphviz-dev  # Ubuntu and Debian
    brew install graphviz  # MacOS
    conda install graphviz python-graphviz  # (Ana)conda
    
    pip install graphviz pygraphviz  # install graphviz and/or pygraphviz manually...
    pip install transitions[diagrams]  # ... or install transitions with 'diagrams' extras
  11. Reuse HierarchicalMachine instances as nested states

    master

    Since version 0.8.0, (Nested)State instances are referenced rather than copied. This allows you to define a HierarchicalMachine for a specific task and embed it as a substate in another machine.

    Key behaviors:

    • Changes to the embedded machine's states/events will influence the parent machine.
    • If you pass a machine via the children keyword, its initial state is assigned to the new parent state. To prevent this, set initial: False in the parent state definition.
    • To make an embedded machine 'return' to a super state upon reaching a specific state, use the remap keyword in the parent state definition.
  12. Pass data to transition callbacks

    master

    You can pass data to callback functions registered during machine initialization using two methods:

    1. Direct Arguments (Default)

    Pass positional or keyword arguments directly to the trigger methods. Note: Every callback function triggered by the transition must be able to handle all arguments passed.

    2. Using send_event=True

    Set send_event=True during Machine initialization. All arguments passed to triggers will be wrapped in an EventData instance and passed as the sole argument to every callback. The EventData object contains .args (positional) and .kwargs (keyword) properties, as well as references to the source, model, transition, machine, and trigger.

    # Method 1: Direct Arguments
    class Matter(object):
        def __init__(self):
            self.set_environment()
        def set_environment(self, temp=0, pressure=101.325):
            self.temp = temp
            self.pressure = pressure
        def print_temperature(self):
            print("Current temperature is %d degrees celsius." % self.temp)
    
    lump = Matter()
    machine = Machine(lump, ['solid', 'liquid'], initial='solid')
    machine.add_transition('melt', 'solid', 'liquid', before='set_environment')
    lump.melt(45)  # positional arg
    
    # Method 2: Using send_event=True
    class Matter(object):
        def __init__(self):
            self.temp = 0
            self.pressure = 101.325
    
        def set_environment(self, event):
            self.temp = event.kwargs.get('temp', 0)
            self.pressure = event.kwargs.get('pressure', 101.325)
    
    lump = Matter()
    machine = Machine(lump, ['solid', 'liquid'], send_event=True, initial='solid')
    machine.add_transition('melt', 'solid', 'liquid', before='set_environment')
    lump.melt(temp=45, pressure=1853.68)