vregistry.py
changeset 0 b97547f5f1fa
child 177 73aa03734425
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """
       
     2 * the vregistry handle various type of objects interacting
       
     3   together. The vregistry handle registration of dynamically loaded
       
     4   objects and provide a convenient api access to those objects
       
     5   according to a context
       
     6 
       
     7 * to interact with the vregistry, object should inherit from the
       
     8   VObject abstract class
       
     9   
       
    10 * the registration procedure is delegated to a registerer. Each
       
    11   registerable vobject must defines its registerer class using the
       
    12   __registerer__ attribute.  A registerer is instantianted at
       
    13   registration time after what the instance is lost
       
    14   
       
    15 * the selection procedure has been generalized by delegating to a
       
    16   selector, which is responsible to score the vobject according to the
       
    17   current state (req, rset, row, col). At the end of the selection, if
       
    18   a vobject class has been found, an instance of this class is
       
    19   returned. The selector is instantiated at vobject registration
       
    20 
       
    21 
       
    22 :organization: Logilab
       
    23 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
    24 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
    25 """
       
    26 __docformat__ = "restructuredtext en"
       
    27 
       
    28 import sys
       
    29 from os import listdir, stat
       
    30 from os.path import dirname, join, realpath, split, isdir
       
    31 from logging import getLogger
       
    32 
       
    33 from cubicweb import CW_SOFTWARE_ROOT, set_log_methods
       
    34 from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject
       
    35 
       
    36 
       
    37 class vobject_helper(object):
       
    38     """object instantiated at registration time to help a wrapped
       
    39     VObject subclass
       
    40     """
       
    41 
       
    42     def __init__(self, registry, vobject):
       
    43         self.registry = registry
       
    44         self.vobject = vobject
       
    45         self.config = registry.config
       
    46         self.schema = registry.schema
       
    47 
       
    48 
       
    49 class registerer(vobject_helper):
       
    50     """do whatever is needed at registration time for the wrapped
       
    51     class, according to current application schema and already
       
    52     registered objects of the same kind (i.e. same registry name and
       
    53     same id).
       
    54 
       
    55     The wrapped class may be skipped, some previously selected object
       
    56     may be kicked out... After whatever works needed, if the object or
       
    57     a transformed object is returned, it will be added to previously
       
    58     registered objects.
       
    59     """
       
    60 
       
    61     def __init__(self, registry, vobject):
       
    62         super(registerer, self).__init__(registry, vobject)
       
    63         self.kicked = set()
       
    64     
       
    65     def do_it_yourself(self, registered):
       
    66         raise NotImplementedError(str(self.vobject))
       
    67         
       
    68     def kick(self, registered, kicked):
       
    69         self.debug('kicking vobject %s', kicked)
       
    70         registered.remove(kicked)
       
    71         self.kicked.add(kicked.classid())
       
    72         
       
    73     def skip(self):
       
    74         self.debug('no schema compat, skipping %s', self.vobject)
       
    75 
       
    76 
       
    77 def selector(cls, *args, **kwargs):
       
    78     """selector is called to help choosing the correct object for a
       
    79     particular request and result set by returning a score.
       
    80 
       
    81     it must implement a .score_method taking a request, a result set and
       
    82     optionaly row and col arguments which return an int telling how well
       
    83     the wrapped class apply to the given request and result set. 0 score
       
    84     means that it doesn't apply.
       
    85     
       
    86     rset may be None. If not, row and col arguments may be optionally
       
    87     given if the registry is scoring a given row or a given cell of
       
    88     the result set (both row and col are int if provided).
       
    89     """    
       
    90     raise NotImplementedError(cls)
       
    91 
       
    92 
       
    93 class autoselectors(type):
       
    94     """implements __selectors__ / __select__ compatibility layer so that:
       
    95 
       
    96     __select__ = chainall(classmethod(A, B, C))
       
    97 
       
    98     can be replaced by something like:
       
    99     
       
   100     __selectors__ = (A, B, C)
       
   101     """
       
   102     def __new__(mcs, name, bases, classdict):
       
   103         if '__select__' in classdict and '__selectors__' in classdict:
       
   104             raise TypeError("__select__ and __selectors__ "
       
   105                             "can't be used together")
       
   106         if '__select__' not in classdict and '__selectors__' in classdict:
       
   107             selectors = classdict['__selectors__']
       
   108             classdict['__select__'] = classmethod(chainall(*selectors))
       
   109         return super(autoselectors, mcs).__new__(mcs, name, bases, classdict)
       
   110 
       
   111     def __setattr__(self, attr, value):
       
   112         if attr == '__selectors__':
       
   113             self.__select__ = classmethod(chainall(*value))
       
   114         super(autoselectors, self).__setattr__(attr, value)
       
   115             
       
   116 
       
   117 class VObject(object):
       
   118     """visual object, use to be handled somehow by the visual components
       
   119     registry.
       
   120 
       
   121     The following attributes should be set on concret vobject subclasses:
       
   122     
       
   123     :__registry__:
       
   124       name of the registry for this object (string like 'views',
       
   125       'templates'...)
       
   126     :id:
       
   127       object's identifier in the registry (string like 'main',
       
   128       'primary', 'folder_box')
       
   129     :__registerer__:
       
   130       registration helper class
       
   131     :__select__:
       
   132       selection helper function
       
   133     :__selectors__:
       
   134       tuple of selectors to be chained
       
   135       (__select__ and __selectors__ are mutually exclusive)
       
   136       
       
   137     Moreover, the `__abstract__` attribute may be set to True to indicate
       
   138     that a vobject is abstract and should not be registered
       
   139     """
       
   140     __metaclass__ = autoselectors
       
   141     # necessary attributes to interact with the registry
       
   142     id = None
       
   143     __registry__ = None
       
   144     __registerer__ = None
       
   145     __select__ = None
       
   146 
       
   147     @classmethod
       
   148     def registered(cls, registry):
       
   149         """called by the registry when the vobject has been registered.
       
   150 
       
   151         It must return the  object that will be actually registered (this
       
   152         may be the right hook to create an instance for example). By
       
   153         default the vobject is returned without any transformation.
       
   154         """
       
   155         return cls
       
   156 
       
   157     @classmethod
       
   158     def selected(cls, *args, **kwargs):
       
   159         """called by the registry when the vobject has been selected.
       
   160         
       
   161         It must return the  object that will be actually returned by the
       
   162         .select method (this may be the right hook to create an
       
   163         instance for example). By default the selected object is
       
   164         returned without any transformation.
       
   165         """
       
   166         return cls
       
   167 
       
   168     @classmethod
       
   169     def classid(cls):
       
   170         """returns a unique identifier for the vobject"""
       
   171         return '%s.%s' % (cls.__module__, cls.__name__)
       
   172 
       
   173 
       
   174 class VRegistry(object):
       
   175     """class responsible to register, propose and select the various
       
   176     elements used to build the web interface. Currently, we have templates,
       
   177     views, actions and components.
       
   178     """
       
   179     
       
   180     def __init__(self, config):#, cache_size=1000):
       
   181         self.config = config
       
   182         # dictionnary of registry (themself dictionnary) by name
       
   183         self._registries = {}
       
   184         self._lastmodifs = {}
       
   185 
       
   186     def reset(self):
       
   187         self._registries = {}
       
   188         self._lastmodifs = {}
       
   189 
       
   190     def __getitem__(self, key):
       
   191         return self._registries[key]
       
   192 
       
   193     def get(self, key, default=None):
       
   194         return self._registries.get(key, default)
       
   195 
       
   196     def items(self):
       
   197         return self._registries.items()
       
   198 
       
   199     def values(self):
       
   200         return self._registries.values()
       
   201 
       
   202     def __contains__(self, key):
       
   203         return key in self._registries
       
   204         
       
   205     def register_vobject_class(self, cls, _kicked=set()):
       
   206         """handle vobject class registration
       
   207         
       
   208         vobject class with __abstract__ == True in their local dictionnary or
       
   209         with a name starting starting by an underscore are not registered.
       
   210         Also a vobject class needs to have __registry__ and id attributes set
       
   211         to a non empty string to be registered.
       
   212 
       
   213         Registration is actually handled by vobject's registerer.
       
   214         """
       
   215         if (cls.__dict__.get('__abstract__') or cls.__name__[0] == '_'
       
   216             or not cls.__registry__ or not cls.id):
       
   217             return
       
   218         # while reloading a module :
       
   219         # if cls was previously kicked, it means that there is a more specific
       
   220         # vobject defined elsewhere re-registering cls would kick it out
       
   221         if cls.classid() in _kicked:
       
   222             self.debug('not re-registering %s because it was previously kicked',
       
   223                       cls.classid())
       
   224         else:
       
   225             regname = cls.__registry__
       
   226             if cls.id in self.config['disable-%s' % regname]:
       
   227                 return
       
   228             registry = self._registries.setdefault(regname, {})
       
   229             vobjects = registry.setdefault(cls.id, [])
       
   230             registerer = cls.__registerer__(self, cls)
       
   231             cls = registerer.do_it_yourself(vobjects)
       
   232             #_kicked |= registerer.kicked
       
   233             if cls:
       
   234                 vobject = cls.registered(self)
       
   235                 try:
       
   236                     vname = vobject.__name__
       
   237                 except AttributeError:
       
   238                     vname = vobject.__class__.__name__
       
   239                 self.debug('registered vobject %s in registry %s with id %s',
       
   240                           vname, cls.__registry__, cls.id)
       
   241                 vobjects.append(vobject)
       
   242             
       
   243     def unregister_module_vobjects(self, modname):
       
   244         """removes registered objects coming from a given module
       
   245 
       
   246         returns a dictionnary classid/class of all classes that will need
       
   247         to be updated after reload (i.e. vobjects referencing classes defined
       
   248         in the <modname> module)
       
   249         """
       
   250         unregistered = {}
       
   251         # browse each registered object
       
   252         for registry, objdict in self.items():
       
   253             for oid, objects in objdict.items():
       
   254                 for obj in objects[:]:
       
   255                     objname = obj.classid()
       
   256                     # if the vobject is defined in this module, remove it
       
   257                     if objname.startswith(modname):
       
   258                         unregistered[objname] = obj
       
   259                         objects.remove(obj)
       
   260                         self.debug('unregistering %s in %s registry',
       
   261                                   objname, registry)
       
   262                     # if not, check if the vobject can be found in baseclasses
       
   263                     # (because we also want subclasses to be updated)
       
   264                     else:
       
   265                         if not isinstance(obj, type):
       
   266                             obj = obj.__class__
       
   267                         for baseclass in obj.__bases__:
       
   268                             if hasattr(baseclass, 'classid'):
       
   269                                 baseclassid = baseclass.classid()
       
   270                                 if baseclassid.startswith(modname):
       
   271                                     unregistered[baseclassid] = baseclass
       
   272                 # update oid entry
       
   273                 if objects:
       
   274                     objdict[oid] = objects
       
   275                 else:
       
   276                     del objdict[oid]
       
   277         return unregistered
       
   278 
       
   279 
       
   280     def update_registered_subclasses(self, oldnew_mapping):
       
   281         """updates subclasses of re-registered vobjects
       
   282 
       
   283         if baseviews.PrimaryView is changed, baseviews.py will be reloaded
       
   284         automatically and the new version of PrimaryView will be registered.
       
   285         But all existing subclasses must also be notified of this change, and
       
   286         that's what this method does
       
   287 
       
   288         :param oldnew_mapping: a dict mapping old version of a class to
       
   289                                the new version
       
   290         """
       
   291         # browse each registered object
       
   292         for objdict in self.values():
       
   293             for objects in objdict.values():
       
   294                 for obj in objects:
       
   295                     if not isinstance(obj, type):
       
   296                         obj = obj.__class__
       
   297                     # build new baseclasses tuple
       
   298                     newbases = tuple(oldnew_mapping.get(baseclass, baseclass)
       
   299                                      for baseclass in obj.__bases__)
       
   300                     # update obj's baseclasses tuple (__bases__) if needed
       
   301                     if newbases != obj.__bases__:
       
   302                         self.debug('updating %s.%s base classes',
       
   303                                   obj.__module__, obj.__name__)
       
   304                         obj.__bases__ = newbases
       
   305 
       
   306     def registry(self, name):
       
   307         """return the registry (dictionary of class objects) associated to
       
   308         this name
       
   309         """
       
   310         try:
       
   311             return self._registries[name]
       
   312         except KeyError:
       
   313             raise RegistryNotFound(name), None, sys.exc_info()[-1]
       
   314 
       
   315     def registry_objects(self, name, oid=None):
       
   316         """returns objects registered with the given oid in the given registry.
       
   317         If no oid is given, return all objects in this registry
       
   318         """
       
   319         registry = self.registry(name)
       
   320         if oid:
       
   321             try:
       
   322                 return registry[oid]
       
   323             except KeyError:
       
   324                 raise ObjectNotFound(oid), None, sys.exc_info()[-1]
       
   325         else:
       
   326             result = []
       
   327             for objs in registry.values():
       
   328                 result += objs
       
   329             return result
       
   330         
       
   331     def select(self, vobjects, *args, **kwargs):
       
   332         """return an instance of the most specific object according
       
   333         to parameters
       
   334 
       
   335         raise NoSelectableObject if not object apply
       
   336         """
       
   337         score, winner = 0, None
       
   338         for vobject in vobjects:
       
   339             vobjectscore = vobject.__select__(*args, **kwargs)
       
   340             if vobjectscore > score:
       
   341                 score, winner = vobjectscore, vobject
       
   342         if winner is None:
       
   343             raise NoSelectableObject('args: %s\nkwargs: %s %s'
       
   344                                      % (args, kwargs.keys(), [repr(v) for v in vobjects]))
       
   345         # return the result of the .selected method of the vobject
       
   346         return winner.selected(*args, **kwargs)
       
   347     
       
   348     def possible_objects(self, registry, *args, **kwargs):
       
   349         """return an iterator on possible objects in a registry for this result set
       
   350 
       
   351         actions returned are classes, not instances
       
   352         """
       
   353         for vobjects in self.registry(registry).values():
       
   354             try:
       
   355                 yield self.select(vobjects, *args, **kwargs)
       
   356             except NoSelectableObject:
       
   357                 continue
       
   358 
       
   359     def select_object(self, registry, cid, *args, **kwargs):
       
   360         """return the most specific component according to the resultset"""
       
   361         return self.select(self.registry_objects(registry, cid), *args, **kwargs)
       
   362 
       
   363     def object_by_id(self, registry, cid, *args, **kwargs):
       
   364         """return the most specific component according to the resultset"""
       
   365         objects = self[registry][cid]
       
   366         assert len(objects) == 1, objects
       
   367         return objects[0].selected(*args, **kwargs)
       
   368     
       
   369     # intialization methods ###################################################
       
   370 
       
   371     
       
   372     def register_objects(self, path, force_reload=None):
       
   373         if force_reload is None:
       
   374             force_reload = self.config.mode == 'dev'
       
   375         elif not force_reload:
       
   376             # force_reload == False usually mean modules have been reloaded
       
   377             # by another connection, so we want to update the registry
       
   378             # content even if there has been no module content modification
       
   379             self.reset()
       
   380         # need to clean sys.path this to avoid import confusion pb (i.e.
       
   381         # having the same module loaded as 'cubicweb.web.views' subpackage and
       
   382         # as views'  or 'web.views' subpackage
       
   383         # this is mainly for testing purpose, we should'nt need this in
       
   384         # production environment
       
   385         for webdir in (join(dirname(realpath(__file__)), 'web'),
       
   386                        join(dirname(__file__), 'web')):
       
   387             if webdir in sys.path:
       
   388                 sys.path.remove(webdir)
       
   389         if CW_SOFTWARE_ROOT in sys.path:
       
   390             sys.path.remove(CW_SOFTWARE_ROOT)        
       
   391         # load views from each directory in the application's path
       
   392         change = False
       
   393         for fileordirectory in path:
       
   394             if isdir(fileordirectory):
       
   395                 if self.read_directory(fileordirectory, force_reload):
       
   396                     change = True
       
   397             else:
       
   398                 directory, filename = split(fileordirectory)
       
   399                 if self.load_file(directory, filename, force_reload):
       
   400                     change = True
       
   401         if change:
       
   402             for registry, objects in self.items():
       
   403                 self.debug('available in registry %s: %s', registry,
       
   404                            sorted(objects))
       
   405         return change
       
   406     
       
   407     def read_directory(self, directory, force_reload=False):
       
   408         """read a directory and register available views"""
       
   409         modified_on = stat(realpath(directory))[-2]
       
   410         # only read directory if it was modified
       
   411         _lastmodifs = self._lastmodifs
       
   412         if directory in _lastmodifs and modified_on <= _lastmodifs[directory]:
       
   413             return False
       
   414         self.info('loading directory %s', directory)
       
   415         for filename in listdir(directory):
       
   416             if filename[-3:] == '.py':
       
   417                 try:
       
   418                     self.load_file(directory, filename, force_reload)
       
   419                 except OSError:
       
   420                     # this typically happens on emacs backup files (.#foo.py)
       
   421                     self.warning('Unable to load file %s. It is likely to be a backup file',
       
   422                                  filename)
       
   423                 except Exception, ex:
       
   424                     if self.config.mode in ('dev', 'test'):
       
   425                         raise
       
   426                     self.exception('%r while loading file %s', ex, filename)
       
   427         _lastmodifs[directory] = modified_on
       
   428         return True
       
   429 
       
   430     def load_file(self, directory, filename, force_reload=False):
       
   431         """load visual objects from a python file"""
       
   432         from logilab.common.modutils import load_module_from_modpath, modpath_from_file
       
   433         filepath = join(directory, filename)
       
   434         modified_on = stat(filepath)[-2]
       
   435         modpath = modpath_from_file(join(directory, filename))
       
   436         modname = '.'.join(modpath)
       
   437         unregistered = {}
       
   438         _lastmodifs = self._lastmodifs
       
   439         if filepath in _lastmodifs:
       
   440             # only load file if it was modified
       
   441             if modified_on <= _lastmodifs[filepath]:
       
   442                 return
       
   443             else:
       
   444                 # if it was modified, unregister all exisiting objects
       
   445                 # from this module, and keep track of what was unregistered
       
   446                 unregistered = self.unregister_module_vobjects(modname)
       
   447         # load the module
       
   448         module = load_module_from_modpath(modpath, use_sys=not force_reload)
       
   449         registered = self.load_module(module)
       
   450         # if something was unregistered, we need to update places where it was
       
   451         # referenced 
       
   452         if unregistered:
       
   453             # oldnew_mapping = {}
       
   454             oldnew_mapping = dict((unregistered[name], registered[name])
       
   455                                   for name in unregistered if name in registered)
       
   456             self.update_registered_subclasses(oldnew_mapping)
       
   457         _lastmodifs[filepath] = modified_on
       
   458         return True
       
   459 
       
   460     def load_module(self, module):
       
   461         registered = {}
       
   462         self.info('loading %s', module)
       
   463         for objname, obj in vars(module).items():
       
   464             if objname.startswith('_'):
       
   465                 continue
       
   466             self.load_ancestors_then_object(module.__name__, registered, obj)
       
   467         return registered
       
   468     
       
   469     def load_ancestors_then_object(self, modname, registered, obj):
       
   470         # skip imported classes
       
   471         if getattr(obj, '__module__', None) != modname:
       
   472             return
       
   473         # skip non registerable object
       
   474         try:
       
   475             if not issubclass(obj, VObject):
       
   476                 return
       
   477         except TypeError:
       
   478             return
       
   479         objname = '%s.%s' % (modname, obj.__name__)
       
   480         if objname in registered:
       
   481             return
       
   482         registered[objname] = obj
       
   483         for parent in obj.__bases__:
       
   484             self.load_ancestors_then_object(modname, registered, parent)
       
   485         self.load_object(obj)
       
   486             
       
   487     def load_object(self, obj):
       
   488         try:
       
   489             self.register_vobject_class(obj)
       
   490         except Exception, ex:
       
   491             if self.config.mode in ('test', 'dev'):
       
   492                 raise
       
   493             self.exception('vobject %s registration failed: %s', obj, ex)
       
   494         
       
   495 # init logging 
       
   496 set_log_methods(VObject, getLogger('cubicweb'))
       
   497 set_log_methods(VRegistry, getLogger('cubicweb.registry'))
       
   498 set_log_methods(registerer, getLogger('cubicweb.registration'))
       
   499 
       
   500 
       
   501 # advanced selector building functions ########################################
       
   502 
       
   503 def chainall(*selectors):
       
   504     """return a selector chaining given selectors. If one of
       
   505     the selectors fail, selection will fail, else the returned score
       
   506     will be the sum of each selector'score
       
   507     """
       
   508     assert selectors
       
   509     def selector(cls, *args, **kwargs):
       
   510         score = 0
       
   511         for selector in selectors:
       
   512             partscore = selector(cls, *args, **kwargs)
       
   513             if not partscore:
       
   514                 return 0
       
   515             score += partscore
       
   516         return score
       
   517     return selector
       
   518 
       
   519 def chainfirst(*selectors):
       
   520     """return a selector chaining given selectors. If all
       
   521     the selectors fail, selection will fail, else the returned score
       
   522     will be the first non-zero selector score
       
   523     """
       
   524     assert selectors
       
   525     def selector(cls, *args, **kwargs):
       
   526         for selector in selectors:
       
   527             partscore = selector(cls, *args, **kwargs)
       
   528             if partscore:
       
   529                 return partscore
       
   530         return 0
       
   531     return selector
       
   532