# -*- coding: iso-8859-1 -*-
#-----------------------------------------------------------------------------
# Modeling Framework: an Object-Relational Bridge for python
#
# Copyright (c) 2001-2004 Sbastien Bigaret <sbigaret@users.sourceforge.net>
# All rights reserved.
#
# This file is part of the Modeling Framework.
#
# This code is distributed under a "3-clause BSD"-style license;
# see the LICENSE file for details.
#-----------------------------------------------------------------------------
""" ... describe me please I feel alone...
$Id: Entity.py 932 2004-07-20 06:21:57Z sbigaret $"""
__version__='$Revision: 932 $'[11:-2]
# Modeling
from Modeling.utils import isListOrTuple,isaValidName,toBoolean,finalize_docstrings
from Modeling.XMLutils import *
from Modeling.EntityClassDescription import EntityClassDescription
from Modeling.KeyValueCoding import KeyValueCoding
from Modeling.Model import ModelError
from Modeling.Attribute import Attribute
from Modeling.Exceptions import ModelValidationError
from Modeling import Validation
from Modeling import Qualifier
from Modeling.GlobalID import KeyGlobalID
# python
from cgi import escape
import re, string, sys, types
from Modeling.logging import debug
ENTITY_DOC = {
'inheritance_methods': 'addSubEntity(), allSubEntities(), '\
'allSubEntitiesNames(), newOrOverriddenProperties(), '\
'newOrOverriddenPropertiesNames(), '\
'newOrOverriddenPythonProperties(), '\
'newOrOverriddenPythonPropertiesNames(), parentEntity(), '\
'parentEntities(), '\
'parentEntityName(), rootEntity(), removeSubEntity(), '\
'subEntities()'
}
def externalNameForInternalName(aName, separatorString='_', useAllCaps=1):
"""
Turns an entity name into a valid name for database schema.
With the default parameters, 'address' becomes 'ADDRESS',
'firstName' becomes 'FIRST_NAME', and 'db2Id' becomes 'DB2_ID'
Default separatorString is '_'.
If 'useAllCaps' is set to 1, 'firstName' becomes 'first_name'
"""
#If 'returnCaps' is set to '0', the returned string consists of lower case
#letters, instead of upper case ones.
#if useCaps:
_regexp=re.compile('([a-z0-9])([A-Z])')
_results=_regexp.sub('\\1'+separatorString+'\\2', aName)
#else:
# _results=aName
if useAllCaps:
return string.upper(_results)
else:
return string.lower(_results)
def nameForExternalName(aName, separatorString='_', initialCaps=0):
"""
Inverse function of externalNameForInternalName()
Example: 'FIRST_NAME' becomes 'firstName', 'DB2_ID' becomes 'db2Id'
and 'ENTITY_NAME' becomes 'EntityName' when initialCaps is set to '1'.
"""
_parts=string.split(aName, separatorString)
_result=""
for _part in _parts:
_result+=string.capitalize(_part)
if not initialCaps and _result:
return string.lower(_result[0])+_result[1:]
return _result
def primaryKeyForGlobalID(aGlobalID):
"""
Returns the primaryKey used when 'aGlobalID' was initialized, or None if
'aGlobalID' is not a KeyGlobalID. The return value is a dictionary mapping
the primary key's component(s) with its (their) respective value.
See also: KeyGlobalID.__init__(), KeyGlobalID.keyValues()
"""
try:
return aGlobalID.keyValues()
except AttributeError:
return None
from Modeling.utils import base_persistent_object
class Entity(base_persistent_object, XMLCapability, KeyValueCoding):
"""Describes an entity
An entity defines a class and its corresponding persistence schema.
Implementation notes:
- the role of 'typeName' is not clear enough for the moment being. It should
probably be moved into a special recipient for parameters specifically
needed at generation-time (as well as others generation-dedicated values:
...)
Methods related to inheritance: %(inheritance_methods)s
Unimplemented features:
- fetch specifications
- (beginning of-!) shared objects
"""
# check: typeName XOR isAbstract _TBD
# Support for older instances
_className=''
_isReadOnly=0
_externalName=''
_PKs=()
_attrsUsedForLocking=()
_moduleName=None
_comment=''
def __init__(self, name='', aModel=None):
"Initializes an entity. A name **must** be provided."
# Commented out becoz of persistence
if (not name):# or (not aModel):
raise ModelError, "A name should be provided at creation time."
assert ( type(name)==types.StringType
or type(name)==types.UnicodeType) , \
"parameter name should be a string or unicode (type: <pre>'%s'</pre>, value: <pre>'%s'</pre>)" % (str(type(name)), str(name))
name = str(name) # TBD: Unicode... ugly workaround
self._attributes={} # name->Attribute
self._attrsUsedForLocking=()
self._className=None
self._comment=''
self._externalName=''
self._model=None
self._moduleName=None
if aModel: aModel.addEntity(aModel)
self._name=None
self.setName(name)
self._relationships={} # name->Relationship
self._parentEntity=None # single?!
self._PKs=()
self._subEntities=()
self._typeName=''
self._isAbstract=0
self._isReadOnly=0
def addAttribute(self, anAttribute):
"""
Adds 'anAttribute' to the set of this entity's attributes.
Raises 'ModelError' if 'anAttribute' is not a 'Attribute'
instance.
Raises 'ValueError' if anAttribute's name is already registered by an
attribute or a relationship. See also: definedKeys()
"""
#assert type(anAttribute)==types.InstanceType and anAttribute.__class__ is Attribute
if anAttribute.name() in self.definedKeys():
raise ValueError, "Attempt to insert an attribute in entity %s but a key named '%s' is already registered." % (self.name(), anAttribute.name())
self._attributes[anAttribute.name()]=anAttribute
self._p_changed=1
#_attrs = list(self._attributes)
#_attrs.append(anAttribute)
#self._attributes = tuple(_attrs)
anAttribute._setEntity(self)
def addRelationship(self, aRelationship):
"""
Adds 'aRelationship' to the set of this entity's relationships.
Raises 'ModelError' if 'aRelationship' is not a 'Relationship'
instance.
Raises 'ValueError' if aRelationship's name is already registered by an
attribute or a relationship. See also: definedKeys()
"""
#assert type(aRelationship)==types.InstanceType and aRelationship.__class__ is Relationship
if aRelationship.name() in self.definedKeys():
raise ValueError, "Attempt to insert a relationship in entity %s but a key named '%s' is already registered." % (self.name(), aRelationship.name())
self._relationships[aRelationship.name()]=aRelationship
self._p_changed=1
#_rels = list(self._relationships)
#_rels.append(aRelationship)
#self._relationships = tuple(_rels)
aRelationship.setEntity(self)
def addSubEntity(self, aChildEntity):
"""
Registers the supplied entity as a sub-entity (inheritance)
See also: %(inheritance_methods)s
"""
if aChildEntity in self._subEntities:
raise ModelError, "Attempt to insert a subentity in entity %s but a sub-entity named '%s' is already registered." % (self.name(), aChildEntity.name())
_subs = list(self._subEntities)
_subs.append(aChildEntity)
self._subEntities = tuple(_subs)
aChildEntity._setParentEntity(self)
def allSubEntities(self):
"""
Returns the list of all sub-entities of the current entity, i.e. its
direct sub-entities, the direct sub-entities of its sub-entities, etc.
See also: %(inheritance_methods)s
"""
_res=list(self._subEntities)
for e in self._subEntities:
_res+=e.allSubEntities()
return _res
def allSubEntitiesNames(self):
"""
Returns the names of the entities returned by allSubEntities(), as a list
See also: %(inheritance_methods)s
"""
return [e.name() for e in self.allSubEntities()]
def attributeNamed(self, aName):
"Returns the attributes named 'aName' or 'None' if it does not exist."
return self._attributes.get(aName)
#for _attr in self.attributes():
# if _attr.name()==aName: return _attr
#return None
def attributes(self):
"""
Returns the full set of attributes
"""
return self._attributes.values()
def attributesNames(self):
"Returns the full set ('tuple') of attributes' name."
#return tuple(map(lambda o: o.name(), self.attributes()))
return tuple(self._attributes.keys())
def attributesToFetch(self):
"""
Returns a subset (a 'list') of the entity's attributes, those which fall
into at least one of these categories:
- they are not derived,
- they are source attributes of a relationship
"""
if hasattr(self, '_v_attributesToFetch'):
return self._v_attributesToFetch
attributesToFetch=[]
def append(attribute, attributesToFetch):
if attribute not in attributesToFetch:
attributesToFetch.append(attribute)
# PKs
attributesToFetch.extend(self.primaryKeyAttributes())
# Attributes used for locking
for attribute in self.attributesUsedForLocking():
append(attribute, attributesToFetch); continue
# non derived
for attribute in self.attributes():
if not attribute.isDerived():
append(attribute, attributesToFetch); continue
# Relationship's source attributes
for rel in self.relationships():
for srcAttr in rel.sourceAttributes():
append(srcAttr, attributesToFetch)
self._v_attributesToFetch=attributesToFetch
return attributesToFetch
def attributesUsedForLocking(self):
"""
TBD
"""
return self._attrsUsedForLocking
def attributesUsedForLockingNames(self):
return map(lambda o: o.name(), self._attrsUsedForLocking)
def classDescriptionForInstances(self):
"""
Returns the class description assigned to this entity
You should rarely need to call this method directly ; instead, use the
ClassDescription's static method 'classDescriptionForName()' which takes
care of finding the appropriate class description.
See also: ModelSet.classDescriptionForEntityName()
ClassDescription.classDescriptionForName()
"""
debug('_TBD: return either an Entity- or a CMFClassDescription')
return EntityClassDescription(self)
def className(self):
"Returns the class name associated to this entity"
return self._className
def classProperties(self):
"""
Returns the list of class properties (attributes+relationships)
"""
return self.classProperties_attributes() + \
self.classProperties_relationships()
def classPropertiesNames(self):
"""
Returns the list of class properties' names (attributes+relationships)
"""
return map(lambda o: o.name(), self.classProperties())
def classProperties_attributes(self):
"""
Returns the list of attributes that are class properties
"""
return tuple([attr for attr in self.attributes()
if attr.isClassProperty()])
def classProperties_relationships(self):
"""
Returns the list of relationships that are class properties
"""
return tuple([rel for rel in self.relationships()
if rel.isClassProperty()])
def comment(self):
"Returns the comment field"
return self._comment
def definedKeys(self):
"""
Returns the list of the entity's defined keys, i.e. its attributes' and
relationships' names
"""
return self.attributesNames()+self.relationshipsNames()
def definesTableColumns(self):
"""
Tells whether this entity defines table columns: if this entity has an
externalName() and at least one of its attributes has a columnName(), the
answer is '1' (true), '0' (false) otherwise.
See also: SchemaGeneration.tableEntityGroupsForEntities()
"""
if not self._externalName:
return 0
for attribute in self._attributes.values():
if attribute.columnName():
return 1
return 0
def destinationObjectForKeyPath(self, keyPath):
"""
Returns the object ('Entity' or 'Attribute') at the end of
the 'keyPath'. For example, say you have an entity 'Author' with a toMany
relationship 'books' to the entity 'Book' which has an attribute 'title':
the returned value for 'keypath="books.title"' is that Book's attribute
named 'title'.
Returned value is either an 'Attribute' or a 'Entity' object.
Raises ValueError if keyPath is not a valid keypath (i.e. if it is None,
empty or if at least one element in 'keyPath' does not correspond to a
valid object in the model)
See also: objectPathForKeyPath()
"""
return self.objectsPathForKeyPath(keyPath)[-1]
def objectsPathForKeyPath(self, keyPath):
"""
Returns a list consisting in the Relationship instances that needs to be
traversed to reach the final element, plus this final element which is
either an Attribute or an Entity object.
Raises ValueError if 'keyPath' is not a valid path.
See also: destinationObjectForKeyPath()
"""
if not keyPath:
raise ValueError, "Invalid keypath"
objPath=[]
try:
object=self
for key in string.split(keyPath, '.'):
rel=object.relationshipNamed(key)
if rel:
objPath.append(rel)
object=rel.destinationEntity()
else:
object=object.attributeNamed(key)
objPath.append(object)
if not object:
raise ValueError, "Invalid keypath '%s'"%keyPath
if not isinstance(object, Attribute):
objPath.append(rel.destinationEntity())
except AttributeError:
raise ValueError, "Invalid keypath '%s'"%keyPath
return objPath
def externalModelsReferenced(self):
"""
Check all relationships defined in the entity, identify all (destination)
entities which belong to a different model than receiver's one, and
returns the list of their model.
Returns a empty list when no external model are referenced.
"""
_extModels=[]
for _relEntity in map(lambda o: o.destinationEntity(), \
self.relationships()):
if _relEntity.model() != self.model():
_extModels.append(rel_Entity.model())
return _extModels
def externalName(self):
"""
Returns the external name of the entity
"""
return self._externalName
def globalIDForRow(self, aRowDictionary):
"""
Returns the GlobalID corresponding to the supplied raw data, as returned
e.g. by 'AdaptorChannel.fetchRow()', to be used by an object belonging to
this entity. Returned object's class is 'KeyGlobalID'.
Parameter:
aRowDictionary -- a dictionary whose keys are attributes'names, with
their corresponding values.
See also: primaryKeyForRow(), primaryKeyForGlobalID()
"""
pkValues=self.primaryKeyForRow(aRowDictionary)
return KeyGlobalID(self._name, pkValues)
def isaValidAttributeName(self, aName):
"""
Checks that 'aName' is a valid name for an attribute.
A name is valid iff:
- its first letter is a letter or an underscore,
- no attribute is already registered with the same name.
"""
return isaValidName(aName) and aName not in self.attributesNames()
def isaValidRelationshipName(self, aName):
"""
Checks that 'aName' is a valid name for a relationship.
A name is valid iff:
- its first letter is a letter or an underscore,
- no relationship is already registered with the same name.
"""
return isaValidName(aName) and aName not in self.relationshipsNames()
def isAbstractEntity(self):
"Indicates whether the entity is abstract."
return self._isAbstract
isAbstract=isAbstractEntity
def isPrimaryKeyValidInObject(self, anObject):
"_TBD to be implemented"
raise "Unimplemented yet!"
def isReadOnly(self):
"Tells whether the receiver is a read-only attribute"
return self._isReadOnly
def isValidAttributeUsedForLocking(self, anAttribute):
"""
Returns false if the attribute is not one of the Entity's attributes, or
if it answers true to the message 'isDerived()' ; otherwise, returns true.
"""
if type(anAttribute)==type('string'):
anAttribute=self.attributeNamed(anAttribute)
if anAttribute is None: return 0
if anAttribute not in self.attributes():
return 0
if anAttribute.isDerived():
return 0
return 1
def isValidPrimaryKey(self, anAttribute):
"""
Checks whether the supplied attribute is a valid possible primary key.
Parameter anAttribute can be either an Attribute instance or an
attribute's name -- in any case the corresponding attribute's type can
be either 'int' or 'string', nothing else.
Returns *false* (integer '0') if anAttribute cannot be found in the
entity, or if it does not belong to the entity ; returns '1' for *true*.
"""
# _TBD ...if atribute is derived
if type(anAttribute)==type('string'):
anAttribute=self.attributeNamed(anAttribute)
if anAttribute is None: return 0
# Check entityName() rather than entity() itself since anAttribute's
# entity can be acquisition-wrapped
if anAttribute.entity().name()!=self.name(): return 0
if anAttribute.type() not in ('string', 'int'): return 0
return 1
# properties: later user (e.g. 'id' is an hidden type) (externalName: same/table)
def model(self):
"Returns the model this entity is related to"
return self._model
parent=model
def modelName(self):
"Returns the name of the model this entity is related to"
try:
return self._model.name()
except AttributeError:
return ''
def moduleName(self):
"""
Returns the module's name where the corresponding class can be found
If unset, defaults to 'className'
See also: className(), Model.packageName()
"""
if not self._moduleName:
return self._className
return self._moduleName
def name(self):
"Returns the Entity's name"
return self._name
def parentEntities(self):
"""Returns the parent entities (a la super-class, cf. inheritance)
The returned set is ordered: it begins with the direct parent, and ends
with the rootEntity().
See also: %(inheritance_methods)s
"""
parents=[]
parent=self.parentEntity()
while parent:
parents.append(parent)
parent=parent.parentEntity()
return parents
def parentEntity(self):
"""Returns the parent entity (a la super-class, cf. inheritance)
See also: %(inheritance_methods)s
"""
return self._parentEntity
def parentEntityName(self):
"""
Returns the parent entity's name (a la super-class, cf. inheritance)
See also: %(inheritance_methods)s
"""
return self._parentEntity and self._parentEntity.name() or ''
def primaryKeyAttributeNames(self):
"""
Returns a list containing the names of the attributes assigned to the
entity's PK.
"""
return map(lambda o: o.name(), self._PKs)
def primaryKeyAttributes(self):
"""
Returns the array of attributes used as primary keys
"""
return self._PKs
def primaryKeyForGlobalID(self, aGlobalID):
"""
Returns the primaryKey used when 'aGlobalID' was initialized, or None if
'aGlobalID' is not a KeyGlobalID. The return value is a dictionary mapping
the primary key's component(s) with its (their) respective value.
Note: this method is NOT relative to the current Entity and is strictly
equivalent to KeyGlobalID.keyValues()
See also: globalIDForRow(), primaryKeyForRow(),
KeyGlobalID.__init__(), KeyGlobalID.keyValues()
"""
return primaryKeyForGlobalID(aGlobalID)
def primaryKeyForRow(self, aRowDictionary):
"""
Computes the primary key from 'aRowDictionary'.
'aRowDictionary' is a database row, i.e. a dictionary mapping a (TABLE's)
columns' names to their values. The returned primary key is a
sub-dictionary of aRowDictionary containing only the primary key.
See also: primaryKeyAttributeNames(), globalIDForRow()
"""
pkValues={}
for attributeName in self.primaryKeyAttributeNames():
pkValues[attributeName]=aRowDictionary[attributeName]
return pkValues
def primaryKeyRootName(self):
"""
Returns the name to be used by SchemaGeneration and AdaptorChannel
concrete subclasses, e.g. to generate and use SQL sequence to get primary
keys of newly inserted objects.
This method returns the externalName of the underlying entity's
rootEntity, or simply its name if it has no externalName assigned
(e.g. abstract entities).
See also: rootEntity()
"""
root=self.rootEntity()
return root.externalName() or root.name()
def propertyNamed(self, aName):
"""
Return the property named 'aName' (either an Attribute or a Relationship)
"""
return self.attributeNamed(aName) or self.relationshipNamed(aName)
def propertyNameDidChange(self, oldName):
"""
Internally called by Attribute and Relationship.setName() after a
property's name has changed.
Parameter:
oldName -- the name of the property before it was changed
Raises ValueError if no property could be found under the supplied name
"""
attr=self.attributeNamed(oldName)
if attr:
del self._attributes[oldName]
self._attributes[attr.name()]=attr
self._p_changed=1
return
rel=self.relationshipNamed(oldName)
if rel:
del self._relationships[oldName]
self._relationships[rel.name()]=rel
self._p_changed=1
return
raise ValueError, "Unable to find property with name '%s'"%oldName
def qualifierForPrimaryKey(self, row):
"""
Returns the qualifier on can use to fetch the object identified by
primary key values as supplied in 'row'
This is typically called when a toOne fault ('AccessFaultHandler') is
triggered and has to fetch the corresponding datas, or when an
DatabaseObject is turned back into a fault.
Raises ValueError if anything wrong happens (empty dictionary, keys that
do not correspond to the entity's PKs, etc.)
Parameter:
row -- a dictionary with keys as PK attributes' names and their
corresponding values.
See also: interface 'Faulting', 'FaultHandler', 'AccessFaultHandler'
"""
try:
# check that the row is OK
pkValues={}
for key in self.primaryKeyAttributeNames():
valueForKey=row.get(key)
if not valueForKey:
raise
pkValues[key]=valueForKey
return Qualifier.qualifierToMatchAllValues(pkValues)
except:
raise ValueError, 'Cannot compute the qualifier for PK for entity: %s '\
'and row: %s'%(self._name, row)
def relationshipNamed(self, aName):
"Returns the attributes named 'aName' or None if it does not exist."
return self._relationships.get(aName)
#for _rel in self.relationships():
# if _rel.name() == aName: return _rel
#return None
def relationshipsNames(self):
"Returns the full set ('tuple') of relationships' name."
return tuple(self._relationships.keys())
#return tuple(map(lambda o: o.name(), self.relationships()))
def relationships(self):
"""
Returns the full set of relationships ; it is returned as a dictionary
where keys are 'relationshipName's and values 'Relationship' instances.
"""
return self._relationships.values()
def removeAttribute(self, anAttribute):
"""
Removes the supplied entity from the registered child entities.
Parameter 'anEntity' can be either an 'Entity' instance or a
subentity's name.
Raises 'ModelError' if 'anEntity' cannot be found.
"""
# _TBD: cf. referencesProperty(aProperty) (not implemented) which should
# _TBD: be called before attempting to remove an attribute (or a rel.)
#assert type(anAttribute) in (types.InstanceType, types.StringType) #+Assert
if type(anAttribute)==types.StringType:
attrName = anAttribute
anAttribute = self.attributeNamed(anAttribute)
else:
attrName = anAttribute.name()
try:
del self._attributes[attrName]
#_attrs = list(self._attributes)
#_attrs.remove(anAttribute)
#self._attributes=tuple(_attrs)
except KeyError:
raise ModelError, "Entity '%s' has no attribute named '%s'" % (self.name(), attrName)
self._p_changed=1
anAttribute._setEntity(None)
def removeRelationship(self, aRelationship):
"""
Removes the supplied relationship from the registered relationships.
Parameter 'aRelationship' can be either an 'Relationship'
instance or a relation's name.
Raises 'ModelError' if 'aRelationship' cannot be found.
"""
# _TBD: cf. referencesProperty(aProperty) (not implemented) which should
# _TBD: be called before attempting to remove an attribute (or a rel.)
#assert type(aRelationship) in (types.InstanceType, types.StringType) #+Assert
if type(aRelationship)==types.StringType:
relName = aRelationship
aRelationship = self.relationshipNamed(aRelationship)
else:
relName = aRelationship.name()
try:
del self._relationships[relName]
#_rels = list(self._relationships)
#_rels.remove(aRelationship)
#self._relationships=tuple(_rels)
except KeyError:
raise ModelError, "Entity '%s' has no relationship named '%s'" % (self.name(), relName)
self._p_changed=1
aRelationship.setEntity(None)
def removeSubEntity(self, aChildEntity):
"""
Removes the supplied entity from the registered child entities.
Parameter 'aChildEntity' can be either an 'Entity' instance or a
subentity's name.
Raises 'ModelError' if 'anEntity' cannot be found.
See also: %(inheritance_methods)s
"""
if type(aChildEntity)==types.StringType:
aChildEntity = self.relationshipNamed(aChildEntity)
try:
_subs = list(self._subEntities)
_subs.remove(aChildEntity)
self._subEntities=tuple(_subs)
except ValueError:
raise ModelError, "Entity '%s' has no sub-entity named '%s'" % (self.name(), aChildEntity.name())
aChildEntity._setParentEntity(None)
def restrictingQualifier(self):
"""
Returns the restrcting qualifier. Not implemented yet, simply returns None
NB: a restricting qualifier is handy when all entities and their root
entity (speaking of inheritance) map to a single database.
"""
return None
def rootEntity(self):
"""
Returns the root entity of the current entity
See also: %(inheritance_methods)s
"""
if self._parentEntity:
return self._parentEntity.rootEntity()
else:
return self
def setAttributesUsedForLocking(self, attributes):
"""
"""
# _TBD Implementation Note: Attributes and Attributes'names can be mixed
# _TBD within the same array, although this is strongly discouraged.
if not isListOrTuple(attributes):
raise ValueError, "Parameter 'attributes' must be a sequence"
if not attributes:
self._attrsUsedForLocking=()
return
_ok=1
_attrs=[]
for attr in attributes:
if not self.isValidAttributeUsedForLocking(attr):
_ok=0
break
# This allows developper to provide a list of objects where these
# objects are instances of one of Attribute's derived classes
if type(attr) is types.StringType: attr=self.attributeNamed(attr)
_attrs.append(self.attributeNamed(attr.name()))
if not _ok:
raise ModelError, "Invalid attributes"
self._attrsUsedForLocking=tuple(_attrs)
def setClassName(self, aName):
"Sets the class name associated to this entity"
self._className=aName
def setComment(self, aComment):
"Sets the comment field"
self._comment=aComment
def setExternalName(self, externalName):
"Sets the external name"
self._externalName=externalName
def setIsAbstractEntity(self, aBool):
"""
Tells this entity whether it is abstract, according to the
supplied *boolean*.
if aBool is a string equal to '0', it evaluates to 'false'
"""
self._isAbstract=toBoolean(aBool)
setIsAbstract=setIsAbstractEntity
def setModuleName(self, aName):
"""
Sets the module's name where the corresponding class can be found
See also: moduleName()
"""
self._moduleName=aName
#def _setModel(self, aModel):
# "Sets the associated model. This method should NOT be called directly."
# assert type(aModel) is types.InstanceType and aModel.__class__ is Model
# self._model = aModel
def setParentEntity(self, anEntity):
"""
See also: %(inheritance_methods)s
"""
if type(anEntity)==type(''):
anEntity=self.model().entityNamed(anEntity)
anEntity.addSubEntity(self)
def _setParentEntity(self, anEntity):
#assert type(anEntity)==types.InstanceType and anEntity.__class__==Entity
self._parentEntity = anEntity
def setPrimaryKeyAttributes(self, PKs):
"""
Assigns the set of attributes used as primary keys ; empties the list of
PKs if parameter is an empty sequence.
Parameter 'PKs' can be either a sequence of Attributes or a sequence of
Attributes' names.
Raise ValueError if parameter 'PKs' is not a sequence.
Raises 'ModelError' if 'self.isValidPrimaryKey()' returns false for at
least one of the attributes.
"""
# _TBD Implementation Note: Attributes and Attributes'names can be mixed
# _TBD within the same array, although this is strongly discouraged.
if PKs in ( (), [] ):
self._PKs=()
return
if not isListOrTuple(PKs):
raise ValueError, "Parameter 'PKs' must be a sequence"
#if len(PKs)>1:
# raise NotImplementedError,'Sorry, we only support singletons as PKs '\
# 'for the moment being'
_ok=1
_PKs=[]
for PK in PKs:
if not self.isValidPrimaryKey(PK):
_ok=0
break
# This allows developper to provide a list of objects where these
# objects are instances of one of Attribute's derived classes
if type(PK) is types.StringType: PK=self.attributeNamed(PK)
_PKs.append(self.attributeNamed(PK.name()))
if not _ok:
raise ModelError, "Invalid PKs"
self._PKs=tuple(_PKs)
def _setModel(self, aModel):
"""
Private method called by Model.addEntity() after the necessary checks that
take care of enforcing that entities share a single namespace for their
names are done.
You should never call this method directly, since this could defeat the
checks described above: in that case, the Entity could become unreachable
from either its Model or ModelSet.
"""
self._model = aModel
def setName(self, aName):
"""
Sets the name for the entity.
Raises 'ModelError' if the name is invalid or if it is already used by
an other entity in the Model or the enclosing ModelSet.
See also: Model.isEntityNameDeclaredInNamespace()
"""
if aName is not None and aName==self._name: return
if not isaValidName(aName):
raise ModelError, "Supplied name incorrect (%s)" % aName
if self._model and self._model.isEntityNameDeclaredInNamespace(aName):
raise ModelError, "Attempt to change an entity's name but that "\
"name: %s is already registered." % (aName,)
self._name = aName
def setReadOnly(self, aBoolean):
"Sets the read-only status for this entity"
self._isReadOnly=toBoolean(aBoolean)
def setRestrictingQualifier(self, aQualifier):
"""
Sets the restricting qualifier.
Not implemented yet, this method unconditionally raises
NotImplementedError.
See also: restrictingQualifier()
"""
raise NotImplementedError, 'Unimplemented yet'
def setTypeName(self, aName):
"Sets the meta-type associated with this entity."
#assert type(aName) is types.StringType
self._typeName = aName
## __TBD Commented out: this is stupid. This portion of code will disappear
## __TBD as soon as I'm sure it is really useless!! [snapshotForRow]
## __TBD was called: by: DatabaseChannel.fetchObject()
#def snapshotForRow(self, aRowDictionary):
# """
# Builds the snapshot for the corresponding entity, given the aRowDictionary
# (e.g. the raw data as extracted by 'AdaptorChannel.fetchRow()').
#
# See also: attributesUsedForLocking()
# """
# dict={}
# for attributeName in self.attributesUsedForLockingNames():
# dict[attributeName]=aRowDictionary[attributeName]
# return dict
def subEntities(self):
"""
See also: %(inheritance_methods)s
"""
return self._subEntities
def subEntitiesNamed(self, aName):
"""
Returns the sub-entity named 'aName' or None if it does not exist.
See also: %(inheritance_methods)s
"""
for _sub in self.subEntities():
if _sub.name() == aName: return _sub
return None
def typeName(self):
"Returns the meta-type associated with this entity"
# _TBD: suppress the explicit storage of this and get it dynamically from
# _TBD class==className
return self._typeName
def validateValue(self, object):
"""
Checks whether the supplied object is valid.
Each attribute and relationship related to this entity is checked against
the object's values.
Raises:
- Validation.ValidationException if the supplied object is not related
to the current entity, or if it is not under model control at all.
- Validation.ValidationException if the validation failed
"""
try:
if object.entityName() != self.name():
raise ModelValidationError, \
"Parameter 'object' ('%s') 's entityName is not '%s'" \
% (str(object), self.name())
except ModelValidationError: raise
except AttributeError:
raise ModelValidationError, \
"Parameter 'object' ('%s') does not respond to 'entityName()'" \
% (object,)
_error=Validation.ValidationException()
# Validation of class properties
for _prop in self.classProperties():
try:
_prop.validateValue(object.storedValueForKey(_prop.name()), object)
except Validation.ValidationException, exc:
_error.aggregateException(exc)
# Time to validate object as a whole: ok, this is done in ClassDescription
_error.finalize()
#def __eq__(self, anEntity):
# "equality test"
# raise "Unimplemented"
##
## Methods related to inheritance
##
def clone(self, name=None, model=None):
"""
Returns a copy of the current Entity
This is not used in the core, but a modelization tool could take
advantage of it to prepare a subEntity for the current Entity.
Note: remember that, contrary to a object-model where inherited properties
are not specified again in subEntity, the goal of a model is to propose a
description on how properties are mapped to a database, hence: a subentity
describes each of it parent's properties as well (see documentation on
howto to design a model, paragraph 'Dealing with inheritance', for further
details).
Parameters:
name -- optional. It it is provided, the cloned entity gets that name,
otherwise defaults to 'self.name()'
model -- optional. If it is provided, the cloned Entity is added to the
model ; note that in this case, it is strongly recommended to provide
the name for the clone (since two entities cannot have the same name
within a single Model or ModelSet --cf. Model.addEntity())
"""
clone=Entity(name or self.name())
# the mechanism used here relies on functionalities decl. in XMLCapability
clonedProps=('className', 'externalName', 'isReadOnly', 'isAbstract',
'parentEntity', 'typeName')
#
for prop in clonedProps:
clone.xmlSetAttribute(prop)(self.xmlGetAttribute(prop)())
for attr in self.attributes():
clone.addAttribute(attr.clone())
for rel in self.relationships():
rel.clone(clone)
clone.setPrimaryKeyAttributes(self.primaryKeyAttributeNames())
clone.setAttributesUsedForLocking(self.attributesUsedForLockingNames())
if model:
model.addEntity(clone)
return clone
def definesProperty(self, property):
"""
Tells whether this Entity declares the supplied property
Parameter:
property -- an Attribute or Relationship instance
Returns a true value (integer 1) iff all the following conditions evaluate
to true:
- the property and the corresponding one in 'self' have the same python
type if both are attributes (cf. Attribute.type()), or the same
destinationEntity if both are relationships.
Otherwise, returns false.
This is not used within the framework's core, rather when python code is
generating from a model ; this allows the generatiion mechanism to know if
an attribute is overridden or new in a subclass.
See also: newOrOverriddenProperties()
"""
if property.name() in self.attributesNames()+self.relationshipsNames():
try:
self_prop=self.attributeNamed(property.name())
if self_prop and self_prop.type()==property.type():
return 1
self_prop=self.relationshipNamed(property.name())
if self_prop and \
self_prop.destinationEntity()==property.destinationEntity():
return 1
except AttributeError:
# this can happen if, e.g., property is an Attribute and property
# is a relationship
return 0
return 0
def definesPythonProperty(self, property):
"""
Tells whether this Entity declares the supplied (python) property
Parameter:
property -- an Attribute or Relationship instance
Returns a true value (integer 1) iff all the following conditions evaluate
to true:
- the property'name is in 'self.classPropertiesNames()'
- the property and the corresponding one in 'self' have the same python
type if both are attributes (cf. Attribute.type()), or the same
destinationEntity if both are relationships.
Otherwise, returns false.
This is not used within the framework's core, rather when python code is
generating from a model ; this allows the generatiion mechanism to know if
an attribute is overridden or new in a subclass.
See also: newOrOverriddenPythonProperties()
"""
if property.name() in self.classPropertiesNames():
try:
self_prop=self.attributeNamed(property.name())
if self_prop and self_prop.type()==property.type():
return 1
self_prop=self.relationshipNamed(property.name())
if self_prop and \
self_prop.destinationEntity()==property.destinationEntity():
return 1
except AttributeError:
# this can happen if, e.g., property is an Attribute and self_prop
# is a relationship
return 0
return 0
def newOrOverriddenProperties(self):
"""
Returns the list of entity's properties (attributes or relationships)
which are not declared in the parent entity, or all its attributes and
relationships if the entity does not have a parent entity.
This is not used within the framework's core, but a GUI designed to build
model can take advantage of this to distinguish inherited from
non-inherited properties in an entity.
See also: definesProperty()
See also: %(inheritance_methods)s
"""
props=self.attributes()+self.relationships()
parent=self.parentEntity()
if not parent:
return props
return [p for p in props if not parent.definesProperty(p)]
def newOrOverriddenPropertiesNames(self):
"""
Returns the list of the names of properties returned by
newOrOverriddenProperties()
See also: newOrOverriddenProperties(), newOrOverriddenPythonProperties()
"""
return [p.name() for p in self.newOrOverriddenProperties()]
def newOrOverriddenPythonProperties(self):
"""
Returns the list of entity's python properties (attributes or
relationships) which are not declared in the parent entity, or all its
attributes and relationships if the entity does not have a parent entity.
This is not used within the framework's core, but a process generating
code templates from a model can take advantage of this to distinguish
inherited python properties from new ones in a given entity.
For a description of what precisely is a 'python property', see
definesPythonProperty()
See also: newOrOverriddenProperties()
"""
props=self.classProperties()
parent=self.parentEntity()
if not parent:
return props
return [p for p in props if not parent.definesPythonProperty(p)]
def newOrOverriddenPythonPropertiesNames(self):
"""
Returns the list of the names of properties returned by
newOrOverriddenPythonProperties()
"""
return [p.name() for p in self.newOrOverriddenPythonProperties()]
## XMLCapability
def initWithXMLDOMNode(self, aNode, phase=1, parent=None,
encoding='iso-8859-1'):
"""
Initializes an entity with the supplied xml.dom.node.
Parameter phase can be either 1 or 2 (but nothing else).
Phase 1 initializes the attribute, phase 2 initializes relationships
A XMLutils.XMLImportError is raised if the entity is not empty, i.e. if it
already holds some attributes and/or relationships.
"""
if phase==1 and (self.attributes() or self.relationships()):
raise XMLImportError, "Cannot initialize a non-empty entity"
if phase not in (1,2):
raise ValueError, 'Ooops, parameter phase should be 1 or 2!'
k_v=aNode.attributes.items()
for attributeName, value in k_v:
# Iterate on attributes which are in the xml
attrType=self.xmlAttributeType(attributeName)
set=self.xmlSetAttribute(attributeName)
if attrType=='string': value=unicodeToStr(value, encoding)
elif attrType=='number': value=int(value)
elif attrType=='bool': value=int(value)
set(value)
if phase==1:
# Attributes first!
attributesNodes=xpath.Evaluate('attribute', contextNode=aNode)
for node in attributesNodes:
name=node.getAttribute('name')
attribute=Attribute(unicodeToStr(name, encoding))
attribute.initWithXMLDOMNode(node, encoding)
self.addAttribute(attribute)
# PKs
PKsNodes=xpath.Evaluate('primaryKey', contextNode=aNode)
_pks=[]
for node in PKsNodes:
name=node.getAttribute('attributeName')
attribute=self.attributeNamed(unicodeToStr(name))
if attribute is None:
raise XMLImportError, "Cannot assign to entity '%s' the attribute "\
"'%s' as primary key since this attribute is not defined"%\
(self.name(), name)
_pks.append(attribute)
self.setPrimaryKeyAttributes(_pks)
# Attributes used for locking
lockinNodes=xpath.Evaluate('attributesUsedForLocking', contextNode=aNode)
_lockins=[]
for node in lockinNodes:
name=node.getAttribute('attributeName')
attribute=self.attributeNamed(unicodeToStr(name))
if attribute is None:
raise XMLImportError, "Cannot assign to entity '%s' the attribute "\
"'%s' as a 'used for locking' attribute since this attribute "\
"is not defined" % (self.name(), name)
_lockins.append(attribute)
self.setAttributesUsedForLocking(_lockins)
elif phase==2:
# Relationships & inheritance
from Relationship import SimpleRelationship,FlattenedRelationship
parent=xpath.Evaluate('string(@parentEntity)', contextNode=aNode)
if parent and parent!='None':
parent=unicodeToStr(parent, encoding)
self.setParentEntity(parent)
relationshipsNodes=xpath.Evaluate('relation', contextNode=aNode)
for node in relationshipsNodes:
name=node.getAttribute('name')
relationship=SimpleRelationship(unicodeToStr(name, encoding))
self.addRelationship(relationship)
relationship.initWithXMLDOMNode(node, encoding)
relationshipsNodes=xpath.Evaluate('flattenedRelation', contextNode=aNode)
for node in relationshipsNodes:
name=node.getAttribute('name')
relationship=FlattenedRelationship(unicodeToStr(name, encoding))
self.addRelationship(relationship)
relationship.initWithXMLDOMNode(node, encoding)
return
def getXMLDOM(self, doc=None, parentNode=None, encoding='iso-8859-1'):
"""
Returns the (DOM) DocumentObject for the receiver.
Parameters 'doc' and 'parentDoc' should be both omitted or supplied.
If they are omitted, a new DocumentObject is created.
If they are supplied, elements are added to the parentNode.
Returns: the (possibly new) DocumentObject.
"""
if (doc is None) ^ (parentNode is None):
raise ValueError, "Parameters 'doc' and 'parentNode' should be together supplied or omitted"
if doc is None:
doc=createDOMDocumentObject('model')
parentNode=doc.documentElement
node=parentNode
else:
node=doc.createElement('entity')
parentNode.appendChild(node)
exportAttrDict=self.xmlAttributesDict()
for attr in exportAttrDict.keys():
attrType=self.xmlAttributeType(attr)
value=self.xmlGetAttribute(attr)()
if attrType=='bool': value=int(value)
value=strToUnicode(str(value), encoding)
node.setAttribute(attr, value)
# PKs manually added
for attribute in self.primaryKeyAttributes():
pknode=doc.createElement('primaryKey')
pknode.setAttribute('attributeName', strToUnicode(attribute.name()))
node.appendChild(pknode)
# Attributes usedForLocking manually added
for attribute in self.attributesUsedForLocking():
pknode=doc.createElement('attributesUsedForLocking')
pknode.setAttribute('attributeName', strToUnicode(attribute.name()))
node.appendChild(pknode)
#cmpProp=lambda p1, p2: (p2.isClassProperty()-p1.isClassProperty()) or \
# cmp(string.lower(p1.name()), string.lower(p2.name()))
cmpProp=lambda p1, p2: cmp(string.lower(p1.name()),string.lower(p2.name()))
attributes=list(self.attributes())
attributes.sort(cmpProp)
relationships=list(self.relationships())
relationships.sort(cmpProp)
for attribute in attributes: # Attributes
attribute.getXMLDOM(doc, node, encoding)
for relationship in relationships: # Relationships
relationship.getXMLDOM(doc, node, encoding)
return doc
def xmlAttributesDict(self):
"."
return {
'name': ('string',
lambda self=None,p=None: None,
self.name ),
'className': ( 'string',
self.setClassName,
self.className ),
'comment': ( 'string',
self.setComment,
self.comment),
'externalName': ( 'string',
self.setExternalName,
self.externalName ),
'isReadOnly': ( 'bool',
self.setReadOnly,
self.isReadOnly ),
'isAbstract': ( 'bool',
self.setIsAbstract,
self.isAbstract ),
'moduleName': ( 'string',
self.setModuleName,
self.moduleName ),
'parentEntity': ( 'string',
lambda self=None,p=None: None, # self.setParentEntity,
self.parentEntityName ),
#'primaryKey': ( 'string',
# self.a,
# self. ),
'typeName': ( 'string',
self.setTypeName,
self.typeName )
}
##
## KeyValueCoding error handling
##
def handleAssignementForUnboundKey(self, value, key):
if key=='doc': self.setComment(value)
else:
raise AttributeError, key
handleTakeStoredValueForUnboundKey=handleAssignementForUnboundKey
##
## TBD Validation of an Entity
##
##
#finalize_docstrings(Entity, ENTITY_DOC)
|