vregistry.py
changeset 8190 2a3c1b787688
parent 7990 a673d1d9a738
child 8202 517fbaad0e6e
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
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    14 # details.
    14 # details.
    15 #
    15 #
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """
       
    19 * the vregistry handles various types of objects interacting
       
    20   together. The vregistry handles registration of dynamically loaded
       
    21   objects and provides a convenient api to access those objects
       
    22   according to a context
       
    23 
       
    24 * to interact with the vregistry, objects should inherit from the
       
    25   AppObject abstract class
       
    26 
       
    27 * the selection procedure has been generalized by delegating to a
       
    28   selector, which is responsible to score the appobject according to the
       
    29   current state (req, rset, row, col). At the end of the selection, if
       
    30   a appobject class has been found, an instance of this class is
       
    31   returned. The selector is instantiated at appobject registration
       
    32 """
       
    33 
       
    34 __docformat__ = "restructuredtext en"
       
    35 
       
    36 import sys
       
    37 from os import listdir, stat
       
    38 from os.path import dirname, join, realpath, isdir, exists
       
    39 from logging import getLogger
       
    40 from warnings import warn
    18 from warnings import warn
    41 
    19 warn('[3.15] moved to logilab.common.registry', DeprecationWarning, stacklevel=2)
    42 from logilab.common.deprecation import deprecated, class_moved
    20 from logilab.common.registry import *
    43 from logilab.common.logging_ext import set_log_methods
       
    44 
       
    45 from cubicweb import CW_SOFTWARE_ROOT
       
    46 from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject
       
    47 from cubicweb.appobject import AppObject, class_regid
       
    48 
       
    49 
       
    50 def _toload_info(path, extrapath, _toload=None):
       
    51     """return a dictionary of <modname>: <modpath> and an ordered list of
       
    52     (file, module name) to load
       
    53     """
       
    54     from logilab.common.modutils import modpath_from_file
       
    55     if _toload is None:
       
    56         assert isinstance(path, list)
       
    57         _toload = {}, []
       
    58     for fileordir in path:
       
    59         if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
       
    60             subfiles = [join(fileordir, fname) for fname in listdir(fileordir)]
       
    61             _toload_info(subfiles, extrapath, _toload)
       
    62         elif fileordir[-3:] == '.py':
       
    63             modpath = modpath_from_file(fileordir, extrapath)
       
    64             # omit '__init__' from package's name to avoid loading that module
       
    65             # once for each name when it is imported by some other appobject
       
    66             # module. This supposes import in modules are done as::
       
    67             #
       
    68             #   from package import something
       
    69             #
       
    70             # not::
       
    71             #
       
    72             #  from package.__init__ import something
       
    73             #
       
    74             # which seems quite correct.
       
    75             if modpath[-1] == '__init__':
       
    76                 modpath.pop()
       
    77             modname = '.'.join(modpath)
       
    78             _toload[0][modname] = fileordir
       
    79             _toload[1].append((fileordir, modname))
       
    80     return _toload
       
    81 
       
    82 
       
    83 def classid(cls):
       
    84     """returns a unique identifier for an appobject class"""
       
    85     return '%s.%s' % (cls.__module__, cls.__name__)
       
    86 
       
    87 def class_registries(cls, registryname):
       
    88     if registryname:
       
    89         return (registryname,)
       
    90     return cls.__registries__
       
    91 
       
    92 
       
    93 class Registry(dict):
       
    94 
       
    95     def __init__(self, config):
       
    96         super(Registry, self).__init__()
       
    97         self.config = config
       
    98 
       
    99     def __getitem__(self, name):
       
   100         """return the registry (dictionary of class objects) associated to
       
   101         this name
       
   102         """
       
   103         try:
       
   104             return super(Registry, self).__getitem__(name)
       
   105         except KeyError:
       
   106             raise ObjectNotFound(name), None, sys.exc_info()[-1]
       
   107 
       
   108     def initialization_completed(self):
       
   109         for appobjects in self.itervalues():
       
   110             for appobjectcls in appobjects:
       
   111                 appobjectcls.__registered__(self)
       
   112 
       
   113     def register(self, obj, oid=None, clear=False):
       
   114         """base method to add an object in the registry"""
       
   115         assert not '__abstract__' in obj.__dict__
       
   116         oid = oid or class_regid(obj)
       
   117         assert oid
       
   118         if clear:
       
   119             appobjects = self[oid] =  []
       
   120         else:
       
   121             appobjects = self.setdefault(oid, [])
       
   122         assert not obj in appobjects, \
       
   123                'object %s is already registered' % obj
       
   124         appobjects.append(obj)
       
   125 
       
   126     def register_and_replace(self, obj, replaced):
       
   127         # XXXFIXME this is a duplication of unregister()
       
   128         # remove register_and_replace in favor of unregister + register
       
   129         # or simplify by calling unregister then register here
       
   130         if not isinstance(replaced, basestring):
       
   131             replaced = classid(replaced)
       
   132         # prevent from misspelling
       
   133         assert obj is not replaced, 'replacing an object by itself: %s' % obj
       
   134         registered_objs = self.get(class_regid(obj), ())
       
   135         for index, registered in enumerate(registered_objs):
       
   136             if classid(registered) == replaced:
       
   137                 del registered_objs[index]
       
   138                 break
       
   139         else:
       
   140             self.warning('trying to replace an unregistered view %s by %s',
       
   141                          replaced, obj)
       
   142         self.register(obj)
       
   143 
       
   144     def unregister(self, obj):
       
   145         clsid = classid(obj)
       
   146         oid = class_regid(obj)
       
   147         for registered in self.get(oid, ()):
       
   148             # use classid() to compare classes because vreg will probably
       
   149             # have its own version of the class, loaded through execfile
       
   150             if classid(registered) == clsid:
       
   151                 self[oid].remove(registered)
       
   152                 break
       
   153         else:
       
   154             self.warning('can\'t remove %s, no id %s in the registry',
       
   155                          clsid, oid)
       
   156 
       
   157     def all_objects(self):
       
   158         """return a list containing all objects in this registry.
       
   159         """
       
   160         result = []
       
   161         for objs in self.values():
       
   162             result += objs
       
   163         return result
       
   164 
       
   165     # dynamic selection methods ################################################
       
   166 
       
   167     def object_by_id(self, oid, *args, **kwargs):
       
   168         """return object with the `oid` identifier. Only one object is expected
       
   169         to be found.
       
   170 
       
   171         raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
       
   172 
       
   173         raise :exc:`AssertionError` if there is more than one object there
       
   174         """
       
   175         objects = self[oid]
       
   176         assert len(objects) == 1, objects
       
   177         return objects[0](*args, **kwargs)
       
   178 
       
   179     def select(self, __oid, *args, **kwargs):
       
   180         """return the most specific object among those with the given oid
       
   181         according to the given context.
       
   182 
       
   183         raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
       
   184 
       
   185         raise :exc:`NoSelectableObject` if not object apply
       
   186         """
       
   187         obj =  self._select_best(self[__oid], *args, **kwargs)
       
   188         if obj is None:
       
   189             raise NoSelectableObject(args, kwargs, self[__oid] )
       
   190         return obj
       
   191 
       
   192     def select_or_none(self, __oid, *args, **kwargs):
       
   193         """return the most specific object among those with the given oid
       
   194         according to the given context, or None if no object applies.
       
   195         """
       
   196         try:
       
   197             return self.select(__oid, *args, **kwargs)
       
   198         except (NoSelectableObject, ObjectNotFound):
       
   199             return None
       
   200 
       
   201     def possible_objects(self, *args, **kwargs):
       
   202         """return an iterator on possible objects in this registry for the given
       
   203         context
       
   204         """
       
   205         for appobjects in self.itervalues():
       
   206             obj = self._select_best(appobjects,  *args, **kwargs)
       
   207             if obj is None:
       
   208                 continue
       
   209             yield obj
       
   210 
       
   211     def _select_best(self, appobjects, *args, **kwargs):
       
   212         """return an instance of the most specific object according
       
   213         to parameters
       
   214 
       
   215         return None if not object apply (don't raise `NoSelectableObject` since
       
   216         it's costly when searching appobjects using `possible_objects`
       
   217         (e.g. searching for hooks).
       
   218         """
       
   219         score, winners = 0, None
       
   220         for appobject in appobjects:
       
   221             appobjectscore = appobject.__select__(appobject, *args, **kwargs)
       
   222             if appobjectscore > score:
       
   223                 score, winners = appobjectscore, [appobject]
       
   224             elif appobjectscore > 0 and appobjectscore == score:
       
   225                 winners.append(appobject)
       
   226         if winners is None:
       
   227             return None
       
   228         if len(winners) > 1:
       
   229             # log in production environement / test, error while debugging
       
   230             msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)'
       
   231             if self.config.debugmode or self.config.mode == 'test':
       
   232                 # raise bare exception in debug mode
       
   233                 raise Exception(msg % (winners, args, kwargs.keys()))
       
   234             self.error(msg, winners, args, kwargs.keys())
       
   235         # return the result of calling the appobject
       
   236         return winners[0](*args, **kwargs)
       
   237 
       
   238     # these are overridden by set_log_methods below
       
   239     # only defining here to prevent pylint from complaining
       
   240     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
       
   241 
       
   242 
       
   243 class VRegistry(dict):
       
   244     """class responsible to register, propose and select the various
       
   245     elements used to build the web interface. Currently, we have templates,
       
   246     views, actions and components.
       
   247     """
       
   248 
       
   249     def __init__(self, config):
       
   250         super(VRegistry, self).__init__()
       
   251         self.config = config
       
   252         # need to clean sys.path this to avoid import confusion pb (i.e.  having
       
   253         # the same module loaded as 'cubicweb.web.views' subpackage and as
       
   254         # views' or 'web.views' subpackage. This is mainly for testing purpose,
       
   255         # we should'nt need this in production environment
       
   256         for webdir in (join(dirname(realpath(__file__)), 'web'),
       
   257                        join(dirname(__file__), 'web')):
       
   258             if webdir in sys.path:
       
   259                 sys.path.remove(webdir)
       
   260         if CW_SOFTWARE_ROOT in sys.path:
       
   261             sys.path.remove(CW_SOFTWARE_ROOT)
       
   262 
       
   263     def reset(self):
       
   264         # don't use self.clear, we want to keep existing subdictionaries
       
   265         for subdict in self.itervalues():
       
   266             subdict.clear()
       
   267         self._lastmodifs = {}
       
   268 
       
   269     def __getitem__(self, name):
       
   270         """return the registry (dictionary of class objects) associated to
       
   271         this name
       
   272         """
       
   273         try:
       
   274             return super(VRegistry, self).__getitem__(name)
       
   275         except KeyError:
       
   276             raise RegistryNotFound(name), None, sys.exc_info()[-1]
       
   277 
       
   278     # methods for explicit (un)registration ###################################
       
   279 
       
   280     # default class, when no specific class set
       
   281     REGISTRY_FACTORY = {None: Registry}
       
   282 
       
   283     def registry_class(self, regid):
       
   284         try:
       
   285             return self.REGISTRY_FACTORY[regid]
       
   286         except KeyError:
       
   287             return self.REGISTRY_FACTORY[None]
       
   288 
       
   289     def setdefault(self, regid):
       
   290         try:
       
   291             return self[regid]
       
   292         except KeyError:
       
   293             self[regid] = self.registry_class(regid)(self.config)
       
   294             return self[regid]
       
   295 
       
   296 #     def clear(self, key):
       
   297 #         regname, oid = key.split('.')
       
   298 #         self[regname].pop(oid, None)
       
   299 
       
   300     def register_all(self, objects, modname, butclasses=()):
       
   301         """register all `objects` given. Objects which are not from the module
       
   302         `modname` or which are in `butclasses` won't be registered.
       
   303 
       
   304         Typical usage is:
       
   305 
       
   306         .. sourcecode:: python
       
   307 
       
   308             vreg.register_all(globals().values(), __name__, (ClassIWantToRegisterExplicitly,))
       
   309 
       
   310         So you get partially automatic registration, keeping manual registration
       
   311         for some object (to use
       
   312         :meth:`~cubicweb.cwvreg.CubicWebRegistry.register_and_replace` for
       
   313         instance)
       
   314         """
       
   315         for obj in objects:
       
   316             try:
       
   317                 if obj.__module__ != modname or obj in butclasses:
       
   318                     continue
       
   319                 oid = class_regid(obj)
       
   320             except AttributeError:
       
   321                 continue
       
   322             if oid and not '__abstract__' in obj.__dict__:
       
   323                 self.register(obj, oid=oid)
       
   324 
       
   325     def register(self, obj, registryname=None, oid=None, clear=False):
       
   326         """register `obj` application object into `registryname` or
       
   327         `obj.__registry__` if not specified, with identifier `oid` or
       
   328         `obj.__regid__` if not specified.
       
   329 
       
   330         If `clear` is true, all objects with the same identifier will be
       
   331         previously unregistered.
       
   332         """
       
   333         assert not '__abstract__' in obj.__dict__
       
   334         try:
       
   335             vname = obj.__name__
       
   336         except AttributeError:
       
   337             # XXX may occurs?
       
   338             vname = obj.__class__.__name__
       
   339         for registryname in class_registries(obj, registryname):
       
   340             registry = self.setdefault(registryname)
       
   341             registry.register(obj, oid=oid, clear=clear)
       
   342             self.debug('register %s in %s[\'%s\']',
       
   343                        vname, registryname, oid or class_regid(obj))
       
   344         self._loadedmods.setdefault(obj.__module__, {})[classid(obj)] = obj
       
   345 
       
   346     def unregister(self, obj, registryname=None):
       
   347         """unregister `obj` application object from the registry `registryname` or
       
   348         `obj.__registry__` if not specified.
       
   349         """
       
   350         for registryname in class_registries(obj, registryname):
       
   351             self[registryname].unregister(obj)
       
   352 
       
   353     def register_and_replace(self, obj, replaced, registryname=None):
       
   354         """register `obj` application object into `registryname` or
       
   355         `obj.__registry__` if not specified. If found, the `replaced` object
       
   356         will be unregistered first (else a warning will be issued as it's
       
   357         generally unexpected).
       
   358         """
       
   359         for registryname in class_registries(obj, registryname):
       
   360             self[registryname].register_and_replace(obj, replaced)
       
   361 
       
   362     # initialization methods ###################################################
       
   363 
       
   364     def init_registration(self, path, extrapath=None):
       
   365         self.reset()
       
   366         # compute list of all modules that have to be loaded
       
   367         self._toloadmods, filemods = _toload_info(path, extrapath)
       
   368         # XXX is _loadedmods still necessary ? It seems like it's useful
       
   369         #     to avoid loading same module twice, especially with the
       
   370         #     _load_ancestors_then_object logic but this needs to be checked
       
   371         self._loadedmods = {}
       
   372         return filemods
       
   373 
       
   374     def register_objects(self, path, extrapath=None):
       
   375         # load views from each directory in the instance's path
       
   376         filemods = self.init_registration(path, extrapath)
       
   377         for filepath, modname in filemods:
       
   378             self.load_file(filepath, modname)
       
   379         self.initialization_completed()
       
   380 
       
   381     def initialization_completed(self):
       
   382         for regname, reg in self.iteritems():
       
   383             reg.initialization_completed()
       
   384 
       
   385     def _mdate(self, filepath):
       
   386         try:
       
   387             return stat(filepath)[-2]
       
   388         except OSError:
       
   389             # this typically happens on emacs backup files (.#foo.py)
       
   390             self.warning('Unable to load %s. It is likely to be a backup file',
       
   391                          filepath)
       
   392             return None
       
   393 
       
   394     def is_reload_needed(self, path):
       
   395         """return True if something module changed and the registry should be
       
   396         reloaded
       
   397         """
       
   398         lastmodifs = self._lastmodifs
       
   399         for fileordir in path:
       
   400             if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
       
   401                 if self.is_reload_needed([join(fileordir, fname)
       
   402                                           for fname in listdir(fileordir)]):
       
   403                     return True
       
   404             elif fileordir[-3:] == '.py':
       
   405                 mdate = self._mdate(fileordir)
       
   406                 if mdate is None:
       
   407                     continue # backup file, see _mdate implementation
       
   408                 elif "flymake" in fileordir:
       
   409                     # flymake + pylint in use, don't consider these they will corrupt the registry
       
   410                     continue
       
   411                 if fileordir not in lastmodifs or lastmodifs[fileordir] < mdate:
       
   412                     self.info('File %s changed since last visit', fileordir)
       
   413                     return True
       
   414         return False
       
   415 
       
   416     def load_file(self, filepath, modname):
       
   417         """load app objects from a python file"""
       
   418         from logilab.common.modutils import load_module_from_name
       
   419         if modname in self._loadedmods:
       
   420             return
       
   421         self._loadedmods[modname] = {}
       
   422         mdate = self._mdate(filepath)
       
   423         if mdate is None:
       
   424             return # backup file, see _mdate implementation
       
   425         elif "flymake" in filepath:
       
   426             # flymake + pylint in use, don't consider these they will corrupt the registry
       
   427             return
       
   428         # set update time before module loading, else we get some reloading
       
   429         # weirdness in case of syntax error or other error while importing the
       
   430         # module
       
   431         self._lastmodifs[filepath] = mdate
       
   432         # load the module
       
   433         module = load_module_from_name(modname)
       
   434         self.load_module(module)
       
   435 
       
   436     def load_module(self, module):
       
   437         self.info('loading %s from %s', module.__name__, module.__file__)
       
   438         if hasattr(module, 'registration_callback'):
       
   439             module.registration_callback(self)
       
   440         else:
       
   441             for objname, obj in vars(module).items():
       
   442                 if objname.startswith('_'):
       
   443                     continue
       
   444                 self._load_ancestors_then_object(module.__name__, obj)
       
   445 
       
   446     def _load_ancestors_then_object(self, modname, appobjectcls):
       
   447         """handle automatic appobject class registration:
       
   448 
       
   449         - first ensure parent classes are already registered
       
   450 
       
   451         - class with __abstract__ == True in their local dictionnary or
       
   452           with a name starting with an underscore are not registered
       
   453 
       
   454         - appobject class needs to have __registry__ and __regid__ attributes
       
   455           set to a non empty string to be registered.
       
   456         """
       
   457         # imported classes
       
   458         objmodname = getattr(appobjectcls, '__module__', None)
       
   459         if objmodname != modname:
       
   460             if objmodname in self._toloadmods:
       
   461                 self.load_file(self._toloadmods[objmodname], objmodname)
       
   462             return
       
   463         # skip non registerable object
       
   464         try:
       
   465             if not issubclass(appobjectcls, AppObject):
       
   466                 return
       
   467         except TypeError:
       
   468             return
       
   469         clsid = classid(appobjectcls)
       
   470         if clsid in self._loadedmods[modname]:
       
   471             return
       
   472         self._loadedmods[modname][clsid] = appobjectcls
       
   473         for parent in appobjectcls.__bases__:
       
   474             self._load_ancestors_then_object(modname, parent)
       
   475         if (appobjectcls.__dict__.get('__abstract__')
       
   476             or appobjectcls.__name__[0] == '_'
       
   477             or not appobjectcls.__registries__
       
   478             or not class_regid(appobjectcls)):
       
   479             return
       
   480         try:
       
   481             self.register(appobjectcls)
       
   482         except Exception, ex:
       
   483             if self.config.mode in ('test', 'dev'):
       
   484                 raise
       
   485             self.exception('appobject %s registration failed: %s',
       
   486                            appobjectcls, ex)
       
   487     # these are overridden by set_log_methods below
       
   488     # only defining here to prevent pylint from complaining
       
   489     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
       
   490 
       
   491 
       
   492 # init logging
       
   493 set_log_methods(VRegistry, getLogger('cubicweb.vreg'))
       
   494 set_log_methods(Registry, getLogger('cubicweb.registry'))
       
   495 
       
   496 
       
   497 # XXX bw compat functions #####################################################
       
   498 
       
   499 from cubicweb.appobject import objectify_selector, AndSelector, OrSelector, Selector
       
   500 
       
   501 Selector = class_moved(Selector)