Secure your code as it's written. Use Snyk Code to scan source code in minutes - no build needed - and fix issues immediately.
@Machine.initial.setter
def initial(self, value):
if isinstance(value, NestedState):
if value.name not in self.states:
self.add_state(value)
else:
assert self._has_state(value)
state = value
else:
state_name = value.name if isinstance(value, Enum) else value
if state_name not in self.states:
self.add_state(state_name)
state = self.get_state(state_name)
if state.initial:
self.initial = state.initial
else:
self._initial = state.name
""" An event type which uses the parent's machine context map when triggered. """
def trigger(self, model, *args, **kwargs):
""" Extends transitions.core.Event.trigger by using locks/machine contexts. """
# pylint: disable=protected-access
# noinspection PyProtectedMember
# LockedMachine._locked should not be called somewhere else. That's why it should not be exposed
# to Machine users.
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):
""" Machine class which manages contexts. In it's default version the machine uses a `threading.Lock`
context to lock access to its methods and event triggers bound to model objects.
Attributes:
machine_context (dict): A dict of context managers to be entered whenever a machine method is
called or an event is triggered. Contexts are managed for each model individually.
"""
event_cls = LockedEvent
def __init__(self, *args, **kwargs):
self._locked = 0
try:
self.machine_context = listify(kwargs.pop('machine_context'))
except KeyError:
self.machine_context = [PicklableLock()]
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.
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.
"""
# calling super in case `Machine` is used as a mix in
# all keyword arguments should be consumed by now if this is not the case
try:
super(Machine, self).__init__(**kwargs)
except TypeError as err:
raise ValueError('Passing arguments {0} caused an inheritance error: {1}'.format(kwargs.keys(), err))
# 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
pass
class LockedHierarchicalGraphMachine(GraphMachine, LockedMachine, HierarchicalMachine):
"""
A threadsafe hiearchical machine with graph support.
"""
transition_cls = NestedGraphTransition
event_cls = LockedNestedEvent
graph_cls = NestedGraph
# 3d tuple (graph, nested, locked)
_CLASS_MAP = {
(False, False, False): Machine,
(False, False, True): LockedMachine,
(False, True, False): HierarchicalMachine,
(False, True, True): LockedHierarchicalMachine,
(True, False, False): GraphMachine,
(True, False, True): LockedGraphMachine,
(True, True, False): HierarchicalGraphMachine,
(True, True, True): LockedHierarchicalGraphMachine
}
try:
for trans in self.transitions[event_data.state.name]:
event_data.transition = trans
if await trans.execute(event_data):
event_data.result = True
break
except Exception as err:
event_data.error = err
raise
finally:
await self.machine.callbacks(self.machine.finalize_event, event_data)
_LOGGER.debug("%sExecuted machine finalize callbacks", self.machine.name)
return event_data.result
class AsyncMachine(Machine):
""" Machine manages states, transitions and models. In case it is initialized without a specific model
(or specifically no model), it will also act as a model itself. Machine takes also care of decorating
models with conveniences functions related to added transitions and states during runtime.
Attributes:
states (OrderedDict): Collection of all registered states.
events (dict): Collection of transitions ordered by trigger/event.
models (list): List of models attached to the machine.
initial (str): Name of the initial state for new models.
prepare_event (list): Callbacks executed when an event is triggered.
before_state_change (list): Callbacks executed after condition checks but before transition is conducted.
Callbacks will be executed BEFORE the custom callbacks assigned to the transition.
after_state_change (list): Callbacks executed after the transition has been conducted.
Callbacks will be executed AFTER the custom callbacks assigned to the transition.
finalize_event (list): Callbacks will be executed after all transitions callbacks have been executed.
Callbacks mentioned here will also be called if a transition or condition check raised an error.
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.name, self.name,
model.state)
if self.machine.get_state(model.state).ignore_invalid_triggers:
_LOGGER.warning(msg)
else:
raise MachineError(msg)
event_data = EventData(state, self, self.machine,
model, args=args, kwargs=kwargs)
return self._process(event_data)
class HierarchicalMachine(Machine):
""" Extends transitions.core.Machine by capabilities to handle nested states.
A hierarchical machine REQUIRES NestedStates (or any subclass of it) to operate.
"""
state_cls = NestedState
transition_cls = NestedTransition
event_cls = NestedEvent
def __init__(self, *args, **kwargs):
self._buffered_transitions = []
_super(HierarchicalMachine, self).__init__(*args, **kwargs)
@Machine.initial.setter
def initial(self, value):
if isinstance(value, NestedState):
if value.name not in self.states:
from six import string_types, iteritems
from functools import partial
import itertools
import importlib
from collections import defaultdict
from ..core import Machine
import numbers
class MarkupMachine(Machine):
# Special attributes such as NestedState._name/_parent or Transition._condition are handled differently
state_attributes = ['on_exit', 'on_enter', 'ignore_invalid_triggers', 'timeout', 'on_timeout', 'tags']
transition_attributes = ['source', 'dest', 'prepare', 'before', 'after']
def __init__(self, *args, **kwargs):
self._markup = kwargs.pop('markup', {})
self._auto_transitions_markup = kwargs.pop('auto_transitions_markup', False)
self.skip_references = True
if self._markup:
models_markup = self._markup.pop('models', [])
super(MarkupMachine, self).__init__(None, **self._markup)
for m in models_markup:
self._add_markup_model(m)
else:
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)
""" An event type which uses the parent's machine context map when triggered. """
def trigger(self, model, *args, **kwargs):
""" Extends transitions.core.Event.trigger by using locks/machine contexts. """
# pylint: disable=protected-access
# noinspection PyProtectedMember
# LockedMachine._locked should not be called somewhere else. That's why it should not be exposed
# to Machine users.
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):
""" Machine class which manages contexts. In it's default version the machine uses a `threading.Lock`
context to lock access to its methods and event triggers bound to model objects.
Attributes:
machine_context (dict): A dict of context managers to be entered whenever a machine method is
called or an event is triggered. Contexts are managed for each model individually.
"""
event_cls = LockedEvent
def __init__(self, *args, **kwargs):
self._locked = 0
try:
self.machine_context = listify(kwargs.pop('machine_context'))
except KeyError:
self.machine_context = [PicklableLock()]