buildobjs.py 16.5 KB
Newer Older
root's avatar
root committed
1
"""defines classes used to build a schema
Sylvain's avatar
Sylvain committed
2
3
4
5

:organization: Logilab
:copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
root's avatar
root committed
6
"""
Sylvain's avatar
Sylvain committed
7
__docformat__ = "restructuredtext en"
root's avatar
root committed
8

Sylvain's avatar
Sylvain committed
9
10
from logilab.common import attrdict
from logilab.common.compat import sorted
11

Sylvain's avatar
Sylvain committed
12
from yams import BASE_TYPES, MARKER, BadSchemaDefinition
"Sylvain ext:(%22)'s avatar
"Sylvain ext:(%22) committed
13
14
from yams.constraints import SizeConstraint, UniqueConstraint, \
     StaticVocabularyConstraint
root's avatar
root committed
15

Adrien Di Mascio's avatar
Adrien Di Mascio committed
16
17
__all__ = ('ObjectRelation', 'SubjectRelation', 'BothWayRelation',
           'RelationDefinition', 'EntityType', 'MetaEntityType',
18
19
20
21
           'RestrictedEntityType', 'UserEntityType', 'MetaUserEntityType',
           'RelationType', 'MetaRelationType', 'UserRelationType',
           'MetaUserRelationType', 'AttributeRelationType',
           'MetaAttributeRelationType',
Adrien Di Mascio's avatar
Adrien Di Mascio committed
22
23
24
           'SubjectRelation', 'ObjectRelation', 'BothWayRelation',
           ) + tuple(BASE_TYPES)

Sylvain's avatar
Sylvain committed
25
26
27
28
29
30
ETYPE_PROPERTIES = ('meta', 'description', 'permissions')
RTYPE_PROPERTIES = ('meta', 'symetric', 'inlined', 'description', 'permissions')
RDEF_PROPERTIES = ('cardinality', 'constraints', 'composite',
                   'order', 'description',
                   'default', 'uid', 'indexed', 'uid', 
                   'fulltextindexed', 'internationalizable')
root's avatar
root committed
31

Sylvain's avatar
Sylvain committed
32
REL_PROPERTIES = RTYPE_PROPERTIES+RDEF_PROPERTIES
33
34


"Sylvain ext:(%22)'s avatar
"Sylvain ext:(%22) committed
35
def add_constraint(kwargs, constraint):
36
37
38
39
40
41
42
    constraints = kwargs.setdefault('constraints', [])
    for i, existingconstraint in enumerate(constraints):
        if existingconstraint.__class__ is constraint.__class__:
            constraints[i] = constraint
            return
    constraints.append(constraint)
        
Sylvain's avatar
Sylvain committed
43
44
45
46
47
48
49
50
def add_relation(relations, rdef, name=None):
    if isinstance(rdef, BothWayRelation):
        add_relation(relations, rdef.subjectrel, name)
        add_relation(relations, rdef.objectrel, name)
    else:
        if name is not None:
            rdef.name = name
        relations.append(rdef)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
51

Sylvain's avatar
Sylvain committed
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def check_kwargs(kwargs, attributes):
    for key in kwargs:
        if not key in attributes: 
            raise BadSchemaDefinition('no such property %r' % key)
    
def copy_attributes(fromobj, toobj, attributes):    
    for attr in attributes:
        value = getattr(fromobj, attr, MARKER)
        if value is MARKER:
            continue
        ovalue = getattr(toobj, attr, MARKER)
        if not ovalue is MARKER and value != ovalue:
            raise BadSchemaDefinition('conflicting values %s/%s for property %s of %s'
                                      % (ovalue, value, attr, toobj))
        setattr(toobj, attr, value)

class Relation(object):
    """abstract class which have to be defined before the metadefinition
    meta-class
    """
Adrien Di Mascio's avatar
Adrien Di Mascio committed
72

Sylvain's avatar
Sylvain committed
73
# first class schema definition objects #######################################
74

