cwvreg.py 25.4 KB
Newer Older
Sylvain Thénault's avatar
Sylvain Thénault committed
1
# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
3
4
5
6
7
8
9
10
# 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.
#
11
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
12
13
14
15
16
17
# 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/>.
18
19
"""
Cubicweb registries
Adrien Di Mascio's avatar
Adrien Di Mascio committed
20
"""
Sylvain Thénault's avatar
Sylvain Thénault committed
21

22
23
import sys
from os.path import join, dirname, realpath
24
from datetime import datetime, date, time, timedelta
Pierre-Yves David's avatar
Pierre-Yves David committed
25
from functools import reduce
26

27
from logilab.common.decorators import cached, clear_cache
28
from logilab.common.deprecation import class_deprecated
29
from logilab.common.modutils import clean_sys_modules
30
from logilab.common.registry import RegistryStore, Registry, ObjectNotFound, RegistryNotFound
Adrien Di Mascio's avatar
Adrien Di Mascio committed
31
32

from rql import RQLHelper
33
from yams.constraints import BASE_CONVERTERS
Adrien Di Mascio's avatar
Adrien Di Mascio committed
34

Sylvain Thénault's avatar
Sylvain Thénault committed
35
from cubicweb import _
36
from cubicweb.debug import emit_to_debug_channel
37
from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER,
38
                      onevent, Binary, UnknownProperty, UnknownEid)
Aurelien Campeas's avatar
Aurelien Campeas committed
39
from cubicweb.predicates import appobject_selectable, _reset_is_instance_cache
Adrien Di Mascio's avatar
Adrien Di Mascio committed
40

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

@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)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
56

57

58
def require_appobject(obj):
59
60
    """return appobjects required by the given object by searching for
    `appobject_selectable` predicate
61
62
63
64
65
    """
    impl = obj.__select__.search_selector(appobject_selectable)
    if impl:
        return (impl.registry, impl.regids)
    return None
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
66

Adrien Di Mascio's avatar
Adrien Di Mascio committed
67

68
69
class CWRegistry(Registry):
    def __init__(self, vreg):
70
71
72
        """
        :param vreg: the :py:class:`CWRegistryStore` managing this registry.
        """
73
        super(CWRegistry, self).__init__(True)
74
        self.vreg = vreg
75

76
77
78
79
80
81
82
83
84
85
    def _select_best(self, objects, *args, **kwargs):
        """
        Overwrite version of Registry._select_best to emit debug information.
        """
        def emit_registry_debug_information(debug_registry_select_best):
            emit_to_debug_channel("registry_decisions", debug_registry_select_best)

        kwargs["debug_callback"] = emit_registry_debug_information
        return super()._select_best(objects, *args, **kwargs)

86
87
    @property
    def schema(self):
88
89
        """The :py:class:`cubicweb.schema.CubicWebSchema`
        """
90
        return self.vreg.schema
91

92
    def poss_visible_objects(self, *args, **kwargs):
93
94
95
96
97
        """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)
Sylvain Thénault's avatar
Sylvain Thénault committed
98
99
                       if x.cw_propval('visible')],
                      key=lambda x: x.cw_propval('order'))
100
101


102
103
104
105
106
107
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)

108

109
110
111
112
class InstancesRegistry(CWRegistry):

    def selected(self, winner, args, kwargs):
        """overriden to avoid the default 'instanciation' behaviour, ie
113
        `winner(*args, **kwargs)`
