transitions-0.5.3/0000755000076500000240000000000013112746733015026 5ustar alneumanstaff00000000000000transitions-0.5.3/LICENSE0000644000076500000240000000235112647164525016041 0ustar alneumanstaff00000000000000******* License ******* The transitions package, including all examples, code snippets and attached documentation is covered by the MIT license. :: The MIT License Copyright (c) 2014 Tal Yarkoni Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.transitions-0.5.3/MANIFEST.in0000644000076500000240000000002013077421200016541 0ustar alneumanstaff00000000000000include LICENSE transitions-0.5.3/PKG-INFO0000644000076500000240000000136713112746733016132 0ustar alneumanstaff00000000000000Metadata-Version: 1.1 Name: transitions Version: 0.5.3 Summary: A lightweight, object-oriented Python state machine implementation. Home-page: http://github.com/tyarkoni/transitions Author: Tal Yarkoni Author-email: tyarkoni@gmail.com License: MIT Download-URL: https://github.com/tyarkoni/transitions/archive/0.5.3.tar.gz Description: UNKNOWN Platform: UNKNOWN Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 transitions-0.5.3/setup.cfg0000644000076500000240000000014413112746733016646 0ustar alneumanstaff00000000000000[metadata] description-file = README.md [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 transitions-0.5.3/setup.py0000644000076500000240000000303713077417015016541 0ustar alneumanstaff00000000000000import sys from setuptools import setup, find_packages with open('transitions/version.py') as f: exec(f.read()) if len(set(('test', 'easy_install')).intersection(sys.argv)) > 0: import setuptools tests_require = ['dill', 'pygraphviz'] extra_setuptools_args = {} if 'setuptools' in sys.modules: tests_require.append('nose') extra_setuptools_args = dict( test_suite='nose.collector', extras_require=dict( test='nose>=0.10.1') ) setup( name="transitions", version=__version__, description="A lightweight, object-oriented Python state machine implementation.", maintainer='Tal Yarkoni', maintainer_email='tyarkoni@gmail.com', url='http://github.com/tyarkoni/transitions', packages=find_packages(exclude=['tests', 'test_*']), package_data={'transitions': ['data/*'], 'transitions.tests': ['data/*'] }, include_package_data=True, install_requires=['six'], tests_require=tests_require, license='MIT', download_url='https://github.com/tyarkoni/transitions/archive/%s.tar.gz' % __version__, classifiers=[ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', ], **extra_setuptools_args ) transitions-0.5.3/transitions/0000755000076500000240000000000013112746733017403 5ustar alneumanstaff00000000000000transitions-0.5.3/transitions/__init__.py0000644000076500000240000000057213060243042021503 0ustar alneumanstaff00000000000000from __future__ import absolute_import from .version import __version__ from .core import (State, Transition, Event, EventData, Machine, MachineError, logger) __copyright__ = "Copyright (c) 2017 Tal Yarkoni" __license__ = "MIT" __summary__ = "A lightweight, object-oriented finite state machine in Python" __uri__ = "https://github.com/tyarkoni/transitions" transitions-0.5.3/transitions/core.py0000644000076500000240000011647513107012451020706 0ustar alneumanstaff00000000000000try: from builtins import object except ImportError: # python2 pass import inspect import itertools import logging from collections import OrderedDict from collections import defaultdict from collections import deque from functools import partial from six import string_types import warnings warnings.simplefilter('default') logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) def listify(obj): if obj is None: return [] else: return obj if isinstance(obj, (list, tuple, type(None))) else [obj] def get_trigger(model, trigger_name, *args, **kwargs): func = getattr(model, trigger_name, None) if func: return func(*args, **kwargs) raise AttributeError("Model has no trigger named '%s'" % trigger_name) def prep_ordered_arg(desired_length, arg_name): """Ensure arguments to add_ordered_transitions are the proper length and replicate the given argument if only one given (apply same condition, callback to all transitions) """ arg_name = listify(arg_name) if arg_name else [None] if len(arg_name) != desired_length and len(arg_name) != 1: raise ValueError("Argument length must be either 1 or the same length as " "the number of transitions.") if len(arg_name) == 1: return arg_name * desired_length return arg_name class State(object): def __init__(self, name, on_enter=None, on_exit=None, ignore_invalid_triggers=False): """ Args: name (string): The name of the state on_enter (string, list): Optional callable(s) to trigger when a state is entered. Can be either a string providing the name of a callable, or a list of strings. on_exit (string, list): Optional callable(s) to trigger when a state is exited. Can be either a string providing the name of a callable, or a list of strings. ignore_invalid_triggers (Boolean): Optional flag to indicate if unhandled/invalid triggers should raise an exception """ self.name = name self.ignore_invalid_triggers = ignore_invalid_triggers self.on_enter = listify(on_enter) if on_enter else [] self.on_exit = listify(on_exit) if on_exit else [] def enter(self, event_data): """ Triggered when a state is entered. """ logger.debug("%sEntering state %s. Processing callbacks...", event_data.machine.id, self.name) for oe in self.on_enter: event_data.machine._callback(oe, event_data) logger.info("%sEntered state %s", event_data.machine.id, self.name) def exit(self, event_data): """ Triggered when a state is exited. """ logger.debug("%sExiting state %s. Processing callbacks...", event_data.machine.id, self.name) for oe in self.on_exit: event_data.machine._callback(oe, event_data) logger.info("%sExited state %s", event_data.machine.id, self.name) def add_callback(self, trigger, func): """ Add a new enter or exit callback. Args: trigger (string): The type of triggering event. Must be one of 'enter' or 'exit'. func (string): The name of the callback function. """ callback_list = getattr(self, 'on_' + trigger) callback_list.append(func) def __repr__(self): return "<%s('%s')@%s>" % (type(self).__name__, self.name, id(self)) class Condition(object): def __init__(self, func, target=True): """ Args: func (string): Name of the condition-checking callable target (bool): Indicates the target state--i.e., when True, the condition-checking callback should return True to pass, and when False, the callback should return False to pass. Notes: This class should not be initialized or called from outside a Transition instance, and exists at module level (rather than nesting under the transition class) only because of a bug in dill that prevents serialization under Python 2.7. """ self.func = func self.target = target def check(self, event_data): """ Check whether the condition passes. Args: event_data (EventData): An EventData instance to pass to the condition (if event sending is enabled) or to extract arguments from (if event sending is disabled). Also contains the data model attached to the current machine which is used to invoke the condition. """ predicate = getattr(event_data.model, self.func) if isinstance(self.func, string_types) else self.func if event_data.machine.send_event: return predicate(event_data) == self.target else: return predicate( *event_data.args, **event_data.kwargs) == self.target def __repr__(self): return "<%s(%s)@%s>" % (type(self).__name__, self.func, id(self)) class Transition(object): def __init__(self, source, dest, conditions=None, unless=None, before=None, after=None, prepare=None): """ Args: source (string): The name of the source State. dest (string): The name of the destination State. conditions (string, list): Condition(s) that must pass in order for the transition to take place. Either a string providing the name of a callable, or a list of callables. For the transition to occur, ALL callables must return True. unless (string, list): Condition(s) that must return False in order for the transition to occur. Behaves just like conditions arg otherwise. before (string or list): callbacks to trigger before the transition. after (string or list): callbacks to trigger after the transition. prepare (string or list): callbacks to trigger before conditions are checked """ self.source = source self.dest = dest self.prepare = [] if prepare is None else listify(prepare) self.before = [] if before is None else listify(before) self.after = [] if after is None else listify(after) self.conditions = [] if conditions is not None: for c in listify(conditions): self.conditions.append(Condition(c)) if unless is not None: for u in listify(unless): self.conditions.append(Condition(u, target=False)) def execute(self, event_data): """ Execute the transition. Args: event: An instance of class EventData. Returns: boolean indicating whether or not the transition was successfully executed (True if successful, False if not). """ logger.debug("%sInitiating transition from state %s to state %s...", event_data.machine.id, self.source, self.dest) machine = event_data.machine for func in self.prepare: machine._callback(func, event_data) logger.debug("Executed callback '%s' before conditions." % func) for c in self.conditions: if not c.check(event_data): logger.debug("%sTransition condition failed: %s() does not " + "return %s. Transition halted.", event_data.machine.id, c.func, c.target) return False for func in itertools.chain(machine.before_state_change, self.before): machine._callback(func, event_data) logger.debug("%sExecuted callback '%s' before transition.", event_data.machine.id, func) self._change_state(event_data) for func in itertools.chain(self.after, machine.after_state_change): machine._callback(func, event_data) logger.debug("%sExecuted callback '%s' after transition.", event_data.machine.id, func) return True def _change_state(self, event_data): event_data.machine.get_state(self.source).exit(event_data) event_data.machine.set_state(self.dest, event_data.model) event_data.update(event_data.model) event_data.machine.get_state(self.dest).enter(event_data) def add_callback(self, trigger, func): """ Add a new before, after, or prepare callback. Args: trigger (string): The type of triggering event. Must be one of 'before', 'after' or 'prepare'. func (string): The name of the callback function. """ callback_list = getattr(self, trigger) callback_list.append(func) def __repr__(self): return "<%s('%s', '%s')@%s>" % (type(self).__name__, self.source, self.dest, id(self)) class EventData(object): def __init__(self, state, event, machine, model, args, kwargs): """ Args: state (State): The State from which the Event was triggered. event (Event): The triggering Event. machine (Machine): The current Machine instance. model (object): The model/object the machine is bound to. args (list): Optional positional arguments from trigger method to store internally for possible later use. kwargs (dict): Optional keyword arguments from trigger method to store internally for possible later use. """ self.state = state self.event = event self.machine = machine self.model = model self.args = args self.kwargs = kwargs self.transition = None self.error = None self.result = False def update(self, model): """ Updates the current State to accurately reflect the Machine. """ self.state = self.machine.get_state(model.state) def __repr__(self): return "<%s('%s', %s)@%s>" % (type(self).__name__, self.state, getattr(self, 'transition'), id(self)) class Event(object): def __init__(self, name, machine): """ Args: name (string): The name of the event, which is also the name of the triggering callable (e.g., 'advance' implies an advance() method). machine (Machine): The current Machine instance. """ self.name = name self.machine = machine self.transitions = defaultdict(list) def add_transition(self, transition): """ Add a transition to the list of potential transitions. Args: transition (Transition): The Transition instance to add to the list. """ self.transitions[transition.source].append(transition) def trigger(self, model, *args, **kwargs): f = partial(self._trigger, model, *args, **kwargs) return self.machine._process(f) def _trigger(self, model, *args, **kwargs): """ Serially execute all transitions that match the current state, halting as soon as one successfully completes. Args: args and kwargs: Optional positional or named arguments that will be passed onto the EventData object, enabling arbitrary state information to be passed on to downstream triggered functions. Returns: boolean indicating whether or not a transition was successfully executed (True if successful, False if not). """ state = self.machine.get_state(model.state) if state.name not in self.transitions: msg = "%sCan't trigger event %s from state %s!" % (self.machine.id, self.name, state.name) if state.ignore_invalid_triggers: logger.warning(msg) return False else: raise MachineError(msg) event_data = EventData(state, self, self.machine, model, args=args, kwargs=kwargs) for func in self.machine.prepare_event: self.machine._callback(func, event_data) logger.debug("Executed machine preparation callback '%s' before conditions." % func) try: for t in self.transitions[state.name]: event_data.transition = t if t.execute(event_data): event_data.result = True break except Exception as e: event_data.error = e raise finally: for func in self.machine.finalize_event: self.machine._callback(func, event_data) logger.debug("Executed machine finalize callback '%s'." % func) return event_data.result def __repr__(self): return "<%s('%s')@%s>" % (type(self).__name__, self.name, id(self)) def add_callback(self, trigger, func): """ Add a new before or after callback to all available transitions. Args: trigger (string): The type of triggering event. Must be one of 'before', 'after' or 'prepare'. func (string): The name of the callback function. """ for t in itertools.chain(*self.transitions.values()): t.add_callback(trigger, func) class Machine(object): # Callback naming parameters callbacks = ['before', 'after', 'prepare', 'on_enter', 'on_exit'] separator = '_' wildcard_all = '*' wildcard_same = '=' def __init__(self, model='self', states=None, initial='initial', transitions=None, send_event=False, auto_transitions=True, ordered_transitions=False, ignore_invalid_triggers=None, before_state_change=None, after_state_change=None, name=None, queued=False, add_self=True, prepare_event=None, finalize_event=None, **kwargs): """ Args: model (object): The object(s) whose states we want to manage. If 'self', the current Machine instance will be used the model (i.e., all triggering events will be attached to the Machine itself). states (list): A list of valid states. Each element can be either a string or a State instance. If string, a new generic State instance will be created that has the same name as the string. initial (string or State): The initial state of the Machine. transitions (list): An optional list of transitions. Each element is a dictionary of named arguments to be passed onto the Transition initializer. send_event (boolean): When True, any arguments passed to trigger methods will be wrapped in an EventData object, allowing indirect and encapsulated access to data. When False, all positional and keyword arguments will be passed directly to all callback methods. auto_transitions (boolean): When True (default), every state will automatically have an associated to_{state}() convenience trigger in the base model. ordered_transitions (boolean): Convenience argument that calls add_ordered_transitions() at the end of initialization if set to True. ignore_invalid_triggers: when True, any calls to trigger methods that are not valid for the present state (e.g., calling an a_to_b() trigger when the current state is c) will be silently ignored rather than raising an invalid transition exception. before_state_change: A callable called on every change state before the transition happened. It receives the very same args as normal callbacks. after_state_change: A callable called on every change state after the transition happened. It receives the very same args as normal callbacks. name: If a name is set, it will be used as a prefix for logger output queued (boolean): When True, processes transitions sequentially. A trigger executed in a state callback function will be queued and executed later. Due to the nature of the queued processing, all transitions will _always_ return True since conditional checks cannot be conducted at queueing time. add_self (boolean): If no model(s) provided, intialize state machine against self. prepare_event: A callable called on for before possible transitions will be processed. It receives the very same args as normal callbacks. finalize_event: A callable called on for each triggered event after transitions have been processed. This is also called when a transition raises an exception. **kwargs additional arguments passed to next class in MRO. This can be ignored in most cases. """ try: super(Machine, self).__init__(**kwargs) except TypeError as e: raise ValueError('Passing arguments {0} caused an inheritance error: {1}'.format(kwargs.keys(), e)) # initialize protected attributes first self._queued = queued self._transition_queue = deque() self._before_state_change = [] self._after_state_change = [] self._prepare_event = [] self._finalize_event = [] self._initial = None self.states = OrderedDict() self.events = {} self.send_event = send_event self.auto_transitions = auto_transitions self.ignore_invalid_triggers = ignore_invalid_triggers self.prepare_event = prepare_event self.before_state_change = before_state_change self.after_state_change = after_state_change self.finalize_event = finalize_event self.id = name + ": " if name is not None else "" self.models = [] if model is None and add_self: model = 'self' warnings.warn("Starting from transitions version 0.6.0, passing model=None to the " "constructor will no longer add the machine instance as a model but add " "NO model at all. Consequently, add_self will be removed. To add the " "machine as a model (and also hide this warning) use the new default " "value model='self' instead.", PendingDeprecationWarning) if add_self is not True: warnings.warn("Starting from transitions version 0.6.0, passing model=None to the " "constructor will no longer add the machine instance as a model but add " "NO model at all. Consequently, add_self will be removed.", PendingDeprecationWarning) if model and initial is None: initial = 'initial' warnings.warn("Starting from transitions version 0.6.0, passing initial=None to the constructor " "will no longer create and set the 'initial' state. If no initial" "state is provided but model is not None, an error will be raised.", PendingDeprecationWarning) if states is not None: self.add_states(states) if initial is not None: if isinstance(initial, State): if initial.name not in self.states: self.add_state(initial) else: assert self._has_state(initial) self._initial = initial.name else: if initial not in self.states: self.add_state(initial) self._initial = initial if transitions is not None: transitions = listify(transitions) for t in transitions: if isinstance(t, list): self.add_transition(*t) else: self.add_transition(**t) if ordered_transitions: self.add_ordered_transitions() if model: self.add_model(model) def add_model(self, model, initial=None): """ Register a model with the state machine, initializing triggers and callbacks. """ models = listify(model) if initial is None: if self._initial is None: raise ValueError("No initial state configured for machine, must specify when adding model.") else: initial = self._initial for model in models: model = self if model == 'self' else model if model not in self.models: if hasattr(model, 'trigger'): logger.warning("%sModel already contains an attribute 'trigger'. Skip method binding ", self.id) else: model.trigger = partial(get_trigger, model) for trigger, _ in self.events.items(): self._add_trigger_to_model(trigger, model) for _, state in self.states.items(): self._add_model_to_state(state, model) self.set_state(initial, model=model) self.models.append(model) def remove_model(self, model): """ Deregister a model with the state machine. The model will still contain all previously added triggers and callbacks, but will not receive updates when states or transitions are added to the Machine. """ models = listify(model) for model in models: self.models.remove(model) @staticmethod def _create_transition(*args, **kwargs): return Transition(*args, **kwargs) @staticmethod def _create_event(*args, **kwargs): return Event(*args, **kwargs) @staticmethod def _create_state(*args, **kwargs): return State(*args, **kwargs) @property def initial(self): """ Return the initial state. """ return self._initial @property def has_queue(self): """ Return boolean indicating if machine has queue or not """ return self._queued @property def model(self): if len(self.models) == 1: return self.models[0] else: return self.models @property def before_state_change(self): return self._before_state_change # this should make sure that _before_state_change is always a list @before_state_change.setter def before_state_change(self, value): self._before_state_change = listify(value) @property def after_state_change(self): return self._after_state_change # this should make sure that _after_state_change is always a list @after_state_change.setter def after_state_change(self, value): self._after_state_change = listify(value) @property def prepare_event(self): return self._prepare_event # this should make sure that prepare_event is always a list @prepare_event.setter def prepare_event(self, value): self._prepare_event = listify(value) @property def finalize_event(self): return self._finalize_event # this should make sure that finalize_event is always a list @finalize_event.setter def finalize_event(self, value): self._finalize_event = listify(value) def is_state(self, state, model): """ Check whether the current state matches the named state. """ return model.state == state def get_state(self, state): """ Return the State instance with the passed name. """ if state not in self.states: raise ValueError("State '%s' is not a registered state." % state) return self.states[state] def set_state(self, state, model=None): """ Set the current state. """ if isinstance(state, string_types): state = self.get_state(state) models = self.models if model is None else listify(model) for m in models: m.state = state.name def add_state(self, *args, **kwargs): """ Alias for add_states. """ self.add_states(*args, **kwargs) def add_states(self, states, on_enter=None, on_exit=None, ignore_invalid_triggers=None): """ Add new state(s). Args: state (list, string, dict, or State): a list, a State instance, the name of a new state, or a dict with keywords to pass on to the State initializer. If a list, each element can be of any of the latter three types. on_enter (string or list): callbacks to trigger when the state is entered. Only valid if first argument is string. on_exit (string or list): callbacks to trigger when the state is exited. Only valid if first argument is string. ignore_invalid_triggers: when True, any calls to trigger methods that are not valid for the present state (e.g., calling an a_to_b() trigger when the current state is c) will be silently ignored rather than raising an invalid transition exception. Note that this argument takes precedence over the same argument defined at the Machine level, and is in turn overridden by any ignore_invalid_triggers explicitly passed in an individual state's initialization arguments. """ ignore = ignore_invalid_triggers if ignore is None: ignore = self.ignore_invalid_triggers states = listify(states) for state in states: if isinstance(state, string_types): state = self._create_state( state, on_enter=on_enter, on_exit=on_exit, ignore_invalid_triggers=ignore) elif isinstance(state, dict): if 'ignore_invalid_triggers' not in state: state['ignore_invalid_triggers'] = ignore state = self._create_state(**state) self.states[state.name] = state for model in self.models: self._add_model_to_state(state, model) # Add automatic transitions after all states have been created if self.auto_transitions: for s in self.states.keys(): self.add_transition('to_%s' % s, self.wildcard_all, s) def _add_model_to_state(self, state, model): setattr(model, 'is_%s' % state.name, partial(self.is_state, state.name, model)) # Add enter/exit callbacks if there are existing bound methods enter_callback = 'on_enter_' + state.name if hasattr(model, enter_callback) and \ inspect.ismethod(getattr(model, enter_callback)): state.add_callback('enter', enter_callback) exit_callback = 'on_exit_' + state.name if hasattr(model, exit_callback) and \ inspect.ismethod(getattr(model, exit_callback)): state.add_callback('exit', exit_callback) def _add_trigger_to_model(self, trigger, model): trig_func = partial(self.events[trigger].trigger, model) setattr(model, trigger, trig_func) def get_triggers(self, *args): states = set(args) return [t for (t, ev) in self.events.items() if any(state in ev.transitions for state in states)] def add_transition(self, trigger, source, dest, conditions=None, unless=None, before=None, after=None, prepare=None, **kwargs): """ Create a new Transition instance and add it to the internal list. Args: trigger (string): The name of the method that will trigger the transition. This will be attached to the currently specified model (e.g., passing trigger='advance' will create a new advance() method in the model that triggers the transition.) source(string): The name of the source state--i.e., the state we are transitioning away from. This can be a single state, a list of states or an asterisk for all states. dest (string): The name of the destination State--i.e., the state we are transitioning into. This can be a single state or an equal sign to specify that the transition should be reflexive so that the destination will be the same as the source for every given source. conditions (string or list): Condition(s) that must pass in order for the transition to take place. Either a list providing the name of a callable, or a list of callables. For the transition to occur, ALL callables must return True. unless (string, list): Condition(s) that must return False in order for the transition to occur. Behaves just like conditions arg otherwise. before (string or list): Callables to call before the transition. after (string or list): Callables to call after the transition. prepare (string or list): Callables to call when the trigger is activated **kwargs: Additional arguments which can be passed to the created transition. This is useful if you plan to extend Machine.Transition and require more parameters. """ if trigger not in self.events: self.events[trigger] = self._create_event(trigger, self) for model in self.models: self._add_trigger_to_model(trigger, model) if isinstance(source, string_types): source = list(self.states.keys()) if source == self.wildcard_all else [source] else: source = [s.name if self._has_state(s) else s for s in listify(source)] for s in source: d = s if dest == self.wildcard_same else dest if self._has_state(d): d = d.name t = self._create_transition(s, d, conditions, unless, before, after, prepare, **kwargs) self.events[trigger].add_transition(t) def add_ordered_transitions(self, states=None, trigger='next_state', loop=True, loop_includes_initial=True, conditions=None, unless=None, before=None, after=None, prepare=None, **kwargs): """ Add a set of transitions that move linearly from state to state. Args: states (list): A list of state names defining the order of the transitions. E.g., ['A', 'B', 'C'] will generate transitions for A --> B, B --> C, and C --> A (if loop is True). If states is None, all states in the current instance will be used. trigger (string): The name of the trigger method that advances to the next state in the sequence. loop (boolean): Whether or not to add a transition from the last state to the first state. loop_includes_initial (boolean): If no initial state was defined in the machine, setting this to True will cause the _initial state placeholder to be included in the added transitions. conditions (string or list): Condition(s) that must pass in order for the transition to take place. Either a list providing the name of a callable, or a list of callables. For the transition to occur, ALL callables must return True. unless (string, list): Condition(s) that must return False in order for the transition to occur. Behaves just like conditions arg otherwise. before (string or list): Callables to call before the transition. after (string or list): Callables to call after the transition. prepare (string or list): Callables to call when the trigger is activated **kwargs: Additional arguments which can be passed to the created transition. This is useful if you plan to extend Machine.Transition and require more parameters. """ if states is None: states = list(self.states.keys()) # need to listify for Python3 len_transitions = len(states) if len_transitions < 2: raise ValueError("Can't create ordered transitions on a Machine " "with fewer than 2 states.") if not loop: len_transitions -= 1 # ensure all args are the proper length conditions = prep_ordered_arg(len_transitions, conditions) unless = prep_ordered_arg(len_transitions, unless) before = prep_ordered_arg(len_transitions, before) after = prep_ordered_arg(len_transitions, after) prepare = prep_ordered_arg(len_transitions, prepare) states.remove(self._initial) self.add_transition(trigger, self._initial, states[0], conditions=conditions[0], unless=unless[0], before=before[0], after=after[0], prepare=prepare[0], **kwargs) for i in range(1, len(states)): self.add_transition(trigger, states[i - 1], states[i], conditions=conditions[i], unless=unless[i], before=before[i], after=after[i], prepare=prepare[i], **kwargs) if loop: self.add_transition(trigger, states[-1], self._initial if loop_includes_initial else states[0], conditions=conditions[-1], unless=unless[-1], before=before[-1], after=after[-1], prepare=prepare[-1], **kwargs) def remove_transition(self, trigger, source="*", dest="*"): """ Removes a transition from the Machine and all models. Args: trigger (string): Trigger name of the transition source (string): Limits removal to transitions from a certain state. dest (string): Limits removal to transitions to a certain state. """ source = listify(source) if source != "*" else source dest = listify(dest) if dest != "*" else dest # outer comprehension, keeps events if inner comprehension returns lists with length > 0 self.events[trigger].transitions = {key: value for key, value in {k: [t for t in v # keep entries if source should not be filtered; same for dest. if (source is not "*" and t.source not in source) or (dest is not "*" and t.dest not in dest)] # }.items() takes the result of the inner comprehension and uses it # for the outer comprehension (see first line of comment) for k, v in self.events[trigger].transitions.items()}.items() if len(value) > 0} # if no transition is left remove the trigger from the machine and all models if len(self.events[trigger].transitions) == 0: for m in self.models: delattr(m, trigger) del self.events[trigger] def _callback(self, func, event_data): """ Trigger a callback function, possibly wrapping it in an EventData instance. Args: func (callable): The callback function. event_data (EventData): An EventData instance to pass to the callback (if event sending is enabled) or to extract arguments from (if event sending is disabled). """ if isinstance(func, string_types): func = getattr(event_data.model, func) if self.send_event: func(event_data) else: func(*event_data.args, **event_data.kwargs) def _has_state(self, s): if isinstance(s, State): if s in self.states.values(): return True else: raise ValueError('State %s has not been added to the machine' % s.name) else: return False def _process(self, trigger): # default processing if not self.has_queue: if not self._transition_queue: # if trigger raises an Error, it has to be handled by the Machine.process caller return trigger() else: raise MachineError("Attempt to process events synchronously while transition queue is not empty!") # process queued events self._transition_queue.append(trigger) # another entry in the queue implies a running transition; skip immediate execution if len(self._transition_queue) > 1: return True # execute as long as transition queue is not empty while self._transition_queue: try: self._transition_queue[0]() self._transition_queue.popleft() except Exception: # if a transition raises an exception, clear queue and delegate exception handling self._transition_queue.clear() raise return True @classmethod def _identify_callback(cls, name): # Does the prefix match a known callback? try: callback_type = cls.callbacks[[name.find(x) for x in cls.callbacks].index(0)] except ValueError: return None, None # Extract the target by cutting the string after the type and separator target = name[len(callback_type) + len(cls.separator):] # Make sure there is actually a target to avoid index error and enforce _ as a separator if target == '' or name[len(callback_type)] != cls.separator: return None, None return callback_type, target def __getattr__(self, name): # Machine.__dict__ does not contain double underscore variables. # Class variables will be mangled. if name.startswith('__'): raise AttributeError("'{}' does not exist on " .format(name, id(self))) # Could be a callback callback_type, target = self._identify_callback(name) if callback_type is not None: if callback_type in ['before', 'after', 'prepare']: if target not in self.events: raise AttributeError("event '{}' is not registered on " .format(target, id(self))) return partial(self.events[target].add_callback, callback_type) elif callback_type in ['on_enter', 'on_exit']: state = self.get_state(target) return partial(state.add_callback, callback_type[3:]) # Nothing matched raise AttributeError("'{}' does not exist on ".format(name, id(self))) class MachineError(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) transitions-0.5.3/transitions/extensions/0000755000076500000240000000000013112746733021602 5ustar alneumanstaff00000000000000transitions-0.5.3/transitions/extensions/__init__.py0000644000076500000240000000042113024731355023704 0ustar alneumanstaff00000000000000from .diagrams import GraphMachine from .nesting import HierarchicalMachine from .locking import LockedMachine from .factory import MachineFactory, HierarchicalGraphMachine, LockedHierarchicalGraphMachine from .factory import LockedHierarchicalMachine, LockedGraphMachine transitions-0.5.3/transitions/extensions/diagrams.py0000644000076500000240000003707713105362257023756 0ustar alneumanstaff00000000000000import abc from ..core import Machine from ..core import Transition from .nesting import HierarchicalMachine try: import pygraphviz as pgv except ImportError: # pragma: no cover pgv = None import logging from functools import partial logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) class Diagram(object): def __init__(self, machine): self.machine = machine @abc.abstractmethod def get_graph(self): raise Exception('Abstract base Diagram.get_graph called!') class Graph(Diagram): machine_attributes = { 'directed': True, 'strict': False, 'rankdir': 'LR', 'ratio': '0.3', } style_attributes = { 'node': { 'default': { 'shape': 'circle', 'height': '1.2', 'style': 'filled', 'fillcolor': 'white', 'color': 'black', }, 'active': { 'color': 'red', 'fillcolor': 'darksalmon', 'shape': 'doublecircle' }, 'previous': { 'color': 'blue', 'fillcolor': 'azure2', } }, 'edge': { 'default': { 'color': 'black' }, 'previous': { 'color': 'blue' } }, 'graph': { 'default': { 'color': 'black', 'fillcolor': 'white' }, 'previous': { 'color': 'blue', 'fillcolor': 'azure2', 'style': 'filled' }, 'active': { 'color': 'red', 'fillcolor': 'darksalmon', 'style': 'filled' }, } } def __init__(self, *args, **kwargs): super(Graph, self).__init__(*args, **kwargs) def _add_nodes(self, states, container): for state in states: shape = self.style_attributes['node']['default']['shape'] container.add_node(state, shape=shape) def _add_edges(self, events, container): for event in events.values(): label = str(event.name) if self._omit_auto_transitions(event, label): continue for transitions in event.transitions.items(): src = transitions[0] edge_attr = {} for t in transitions[1]: dst = t.dest edge_attr['label'] = self._transition_label(label, t) if container.has_edge(src, dst): edge = container.get_edge(src, dst) edge.attr['label'] = edge.attr['label'] + ' | ' + edge_attr['label'] else: container.add_edge(src, dst, **edge_attr) def _omit_auto_transitions(self, event, label): return self._is_auto_transition(event, label) and not self.machine.show_auto_transitions # auto transition events commonly a) start with the 'to_' prefix, followed by b) the state name # and c) contain a transition from each state to the target state (including the target) def _is_auto_transition(self, event, label): if label.startswith('to_') and len(event.transitions) == len(self.machine.states): state_name = label[len('to_'):] if state_name in self.machine.states: return True return False def rep(self, f): return f.__name__ if callable(f) else f def _transition_label(self, edge_label, tran): if self.machine.show_conditions and tran.conditions: return '{edge_label} [{conditions}]'.format( edge_label=edge_label, conditions=' & '.join( self.rep(c.func) if c.target else '!' + self.rep(c.func) for c in tran.conditions ), ) return edge_label def get_graph(self, title=None): """ Generate a DOT graph with pygraphviz, returns an AGraph object Args: title (string): Optional title for the graph. """ if not pgv: # pragma: no cover raise Exception('AGraph diagram requires pygraphviz') if title is False: title = '' fsm_graph = pgv.AGraph(label=title, compound=True, **self.machine_attributes) fsm_graph.node_attr.update(self.style_attributes['node']['default']) fsm_graph.edge_attr.update(self.style_attributes['edge']['default']) # For each state, draw a circle self._add_nodes(self.machine.states, fsm_graph) self._add_edges(self.machine.events.copy(), fsm_graph) setattr(fsm_graph, 'style_attributes', self.style_attributes) return fsm_graph class NestedGraph(Graph): machine_attributes = Graph.machine_attributes.copy() machine_attributes.update( {'clusterrank': 'local', 'rankdir': 'TB', 'ratio': 0.8}) def __init__(self, *args, **kwargs): self.seen_nodes = [] self.seen_transitions = [] super(NestedGraph, self).__init__(*args, **kwargs) self.style_attributes['edge']['default']['minlen'] = 2 def _add_nodes(self, states, container): states = [self.machine.get_state(state) for state in states] if isinstance(states, dict) else states for state in states: if state.name in self.seen_nodes: continue self.seen_nodes.append(state.name) if len(state.children) > 0: cluster_name = "cluster_" + state.name sub = container.add_subgraph(name=cluster_name, label=state.name, rank='source', **self.style_attributes['graph']['default']) anchor_name = state.name + "_anchor" root_container = sub.add_subgraph(name=state.name + '_root') child_container = sub.add_subgraph(name=cluster_name + '_child', label='', color=None) root_container.add_node(anchor_name, shape='point', fillcolor='black', width='0.1') self._add_nodes(state.children, child_container) else: container.add_node(state.name, shape=self.style_attributes['node']['default']['shape']) def _add_edges(self, events, container): for sub in container.subgraphs_iter(): events = self._add_edges(events, sub) for event in events.values(): label = str(event.name) if self._omit_auto_transitions(event, label): continue for transitions in event.transitions.items(): src = transitions[0] if not container.has_node(src) and _get_subgraph(container, "cluster_" + src) is None: continue src = self.machine.get_state(src) edge_attr = {} label_pos = 'label' if len(src.children) > 0: edge_attr['ltail'] = "cluster_" + src.name src = src.name + "_anchor" else: src = src.name for t in transitions[1]: if t in self.seen_transitions: continue if not container.has_node(t.dest) and _get_subgraph(container, 'cluster_' + t.dest) is None: continue self.seen_transitions.append(t) dst = self.machine.get_state(t.dest) if len(dst.children) > 0: edge_attr['lhead'] = "cluster_" + dst.name dst = dst.name + '_anchor' else: dst = dst.name if 'ltail' in edge_attr: if _get_subgraph(container, edge_attr['ltail']).has_node(dst): del edge_attr['ltail'] edge_attr[label_pos] = self._transition_label(label, t) if container.has_edge(src, dst): edge = container.get_edge(src, dst) edge.attr[label_pos] += ' | ' + edge_attr[label_pos] else: container.add_edge(src, dst, **edge_attr) return events class GraphMachine(Machine): _pickle_blacklist = ['graph'] def __getstate__(self): return {k: v for k, v in self.__dict__.items() if k not in self._pickle_blacklist} def __setstate__(self, state): self.__dict__.update(state) for model in self.models: graph = self._get_graph(model, title=self.title) self.set_node_style(graph, model.state, 'active') def __init__(self, *args, **kwargs): # remove graph config from keywords self.title = kwargs.pop('title', 'State Machine') self.show_conditions = kwargs.pop('show_conditions', False) self.show_auto_transitions = kwargs.pop('show_auto_transitions', False) # Update March 2017: This temporal overwrite does not work # well with inheritance. Since the tests pass I will disable it # for now. If issues arise during initialization we might have to review this again. # # temporally disable overwrites since graphing cannot # # be initialized before base machine # add_states = self.add_states # add_transition = self.add_transition # self.add_states = super(GraphMachine, self).add_states # self.add_transition = super(GraphMachine, self).add_transition super(GraphMachine, self).__init__(*args, **kwargs) # # Second part of overwrite # self.add_states = add_states # self.add_transition = add_transition # Create graph at beginning for model in self.models: if hasattr(model, 'get_graph'): raise AttributeError('Model already has a get_graph attribute. Graph retrieval cannot be bound.') setattr(model, 'get_graph', partial(self._get_graph, model)) model.get_graph() self.set_node_state(model.graph, self.initial, 'active') # for backwards compatibility assign get_combined_graph to get_graph # if model is not the machine if not hasattr(self, 'get_graph'): setattr(self, 'get_graph', self.get_combined_graph) def _get_graph(self, model, title=None, force_new=False, show_roi=False): if title is None: title = self.title if not hasattr(model, 'graph') or force_new: model.graph = NestedGraph(self).get_graph(title) if isinstance(self, HierarchicalMachine) \ else Graph(self).get_graph(title) self.set_node_state(model.graph, model.state, state='active') return model.graph if not show_roi else self._graph_roi(model) def get_combined_graph(self, title=None, force_new=False, show_roi=False): logger.info('Returning graph of the first model. In future releases, this ' + 'method will return a combined graph of all models.') return self._get_graph(self.models[0], title, force_new, show_roi) def set_edge_state(self, graph, edge_from, edge_to, state='default', label=None): """ Mark a node as active by changing the attributes """ if not self.show_auto_transitions and not graph.has_edge(edge_from, edge_to): graph.add_edge(edge_from, edge_to, label) edge = graph.get_edge(edge_from, edge_to) self.set_edge_style(graph, edge, state) def add_states(self, *args, **kwargs): super(GraphMachine, self).add_states(*args, **kwargs) for model in self.models: model.get_graph(force_new=True) def add_transition(self, *args, **kwargs): super(GraphMachine, self).add_transition(*args, **kwargs) for model in self.models: model.get_graph(force_new=True) def reset_graph(self, graph): # Reset all the edges for e in graph.edges_iter(): self.set_edge_style(graph, e, 'default') for n in graph.nodes_iter(): if 'point' not in n.attr['shape']: self.set_node_style(graph, n, 'default') for g in graph.subgraphs_iter(): self.set_graph_style(graph, g, 'default') def set_node_state(self, graph, node_name, state='default'): if graph.has_node(node_name): node = graph.get_node(node_name) func = self.set_node_style else: node = graph node = _get_subgraph(node, 'cluster_' + node_name) func = self.set_graph_style func(graph, node, state) @staticmethod def _graph_roi(model): g = model.graph filtered = g.copy() kept_nodes = set() active_state = model.state if g.has_node(model.state) else model.state + '_anchor' kept_nodes.add(active_state) # remove all edges that have no connection to the currently active state for e in filtered.edges(): if active_state not in e: filtered.delete_edge(e) # find the ingoing edge by color; remove the rest for e in filtered.in_edges(active_state): if e.attr['color'] == g.style_attributes['edge']['previous']['color']: kept_nodes.add(e[0]) else: filtered.delete_edge(e) # remove outgoing edges from children for e in filtered.out_edges_iter(active_state): kept_nodes.add(e[1]) for n in filtered.nodes(): if n not in kept_nodes: filtered.delete_node(n) return filtered @staticmethod def set_node_style(graph, node_name, style='default'): node = graph.get_node(node_name) style_attr = graph.style_attributes.get('node', {}).get(style) node.attr.update(style_attr) @staticmethod def set_edge_style(graph, edge, style='default'): style_attr = graph.style_attributes.get('edge', {}).get(style) edge.attr.update(style_attr) @staticmethod def set_graph_style(graph, item, style='default'): style_attr = graph.style_attributes.get('graph', {}).get(style) item.graph_attr.update(style_attr) @staticmethod def _create_transition(*args, **kwargs): return TransitionGraphSupport(*args, **kwargs) class TransitionGraphSupport(Transition): def _change_state(self, event_data): machine = event_data.machine model = event_data.model dest = machine.get_state(self.dest) # Mark the active node machine.reset_graph(model.graph) # Mark the previous node and path used if self.source is not None: source = machine.get_state(self.source) machine.set_node_state(model.graph, source.name, state='previous') machine.set_node_state(model.graph, dest.name, state='active') if hasattr(source, 'children') and len(source.children) > 0: source = source.name + '_anchor' else: source = source.name if hasattr(dest, 'children') and len(dest.children) > 0: dest = dest.name + '_anchor' else: dest = dest.name machine.set_edge_state(model.graph, source, dest, state='previous', label=event_data.event.name) super(TransitionGraphSupport, self)._change_state(event_data) def _get_subgraph(g, name): sg = g.get_subgraph(name) if sg: return sg for sub in g.subgraphs_iter(): sg = _get_subgraph(sub, name) if sg: return sg return None transitions-0.5.3/transitions/extensions/factory.py0000644000076500000240000000341212731775171023627 0ustar alneumanstaff00000000000000from ..core import Machine from .nesting import HierarchicalMachine, NestedTransition, NestedEvent from .locking import LockedMachine, LockedEvent from .diagrams import GraphMachine, TransitionGraphSupport class MachineFactory(object): # get one of the predefined classes which fulfill the criteria @staticmethod def get_predefined(graph=False, nested=False, locked=False): if graph and nested and locked: return LockedHierarchicalGraphMachine elif locked and nested: return LockedHierarchicalMachine elif locked and graph: return LockedGraphMachine elif nested and graph: return HierarchicalGraphMachine elif graph: return GraphMachine elif nested: return HierarchicalMachine elif locked: return LockedMachine else: return Machine class NestedGraphTransition(TransitionGraphSupport, NestedTransition): pass class LockedNestedEvent(LockedEvent, NestedEvent): pass class HierarchicalGraphMachine(GraphMachine, HierarchicalMachine): @staticmethod def _create_transition(*args, **kwargs): return NestedGraphTransition(*args, **kwargs) class LockedHierarchicalMachine(LockedMachine, HierarchicalMachine): @staticmethod def _create_event(*args, **kwargs): return LockedNestedEvent(*args, **kwargs) class LockedGraphMachine(GraphMachine, LockedMachine): pass class LockedHierarchicalGraphMachine(GraphMachine, LockedMachine, HierarchicalMachine): @staticmethod def _create_transition(*args, **kwargs): return NestedGraphTransition(*args, **kwargs) @staticmethod def _create_event(*args, **kwargs): return LockedNestedEvent(*args, **kwargs) transitions-0.5.3/transitions/extensions/locking.py0000644000076500000240000001050213102052342023561 0ustar alneumanstaff00000000000000from transitions.core import Machine, Event, listify from collections import defaultdict from functools import partial from threading import Lock import inspect import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) try: from contextlib import nested # Python 2 from thread import get_ident # with nested statements now raise a DeprecationWarning. Should be replaced with ExitStack-like approaches. import warnings warnings.simplefilter('ignore', DeprecationWarning) except ImportError: from contextlib import ExitStack, contextmanager from threading import get_ident @contextmanager def nested(*contexts): """ Reimplementation of nested in python 3. """ with ExitStack() as stack: for ctx in contexts: stack.enter_context(ctx) yield contexts class PickleableLock(object): def __init__(self): self.lock = Lock() def __getstate__(self): return '' def __setstate__(self, value): return self.__init__() def __getattr__(self, item): return self.lock.__getattr__(item) def __enter__(self): self.lock.__enter__() def __exit__(self, exc_type, exc_val, exc_tb): self.lock.__exit__(exc_type, exc_val, exc_tb) class LockedEvent(Event): def trigger(self, model, *args, **kwargs): if self.machine._locked != get_ident(): with nested(*self.machine.model_context_map[model]): return super(LockedEvent, self).trigger(model, *args, **kwargs) else: return super(LockedEvent, self).trigger(model, *args, **kwargs) class LockedMachine(Machine): def __init__(self, *args, **kwargs): self._locked = 0 try: self.machine_context = listify(kwargs.pop('machine_context')) except KeyError: self.machine_context = [PickleableLock()] self.machine_context.append(self) self.model_context_map = defaultdict(list) super(LockedMachine, self).__init__(*args, **kwargs) def add_model(self, model, *args, **kwargs): models = listify(model) try: model_context = listify(kwargs.pop('model_context')) except KeyError: model_context = [] output = super(LockedMachine, self).add_model(models, *args, **kwargs) for model in models: model = self if model == 'self' else model self.model_context_map[model].extend(self.machine_context) self.model_context_map[model].extend(model_context) return output def remove_model(self, model, *args, **kwargs): models = listify(model) for model in models: del self.model_context_map[model] return super(LockedMachine, self).remove_model(models, *args, **kwargs) def __getattribute__(self, item): f = super(LockedMachine, self).__getattribute__ tmp = f(item) if not item.startswith('_') and inspect.ismethod(tmp): return partial(f('_locked_method'), tmp) return tmp def __getattr__(self, item): try: return super(LockedMachine, self).__getattribute__(item) except AttributeError: return super(LockedMachine, self).__getattr__(item) # Determine if the returned method is a partial and make sure the returned partial has # not been created by Machine.__getattr__. # https://github.com/tyarkoni/transitions/issues/214 def _add_model_to_state(self, state, model): super(LockedMachine, self)._add_model_to_state(state, model) for prefix in ['enter', 'exit']: callback = "on_{0}_".format(prefix) + state.name func = getattr(model, callback, None) if isinstance(func, partial) and func.func != state.add_callback: state.add_callback(prefix, callback) def _locked_method(self, func, *args, **kwargs): if self._locked != get_ident(): with nested(*self.machine_context): return func(*args, **kwargs) else: return func(*args, **kwargs) def __enter__(self): self._locked = get_ident() def __exit__(self, *exc): self._locked = 0 @staticmethod def _create_event(*args, **kwargs): return LockedEvent(*args, **kwargs) transitions-0.5.3/transitions/extensions/nesting.py0000644000076500000240000003573613077321321023631 0ustar alneumanstaff00000000000000from ..core import Machine, Transition, State, Event, listify, MachineError, EventData from six import string_types import copy from functools import partial import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) class FunctionWrapper(object): def __init__(self, func, path): if len(path) > 0: self.add(func, path) self._func = None else: self._func = func def add(self, func, path): if len(path) > 0: name = path[0] if name[0].isdigit(): name = 's' + name if hasattr(self, name): getattr(self, name).add(func, path[1:]) else: x = FunctionWrapper(func, path[1:]) setattr(self, name, x) else: self._func = func def __call__(self, *args, **kwargs): return self._func(*args, **kwargs) # Added parent and children parameter children is a list of NestedStates # and parent is the full name of the parent e.g. Foo_Bar_Baz. class NestedState(State): separator = '_' def __init__(self, name, on_enter=None, on_exit=None, ignore_invalid_triggers=None, parent=None, initial=None): self._name = name self._initial = initial self._parent = None self.parent = parent super(NestedState, self).__init__(name=name, on_enter=on_enter, on_exit=on_exit, ignore_invalid_triggers=ignore_invalid_triggers) self.children = [] @property def parent(self): return self._parent @parent.setter def parent(self, value): if value is not None: self._parent = value self._parent.children.append(self) @property def initial(self): return self.name + NestedState.separator + self._initial if self._initial else None @property def level(self): return self.parent.level + 1 if self.parent is not None else 0 @property def name(self): return (self.parent.name + NestedState.separator + self._name) if self.parent else self._name @name.setter def name(self, value): self._name = value def exit_nested(self, event_data, target_state): if self.level > target_state.level: self.exit(event_data) return self.parent.exit_nested(event_data, target_state) elif self.level <= target_state.level: tmp_state = target_state while self.level != tmp_state.level: tmp_state = tmp_state.parent tmp_self = self while tmp_self.level > 0 and tmp_state.parent.name != tmp_self.parent.name: tmp_self.exit(event_data) tmp_self = tmp_self.parent tmp_state = tmp_state.parent if tmp_self != tmp_state: tmp_self.exit(event_data) return tmp_self.level else: return tmp_self.level + 1 def enter_nested(self, event_data, level=None): if level is not None and level <= self.level: if level != self.level: self.parent.enter_nested(event_data, level) self.enter(event_data) class NestedTransition(Transition): def execute(self, event_data): dest_state = event_data.machine.get_state(self.dest) while dest_state.initial: dest_state = event_data.machine.get_state(dest_state.initial) self.dest = dest_state.name return super(NestedTransition, self).execute(event_data) # The actual state change method 'execute' in Transition was restructured to allow overriding def _change_state(self, event_data): machine = event_data.machine model = event_data.model dest_state = machine.get_state(self.dest) source_state = machine.get_state(model.state) lvl = source_state.exit_nested(event_data, dest_state) event_data.machine.set_state(self.dest, model) event_data.update(model) dest_state.enter_nested(event_data, lvl) class NestedEvent(Event): def _trigger(self, model, *args, **kwargs): state = self.machine.get_state(model.state) while state.parent and state.name not in self.transitions: state = state.parent if state.name not in self.transitions: msg = "%sCan't trigger event %s from state %s!" % (self.machine.id, self.name, model.state) if self.machine.get_state(model.state).ignore_invalid_triggers: logger.warning(msg) else: raise MachineError(msg) event_data = EventData(self.machine.get_state(model.state), self, self.machine, model, args=args, kwargs=kwargs) for func in self.machine.prepare_event: self.machine._callback(func, event_data) logger.debug("Executed machine preparation callback '%s' before conditions." % func) try: for t in self.transitions[state.name]: event_data.transition = t if t.execute(event_data): event_data.result = True break except Exception as e: event_data.error = e raise finally: for func in self.machine.finalize_event: self.machine._callback(func, event_data) logger.debug("Executed machine finalize callback '%s'." % func) return event_data.result class HierarchicalMachine(Machine): def __init__(self, *args, **kwargs): self._buffered_transitions = [] super(HierarchicalMachine, self).__init__(*args, **kwargs) def add_model(self, model): super(HierarchicalMachine, self).add_model(model) models = listify(model) for m in models: m = self if m == 'self' else m if hasattr(m, 'to'): logger.warning("%sModel already has a 'to'-method. It will NOT be overwritten by NestedMachine", self.id) else: to_func = partial(self.to, m) setattr(m, 'to', to_func) # Instead of creating transitions directly, Machine now use a factory method which can be overridden @staticmethod def _create_transition(*args, **kwargs): return NestedTransition(*args, **kwargs) @staticmethod def _create_event(*args, **kwargs): return NestedEvent(*args, **kwargs) @staticmethod def _create_state(*args, **kwargs): return NestedState(*args, **kwargs) def is_state(self, state_name, model, allow_substates=False): if not allow_substates: return model.state == state_name temp_state = self.get_state(model.state) while not temp_state.name == state_name and temp_state.level > 0: temp_state = temp_state.parent return temp_state.name == state_name def traverse(self, states, on_enter=None, on_exit=None, ignore_invalid_triggers=None, parent=None, remap={}): states = listify(states) new_states = [] ignore = ignore_invalid_triggers if ignore is None: ignore = self.ignore_invalid_triggers for state in states: tmp_states = [] # other state representations are handled almost like in the base class but a parent parameter is added if isinstance(state, string_types): if state in remap: continue tmp_states.append(self._create_state(state, on_enter=on_enter, on_exit=on_exit, parent=parent, ignore_invalid_triggers=ignore)) elif isinstance(state, dict): if state['name'] in remap: continue state = copy.deepcopy(state) if 'ignore_invalid_triggers' not in state: state['ignore_invalid_triggers'] = ignore state['parent'] = parent if 'children' in state: # Concat the state names with the current scope. The scope is the concatenation of all # previous parents. Call traverse again to check for more nested states. p = self._create_state(state['name'], on_enter=on_enter, on_exit=on_exit, ignore_invalid_triggers=ignore, parent=parent, initial=state.get('initial', None)) nested = self.traverse(state['children'], on_enter=on_enter, on_exit=on_exit, ignore_invalid_triggers=ignore, parent=p, remap=state.get('remap', {})) tmp_states.append(p) tmp_states.extend(nested) else: tmp_states.insert(0, self._create_state(**state)) elif isinstance(state, HierarchicalMachine): # copy only states not mentioned in remap copied_states = [s for s in state.states.values() if s.name not in remap] # inner_states are the root states of the passed machine # which have be attached to the parent inner_states = [s for s in copied_states if s.level == 0] for s in inner_states: s.parent = parent tmp_states.extend(copied_states) for trigger, event in state.events.items(): if trigger.startswith('to_'): path = trigger[3:].split(NestedState.separator) # do not copy auto_transitions since they would not be valid anymore; # trigger and destination do not exist in the new environment if path[0] in remap: continue ppath = parent.name.split(NestedState.separator) path = ['to_' + ppath[0]] + ppath[1:] + path trigger = '.'.join(path) # adjust all transition start and end points to new state names for transitions in event.transitions.values(): for transition in transitions: src = transition.source # transitions from remapped states will be filtered to prevent # unexpected behaviour in the parent machine if src in remap: continue dst = parent.name + NestedState.separator + transition.dest\ if transition.dest not in remap else remap[transition.dest] conditions = [] unless = [] for c in transition.conditions: conditions.append(c.func) if c.target else unless.append(c.func) self._buffered_transitions.append({'trigger': trigger, 'source': parent.name + NestedState.separator + src, 'dest': dst, 'conditions': conditions, 'unless': unless, 'prepare': transition.prepare, 'before': transition.before, 'after': transition.after}) elif isinstance(state, NestedState): tmp_states.append(state) else: raise ValueError("%s cannot be added to the machine since its type is not known." % state) new_states.extend(tmp_states) duplicate_check = [] for s in new_states: if s.name in duplicate_check: state_names = [s.name for s in new_states] raise ValueError("State %s cannot be added since it is already in state list %s." % (s.name, state_names)) else: duplicate_check.append(s.name) return new_states def add_states(self, states, *args, **kwargs): # preprocess states to flatten the configuration and resolve nesting new_states = self.traverse(states, *args, **kwargs) super(HierarchicalMachine, self).add_states(new_states, *args, **kwargs) # for t in self._buffered_transitions: # print(t['trigger']) while len(self._buffered_transitions) > 0: args = self._buffered_transitions.pop() self.add_transition(**args) def get_triggers(self, *args): # add parents to state set states = [] for state in args: s = self.get_state(state) while s.parent: states.append(s.parent.name) s = s.parent states.extend(args) return super(HierarchicalMachine, self).get_triggers(*states) def add_transition(self, trigger, source, dest, conditions=None, unless=None, before=None, after=None, prepare=None, **kwargs): if isinstance(source, string_types): source = [x.name for x in self.states.values()] if source == '*' else [source] # FunctionWrappers are only necessary if a custom separator is used if trigger not in self.events: self.events[trigger] = self._create_event(trigger, self) for model in self.models: self._add_trigger_to_model(trigger, model) super(HierarchicalMachine, self).add_transition(trigger, source, dest, conditions=conditions, unless=unless, prepare=prepare, before=before, after=after, **kwargs) def _add_trigger_to_model(self, trigger, model): if trigger.startswith('to_') and NestedState.separator != '_': path = trigger[3:].split(NestedState.separator) print(path) trig_func = partial(self.events[trigger].trigger, model) if hasattr(model, 'to_' + path[0]): t = getattr(model, 'to_' + path[0]) t.add(trig_func, path[1:]) else: t = FunctionWrapper(trig_func, path[1:]) setattr(model, 'to_' + path[0], t) else: super(HierarchicalMachine, self)._add_trigger_to_model(trigger, model) def on_enter(self, state_name, callback): self.get_state(state_name).add_callback('enter', callback) def on_exit(self, state_name, callback): self.get_state(state_name).add_callback('exit', callback) def to(self, model, state_name, *args, **kwargs): event = EventData(self.get_state(model.state), Event('to', self), self, model, args=args, kwargs=kwargs) self._create_transition(model.state, state_name).execute(event) transitions-0.5.3/transitions/version.py0000644000076500000240000000002613102053305021421 0ustar alneumanstaff00000000000000__version__ = '0.5.3' transitions-0.5.3/transitions.egg-info/0000755000076500000240000000000013112746733021075 5ustar alneumanstaff00000000000000transitions-0.5.3/transitions.egg-info/dependency_links.txt0000644000076500000240000000000113112746732025142 0ustar alneumanstaff00000000000000 transitions-0.5.3/transitions.egg-info/PKG-INFO0000644000076500000240000000136713112746732022200 0ustar alneumanstaff00000000000000Metadata-Version: 1.1 Name: transitions Version: 0.5.3 Summary: A lightweight, object-oriented Python state machine implementation. Home-page: http://github.com/tyarkoni/transitions Author: Tal Yarkoni Author-email: tyarkoni@gmail.com License: MIT Download-URL: https://github.com/tyarkoni/transitions/archive/0.5.3.tar.gz Description: UNKNOWN Platform: UNKNOWN Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 transitions-0.5.3/transitions.egg-info/requires.txt0000644000076500000240000000003113112746732023466 0ustar alneumanstaff00000000000000six [test] nose>=0.10.1 transitions-0.5.3/transitions.egg-info/SOURCES.txt0000644000076500000240000000070313112746733022761 0ustar alneumanstaff00000000000000LICENSE MANIFEST.in setup.cfg setup.py transitions/__init__.py transitions/core.py transitions/version.py transitions.egg-info/PKG-INFO transitions.egg-info/SOURCES.txt transitions.egg-info/dependency_links.txt transitions.egg-info/requires.txt transitions.egg-info/top_level.txt transitions/extensions/__init__.py transitions/extensions/diagrams.py transitions/extensions/factory.py transitions/extensions/locking.py transitions/extensions/nesting.pytransitions-0.5.3/transitions.egg-info/top_level.txt0000644000076500000240000000001413112746732023621 0ustar alneumanstaff00000000000000transitions