Adrien Di Mascio's avatar
Adrien Di Mascio committed
75
class Definition(object):
root's avatar
root committed
76
    """abstract class for entity / relation definition classes"""
77

Sylvain's avatar
Sylvain committed
78
79
    meta = MARKER
    description = MARKER
root's avatar
root committed
80
    
Sylvain's avatar
Sylvain committed
81
    def __init__(self, name=None):
82
83
        self.name = (name or getattr(self, 'name', None)
                     or self.__class__.__name__)
Sylvain's avatar
Sylvain committed
84
85
        if self.__doc__:
            self.description = ' '.join(self.__doc__.split())
root's avatar
root committed
86

Sylvain's avatar
Sylvain committed
87
88
89
90
91
92
93
94
    def __repr__(self):
        return '<%s %r @%x>' % (self.__class__.__name__, self.name, id(self))

    def expand_type_definitions(self, defined):
        """schema building step 1:

        register definition objects by adding them to the `defined` dictionnary
        """
root's avatar
root committed
95
        raise NotImplementedError()
Sylvain's avatar
Sylvain committed
96
97
98
    
    def expand_relation_definitions(self, defined, schema):
        """schema building step 2:
root's avatar
root committed
99

Sylvain's avatar
Sylvain committed
100
101
102
        register all relations definition, expanding wildcard if necessary
        """
        raise NotImplementedError()
root's avatar
root committed
103
104


Adrien Di Mascio's avatar
Adrien Di Mascio committed
105
class metadefinition(type):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
106
107
108
    """this metaclass builds the __relations__ attribute
    of EntityType's subclasses
    """
Adrien Di Mascio's avatar
Adrien Di Mascio committed
109
    def __new__(mcs, name, bases, classdict):
110
111
112
113
        classdict['__relations__'] = rels = []
        relations = {}
        for rname, rdef in classdict.items():
            if isinstance(rdef, Relation):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
114
115
                # relation's name **must** be removed from class namespace
                # to avoid conflicts with instance's potential attributes
116
117
118
119
                del classdict[rname]
                relations[rname] = rdef
        defclass = super(metadefinition, mcs).__new__(mcs, name, bases, classdict)
        for rname, rdef in relations.items():
Sylvain's avatar
Sylvain committed
120
121
            add_relation(defclass.__relations__, rdef, rname)
        # take base classes'relations into account
Adrien Di Mascio's avatar
Adrien Di Mascio committed
122
123
        for base in bases:
            rels.extend(getattr(base, '__relations__', []))
124
125
126
        # sort relations by creation rank
        defclass.__relations__ = sorted(rels, key=lambda r: r.creation_rank)
        return defclass
Adrien Di Mascio's avatar
Adrien Di Mascio committed
127
128
    
        
root's avatar
root committed
129
class EntityType(Definition):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
130
131

    __metaclass__ = metadefinition
Sylvain's avatar
Sylvain committed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
        
    def __init__(self, name=None, **kwargs):
        super(EntityType, self).__init__(name)
        check_kwargs(kwargs, ETYPE_PROPERTIES)
        copy_attributes(attrdict(kwargs), self, ETYPE_PROPERTIES)
        # if not hasattr(self, 'relations'):
        self.relations = list(self.__relations__)

    def __str__(self):
        return 'entity type %r' % self.name
    
    def expand_type_definitions(self, defined):
        """schema building step 1:

        register definition objects by adding them to the `defined` dictionnary
        """
        assert not self.name in defined
        defined[self.name] = self
        for relation in self.relations:
            rtype = RelationType(relation.name)
            copy_attributes(relation, rtype, RTYPE_PROPERTIES)
            if relation.name in defined:
                copy_attributes(rtype, defined[relation.name], RTYPE_PROPERTIES)
            else:
                defined[relation.name] = rtype
        
    def expand_relation_definitions(self, defined, schema):
        """schema building step 2:

        register all relations definition, expanding wildcards if necessary
        """
        order = 1
        for relation in self.relations:
            if isinstance(relation, SubjectRelation):
                rdef = RelationDefinition(subject=self.name, name=relation.name,
                                          object=relation.etype, order=order)
                copy_attributes(relation, rdef, RDEF_PROPERTIES)
            elif isinstance(relation, ObjectRelation):
                rdef = RelationDefinition(subject=relation.etype,
                                          name=relation.name,
                                          object=self.name, order=order)
                copy_attributes(relation, rdef, RDEF_PROPERTIES)
            else:
                raise BadSchemaDefinition('dunno how to handle %s' % relation)
            order += 1
            rdef._add_relations(defined, schema)