114
115
116
117
        """
        return winner


118
119
class ETypeRegistry(CWRegistry):

120
121
122
    def clear_caches(self):
        clear_cache(self, 'etype_class')
        clear_cache(self, 'parent_classes')
123
        _reset_is_instance_cache(self.vreg)
124

125
    def initialization_completed(self):
126
127
        """on registration completed, clear etype_class internal cache
        """
128
129
        super(ETypeRegistry, self).initialization_completed()
        # clear etype cache if you don't want to run into deep weirdness
130
        self.clear_caches()
131
132
133
134
        # rebuild all classes to avoid potential memory fragmentation
        # (see #2719113)
        for eschema in self.vreg.schema.entities():
            self.etype_class(eschema)
135
136

    def register(self, obj, **kwargs):
137
        obj = related_appobject(obj)
138
        oid = kwargs.get('oid') or obj.__regid__
Denis Laxalde's avatar
Denis Laxalde committed
139
        if oid != 'Any' and oid not in self.schema:
140
            self.error('don\'t register %s, %s type not defined in the '
Sylvain Thénault's avatar
Sylvain Thénault committed
141
                       'schema', obj, oid)
142
143
144
145
            return
        kwargs['clear'] = True
        super(ETypeRegistry, self).register(obj, **kwargs)

146
147
148
149
    def iter_classes(self):
        for etype in self.vreg.schema.entities():
            yield self.etype_class(etype)

150
151
    @cached
    def parent_classes(self, etype):
152
        if etype == 'Any':
153
154
155
156
            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')
157

158
159
160
    @cached
    def etype_class(self, etype):
        """return an entity class for the given entity type.
161
162
163
164
165

        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.
166
167
168
        """
        etype = str(etype)
        if etype == 'Any':
169
170
171
            objects = self['Any']
            assert len(objects) == 1, objects
            return objects[0]
172
173
        eschema = self.schema.eschema(etype)
        baseschemas = [eschema] + eschema.ancestors()
174
175
        # browse ancestors from most specific to most generic and try to find an
        # associated custom entity class
176
177
178
179
180
181
        for baseschema in baseschemas:
            try:
                btype = ETYPE_NAME_MAP[baseschema]
            except KeyError:
                btype = str(baseschema)
            try:
182
183
                objects = self[btype]
                assert len(objects) == 1, objects
184
185
186
187
188
189
                if btype == etype:
                    cls = objects[0]
                else:
                    # recurse to ensure issubclass(etype_class('Child'),
                    #                              etype_class('Parent'))
                    cls = self.etype_class(btype)
190
191
192
193
194
195
                break
            except ObjectNotFound:
                pass
        else:
            # no entity class for any of the ancestors, fallback to the default
            # one
196
197
198
            objects = self['Any']
            assert len(objects) == 1, objects
            cls = objects[0]
199
        # make a copy event if cls.__regid__ == etype, else we may have pb for
Sylvain Thénault's avatar
Sylvain Thénault committed
200
201
        # client application using multiple connections to different
        # repositories (eg shingouz)
202
203
204
205
        # __autogenerated__ attribute is just a marker
        cls = type(str(etype), (cls,), {'__autogenerated__': True,
                                        '__doc__': cls.__doc__,
                                        '__module__': cls.__module__})
206
        cls.__regid__ = etype
207
        cls.__initialize__(self.schema)
208
209
        return cls

210
211
212
213
214
215
216
217
218
219
    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)

220
221
222

class ViewsRegistry(CWRegistry):

Sylvain Thénault's avatar
Sylvain Thénault committed
223
    def main_template(self, req, oid='main-template', rset=None, **kwargs):
224
225
226
227
        """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
        """
Sylvain Thénault's avatar
Sylvain Thénault committed
228
229
        obj = self.select(oid, req, rset=rset, **kwargs)
        res = obj.render(**kwargs)
Denis Laxalde's avatar
Denis Laxalde committed
230
        if isinstance(res, str):
231
            return res.encode(req.encoding)
Denis Laxalde's avatar
Denis Laxalde committed
232
        assert isinstance(res, bytes)
233
234
235
236
237
238
239
240
241
242
        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
243
244
            views = [view for view in views
                     if not isinstance(view, class_deprecated)]
245
            try:
246
                view = self._select_best(views, req, rset=rset, **kwargs)
247
                if view is not None and view.linkable():
248
249
250
251
252
253
254
                    yield view
            except Exception:
                self.exception('error while trying to select %s view for %s',
                               vid, rset)


class ActionsRegistry(CWRegistry):
255
256
257
258
    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)
259
260
261

    def possible_actions(self, req, rset=None, **kwargs):
        if rset is None:
262
            actions = self.poss_visible_objects(req, rset=rset, **kwargs)
263
        else:
Denis Laxalde's avatar
Denis Laxalde committed
264
            actions = rset.possible_actions(**kwargs)  # cached implementation
265
266
267
268
269
270
        result = {}
        for action in actions:
            result.setdefault(action.category, []).append(action)
        return result


271
272
273
274
class CtxComponentsRegistry(CWRegistry):
    def poss_visible_objects(self, *args, **kwargs):
        """return an ordered list of possible components"""
        context = kwargs.pop('context')
275
276
277
        if '__cache' in kwargs:
            cache = kwargs.pop('__cache')
        elif kwargs.get('rset') is None:
278
279
280
281
282
283
284
285
            cache = args[0]
        else:
            cache = kwargs['rset']
        try:
            cached = cache.__components_cache
        except AttributeError:
            ctxcomps = super(CtxComponentsRegistry, self).poss_visible_objects(
                *args, **kwargs)
286
287
288
289
290
291
292
293
            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
294
295
            cached = cache.__components_cache = {}
            for component in ctxcomps:
296
297
298
                cctx = component.cw_propval('context')
                component.cw_extra_kwargs['context'] = cctx
                cached.setdefault(cctx, []).append(component)
299
300
301
302
303
304
305
        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


306
class CWRegistryStore(RegistryStore):
307
    """Central registry for the cubicweb instance, extending the generic
