--- a/appobject.py Mon Aug 03 14:14:07 2009 +0200
+++ b/appobject.py Mon Aug 03 15:16:47 2009 +0200
@@ -1,4 +1,6 @@
-"""Base class for dynamically loaded objects manipulated in the web interface
+"""Base class for dynamically loaded objects accessible through the vregistry.
+
+You'll also find some convenience classes to build selectors.
:organization: Logilab
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
@@ -7,20 +9,23 @@
"""
__docformat__ = "restructuredtext en"
+import types
+from logging import getLogger
from datetime import datetime, timedelta, time
from logilab.common.decorators import classproperty
from logilab.common.deprecation import deprecated
+from logilab.common.logging_ext import set_log_methods
from rql.nodes import VariableRef, SubQuery
from rql.stmts import Union, Select
from cubicweb import Unauthorized, NoSelectableObject
-from cubicweb.vregistry import VObject, AndSelector
-from cubicweb.selectors import yes
from cubicweb.utils import UStringIO, ustrftime, strptime, todate, todatetime
ONESECOND = timedelta(0, 1, 0)
+CACHE_REGISTRY = {}
+
class Cache(dict):
def __init__(self):
@@ -29,14 +34,200 @@
self.cache_creation_date = _now
self.latest_cache_lookup = _now
-CACHE_REGISTRY = {}
+
+# selector base classes and operations ########################################
+
+def objectify_selector(selector_func):
+ """convenience decorator for simple selectors where a class definition
+ would be overkill::
+
+ @objectify_selector
+ def yes(cls, *args, **kwargs):
+ return 1
+
+ """
+ return type(selector_func.__name__, (Selector,),
+ {'__call__': lambda self, *args, **kwargs: selector_func(*args, **kwargs)})
+
+
+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
+
+
+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 or selector instance in the selectors
+ tree. Return it of None if not found
+ """
+ if self is selector:
+ return self
+ if isinstance(selector, type) 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 __or__(self, other):
+ return OrSelector(self, other)
+ def __ror__(self, other):
+ return OrSelector(other, self)
+
+ 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__)
+
+
+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:
-class AppObject(VObject):
- """This is the base class for CubicWeb application objects
- which are selected according to a request and result set.
+ AndSelector(AndSelector(sel1, sel2), AndSelector(sel3, sel4))
+ ==> AndSelector(sel1, sel2, sel3, sel4)
+ """
+ merged_selectors = []
+ for selector in selectors:
+ try:
+ selector = _instantiate_selector(selector)
+ except:
+ 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 in the selectors
+ tree. Return it of 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
+ return None
+
+
+class AndSelector(MultiSelector):
+ """and-chained selectors (formerly known as chainall)"""
+ 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
+
- Classes are kept in the vregistry and instantiation is done at selection
- time.
+class OrSelector(MultiSelector):
+ """or-chained selectors (formerly known as chainfirst)"""
+ 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
+
+ def __call__(self, cls, *args, **kwargs):
+ score = self.selector(cls, *args, **kwargs)
+ return int(not score)
+
+ def __str__(self):
+ return 'NOT(%s)' % super(NotSelector, self).__str__()
+
+
+class yes(Selector):
+ """return arbitrary score
+
+ default score of 0.5 so any other selector take precedence
+ """
+ def __init__(self, score=0.5):
+ self.score = score
+
+ def __call__(self, *args, **kwargs):
+ return self.score
+
+
+# the base class for all appobjects ############################################
+
+class AppObject(object):
+ """This is the base class for CubicWeb application objects which are
+ selected according to a context (usually at least a request and a result
+ set).
+
+ Concrete application objects classes are designed to be loaded by the
+ vregistry and should be accessed through it, not by direct instantiation.
+
+ The following attributes should be set on concret appobject classes:
+ :__registry__:
+ name of the registry for this object (string like 'views',
+ 'templates'...)
+ :id:
+ object's identifier in the registry (string like 'main',
+ 'primary', 'folder_box')
+ :__select__:
+ class'selector
+
+ Moreover, the `__abstract__` attribute may be set to True to indicate
+ that a appobject is abstract and should not be registered.
At registration time, the following attributes are set on the class:
:vreg:
@@ -46,20 +237,64 @@
:config:
the instance's configuration
- At instantiation time, the following attributes are set on the instance:
+ At selection time, the following attributes are set on the instance:
:req:
current request
:rset:
- result set on which the object is applied
+ context result set or None
+ :row:
+ if a result set is set and the context is about a particular cell in the
+ result set, and not the result set as a whole, specify the row number we
+ are interested in, else None
+ :col:
+ if a result set is set and the context is about a particular cell in the
+ result set, and not the result set as a whole, specify the col number we
+ are interested in, else None
"""
+ __registry__ = None
+ id = None
__select__ = yes()
@classmethod
- def registered(cls, reg):
- super(AppObject, cls).registered(reg)
- cls.vreg = reg.vreg
- cls.schema = reg.schema
- cls.config = reg.config
+ def classid(cls):
+ """returns a unique identifier for the appobject"""
+ return '%s.%s' % (cls.__module__, cls.__name__)
+
+ # XXX bw compat code
+ @classmethod
+ def build___select__(cls):
+ for klass in cls.mro():
+ if klass.__name__ == 'AppObject':
+ continue # the bw compat __selector__ is there
+ klassdict = klass.__dict__
+ if ('__select__' in klassdict and '__selectors__' in klassdict
+ and '__selgenerated__' not in klassdict):
+ raise TypeError("__select__ and __selectors__ can't be used together on class %s" % cls)
+ if '__selectors__' in klassdict and '__selgenerated__' not in klassdict:
+ cls.__selgenerated__ = True
+ # case where __selectors__ is defined locally (but __select__
+ # is in a parent class)
+ selectors = klassdict['__selectors__']
+ if len(selectors) == 1:
+ # micro optimization: don't bother with AndSelector if there's
+ # only one selector
+ select = _instantiate_selector(selectors[0])
+ else:
+ select = AndSelector(*selectors)
+ cls.__select__ = select
+
+ @classmethod
+ def registered(cls, registry):
+ """called by the registry when the appobject has been registered.
+
+ It must return the object that will be actually registered (this may be
+ the right hook to create an instance for example). By default the
+ appobject is returned without any transformation.
+ """
+ cls.build___select__()
+ cls.vreg = registry.vreg
+ cls.schema = registry.schema
+ cls.config = registry.config
cls.register_properties()
return cls
@@ -69,9 +304,13 @@
@classmethod
def selected(cls, *args, **kwargs):
- """by default web app objects are usually instantiated on
- selection according to a request, a result set, and optional
- row and col
+ """called by the registry when the appobject has been selected.
+
+ It must return the object that will be actually returned by the .select
+ method (this may be the right hook to create an instance for
+ example). By default the selected object is called using the given args
+ and kwargs and the resulting value (usually a class instance) is
+ returned without any transformation.
"""
return cls(*args, **kwargs)
@@ -340,3 +579,5 @@
first = rql.split(' ', 1)[0].lower()
if first in ('insert', 'set', 'delete'):
raise Unauthorized(self.req._('only select queries are authorized'))
+
+set_log_methods(AppObject, getLogger('cubicweb.appobject'))