def add_transition(self, event_name, initial_states, resulting_state, pre_hooks=None, post_hooks=None, error_hooks=None): """ Declare a transition that is valid only if the current state is one of `initial_states`. The state after the transition will be `resulting_state`. :param event_name: :param initial_states: Can be a string if single state or a list if many states. Can accept wildcard "*" for all possible states. :param resulting_state: :return: """ event_name = self.name_for_event(event_name) initial_states = [ self.name_for_state(s) for s in listify(initial_states) ] resulting_state = self.name_for_state(resulting_state) # Not adding self._pre_hooks or self._post_hooks to `add_transition` for better control. # Otherwise self._pre_hooks+listify(pre_hooks) etc can be done. self._machine.add_transition(event_name, source=initial_states, dest=resulting_state, before=listify(pre_hooks), after=listify(post_hooks))
def set_state(self, states, model=None): """ Set the current state. Args: states (list of str or Enum or State): value of state(s) to be set model (optional[object]): targeted model; if not set, all models will be set to 'state' """ values = [self._set_state(value) for value in listify(states)] models = self.models if model is None else listify(model) for mod in models: setattr(mod, self.model_attribute, values if len(values) > 1 else values[0])
def test_listify(self): self.assertEqual(listify(4), [4]) self.assertEqual(listify(None), []) self.assertEqual(listify((4, 5)), (4, 5)) self.assertEqual(listify([1, 3]), [1, 3]) class Foo: pass obj = Foo() proxy = weakref.proxy(obj) del obj self.assertEqual(listify(proxy), [proxy])
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: self.model_context_map[model].extend(self.machine_context) self.model_context_map[model].extend(model_context) return output
def _update_model(event_data, tree): model_states = _build_state_list( tree, event_data.machine.state_cls.separator) with event_data.machine(): event_data.machine.set_state(model_states, event_data.model) states = event_data.machine.get_states(listify(model_states)) event_data.state = states[0] if len(states) == 1 else states
def remove_model(self, model, *args, **kwargs): models = listify(model) for model in models: del self.model_context_map[model] return super(LockedMachine, self).add_model(models, *args, **kwargs)
def _change_state(self, event_data): graph = event_data.machine.model_graphs[event_data.model] graph.reset_styling() graph.set_previous_transition(self.source, self.dest, event_data.event.name) _super(TransitionGraphSupport, self)._change_state(event_data) # pylint: disable=protected-access for state in _flatten(listify(getattr(event_data.model, event_data.machine.model_attribute))): graph.set_node_style(self.dest if hasattr(state, 'name') else state, 'active')
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 add_substates(self, states): """ Adds a list of states to the current state. Args: states (list): List of states to add to the current state. """ for state in listify(states): self.states[state.name] = state
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 remove_model(self, model): """ Extends `transitions.core.Machine.remove_model` by removing model specific context maps from the machine when the model itself is removed. """ models = listify(model) for mod in models: del self.model_context_map[mod] return _super(LockedMachine, self).remove_model(models)
def add_model(self, model, initial=None): models = listify(model) super(GraphMachine, self).add_model(models, initial) for mod in models: mod = self if mod == 'self' else mod if hasattr(mod, 'get_graph'): raise AttributeError('Model already has a get_graph attribute. Graph retrieval cannot be bound.') setattr(mod, 'get_graph', partial(self._get_graph, mod)) _ = mod.get_graph(title=self.title, force_new=True) # initialises graph
def add_model(self, model, initial=None, model_context=None): """ Extends `transitions.core.Machine.add_model` by `model_context` keyword. Args: model (list or object): A model (list) to be managed by the machine. initial (string or State): The initial state of the passed model[s]. model_context (list or object): If passed, assign the context (list) to the machines model specific context map. """ models = listify(model) model_context = listify(model_context) if model_context is not None else [] output = _super(LockedMachine, self).add_model(models, initial) for mod in models: mod = self if mod == 'self' else mod self.model_context_map[mod].extend(self.machine_context) self.model_context_map[mod].extend(model_context) return output
def _enter_nested(self, root, dest, prefix_path, event_data): if root: state_name = root.pop(0) with event_data.machine(state_name): return self._enter_nested(root, dest, prefix_path, event_data) elif dest: new_states = OrderedDict() state_name = dest.pop(0) with event_data.machine(state_name): new_states[state_name], new_enter = self._enter_nested( [], dest, prefix_path + [state_name], event_data) enter_partials = [ partial(event_data.machine.scoped.scoped_enter, event_data, prefix_path) ] + new_enter return new_states, enter_partials elif event_data.machine.scoped.initial: new_states = OrderedDict() enter_partials = [] q = [] prefix = prefix_path scoped_tree = new_states initial_states = [ event_data.machine.scoped.states[i] for i in listify(event_data.machine.scoped.initial) ] while True: event_data.scope = prefix for state in initial_states: enter_partials.append( partial(state.scoped_enter, event_data, prefix)) scoped_tree[state.name] = OrderedDict() if state.initial: q.append( (scoped_tree[state.name], prefix + [state.name], [state.states[i] for i in listify(state.initial)])) if not q: break scoped_tree, prefix, initial_states = q.pop(0) return new_states, enter_partials else: return {}, []
def add_model(self, model, initial=None, model_context=None): """ Extends `transitions.core.Machine.add_model` by `model_context` keyword. Args: model (list or object): A model (list) to be managed by the machine. initial (str, Enum or State): The initial state of the passed model[s]. model_context (list or object): If passed, assign the context (list) to the machines model specific context map. """ models = listify(model) model_context = listify( model_context) if model_context is not None else [] output = _super(LockedMachine, self).add_model(models, initial) for mod in models: mod = self if mod == 'self' else mod self.model_context_map[mod].extend(self.machine_context) self.model_context_map[mod].extend(model_context) return output
def __init__(self, *args, **kwargs): self._locked = 0 try: self.machine_context = listify(kwargs.pop('machine_context')) except KeyError: self.machine_context = [PicklableLock()] self.machine_context.append(self) self.model_context_map = defaultdict(list) _super(LockedMachine, self).__init__(*args, **kwargs)
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 __init__(self, *args, **kwargs): try: self.machine_context = listify(kwargs.pop('machine_context')) except KeyError: self.machine_context = [RLock()] self.model_context_map = defaultdict(list) super(LockedMachine, self).__init__(*args, **kwargs) if self.machine_context: for model in self.models: self.model_context_map[model].extend(self.machine_context)
def add_model(self, model, initial=None): """ Extends transitions.core.Machine.add_model by applying a custom 'to' function to the added model. """ _super(HierarchicalMachine, self).add_model(model, initial=initial) models = listify(model) for mod in models: mod = self if mod == 'self' else mod # TODO: Remove 'mod != self' in 0.7.0 if hasattr(mod, 'to') and mod != self: _LOGGER.warning( "%sModel already has a 'to'-method. It will NOT " "be overwritten by NestedMachine", self.name) else: to_func = partial(self.to_state, mod) setattr(mod, 'to', to_func)
def _get_graph(self, model, title=None, force_new=False, show_roi=False): if force_new: grph = self.graph_cls(self, title=title if title is not None else self.title) self.model_graphs[model] = grph try: for state in _flatten(listify(getattr(model, self.model_attribute))): grph.set_node_style(self.dest if hasattr(state, 'name') else state, 'active') except AttributeError: _LOGGER.info("Could not set active state of diagram") try: m = self.model_graphs[model] except KeyError: _ = self._get_graph(model, title, force_new=True) m = self.model_graphs[model] m.roi_state = getattr(model, self.model_attribute) if show_roi else None return m.get_graph(title=title)
def _trigger_event(self, _model, _trigger, _state_tree, *args, **kwargs): if _state_tree is None: _state_tree = self._build_state_tree( listify(getattr(_model, self.model_attribute)), self.state_cls.separator) res = {} for key, value in _state_tree.items(): if value: with self(key): res[key] = self._trigger_event(_model, _trigger, value, *args, **kwargs) if not res.get(key, None) and _trigger in self.events: res[key] = self.events[_trigger].trigger( _model, self, *args, **kwargs) return None if not res or all( v is None for v in res.values()) else any(res.values())
def _resolve_initial(self, models, state_name_path, prefix=[]): if state_name_path: state_name = state_name_path.pop(0) with self(state_name): return self._resolve_initial(models, state_name_path, prefix=prefix + [state_name]) if self.scoped.initial: entered_states = [] for initial_state_name in listify(self.scoped.initial): with self(initial_state_name): entered_states.append( self._resolve_initial(models, [], prefix=prefix + [self.scoped.name])) return entered_states if len( entered_states) > 1 else entered_states[0] return self.state_cls.separator.join(prefix)
def _resolve_transition(self, event_data): machine = event_data.machine dst_name_path = machine.get_local_name(self.dest, join=False) _ = machine.get_state(dst_name_path) model_states = listify( getattr(event_data.model, machine.model_attribute)) state_tree = machine._build_state_tree(model_states, machine.state_cls.separator) scope = machine.get_global_name(join=False) src_name_path = event_data.source_path if src_name_path == dst_name_path: root = src_name_path[:-1] # exit and enter the same state else: root = [] while dst_name_path and src_name_path and src_name_path[ 0] == dst_name_path[0]: root.append(src_name_path.pop(0)) dst_name_path.pop(0) scoped_tree = reduce(dict.get, scope + root, state_tree) exit_partials = [] if src_name_path: for state_name in _resolve_order(scoped_tree): cb = partial( machine.get_state(root + state_name).scoped_exit, event_data, scope + root + state_name[:-1]) exit_partials.append(cb) if dst_name_path: new_states, enter_partials = self._enter_nested( root, dst_name_path, scope + root, event_data) else: new_states, enter_partials = {}, [] for key in scoped_tree: del scoped_tree[key] for new_key, value in new_states.items(): scoped_tree[new_key] = value break return state_tree, exit_partials, enter_partials
def add_model(self, model, initial=None): """ Extends transitions.core.Machine.add_model by applying a custom 'to' function to the added model. """ models = [mod if mod != 'self' else self for mod in listify(model)] _super(HierarchicalMachine, self).add_model(models, initial=initial) initial_name = getattr(models[0], self.model_attribute) if hasattr(initial_name, 'name'): initial_name = initial_name.name initial_states = self._resolve_initial( models, initial_name.split(self.state_cls.separator)) for mod in models: self.set_state(initial_states, mod) if hasattr(mod, 'to'): _LOGGER.warning( "%sModel already has a 'to'-method. It will NOT " "be overwritten by NestedMachine", self.name) else: to_func = partial(self.to_state, mod) setattr(mod, 'to', to_func)
def add_transition(self, trigger, source, dest, conditions=None, unless=None, before=None, after=None, prepare=None, **kwargs): if source != self.wildcard_all: source = [ self.state_cls.separator.join(self._get_enum_path(s)) if isinstance(s, Enum) else s for s in listify(source) ] if dest != self.wildcard_same: dest = self.state_cls.separator.join( self._get_enum_path(dest)) if isinstance(dest, Enum) else dest _super(HierarchicalMachine, self).add_transition(trigger, source, dest, conditions, unless, before, after, prepare, **kwargs)
def on_timeout(self, value): """ Listifies passed values and assigns them to on_timeout.""" self._on_timeout = listify(value)
def _add_error_hooks(self, error_hooks): self._error_hooks += listify(error_hooks)
def test_listify(self): self.assertEquals(listify(4), [4]) self.assertEquals(listify(None), []) self.assertEquals(listify((4, 5)), (4, 5)) self.assertEquals(listify([1, 3]), [1, 3])
def _add_post_hooks(self, post_hooks): self._post_hooks += listify(post_hooks)
def add_states(self, states, on_enter=None, on_exit=None, ignore_invalid_triggers=None, **kwargs): """ Add new nested state(s). Args: states (list, str, dict, Enum or NestedState): a list, a NestedState instance, the name of a new state, an enumeration (member) or a dict with keywords to pass on to the NestedState initializer. If a list, each element can be a string, NestedState or enumeration member. on_enter (str or list): callbacks to trigger when the state is entered. Only valid if first argument is string. on_exit (str 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. **kwargs additional keyword arguments used by state mixins. """ remap = kwargs.pop('remap', None) for state in listify(states): if isinstance(state, Enum) and isinstance(state.value, EnumMeta): state = {'name': state.name, 'children': state.value} if isinstance(state, string_types): if remap is not None and state in remap: return domains = state.split(self.state_cls.separator, 1) if len(domains) > 1: try: self.get_state(domains[0]) except ValueError: self.add_state( domains[0], on_enter=on_enter, on_exit=on_exit, ignore_invalid_triggers=ignore_invalid_triggers, **kwargs) with self(domains[0]): self.add_states( domains[1], on_enter=on_enter, on_exit=on_exit, ignore_invalid_triggers=ignore_invalid_triggers, **kwargs) else: if state in self.states: raise ValueError( "State %s cannot be added since it already exists." % (state, )) new_state = self._create_state(state) self.states[new_state.name] = new_state self._init_state(new_state) elif isinstance(state, Enum): if remap is not None and state.name in remap: return new_state = self._create_state(state) if state.name in self.states: raise ValueError( "State %s cannot be added since it already exists." % (state.name, )) self.states[new_state.name] = new_state self._init_state(new_state) elif isinstance(state, dict): if remap is not None and state['name'] in remap: return state = state.copy( ) # prevent messing with the initially passed dict remap = state.pop('remap', None) state_children = state.pop('children', []) state_parallel = state.pop('parallel', []) transitions = state.pop('transitions', []) new_state = self._create_state(**state) self.states[new_state.name] = new_state self._init_state(new_state) remapped_transitions = [] with self(new_state.name): if state_parallel: self.add_states(state_parallel, remap=remap, **kwargs) new_state.initial = [ s if isinstance(s, string_types) else s['name'] for s in state_parallel ] else: self.add_states(state_children, remap=remap, **kwargs) if remap is not None: drop_event = [] for evt in self.events.values(): self.events[evt.name] = copy.copy(evt) for trigger, event in self.events.items(): drop_source = [] event.transitions = copy.deepcopy( event.transitions) for source_name, trans_source in event.transitions.items( ): if source_name in remap: drop_source.append(source_name) continue drop_trans = [] for trans in trans_source: if trans.dest in remap: conditions, unless = [], [] for cond in trans.conditions: # split a list in two lists based on the accessors (cond.target) truth value (unless, conditions)[cond.target].append( cond.func) remapped_transitions.append({ 'trigger': trigger, 'source': new_state.name + self.state_cls.separator + trans.source, 'dest': remap[trans.dest], 'conditions': conditions, 'unless': unless, 'prepare': trans.prepare, 'before': trans.before, 'after': trans.after }) drop_trans.append(trans) for t in drop_trans: trans_source.remove(t) if not trans_source: drop_source.append(source_name) for s in drop_source: del event.transitions[s] if not event.transitions: drop_event.append(trigger) for e in drop_event: del self.events[e] if transitions: self.add_transitions(transitions) self.add_transitions(remapped_transitions) elif isinstance(state, NestedState): if state.name in self.states: raise ValueError( "State %s cannot be added since it already exists." % (state.name, )) self.states[state.name] = state self._init_state(state) elif isinstance(state, Machine): new_states = [ s for s in state.states.values() if remap is None or s not in remap ] self.add_states(new_states) for ev in state.events.values(): self.events[ev.name] = ev if self.scoped.initial is None: self.scoped.initial = state.initial elif isinstance(state, State) and not isinstance(state, NestedState): raise ValueError( "A passed state object must derive from NestedState! " "A default State object is not sufficient") else: raise ValueError("Cannot add state of type %s. " % (type(state).__name__, ))
def _traverse(self, states, on_enter=None, on_exit=None, ignore_invalid_triggers=None, parent=None, remap=None): """ Parses passed value to build a nested state structure recursively. Args: states (list, str, 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 (str or list): callbacks to trigger when the state is entered. Only valid if first argument is string. on_exit (str 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. parent (NestedState or str): parent state for nested states. remap (dict): reassigns transitions named `key from nested machines to parent state `value`. Returns: list of new `NestedState` objects """ states = listify(states) new_states = [] ignore = ignore_invalid_triggers remap = {} if remap is None else remap parent = self.get_state(parent) if isinstance(parent, (string_types, Enum)) else parent 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, Enum)): 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 # shallow copy the dictionary to alter/add some parameters state = copy(state) if 'ignore_invalid_triggers' not in state: state['ignore_invalid_triggers'] = ignore if 'parent' not in state: state['parent'] = parent try: state_children = state.pop( 'children') # throws KeyError when no children set state_remap = state.pop('remap', None) state_parent = self._create_state(**state) nested = self._traverse(state_children, parent=state_parent, remap=state_remap) tmp_states.append(state_parent) tmp_states.extend(nested) except KeyError: tmp_states.insert(0, self._create_state(**state)) elif isinstance(state, HierarchicalMachine): # set initial state of parent if it is None if parent.initial is None: parent.initial = state.initial # (deep) copy only states not mentioned in remap copied_states = [ s for s in deepcopy(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 inner in inner_states: inner.parent = parent tmp_states.extend(copied_states) for trigger, event in state.events.items(): if trigger.startswith('to_'): path = trigger[3:].split(self.state_cls.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(self.state_cls.separator) path = ['to_' + ppath[0]] + ppath[1:] + path trigger = '.'.join(path) # (deep) copy transitions and # adjust all transition start and end points to new state names for transitions in deepcopy(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 + self.state_cls.separator + transition.dest\ if transition.dest not in remap else remap[transition.dest] conditions, unless = [], [] for cond in transition.conditions: # split a list in two lists based on the accessors (cond.target) truth value (unless, conditions)[cond.target].append(cond.func) self._buffered_transitions.append({ 'trigger': trigger, 'source': parent.name + self.state_cls.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) if state.children: tmp_states.extend( self._traverse( state.children, on_enter=on_enter, on_exit=on_exit, ignore_invalid_triggers=ignore_invalid_triggers, parent=state, remap=remap)) else: raise ValueError( "%s is not an instance or subclass of NestedState " "required by HierarchicalMachine." % state) new_states.extend(tmp_states) duplicate_check = [] for new in new_states: if new.name in duplicate_check: # collect state names for the following error message state_names = [s.name for s in new_states] raise ValueError( "State %s cannot be added since it is already in state list %s." % (new.name, state_names)) else: duplicate_check.append(new.name) return new_states
def test_listify(self): self.assertEqual(listify(4), [4]) self.assertEqual(listify(None), []) self.assertEqual(listify((4, 5)), (4, 5)) self.assertEqual(listify([1, 3]), [1, 3])
def _add_pre_hooks(self, pre_hooks): self._pre_hooks += listify(pre_hooks)