308
    RegistryStore with some cubicweb specific stuff.
309

310
    This is one of the central object in cubicweb instance, coupling
311
312
    dynamically loaded objects with the schema and the configuration objects.

313
    It specializes the RegistryStore by adding some convenience methods to access to
314
    stored objects. Currently we have the following registries of objects known
315
    by the web instance (library may use some others additional registries):
316

317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
    * '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
334
    """
335

336
337
338
339
340
    REGISTRY_FACTORY = {None: CWRegistry,
                        'etypes': ETypeRegistry,
                        'views': ViewsRegistry,
                        'actions': ActionsRegistry,
                        'ctxcomponents': CtxComponentsRegistry,
341
                        'uicfg': InstancesRegistry,
342
343
                        }

344
    def __init__(self, config, initlog=True):
345
346
        if initlog:
            # first init log service
347
            config.init_log()
348
349
350
351
352
353
354
355
356
357
358
359
        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)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
360
361
        self.schema = None
        self.initialized = False
362

363
364
365
366
367
368
369
    def setdefault(self, regid):
        try:
            return self[regid]
        except RegistryNotFound:
            self[regid] = self.registry_class(regid)(self)
            return self[regid]

Adrien Di Mascio's avatar
Adrien Di Mascio committed
370
    def items(self):
371
        return [item for item in super(CWRegistryStore, self).items()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
372
                if not item[0] in ('propertydefs', 'propertyvalues')]
Denis Laxalde's avatar
Denis Laxalde committed
373

374
    def iteritems(self):
375
        return (item for item in super(CWRegistryStore, self).items()
376
                if not item[0] in ('propertydefs', 'propertyvalues'))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
377
378

    def values(self):
379
        return [value for key, value in self.items()]
Denis Laxalde's avatar
Denis Laxalde committed
380

381
382
    def itervalues(self):
        return (value for key, value in self.items())
383

384
    def reset(self):
385
        CW_EVENT_MANAGER.emit('before-registry-reset', self)
386
        super(CWRegistryStore, self).reset()
387
        self._needs_appobject = {}
388
389
390
        # two special registries, propertydefs which care all the property
        # definitions, and propertyvals which contains values for those
        # properties
391
392
393
        if not self.initialized:
            self['propertydefs'] = {}
            self['propertyvalues'] = self.eprop_values = {}
394
            for key, propdef in self.config.cwproperty_definitions():
395
                self.register_property(key, **propdef)
396
        CW_EVENT_MANAGER.emit('after-registry-reset', self)
397

398
399
400
401
402
403
404
405
406
407
408
    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)

Adrien Di Mascio's avatar
Adrien Di Mascio committed
409
    def set_schema(self, schema):
410
        """set instance'schema and load application objects"""
411
        self._set_schema(schema)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