178

Sylvain's avatar
Sylvain committed
179
180
    # methods that can be used to extend an existant schema definition ########
    
181
    def extend(self, othermetadefcls):
182
        for rdef in othermetadefcls.__relations__:
183
            self.add_relation(rdef)
Sylvain's avatar
Sylvain committed
184
            
185
    def add_relation(self, rdef, name=None):
Sylvain's avatar
Sylvain committed
186
        add_relation(self.relations, rdef, name)
187
            
188
    def remove_relation(self, name):
Sylvain's avatar
Sylvain committed
189
        for rdef in self._get_relations(name):
190
            self.relations.remove(rdef)
191
            
Sylvain's avatar
Sylvain committed
192
193
194
    def _get_relations(self, name):
        """get relation definitions by name (may have multiple definitions with
        the same name if the relation is both a subject and object relation)
195
        """
196
        for rdef in self.relations[:]:
197
198
            if rdef.name == name:
                yield rdef
Adrien Di Mascio's avatar
Adrien Di Mascio committed
199

root's avatar
root committed
200
    
Sylvain's avatar
Sylvain committed
201
202
203
class RelationType(Definition):
    symetric = MARKER
    inlined = MARKER
syt's avatar
use set    
syt committed
204
    
Sylvain's avatar
Sylvain committed
205
206
207
208
209
210
211
    def __init__(self, name=None, **kwargs):
        super(RelationType, self).__init__(name)
        check_kwargs(kwargs, RTYPE_PROPERTIES)
        copy_attributes(attrdict(kwargs), self, RTYPE_PROPERTIES)

    def __str__(self):
        return 'relation type %r' % self.name
root's avatar
root committed
212
    
Sylvain's avatar
Sylvain committed
213
214
    def expand_type_definitions(self, defined):
        """schema building step 1:
root's avatar
root committed
215

Sylvain's avatar
Sylvain committed
216
217
218
219
220
221
222
223
224
225
        register definition objects by adding them to the `defined` dictionnary
        """
        if self.name in defined:
            copy_attributes(self, defined[self.name],
                            REL_PROPERTIES + ('subject', 'object'))
        else:
            defined[self.name] = self
            
    def expand_relation_definitions(self, defined, schema):
        """schema building step 2:
root's avatar
root committed
226

Sylvain's avatar
Sylvain committed
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
        register all relations definition, expanding wildcard if necessary
        """
        if getattr(self, 'subject', None) or getattr(self, 'object', None):
            assert self.subject and self.object
            rdef = RelationDefinition(subject=self.subject, name=self.name,
                                      object=self.object)
            copy_attributes(self, rdef, RDEF_PROPERTIES)
            rdef._add_relations(defined, schema)


class RelationDefinition(Definition):
    subject = MARKER
    object = MARKER
    cardinality = MARKER
    constraints = MARKER
    symetric = MARKER
    inlined = MARKER
    
    def __init__(self, subject=None, name=None, object=None,**kwargs):
246
247
248
249
250
251
252
253
        if subject:
            self.subject = subject
        else:
            self.subject = self.__class__.subject
        if object:
            self.object = object
        else:
            self.object = self.__class__.object
Sylvain's avatar
Sylvain committed
254
255
256
257
258
        super(RelationDefinition, self).__init__(name)
        check_kwargs(kwargs, RDEF_PROPERTIES)
        copy_attributes(attrdict(kwargs), self, RDEF_PROPERTIES)
        if self.constraints:
            self.constraints = list(self.constraints)
