Re: Contextual / DecoratorTools.classy oddity

"Sergey Schetinin" <[email protected]>
Newsgroups gmane.comp.python.peak
Message-ID <[email protected]>
I ended up implementing my own system for services which suites my
needs very well. Maybe a future version of Contextual will support my
use cases, but so far I was just fighting with it, so rolling out my
own is justified. I'll describe what I came up with cause I'm quite
happy with how it turned out, and as I said, maybe Phillip will decide
to include some similar features in Contextual. I attach the
implementation which is about 150 lines long and includes some ad-hoc
test suite.

First of all I didn't want service to be lazily initialized, so one
has to call Service.activate() to make it available. Also, to take
example from Contextual documentation, if Counter was already active
in current state ExtendedCounter would fail to activate because if any
service is active it has to be available via all service classes it
overrides. Also, subclassing services overrides them and overriding is
transitive, so if C overrides B and B overrides A, then C implicitly
overrides A.

There are only a few state manipulations possible:
 * instantiate a new, empty one: State()
 * get current state object: State.get()
 * create a nested state: State.get().child()
 * and make state current (__enter__ / __exit__)

Nested states allow overriding of already activated services, so this
would work:

Counter.activate()
with State.get().child():
    ExtendedCounter.activate()
    assert ExtendedCounter.get() is Counter.get()


There a root state that is the default one for all threads, so any
services that are not thread-safe should only be activated in nested
states.

Optional lazy activation would be trivial to add, but I really don't
need it, the requirement to explicitly activate services turned out to
be a very good decision in my case. Also, this makes possible for
service constructors to take parameters (which can be passed to
activate()).



> The issue here is that B and C have their own unique metaclasses, which are
> not derived from one another.  Specifically, Service creates a unique
> metaclass for each derived class, and these independent metaclasses can't be
> mixed.  You need to do something like this if you want to inherit from more
> than one Service subclass:
>
>    class D(B, C):
>        class __metaclass__(type(B), type(C)):
>            pass
>
> I've personally never needed to multiple-inherit Services, so didn't notice.
>  It's a bit of a wart, I suppose, but I'm not sure whether I care, or if
> it's a good idea to do in the first place.  (That is, there might be other
> negative consequences of doing it.)
>
> Anyway, the limitation here is of peak.context, which as I've mentioned
> previously, isn't really ready for prime time.  "classy" on its own doesn't
> do this, it's the metaclass-per-Service-subclass that's the cause here.
>
>



-- 
Best Regards,
Sergey Schetinin

http://s3bk.com/ -- S3 Backup
http://word-to-html.com/ -- Word to HTML Converter

_______________________________________________
PEAK mailing list
[email protected]
http://www.eby-sarna.com/mailman/listinfo/peak
services.py (text/x-python, 5.9 KB)
from __future__ import with_statement
from UserDict import DictMixin
from peak.util.decorators import classy
from peak.util.addons import Registry
from threading import currentThread, RLock
from logging import getLogger


services_log = getLogger('services')

__all__ = ['State', 'Service', 'overrides']


class State(object, DictMixin):
    def __init__(self, parent=None):
        self.parent = parent
        self.data = {}
        if parent is None:
            self.lock = RLock()
        else:
            self.lock = parent.lock

    def keys(self):
        keys = set(self.data.keys())
        if self.parent is not None:
            keys.update(self.parent.keys())
        return keys

    def __getitem__(self, key):
        with self.lock:
            if key in self.data:
                return self.data[key]
            elif self.parent is not None:
                return self.parent[key]
            else:
                raise KeyError("Service unavailable: %s" % key)

    def __setitem__(self, key, value):
        with self.lock:
            self._check_key(key)
            self.data[key] = value

    def update(self, dct):
        with self.lock:
            for key in dct:
                self._check_key(key)
            super(State, self).update(dct)


    def _check_key(self, key):
        if key in self.data:
            raise KeyError("Service instance for %r is already set: %r" % (key, self.data[key]))



    def child(self):
        return State(self)

    def __enter__(self):
        services_log.debug("Entering state %s", self)
        state_stack().append(self)
        return self

    def __exit__(self, *exc_info):
        services_log.debug("Exiting state %s", self)
        assert State.get() is self
        del state_stack()[-1]

    @staticmethod
    def get():
        return state_stack()[-1]


