# -*- 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.
#-----------------------------------------------------------------------------
"""
EntityClassDescription
Class description fills the gap between a model and informations needed at
run-time, such as ...
Automatically registered when a model is loaded/instanciated.
Module's functions are the one a ZModelizationTool should be able to answer.
(or something else?)
CVS information
$Id: EntityClassDescription.py 932 2004-07-20 06:21:57Z sbigaret $
"""
from RelationshipManipulation import \
removeObjectFromBothSidesOfRelationshipWithKey
from interfaces.ClassDescription import ClassDescriptionInterface
from utils import capitalizeFirstLetter,getModule
from logging import trace,debug
# Integer constants
from ClassDescription import \
DELETE_NULLIFY, DELETE_DENY, DELETE_CASCADE, DELETE_NOACTION
import ClassDescription
from ClassDescription import classDescriptionForName
import Validation
from interfaces.Validation import \
CUSTOM_KEY_VALIDATION, \
OBJECT_WIDE, \
OBJECT_WIDE_KEY, \
DELETE_DENY_KEY
__version__='$Revision: 932 $'[11:-2]
class EntityClassDescription(ClassDescription.ClassDescription):
"""
*Documentation forthcoming*
'EntityClassDescription' is a concrete subclass of 'ClassDescription' which
uses an Entity to service the messages it responds to.
"""
__implements__=(ClassDescriptionInterface,)
def __init__(self, anEntity):
"""
Initializes a classe description with the supplied entity.
"""
# we just need to keep a reference to the supplied entity, that's it.
assert(anEntity is not None)
self._entity=anEntity
def adaptorName(self):
"""
Returns the name of the adaptor (like: 'Postgres') which is used to make
persistent the corresponding instances
"""
return self._entity.model().adaptorName()
def allAttributesKeys(self):
"""
Returned the set of Attributes defined by the underlying Entity.
See also: attributesKeys()
"""
return tuple([attr.name() for attr in self._entity.attributes()])
def allToManyRelationshipKeys(self):
"""
Returned the set (tuple) of toMany relationships' names defined by the
underlying Entity.
See also: toManyRelationshipKeys()
"""
toManyRels=filter(lambda rel: rel.isToMany(), self._entity.relationships())
return tuple([rel.name() for rel in toManyRels])
def allToOneRelationshipKeys(self):
"""
Returned the set (tuple) of toOne relationships' names defined by the
underlying Entity.
See also: toOneRelationshipKeys()
"""
toOneRels=filter(lambda rel: rel.isToOne(), self._entity.relationships())
return tuple([rel.name() for rel in toOneRels])
def attributesKeys(self):
"""
Returns the set (tuple) of Attributes that are designated as class
properties in the underlying Entity.
See also: allAttributesKeys()
"""
return tuple([attr.name()
for attr in self._entity.attributes()
if attr.isClassProperty()])
#l=filter(lambda attr: attr.isClassProperty(), self._entity.attributes())
#return tuple(map(lambda attr: attr.name(), l))
def awakeObjectFromInsertion(self, object, editingContext):
"latter"
ClassDescription.ClassDescription.awakeObjectFromInsertion.im_func(self, object, editingContext)
debug('Not implemented yet')
def awakeObjectFromFetch(self, object, editingContext):
"latter"
ClassDescription.ClassDescription.awakeObjectFromFetch.im_func(self, object, editingContext)
debug('Not implemented yet')
def classDescriptionForDestinationKey(self, aKey):
"""
Returns the class description registered for the destination entity of
the underlying Entity's relationship 'aKey'
"""
dest=self._entity.relationshipNamed(aKey).destinationEntity()
return ClassDescription.classDescriptionForName(dest.name())
def classForInstances(self):
"""
"""
delegate=self.delegate()
theClass=None
if delegate and delegate.respondsTo('classForClassDescription'):
theClass=delegate.classForClassDescription(self)
if not theClass:
theClass=classForEntity(self._entity)
return theClass
def createInstanceWithEditingContext(self, editingContext):
"""
Returns an instance of the appropriate class, as specified by the
underlying 'entity.className()'. The default implementation searches a
class 'entity.className()' in a module 'entity.model'.
You can provide your own logic by providing a delegate to the
classDescription which implements method 'classForClassDescription()'. If
the delegate returns None, than it falls back to the standard behaviour.
Raises ValueError if such an instance cannot be created.
"""
try:
theClass=self.classForInstances()
except ImportError:
raise
debug('theClass: %s'%theClass)
return theClass()
def deleteRuleForRelationshipKey(self, aKey):
"-"
return self._entity.relationshipNamed(aKey).deleteRule()
def displayNameForKey(self, aKey):
"-"
_attr=self._entity.attributeNamed(aKey)
_rel=self._entity.relationshipNamed(aKey)
if _attr:
return _attr.displayLabel()
if _rel:
return _rel.displayLabel()
return None
def entity(self):
"""
Returns the underlying entity
"""
return self._entity
def entityName(self):
"""
Returns the undelying entity's name
"""
return self._entity.name()
def foreignKeys(self):
"""
Returns the names of foreign keys for the object, i.e. the source
attributes' names of to-one relationships.
See also: toOneRelationshipKeys(), allToOneRelationshipKeys()
"""
toOneRels=filter(lambda rel: rel.isToOne(), self._entity.relationships())
res=[]
for rel in toOneRels:
res.extend([a.name() for a in rel.sourceAttributes()])
return res
def inverseForRelationshipKey(self, aKey):
"-"
try:
return self._entity.relationshipNamed(aKey).inverseRelationship().name()
except AttributeError:
return None
def primaryKeys(self):
"Returns the names of primary keys for the object."
return tuple(self._entity.primaryKeyAttributeNames())
def propagateInsertionForObject(self, anObject, editingContext):
"""
Examines the object's relationships and searches for related objects not
inserted in 'editingContext', then inserts these objects into
editingContext if they are not already registered.
It is normally called when an EditingContext gets the
processRecentChanges() message.
See also: EditingContext.propagatesInsertionForRelatedObjects()
"""
## To-One
for key in self.toOneRelationshipKeys():
trace('toOne %s on %s'%(key, str(anObject)))
relatedObject=anObject.storedValueForKey(key)
if relatedObject is None or relatedObject.isFault():
continue
if editingContext.globalIDForObject(relatedObject) is None:
editingContext.insertObject(relatedObject)
## To-many
for key in self.toManyRelationshipKeys():
trace('toMany %s on %s'%(key, str(anObject)))
relatedObjects=anObject.storedValueForKey(key)
try:
if not relatedObjects or relatedObjects.isFault():
continue
except AttributeError: # possibly _p_isFault on relatedObjects=sequence
pass
for o in relatedObjects:
if editingContext.globalIDForObject(o) is None:
editingContext.insertObject(o)
def propagateDeleteForObject(self, anObject, editingContext):
"""
Examines the deleted object's relationships and their delete rules, and
takes the appropriate action.
The process of propagating the deletion of an objects examines each
relationship, then acts that way:
- if the delete rule is 'DELETE_NOACTION', simply returns
- same for 'DELETE_DENY': we leave things untouched and wait for the
validation process to raise the appropriate error for such
inconsistencies
- if the delete rule is 'DELETE_NULLIFY', the relation(s) the deleted
object has with other object(s) is/are dismissed so that the graph
of objects is left consistent afterwards
- if the delete rule is 'DELETE_CASCADE', objects in relation with the
deleted object are also deleted --this is obviously detected by
the EditingContext which will then ask them to propagate their
deletion as well.
See also: CustomObject.propagateDeleteWithEditingContext()
EditingContext.processRecentChanges()
Implementation notes:
The propagation process is not optimized at all.
"""
## To-one
for key in self.toOneRelationshipKeys():
trace('toOne %s on %s'%(key, str(anObject)))
if self.deleteRuleForRelationshipKey(key) in (DELETE_NOACTION,
DELETE_DENY):
continue
relatedObject=anObject.storedValueForKey(key)
if relatedObject is None:
continue
relatedObject.willRead() # trigger a possible fault explicitly
if self.deleteRuleForRelationshipKey(key)==DELETE_NULLIFY:
removeObjectFromBothSidesOfRelationshipWithKey(anObject, relatedObject,
key)
continue
if self.deleteRuleForRelationshipKey(key)==DELETE_CASCADE:
# break reference cycle
# __TBD do we need to do this in this case???
removeObjectFromBothSidesOfRelationshipWithKey(anObject, relatedObject,
key)
editingContext.deleteObject(relatedObject)
continue
## To-many
for key in self.toManyRelationshipKeys():
trace('toMany %s on %s'%(key, str(anObject)))
if self.deleteRuleForRelationshipKey(key) in (DELETE_NOACTION,
DELETE_DENY):
continue
##
## _TBD See FaultHandler/implementation notes for these 3 lines
##
relatedObjects=anObject.storedValueForKey(key)
len(relatedObjects) # trigger a possible toMany fault explicitly
relatedObjects=anObject.storedValueForKey(key)
if self.deleteRuleForRelationshipKey(key)==DELETE_NULLIFY:
for relObj in relatedObjects:
removeObjectFromBothSidesOfRelationshipWithKey(anObject, relObj, key)
continue
if self.deleteRuleForRelationshipKey(key)==DELETE_CASCADE:
for relObj in relatedObjects:
# break reference cycle
# __TBD do we need to do this in this case???
removeObjectFromBothSidesOfRelationshipWithKey(anObject, relObj, key)
editingContext.deleteObject(relObj)
continue
def rootClassDescription(self):
"""
Returns the ClassDescription being the root of the inheritance hierarchy
to which the current ClassDescription is bound, or 'self' if it does not
participate in an inheritance hierarchy
See also: superClassDescription(), Entity.rootEntity()
"""
return classDescriptionForName(self._entity.rootEntity().name())
def superClassDescription(self):
"""
Returns the class description being the parent of the current
ClassDescription, or 'None' if the latter does not participate in an
inheritance hierarchy.
See also: rootClassDescription(), Entity.parentEntity()
"""
_parentEntityName=self._entity.parentEntityName()
return _parentEntityName and classDescriptionForName(_parentEntityName) \
or None
def toManyRelationshipKeys(self):
"""
See also: allToManyRelationshipKeys()
"""
toManyRels=filter(lambda rel: rel.isToMany(), self._entity.relationships())
return tuple([rel.name() for rel in toManyRels if rel.isClassProperty()])
def toOneRelationshipKeys(self):
"""
See also: allToOneRelationshipKeys()
"""
toOneRels=filter(lambda rel: rel.isToOne(), self._entity.relationships())
return tuple([rel.name() for rel in toOneRels if rel.isClassProperty()])
def userPresentableDescriptionForObject(self, anObject):
"-"
raise 'Unimplemented'
##
## Validation
##
def validateObjectForDelete(self, anObject):
"""
Checks that for every relationship declared with a 'DELETE_DENY' rule,
'anObject' has no more objects for that key ; if it does, it raises
'Validation.ValidationException'.
See also: CustomObject.validateForDelete(),
Relationship.deleteRule()
"""
# Check relationships
error=Validation.ValidationException()
for relation in self._entity.relationships():
if relation.deleteRule()==DELETE_DENY:
value=anObject.storedValueForKey(relation.name())
if value:
error.aggregateError(DELETE_DENY_KEY, relation.name())
error.finalize()
return
def validateObjectForSave(self, anObject):
"""
Currently does nothing, just returns
"""
return
def validateValueForKey(self, aValue, aKey):
"""
"""
_attribute=self._entity.attributeNamed(aKey)
_relationship=self._entity.relationshipNamed(aKey)
if _attribute is None and _relationship is None:
raise AttributeError, \
"Entity %s has no attribute or relationship named %s" \
% (self._entity.name(), aKey)
prop=_attribute or _relationship
prop.validateValue(aValue)
#if _relationship:
# _relationship.validateValue(aValue)
# return
#
#else: #Attribute
# validationError=Validation.ValidationException()
# try:
# #import pdb; pdb.set_trace()
# _attribute.validateValue(aValue, object)
# except Validation.ValidationException, exc:
# validationError.aggregateException(exc)
# # Custom bizness logic: validate<AttributeName>
# _validateBizLogic='object.validate'+capitalizeFirstLetter(aKey)
# _validateFunction=None
# try:
# _validateFunction=eval(_validateBizLogic)
# except AttributeError:
# # Function is not defined, fine
# pass
#
# if _validateFunction:
# try:
# apply(_validateFunction, (value,))
# except Validation.ValidationException, exc:
# validationError.aggregateException(exc)
# validationError.addErrorForKey(Validation.CUSTOM_KEY_VALIDATION,
# self.name())
# validationError.finalize()
def XMLDescriptionForObject(self, anObject):
"Returns an DOM representing the object"
raise 'Unimplemented'
##
def classForEntity(entity):
"""
Defines the default mechanism used to find the class of a given entity.
This is equivalent to the following statement::
from <packageName>.<moduleName> import <className>
where 'packageName' the entity's model 'packageName()', and 'moduleName' and
'className' are the entity's properties.
Raises ImportError in case it cannot be found
"""
moduleName=entity.moduleName()
className=entity.className()
packageName=entity.model().packageName()
import string, sys, imp
fullPath = packageName+'.'+moduleName
sys_path_ori=sys.path
# Check that this isn't already defined, since load_module reloads the module
theClass = getattr(sys.modules.get(fullPath), className, None)
if theClass:
return theClass
err_msg="""Unable to locate class %s corresponding to entity '%s'.
It is possible that the model's packageName, or the entity's moduleName or
className do not correspond to where the module is installed --for example,
you might have moved it to a sub-package. You can solve this easily by
updating your model so that 'packageName.moduleName.className' points to
the exact location where the class is installed
"""
paths=string.split(fullPath, '.')
module=path=exc_raised=None
for name in paths:
try:
module=getModule(name,path)
path=getattr(module,'__path__', None)
if path: sys.path=path+sys.path
except:
exc_raised=sys.exc_info()[:2]
break
sys.path=sys_path_ori
if not module or exc_raised: # we got an error, the module was not found
err_msg=err_msg%(fullPath+'.'+className, entity.name())
if exc_raised:
import traceback, cStringIO
exc=cStringIO.StringIO()
traceback.print_exception(exc_raised[0], exc_raised[1], None, file=exc)
err_msg+="\nOriginal exception was: %s"%exc.getvalue()
raise ImportError, err_msg
retClass=getattr(module, className, None)
if not retClass:
err_msg=err_msg%(fullPath+'.'+className, entity.name())
err_msg+="\nReason: Unable to find class %s in module %s"%(className,
fullPath)
raise ImportError, err_msg
return retClass
|