root's avatar
root committed
259
        
Sylvain's avatar
Sylvain committed
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
    def __str__(self):
        return 'relation definition (%(subject)s %(name)s %(object)s)' % self.__dict__

    def expand_type_definitions(self, defined):
        """schema building step 1:

        register definition objects by adding them to the `defined` dictionnary
        """
        rtype = RelationType(self.name)
        copy_attributes(self, rtype, RTYPE_PROPERTIES)
        if self.name in defined:
            copy_attributes(rtype, defined[self.name], RTYPE_PROPERTIES)
        else:
            defined[self.name] = rtype
        key = (self.subject, self.name, self.object)
        if key in defined:
            raise BadSchemaDefinition('duplicated relation definition %r'
                                      % self)
        defined[key] = self
root's avatar
root committed
279
        
Sylvain's avatar
Sylvain committed
280
281
282
283
284
285
286
    def expand_relation_definitions(self, defined, schema):
        """schema building step 2:

        register all relations definition, expanding wildcard if necessary
        """
        assert self.subject and self.object
        self._add_relations(defined, schema)
287
    
Sylvain's avatar
Sylvain committed
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
    def _add_relations(self, defined, schema):
        rtype = defined[self.name]
        copy_attributes(rtype, self, RDEF_PROPERTIES)
        # process default cardinality and constraints if not set yet
        cardinality = self.cardinality
        if cardinality is MARKER:
            if self.object in BASE_TYPES:
                self.cardinality = '?1'
            else:
                self.cardinality = '**'
        else:
            assert len(cardinality) == 2
            assert cardinality[0] in '1?+*'
            assert cardinality[1] in '1?+*'
        if not self.constraints:
            self.constraints = ()
        rschema = schema.rschema(self.name)
        for subj in self._actual_types(schema, self.subject):
            for obj in self._actual_types(schema, self.object):
                rdef = RelationDefinition(subj, self.name, obj)
                copy_attributes(self, rdef, RDEF_PROPERTIES)
                schema.add_relation_def(rdef)
                    
    def _actual_types(self, schema, etype):
        if etype == '*':
            return self._wildcard_etypes(schema)
        elif etype == '**':
            return self._pow_etypes(schema)
        elif isinstance(etype, (tuple, list)):
            return etype
        return (etype,)
        
    def _wildcard_etypes(self, schema):
        for eschema in schema.entities():
            if eschema.is_final() or eschema.meta:
                continue
            yield eschema.type
        
    def _pow_etypes(self, schema):
        for eschema in schema.entities():
            if eschema.is_final():
                continue
            yield eschema.type


# various derivated classes with some predefined values #######################
root's avatar
root committed
334
335
336
337
338
339
340
341
342
343

class MetaEntityType(EntityType):
    permissions = {
        'read':   ('managers', 'users', 'guests',),
        'add':    ('managers',),
        'delete': ('managers',),
        'update': ('managers', 'owners',),
        }
    meta = True

344
345
346
347
348
349
350
351
class RestrictedEntityType(MetaEntityType):
    permissions = {
        'read':   ('managers', 'users',),
        'add':    ('managers',),
        'delete': ('managers',),
        'update': ('managers', 'owners',),
        }

root's avatar
root committed
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
class UserEntityType(EntityType):
    permissions = {
        'read':   ('managers', 'users', 'guests',),
        'add':    ('managers', 'users',),
        'delete': ('managers', 'owners',),
        'update': ('managers', 'owners',),
        }
    
class MetaUserEntityType(UserEntityType):
    meta = True


class MetaRelationType(RelationType):
    permissions = {
        'read':   ('managers', 'users', 'guests',),
        'add':    ('managers',),
        'delete': ('managers',),
        }
    meta = True

class UserRelationType(RelationType):
    permissions = {
        'read':   ('managers', 'users', 'guests',),
        'add':    ('managers', 'users',),
        'delete': ('managers', 'users',),
        }

class MetaUserRelationType(UserRelationType):
    meta = True
    

