Commit f3d758cd authored by Sylvain's avatar Sylvain
Browse files

schema building refactoring

parent ad6bcf3df1e8
......@@ -2,8 +2,12 @@ ChangeLog for yams
------------------
--
* heavy refactoring of the schema building process
* rename rproperty_keys to iter_rdef, new has_rdef method
* use dbhelper to generate index sql
* don't use ordered_relation when it's not necessary
* removed deprecated code
2007-10-29 -- 0.14.0
* schema building refactoring to read schema from a bunch of directories
* drop .perms file support
......
"""model object and utilities to define generic Entities/Relations schemas
:organization: Logilab
:copyright: 2004-2007 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:copyright: 2004-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
# set _ builtin to unicode by default, should be overriden if necessary
import __builtin__
__builtin__._ = unicode
from logilab.common.compat import set
BASE_TYPES = set(('String', 'Int', 'Float', 'Boolean', 'Date',
'Time', 'Datetime', 'Interval', 'Password', 'Bytes'))
from logilab.common import nullobject
MARKER = nullobject()
class FileReader(object):
"""abstract class for file readers"""
def __init__(self, loader, defaulthandler=None, readdeprecated=False):
self.loader = loader
self.default_hdlr = defaulthandler
self.read_deprecated = readdeprecated
self._current_file = None
self._current_line = None
self._current_lineno = None
def __call__(self, filepath):
self._current_file = filepath
self.read_file(filepath)
def error(self, msg=None):
"""raise a contextual exception"""
raise BadSchemaDefinition(self._current_line, self._current_file, msg)
def read_file(self, filepath):
"""default implementation, calling .read_line method for each
non blank lines, and ignoring lines starting by '#' which are
considered as comment lines
"""
for i, line in enumerate(file(filepath)):
line = line.strip()
if not line or line.startswith('#'):
continue
self._current_line = line
self._current_lineno = i
if line.startswith('//'):
if self.read_deprecated:
self.read_line(line[2:])
else:
self.read_line(line)
def read_line(self, line):
"""need overriding !"""
raise NotImplementedError()
from yams._exceptions import *
from yams.schema import Schema, EntitySchema, RelationSchema
from yams.reader import SchemaLoader
# set _ builtin to unicode by default, should be overriden if necessary
import __builtin__
__builtin__._ = unicode
"""Exceptions shared by different ER-Schema modules.
:organization: Logilab
:copyright: 2004-2007 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:copyright: 2004-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
......
......@@ -6,18 +6,13 @@
"""
__docformat__ = "restructuredtext en"
__docformat__ = "restructuredtext en"
__metaclass__ = type
from logilab.common.compat import set, sorted
from logilab.common import attrdict
from logilab.common.compat import sorted
from yams import BadSchemaDefinition
from yams import BASE_TYPES, MARKER, BadSchemaDefinition
from yams.constraints import SizeConstraint, UniqueConstraint, \
StaticVocabularyConstraint
BASE_TYPES = set(('String', 'Int', 'Float', 'Boolean', 'Date',
'Time', 'Datetime', 'Interval', 'Password', 'Bytes'))
__all__ = ('ObjectRelation', 'SubjectRelation', 'BothWayRelation',
'RelationDefinition', 'EntityType', 'MetaEntityType',
'RestrictedEntityType', 'UserEntityType', 'MetaUserEntityType',
......@@ -27,50 +22,16 @@ __all__ = ('ObjectRelation', 'SubjectRelation', 'BothWayRelation',
'SubjectRelation', 'ObjectRelation', 'BothWayRelation',
) + tuple(BASE_TYPES)
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')
# \(Object\|Subject\)Relation(relations, '\([a-z_A-Z]+\)',
# -->
# \2 = \1Relation(
class Relation(object): pass
class ObjectRelation(Relation):
cardinality = None
constraints = ()
created = 0
def __init__(self, etype, **kwargs):
ObjectRelation.created += 1
self.creation_rank = ObjectRelation.created
self.name = '<undefined>'
self.etype = etype
self.constraints = list(self.constraints)
self.__dict__.update(kwargs)
def __repr__(self):
return '%(name)s %(etype)s' % self.__dict__
class SubjectRelation(ObjectRelation):
uid = False
indexed = False
fulltextindexed = False
internationalizable = False
default = None
def __repr__(self):
return '%(etype)s %(name)s' % self.__dict__
REL_PROPERTIES = RTYPE_PROPERTIES+RDEF_PROPERTIES
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
def add_constraint(kwargs, constraint):
constraints = kwargs.setdefault('constraints', [])
for i, existingconstraint in enumerate(constraints):
......@@ -78,113 +39,67 @@ def add_constraint(kwargs, constraint):
constraints[i] = constraint
return
constraints.append(constraint)
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})
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)
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
"""
RDEF_PROPERTIES = ('meta', 'symetric', 'inlined',
'cardinality', 'constraints', 'composite',
'order', 'description',
'default', 'uid', 'indexed', 'uid',
'fulltextindexed', 'internationalizable')
# first class schema definition objects #######################################
def copy_attributes(fromobj, toobj):
for attr in RDEF_PROPERTIES:
try:
setattr(toobj, attr, getattr(fromobj, attr))
except AttributeError:
continue
class Definition(object):
"""abstract class for entity / relation definition classes"""
meta = False
subject, object = None, None
meta = MARKER
description = MARKER
def __init__(self, name=None, **kwargs):
self.__dict__.update(kwargs)
def __init__(self, name=None):
self.name = (name or getattr(self, 'name', None)
or self.__class__.__name__)
# XXX check properties
#for key, val in kwargs.items():
# if not hasattr(self, key):
# self.error('no such property %r' % key)
if self.__doc__:
self.description = ' '.join(self.__doc__.split())
def register_relations(self, schema):
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
"""
raise NotImplementedError()
def expand_relation_definitions(self, defined, schema):
"""schema building step 2:
def add_relations(self, schema):
try:
rschema = schema.rschema(self.name)
except KeyError:
# .rel file compat: the relation type may have not been added
rtype = RelationType(**self.__dict__)
rschema = schema.add_relation_type(rtype)
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)
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
register all relations definition, expanding wildcard if necessary
"""
raise NotImplementedError()
class metadefinition(type):
......@@ -211,20 +126,58 @@ class metadefinition(type):
return defclass
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)
class EntityType(Definition):
__metaclass__ = metadefinition
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)
# methods that can be used to extend an existant schema definition ########
def extend(self, othermetadefcls):
for rdef in othermetadefcls.__relations__:
self.add_relation(rdef)
......@@ -233,82 +186,63 @@ class EntityType(Definition):
add_relation(self.relations, rdef, name)
def remove_relation(self, name):
for rdef in self.get_relations(name):
for rdef in self._get_relations(name):
self.relations.remove(rdef)
def get_relations(self, name):
"""get a relation definitions by name
XXX take care, if the relation is both and subject/object, the
first one encountered will be returned
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)
"""
for rdef in self.relations[:]:
if rdef.name == name:
yield rdef
def __init__(self, *args, **kwargs):
super(EntityType, self).__init__(*args, **kwargs)
# if not hasattr(self, 'relations'):
self.relations = list(self.__relations__)
def register_relations(self, schema):
order = 1
for relation in self.relations:
if isinstance(relation, SubjectRelation):
kwargs = relation.__dict__.copy()
del kwargs['name']
rdef = RelationDefinition(subject=self.name, name=relation.name,
object=relation.etype, order=order,
**kwargs)
order += 1
elif isinstance(relation, ObjectRelation):
kwargs = relation.__dict__.copy()
del kwargs['name']
rdef = RelationDefinition(subject=relation.etype,
name=relation.name,
object=self.name, order=order,
**kwargs)
order += 1
else:
raise BadSchemaDefinition('dunno how to handle %s' % relation)
rdef.add_relations(schema)
class RelationBase(Definition):
cardinality = None
constraints = ()
symetric = False
inlined = False
class RelationType(Definition):
symetric = MARKER
inlined = MARKER
def __init__(self, *args, **kwargs):
super(RelationBase, self).__init__(*args, **kwargs)
self.constraints = list(self.constraints)
if self.object is None:
return
cardinality = self.cardinality
if cardinality is None:
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?+*'
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
class RelationType(RelationBase):
object = None
def register_relations(self, schema):
if getattr(self, 'subject', None) or getattr(self, 'object', None):
assert self.subject and self.object
self.add_relations(schema)
def expand_type_definitions(self, defined):
"""schema building step 1:
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:
class RelationDefinition(RelationBase):
subject = None
object = None
def __init__(self, subject=None, name=None, object=None, **kwargs):
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):
if subject:
self.subject = subject
else:
......@@ -317,19 +251,86 @@ class RelationDefinition(RelationBase):
self.object = object
else:
self.object = self.__class__.object
if name:
self.name = name
elif not getattr(self, 'name', None):
self.name = self.__class__.__name__
super(RelationDefinition, self).__init__(**kwargs)
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)
def register_relations(self, schema):
assert self.subject and self.object
self.add_relations(schema)
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
def __repr__(self):
return '%(subject)s %(name)s %(object)s' % self.__dict__
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)
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():