| Index: third_party/logilab/common/registry.py
|
| ===================================================================
|
| --- third_party/logilab/common/registry.py (revision 0)
|
| +++ third_party/logilab/common/registry.py (working copy)
|
| @@ -0,0 +1,1119 @@
|
| +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
|
| +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
| +#
|
| +# This file is part of Logilab-common.
|
| +#
|
| +# Logilab-common is free software: you can redistribute it and/or modify it
|
| +# under the terms of the GNU Lesser General Public License as published by the
|
| +# Free Software Foundation, either version 2.1 of the License, or (at your
|
| +# option) any later version.
|
| +#
|
| +# Logilab-common is distributed in the hope that it will be useful, but WITHOUT
|
| +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
| +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
| +# details.
|
| +#
|
| +# You should have received a copy of the GNU Lesser General Public License along
|
| +# with Logilab-common. If not, see <http://www.gnu.org/licenses/>.
|
| +"""This module provides bases for predicates dispatching (the pattern in use
|
| +here is similar to what's refered as multi-dispatch or predicate-dispatch in the
|
| +literature, though a bit different since the idea is to select across different
|
| +implementation 'e.g. classes), not to dispatch a message to a function or
|
| +method. It contains the following classes:
|
| +
|
| +* :class:`RegistryStore`, the top level object which loads implementation
|
| + objects and stores them into registries. You'll usually use it to access
|
| + registries and their contained objects;
|
| +
|
| +* :class:`Registry`, the base class which contains objects semantically grouped
|
| + (for instance, sharing a same API, hence the 'implementation' name). You'll
|
| + use it to select the proper implementation according to a context. Notice you
|
| + may use registries on their own without using the store.
|
| +
|
| +.. Note::
|
| +
|
| + implementation objects are usually designed to be accessed through the
|
| + registry and not by direct instantiation, besides to use it as base classe.
|
| +
|
| +The selection procedure is delegated to a selector, which is responsible for
|
| +scoring the object according to some context. At the end of the selection, if an
|
| +implementation has been found, an instance of this class is returned. A selector
|
| +is built from one or more predicates combined together using AND, OR, NOT
|
| +operators (actually `&`, `|` and `~`). You'll thus find some base classes to
|
| +build predicates:
|
| +
|
| +* :class:`Predicate`, the abstract base predicate class
|
| +
|
| +* :class:`AndPredicate`, :class:`OrPredicate`, :class:`NotPredicate`, which you
|
| + shouldn't have to use directly. You'll use `&`, `|` and '~' operators between
|
| + predicates directly
|
| +
|
| +* :func:`objectify_predicate`
|
| +
|
| +You'll eventually find one concrete predicate: :class:`yes`
|
| +
|
| +.. autoclass:: RegistryStore
|
| +.. autoclass:: Registry
|
| +
|
| +Predicates
|
| +----------
|
| +.. autoclass:: Predicate
|
| +.. autofunc:: objectify_predicate
|
| +.. autoclass:: yes
|
| +
|
| +Debugging
|
| +---------
|
| +.. autoclass:: traced_selection
|
| +
|
| +Exceptions
|
| +----------
|
| +.. autoclass:: RegistryException
|
| +.. autoclass:: RegistryNotFound
|
| +.. autoclass:: ObjectNotFound
|
| +.. autoclass:: NoSelectableObject
|
| +"""
|
| +
|
| +from __future__ import print_function
|
| +
|
| +__docformat__ = "restructuredtext en"
|
| +
|
| +import sys
|
| +import types
|
| +import weakref
|
| +import traceback as tb
|
| +from os import listdir, stat
|
| +from os.path import join, isdir, exists
|
| +from logging import getLogger
|
| +from warnings import warn
|
| +
|
| +from six import string_types, add_metaclass
|
| +
|
| +from logilab.common.modutils import modpath_from_file
|
| +from logilab.common.logging_ext import set_log_methods
|
| +from logilab.common.decorators import classproperty
|
| +
|
| +
|
| +class RegistryException(Exception):
|
| + """Base class for registry exception."""
|
| +
|
| +class RegistryNotFound(RegistryException):
|
| + """Raised when an unknown registry is requested.
|
| +
|
| + This is usually a programming/typo error.
|
| + """
|
| +
|
| +class ObjectNotFound(RegistryException):
|
| + """Raised when an unregistered object is requested.
|
| +
|
| + This may be a programming/typo or a misconfiguration error.
|
| + """
|
| +
|
| +class NoSelectableObject(RegistryException):
|
| + """Raised when no object is selectable for a given context."""
|
| + def __init__(self, args, kwargs, objects):
|
| + self.args = args
|
| + self.kwargs = kwargs
|
| + self.objects = objects
|
| +
|
| + def __str__(self):
|
| + return ('args: %s, kwargs: %s\ncandidates: %s'
|
| + % (self.args, self.kwargs.keys(), self.objects))
|
| +
|
| +
|
| +def _modname_from_path(path, extrapath=None):
|
| + modpath = modpath_from_file(path, extrapath)
|
| + # omit '__init__' from package's name to avoid loading that module
|
| + # once for each name when it is imported by some other object
|
| + # module. This supposes import in modules are done as::
|
| + #
|
| + # from package import something
|
| + #
|
| + # not::
|
| + #
|
| + # from package.__init__ import something
|
| + #
|
| + # which seems quite correct.
|
| + if modpath[-1] == '__init__':
|
| + modpath.pop()
|
| + return '.'.join(modpath)
|
| +
|
| +
|
| +def _toload_info(path, extrapath, _toload=None):
|
| + """Return a dictionary of <modname>: <modpath> and an ordered list of
|
| + (file, module name) to load
|
| + """
|
| + if _toload is None:
|
| + assert isinstance(path, list)
|
| + _toload = {}, []
|
| + for fileordir in path:
|
| + if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
|
| + subfiles = [join(fileordir, fname) for fname in listdir(fileordir)]
|
| + _toload_info(subfiles, extrapath, _toload)
|
| + elif fileordir[-3:] == '.py':
|
| + modname = _modname_from_path(fileordir, extrapath)
|
| + _toload[0][modname] = fileordir
|
| + _toload[1].append((fileordir, modname))
|
| + return _toload
|
| +
|
| +
|
| +class RegistrableObject(object):
|
| + """This is the base class for registrable objects which are selected
|
| + according to a context.
|
| +
|
| + :attr:`__registry__`
|
| + name of the registry for this object (string like 'views',
|
| + 'templates'...). You may want to define `__registries__` directly if your
|
| + object should be registered in several registries.
|
| +
|
| + :attr:`__regid__`
|
| + object's identifier in the registry (string like 'main',
|
| + 'primary', 'folder_box')
|
| +
|
| + :attr:`__select__`
|
| + class'selector
|
| +
|
| + Moreover, the `__abstract__` attribute may be set to True to indicate that a
|
| + class is abstract and should not be registered.
|
| +
|
| + You don't have to inherit from this class to put it in a registry (having
|
| + `__regid__` and `__select__` is enough), though this is needed for classes
|
| + that should be automatically registered.
|
| + """
|
| +
|
| + __registry__ = None
|
| + __regid__ = None
|
| + __select__ = None
|
| + __abstract__ = True # see doc snipppets below (in Registry class)
|
| +
|
| + @classproperty
|
| + def __registries__(cls):
|
| + if cls.__registry__ is None:
|
| + return ()
|
| + return (cls.__registry__,)
|
| +
|
| +
|
| +class RegistrableInstance(RegistrableObject):
|
| + """Inherit this class if you want instances of the classes to be
|
| + automatically registered.
|
| + """
|
| +
|
| + def __new__(cls, *args, **kwargs):
|
| + """Add a __module__ attribute telling the module where the instance was
|
| + created, for automatic registration.
|
| + """
|
| + obj = super(RegistrableInstance, cls).__new__(cls)
|
| + # XXX subclass must no override __new__
|
| + filepath = tb.extract_stack(limit=2)[0][0]
|
| + obj.__module__ = _modname_from_path(filepath)
|
| + return obj
|
| +
|
| +
|
| +class Registry(dict):
|
| + """The registry store a set of implementations associated to identifier:
|
| +
|
| + * to each identifier are associated a list of implementations
|
| +
|
| + * to select an implementation of a given identifier, you should use one of the
|
| + :meth:`select` or :meth:`select_or_none` method
|
| +
|
| + * to select a list of implementations for a context, you should use the
|
| + :meth:`possible_objects` method
|
| +
|
| + * dictionary like access to an identifier will return the bare list of
|
| + implementations for this identifier.
|
| +
|
| + To be usable in a registry, the only requirement is to have a `__select__`
|
| + attribute.
|
| +
|
| + At the end of the registration process, the :meth:`__registered__`
|
| + method is called on each registered object which have them, given the
|
| + registry in which it's registered as argument.
|
| +
|
| + Registration methods:
|
| +
|
| + .. automethod: register
|
| + .. automethod: unregister
|
| +
|
| + Selection methods:
|
| +
|
| + .. automethod: select
|
| + .. automethod: select_or_none
|
| + .. automethod: possible_objects
|
| + .. automethod: object_by_id
|
| + """
|
| + def __init__(self, debugmode):
|
| + super(Registry, self).__init__()
|
| + self.debugmode = debugmode
|
| +
|
| + def __getitem__(self, name):
|
| + """return the registry (list of implementation objects) associated to
|
| + this name
|
| + """
|
| + try:
|
| + return super(Registry, self).__getitem__(name)
|
| + except KeyError:
|
| + exc = ObjectNotFound(name)
|
| + exc.__traceback__ = sys.exc_info()[-1]
|
| + raise exc
|
| +
|
| + @classmethod
|
| + def objid(cls, obj):
|
| + """returns a unique identifier for an object stored in the registry"""
|
| + return '%s.%s' % (obj.__module__, cls.objname(obj))
|
| +
|
| + @classmethod
|
| + def objname(cls, obj):
|
| + """returns a readable name for an object stored in the registry"""
|
| + return getattr(obj, '__name__', id(obj))
|
| +
|
| + def initialization_completed(self):
|
| + """call method __registered__() on registered objects when the callback
|
| + is defined"""
|
| + for objects in self.values():
|
| + for objectcls in objects:
|
| + registered = getattr(objectcls, '__registered__', None)
|
| + if registered:
|
| + registered(self)
|
| + if self.debugmode:
|
| + wrap_predicates(_lltrace)
|
| +
|
| + def register(self, obj, oid=None, clear=False):
|
| + """base method to add an object in the registry"""
|
| + assert not '__abstract__' in obj.__dict__, obj
|
| + assert obj.__select__, obj
|
| + oid = oid or obj.__regid__
|
| + assert oid, ('no explicit name supplied to register object %s, '
|
| + 'which has no __regid__ set' % obj)
|
| + if clear:
|
| + objects = self[oid] = []
|
| + else:
|
| + objects = self.setdefault(oid, [])
|
| + assert not obj in objects, 'object %s is already registered' % obj
|
| + objects.append(obj)
|
| +
|
| + def register_and_replace(self, obj, replaced):
|
| + """remove <replaced> and register <obj>"""
|
| + # XXXFIXME this is a duplication of unregister()
|
| + # remove register_and_replace in favor of unregister + register
|
| + # or simplify by calling unregister then register here
|
| + if not isinstance(replaced, string_types):
|
| + replaced = self.objid(replaced)
|
| + # prevent from misspelling
|
| + assert obj is not replaced, 'replacing an object by itself: %s' % obj
|
| + registered_objs = self.get(obj.__regid__, ())
|
| + for index, registered in enumerate(registered_objs):
|
| + if self.objid(registered) == replaced:
|
| + del registered_objs[index]
|
| + break
|
| + else:
|
| + self.warning('trying to replace %s that is not registered with %s',
|
| + replaced, obj)
|
| + self.register(obj)
|
| +
|
| + def unregister(self, obj):
|
| + """remove object <obj> from this registry"""
|
| + objid = self.objid(obj)
|
| + oid = obj.__regid__
|
| + for registered in self.get(oid, ()):
|
| + # use self.objid() to compare objects because vreg will probably
|
| + # have its own version of the object, loaded through execfile
|
| + if self.objid(registered) == objid:
|
| + self[oid].remove(registered)
|
| + break
|
| + else:
|
| + self.warning('can\'t remove %s, no id %s in the registry',
|
| + objid, oid)
|
| +
|
| + def all_objects(self):
|
| + """return a list containing all objects in this registry.
|
| + """
|
| + result = []
|
| + for objs in self.values():
|
| + result += objs
|
| + return result
|
| +
|
| + # dynamic selection methods ################################################
|
| +
|
| + def object_by_id(self, oid, *args, **kwargs):
|
| + """return object with the `oid` identifier. Only one object is expected
|
| + to be found.
|
| +
|
| + raise :exc:`ObjectNotFound` if there are no object with id `oid` in this
|
| + registry
|
| +
|
| + raise :exc:`AssertionError` if there is more than one object there
|
| + """
|
| + objects = self[oid]
|
| + assert len(objects) == 1, objects
|
| + return objects[0](*args, **kwargs)
|
| +
|
| + def select(self, __oid, *args, **kwargs):
|
| + """return the most specific object among those with the given oid
|
| + according to the given context.
|
| +
|
| + raise :exc:`ObjectNotFound` if there are no object with id `oid` in this
|
| + registry
|
| +
|
| + raise :exc:`NoSelectableObject` if no object can be selected
|
| + """
|
| + obj = self._select_best(self[__oid], *args, **kwargs)
|
| + if obj is None:
|
| + raise NoSelectableObject(args, kwargs, self[__oid] )
|
| + return obj
|
| +
|
| + def select_or_none(self, __oid, *args, **kwargs):
|
| + """return the most specific object among those with the given oid
|
| + according to the given context, or None if no object applies.
|
| + """
|
| + try:
|
| + return self._select_best(self[__oid], *args, **kwargs)
|
| + except ObjectNotFound:
|
| + return None
|
| +
|
| + def possible_objects(self, *args, **kwargs):
|
| + """return an iterator on possible objects in this registry for the given
|
| + context
|
| + """
|
| + for objects in self.values():
|
| + obj = self._select_best(objects, *args, **kwargs)
|
| + if obj is None:
|
| + continue
|
| + yield obj
|
| +
|
| + def _select_best(self, objects, *args, **kwargs):
|
| + """return an instance of the most specific object according
|
| + to parameters
|
| +
|
| + return None if not object apply (don't raise `NoSelectableObject` since
|
| + it's costly when searching objects using `possible_objects`
|
| + (e.g. searching for hooks).
|
| + """
|
| + score, winners = 0, None
|
| + for obj in objects:
|
| + objectscore = obj.__select__(obj, *args, **kwargs)
|
| + if objectscore > score:
|
| + score, winners = objectscore, [obj]
|
| + elif objectscore > 0 and objectscore == score:
|
| + winners.append(obj)
|
| + if winners is None:
|
| + return None
|
| + if len(winners) > 1:
|
| + # log in production environement / test, error while debugging
|
| + msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)'
|
| + if self.debugmode:
|
| + # raise bare exception in debug mode
|
| + raise Exception(msg % (winners, args, kwargs.keys()))
|
| + self.error(msg, winners, args, kwargs.keys())
|
| + # return the result of calling the object
|
| + return self.selected(winners[0], args, kwargs)
|
| +
|
| + def selected(self, winner, args, kwargs):
|
| + """override here if for instance you don't want "instanciation"
|
| + """
|
| + return winner(*args, **kwargs)
|
| +
|
| + # these are overridden by set_log_methods below
|
| + # only defining here to prevent pylint from complaining
|
| + info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
|
| +
|
| +
|
| +def obj_registries(cls, registryname=None):
|
| + """return a tuple of registry names (see __registries__)"""
|
| + if registryname:
|
| + return (registryname,)
|
| + return cls.__registries__
|
| +
|
| +
|
| +class RegistryStore(dict):
|
| + """This class is responsible for loading objects and storing them
|
| + in their registry which is created on the fly as needed.
|
| +
|
| + It handles dynamic registration of objects and provides a
|
| + convenient api to access them. To be recognized as an object that
|
| + should be stored into one of the store's registry
|
| + (:class:`Registry`), an object must provide the following
|
| + attributes, used control how they interact with the registry:
|
| +
|
| + :attr:`__registries__`
|
| + list of registry names (string like 'views', 'templates'...) into which
|
| + the object should be registered
|
| +
|
| + :attr:`__regid__`
|
| + object identifier in the registry (string like 'main',
|
| + 'primary', 'folder_box')
|
| +
|
| + :attr:`__select__`
|
| + the object predicate selectors
|
| +
|
| + Moreover, the :attr:`__abstract__` attribute may be set to `True`
|
| + to indicate that an object is abstract and should not be registered
|
| + (such inherited attributes not considered).
|
| +
|
| + .. Note::
|
| +
|
| + When using the store to load objects dynamically, you *always* have
|
| + to use **super()** to get the methods and attributes of the
|
| + superclasses, and not use the class identifier. If not, you'll get into
|
| + trouble at reload time.
|
| +
|
| + For example, instead of writing::
|
| +
|
| + class Thing(Parent):
|
| + __regid__ = 'athing'
|
| + __select__ = yes()
|
| +
|
| + def f(self, arg1):
|
| + Parent.f(self, arg1)
|
| +
|
| + You must write::
|
| +
|
| + class Thing(Parent):
|
| + __regid__ = 'athing'
|
| + __select__ = yes()
|
| +
|
| + def f(self, arg1):
|
| + super(Thing, self).f(arg1)
|
| +
|
| + Controlling object registration
|
| + -------------------------------
|
| +
|
| + Dynamic loading is triggered by calling the
|
| + :meth:`register_objects` method, given a list of directories to
|
| + inspect for python modules.
|
| +
|
| + .. automethod: register_objects
|
| +
|
| + For each module, by default, all compatible objects are registered
|
| + automatically. However if some objects come as replacement of
|
| + other objects, or have to be included only if some condition is
|
| + met, you'll have to define a `registration_callback(vreg)`
|
| + function in the module and explicitly register **all objects** in
|
| + this module, using the api defined below.
|
| +
|
| +
|
| + .. automethod:: RegistryStore.register_all
|
| + .. automethod:: RegistryStore.register_and_replace
|
| + .. automethod:: RegistryStore.register
|
| + .. automethod:: RegistryStore.unregister
|
| +
|
| + .. Note::
|
| + Once the function `registration_callback(vreg)` is implemented in a
|
| + module, all the objects from this module have to be explicitly
|
| + registered as it disables the automatic object registration.
|
| +
|
| +
|
| + Examples:
|
| +
|
| + .. sourcecode:: python
|
| +
|
| + def registration_callback(store):
|
| + # register everything in the module except BabarClass
|
| + store.register_all(globals().values(), __name__, (BabarClass,))
|
| +
|
| + # conditionally register BabarClass
|
| + if 'babar_relation' in store.schema:
|
| + store.register(BabarClass)
|
| +
|
| + In this example, we register all application object classes defined in the module
|
| + except `BabarClass`. This class is then registered only if the 'babar_relation'
|
| + relation type is defined in the instance schema.
|
| +
|
| + .. sourcecode:: python
|
| +
|
| + def registration_callback(store):
|
| + store.register(Elephant)
|
| + # replace Babar by Celeste
|
| + store.register_and_replace(Celeste, Babar)
|
| +
|
| + In this example, we explicitly register classes one by one:
|
| +
|
| + * the `Elephant` class
|
| + * the `Celeste` to replace `Babar`
|
| +
|
| + If at some point we register a new appobject class in this module, it won't be
|
| + registered at all without modification to the `registration_callback`
|
| + implementation. The first example will register it though, thanks to the call
|
| + to the `register_all` method.
|
| +
|
| + Controlling registry instantiation
|
| + ----------------------------------
|
| +
|
| + The `REGISTRY_FACTORY` class dictionary allows to specify which class should
|
| + be instantiated for a given registry name. The class associated to `None`
|
| + key will be the class used when there is no specific class for a name.
|
| + """
|
| +
|
| + def __init__(self, debugmode=False):
|
| + super(RegistryStore, self).__init__()
|
| + self.debugmode = debugmode
|
| +
|
| + def reset(self):
|
| + """clear all registries managed by this store"""
|
| + # don't use self.clear, we want to keep existing subdictionaries
|
| + for subdict in self.values():
|
| + subdict.clear()
|
| + self._lastmodifs = {}
|
| +
|
| + def __getitem__(self, name):
|
| + """return the registry (dictionary of class objects) associated to
|
| + this name
|
| + """
|
| + try:
|
| + return super(RegistryStore, self).__getitem__(name)
|
| + except KeyError:
|
| + exc = RegistryNotFound(name)
|
| + exc.__traceback__ = sys.exc_info()[-1]
|
| + raise exc
|
| +
|
| + # methods for explicit (un)registration ###################################
|
| +
|
| + # default class, when no specific class set
|
| + REGISTRY_FACTORY = {None: Registry}
|
| +
|
| + def registry_class(self, regid):
|
| + """return existing registry named regid or use factory to create one and
|
| + return it"""
|
| + try:
|
| + return self.REGISTRY_FACTORY[regid]
|
| + except KeyError:
|
| + return self.REGISTRY_FACTORY[None]
|
| +
|
| + def setdefault(self, regid):
|
| + try:
|
| + return self[regid]
|
| + except RegistryNotFound:
|
| + self[regid] = self.registry_class(regid)(self.debugmode)
|
| + return self[regid]
|
| +
|
| + def register_all(self, objects, modname, butclasses=()):
|
| + """register registrable objects into `objects`.
|
| +
|
| + Registrable objects are properly configured subclasses of
|
| + :class:`RegistrableObject`. Objects which are not defined in the module
|
| + `modname` or which are in `butclasses` won't be registered.
|
| +
|
| + Typical usage is:
|
| +
|
| + .. sourcecode:: python
|
| +
|
| + store.register_all(globals().values(), __name__, (ClassIWantToRegisterExplicitly,))
|
| +
|
| + So you get partially automatic registration, keeping manual registration
|
| + for some object (to use
|
| + :meth:`~logilab.common.registry.RegistryStore.register_and_replace` for
|
| + instance).
|
| + """
|
| + assert isinstance(modname, string_types), \
|
| + 'modname expected to be a module name (ie string), got %r' % modname
|
| + for obj in objects:
|
| + if self.is_registrable(obj) and obj.__module__ == modname and not obj in butclasses:
|
| + if isinstance(obj, type):
|
| + self._load_ancestors_then_object(modname, obj, butclasses)
|
| + else:
|
| + self.register(obj)
|
| +
|
| + def register(self, obj, registryname=None, oid=None, clear=False):
|
| + """register `obj` implementation into `registryname` or
|
| + `obj.__registries__` if not specified, with identifier `oid` or
|
| + `obj.__regid__` if not specified.
|
| +
|
| + If `clear` is true, all objects with the same identifier will be
|
| + previously unregistered.
|
| + """
|
| + assert not obj.__dict__.get('__abstract__'), obj
|
| + for registryname in obj_registries(obj, registryname):
|
| + registry = self.setdefault(registryname)
|
| + registry.register(obj, oid=oid, clear=clear)
|
| + self.debug("register %s in %s['%s']",
|
| + registry.objname(obj), registryname, oid or obj.__regid__)
|
| + self._loadedmods.setdefault(obj.__module__, {})[registry.objid(obj)] = obj
|
| +
|
| + def unregister(self, obj, registryname=None):
|
| + """unregister `obj` object from the registry `registryname` or
|
| + `obj.__registries__` if not specified.
|
| + """
|
| + for registryname in obj_registries(obj, registryname):
|
| + registry = self[registryname]
|
| + registry.unregister(obj)
|
| + self.debug("unregister %s from %s['%s']",
|
| + registry.objname(obj), registryname, obj.__regid__)
|
| +
|
| + def register_and_replace(self, obj, replaced, registryname=None):
|
| + """register `obj` object into `registryname` or
|
| + `obj.__registries__` if not specified. If found, the `replaced` object
|
| + will be unregistered first (else a warning will be issued as it is
|
| + generally unexpected).
|
| + """
|
| + for registryname in obj_registries(obj, registryname):
|
| + registry = self[registryname]
|
| + registry.register_and_replace(obj, replaced)
|
| + self.debug("register %s in %s['%s'] instead of %s",
|
| + registry.objname(obj), registryname, obj.__regid__,
|
| + registry.objname(replaced))
|
| +
|
| + # initialization methods ###################################################
|
| +
|
| + def init_registration(self, path, extrapath=None):
|
| + """reset registry and walk down path to return list of (path, name)
|
| + file modules to be loaded"""
|
| + # XXX make this private by renaming it to _init_registration ?
|
| + self.reset()
|
| + # compute list of all modules that have to be loaded
|
| + self._toloadmods, filemods = _toload_info(path, extrapath)
|
| + # XXX is _loadedmods still necessary ? It seems like it's useful
|
| + # to avoid loading same module twice, especially with the
|
| + # _load_ancestors_then_object logic but this needs to be checked
|
| + self._loadedmods = {}
|
| + return filemods
|
| +
|
| + def register_objects(self, path, extrapath=None):
|
| + """register all objects found walking down <path>"""
|
| + # load views from each directory in the instance's path
|
| + # XXX inline init_registration ?
|
| + filemods = self.init_registration(path, extrapath)
|
| + for filepath, modname in filemods:
|
| + self.load_file(filepath, modname)
|
| + self.initialization_completed()
|
| +
|
| + def initialization_completed(self):
|
| + """call initialization_completed() on all known registries"""
|
| + for reg in self.values():
|
| + reg.initialization_completed()
|
| +
|
| + def _mdate(self, filepath):
|
| + """ return the modification date of a file path """
|
| + try:
|
| + return stat(filepath)[-2]
|
| + except OSError:
|
| + # this typically happens on emacs backup files (.#foo.py)
|
| + self.warning('Unable to load %s. It is likely to be a backup file',
|
| + filepath)
|
| + return None
|
| +
|
| + def is_reload_needed(self, path):
|
| + """return True if something module changed and the registry should be
|
| + reloaded
|
| + """
|
| + lastmodifs = self._lastmodifs
|
| + for fileordir in path:
|
| + if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
|
| + if self.is_reload_needed([join(fileordir, fname)
|
| + for fname in listdir(fileordir)]):
|
| + return True
|
| + elif fileordir[-3:] == '.py':
|
| + mdate = self._mdate(fileordir)
|
| + if mdate is None:
|
| + continue # backup file, see _mdate implementation
|
| + elif "flymake" in fileordir:
|
| + # flymake + pylint in use, don't consider these they will corrupt the registry
|
| + continue
|
| + if fileordir not in lastmodifs or lastmodifs[fileordir] < mdate:
|
| + self.info('File %s changed since last visit', fileordir)
|
| + return True
|
| + return False
|
| +
|
| + def load_file(self, filepath, modname):
|
| + """ load registrable objects (if any) from a python file """
|
| + from logilab.common.modutils import load_module_from_name
|
| + if modname in self._loadedmods:
|
| + return
|
| + self._loadedmods[modname] = {}
|
| + mdate = self._mdate(filepath)
|
| + if mdate is None:
|
| + return # backup file, see _mdate implementation
|
| + elif "flymake" in filepath:
|
| + # flymake + pylint in use, don't consider these they will corrupt the registry
|
| + return
|
| + # set update time before module loading, else we get some reloading
|
| + # weirdness in case of syntax error or other error while importing the
|
| + # module
|
| + self._lastmodifs[filepath] = mdate
|
| + # load the module
|
| + module = load_module_from_name(modname)
|
| + self.load_module(module)
|
| +
|
| + def load_module(self, module):
|
| + """Automatically handle module objects registration.
|
| +
|
| + Instances are registered as soon as they are hashable and have the
|
| + following attributes:
|
| +
|
| + * __regid__ (a string)
|
| + * __select__ (a callable)
|
| + * __registries__ (a tuple/list of string)
|
| +
|
| + For classes this is a bit more complicated :
|
| +
|
| + - first ensure parent classes are already registered
|
| +
|
| + - class with __abstract__ == True in their local dictionary are skipped
|
| +
|
| + - object class needs to have registries and identifier properly set to a
|
| + non empty string to be registered.
|
| + """
|
| + self.info('loading %s from %s', module.__name__, module.__file__)
|
| + if hasattr(module, 'registration_callback'):
|
| + module.registration_callback(self)
|
| + else:
|
| + self.register_all(vars(module).values(), module.__name__)
|
| +
|
| + def _load_ancestors_then_object(self, modname, objectcls, butclasses=()):
|
| + """handle class registration according to rules defined in
|
| + :meth:`load_module`
|
| + """
|
| + # backward compat, we used to allow whatever else than classes
|
| + if not isinstance(objectcls, type):
|
| + if self.is_registrable(objectcls) and objectcls.__module__ == modname:
|
| + self.register(objectcls)
|
| + return
|
| + # imported classes
|
| + objmodname = objectcls.__module__
|
| + if objmodname != modname:
|
| + # The module of the object is not the same as the currently
|
| + # worked on module, or this is actually an instance, which
|
| + # has no module at all
|
| + if objmodname in self._toloadmods:
|
| + # if this is still scheduled for loading, let's proceed immediately,
|
| + # but using the object module
|
| + self.load_file(self._toloadmods[objmodname], objmodname)
|
| + return
|
| + # ensure object hasn't been already processed
|
| + clsid = '%s.%s' % (modname, objectcls.__name__)
|
| + if clsid in self._loadedmods[modname]:
|
| + return
|
| + self._loadedmods[modname][clsid] = objectcls
|
| + # ensure ancestors are registered
|
| + for parent in objectcls.__bases__:
|
| + self._load_ancestors_then_object(modname, parent, butclasses)
|
| + # ensure object is registrable
|
| + if objectcls in butclasses or not self.is_registrable(objectcls):
|
| + return
|
| + # backward compat
|
| + reg = self.setdefault(obj_registries(objectcls)[0])
|
| + if reg.objname(objectcls)[0] == '_':
|
| + warn("[lgc 0.59] object whose name start with '_' won't be "
|
| + "skipped anymore at some point, use __abstract__ = True "
|
| + "instead (%s)" % objectcls, DeprecationWarning)
|
| + return
|
| + # register, finally
|
| + self.register(objectcls)
|
| +
|
| + @classmethod
|
| + def is_registrable(cls, obj):
|
| + """ensure `obj` should be registered
|
| +
|
| + as arbitrary stuff may be registered, do a lot of check and warn about
|
| + weird cases (think to dumb proxy objects)
|
| + """
|
| + if isinstance(obj, type):
|
| + if not issubclass(obj, RegistrableObject):
|
| + # ducktyping backward compat
|
| + if not (getattr(obj, '__registries__', None)
|
| + and getattr(obj, '__regid__', None)
|
| + and getattr(obj, '__select__', None)):
|
| + return False
|
| + elif issubclass(obj, RegistrableInstance):
|
| + return False
|
| + elif not isinstance(obj, RegistrableInstance):
|
| + return False
|
| + if not obj.__regid__:
|
| + return False # no regid
|
| + registries = obj.__registries__
|
| + if not registries:
|
| + return False # no registries
|
| + selector = obj.__select__
|
| + if not selector:
|
| + return False # no selector
|
| + if obj.__dict__.get('__abstract__', False):
|
| + return False
|
| + # then detect potential problems that should be warned
|
| + if not isinstance(registries, (tuple, list)):
|
| + cls.warning('%s has __registries__ which is not a list or tuple', obj)
|
| + return False
|
| + if not callable(selector):
|
| + cls.warning('%s has not callable __select__', obj)
|
| + return False
|
| + return True
|
| +
|
| + # these are overridden by set_log_methods below
|
| + # only defining here to prevent pylint from complaining
|
| + info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
|
| +
|
| +
|
| +# init logging
|
| +set_log_methods(RegistryStore, getLogger('registry.store'))
|
| +set_log_methods(Registry, getLogger('registry'))
|
| +
|
| +
|
| +# helpers for debugging selectors
|
| +TRACED_OIDS = None
|
| +
|
| +def _trace_selector(cls, selector, args, ret):
|
| + vobj = args[0]
|
| + if TRACED_OIDS == 'all' or vobj.__regid__ in TRACED_OIDS:
|
| + print('%s -> %s for %s(%s)' % (cls, ret, vobj, vobj.__regid__))
|
| +
|
| +def _lltrace(selector):
|
| + """use this decorator on your predicates so they become traceable with
|
| + :class:`traced_selection`
|
| + """
|
| + def traced(cls, *args, **kwargs):
|
| + ret = selector(cls, *args, **kwargs)
|
| + if TRACED_OIDS is not None:
|
| + _trace_selector(cls, selector, args, ret)
|
| + return ret
|
| + traced.__name__ = selector.__name__
|
| + traced.__doc__ = selector.__doc__
|
| + return traced
|
| +
|
| +class traced_selection(object): # pylint: disable=C0103
|
| + """
|
| + Typical usage is :
|
| +
|
| + .. sourcecode:: python
|
| +
|
| + >>> from logilab.common.registry import traced_selection
|
| + >>> with traced_selection():
|
| + ... # some code in which you want to debug selectors
|
| + ... # for all objects
|
| +
|
| + This will yield lines like this in the logs::
|
| +
|
| + selector one_line_rset returned 0 for <class 'elephant.Babar'>
|
| +
|
| + You can also give to :class:`traced_selection` the identifiers of objects on
|
| + which you want to debug selection ('oid1' and 'oid2' in the example above).
|
| +
|
| + .. sourcecode:: python
|
| +
|
| + >>> with traced_selection( ('regid1', 'regid2') ):
|
| + ... # some code in which you want to debug selectors
|
| + ... # for objects with __regid__ 'regid1' and 'regid2'
|
| +
|
| + A potentially useful point to set up such a tracing function is
|
| + the `logilab.common.registry.Registry.select` method body.
|
| + """
|
| +
|
| + def __init__(self, traced='all'):
|
| + self.traced = traced
|
| +
|
| + def __enter__(self):
|
| + global TRACED_OIDS
|
| + TRACED_OIDS = self.traced
|
| +
|
| + def __exit__(self, exctype, exc, traceback):
|
| + global TRACED_OIDS
|
| + TRACED_OIDS = None
|
| + return traceback is None
|
| +
|
| +# selector base classes and operations ########################################
|
| +
|
| +def objectify_predicate(selector_func):
|
| + """Most of the time, a simple score function is enough to build a selector.
|
| + The :func:`objectify_predicate` decorator turn it into a proper selector
|
| + class::
|
| +
|
| + @objectify_predicate
|
| + def one(cls, req, rset=None, **kwargs):
|
| + return 1
|
| +
|
| + class MyView(View):
|
| + __select__ = View.__select__ & one()
|
| +
|
| + """
|
| + return type(selector_func.__name__, (Predicate,),
|
| + {'__doc__': selector_func.__doc__,
|
| + '__call__': lambda self, *a, **kw: selector_func(*a, **kw)})
|
| +
|
| +
|
| +_PREDICATES = {}
|
| +
|
| +def wrap_predicates(decorator):
|
| + for predicate in _PREDICATES.values():
|
| + if not '_decorators' in predicate.__dict__:
|
| + predicate._decorators = set()
|
| + if decorator in predicate._decorators:
|
| + continue
|
| + predicate._decorators.add(decorator)
|
| + predicate.__call__ = decorator(predicate.__call__)
|
| +
|
| +class PredicateMetaClass(type):
|
| + def __new__(mcs, *args, **kwargs):
|
| + # use __new__ so subclasses doesn't have to call Predicate.__init__
|
| + inst = type.__new__(mcs, *args, **kwargs)
|
| + proxy = weakref.proxy(inst, lambda p: _PREDICATES.pop(id(p)))
|
| + _PREDICATES[id(proxy)] = proxy
|
| + return inst
|
| +
|
| +
|
| +@add_metaclass(PredicateMetaClass)
|
| +class Predicate(object):
|
| + """base class for selector classes providing implementation
|
| + for operators ``&``, ``|`` and ``~``
|
| +
|
| + This class is only here to give access to binary operators, the selector
|
| + logic itself should be implemented in the :meth:`__call__` method. Notice it
|
| + should usually accept any arbitrary arguments (the context), though that may
|
| + vary depending on your usage of the registry.
|
| +
|
| + a selector is called to help choosing the correct object for a
|
| + particular context by returning a score (`int`) telling how well
|
| + the implementation given as first argument fit to the given context.
|
| +
|
| + 0 score means that the class doesn't apply.
|
| + """
|
| +
|
| + @property
|
| + def func_name(self):
|
| + # backward compatibility
|
| + return self.__class__.__name__
|
| +
|
| + def search_selector(self, selector):
|
| + """search for the given selector, selector instance or tuple of
|
| + selectors in the selectors tree. Return None if not found.
|
| + """
|
| + if self is selector:
|
| + return self
|
| + if (isinstance(selector, type) or isinstance(selector, tuple)) and \
|
| + isinstance(self, selector):
|
| + return self
|
| + return None
|
| +
|
| + def __str__(self):
|
| + return self.__class__.__name__
|
| +
|
| + def __and__(self, other):
|
| + return AndPredicate(self, other)
|
| + def __rand__(self, other):
|
| + return AndPredicate(other, self)
|
| + def __iand__(self, other):
|
| + return AndPredicate(self, other)
|
| + def __or__(self, other):
|
| + return OrPredicate(self, other)
|
| + def __ror__(self, other):
|
| + return OrPredicate(other, self)
|
| + def __ior__(self, other):
|
| + return OrPredicate(self, other)
|
| +
|
| + def __invert__(self):
|
| + return NotPredicate(self)
|
| +
|
| + # XXX (function | function) or (function & function) not managed yet
|
| +
|
| + def __call__(self, cls, *args, **kwargs):
|
| + return NotImplementedError("selector %s must implement its logic "
|
| + "in its __call__ method" % self.__class__)
|
| +
|
| + def __repr__(self):
|
| + return u'<Predicate %s at %x>' % (self.__class__.__name__, id(self))
|
| +
|
| +
|
| +class MultiPredicate(Predicate):
|
| + """base class for compound selector classes"""
|
| +
|
| + def __init__(self, *selectors):
|
| + self.selectors = self.merge_selectors(selectors)
|
| +
|
| + def __str__(self):
|
| + return '%s(%s)' % (self.__class__.__name__,
|
| + ','.join(str(s) for s in self.selectors))
|
| +
|
| + @classmethod
|
| + def merge_selectors(cls, selectors):
|
| + """deal with selector instanciation when necessary and merge
|
| + multi-selectors if possible:
|
| +
|
| + AndPredicate(AndPredicate(sel1, sel2), AndPredicate(sel3, sel4))
|
| + ==> AndPredicate(sel1, sel2, sel3, sel4)
|
| + """
|
| + merged_selectors = []
|
| + for selector in selectors:
|
| + # XXX do we really want magic-transformations below?
|
| + # if so, wanna warn about them?
|
| + if isinstance(selector, types.FunctionType):
|
| + selector = objectify_predicate(selector)()
|
| + if isinstance(selector, type) and issubclass(selector, Predicate):
|
| + selector = selector()
|
| + assert isinstance(selector, Predicate), selector
|
| + if isinstance(selector, cls):
|
| + merged_selectors += selector.selectors
|
| + else:
|
| + merged_selectors.append(selector)
|
| + return merged_selectors
|
| +
|
| + def search_selector(self, selector):
|
| + """search for the given selector or selector instance (or tuple of
|
| + selectors) in the selectors tree. Return None if not found
|
| + """
|
| + for childselector in self.selectors:
|
| + if childselector is selector:
|
| + return childselector
|
| + found = childselector.search_selector(selector)
|
| + if found is not None:
|
| + return found
|
| + # if not found in children, maybe we are looking for self?
|
| + return super(MultiPredicate, self).search_selector(selector)
|
| +
|
| +
|
| +class AndPredicate(MultiPredicate):
|
| + """and-chained selectors"""
|
| + def __call__(self, cls, *args, **kwargs):
|
| + score = 0
|
| + for selector in self.selectors:
|
| + partscore = selector(cls, *args, **kwargs)
|
| + if not partscore:
|
| + return 0
|
| + score += partscore
|
| + return score
|
| +
|
| +
|
| +class OrPredicate(MultiPredicate):
|
| + """or-chained selectors"""
|
| + def __call__(self, cls, *args, **kwargs):
|
| + for selector in self.selectors:
|
| + partscore = selector(cls, *args, **kwargs)
|
| + if partscore:
|
| + return partscore
|
| + return 0
|
| +
|
| +class NotPredicate(Predicate):
|
| + """negation selector"""
|
| + def __init__(self, selector):
|
| + self.selector = selector
|
| +
|
| + def __call__(self, cls, *args, **kwargs):
|
| + score = self.selector(cls, *args, **kwargs)
|
| + return int(not score)
|
| +
|
| + def __str__(self):
|
| + return 'NOT(%s)' % self.selector
|
| +
|
| +
|
| +class yes(Predicate): # pylint: disable=C0103
|
| + """Return the score given as parameter, with a default score of 0.5 so any
|
| + other selector take precedence.
|
| +
|
| + Usually used for objects which can be selected whatever the context, or
|
| + also sometimes to add arbitrary points to a score.
|
| +
|
| + Take care, `yes(0)` could be named 'no'...
|
| + """
|
| + def __init__(self, score=0.5):
|
| + self.score = score
|
| +
|
| + def __call__(self, *args, **kwargs):
|
| + return self.score
|
| +
|
| +
|
| +# deprecated stuff #############################################################
|
| +
|
| +from logilab.common.deprecation import deprecated
|
| +
|
| +@deprecated('[lgc 0.59] use Registry.objid class method instead')
|
| +def classid(cls):
|
| + return '%s.%s' % (cls.__module__, cls.__name__)
|
| +
|
| +@deprecated('[lgc 0.59] use obj_registries function instead')
|
| +def class_registries(cls, registryname):
|
| + return obj_registries(cls, registryname)
|
| +
|
|
|