--- a/cwvreg.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,657 +0,0 @@
-# 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,
- })