def _create_transitions(self, transitions): """creates a dictionary of (old state name, new state name): Transition key value pairs""" if has_doubles([(t["old_state"], t.get("new_state")) for t in transitions]): raise MachineError( "two transitions between same states in state machine configuration" ) transition_dict = {} transitions = self._expand_transitions(transitions) for transition in transitions: old_path, new_path = Path(transition["old_state"]), Path( transition["new_state"]) if (old_path, new_path) in transition_dict: raise MachineError( "two transitions between same sub_states in state machine") if not (old_path.has_in(self) and new_path.has_in(self)): raise MachineError( "non-existing state when constructing transitions") transition_dict[old_path, new_path] = self.transition_class(machine=self, **transition) return transition_dict
def set_state(self, obj, state): """ Executes the transition when called by setting the state: obj.state = 'some_state' """ if obj.state != state: transition = self._get_transition(Path(obj.state), Path(state)) if not transition: raise TransitionError("transition <%s, %s> does not exist" % (obj.state, state)) transition.execute(obj)
def get_initial_path(self, initial=None): """ returns the path string to the actual initial state the object will be in """ try: full_initial_path = Path( initial or ()).get_in(self).get_nested_initial_state().full_path except KeyError: raise TransitionError("state '%s' does not exist in state '%s'" % (initial, self.name)) return full_initial_path.tail(self.full_path)
def __getitem__(self, key): """ Gets sub states according to string key or transition according to the 2-tuple (e.g. key: ("on.washing", "off.broken")) """ if isinstance(key, str): return self.sub_states[key] elif isinstance(key, tuple) and len(key) == 2: return self.transitions[Path(key[0]), Path(key[1])] raise KeyError("key is not a string or 2-tuple")
def _create_triggering(self, transitions): """creates a dictionary of (old state name/path, trigger name): Transition key value pairs""" trigger_dict = defaultdict(list) for transition in transitions: old_path, new_path = Path(transition["old_state"]), Path( transition["new_state"]) for trigger_name in listify(transition.get("triggers", ())): trigger_dict[(old_path, trigger_name)].append(self.transitions[old_path, new_path]) return self._check_triggering(trigger_dict)
def __init__(self, machine=None, on_entry=(), on_exit=(), condition=(), *args, **kwargs): """ Constructor of ChildState: :param machine: state machine that contains this state :param on_entry: callback(s) that will be called, when an object enters this state :param on_exit: callback(s) that will be called, when an object exits this state :param condition: callback(s) (all()) that determine whether entry in this state is allowed """ super(State, self).__init__(*args, **kwargs) self.machine = machine self.on_entry = callbackify(on_entry) self.on_exit = callbackify(on_exit) self.condition = callbackify(condition) if condition else None self.initial_path = Path() self.before_entry = [] self.after_entry = [] self.before_exit = [] self.after_exit = []
def full_path(self): """ returns the path from the top state machine to this state """ try: return self.__full_path except AttributeError: self.__full_path = Path(reversed([s.name for s in self.iter_up()])) return self.__full_path
def __init__(self, machine, old_state, new_state, on_transfer=(), condition=(), triggers=(), info=""): self.machine = machine self._validate_states(old_state, new_state) self.old_path = Path(old_state) self.new_path = Path(new_state) self.old_state = self.old_path.get_in(machine) self.new_state = self.new_path.get_in(machine) self.on_transfer = callbackify(on_transfer) self.condition = callbackify(condition) if condition else None self.triggers = triggers self.info = info
def _config(self, **kwargs): kwargs = deepcopy(kwargs) def convert(item): if isinstance(item, str): return item return nameify(item) return Path.apply_all(kwargs, convert)
def clear_after_exit(self, state): """ clears all dynamic (post-construction) callbacks to be called on exit of this or a sub-state""" Path(state).get_in(self).after_exit[:] = []
class Transition(object): """class for the internal representation of transitions in the state machine""" def __init__(self, machine, old_state, new_state, on_transfer=(), condition=(), triggers=(), info=""): self.machine = machine self._validate_states(old_state, new_state) self.old_path = Path(old_state) self.new_path = Path(new_state) self.old_state = self.old_path.get_in(machine) self.new_state = self.new_path.get_in(machine) self.on_transfer = callbackify(on_transfer) self.condition = callbackify(condition) if condition else None self.triggers = triggers self.info = info def _validate_states(self, old_state, new_state): """ assures that no internal transitions are defined on an outer state level""" old_states = old_state.split(".", 1) new_states = new_state.split(".", 1) if (len(old_states) > 1 or len(new_states) > 1) and old_states[0] == new_states[0]: raise MachineError( "inner transitions in a nested state machine cannot be defined at the outer level" ) def update_state(self, obj): obj._state = str(self.machine.full_path + self.new_path + self.new_path.get_in(self.machine).initial_path) @contextmanager def transitioning(self, obj): """ contextmanager to restore the previous state when any exception is raised in the callbacks """ old_state = obj._state try: yield except BaseException: obj._state = old_state raise def _execute(self, obj, *args, **kwargs): self.machine.do_prepare(obj, *args, **kwargs) if ((not self.condition or self.condition(obj, *args, **kwargs)) and (not self.new_state.condition or self.new_state.condition(obj, *args, **kwargs))): self.machine.do_exit(obj, *args, **kwargs) self.on_transfer(obj, *args, **kwargs) self.update_state(obj) self.machine.do_enter(obj, *args, **kwargs) return True return False def execute(self, obj, *args, **kwargs): """ Method calling all the callbacks of a state transition ans changing the actual object state (if condition returns True). :param obj: object of which the state is managed :param args: arguments of the callback :param kwargs: keyword arguments of the callback :return: bool, whether the transition took place """ with self.transitioning(obj): context_manager = self.machine.get_context_manager() if context_manager: with context_manager(obj, **kwargs) as context: return self._execute(obj, context=context, *args, **kwargs) else: return self._execute(obj, *args, **kwargs) def __str__(self): """string representing the transition""" return "<%s, %s>" % (str(self.old_path), str(self.new_path))
def do_enter(self, obj, *args, **kwargs): for state in Path(obj.state).tail(self.full_path).iter_in(self): state._enter(obj, *args, **kwargs)
def do_prepare(self, obj, *args, **kwargs): for state in Path(obj.state).tail(self.full_path).iter_out( self, include=True): state.prepare(obj, *args, **kwargs)
def do_exit(self, obj, *args, **kwargs): for state in Path(obj.state).tail(self.full_path).iter_out(self): state._exit(obj, *args, **kwargs)
def trigger(self, obj, trigger, *args, **kwargs): """ Executes the transition when called through a trigger """ for transition in self._get_transitions(Path(obj.state), trigger): if transition.execute(obj=obj, *args, **kwargs): return True return False
def inner_trigger(*args, **kwargs): for transition in self._get_transitions(Path(obj.state), trigger): if transition.execute(obj, *args, **kwargs): return True return False
def add_before_entry(self, state, *callbacks): """ adds a dynamic (post-construction) callback to be called on entry of this or a sub-state""" Path(state).get_in(self).before_entry.extend(callbacks)
def add_after_exit(self, state, *callbacks): """ adds a dynamic (post-construction) callback to be called on exit of this or a sub-state""" Path(state).get_in(self).after_exit.extend(callbacks)
def clear_before_entry(self, state): """ clears all dynamic (post-construction) callbacks to be called on entry of this or a sub-state""" Path(state).get_in(self).before_entry[:] = []