Commit 1f96f351 authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[vreg] move base registry implementation to logilab.common. Closes #1916014

A new logilab.common.registry module has been created with content from

* cw.vreg (the whole things that was in there)
* cw.appobject (base selectors and all).

In the process, we've done some renaming:

* former selector functions are now known as "predicate", though you still
  use predicates to build an object'selector

* hence `objectify_selector` decorator is now `objectify_predicate`

* the top level registry is now `RegistryStore` (was `VRegistry`)

Also there is no more need for the @lltrace decorator.

On the CubicWeb side, the `selectors` module has been renamed to `predicates`.

There should be full backward compat with proper deprecation warnings.
parent 2ee0ef069fa7
......@@ -55,6 +55,7 @@ set_log_methods(sys.modules[__name__], logging.getLogger('cubicweb'))
# make all exceptions accessible from the package
from cubicweb._exceptions import *
from logilab.common.registry import ObjectNotFound, NoSelectableObject, RegistryNotFound
# convert eid to the right type, raise ValueError if it's not a valid eid
typed_eid = int
......
......@@ -40,7 +40,7 @@ classifiers = [
]
__depends__ = {
'logilab-common': '>= 0.57.0',
'logilab-common': '>= 0.58.0',
'logilab-mtconverter': '>= 0.8.0',
'rql': '>= 0.28.0',
'yams': '>= 0.34.0',
......
# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......@@ -15,10 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""Exceptions shared by different cubicweb packages.
"""Exceptions shared by different cubicweb packages."""
"""
__docformat__ = "restructuredtext en"
from yams import ValidationError
......@@ -114,32 +112,8 @@ class EidNotInSource(SourceException):
# registry exceptions #########################################################
class RegistryException(CubicWebException):
"""raised when an unregistered view is called"""
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 appobject is selectable for a given context."""
def __init__(self, args, kwargs, appobjects):
self.args = args
self.kwargs = kwargs
self.appobjects = appobjects
def __str__(self):
return ('args: %s, kwargs: %s\ncandidates: %s'
% (self.args, self.kwargs.keys(), self.appobjects))
# pre 3.15 bw compat
from logilab.common.registry import RegistryException, ObjectNotFound, NoSelectableObject
class UnknownProperty(RegistryException):
"""property found in database but unknown in registry"""
......@@ -161,3 +135,4 @@ class ExecutionError(Exception):
# pylint: disable=W0611
from logilab.common.clcommands import BadCommandUsage
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......@@ -38,278 +38,19 @@ from warnings import warn
from logilab.common.deprecation import deprecated
from logilab.common.decorators import classproperty
from logilab.common.logging_ext import set_log_methods
from logilab.common.registry import yes
from cubicweb.cwconfig import CubicWebConfiguration
def class_regid(cls):
"""returns a unique identifier for an appobject class"""
return cls.__regid__
# helpers for debugging selectors
TRACED_OIDS = None
def _trace_selector(cls, selector, args, ret):
# /!\ lltrace decorates pure function or __call__ method, this
# means argument order may be different
if isinstance(cls, Selector):
selname = str(cls)
vobj = args[0]
else:
selname = selector.__name__
vobj = cls
if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS:
#SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
def lltrace(selector):
"""use this decorator on your selectors so the becomes traceable with
:class:`traced_selection`
"""
# don't wrap selectors if not in development mode
if CubicWebConfiguration.mode == 'system': # XXX config.debug
return selector
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):
"""
Typical usage is :
.. sourcecode:: python
>>> from cubicweb.selectors import traced_selection
>>> with traced_selection():
... # some code in which you want to debug selectors
... # for all objects
Don't forget the 'from __future__ import with_statement' at the module top-level
if you're using python prior to 2.6.
This will yield lines like this in the logs::
selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
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 usefull point to set up such a tracing function is
the `cubicweb.vregistry.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_selector(selector_func):
"""Most of the time, a simple score function is enough to build a selector.
The :func:`objectify_selector` decorator turn it into a proper selector
class::
@objectify_selector
def one(cls, req, rset=None, **kwargs):
return 1
class MyView(View):
__select__ = View.__select__ & one()
"""
return type(selector_func.__name__, (Selector,),
{'__doc__': selector_func.__doc__,
'__call__': lambda self, *a, **kw: selector_func(*a, **kw)})
def _instantiate_selector(selector):
"""ensures `selector` is a `Selector` instance
NOTE: This should only be used locally in build___select__()
XXX: then, why not do it ??
"""
if isinstance(selector, types.FunctionType):
return objectify_selector(selector)()
if isinstance(selector, type) and issubclass(selector, Selector):
return selector()
return selector
# XXX for bw compat
from logilab.common.registry import objectify_predicate, traced_selection
class Selector(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 __call__ method
a selector is called to help choosing the correct object for a
particular context by returning a score (`int`) telling how well
the class given as first argument apply 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 AndSelector(self, other)
def __rand__(self, other):
return AndSelector(other, self)
def __iand__(self, other):
return AndSelector(self, other)
def __or__(self, other):
return OrSelector(self, other)
def __ror__(self, other):
return OrSelector(other, self)
def __ior__(self, other):
return OrSelector(self, other)
def __invert__(self):
return NotSelector(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'<Selector %s at %x>' % (self.__class__.__name__, id(self))
class MultiSelector(Selector):
"""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:
AndSelector(AndSelector(sel1, sel2), AndSelector(sel3, sel4))
==> AndSelector(sel1, sel2, sel3, sel4)
"""
merged_selectors = []
for selector in selectors:
try:
selector = _instantiate_selector(selector)
except Exception:
pass
#assert isinstance(selector, Selector), 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(MultiSelector, self).search_selector(selector)
class AndSelector(MultiSelector):
"""and-chained selectors (formerly known as chainall)"""
@lltrace
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 OrSelector(MultiSelector):
"""or-chained selectors (formerly known as chainfirst)"""
@lltrace
def __call__(self, cls, *args, **kwargs):
for selector in self.selectors:
partscore = selector(cls, *args, **kwargs)
if partscore:
return partscore
return 0
class NotSelector(Selector):
"""negation selector"""
def __init__(self, selector):
self.selector = selector
@lltrace
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(Selector):
"""Return the score given as parameter, with a default score of 0.5 so any
other selector take precedence.
Usually used for appobjects 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
objectify_selector = deprecated('[3.15] objectify_selector has been renamed to objectify_predicates in logilab.common.registry')(objectify_predicate)
traced_selection = deprecated('[3.15] traced_selection has been moved to logilab.common.registry')(traced_selection)
@deprecated('[3.15] lltrace decorator can now be removed')
def lltrace(func):
return func
# the base class for all appobjects ############################################
......@@ -464,3 +205,6 @@ class AppObject(object):
info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
set_log_methods(AppObject, getLogger('cubicweb.appobject'))
# defined here to avoid warning on usage on the AppObject class
yes = deprecated('[3.15] yes has been moved to logilab.common.registry')(yes)
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......@@ -15,12 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
""".. VRegistry:
""".. RegistryStore:
The `VRegistry`
---------------
The `RegistryStore`
-------------------
The `VRegistry` can be seen as a two-level dictionary. It contains
The `RegistryStore` can be seen as a two-level dictionary. It contains
all dynamically loaded objects (subclasses of :ref:`appobject`) to
build a |cubicweb| application. Basically:
......@@ -34,7 +34,7 @@ build a |cubicweb| application. Basically:
A *registry* holds a specific kind of application objects. There is
for instance a registry for entity classes, another for views, etc...
The `VRegistry` has two main responsibilities:
The `RegistryStore` has two main responsibilities:
- being the access point to all registries
......@@ -76,13 +76,13 @@ API for objects registration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here are the registration methods that you can use in the `registration_callback`
to register your objects to the `VRegistry` instance given as argument (usually
to register your objects to the `RegistryStore` instance given as argument (usually
named `vreg`):
.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_all
.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_and_replace
.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register
.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.unregister
.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_all
.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_and_replace
.. automethod:: cubicweb.cwvreg.CWRegistryStore.register
.. automethod:: cubicweb.cwvreg.CWRegistryStore.unregister
Examples:
......@@ -193,41 +193,44 @@ selectors that will inspect their content and return a score accordingly.
__docformat__ = "restructuredtext en"
_ = unicode
import sys
from os.path import join, dirname, realpath
from warnings import warn
from datetime import datetime, date, time, timedelta
from logilab.common.decorators import cached, clear_cache
from logilab.common.deprecation import deprecated, class_deprecated
from logilab.common.modutils import cleanup_sys_modules
from logilab.common.registry import (
RegistryStore, Registry, classid,
ObjectNotFound, NoSelectableObject, RegistryNotFound)
from rql import RQLHelper
from yams.constraints import BASE_CONVERTERS
from cubicweb import (ETYPE_NAME_MAP, Binary, UnknownProperty, UnknownEid,
ObjectNotFound, NoSelectableObject, RegistryNotFound,
CW_EVENT_MANAGER)
from cubicweb.vregistry import VRegistry, Registry, class_regid, classid
from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER,
Binary, UnknownProperty, UnknownEid)
from cubicweb.rtags import RTAGS
from cubicweb.predicates import (implements, appobject_selectable,
_reset_is_instance_cache)
def clear_rtag_objects():
for rtag in RTAGS:
rtag.clear()
def use_interfaces(obj):
"""return interfaces used by the given object by searching for implements
selectors
"""return interfaces required by the given object by searching for
`implements` predicate
"""
from cubicweb.selectors import implements
impl = obj.__select__.search_selector(implements)
if impl:
return sorted(impl.expected_ifaces)
return ()
def require_appobject(obj):
"""return interfaces used by the given object by searching for implements
selectors
"""return appobjects required by the given object by searching for
`appobject_selectable` predicate
"""
from cubicweb.selectors import appobject_selectable
impl = obj.__select__.search_selector(appobject_selectable)
if impl:
return (impl.registry, impl.regids)
......@@ -253,16 +256,13 @@ class CWRegistry(Registry):
key=lambda x: x.cw_propval('order'))
VRegistry.REGISTRY_FACTORY[None] = CWRegistry
class ETypeRegistry(CWRegistry):
def clear_caches(self):
clear_cache(self, 'etype_class')
clear_cache(self, 'parent_classes')
from cubicweb import selectors
selectors._reset_is_instance_cache(self.vreg)
_reset_is_instance_cache(self.vreg)
def initialization_completed(self):
"""on registration completed, clear etype_class internal cache
......@@ -272,7 +272,7 @@ class ETypeRegistry(CWRegistry):
self.clear_caches()
def register(self, obj, **kwargs):
oid = kwargs.get('oid') or class_regid(obj)
oid = kwargs.get('oid') or obj.__regid__
if oid != 'Any' and not oid in self.schema:
self.error('don\'t register %s, %s type not defined in the '
'schema', obj, oid)
......@@ -354,8 +354,6 @@ class ETypeRegistry(CWRegistry):
fetchattrs_list.append(set(etypecls.fetch_attrs))
return reduce(set.intersection, fetchattrs_list)
VRegistry.REGISTRY_FACTORY['etypes'] = ETypeRegistry
class ViewsRegistry(CWRegistry):
......@@ -389,8 +387,6 @@ class ViewsRegistry(CWRegistry):
self.exception('error while trying to select %s view for %s',
vid, rset)
VRegistry.REGISTRY_FACTORY['views'] = ViewsRegistry
class ActionsRegistry(CWRegistry):
def poss_visible_objects(self, *args, **kwargs):
......@@ -408,8 +404,6 @@ class ActionsRegistry(CWRegistry):
result.setdefault(action.category, []).append(action)
return result
VRegistry.REGISTRY_FACTORY['actions'] = ActionsRegistry
class CtxComponentsRegistry(CWRegistry):
def poss_visible_objects(self, *args, **kwargs):
......@@ -445,8 +439,6 @@ class CtxComponentsRegistry(CWRegistry):
component.cw_extra_kwargs['context'] = context
return thisctxcomps
VRegistry.REGISTRY_FACTORY['ctxcomponents'] = CtxComponentsRegistry
class BwCompatCWRegistry(object):
def __init__(self, vreg, oldreg, redirecttoreg):
......@@ -462,14 +454,15 @@ class BwCompatCWRegistry(object):
def clear(self): pass
def initialization_completed(self): pass
class CubicWebVRegistry(VRegistry):
class CWRegistryStore(RegistryStore):
"""Central registry for the cubicweb instance, extending the generic
VRegistry with some cubicweb specific stuff.
RegistryStore with some cubicweb specific stuff.
This is one of the central object in cubicweb instance, coupling
dynamically loaded objects with the schema and the configuration objects.
It specializes the VRegistry by adding some convenience methods to access to
It specializes the RegistryStore by adding some convenience methods to access to
stored objects. Currently we have the following registries of objects known
by the web instance (library may use some others additional registries):
......@@ -492,11 +485,29 @@ class CubicWebVRegistry(VRegistry):
plugged into the application
"""
REGISTRY_FACTORY = {None: CWRegistry,
'etypes': ETypeRegistry,
'views': ViewsRegistry,
'actions': ActionsRegistry,
'ctxcomponents': CtxComponentsRegistry,
}
def __init__(self, config, initlog=True):
if initlog:
# first init log service
config.init_log()
super(CubicWebVRegistry, self).__init__(config)
super(CWRegistryStore, self).__init__(config.debugmode)
self.config = config
# need to clean sys.path this to avoid import confusion pb (i.e. having
# the same module loaded as 'cubicweb.web.views' subpackage and as
# views' or 'web.views' subpackage. This is mainly for testing purpose,
# we should'nt need this in production environment
for webdir in (join(dirname(realpath(__file__)), 'web'),
join(dirname(__file__), 'web')):
if webdir in sys.path:
sys.path.remove(webdir)
if CW_SOFTWARE_ROOT in sys.path:
sys.path.remove(CW_SOFTWARE_ROOT)
self.schema = None
self.initialized = False
# XXX give force_reload (or refactor [re]loading...)
......@@ -515,10 +526,10 @@ class CubicWebVRegistry(VRegistry):
return self[regid]
def items(self):
return [item for item in super(CubicWebVRegistry, self).items()
return [item for item in super(CWRegistryStore, self).items()
if not item[0] in ('propertydefs', 'propertyvalues')]
def iteritems(self):
return (item for item in super(CubicWebVRegistry, self).iteritems()
return (item for item in super(CWRegistryStore, self).iteritems()
if not item[0] in ('propertydefs', 'propertyvalues'))
def values(self):
......@@ -528,7 +539,7 @@ class CubicWebVRegistry(VRegistry):
def reset(self):
CW_EVENT_MANAGER.emit('before-registry-reset', self)
super(CubicWebVRegistry, self).reset()
super(CWRegistryStore, self).reset()
self._needs_iface = {}
self._needs_appobject = {}
# two special registries, propertydefs which care all the property
......@@ -597,7 +608,7 @@ class CubicWebVRegistry(VRegistry):
the given `ifaces` interfaces at the end of the registration process.
Extra keyword arguments are given to the
:meth:`~cubicweb.cwvreg.CubicWebVRegistry.register` function.
:meth:`~cubicweb.cwvreg.CWRegistryStore.register` function.
"""
self.register(obj, **kwargs)
if not isinstance(ifaces, (tuple, list)):
......@@ -613,7 +624,7 @@ class CubicWebVRegistry(VRegistry):
If `clear` is true, all objects with the same identifier will be
previously unregistered.
"""
super(CubicWebVRegistry, self).register(obj, *args, **kwargs)
super(CWRegistryStore, self).register(obj, *args, **kwargs)
# XXX bw compat
ifaces = use_interfaces(obj)
if ifaces:
......@@ -630,7 +641,7 @@ class CubicWebVRegistry(VRegistry):
def register_objects(self, path):
"""overriden to give cubicweb's extrapath (eg cubes package's __path__)
"""
super(CubicWebVRegistry, self).register_objects(
super(CWRegistryStore, self).register_objects(
path, self.config.extrapath)
def initialization_completed(self):
......@@ -685,7 +696,7 @@ class CubicWebVRegistry(VRegistry):
self.debug('unregister %s (no %s object in registry %s)',
classid(obj), ' or '.join(regids), regname)