transitions extensions factory adds locking to machine methods as well as model functions that trigger events additionally the user can inject herhis own context manager into the machine if required with nested statements now raise a deprecationwarning should be replaced with exitstacklike approaches reimplementation of nested in python 3 with exitstack as stack for ctx in contexts stack entercontextctx yield contexts class picklablelock def initself self lock lock def getstateself return def setstateself value return self init def enterself self lock enter def exitself exctype excval exctb self lock exitexctype excval exctb class identmanager an event type which uses the parent s machine context map when triggered def triggerself model args kwargs pylint disableprotectedaccess noinspection pyprotectedmember lockedmachine locked should not be called somewhere else that s why it should not be exposed to machine users 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 machinecontext 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 when we attempt to pickle a locked machine using ids wont suffice to unpickle the contexts since ids have changed we use a reference store with objects as dictionary keys to resolve the newly created references this should induce no restrictions compared to transitions 0 8 8 but enable the usage of unhashable objects in locked machine extends transitions core machine addmodel by modelcontext 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 models modelcontext list or object if passed assign the context list to the machines model specific context map extends transitions core machine removemodel by removing model specific context maps from the machine when the model itself is removed models listifymodel for mod in models del self modelcontextmapidmod return superlockedmachine self removemodelmodels def getattributeself item getattr superlockedmachine self getattribute tmp getattritem if not item startswith and inspect ismethodtmp return partialgetattr lockedmethod tmp return tmp def getattrself item try return superlockedmachine self getattributeitem except attributeerror return superlockedmachine self getattritem determine if the returned method is a partial and make sure the returned partial has not been machine getattr https github comtyarkonitransitionsissues214 def addmodeltostateself state model superlockedmachine self addmodeltostatestate model pylint disableprotectedaccess for prefix in self statecls dynamicmethods callback 01 formatprefix self getqualifiedstatenamestate func getattrmodel callback none if isinstancefunc partial and func func state addcallback state addcallbackprefix3 callback this needs to be overridden by the hsm variant to resolve names correctly def getqualifiedstatenameself state return state name def lockedmethodself func args kwargs if self ident current getident with nestedself machinecontext return funcargs kwargs else return funcargs kwargs python 2 pragma no cover with nested statements now raise a deprecationwarning should be replaced with exitstack like approaches pragma no cover reimplementation of nested in python 3 a wrapper for threading lock which discards its state during pickling and is reinitialized unlocked when unpickled manages the identity of threads to detect whether the current thread already has a lock an event type which uses the parent s machine context map when triggered 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 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 when we attempt to pickle a locked machine using ids wont suffice to unpickle the contexts since ids have changed we use a reference store with objects as dictionary keys to resolve the newly created references this should induce no restrictions compared to transitions 0 8 8 but enable the usage of unhashable objects in locked machine 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 extends transitions core machine remove_model by removing model specific context maps from the machine when the model itself is removed determine if the returned method is a partial and make sure the returned partial has not been machine __getattr__ https github com tyarkoni transitions issues 214 pylint disable protected access this needs to be overridden by the hsm variant to resolve names correctly
from collections import defaultdict
from functools import partial
from threading import Lock
import inspect
import warnings
import logging

from transitions.core import Machine, Event, listify

_LOGGER = logging.getLogger(__name__)
_LOGGER.addHandler(logging.NullHandler())


try:
    from contextlib import nested  
    from thread import get_ident  
    
    warnings.simplefilter('ignore', DeprecationWarning)  

except ImportError:
    from contextlib import ExitStack, contextmanager
    from threading import get_ident

    @contextmanager
    def nested(*contexts):
        
        with ExitStack() as stack:
            for ctx in contexts:
                stack.enter_context(ctx)
            yield contexts


class PicklableLock:
    

    def __init__(self):
        self.lock = Lock()

    def __getstate__(self):
        return ''

    def __setstate__(self, value):
        return self.__init__()

    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 IdentManager:
    

    def __init__(self):
        self.current = 0

    def __enter__(self):
        self.current = get_ident()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.current = 0


class LockedEvent(Event):
    

    def trigger(self, model, *args, **kwargs):
        
        
        
        
        
        if self.machine._ident.current != get_ident():
            with nested(*self.machine.model_context_map[id(model)]):
                return super(LockedEvent, self).trigger(model, *args, **kwargs)
        else:
            return super(LockedEvent, self).trigger(model, *args, **kwargs)


class LockedMachine(Machine):
    

    event_cls = LockedEvent

    def __init__(self, model=Machine.self_literal, 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, prepare_event=None, finalize_event=None, model_attribute='state', on_exception=None,
                 machine_context=None, **kwargs):

        self._ident = IdentManager()
        self.machine_context = listify(machine_context) or [PicklableLock()]
        self.machine_context.append(self._ident)
        self.model_context_map = defaultdict(list)

        super(LockedMachine, self).__init__(
            model=model, states=states, initial=initial, transitions=transitions,
            send_event=send_event, auto_transitions=auto_transitions,
            ordered_transitions=ordered_transitions, ignore_invalid_triggers=ignore_invalid_triggers,
            before_state_change=before_state_change, after_state_change=after_state_change, name=name,
            queued=queued, prepare_event=prepare_event, finalize_event=finalize_event,
            model_attribute=model_attribute, on_exception=on_exception, **kwargs
        )

    
    
    
    
    def __getstate__(self):
        state = {k: v for k, v in self.__dict__.items()}
        del state['model_context_map']
        state['_model_context_map_store'] = {mod: self.model_context_map[id(mod)] for mod in self.models}
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        self.model_context_map = defaultdict(list)
        for model in self.models:
            self.model_context_map[id(model)] = self._model_context_map_store[model]
        del self._model_context_map_store

    def add_model(self, model, initial=None, model_context=None):
        
        models = listify(model)
        model_context = listify(model_context) if model_context is not None else []
        super(LockedMachine, self).add_model(models, initial)

        for mod in models:
            mod = self if mod is self.self_literal else mod
            self.model_context_map[id(mod)].extend(self.machine_context)
            self.model_context_map[id(mod)].extend(model_context)

    def remove_model(self, model):
        
        models = listify(model)

        for mod in models:
            del self.model_context_map[id(mod)]

        return super(LockedMachine, self).remove_model(models)

    def __getattribute__(self, item):
        get_attr = super(LockedMachine, self).__getattribute__
        tmp = get_attr(item)
        if not item.startswith('_') and inspect.ismethod(tmp):
            return partial(get_attr('_locked_method'), tmp)
        return tmp

    def __getattr__(self, item):
        try:
            return super(LockedMachine, self).__getattribute__(item)
        except AttributeError:
            return super(LockedMachine, self).__getattr__(item)

    
    
    
    def _add_model_to_state(self, state, model):
        super(LockedMachine, self)._add_model_to_state(state, model)  
        for prefix in self.state_cls.dynamic_methods:
            callback = "{0}_{1}".format(prefix, self._get_qualified_state_name(state))
            func = getattr(model, callback, None)
            if isinstance(func, partial) and func.func != state.add_callback:
                state.add_callback(prefix[3:], callback)

    
    def _get_qualified_state_name(self, state):
        return state.name

    def _locked_method(self, func, *args, **kwargs):
        if self._ident.current != get_ident():
            with nested(*self.machine_context):
                return func(*args, **kwargs)
        else:
            return func(*args, **kwargs)