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)