root_state = State()

def state_stack():
    return currentThread().__dict__.setdefault(State, [root_state])




class ServiceOverrides(Registry):
    def created_for(self, service):
        self[service] = True


def overrides(*services):
    meta = ServiceOverrides.for_enclosing_class()
    for service in services:
        for overriden in ServiceOverrides(service):
            meta[overriden] = True



def redirect_attribute(cls, name, payload):
    setattr(type(cls), name, property(
        lambda s: getattr(s.get(), name),
        lambda s,v: setattr(s.get(), name, v),
        lambda s: delattr(s.get(), name),
    ))


class ServiceAvailableProperty(object):
    def __get__(self, instance, owner):
        return owner in State.get()


_service_methods = frozenset('__getitem__ __setitem__ __delitem__ __iter__'.split())

class Service(classy):
    def __class_init__(cls, name, bases, cdict, supr):
        supr()(cls, name, bases, cdict, supr)

        metabases = (type(cls),)
        #if len(bases) > 1:
        #    metabases += tuple(map(type, bases[1:]))
        #print cls, bases, metabases
        cls.__class__ = type(metabases[0])(cls.__name__ + 'Class', metabases,
            dict(__module__=cls.__module__))#, __for_class__=cls))

        for k, v in cdict.items():
            if isinstance(k, basestring):
                if isinstance(v, (classmethod, staticmethod, ServiceAvailableProperty)):
                    continue
                #if isinstance(v, property):
                #    if isinstance(v.fget, (classmethod, staticmethod)):
                #        continue
                if not k.startswith('_') or k in _service_methods:
                    redirect_attribute(cls, k, v)


        if bases == (classy,):
            return

        overrides = ServiceOverrides(cls)
        for base in bases:
            if issubclass(base, Service) and base is not Service:
                overrides[base] = True
                overrides.update(ServiceOverrides(base))



    @classmethod
    def get(cls):
        return State.get()[cls]

    available = ServiceAvailableProperty()

    @classmethod
    def activate(cls, *args, **kw):
        services_log.info("Activating service %s", cls)
        service = cls(*args, **kw)
        upd = dict.fromkeys(ServiceOverrides(cls).keys(), service)
        State.get().update(upd)




if __name__ == '__main__':
    class Base(Service):
        pass

    class Ext(Service):
        overrides(Base)
        a = 1

    class ExtPlus(Ext):
        pass

    from pprint import *
    from nose.tools import *
    assert set(ServiceOverrides(ExtPlus).keys()) == set([Base, Ext, ExtPlus])

    assert_raises(KeyError, Base.get)
    assert not Base.available
    Base.activate()
    assert Base.available
    assert not Ext.available
    base = Base.get()
    assert_raises(KeyError, Ext.get)
    with State.get().child():
        Ext.activate()
        assert Ext.get() is Base.get() is not base

    assert Base.get() is base
    assert_raises(KeyError, Ext.activate)
    with State():
        Ext.activate()
        assert isinstance(Base.get(), Ext)
    with State.get().child() as state:
        Ext.activate()
        #pprint(state.factories)
        assert isinstance(Base.get(), Ext)
    with State.get().child():
        ExtPlus.activate()
        assert Ext.get() is ExtPlus.get()
        assert Base.get() is ExtPlus.get()
        assert_raises(KeyError, Ext.activate)
        assert ExtPlus.a == 1
        assert Ext.a == 1
        Ext.a = 2
        assert ExtPlus.a == 2
        assert_raises(AttributeError, lambda: Base.a)
    assert_raises(KeyError, ExtPlus.get)

    with State():
        Ext.activate()
        assert isinstance(Base.get(), Ext)
        assert Base.available

    with State.get().child():
        assert Base.get() is base
        ExtPlus.activate()
        Ext.a = 'foo'
        with State.get().child():
            assert ExtPlus.get() is Ext.get() is Base.get()
            assert ExtPlus.a == 'foo'
        assert_raises(KeyError, ExtPlus.activate)
        assert_raises(KeyError, Ext.activate)
        assert_raises(KeyError, Base.activate)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.