backported cubicweb's registry. Closes #84654

authorSylvain Th?nault <sylvain.thenault@logilab.fr>
changesetc5ad23f5c0a4
branchdefault
phasepublic
hiddenno
parent revision#33a8ec1687e0 backport stable
child revision#2dddccc63dad testlib: ensure DocTest does not alter __builtins__
files modified by this revision
ChangeLog
registry.py
test/unittest_registry.py
# HG changeset patch
# User Sylvain Thénault <sylvain.thenault@logilab.fr>
# Date 1326375014 -3600
# Thu Jan 12 14:30:14 2012 +0100
# Node ID c5ad23f5c0a4f9d6be41f4c2607618932c8f50d7
# Parent 33a8ec1687e0fc94e28f45043f0fd405307501ec
backported cubicweb's registry. Closes #84654

diff --git a/ChangeLog b/ChangeLog
@@ -1,8 +1,12 @@
1  ChangeLog for logilab.common
2  ============================
3 
4 +	--
5 +    * new `registry` module containing a backport of CubicWeb selectable objects registry (closes #84654)
6 +
7 +
8  2011-10-28  --  0.57.1
9      * daemon: change $HOME after dropping privileges (closes #81297)
10 
11      * compat: method_type for py3k use instance of the class to have a
12        real instance method (closes: #79268)
diff --git a/registry.py b/registry.py
@@ -0,0 +1,958 @@
13 +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
14 +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
15 +#
16 +# This file is part of Logilab-common.
17 +#
18 +# Logilab-common is free software: you can redistribute it and/or modify it
19 +# under the terms of the GNU Lesser General Public License as published by the
20 +# Free Software Foundation, either version 2.1 of the License, or (at your
21 +# option) any later version.
22 +#
23 +# Logilab-common is distributed in the hope that it will be useful, but WITHOUT
24 +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
25 +# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
26 +# details.
27 +#
28 +# You should have received a copy of the GNU Lesser General Public License along
29 +# with Logilab-common.  If not, see <http://www.gnu.org/licenses/>.
30 +"""This module provides bases for predicates dispatching (the pattern in use
31 +here is similar to what's refered as multi-dispatch or predicate-dispatch in the
32 +literature, though a bit different since the idea is to select across different
33 +implementation 'e.g. classes), not to dispatch a message to a function or
34 +method. It contains the following classes:
35 +
36 +* :class:`RegistryStore`, the top level object which loads implementation
37 +  objects and stores them into registries. You'll usually use it to access
38 +  registries and their contained objects;
39 +
40 +* :class:`Registry`, the base class which contains objects semantically grouped
41 +  (for instance, sharing a same API, hence the 'implementation' name). You'll
42 +  use it to select the proper implementation according to a context. Notice you
43 +  may use registries on their own without using the store.
44 +
45 +.. Note::
46 +
47 +  implementation objects are usually designed to be accessed through the
48 +  registry and not by direct instantiation, besides to use it as base classe.
49 +
50 +The selection procedure is delegated to a selector, which is responsible for
51 +scoring the object according to some context. At the end of the selection, if an
52 +implementation has been found, an instance of this class is returned. A selector
53 +is built from one or more predicates combined together using AND, OR, NOT
54 +operators (actually `&`, `|` and `~`). You'll thus find some base classes to
55 +build predicates:
56 +
57 +* :class:`Predicate`, the abstract base predicate class
58 +
59 +* :class:`AndPredicate`, :class:`OrPredicate`, :class:`NotPredicate`, which you
60 +  shouldn't have to use directly. You'll use `&`, `|` and '~' operators between
61 +  predicates directly
62 +
63 +* :func:`objectify_predicate`
64 +
65 +You'll eventually find one concrete predicate: :class:`yes`
66 +
67 +.. autoclass:: RegistryStore
68 +.. autoclass:: Registry
69 +
70 +Predicates
71 +----------
72 +.. autoclass:: Predicate
73 +.. autofunc:: objectify_predicate
74 +.. autoclass:: yes
75 +
76 +Debugging
77 +---------
78 +.. autoclass:: traced_selection
79 +
80 +Exceptions
81 +----------
82 +.. autoclass:: RegistryException
83 +.. autoclass:: RegistryNotFound
84 +.. autoclass:: ObjectNotFound
85 +.. autoclass:: NoSelectableObject
86 +"""
87 +
88 +__docformat__ = "restructuredtext en"
89 +
90 +import sys
91 +import types
92 +import weakref
93 +from os import listdir, stat
94 +from os.path import dirname, join, realpath, isdir, exists
95 +from logging import getLogger
96 +
97 +from logilab.common.decorators import classproperty
98 +from logilab.common.logging_ext import set_log_methods
99 +
100 +
101 +class RegistryException(Exception):
102 +    """Base class for registry exception."""
103 +
104 +class RegistryNotFound(RegistryException):
105 +    """Raised when an unknown registry is requested.
106 +
107 +    This is usually a programming/typo error.
108 +    """
109 +
110 +class ObjectNotFound(RegistryException):
111 +    """Raised when an unregistered object is requested.
112 +
113 +    This may be a programming/typo or a misconfiguration error.
114 +    """
115 +
116 +class NoSelectableObject(RegistryException):
117 +    """Raised when no object is selectable for a given context."""
118 +    def __init__(self, args, kwargs, objects):
119 +        self.args = args
120 +        self.kwargs = kwargs
121 +        self.objects = objects
122 +
123 +    def __str__(self):
124 +        return ('args: %s, kwargs: %s\ncandidates: %s'
125 +                % (self.args, self.kwargs.keys(), self.objects))
126 +
127 +
128 +def _toload_info(path, extrapath, _toload=None):
129 +    """Return a dictionary of <modname>: <modpath> and an ordered list of
130 +    (file, module name) to load
131 +    """
132 +    from logilab.common.modutils import modpath_from_file
133 +    if _toload is None:
134 +        assert isinstance(path, list)
135 +        _toload = {}, []
136 +    for fileordir in path:
137 +        if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
138 +            subfiles = [join(fileordir, fname) for fname in listdir(fileordir)]
139 +            _toload_info(subfiles, extrapath, _toload)
140 +        elif fileordir[-3:] == '.py':
141 +            modpath = modpath_from_file(fileordir, extrapath)
142 +            # omit '__init__' from package's name to avoid loading that module
143 +            # once for each name when it is imported by some other object
144 +            # module. This supposes import in modules are done as::
145 +            #
146 +            #   from package import something
147 +            #
148 +            # not::
149 +            #
150 +            #   from package.__init__ import something
151 +            #
152 +            # which seems quite correct.
153 +            if modpath[-1] == '__init__':
154 +                modpath.pop()
155 +            modname = '.'.join(modpath)
156 +            _toload[0][modname] = fileordir
157 +            _toload[1].append((fileordir, modname))
158 +    return _toload
159 +
160 +
161 +def classid(cls):
162 +    """returns a unique identifier for an object class"""
163 +    return '%s.%s' % (cls.__module__, cls.__name__)
164 +
165 +def class_registries(cls, registryname):
166 +    if registryname:
167 +        return (registryname,)
168 +    return cls.__registries__
169 +
170 +
171 +class Registry(dict):
172 +    """The registry store a set of implementations associated to identifier:
173 +
174 +    * to each identifier are associated a list of implementations
175 +
176 +    * to select an implementation of a given identifier, you should use one of the
177 +      :meth:`select` or :meth:`select_or_none` method
178 +
179 +    * to select a list of implementations for a context, you should use the
180 +      :meth:`possible_objects` method
181 +
182 +    * dictionary like access to an identifier will return the bare list of
183 +      implementations for this identifier.
184 +
185 +    To be usable in a registry, the only requirement is to have a `__select__`
186 +    attribute.
187 +
188 +    At the end of the registration process, the :meth:`__registered__`
189 +    method is called on each registered object which have them, given the
190 +    registry in which it's registered as argument.
191 +
192 +    Registration methods:
193 +
194 +    .. automethod: register
195 +    .. automethod: unregister
196 +
197 +    Selection methods:
198 +
199 +    .. automethod: select
200 +    .. automethod: select_or_none
201 +    .. automethod: possible_objects
202 +    .. automethod: object_by_id
203 +    """
204 +    def __init__(self, debugmode):
205 +        super(Registry, self).__init__()
206 +        self.debugmode = debugmode
207 +
208 +    def __getitem__(self, name):
209 +        """return the registry (list of implementation objects) associated to
210 +        this name
211 +        """
212 +        try:
213 +            return super(Registry, self).__getitem__(name)
214 +        except KeyError:
215 +            raise ObjectNotFound(name), None, sys.exc_info()[-1]
216 +
217 +    def initialization_completed(self):
218 +        for objects in self.itervalues():
219 +            for objectcls in objects:
220 +                registered = getattr(objectcls, '__registered__', None)
221 +                if registered:
222 +                    registered(self)
223 +        if self.debugmode:
224 +            wrap_predicates(_lltrace)
225 +
226 +    def register(self, obj, oid=None, clear=False):
227 +        """base method to add an object in the registry"""
228 +        assert not '__abstract__' in obj.__dict__
229 +        assert obj.__select__
230 +        oid = oid or obj.__regid__
231 +        assert oid
232 +        if clear:
233 +            objects = self[oid] =  []
234 +        else:
235 +            objects = self.setdefault(oid, [])
236 +        assert not obj in objects, \
237 +               'object %s is already registered' % obj
238 +        objects.append(obj)
239 +
240 +    def register_and_replace(self, obj, replaced):
241 +        # XXXFIXME this is a duplication of unregister()
242 +        # remove register_and_replace in favor of unregister + register
243 +        # or simplify by calling unregister then register here
244 +        if not isinstance(replaced, basestring):
245 +            replaced = classid(replaced)
246 +        # prevent from misspelling
247 +        assert obj is not replaced, 'replacing an object by itself: %s' % obj
248 +        registered_objs = self.get(obj.__regid__, ())
249 +        for index, registered in enumerate(registered_objs):
250 +            if classid(registered) == replaced:
251 +                del registered_objs[index]
252 +                break
253 +        else:
254 +            self.warning('trying to replace an unregistered view %s by %s',
255 +                         replaced, obj)
256 +        self.register(obj)
257 +
258 +    def unregister(self, obj):
259 +        clsid = classid(obj)
260 +        oid = obj.__regid__
261 +        for registered in self.get(oid, ()):
262 +            # use classid() to compare classes because vreg will probably
263 +            # have its own version of the class, loaded through execfile
264 +            if classid(registered) == clsid:
265 +                self[oid].remove(registered)
266 +                break
267 +        else:
268 +            self.warning('can\'t remove %s, no id %s in the registry',
269 +                         clsid, oid)
270 +
271 +    def all_objects(self):
272 +        """return a list containing all objects in this registry.
273 +        """
274 +        result = []
275 +        for objs in self.values():
276 +            result += objs
277 +        return result
278 +
279 +    # dynamic selection methods ################################################
280 +
281 +    def object_by_id(self, oid, *args, **kwargs):
282 +        """return object with the `oid` identifier. Only one object is expected
283 +        to be found.
284 +
285 +        raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
286 +
287 +        raise :exc:`AssertionError` if there is more than one object there
288 +        """
289 +        objects = self[oid]
290 +        assert len(objects) == 1, objects
291 +        return objects[0](*args, **kwargs)
292 +
293 +    def select(self, __oid, *args, **kwargs):
294 +        """return the most specific object among those with the given oid
295 +        according to the given context.
296 +
297 +        raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
298 +
299 +        raise :exc:`NoSelectableObject` if not object apply
300 +        """
301 +        obj =  self._select_best(self[__oid], *args, **kwargs)
302 +        if obj is None:
303 +            raise NoSelectableObject(args, kwargs, self[__oid] )
304 +        return obj
305 +
306 +    def select_or_none(self, __oid, *args, **kwargs):
307 +        """return the most specific object among those with the given oid
308 +        according to the given context, or None if no object applies.
309 +        """
310 +        try:
311 +            return self.select(__oid, *args, **kwargs)
312 +        except (NoSelectableObject, ObjectNotFound):
313 +            return None
314 +
315 +    def possible_objects(self, *args, **kwargs):
316 +        """return an iterator on possible objects in this registry for the given
317 +        context
318 +        """
319 +        for objects in self.itervalues():
320 +            obj = self._select_best(objects,  *args, **kwargs)
321 +            if obj is None:
322 +                continue
323 +            yield obj
324 +
325 +    def _select_best(self, objects, *args, **kwargs):
326 +        """return an instance of the most specific object according
327 +        to parameters
328 +
329 +        return None if not object apply (don't raise `NoSelectableObject` since
330 +        it's costly when searching objects using `possible_objects`
331 +        (e.g. searching for hooks).
332 +        """
333 +        score, winners = 0, None
334 +        for object in objects:
335 +            objectscore = object.__select__(object, *args, **kwargs)
336 +            if objectscore > score:
337 +                score, winners = objectscore, [object]
338 +            elif objectscore > 0 and objectscore == score:
339 +                winners.append(object)
340 +        if winners is None:
341 +            return None
342 +        if len(winners) > 1:
343 +            # log in production environement / test, error while debugging
344 +            msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)'
345 +            if self.debugmode:
346 +                # raise bare exception in debug mode
347 +                raise Exception(msg % (winners, args, kwargs.keys()))
348 +            self.error(msg, winners, args, kwargs.keys())
349 +        # return the result of calling the object
350 +        return winners[0](*args, **kwargs)
351 +
352 +    # these are overridden by set_log_methods below
353 +    # only defining here to prevent pylint from complaining
354 +    info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
355 +
356 +
357 +class RegistryStore(dict):
358 +    """This class is responsible for loading implementations and storing them
359 +    in their registry which are created on the fly as needed.
360 +
361 +    It handles dynamic registration of objects and provides a convenient api to
362 +    access them. To be recognized as an object that should be stored into one of
363 +    the store's registry (:class:`Registry`), an object (usually a class) has
364 +    the following attributes, used control how they interact with the registry:
365 +
366 +    :attr:`__registry__` or `__registries__`
367 +      name of the registry for this object (string like 'views', 'templates'...)
368 +      or list of registry names if you want your object to be added to multiple
369 +      registries
370 +
371 +    :attr:`__regid__`
372 +      implementation's identifier in the registry (string like 'main',
373 +      'primary', 'folder_box')
374 +
375 +    :attr:`__select__`
376 +      the implementation's selector
377 +
378 +    Moreover, the :attr:`__abstract__` attribute may be set to `True` to
379 +    indicate that a class is abstract and should not be registered (inherited
380 +    attributes not considered).
381 +
382 +    .. Note::
383 +
384 +      When using the store to load objects dynamically, you *always* have
385 +      to use **super()** to get the methods and attributes of the
386 +      superclasses, and not use the class identifier. Else, you'll get into
387 +      trouble when reloading comes into the place.
388 +
389 +      For example, instead of writing::
390 +
391 +          class Thing(Parent):
392 +              __regid__ = 'athing'
393 +              __select__ = yes()
394 +              def f(self, arg1):
395 +                  Parent.f(self, arg1)
396 +
397 +      You must write::
398 +
399 +          class Thing(Parent):
400 +              __regid__ = 'athing'
401 +              __select__ = yes()
402 +              def f(self, arg1):
403 +                  super(Parent, self).f(arg1)
404 +
405 +    Controlling objects registration
406 +    --------------------------------
407 +
408 +    Dynamic loading is triggered by calling the :meth:`register_objects` method,
409 +    given a list of directory to inspect for python modules.
410 +
411 +    .. automethod: register_objects
412 +
413 +    For each module, by default, all compatible objects are registered
414 +    automatically, though if some objects have to replace other objects, or have
415 +    to be included only if some condition is met, you'll have to define a
416 +    `registration_callback(vreg)` function in your module and explicitly
417 +    register **all objects** in this module, using the api defined below.
418 +
419 +
420 +    .. automethod:: RegistryStore.register_all
421 +    .. automethod:: RegistryStore.register_and_replace
422 +    .. automethod:: RegistryStore.register
423 +    .. automethod:: RegistryStore.unregister
424 +
425 +    .. Note::
426 +        Once the function `registration_callback(vreg)` is implemented in a
427 +        module, all the objects from this module have to be explicitly
428 +        registered as it disables the automatic objects registration.
429 +
430 +
431 +    Examples:
432 +
433 +    .. sourcecode:: python
434 +
435 +       # cubicweb/web/views/basecomponents.py
436 +       def registration_callback(store):
437 +          # register everything in the module except SeeAlsoComponent
438 +          store.register_all(globals().values(), __name__, (SeeAlsoVComponent,))
439 +          # conditionally register SeeAlsoVComponent
440 +          if 'see_also' in store.schema:
441 +              store.register(SeeAlsoVComponent)
442 +
443 +    In this example, we register all application object classes defined in the module
444 +    except `SeeAlsoVComponent`. This class is then registered only if the 'see_also'
445 +    relation type is defined in the instance'schema.
446 +
447 +    .. sourcecode:: python
448 +
449 +       # goa/appobjects/sessions.py
450 +       def registration_callback(store):
451 +          store.register(SessionsCleaner)
452 +          # replace AuthenticationManager by GAEAuthenticationManager
453 +          store.register_and_replace(GAEAuthenticationManager, AuthenticationManager)
454 +          # replace PersistentSessionManager by GAEPersistentSessionManager
455 +          store.register_and_replace(GAEPersistentSessionManager, PersistentSessionManager)
456 +
457 +    In this example, we explicitly register classes one by one:
458 +
459 +    * the `SessionCleaner` class
460 +    * the `GAEAuthenticationManager` to replace the `AuthenticationManager`
461 +    * the `GAEPersistentSessionManager` to replace the `PersistentSessionManager`
462 +
463 +    If at some point we register a new appobject class in this module, it won't be
464 +    registered at all without modification to the `registration_callback`
465 +    implementation. The previous example will register it though, thanks to the call
466 +    to the `register_all` method.
467 +
468 +    Controlling registry instantation
469 +    ---------------------------------
470 +    The `REGISTRY_FACTORY` class dictionary allows to specify which class should
471 +    be instantiated for a given registry name. The class associated to `None` in
472 +    it will be the class used when there is no specific class for a name.
473 +    """
474 +
475 +    def __init__(self, debugmode=False):
476 +        super(RegistryStore, self).__init__()
477 +        self.debugmode = debugmode
478 +
479 +    def reset(self):
480 +        # don't use self.clear, we want to keep existing subdictionaries
481 +        for subdict in self.itervalues():
482 +            subdict.clear()
483 +        self._lastmodifs = {}
484 +
485 +    def __getitem__(self, name):
486 +        """return the registry (dictionary of class objects) associated to
487 +        this name
488 +        """
489 +        try:
490 +            return super(RegistryStore, self).__getitem__(name)
491 +        except KeyError:
492 +            raise RegistryNotFound(name), None, sys.exc_info()[-1]
493 +
494 +    # methods for explicit (un)registration ###################################
495 +
496 +    # default class, when no specific class set
497 +    REGISTRY_FACTORY = {None: Registry}
498 +
499 +    def registry_class(self, regid):
500 +        try:
501 +            return self.REGISTRY_FACTORY[regid]
502 +        except KeyError:
503 +            return self.REGISTRY_FACTORY[None]
504 +
505 +    def setdefault(self, regid):
506 +        try:
507 +            return self[regid]
508 +        except KeyError:
509 +            self[regid] = self.registry_class(regid)(self.debugmode)
510 +            return self[regid]
511 +
512 +    def register_all(self, objects, modname, butclasses=()):
513 +        """register all `objects` given. Objects which are not from the module
514 +        `modname` or which are in `butclasses` won't be registered.
515 +
516 +        Typical usage is:
517 +
518 +        .. sourcecode:: python
519 +
520 +            store.register_all(globals().values(), __name__, (ClassIWantToRegisterExplicitly,))
521 +
522 +        So you get partially automatic registration, keeping manual registration
523 +        for some object (to use
524 +        :meth:`~logilab.common.registry.RegistryStore.register_and_replace`
525 +        for instance)
526 +        """
527 +        for obj in objects:
528 +            try:
529 +                if obj.__module__ != modname or obj in butclasses:
530 +                    continue
531 +                oid = obj.__regid__
532 +            except AttributeError:
533 +                continue
534 +            if oid and not obj.__dict__.get('__abstract__'):
535 +                self.register(obj, oid=oid)
536 +
537 +    def register(self, obj, registryname=None, oid=None, clear=False):
538 +        """register `obj` implementation into `registryname` or
539 +        `obj.__registry__` if not specified, with identifier `oid` or
540 +        `obj.__regid__` if not specified.
541 +
542 +        If `clear` is true, all objects with the same identifier will be
543 +        previously unregistered.
544 +        """
545 +        assert not obj.__dict__.get('__abstract__')
546 +        try:
547 +            vname = obj.__name__
548 +        except AttributeError:
549 +            # XXX may occurs?
550 +            vname = obj.__class__.__name__
551 +        for registryname in class_registries(obj, registryname):
552 +            registry = self.setdefault(registryname)
553 +            registry.register(obj, oid=oid, clear=clear)
554 +            self.debug('register %s in %s[\'%s\']',
555 +                       vname, registryname, oid or obj.__regid__)
556 +        self._loadedmods.setdefault(obj.__module__, {})[classid(obj)] = obj
557 +
558 +    def unregister(self, obj, registryname=None):
559 +        """unregister `obj` implementation object from the registry
560 +        `registryname` or `obj.__registry__` if not specified.
561 +        """
562 +        for registryname in class_registries(obj, registryname):
563 +            self[registryname].unregister(obj)
564 +
565 +    def register_and_replace(self, obj, replaced, registryname=None):
566 +        """register `obj` implementation object into `registryname` or
567 +        `obj.__registry__` if not specified. If found, the `replaced` object
568 +        will be unregistered first (else a warning will be issued as it's
569 +        generally unexpected).
570 +        """
571 +        for registryname in class_registries(obj, registryname):
572 +            self[registryname].register_and_replace(obj, replaced)
573 +
574 +    # initialization methods ###################################################
575 +
576 +    def init_registration(self, path, extrapath=None):
577 +        self.reset()
578 +        # compute list of all modules that have to be loaded
579 +        self._toloadmods, filemods = _toload_info(path, extrapath)
580 +        # XXX is _loadedmods still necessary ? It seems like it's useful
581 +        #     to avoid loading same module twice, especially with the
582 +        #     _load_ancestors_then_object logic but this needs to be checked
583 +        self._loadedmods = {}
584 +        return filemods
585 +
586 +    def register_objects(self, path, extrapath=None):
587 +        # load views from each directory in the instance's path
588 +        filemods = self.init_registration(path, extrapath)
589 +        for filepath, modname in filemods:
590 +            self.load_file(filepath, modname)
591 +        self.initialization_completed()
592 +
593 +    def initialization_completed(self):
594 +        for regname, reg in self.iteritems():
595 +            reg.initialization_completed()
596 +
597 +    def _mdate(self, filepath):
598 +        try:
599 +            return stat(filepath)[-2]
600 +        except OSError:
601 +            # this typically happens on emacs backup files (.#foo.py)
602 +            self.warning('Unable to load %s. It is likely to be a backup file',
603 +                         filepath)
604 +            return None
605 +
606 +    def is_reload_needed(self, path):
607 +        """return True if something module changed and the registry should be
608 +        reloaded
609 +        """
610 +        lastmodifs = self._lastmodifs
611 +        for fileordir in path:
612 +            if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
613 +                if self.is_reload_needed([join(fileordir, fname)
614 +                                          for fname in listdir(fileordir)]):
615 +                    return True
616 +            elif fileordir[-3:] == '.py':
617 +                mdate = self._mdate(fileordir)
618 +                if mdate is None:
619 +                    continue # backup file, see _mdate implementation
620 +                elif "flymake" in fileordir:
621 +                    # flymake + pylint in use, don't consider these they will corrupt the registry
622 +                    continue
623 +                if fileordir not in lastmodifs or lastmodifs[fileordir] < mdate:
624 +                    self.info('File %s changed since last visit', fileordir)
625 +                    return True
626 +        return False
627 +
628 +    def load_file(self, filepath, modname):
629 +        """load app objects from a python file"""
630 +        from logilab.common.modutils import load_module_from_name
631 +        if modname in self._loadedmods:
632 +            return
633 +        self._loadedmods[modname] = {}
634 +        mdate = self._mdate(filepath)
635 +        if mdate is None:
636 +            return # backup file, see _mdate implementation
637 +        elif "flymake" in filepath:
638 +            # flymake + pylint in use, don't consider these they will corrupt the registry
639 +            return
640 +        # set update time before module loading, else we get some reloading
641 +        # weirdness in case of syntax error or other error while importing the
642 +        # module
643 +        self._lastmodifs[filepath] = mdate
644 +        # load the module
645 +        module = load_module_from_name(modname)
646 +        self.load_module(module)
647 +
648 +    def load_module(self, module):
649 +        self.info('loading %s from %s', module.__name__, module.__file__)
650 +        if hasattr(module, 'registration_callback'):
651 +            module.registration_callback(self)
652 +        else:
653 +            for objname, obj in vars(module).items():
654 +                if objname.startswith('_'):
655 +                    continue
656 +                self._load_ancestors_then_object(module.__name__, obj)
657 +
658 +    def _load_ancestors_then_object(self, modname, objectcls):
659 +        """handle automatic object class registration:
660 +
661 +        - first ensure parent classes are already registered
662 +
663 +        - class with __abstract__ == True in their local dictionary or
664 +          with a name starting with an underscore are not registered
665 +
666 +        - object class needs to have __registry__ and __regid__ attributes
667 +          set to a non empty string to be registered.
668 +        """
669 +        # imported classes
670 +        objmodname = getattr(objectcls, '__module__', None)
671 +        if objmodname != modname:
672 +            if objmodname in self._toloadmods:
673 +                self.load_file(self._toloadmods[objmodname], objmodname)
674 +            return
675 +        # skip non registerable object
676 +        try:
677 +            if not (getattr(objectcls, '__regid__', None)
678 +                    and getattr(objectcls, '__select__', None)):
679 +                return
680 +        except TypeError:
681 +            return
682 +        clsid = classid(objectcls)
683 +        if clsid in self._loadedmods[modname]:
684 +            return
685 +        self._loadedmods[modname][clsid] = objectcls
686 +        for parent in objectcls.__bases__:
687 +            self._load_ancestors_then_object(modname, parent)
688 +        if (objectcls.__dict__.get('__abstract__')
689 +            or objectcls.__name__[0] == '_'
690 +            or not objectcls.__registries__
691 +            or not objectcls.__regid__):
692 +            return
693 +        try:
694 +            self.register(objectcls)
695 +        except Exception, ex:
696 +            if self.debugmode:
697 +                raise
698 +            self.exception('object %s registration failed: %s',
699 +                           objectcls, ex)
700 +
701 +    # these are overridden by set_log_methods below
702 +    # only defining here to prevent pylint from complaining
703 +    info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
704 +
705 +
706 +# init logging
707 +set_log_methods(RegistryStore, getLogger('registry.store'))
708 +set_log_methods(Registry, getLogger('registry'))
709 +
710 +
711 +# helpers for debugging selectors
712 +TRACED_OIDS = None
713 +
714 +def _trace_selector(cls, selector, args, ret):
715 +    vobj = args[0]
716 +    if TRACED_OIDS == 'all' or vobj.__regid__ in TRACED_OIDS:
717 +        print '%s -> %s for %s(%s)' % (cls, ret, vobj, vobj.__regid__)
718 +
719 +def _lltrace(selector):
720 +    """use this decorator on your predicates so they become traceable with
721 +    :class:`traced_selection`
722 +    """
723 +    def traced(cls, *args, **kwargs):
724 +        ret = selector(cls, *args, **kwargs)
725 +        if TRACED_OIDS is not None:
726 +            _trace_selector(cls, selector, args, ret)
727 +        return ret
728 +    traced.__name__ = selector.__name__
729 +    traced.__doc__ = selector.__doc__
730 +    return traced
731 +
732 +class traced_selection(object):
733 +    """
734 +    Typical usage is :
735 +
736 +    .. sourcecode:: python
737 +
738 +        >>> from logilab.common.registry import traced_selection
739 +        >>> with traced_selection():
740 +        ...     # some code in which you want to debug selectors
741 +        ...     # for all objects
742 +
743 +    Don't forget the 'from __future__ import with_statement' at the module top-level
744 +    if you're using python prior to 2.6.
745 +
746 +    This will yield lines like this in the logs::
747 +
748 +        selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
749 +
750 +    You can also give to :class:`traced_selection` the identifiers of objects on
751 +    which you want to debug selection ('oid1' and 'oid2' in the example above).
752 +
753 +    .. sourcecode:: python
754 +
755 +        >>> with traced_selection( ('regid1', 'regid2') ):
756 +        ...     # some code in which you want to debug selectors
757 +        ...     # for objects with __regid__ 'regid1' and 'regid2'
758 +
759 +    A potentially useful point to set up such a tracing function is
760 +    the `logilab.common.registry.Registry.select` method body.
761 +    """
762 +
763 +    def __init__(self, traced='all'):
764 +        self.traced = traced
765 +
766 +    def __enter__(self):
767 +        global TRACED_OIDS
768 +        TRACED_OIDS = self.traced
769 +
770 +    def __exit__(self, exctype, exc, traceback):
771 +        global TRACED_OIDS
772 +        TRACED_OIDS = None
773 +        return traceback is None
774 +
775 +# selector base classes and operations ########################################
776 +
777 +def objectify_predicate(selector_func):
778 +    """Most of the time, a simple score function is enough to build a selector.
779 +    The :func:`objectify_predicate` decorator turn it into a proper selector
780 +    class::
781 +
782 +        @objectify_predicate
783 +        def one(cls, req, rset=None, **kwargs):
784 +            return 1
785 +
786 +        class MyView(View):
787 +            __select__ = View.__select__ & one()
788 +
789 +    """
790 +    return type(selector_func.__name__, (Predicate,),
791 +                {'__doc__': selector_func.__doc__,
792 +                 '__call__': lambda self, *a, **kw: selector_func(*a, **kw)})
793 +
794 +
795 +_PREDICATES = {}
796 +
797 +def wrap_predicates(decorator):
798 +    for predicate in _PREDICATES.itervalues():
799 +        if not '_decorators' in predicate.__dict__:
800 +            predicate._decorators = set()
801 +        if decorator in predicate._decorators:
802 +            continue
803 +        predicate._decorators.add(decorator)
804 +        predicate.__call__ = decorator(predicate.__call__)
805 +
806 +class PredicateMetaClass(type):
807 +    def __new__(cls, *args, **kwargs):
808 +        # use __new__ so subclasses doesn't have to call Predicate.__init__
809 +        inst = type.__new__(cls, *args, **kwargs)
810 +        proxy = weakref.proxy(inst, lambda p: _PREDICATES.pop(id(p)))
811 +        _PREDICATES[id(proxy)] = proxy
812 +        return inst
813 +
814 +class Predicate(object):
815 +    """base class for selector classes providing implementation
816 +    for operators ``&``, ``|`` and  ``~``
817 +
818 +    This class is only here to give access to binary operators, the selector
819 +    logic itself should be implemented in the :meth:`__call__` method. Notice it
820 +    should usually accept any arbitrary arguments (the context), though that may
821 +    vary depending on your usage of the registry.
822 +
823 +    a selector is called to help choosing the correct object for a
824 +    particular context by returning a score (`int`) telling how well
825 +    the implementation given as first argument fit to the given context.
826 +
827 +    0 score means that the class doesn't apply.
828 +    """
829 +    __metaclass__ = PredicateMetaClass
830 +
831 +    @property
832 +    def func_name(self):
833 +        # backward compatibility
834 +        return self.__class__.__name__
835 +
836 +    def search_selector(self, selector):
837 +        """search for the given selector, selector instance or tuple of
838 +        selectors in the selectors tree. Return None if not found.
839 +        """
840 +        if self is selector:
841 +            return self
842 +        if (isinstance(selector, type) or isinstance(selector, tuple)) and \
843 +               isinstance(self, selector):
844 +            return self
845 +        return None
846 +
847 +    def __str__(self):
848 +        return self.__class__.__name__
849 +
850 +    def __and__(self, other):
851 +        return AndPredicate(self, other)
852 +    def __rand__(self, other):
853 +        return AndPredicate(other, self)
854 +    def __iand__(self, other):
855 +        return AndPredicate(self, other)
856 +    def __or__(self, other):
857 +        return OrPredicate(self, other)
858 +    def __ror__(self, other):
859 +        return OrPredicate(other, self)
860 +    def __ior__(self, other):
861 +        return OrPredicate(self, other)
862 +
863 +    def __invert__(self):
864 +        return NotPredicate(self)
865 +
866 +    # XXX (function | function) or (function & function) not managed yet
867 +
868 +    def __call__(self, cls, *args, **kwargs):
869 +        return NotImplementedError("selector %s must implement its logic "
870 +                                   "in its __call__ method" % self.__class__)
871 +
872 +    def __repr__(self):
873 +        return u'<Predicate %s at %x>' % (self.__class__.__name__, id(self))
874 +
875 +
876 +class MultiPredicate(Predicate):
877 +    """base class for compound selector classes"""
878 +
879 +    def __init__(self, *selectors):
880 +        self.selectors = self.merge_selectors(selectors)
881 +
882 +    def __str__(self):
883 +        return '%s(%s)' % (self.__class__.__name__,
884 +                           ','.join(str(s) for s in self.selectors))
885 +
886 +    @classmethod
887 +    def merge_selectors(cls, selectors):
888 +        """deal with selector instanciation when necessary and merge
889 +        multi-selectors if possible:
890 +
891 +        AndPredicate(AndPredicate(sel1, sel2), AndPredicate(sel3, sel4))
892 +        ==> AndPredicate(sel1, sel2, sel3, sel4)
893 +        """
894 +        merged_selectors = []
895 +        for selector in selectors:
896 +            # XXX do we really want magic-transformations below?
897 +            # if so, wanna warn about them?
898 +            if isinstance(selector, types.FunctionType):
899 +                selector = objectify_predicate(selector)()
900 +            if isinstance(selector, type) and issubclass(selector, Predicate):
901 +                selector = selector()
902 +            assert isinstance(selector, Predicate), selector
903 +            if isinstance(selector, cls):
904 +                merged_selectors += selector.selectors
905 +            else:
906 +                merged_selectors.append(selector)
907 +        return merged_selectors
908 +
909 +    def search_selector(self, selector):
910 +        """search for the given selector or selector instance (or tuple of
911 +        selectors) in the selectors tree. Return None if not found
912 +        """
913 +        for childselector in self.selectors:
914 +            if childselector is selector:
915 +                return childselector
916 +            found = childselector.search_selector(selector)
917 +            if found is not None:
918 +                return found
919 +        # if not found in children, maybe we are looking for self?
920 +        return super(MultiPredicate, self).search_selector(selector)
921 +
922 +
923 +class AndPredicate(MultiPredicate):
924 +    """and-chained selectors"""
925 +    def __call__(self, cls, *args, **kwargs):
926 +        score = 0
927 +        for selector in self.selectors:
928 +            partscore = selector(cls, *args, **kwargs)
929 +            if not partscore:
930 +                return 0
931 +            score += partscore
932 +        return score
933 +
934 +
935 +class OrPredicate(MultiPredicate):
936 +    """or-chained selectors"""
937 +    def __call__(self, cls, *args, **kwargs):
938 +        for selector in self.selectors:
939 +            partscore = selector(cls, *args, **kwargs)
940 +            if partscore:
941 +                return partscore
942 +        return 0
943 +
944 +class NotPredicate(Predicate):
945 +    """negation selector"""
946 +    def __init__(self, selector):
947 +        self.selector = selector
948 +
949 +    def __call__(self, cls, *args, **kwargs):
950 +        score = self.selector(cls, *args, **kwargs)
951 +        return int(not score)
952 +
953 +    def __str__(self):
954 +        return 'NOT(%s)' % self.selector
955 +
956 +
957 +class yes(Predicate):
958 +    """Return the score given as parameter, with a default score of 0.5 so any
959 +    other selector take precedence.
960 +
961 +    Usually used for objects which can be selected whatever the context, or
962 +    also sometimes to add arbitrary points to a score.
963 +
964 +    Take care, `yes(0)` could be named 'no'...
965 +    """
966 +    def __init__(self, score=0.5):
967 +        self.score = score
968 +
969 +    def __call__(self, *args, **kwargs):
970 +        return self.score
diff --git a/test/unittest_registry.py b/test/unittest_registry.py
@@ -0,0 +1,163 @@
971 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
972 +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
973 +#
974 +# This file is part of Logilab-Common.
975 +#
976 +# Logilab-Common is free software: you can redistribute it and/or modify it under the
977 +# terms of the GNU Lesser General Public License as published by the Free
978 +# Software Foundation, either version 2.1 of the License, or (at your option)
979 +# any later version.
980 +#
981 +# Logilab-Common is distributed in the hope that it will be useful, but WITHOUT
982 +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
983 +# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
984 +# details.
985 +#
986 +# You should have received a copy of the GNU Lesser General Public License along
987 +# with Logilab-Common.  If not, see <http://www.gnu.org/licenses/>.
988 +"""unit tests for selectors mechanism"""
989 +from __future__ import with_statement
990 +
991 +import gc
992 +from operator import eq, lt, le, gt
993 +from logilab.common.testlib import TestCase, unittest_main
994 +
995 +from logilab.common.registry import Predicate, AndPredicate, OrPredicate, wrap_predicates
996 +
997 +
998 +class _1_(Predicate):
999 +    def __call__(self, *args, **kwargs):
1000 +        return 1
1001 +
1002 +class _0_(Predicate):
1003 +    def __call__(self, *args, **kwargs):
1004 +        return 0
1005 +
1006 +def _2_(*args, **kwargs):
1007 +    return 2
1008 +
1009 +
1010 +class SelectorsTC(TestCase):
1011 +    def test_basic_and(self):
1012 +        selector = _1_() & _1_()
1013 +        self.assertEqual(selector(None), 2)
1014 +        selector = _1_() & _0_()
1015 +        self.assertEqual(selector(None), 0)
1016 +        selector = _0_() & _1_()
1017 +        self.assertEqual(selector(None), 0)
1018 +
1019 +    def test_basic_or(self):
1020 +        selector = _1_() | _1_()
1021 +        self.assertEqual(selector(None), 1)
1022 +        selector = _1_() | _0_()
1023 +        self.assertEqual(selector(None), 1)
1024 +        selector = _0_() | _1_()
1025 +        self.assertEqual(selector(None), 1)
1026 +        selector = _0_() | _0_()
1027 +        self.assertEqual(selector(None), 0)
1028 +
1029 +    def test_selector_and_function(self):
1030 +        selector = _1_() & _2_
1031 +        self.assertEqual(selector(None), 3)
1032 +        selector = _2_ & _1_()
1033 +        self.assertEqual(selector(None), 3)
1034 +
1035 +    def test_three_and(self):
1036 +        selector = _1_() & _1_() & _1_()
1037 +        self.assertEqual(selector(None), 3)
1038 +        selector = _1_() & _0_() & _1_()
1039 +        self.assertEqual(selector(None), 0)
1040 +        selector = _0_() & _1_() & _1_()
1041 +        self.assertEqual(selector(None), 0)
1042 +
1043 +    def test_three_or(self):
1044 +        selector = _1_() | _1_() | _1_()
1045 +        self.assertEqual(selector(None), 1)
1046 +        selector = _1_() | _0_() | _1_()
1047 +        self.assertEqual(selector(None), 1)
1048 +        selector = _0_() | _1_() | _1_()
1049 +        self.assertEqual(selector(None), 1)
1050 +        selector = _0_() | _0_() | _0_()
1051 +        self.assertEqual(selector(None), 0)
1052 +
1053 +    def test_composition(self):
1054 +        selector = (_1_() & _1_()) & (_1_() & _1_())
1055 +        self.assertTrue(isinstance(selector, AndPredicate))
1056 +        self.assertEqual(len(selector.selectors), 4)
1057 +        self.assertEqual(selector(None), 4)
1058 +        selector = (_1_() & _0_()) | (_1_() & _1_())
1059 +        self.assertTrue(isinstance(selector, OrPredicate))
1060 +        self.assertEqual(len(selector.selectors), 2)
1061 +        self.assertEqual(selector(None), 2)
1062 +
1063 +    def test_search_selectors(self):
1064 +        sel = _1_()
1065 +        self.assertIs(sel.search_selector(_1_), sel)
1066 +        csel = AndPredicate(sel, Predicate())
1067 +        self.assertIs(csel.search_selector(_1_), sel)
1068 +        csel = AndPredicate(Predicate(), sel)
1069 +        self.assertIs(csel.search_selector(_1_), sel)
1070 +        self.assertIs(csel.search_selector((AndPredicate, OrPredicate)), csel)
1071 +        self.assertIs(csel.search_selector((OrPredicate, AndPredicate)), csel)
1072 +        self.assertIs(csel.search_selector((_1_, _0_)),  sel)
1073 +        self.assertIs(csel.search_selector((_0_, _1_)), sel)
1074 +
1075 +    def test_inplace_and(self):
1076 +        selector = _1_()
1077 +        selector &= _1_()
1078 +        selector &= _1_()
1079 +        self.assertEqual(selector(None), 3)
1080 +        selector = _1_()
1081 +        selector &= _0_()
1082 +        selector &= _1_()
1083 +        self.assertEqual(selector(None), 0)
1084 +        selector = _0_()
1085 +        selector &= _1_()
1086 +        selector &= _1_()
1087 +        self.assertEqual(selector(None), 0)
1088 +        selector = _0_()
1089 +        selector &= _0_()
1090 +        selector &= _0_()
1091 +        self.assertEqual(selector(None), 0)
1092 +
1093 +    def test_inplace_or(self):
1094 +        selector = _1_()
1095 +        selector |= _1_()
1096 +        selector |= _1_()
1097 +        self.assertEqual(selector(None), 1)
1098 +        selector = _1_()
1099 +        selector |= _0_()
1100 +        selector |= _1_()
1101 +        self.assertEqual(selector(None), 1)
1102 +        selector = _0_()
1103 +        selector |= _1_()
1104 +        selector |= _1_()
1105 +        self.assertEqual(selector(None), 1)
1106 +        selector = _0_()
1107 +        selector |= _0_()
1108 +        selector |= _0_()
1109 +        self.assertEqual(selector(None), 0)
1110 +
1111 +    def test_wrap_selectors(self):
1112 +        class _temp_(Predicate):
1113 +            def __call__(self, *args, **kwargs):
1114 +                return 0
1115 +        del _temp_ # test weakref
1116 +        s1 = _1_() & _1_()
1117 +        s2 = _1_() & _0_()
1118 +        s3 = _0_() & _1_()
1119 +        gc.collect()
1120 +        self.count = 0
1121 +        def decorate(f, self=self):
1122 +            def wrapper(*args, **kwargs):
1123 +                self.count += 1
1124 +                return f(*args, **kwargs)
1125 +            return wrapper
1126 +        wrap_predicates(decorate)
1127 +        self.assertEqual(s1(None), 2)
1128 +        self.assertEqual(s2(None), 0)
1129 +        self.assertEqual(s3(None), 0)
1130 +        self.assertEqual(self.count, 8)
1131 +
1132 +if __name__ == '__main__':
1133 +    unittest_main()