cubicweb/cwvreg.py
changeset 11057 0b59724cb3f2
parent 10907 9ae707db5265
child 11249 0ff4c02a1871
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/cwvreg.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,657 @@
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Cubicweb registries
+"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+import sys
+from os.path import join, dirname, realpath
+from warnings import warn
+from datetime import datetime, date, time, timedelta
+from functools import reduce
+
+from six import text_type, binary_type
+
+from logilab.common.decorators import cached, clear_cache
+from logilab.common.deprecation import deprecated, class_deprecated
+from logilab.common.modutils import cleanup_sys_modules
+from logilab.common.registry import (
+    RegistryStore, Registry, obj_registries,
+    ObjectNotFound, RegistryNotFound)
+
+from rql import RQLHelper
+from yams.constraints import BASE_CONVERTERS
+
+from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER,
+                      onevent, Binary, UnknownProperty, UnknownEid)
+from cubicweb.predicates import appobject_selectable, _reset_is_instance_cache
+
+
+@onevent('before-registry-reload')
+def cleanup_uicfg_compat():
+    """ backward compat: those modules are now refering to app objects in
+    cw.web.views.uicfg and import * from backward compat. On registry reload, we
+    should pop those modules from the cache so references are properly updated on
+    subsequent reload
+    """
+    if 'cubicweb.web' in sys.modules:
+        if getattr(sys.modules['cubicweb.web'], 'uicfg', None):
+            del sys.modules['cubicweb.web'].uicfg
+        if getattr(sys.modules['cubicweb.web'], 'uihelper', None):
+            del sys.modules['cubicweb.web'].uihelper
+    sys.modules.pop('cubicweb.web.uicfg', None)
+    sys.modules.pop('cubicweb.web.uihelper', None)
+
+
+def require_appobject(obj):
+    """return appobjects required by the given object by searching for
+    `appobject_selectable` predicate
+    """
+    impl = obj.__select__.search_selector(appobject_selectable)
+    if impl:
+        return (impl.registry, impl.regids)
+    return None
+
+
+class CWRegistry(Registry):
+    def __init__(self, vreg):
+        """
+        :param vreg: the :py:class:`CWRegistryStore` managing this registry.
+        """
+        super(CWRegistry, self).__init__(True)
+        self.vreg = vreg
+
+    @property
+    def schema(self):
+        """The :py:class:`cubicweb.schema.CubicWebSchema`
+        """
+        return self.vreg.schema
+
+    def poss_visible_objects(self, *args, **kwargs):
+        """return an ordered list of possible app objects in a given registry,
+        supposing they support the 'visible' and 'order' properties (as most
+        visualizable objects)
+        """
+        return sorted([x for x in self.possible_objects(*args, **kwargs)
+                       if x.cw_propval('visible')],
+                      key=lambda x: x.cw_propval('order'))
+
+
+def related_appobject(obj, appobjectattr='__appobject__'):
+    """ adapts any object to a potential appobject bound to it
+    through the __appobject__ attribute
+    """
+    return getattr(obj, appobjectattr, obj)
+
+
+class InstancesRegistry(CWRegistry):
+
+    def selected(self, winner, args, kwargs):
+        """overriden to avoid the default 'instanciation' behaviour, ie
+        `winner(*args, **kwargs)`
+        """
+        return winner
+
+
+class ETypeRegistry(CWRegistry):
+
+    def clear_caches(self):
+        clear_cache(self, 'etype_class')
+        clear_cache(self, 'parent_classes')
+        _reset_is_instance_cache(self.vreg)
+
+    def initialization_completed(self):
+        """on registration completed, clear etype_class internal cache
+        """
+        super(ETypeRegistry, self).initialization_completed()
+        # clear etype cache if you don't want to run into deep weirdness
+        self.clear_caches()
+        # rebuild all classes to avoid potential memory fragmentation
+        # (see #2719113)
+        for eschema in self.vreg.schema.entities():
+            self.etype_class(eschema)
+
+    def register(self, obj, **kwargs):
+        obj = related_appobject(obj)
+        oid = kwargs.get('oid') or obj.__regid__
+        if oid != 'Any' and not oid in self.schema:
+            self.error('don\'t register %s, %s type not defined in the '
+                       'schema', obj, oid)
+            return
+        kwargs['clear'] = True
+        super(ETypeRegistry, self).register(obj, **kwargs)
+
+    def iter_classes(self):
+        for etype in self.vreg.schema.entities():
+            yield self.etype_class(etype)
+
+    @cached
+    def parent_classes(self, etype):
+        if etype == 'Any':
+            return (), self.etype_class('Any')
+        parents = tuple(self.etype_class(e.type)
+                        for e in self.schema.eschema(etype).ancestors())
+        return parents, self.etype_class('Any')
+
+    @cached
+    def etype_class(self, etype):
+        """return an entity class for the given entity type.
+
+        Try to find out a specific class for this kind of entity or default to a
+        dump of the nearest parent class (in yams inheritance) registered.
+
+        Fall back to 'Any' if not yams parent class found.
+        """
+        etype = str(etype)
+        if etype == 'Any':
+            objects = self['Any']
+            assert len(objects) == 1, objects
+            return objects[0]
+        eschema = self.schema.eschema(etype)
+        baseschemas = [eschema] + eschema.ancestors()
+        # browse ancestors from most specific to most generic and try to find an
+        # associated custom entity class
+        for baseschema in baseschemas:
+            try:
+                btype = ETYPE_NAME_MAP[baseschema]
+            except KeyError:
+                btype = str(baseschema)
+            try:
+                objects = self[btype]
+                assert len(objects) == 1, objects
+                if btype == etype:
+                    cls = objects[0]
+                else:
+                    # recurse to ensure issubclass(etype_class('Child'),
+                    #                              etype_class('Parent'))
+                    cls = self.etype_class(btype)
+                break
+            except ObjectNotFound:
+                pass
+        else:
+            # no entity class for any of the ancestors, fallback to the default
+            # one
+            objects = self['Any']
+            assert len(objects) == 1, objects
+            cls = objects[0]
+        # make a copy event if cls.__regid__ == etype, else we may have pb for
+        # client application using multiple connections to different
+        # repositories (eg shingouz)
+        # __autogenerated__ attribute is just a marker
+        cls = type(str(etype), (cls,), {'__autogenerated__': True,
+                                        '__doc__': cls.__doc__,
+                                        '__module__': cls.__module__})
+        cls.__regid__ = etype
+        cls.__initialize__(self.schema)
+        return cls
+
+    def fetch_attrs(self, targettypes):
+        """return intersection of fetch_attrs of each entity type in
+        `targettypes`
+        """
+        fetchattrs_list = []
+        for ttype in targettypes:
+            etypecls = self.etype_class(ttype)
+            fetchattrs_list.append(set(etypecls.fetch_attrs))
+        return reduce(set.intersection, fetchattrs_list)
+
+
+class ViewsRegistry(CWRegistry):
+
+    def main_template(self, req, oid='main-template', rset=None, **kwargs):
+        """display query by calling the given template (default to main),
+        and returning the output as a string instead of requiring the [w]rite
+        method as argument
+        """
+        obj = self.select(oid, req, rset=rset, **kwargs)
+        res = obj.render(**kwargs)
+        if isinstance(res, text_type):
+            return res.encode(req.encoding)
+        assert isinstance(res, binary_type)
+        return res
+
+    def possible_views(self, req, rset=None, **kwargs):
+        """return an iterator on possible views for this result set
+
+        views returned are classes, not instances
+        """
+        for vid, views in self.items():
+            if vid[0] == '_':
+                continue
+            views = [view for view in views
+                     if not isinstance(view, class_deprecated)]
+            try:
+                view = self._select_best(views, req, rset=rset, **kwargs)
+                if view is not None and view.linkable():
+                    yield view
+            except Exception:
+                self.exception('error while trying to select %s view for %s',
+                               vid, rset)
+
+
+class ActionsRegistry(CWRegistry):
+    def poss_visible_objects(self, *args, **kwargs):
+        """return an ordered list of possible actions"""
+        return sorted(self.possible_objects(*args, **kwargs),
+                      key=lambda x: x.order)
+
+    def possible_actions(self, req, rset=None, **kwargs):
+        if rset is None:
+            actions = self.poss_visible_objects(req, rset=rset, **kwargs)
+        else:
+            actions = rset.possible_actions(**kwargs) # cached implementation
+        result = {}
+        for action in actions:
+            result.setdefault(action.category, []).append(action)
+        return result
+
+
+class CtxComponentsRegistry(CWRegistry):
+    def poss_visible_objects(self, *args, **kwargs):
+        """return an ordered list of possible components"""
+        context = kwargs.pop('context')
+        if '__cache' in kwargs:
+            cache = kwargs.pop('__cache')
+        elif kwargs.get('rset') is None:
+            cache = args[0]
+        else:
+            cache = kwargs['rset']
+        try:
+            cached = cache.__components_cache
+        except AttributeError:
+            ctxcomps = super(CtxComponentsRegistry, self).poss_visible_objects(
+                *args, **kwargs)
+            if cache is None:
+                components = []
+                for component in ctxcomps:
+                    cctx = component.cw_propval('context')
+                    if cctx == context:
+                        component.cw_extra_kwargs['context'] = cctx
+                        components.append(component)
+                return components
+            cached = cache.__components_cache = {}
+            for component in ctxcomps:
+                cctx = component.cw_propval('context')
+                component.cw_extra_kwargs['context'] = cctx
+                cached.setdefault(cctx, []).append(component)
+        thisctxcomps = cached.get(context, ())
+        # XXX set context for bw compat (should now be taken by comp.render())
+        for component in thisctxcomps:
+            component.cw_extra_kwargs['context'] = context
+        return thisctxcomps
+
+
+class BwCompatCWRegistry(object):
+    def __init__(self, vreg, oldreg, redirecttoreg):
+        self.vreg = vreg
+        self.oldreg = oldreg
+        self.redirecto = redirecttoreg
+
+    def __getattr__(self, attr):
+        warn('[3.10] you should now use the %s registry instead of the %s registry'
+             % (self.redirecto, self.oldreg), DeprecationWarning, stacklevel=2)
+        return getattr(self.vreg[self.redirecto], attr)
+
+    def clear(self): pass
+    def initialization_completed(self): pass
+
+
+class CWRegistryStore(RegistryStore):
+    """Central registry for the cubicweb instance, extending the generic
+    RegistryStore with some cubicweb specific stuff.
+
+    This is one of the central object in cubicweb instance, coupling
+    dynamically loaded objects with the schema and the configuration objects.
+
+    It specializes the RegistryStore by adding some convenience methods to access to
+    stored objects. Currently we have the following registries of objects known
+    by the web instance (library may use some others additional registries):
+
+    * 'etypes', entity type classes
+
+    * 'views', views and templates (e.g. layout views)
+
+    * 'components', non contextual components, like magic search, url evaluators
+
+    * 'ctxcomponents', contextual components like boxes and dynamic section
+
+    * 'actions', contextual actions, eg links to display in predefined places in
+      the ui
+
+    * 'forms', describing logic of HTML form
+
+    * 'formrenderers', rendering forms to html
+
+    * 'controllers', primary objects to handle request publishing, directly
+      plugged into the application
+    """
+
+    REGISTRY_FACTORY = {None: CWRegistry,
+                        'etypes': ETypeRegistry,
+                        'views': ViewsRegistry,
+                        'actions': ActionsRegistry,
+                        'ctxcomponents': CtxComponentsRegistry,
+                        'uicfg': InstancesRegistry,
+                        }
+
+    def __init__(self, config, initlog=True):
+        if initlog:
+            # first init log service
+            config.init_log()
+        super(CWRegistryStore, self).__init__(config.debugmode)
+        self.config = config
+        # 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)
+        self.schema = None
+        self.initialized = False
+        self['boxes'] = BwCompatCWRegistry(self, 'boxes', 'ctxcomponents')
+        self['contentnavigation'] = BwCompatCWRegistry(self, 'contentnavigation', 'ctxcomponents')
+
+    def setdefault(self, regid):
+        try:
+            return self[regid]
+        except RegistryNotFound:
+            self[regid] = self.registry_class(regid)(self)
+            return self[regid]
+
+    def items(self):
+        return [item for item in super(CWRegistryStore, self).items()
+                if not item[0] in ('propertydefs', 'propertyvalues')]
+    def iteritems(self):
+        return (item for item in super(CWRegistryStore, self).items()
+                if not item[0] in ('propertydefs', 'propertyvalues'))
+
+    def values(self):
+        return [value for key, value in self.items()]
+    def itervalues(self):
+        return (value for key, value in self.items())
+
+    def reset(self):
+        CW_EVENT_MANAGER.emit('before-registry-reset', self)
+        super(CWRegistryStore, self).reset()
+        self._needs_appobject = {}
+        # two special registries, propertydefs which care all the property
+        # definitions, and propertyvals which contains values for those
+        # properties
+        if not self.initialized:
+            self['propertydefs'] = {}
+            self['propertyvalues'] = self.eprop_values = {}
+            for key, propdef in self.config.cwproperty_definitions():
+                self.register_property(key, **propdef)
+        CW_EVENT_MANAGER.emit('after-registry-reset', self)
+
+    def register_all(self, objects, modname, butclasses=()):
+        butclasses = set(related_appobject(obj)
+                         for obj in butclasses)
+        objects = [related_appobject(obj) for obj in objects]
+        super(CWRegistryStore, self).register_all(objects, modname, butclasses)
+
+    def register_and_replace(self, obj, replaced):
+        obj = related_appobject(obj)
+        replaced = related_appobject(replaced)
+        super(CWRegistryStore, self).register_and_replace(obj, replaced)
+
+    def set_schema(self, schema):
+        """set instance'schema and load application objects"""
+        self._set_schema(schema)
+        # now we can load application's web objects
+        self.reload(self.config.appobjects_path(), force_reload=False)
+        # map lowered entity type names to their actual name
+        self.case_insensitive_etypes = {}
+        for eschema in self.schema.entities():
+            etype = str(eschema)
+            self.case_insensitive_etypes[etype.lower()] = etype
+            clear_cache(eschema, 'ordered_relations')
+            clear_cache(eschema, 'meta_attributes')
+
+    def reload_if_needed(self):
+        path = self.config.appobjects_path()
+        if self.is_reload_needed(path):
+            self.reload(path)
+
+    def _cleanup_sys_modules(self, path):
+        """Remove submodules of `directories` from `sys.modules` and cleanup
+        CW_EVENT_MANAGER accordingly.
+
+        We take care to properly remove obsolete registry callbacks.
+
+        """
+        caches = {}
+        callbackdata = CW_EVENT_MANAGER.callbacks.values()
+        for callbacklist in callbackdata:
+            for callback in callbacklist:
+                func = callback[0]
+                # for non-function callable, we do nothing interesting
+                module = getattr(func, '__module__', None)
+                caches[id(callback)] = module
+        deleted_modules = set(cleanup_sys_modules(path))
+        for callbacklist in callbackdata:
+            for callback in callbacklist[:]:
+                module = caches[id(callback)]
+                if module and module in deleted_modules:
+                    callbacklist.remove(callback)
+
+    def reload(self, path, force_reload=True):
+        """modification detected, reset and reload the vreg"""
+        CW_EVENT_MANAGER.emit('before-registry-reload')
+        if force_reload:
+            self._cleanup_sys_modules(path)
+            cubes = self.config.cubes()
+            # if the fs code use some cubes not yet registered into the instance
+            # we should cleanup sys.modules for those as well to avoid potential
+            # bad class reference pb after reloading
+            cfg = self.config
+            for cube in cfg.expand_cubes(cubes, with_recommends=True):
+                if not cube in cubes:
+                    cpath = cfg.build_appobjects_cube_path([cfg.cube_dir(cube)])
+                    self._cleanup_sys_modules(cpath)
+        self.register_objects(path)
+        CW_EVENT_MANAGER.emit('after-registry-reload')
+
+    def load_file(self, filepath, modname):
+        # override to allow some instrumentation (eg localperms)
+        modpath = modname.split('.')
+        try:
+            self.currently_loading_cube = modpath[modpath.index('cubes') + 1]
+        except ValueError:
+            self.currently_loading_cube = 'cubicweb'
+        return super(CWRegistryStore, self).load_file(filepath, modname)
+
+    def _set_schema(self, schema):
+        """set instance'schema"""
+        self.schema = schema
+        clear_cache(self, 'rqlhelper')
+
+    def update_schema(self, schema):
+        """update .schema attribute on registered objects, necessary for some
+        tests
+        """
+        self.schema = schema
+        for registry, regcontent in self.items():
+            for objects in regcontent.values():
+                for obj in objects:
+                    obj.schema = schema
+
+    def register(self, obj, *args, **kwargs):
+        """register `obj` application object into `registryname` or
+        `obj.__registry__` if not specified, with identifier `oid` or
+        `obj.__regid__` if not specified.
+
+        If `clear` is true, all objects with the same identifier will be
+        previously unregistered.
+        """
+        obj = related_appobject(obj)
+        super(CWRegistryStore, self).register(obj, *args, **kwargs)
+        depends_on = require_appobject(obj)
+        if depends_on is not None:
+            self._needs_appobject[obj] = depends_on
+
+    def register_objects(self, path):
+        """overriden to give cubicweb's extrapath (eg cubes package's __path__)
+        """
+        super(CWRegistryStore, self).register_objects(
+            path, self.config.extrapath)
+
+    def initialization_completed(self):
+        """cw specific code once vreg initialization is completed:
+
+        * remove objects requiring a missing appobject, unless
+          config.cleanup_unused_appobjects is false
+        * init rtags
+        """
+        # we may want to keep interface dependent objects (e.g.for i18n
+        # catalog generation)
+        if self.config.cleanup_unused_appobjects:
+            # remove appobjects which depend on other, unexistant appobjects
+            for obj, (regname, regids) in self._needs_appobject.items():
+                try:
+                    registry = self[regname]
+                except RegistryNotFound:
+                    self.debug('unregister %s (no registry %s)', obj, regname)
+                    self.unregister(obj)
+                    continue
+                for regid in regids:
+                    if registry.get(regid):
+                        break
+                else:
+                    self.debug('unregister %s (no %s object in registry %s)',
+                               registry.objid(obj), ' or '.join(regids), regname)
+                    self.unregister(obj)
+        super(CWRegistryStore, self).initialization_completed()
+        if 'uicfg' in self: # 'uicfg' is not loaded in a pure repository mode
+            for rtags in self['uicfg'].values():
+                for rtag in rtags:
+                    # don't check rtags if we don't want to cleanup_unused_appobjects
+                    rtag.init(self.schema, check=self.config.cleanup_unused_appobjects)
+
+    # rql parsing utilities ####################################################
+
+    @property
+    @cached
+    def rqlhelper(self):
+        return RQLHelper(self.schema,
+                         special_relations={'eid': 'uid', 'has_text': 'fti'})
+
+    def solutions(self, req, rqlst, args):
+        def type_from_eid(eid, req=req):
+            return req.entity_metas(eid)['type']
+        return self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
+
+    def parse(self, req, rql, args=None):
+        rqlst = self.rqlhelper.parse(rql)
+        try:
+            self.solutions(req, rqlst, args)
+        except UnknownEid:
+            for select in rqlst.children:
+                select.solutions = []
+        return rqlst
+
+    # properties handling #####################################################
+
+    def user_property_keys(self, withsitewide=False):
+        if withsitewide:
+            return sorted(k for k in self['propertydefs']
+                          if not k.startswith('sources.'))
+        return sorted(k for k, kd in self['propertydefs'].items()
+                      if not kd['sitewide'] and not k.startswith('sources.'))
+
+    def register_property(self, key, type, help, default=None, vocabulary=None,
+                          sitewide=False):
+        """register a given property"""
+        properties = self['propertydefs']
+        assert type in YAMS_TO_PY, 'unknown type %s' % type
+        properties[key] = {'type': type, 'vocabulary': vocabulary,
+                           'default': default, 'help': help,
+                           'sitewide': sitewide}
+
+    def property_info(self, key):
+        """return dictionary containing description associated to the given
+        property key (including type, defaut value, help and a site wide
+        boolean)
+        """
+        try:
+            return self['propertydefs'][key]
+        except KeyError:
+            if key.startswith('system.version.'):
+                soft = key.split('.')[-1]
+                return {'type': 'String', 'sitewide': True,
+                        'default': None, 'vocabulary': None,
+                        'help': _('%s software version of the database') % soft}
+            raise UnknownProperty('unregistered property %r' % key)
+
+    def property_value(self, key):
+        try:
+            return self['propertyvalues'][key]
+        except KeyError:
+            return self.property_info(key)['default']
+
+    def typed_value(self, key, value):
+        """value is a unicode string, return it correctly typed. Let potential
+        type error propagates.
+        """
+        pdef = self.property_info(key)
+        try:
+            value = YAMS_TO_PY[pdef['type']](value)
+        except (TypeError, ValueError):
+            raise ValueError(_('bad value'))
+        vocab = pdef['vocabulary']
+        if vocab is not None:
+            if callable(vocab):
+                vocab = vocab(None) # XXX need a req object
+            if not value in vocab:
+                raise ValueError(_('unauthorized value'))
+        return value
+
+    def init_properties(self, propvalues):
+        """init the property values registry using the given set of couple (key, value)
+        """
+        self.initialized = True
+        values = self['propertyvalues']
+        for key, val in propvalues:
+            try:
+                values[key] = self.typed_value(key, val)
+            except ValueError as ex:
+                self.warning('%s (you should probably delete that property '
+                             'from the database)', ex)
+            except UnknownProperty as ex:
+                self.warning('%s (you should probably delete that property '
+                             'from the database)', ex)
+
+
+# XXX unify with yams.constraints.BASE_CONVERTERS?
+YAMS_TO_PY = BASE_CONVERTERS.copy()
+YAMS_TO_PY.update({
+    'Bytes':      Binary,
+    'Date':       date,
+    'Datetime':   datetime,
+    'TZDatetime': datetime,
+    'Time':       time,
+    'TZTime':     time,
+    'Interval':   timedelta,
+    })