Source

callchain / callchain / mixin.py

Full commit
# -*- coding: utf-8 -*-
'''root chain mixins'''

from itertools import count, chain
from functools import partial

from twoq.support import isstring
from appspace.builders import Appspace
from stuf.utils import OrderedDict, lazy

from callchain.chainlet.keys import NoService
from callchain.chainlet.registry import EventRegistry
from callchain.base import ResetLocalMixin, ConfigMixin
from callchain.support import Empty, Queue, PriorityQueue, total_ordering

#    def __init__(self, manager, key):
#        '''
#        init
#
#        @param manager: main spine
#        @param key: settings key
#        '''
#        super(Spinelet, self).__init__(manager)
#        # add external application appspace
#        self.M = manager.M
#        # add internal application appspace
#        self._M = manager._M
#        # add internal application settings
#        self._G = manager._G
#        # appspace name
#        self.appspace = self._G.appspace
#        # namespace name
#        self.namespace = self._G[key]
#
#    def _configure(self, label):
#        '''
#        configure appspace class
#
#        @param label: application label
#        '''
#        app = self._M.get(label, self.appspace)
#        # add external application appspace
#        app.M = self.M
#        # add external application settings
#        app.G = self.M.settings.final
#        return app
#
#    @property
#    def Manager(self):
#        '''configured manager class'''
#        app = self._M.get(self.appspace).get(self.namespace.manager)
#        # add external application appspace
#        app.M = self.M
#        return app
#
#    def __init__(self, manager):
#        '''
#        init
#
#        @param manager: main spine
#        '''
#        super(Descriptor, self).__init__(manager)
#        # add internal application appspace
#        self._M = manager._M
#        # add internal application settings
#        self._G = manager._G
#        # appspace name
#        self.appspace = self._G.appspace
#        # namespace name
#        self.namespace = self._G.base
#
#    def _get(self, label):
#        return self._M.get(label, self.appspace)


@total_ordering
class _Partial(object):

    '''partial wrapper'''

    __slots__ = ('_call', 'count', '__call__')

    counter = count()

    def __init__(self, call, *args, **settings):
        self.__call__ = partial(call, *args, **settings)
        priority = settings.pop('priority', None)
        self.count = next(self.counter) if priority is None else priority

    def __lt__(self, other):
        return self.count < other


class BranchMixin(ResetLocalMixin):

    ''''branch mixin'''

    def _setup(self, root):
        '''
        configure branch

        @param root: root chain
        '''
        super(BranchMixin, self)._setup(root)
        root.join(self)


class EventBranchMixin(BranchMixin):

    '''event branch mixin'''

    @lazy
    def E(self):
        '''local event registry'''
        return EventRegistry('events')

    def _eventq(self, event):
        '''
        linked chain bound to `event`

        @param event: event label
        '''
        # fetch linked chain bound to event
        key = self.root.event(event)
        queue = self.E.get(key)
        if queue is None:
            # create linked chain if nonexistent
            queue = self._linkedchain
            self.E.on(key, queue)
        return queue

    def _event(self, event):
        '''
        calls bound to `event`

        @param event: event label
        '''
        key = self.root.event(event)
        return chain(self.E.events(key), self.root.E.events(key))