class AttributeRelationType(RelationType):
    # just set permissions to None so default permissions are set
Sylvain's avatar
Sylvain committed
385
    permissions = MARKER
root's avatar
root committed
386
387
388
    
class MetaAttributeRelationType(AttributeRelationType):
    meta = True
Sylvain's avatar
Sylvain committed
389

root's avatar
root committed
390
    
Sylvain's avatar
Sylvain committed
391
392
393
394
395
396
397
398
399
400
401
# classes used to define relation within entity type classes ##################


# \(Object\|Subject\)Relation(relations, '\([a-z_A-Z]+\)',
# -->
# \2 = \1Relation(

class ObjectRelation(Relation):
    cardinality = MARKER
    constraints = MARKER
    created = 0
root's avatar
root committed
402

Sylvain's avatar
Sylvain committed
403
404
405
406
407
408
409
410
411
    def __init__(self, etype, **kwargs):
        ObjectRelation.created += 1
        self.creation_rank = ObjectRelation.created
        self.name = '<undefined>'
        self.etype = etype
        if self.constraints:
            self.constraints = list(self.constraints)
        check_kwargs(kwargs, REL_PROPERTIES)
        copy_attributes(attrdict(kwargs), self, REL_PROPERTIES)
root's avatar
root committed
412
    
Sylvain's avatar
Sylvain committed
413
414
415
    def __repr__(self):
        return '%(name)s %(etype)s' % self.__dict__

root's avatar
root committed
416
        
Sylvain's avatar
Sylvain committed
417
418
419
420
421
422
class SubjectRelation(ObjectRelation):
    uid = MARKER
    indexed = MARKER
    fulltextindexed = MARKER
    internationalizable = MARKER
    default = MARKER
root's avatar
root committed
423
    
Sylvain's avatar
Sylvain committed
424
425
    def __repr__(self):
        return '%(etype)s %(name)s' % self.__dict__
root's avatar
root committed
426

Sylvain's avatar
Sylvain committed
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477

class BothWayRelation(Relation):

    def __init__(self, subjectrel, objectrel):
        assert isinstance(subjectrel, SubjectRelation)
        assert isinstance(objectrel, ObjectRelation)
        self.subjectrel = subjectrel
        self.objectrel = objectrel
        self.creation_rank = subjectrel.creation_rank


class AbstractTypedAttribute(SubjectRelation):
    """AbstractTypedAttribute is not directly instantiable
    
    subclasses must provide a <etype> attribute to be instantiable
    """
    def __init__(self, **kwargs):
        required = kwargs.pop('required', False)
        if required:
            cardinality = '11'
        else:
            cardinality = '?1'
        kwargs['cardinality'] = cardinality
        maxsize = kwargs.pop('maxsize', None)
        if maxsize is not None:
            add_constraint(kwargs, SizeConstraint(max=maxsize))
        vocabulary = kwargs.pop('vocabulary', None)
        if vocabulary is not None:
            self.set_vocabulary(vocabulary, kwargs)
        unique = kwargs.pop('unique', None)
        if unique:
            add_constraint(kwargs, UniqueConstraint())
        # use the etype attribute provided by subclasses
        super(AbstractTypedAttribute, self).__init__(self.etype, **kwargs)

    def set_vocabulary(self, vocabulary, kwargs=None):
        if kwargs is None:
            kwargs = self.__dict__
        #constraints = kwargs.setdefault('constraints', [])
        add_constraint(kwargs, StaticVocabularyConstraint(vocabulary))
        if self.__class__.__name__ == 'String': # XXX
            maxsize = max(len(x) for x in vocabulary)
            add_constraint(kwargs, SizeConstraint(max=maxsize))
            
    def __repr__(self):
        return '<%(name)s(%(etype)s)>' % self.__dict__
        
# build a specific class for each base type
for basetype in BASE_TYPES:
    globals()[basetype] = type(basetype, (AbstractTypedAttribute,),
                               {'etype' : basetype})
root's avatar
root committed
478