appobject.py
changeset 2657 de974465d381
parent 2656 a93ae0f6c0ad
child 2658 5535857eeaa5
--- 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'))