class CallMixin(ConfigMixin):

    '''chain mixin'''

    def __enter__(self):
        '''enter execution context'''
        return self

    def __exit__(self, e, t, b):
        '''exit execution context'''
        # invoke call chain
        self.commit()

    def _load(self, label):
        '''
        silent internal switch to...

        @param label: label of appspaced thing
        '''
        try:
            # look up internal appspaced linked call chain...
            _M = self._M
            key = _M.service(label)
            return getattr(_M.get(key, key)(self), label)
        except NoService:
            # ...or lookup some other appspaced thing
            return super(CallMixin, self)._load(label)

    @lazy
    def space(self):
        '''external appspace interface'''
        return Appspace(self.M) if self.M is not None else None

    def _setup(self, root):
        '''call chain setup'''
        # call chain queue
        self._chain = self._queue()

    def chain(self, call, key=False, *args, **kw):
        '''
        add `call` or appspaced `call` to call chain, partializing it with any
        passed arguments

        @param call: call or appspaced call label
        @param key: appspace key (default: False)
        '''
        self._chain.put(
            _Partial(call, *(key,) + args, **kw) if not isstring(call)
            else _Partial(self.M.get(call, key), *args, **kw)
        )
        return self

    def commit(self):
        '''consume call chain'''
        with self.ctx3():
            return self._xtend(
                c() for c in self.iterexcept(self._chain.get_nowait, Empty)
            )

    def switch(self, label, key=False):
        '''
        overt switch to linked chain configured in external appspace

        @param label: linked chain label
        @param key: linked chain chain key (default: False)
        '''
        return self.M.get(label, key)(self)

    def tap(self, call, key=False):
        '''
        add call

        @param call: callable or appspace label
        @param key: link call chain key (default: False)
        '''
        return super(CallMixin, self).tap(
            self._M.get(call, key) if isstring(call) else call
        )

    def wrap(self, call, key=False):
        '''build current callable from factory'''
        return super(CallMixin, self).wrap(
            self._M.get(call, key) if isstring(call) else call
        )

    class Meta:
        pass


class PriorityMixin(CallMixin):

    '''chain mixin'''

    _queue = PriorityQueue

    def chain(self, call, key=False, *args, **kw):
        '''
        add `call` or appspaced `call` to call chain, partializing it with any
        passed arguments

        @param call: call or appspaced call label
        @param key: appspace key (default: False)
        '''
        self._chain.put(
            _Partial(call, *(key,) + args, **kw)
            if not isstring(call)
            else _Partial(self.M.get(call, key), *args, **kw)
        )
        return self


class ChainMixin(CallMixin):

    '''chain mixin'''

    _queue = Queue

    def chain(self, call, key=False, *args, **kw):
        '''
        add `call` or appspaced `call` to call chain, partializing it with any
        passed arguments

        @param call: call or appspaced call label
        @param key: appspace key (default: False)
        '''
        if not isstring(call):
            self._chain.put_nowait(self._partial(call, *(key,) + args, **kw))
        else:
            self._chain.put_nowait(
                self._partial(self.M.get(call, key), *args, **kw)
            )
        return self


class EventMixin(ChainMixin):

    '''event chain mixin'''

    @property
    def _linkedchain(self):
        '''new linked chain'''
        return self._M.get('chain', 'event')(self)

    def _events(self, *events):
        '''calls bound to `events`'''
        return chain(*tuple(self._imap(self._event, events)))

    def commit(self):
        '''run event chain'''
        fire = self.fire
        try:
            # 1. "before" event 2. "work" event
            fire('before', 'work')
            # everything else
            super(EventMixin, self).commit()
            # 3. "change" event 4. "any" event 5. "after" event
            fire('change', 'any', 'after')
        except:
            # 6. "problem" event
            fire('problem')
        finally:
            # 7. event that runs "anyway"
            return fire('anyway')

    def fire(self, *events):
        '''
        run calls bound to `events` **NOW**

        @param *events: event labels
        '''
        with self.ctx1(workq=self._WORKVAR):
            return self.exhaustcall(
                lambda x: x(), self._xtend(self._events(*events))._iterable,
            )

    def on(self, event, call, key=False, *args, **kw):
        '''
        bind call to `event`

        @param event: event label
        @param call: label for call or eventspaced thing
        @param key: key label (default: False)
        '''
        self._eventq(event).chain(call, key, *args, **kw)
        return self

    def off(self, event):
        '''
        clear calls bound to `event`

        @param event: event label
        '''
        self.E.unset(event)
        return self

    def trigger(self, *events):
        '''
        extend primary call chain with partials bound to `events`

        @param *events: event labels
        '''
        self._chain.extend(self._events(*events))
        return self

    def queues(self, *events):
        '''
        ordered mapping of processing queues for `events`

        @param *events: event labels
        '''
        return OrderedDict((e, self._eventq(e)) for e in events)