vregistry.py
changeset 0 b97547f5f1fa
child 177 73aa03734425
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vregistry.py	Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,532 @@
+"""
+* the vregistry handle various type of objects interacting
+  together. The vregistry handle registration of dynamically loaded
+  objects and provide a convenient api access to those objects
+  according to a context
+
+* to interact with the vregistry, object should inherit from the
+  VObject abstract class
+  
+* the registration procedure is delegated to a registerer. Each
+  registerable vobject must defines its registerer class using the
+  __registerer__ attribute.  A registerer is instantianted at
+  registration time after what the instance is lost
+  
+* the selection procedure has been generalized by delegating to a
+  selector, which is responsible to score the vobject according to the
+  current state (req, rset, row, col). At the end of the selection, if
+  a vobject class has been found, an instance of this class is
+  returned. The selector is instantiated at vobject registration
+
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+import sys
+from os import listdir, stat
+from os.path import dirname, join, realpath, split, isdir
+from logging import getLogger
+
+from cubicweb import CW_SOFTWARE_ROOT, set_log_methods
+from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject
+
+
+class vobject_helper(object):
+    """object instantiated at registration time to help a wrapped
+    VObject subclass
+    """
+
+    def __init__(self, registry, vobject):
+        self.registry = registry
+        self.vobject = vobject
+        self.config = registry.config
+        self.schema = registry.schema
+
+
+class registerer(vobject_helper):
+    """do whatever is needed at registration time for the wrapped
+    class, according to current application schema and already
+    registered objects of the same kind (i.e. same registry name and
+    same id).
+
+    The wrapped class may be skipped, some previously selected object
+    may be kicked out... After whatever works needed, if the object or
+    a transformed object is returned, it will be added to previously
+    registered objects.
+    """
+
+    def __init__(self, registry, vobject):
+        super(registerer, self).__init__(registry, vobject)
+        self.kicked = set()
+    
+    def do_it_yourself(self, registered):
+        raise NotImplementedError(str(self.vobject))
+        
+    def kick(self, registered, kicked):
+        self.debug('kicking vobject %s', kicked)
+        registered.remove(kicked)
+        self.kicked.add(kicked.classid())
+        
+    def skip(self):
+        self.debug('no schema compat, skipping %s', self.vobject)
+
+
+def selector(cls, *args, **kwargs):
+    """selector is called to help choosing the correct object for a
+    particular request and result set by returning a score.
+
+    it must implement a .score_method taking a request, a result set and
+    optionaly row and col arguments which return an int telling how well
+    the wrapped class apply to the given request and result set. 0 score
+    means that it doesn't apply.
+    
+    rset may be None. If not, row and col arguments may be optionally
+    given if the registry is scoring a given row or a given cell of
+    the result set (both row and col are int if provided).
+    """    
+    raise NotImplementedError(cls)
+
+
+class autoselectors(type):
+    """implements __selectors__ / __select__ compatibility layer so that:
+
+    __select__ = chainall(classmethod(A, B, C))
+
+    can be replaced by something like:
+    
+    __selectors__ = (A, B, C)
+    """
+    def __new__(mcs, name, bases, classdict):
+        if '__select__' in classdict and '__selectors__' in classdict:
+            raise TypeError("__select__ and __selectors__ "
+                            "can't be used together")
+        if '__select__' not in classdict and '__selectors__' in classdict:
+            selectors = classdict['__selectors__']
+            classdict['__select__'] = classmethod(chainall(*selectors))
+        return super(autoselectors, mcs).__new__(mcs, name, bases, classdict)
+
+    def __setattr__(self, attr, value):
+        if attr == '__selectors__':
+            self.__select__ = classmethod(chainall(*value))
+        super(autoselectors, self).__setattr__(attr, value)
+            
+
+class VObject(object):
+    """visual object, use to be handled somehow by the visual components
+    registry.
+
+    The following attributes should be set on concret vobject subclasses:
+    
+    :__registry__:
+      name of the registry for this object (string like 'views',
+      'templates'...)
+    :id:
+      object's identifier in the registry (string like 'main',
+      'primary', 'folder_box')
+    :__registerer__:
+      registration helper class
+    :__select__:
+      selection helper function
+    :__selectors__:
+      tuple of selectors to be chained
+      (__select__ and __selectors__ are mutually exclusive)
+      
+    Moreover, the `__abstract__` attribute may be set to True to indicate
+    that a vobject is abstract and should not be registered
+    """
+    __metaclass__ = autoselectors
+    # necessary attributes to interact with the registry
+    id = None
+    __registry__ = None
+    __registerer__ = None
+    __select__ = None
+
+    @classmethod
+    def registered(cls, registry):
+        """called by the registry when the vobject has been registered.
+
+        It must return the  object that will be actually registered (this
+        may be the right hook to create an instance for example). By
+        default the vobject is returned without any transformation.
+        """
+        return cls
+
+    @classmethod
+    def selected(cls, *args, **kwargs):
+        """called by the registry when the vobject has been selected.
+        
+        It must return the  object that will be actually returned by the
+        .select method (this may be the right hook to create an
+        instance for example). By default the selected object is
+        returned without any transformation.
+        """
+        return cls
+
+    @classmethod
+    def classid(cls):
+        """returns a unique identifier for the vobject"""
+        return '%s.%s' % (cls.__module__, cls.__name__)
+
+
+class VRegistry(object):
+    """class responsible to register, propose and select the various
+    elements used to build the web interface. Currently, we have templates,
+    views, actions and components.
+    """
+    
+    def __init__(self, config):#, cache_size=1000):
+        self.config = config
+        # dictionnary of registry (themself dictionnary) by name
+        self._registries = {}
+        self._lastmodifs = {}
+
+    def reset(self):
+        self._registries = {}
+        self._lastmodifs = {}
+
+    def __getitem__(self, key):
+        return self._registries[key]
+
+    def get(self, key, default=None):
+        return self._registries.get(key, default)
+
+    def items(self):
+        return self._registries.items()
+
+    def values(self):
+        return self._registries.values()
+
+    def __contains__(self, key):
+        return key in self._registries
+        
+    def register_vobject_class(self, cls, _kicked=set()):
+        """handle vobject class registration
+        
+        vobject class with __abstract__ == True in their local dictionnary or
+        with a name starting starting by an underscore are not registered.
+        Also a vobject class needs to have __registry__ and id attributes set
+        to a non empty string to be registered.
+
+        Registration is actually handled by vobject's registerer.
+        """
+        if (cls.__dict__.get('__abstract__') or cls.__name__[0] == '_'
+            or not cls.__registry__ or not cls.id):
+            return
+        # while reloading a module :
+        # if cls was previously kicked, it means that there is a more specific
+        # vobject defined elsewhere re-registering cls would kick it out
+        if cls.classid() in _kicked:
+            self.debug('not re-registering %s because it was previously kicked',
+                      cls.classid())
+        else:
+            regname = cls.__registry__
+            if cls.id in self.config['disable-%s' % regname]:
+                return
+            registry = self._registries.setdefault(regname, {})
+            vobjects = registry.setdefault(cls.id, [])
+            registerer = cls.__registerer__(self, cls)
+            cls = registerer.do_it_yourself(vobjects)
+            #_kicked |= registerer.kicked
+            if cls:
+                vobject = cls.registered(self)
+                try:
+                    vname = vobject.__name__
+                except AttributeError:
+                    vname = vobject.__class__.__name__
+                self.debug('registered vobject %s in registry %s with id %s',
+                          vname, cls.__registry__, cls.id)
+                vobjects.append(vobject)
+            
+    def unregister_module_vobjects(self, modname):
+        """removes registered objects coming from a given module
+
+        returns a dictionnary classid/class of all classes that will need
+        to be updated after reload (i.e. vobjects referencing classes defined
+        in the <modname> module)
+        """
+        unregistered = {}
+        # browse each registered object
+        for registry, objdict in self.items():
+            for oid, objects in objdict.items():
+                for obj in objects[:]:
+                    objname = obj.classid()
+                    # if the vobject is defined in this module, remove it
+                    if objname.startswith(modname):
+                        unregistered[objname] = obj
+                        objects.remove(obj)
+                        self.debug('unregistering %s in %s registry',
+                                  objname, registry)
+                    # if not, check if the vobject can be found in baseclasses
+                    # (because we also want subclasses to be updated)
+                    else:
+                        if not isinstance(obj, type):
+                            obj = obj.__class__
+                        for baseclass in obj.__bases__:
+                            if hasattr(baseclass, 'classid'):
+                                baseclassid = baseclass.classid()
+                                if baseclassid.startswith(modname):
+                                    unregistered[baseclassid] = baseclass
+                # update oid entry
+                if objects:
+                    objdict[oid] = objects
+                else:
+                    del objdict[oid]
+        return unregistered
+
+
+    def update_registered_subclasses(self, oldnew_mapping):
+        """updates subclasses of re-registered vobjects
+
+        if baseviews.PrimaryView is changed, baseviews.py will be reloaded
+        automatically and the new version of PrimaryView will be registered.
+        But all existing subclasses must also be notified of this change, and
+        that's what this method does
+
+        :param oldnew_mapping: a dict mapping old version of a class to
+                               the new version
+        """
+        # browse each registered object
+        for objdict in self.values():
+            for objects in objdict.values():
+                for obj in objects:
+                    if not isinstance(obj, type):
+                        obj = obj.__class__
+                    # build new baseclasses tuple
+                    newbases = tuple(oldnew_mapping.get(baseclass, baseclass)
+                                     for baseclass in obj.__bases__)
+                    # update obj's baseclasses tuple (__bases__) if needed
+                    if newbases != obj.__bases__:
+                        self.debug('updating %s.%s base classes',
+                                  obj.__module__, obj.__name__)
+                        obj.__bases__ = newbases
+
+    def registry(self, name):
+        """return the registry (dictionary of class objects) associated to
+        this name
+        """
+        try:
+            return self._registries[name]
+        except KeyError:
+            raise RegistryNotFound(name), None, sys.exc_info()[-1]
+
+    def registry_objects(self, name, oid=None):
+        """returns objects registered with the given oid in the given registry.
+        If no oid is given, return all objects in this registry
+        """
+        registry = self.registry(name)
+        if oid:
+            try:
+                return registry[oid]
+            except KeyError:
+                raise ObjectNotFound(oid), None, sys.exc_info()[-1]
+        else:
+            result = []
+            for objs in registry.values():
+                result += objs
+            return result
+        
+    def select(self, vobjects, *args, **kwargs):
+        """return an instance of the most specific object according
+        to parameters
+
+        raise NoSelectableObject if not object apply
+        """
+        score, winner = 0, None
+        for vobject in vobjects:
+            vobjectscore = vobject.__select__(*args, **kwargs)
+            if vobjectscore > score:
+                score, winner = vobjectscore, vobject
+        if winner is None:
+            raise NoSelectableObject('args: %s\nkwargs: %s %s'
+                                     % (args, kwargs.keys(), [repr(v) for v in vobjects]))
+        # return the result of the .selected method of the vobject
+        return winner.selected(*args, **kwargs)
+    
+    def possible_objects(self, registry, *args, **kwargs):
+        """return an iterator on possible objects in a registry for this result set
+
+        actions returned are classes, not instances
+        """
+        for vobjects in self.registry(registry).values():
+            try:
+                yield self.select(vobjects, *args, **kwargs)
+            except NoSelectableObject:
+                continue
+
+    def select_object(self, registry, cid, *args, **kwargs):
+        """return the most specific component according to the resultset"""
+        return self.select(self.registry_objects(registry, cid), *args, **kwargs)
+
+    def object_by_id(self, registry, cid, *args, **kwargs):
+        """return the most specific component according to the resultset"""
+        objects = self[registry][cid]
+        assert len(objects) == 1, objects
+        return objects[0].selected(*args, **kwargs)
+    
+    # intialization methods ###################################################
+
+    
+    def register_objects(self, path, force_reload=None):
+        if force_reload is None:
+            force_reload = self.config.mode == 'dev'
+        elif not force_reload:
+            # force_reload == False usually mean modules have been reloaded
+            # by another connection, so we want to update the registry
+            # content even if there has been no module content modification
+            self.reset()
+        # need to clean sys.path this to avoid import confusion pb (i.e.
+        # having the same module loaded as 'cubicweb.web.views' subpackage and
+        # as views'  or 'web.views' subpackage
+        # this is mainly for testing purpose, we should'nt need this in
+        # production environment
+        for webdir in (join(dirname(realpath(__file__)), 'web'),
+                       join(dirname(__file__), 'web')):
+            if webdir in sys.path:
+                sys.path.remove(webdir)
+        if CW_SOFTWARE_ROOT in sys.path:
+            sys.path.remove(CW_SOFTWARE_ROOT)        
+        # load views from each directory in the application's path
+        change = False
+        for fileordirectory in path:
+            if isdir(fileordirectory):
+                if self.read_directory(fileordirectory, force_reload):
+                    change = True
+            else:
+                directory, filename = split(fileordirectory)
+                if self.load_file(directory, filename, force_reload):
+                    change = True
+        if change:
+            for registry, objects in self.items():
+                self.debug('available in registry %s: %s', registry,
+                           sorted(objects))
+        return change
+    
+    def read_directory(self, directory, force_reload=False):
+        """read a directory and register available views"""
+        modified_on = stat(realpath(directory))[-2]
+        # only read directory if it was modified
+        _lastmodifs = self._lastmodifs
+        if directory in _lastmodifs and modified_on <= _lastmodifs[directory]:
+            return False
+        self.info('loading directory %s', directory)
+        for filename in listdir(directory):
+            if filename[-3:] == '.py':
+                try:
+                    self.load_file(directory, filename, force_reload)
+                except OSError:
+                    # this typically happens on emacs backup files (.#foo.py)
+                    self.warning('Unable to load file %s. It is likely to be a backup file',
+                                 filename)
+                except Exception, ex:
+                    if self.config.mode in ('dev', 'test'):
+                        raise
+                    self.exception('%r while loading file %s', ex, filename)
+        _lastmodifs[directory] = modified_on
+        return True
+
+    def load_file(self, directory, filename, force_reload=False):
+        """load visual objects from a python file"""
+        from logilab.common.modutils import load_module_from_modpath, modpath_from_file
+        filepath = join(directory, filename)
+        modified_on = stat(filepath)[-2]
+        modpath = modpath_from_file(join(directory, filename))
+        modname = '.'.join(modpath)
+        unregistered = {}
+        _lastmodifs = self._lastmodifs
+        if filepath in _lastmodifs:
+            # only load file if it was modified
+            if modified_on <= _lastmodifs[filepath]:
+                return
+            else:
+                # if it was modified, unregister all exisiting objects
+                # from this module, and keep track of what was unregistered
+                unregistered = self.unregister_module_vobjects(modname)
+        # load the module
+        module = load_module_from_modpath(modpath, use_sys=not force_reload)
+        registered = self.load_module(module)
+        # if something was unregistered, we need to update places where it was
+        # referenced 
+        if unregistered:
+            # oldnew_mapping = {}
+            oldnew_mapping = dict((unregistered[name], registered[name])
+                                  for name in unregistered if name in registered)
+            self.update_registered_subclasses(oldnew_mapping)
+        _lastmodifs[filepath] = modified_on
+        return True
+
+    def load_module(self, module):
+        registered = {}
+        self.info('loading %s', module)
+        for objname, obj in vars(module).items():
+            if objname.startswith('_'):
+                continue
+            self.load_ancestors_then_object(module.__name__, registered, obj)
+        return registered
+    
+    def load_ancestors_then_object(self, modname, registered, obj):
+        # skip imported classes
+        if getattr(obj, '__module__', None) != modname:
+            return
+        # skip non registerable object
+        try:
+            if not issubclass(obj, VObject):
+                return
+        except TypeError:
+            return
+        objname = '%s.%s' % (modname, obj.__name__)
+        if objname in registered:
+            return
+        registered[objname] = obj
+        for parent in obj.__bases__:
+            self.load_ancestors_then_object(modname, registered, parent)
+        self.load_object(obj)
+            
+    def load_object(self, obj):
+        try:
+            self.register_vobject_class(obj)
+        except Exception, ex:
+            if self.config.mode in ('test', 'dev'):
+                raise
+            self.exception('vobject %s registration failed: %s', obj, ex)
+        
+# init logging 
+set_log_methods(VObject, getLogger('cubicweb'))
+set_log_methods(VRegistry, getLogger('cubicweb.registry'))
+set_log_methods(registerer, getLogger('cubicweb.registration'))
+
+
+# advanced selector building functions ########################################
+
+def chainall(*selectors):
+    """return a selector chaining given selectors. If one of
+    the selectors fail, selection will fail, else the returned score
+    will be the sum of each selector'score
+    """
+    assert selectors
+    def selector(cls, *args, **kwargs):
+        score = 0
+        for selector in selectors:
+            partscore = selector(cls, *args, **kwargs)
+            if not partscore:
+                return 0
+            score += partscore
+        return score
+    return selector
+
+def chainfirst(*selectors):
+    """return a selector chaining given selectors. If all
+    the selectors fail, selection will fail, else the returned score
+    will be the first non-zero selector score
+    """
+    assert selectors
+    def selector(cls, *args, **kwargs):
+        for selector in selectors:
+            partscore = selector(cls, *args, **kwargs)
+            if partscore:
+                return partscore
+        return 0
+    return selector
+