412
        # now we can load application's web objects
413
        self.reload(self.config.appobjects_modnames(), force_reload=False)
414
415
        # map lowered entity type names to their actual name
        self.case_insensitive_etypes = {}
416
417
        for eschema in self.schema.entities():
            etype = str(eschema)
418
            self.case_insensitive_etypes[etype.lower()] = etype
419
420
            clear_cache(eschema, 'ordered_relations')
            clear_cache(eschema, 'meta_attributes')
421

422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
    def is_reload_needed(self, modnames):
        """overriden to handle modules names instead of directories"""
        lastmodifs = self._lastmodifs
        for modname in modnames:
            if modname not in sys.modules:
                # new module to load
                return True
            filepath = sys.modules[modname].__file__
            if filepath.endswith('.py'):
                mdate = self._mdate(filepath)
                if filepath not in lastmodifs or lastmodifs[filepath] < mdate:
                    self.info('File %s changed since last visit', filepath)
                    return True
        return False

437
    def reload_if_needed(self):
438
439
440
        modnames = self.config.appobjects_modnames()
        if self.is_reload_needed(modnames):
            self.reload(modnames)
441

442
443
    def _cleanup_sys_modules(self, modnames):
        """Remove modules and submodules of `modnames` from `sys.modules` and cleanup
444
445
446
447
448
449
450
451
452
453
454
455
456
        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
457
        deleted_modules = set(clean_sys_modules(modnames))
458
459
460
461
462
463
        for callbacklist in callbackdata:
            for callback in callbacklist[:]:
                module = caches[id(callback)]
                if module and module in deleted_modules:
                    callbacklist.remove(callback)

464
    def reload(self, modnames, force_reload=True):
465
        """modification detected, reset and reload the vreg"""
466
        CW_EVENT_MANAGER.emit('before-registry-reload')
467
        if force_reload:
468
            self._cleanup_sys_modules(modnames)
469
470
471
472
473
474
            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):
Denis Laxalde's avatar
Denis Laxalde committed
475
                if cube not in cubes:
476
477
478
                    cube_modnames = cfg.appobjects_cube_modnames(cube)
                    self._cleanup_sys_modules(cube_modnames)
        self.register_modnames(modnames)
479
480
        CW_EVENT_MANAGER.emit('after-registry-reload')

481
482
483
484
485
486
487
488
489
    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)

490
491
492
493
494
    def _set_schema(self, schema):
        """set instance'schema"""
        self.schema = schema
        clear_cache(self, 'rqlhelper')

Adrien Di Mascio's avatar
Adrien Di Mascio committed
495
496
497
498
499
    def update_schema(self, schema):
        """update .schema attribute on registered objects, necessary for some
        tests
        """
        self.schema = schema
500
        for registry, regcontent in self.items():
501
            for objects in regcontent.values():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
502
503
                for obj in objects:
                    obj.schema = schema
504

505
    def register(self, obj, *args, **kwargs):
506
507
508
509
510
511
512
        """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.
        """
513
        obj = related_appobject(obj)
514
        super(CWRegistryStore, self).register(obj, *args, **kwargs)
515
516
517
        depends_on = require_appobject(obj)
        if depends_on is not None:
            self._needs_appobject[obj] = depends_on
518

sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
519
    def initialization_completed(self):
520
521
        """cw specific code once vreg initialization is completed:

522
523
        * remove objects requiring a missing appobject, unless
          config.cleanup_unused_appobjects is false
524
525
        * init rtags
        """
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
526
527
        # we may want to keep interface dependent objects (e.g.for i18n
        # catalog generation)
528
        if self.config.cleanup_unused_appobjects:
Aurelien Campeas's avatar
Aurelien Campeas committed
529
            # remove appobjects which depend on other, unexistant appobjects
530
            for obj, (regname, regids) in self._needs_appobject.items():
531
532
533
                try:
                    registry = self[regname]
                except RegistryNotFound:
534
                    self.debug('unregister %s (no registry %s)', obj, regname)
535
536
                    self.unregister(obj)
                    continue
537
538
539
540
                for regid in regids:
                    if registry.get(regid):
                        break
                else:
541
                    self.debug('unregister %s (no %s object in registry %s)',
542
                               registry.objid(obj), ' or '.join(regids), regname)
543
                    self.unregister(obj)
544
        super(CWRegistryStore, self).initialization_completed()
Denis Laxalde's avatar
Denis Laxalde committed
545
        if 'uicfg' in self:  # 'uicfg' is not loaded in a pure repository mode
546
            for rtags in self['uicfg'].values():
547
                for rtag in rtags:
548
549
                    # don't check rtags if we don't want to cleanup_unused_appobjects
                    rtag.init(self.schema, check=self.config.cleanup_unused_appobjects)
550

551
    # rql parsing utilities ####################################################
552
553

    @property
Adrien Di Mascio's avatar
Adrien Di Mascio committed
554
    @cached
555
556
557
    def rqlhelper(self):
        return RQLHelper(self.schema,
                         special_relations={'eid': 'uid', 'has_text': 'fti'})
558

559
560
    def solutions(self, req, rqlst, args):
        def type_from_eid(eid, req=req):
561
            return req.entity_type(eid)
562
        return self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
563

564
565
566
567
568
569
570
571
    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
Adrien Di Mascio's avatar
Adrien Di Mascio committed
572
573
574
575
576

    # properties handling #####################################################

    def user_property_keys(self, withsitewide=False):
        if withsitewide:
577
578
            return sorted(k for k in self['propertydefs']
                          if not k.startswith('sources.'))
579
        return sorted(k for k, kd in self['propertydefs'].items()
580
                      if not kd['sitewide'] and not k.startswith('sources.'))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
581
582
583
584

    def register_property(self, key, type, help, default=None, vocabulary=None,
                          sitewide=False):
        """register a given property"""
585
        properties = self['propertydefs']
586
        assert type in YAMS_TO_PY, 'unknown type %s' % type
587
        properties[key] = {'type': type, 'vocabulary': vocabulary,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
588
589
590
591
592
593
594
595
596
                           '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:
597
            return self['propertydefs'][key]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
598
599
600
601
602
603
604
        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)
605

Adrien Di Mascio's avatar
Adrien Di Mascio committed
606
607
    def property_value(self, key):
        try:
608
            return self['propertyvalues'][key]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
609
        except KeyError:
610
            return self.property_info(key)['default']
Adrien Di Mascio's avatar
Adrien Di Mascio committed
611
612

    def typed_value(self, key, value):
613
        """value is a unicode string, return it correctly typed. Let potential
Adrien Di Mascio's avatar
Adrien Di Mascio committed
614
615
616
617
618
619
620
621
622
623
        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):
Denis Laxalde's avatar
Denis Laxalde committed
624
625
                vocab = vocab(None)  # XXX need a req object
            if value not in vocab:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
626
627
                raise ValueError(_('unauthorized value'))
        return value
628

Adrien Di Mascio's avatar
Adrien Di Mascio committed
629
630
631
632
    def init_properties(self, propvalues):
        """init the property values registry using the given set of couple (key, value)
        """
        self.initialized = True
633
        values = self['propertyvalues']
Adrien Di Mascio's avatar
Adrien Di Mascio committed
634
635
636
        for key, val in propvalues:
            try:
                values[key] = self.typed_value(key, val)
637
            except ValueError as ex:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
638
639
                self.warning('%s (you should probably delete that property '
                             'from the database)', ex)
640
            except UnknownProperty as ex:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
641
642
643
                self.warning('%s (you should probably delete that property '
                             'from the database)', ex)

644

645
646
647
# XXX unify with yams.constraints.BASE_CONVERTERS?
YAMS_TO_PY = BASE_CONVERTERS.copy()
YAMS_TO_PY.update({
Denis Laxalde's avatar
Denis Laxalde committed
648
649
650
    'Bytes': Binary,
    'Date': date,
    'Datetime': datetime,
651
    'TZDatetime': datetime,
Denis Laxalde's avatar
Denis Laxalde committed
652
653
654
655
    'Time': time,
    'TZTime': time,
    'Interval': timedelta,
})