appobject.py
changeset 8190 2a3c1b787688
parent 7990 a673d1d9a738
child 8240 506ab2e8aeca
equal deleted inserted replaced
8189:2ee0ef069fa7 8190:2a3c1b787688
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    36 from warnings import warn
    36 from warnings import warn
    37 
    37 
    38 from logilab.common.deprecation import deprecated
    38 from logilab.common.deprecation import deprecated
    39 from logilab.common.decorators import classproperty
    39 from logilab.common.decorators import classproperty
    40 from logilab.common.logging_ext import set_log_methods
    40 from logilab.common.logging_ext import set_log_methods
       
    41 from logilab.common.registry import yes
    41 
    42 
    42 from cubicweb.cwconfig import CubicWebConfiguration
    43 from cubicweb.cwconfig import CubicWebConfiguration
    43 
    44 # XXX for bw compat
    44 def class_regid(cls):
    45 from logilab.common.registry import objectify_predicate, traced_selection
    45     """returns a unique identifier for an appobject class"""
    46 
    46     return cls.__regid__
    47 
    47 
    48 objectify_selector = deprecated('[3.15] objectify_selector has been renamed to objectify_predicates in logilab.common.registry')(objectify_predicate)
    48 # helpers for debugging selectors
    49 traced_selection = deprecated('[3.15] traced_selection has been moved to logilab.common.registry')(traced_selection)
    49 TRACED_OIDS = None
    50 
    50 
    51 @deprecated('[3.15] lltrace decorator can now be removed')
    51 def _trace_selector(cls, selector, args, ret):
    52 def lltrace(func):
    52     # /!\ lltrace decorates pure function or __call__ method, this
    53     return func
    53     #     means argument order may be different
       
    54     if isinstance(cls, Selector):
       
    55         selname = str(cls)
       
    56         vobj = args[0]
       
    57     else:
       
    58         selname = selector.__name__
       
    59         vobj = cls
       
    60     if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS:
       
    61         #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
       
    62         print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
       
    63 
       
    64 def lltrace(selector):
       
    65     """use this decorator on your selectors so the becomes traceable with
       
    66     :class:`traced_selection`
       
    67     """
       
    68     # don't wrap selectors if not in development mode
       
    69     if CubicWebConfiguration.mode == 'system': # XXX config.debug
       
    70         return selector
       
    71     def traced(cls, *args, **kwargs):
       
    72         ret = selector(cls, *args, **kwargs)
       
    73         if TRACED_OIDS is not None:
       
    74             _trace_selector(cls, selector, args, ret)
       
    75         return ret
       
    76     traced.__name__ = selector.__name__
       
    77     traced.__doc__ = selector.__doc__
       
    78     return traced
       
    79 
       
    80 class traced_selection(object):
       
    81     """
       
    82     Typical usage is :
       
    83 
       
    84     .. sourcecode:: python
       
    85 
       
    86         >>> from cubicweb.selectors import traced_selection
       
    87         >>> with traced_selection():
       
    88         ...     # some code in which you want to debug selectors
       
    89         ...     # for all objects
       
    90 
       
    91     Don't forget the 'from __future__ import with_statement' at the module top-level
       
    92     if you're using python prior to 2.6.
       
    93 
       
    94     This will yield lines like this in the logs::
       
    95 
       
    96         selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
       
    97 
       
    98     You can also give to :class:`traced_selection` the identifiers of objects on
       
    99     which you want to debug selection ('oid1' and 'oid2' in the example above).
       
   100 
       
   101     .. sourcecode:: python
       
   102 
       
   103         >>> with traced_selection( ('regid1', 'regid2') ):
       
   104         ...     # some code in which you want to debug selectors
       
   105         ...     # for objects with __regid__ 'regid1' and 'regid2'
       
   106 
       
   107     A potentially usefull point to set up such a tracing function is
       
   108     the `cubicweb.vregistry.Registry.select` method body.
       
   109     """
       
   110 
       
   111     def __init__(self, traced='all'):
       
   112         self.traced = traced
       
   113 
       
   114     def __enter__(self):
       
   115         global TRACED_OIDS
       
   116         TRACED_OIDS = self.traced
       
   117 
       
   118     def __exit__(self, exctype, exc, traceback):
       
   119         global TRACED_OIDS
       
   120         TRACED_OIDS = None
       
   121         return traceback is None
       
   122 
       
   123 # selector base classes and operations ########################################
       
   124 
       
   125 def objectify_selector(selector_func):
       
   126     """Most of the time, a simple score function is enough to build a selector.
       
   127     The :func:`objectify_selector` decorator turn it into a proper selector
       
   128     class::
       
   129 
       
   130         @objectify_selector
       
   131         def one(cls, req, rset=None, **kwargs):
       
   132             return 1
       
   133 
       
   134         class MyView(View):
       
   135             __select__ = View.__select__ & one()
       
   136 
       
   137     """
       
   138     return type(selector_func.__name__, (Selector,),
       
   139                 {'__doc__': selector_func.__doc__,
       
   140                  '__call__': lambda self, *a, **kw: selector_func(*a, **kw)})
       
   141 
       
   142 
       
   143 def _instantiate_selector(selector):
       
   144     """ensures `selector` is a `Selector` instance
       
   145 
       
   146     NOTE: This should only be used locally in build___select__()
       
   147     XXX: then, why not do it ??
       
   148     """
       
   149     if isinstance(selector, types.FunctionType):
       
   150         return objectify_selector(selector)()
       
   151     if isinstance(selector, type) and issubclass(selector, Selector):
       
   152         return selector()
       
   153     return selector
       
   154 
       
   155 
       
   156 class Selector(object):
       
   157     """base class for selector classes providing implementation
       
   158     for operators ``&``, ``|`` and  ``~``
       
   159 
       
   160     This class is only here to give access to binary operators, the
       
   161     selector logic itself should be implemented in the __call__ method
       
   162 
       
   163 
       
   164     a selector is called to help choosing the correct object for a
       
   165     particular context by returning a score (`int`) telling how well
       
   166     the class given as first argument apply to the given context.
       
   167 
       
   168     0 score means that the class doesn't apply.
       
   169     """
       
   170 
       
   171     @property
       
   172     def func_name(self):
       
   173         # backward compatibility
       
   174         return self.__class__.__name__
       
   175 
       
   176     def search_selector(self, selector):
       
   177         """search for the given selector, selector instance or tuple of
       
   178         selectors in the selectors tree. Return None if not found.
       
   179         """
       
   180         if self is selector:
       
   181             return self
       
   182         if (isinstance(selector, type) or isinstance(selector, tuple)) and \
       
   183                isinstance(self, selector):
       
   184             return self
       
   185         return None
       
   186 
       
   187     def __str__(self):
       
   188         return self.__class__.__name__
       
   189 
       
   190     def __and__(self, other):
       
   191         return AndSelector(self, other)
       
   192     def __rand__(self, other):
       
   193         return AndSelector(other, self)
       
   194     def __iand__(self, other):
       
   195         return AndSelector(self, other)
       
   196     def __or__(self, other):
       
   197         return OrSelector(self, other)
       
   198     def __ror__(self, other):
       
   199         return OrSelector(other, self)
       
   200     def __ior__(self, other):
       
   201         return OrSelector(self, other)
       
   202 
       
   203     def __invert__(self):
       
   204         return NotSelector(self)
       
   205 
       
   206     # XXX (function | function) or (function & function) not managed yet
       
   207 
       
   208     def __call__(self, cls, *args, **kwargs):
       
   209         return NotImplementedError("selector %s must implement its logic "
       
   210                                    "in its __call__ method" % self.__class__)
       
   211 
       
   212     def __repr__(self):
       
   213         return u'<Selector %s at %x>' % (self.__class__.__name__, id(self))
       
   214 
       
   215 
       
   216 class MultiSelector(Selector):
       
   217     """base class for compound selector classes"""
       
   218 
       
   219     def __init__(self, *selectors):
       
   220         self.selectors = self.merge_selectors(selectors)
       
   221 
       
   222     def __str__(self):
       
   223         return '%s(%s)' % (self.__class__.__name__,
       
   224                            ','.join(str(s) for s in self.selectors))
       
   225 
       
   226     @classmethod
       
   227     def merge_selectors(cls, selectors):
       
   228         """deal with selector instanciation when necessary and merge
       
   229         multi-selectors if possible:
       
   230 
       
   231         AndSelector(AndSelector(sel1, sel2), AndSelector(sel3, sel4))
       
   232         ==> AndSelector(sel1, sel2, sel3, sel4)
       
   233         """
       
   234         merged_selectors = []
       
   235         for selector in selectors:
       
   236             try:
       
   237                 selector = _instantiate_selector(selector)
       
   238             except Exception:
       
   239                 pass
       
   240             #assert isinstance(selector, Selector), selector
       
   241             if isinstance(selector, cls):
       
   242                 merged_selectors += selector.selectors
       
   243             else:
       
   244                 merged_selectors.append(selector)
       
   245         return merged_selectors
       
   246 
       
   247     def search_selector(self, selector):
       
   248         """search for the given selector or selector instance (or tuple of
       
   249         selectors) in the selectors tree. Return None if not found
       
   250         """
       
   251         for childselector in self.selectors:
       
   252             if childselector is selector:
       
   253                 return childselector
       
   254             found = childselector.search_selector(selector)
       
   255             if found is not None:
       
   256                 return found
       
   257         # if not found in children, maybe we are looking for self?
       
   258         return super(MultiSelector, self).search_selector(selector)
       
   259 
       
   260 
       
   261 class AndSelector(MultiSelector):
       
   262     """and-chained selectors (formerly known as chainall)"""
       
   263     @lltrace
       
   264     def __call__(self, cls, *args, **kwargs):
       
   265         score = 0
       
   266         for selector in self.selectors:
       
   267             partscore = selector(cls, *args, **kwargs)
       
   268             if not partscore:
       
   269                 return 0
       
   270             score += partscore
       
   271         return score
       
   272 
       
   273 
       
   274 class OrSelector(MultiSelector):
       
   275     """or-chained selectors (formerly known as chainfirst)"""
       
   276     @lltrace
       
   277     def __call__(self, cls, *args, **kwargs):
       
   278         for selector in self.selectors:
       
   279             partscore = selector(cls, *args, **kwargs)
       
   280             if partscore:
       
   281                 return partscore
       
   282         return 0
       
   283 
       
   284 class NotSelector(Selector):
       
   285     """negation selector"""
       
   286     def __init__(self, selector):
       
   287         self.selector = selector
       
   288 
       
   289     @lltrace
       
   290     def __call__(self, cls, *args, **kwargs):
       
   291         score = self.selector(cls, *args, **kwargs)
       
   292         return int(not score)
       
   293 
       
   294     def __str__(self):
       
   295         return 'NOT(%s)' % self.selector
       
   296 
       
   297 
       
   298 class yes(Selector):
       
   299     """Return the score given as parameter, with a default score of 0.5 so any
       
   300     other selector take precedence.
       
   301 
       
   302     Usually used for appobjects which can be selected whatever the context, or
       
   303     also sometimes to add arbitrary points to a score.
       
   304 
       
   305     Take care, `yes(0)` could be named 'no'...
       
   306     """
       
   307     def __init__(self, score=0.5):
       
   308         self.score = score
       
   309 
       
   310     def __call__(self, *args, **kwargs):
       
   311         return self.score
       
   312 
       
   313 
    54 
   314 # the base class for all appobjects ############################################
    55 # the base class for all appobjects ############################################
   315 
    56 
   316 class AppObject(object):
    57 class AppObject(object):
   317     """This is the base class for CubicWeb application objects which are
    58     """This is the base class for CubicWeb application objects which are
   462     # these are overridden by set_log_methods below
   203     # these are overridden by set_log_methods below
   463     # only defining here to prevent pylint from complaining
   204     # only defining here to prevent pylint from complaining
   464     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
   205     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
   465 
   206 
   466 set_log_methods(AppObject, getLogger('cubicweb.appobject'))
   207 set_log_methods(AppObject, getLogger('cubicweb.appobject'))
       
   208 
       
   209 # defined here to avoid warning on usage on the AppObject class
       
   210 yes = deprecated('[3.15] yes has been moved to logilab.common.registry')(yes)