# -*- 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.
#-----------------------------------------------------------------------------
"""
FaultHandler & subclasses
A FaultHandler is an object that takes care of the full initialization of
an object conforming to the Faulting interface.
Module's methods manipulates the Faulting interface an object conforms to.
Classes
The module defined four different classes:
- 'FaultHandler' is the parent class of all fault handlers. It defines the
generic API a fault handler uses to finalize initialization of an
object or to make an object a fault.
- 'AccessGenericFaultHandler'
- 'AccessArrayFaultHandler'
- 'AccessFaultHandler'
See also: Faulting interface
CVS information
$Id: FaultHandler.py 932 2004-07-20 06:21:57Z sbigaret $
"""
__version__='$Revision: 932 $'[11:-2]
import sys, weakref
# Framework
from utils import staticmethod
import Qualifier, FetchSpecification
from GlobalID import GlobalIDChangedNotification
from logging import trace
def clearFault(anObject):
"""
Informs the object that it has been fully initialized.
"""
#anObject.
raise 'Unimplemented'
def handlerForFault(anObject):
"""
Returns the fault handler for the object, or None if the object is not
a fault.
Simply sends message 'Faulting.faultHandler()' to 'anObject'.
Parameter:
anObject -- an object conforming to the Faulting interface
"""
return anObject.faultHandler()
def isFault(anObject):
"""
Tells whether the object is a fault. Simply calls Faulting.isFault() on
'anObject'.
Parameter:
anObject -- an object conforming to the Faulting interface
"""
return anObject.isFault()
def makeObjectIntoFault(anObject, aFaultHandler):
"""
Makes 'anObject' a fault. Simply calls Faulting.makeObjectIntoFault() on
that object.
Parameters:
anObject -- an object conforming to the Faulting interface
aFaultHandler -- the fault handler responsible for finishing initialization
of 'anObject'
"""
anObject.turnIntoFault(aFaultHandler)
##
## FaultHandler
##
class FaultHandler:
"""
A FaultHandler works with any object implementing the Faulting interface ;
its purpose is to help any conforming object, postponing their
initialization until needed, to get their actual datas from DB.
"""
# Instance methods
def completeInitializationOfObject(self, anObject): # abstract
"""
abstract
"""
__abstract__()
#createFaultForDeferredFault(aFault, aSourceDatabaseObject)
#descriptionForObject(anObject)
def faultWillFire(self, anObject):
"""
Informs the FaultHandler that the object is about to trigger its fault.
Default implementation does nothing, subclasses do not need to call this
method when subclassing;
Parameter:
anObject -- an object conforming to the 'Faulting' interface
"""
pass
def targetClass(self):
"""
"""
pass
##
## AccessGenericFaultHandler
##
class AccessGenericFaultHandler(FaultHandler):
"""
This is an abstract class providing the following capabilities:
- it provides the faults the ability to be chained/grouped
together. **Note**: this is not used in the framework yet, but it will!
- It also specifies a generic API based on the fact that this sort of
FaultHandlers is based on the "Access Layer", i.e. uses a
'DatabaseContext' and an 'EditingContext' to fetch the datas from an
external store/database.
Implementation notes:
!!! DatabaseContext: should be the right one (not the original but the one
that is responsible for the object). Maybe this can be checked
somewhere TBD
Subclasses must use the '_setContext()' method to initialize the
AccessGenericFaultHandler (typically it will be called in '__init__()')
"""
_generation=0 # not used yet / for use w/ the chain of fault handlers.
_next=None
_previous=None
_dbContext=None
_weakref_ec=None
def completeInitializationOfObject(self, anObject):
"""
Abstract method implemented by subclasses to populate 'anObject'
Parameter:
anObject -- an object implementing the 'Faulting' interface. Within the
framework, it is typically a 'DatabaseObject'
"""
__abstract__()
def databaseContext(self):
"""
Returns the 'DatabaseContext' object the fault handler uses to fetch data
rows from a database.
See also: 'editingContext()'
"""
return self._dbContext
def editingContext(self):
"""
Returns the underlying EditingContext --see _setContext().
Return value may be None if unset or if the underlying editing context has
been garbage-collected (NB: the latter case should not actually happen
since an object/fault ``belonging'' to a garbage-collected/deleted
EditingContext should normally _not_ be hold elsewhere).
"""
return self._weakref_ec()
def faultWillFire(self, anObject):
"""
Removes itself from the chain of handlers
Subclasses can override this method ; if they do, their implementation
must call this implementation.
"""
self._removeSelfFromChain()
def generation(self):
"""
Return the ``generation'' of the fault --see: 'linkAfterHandler()'.
"""
def linkAfterHandler(self, anAccessGenericFaultHandler, generation=None):
"""
Adds the FaultHandler to the chain ended by 'anAccessGenericFaultHandler'.
'self' should not have any predecessor, neither
'anAccessGenericFaultHandler' should have any successor in the chain.
**NB**: Chaining fault handlers is not used in the framework yet.
Parameters:
self -- an AccessGenericFaultHandler for which 'previous()' answers None
anAccessGenericFaultHandler -- an AccessGenericFaultHandler for which
'next()' returns None
generation -- a float indicating when the fault was created. If it is
omitted or 'None', it defaults to 'time.time()'
"""
if generation is None:
import time
generation=time.time()
if self._previous or anAccessGenericFaultHandler._next:
raise ValueError, 'Cannot link self after an handler: self is already '\
'linked after an other handler, or parameter '\
"'anAccessGenericFaultHandler' has another handler linked after "\
'itself'
anAccessGenericFaultHandler._next=self
self._previous=anAccessGenericFaultHandler
def next(self):
"""
Returns the successor of 'self' in the chain, or None.
**NB**: Chaining fault handlers is not used in the framework yet.
"""
return self._next
def previous(self):
"""
Returns the predecessor of 'self' in the chain, or None
**NB**: Chaining fault handlers is not used in the framework yet.
"""
return self._previous
def _setContext(self, aDatabaseContext, anEditingContext):
"""
Initializes the AccessGenericFaultHandler ; subclasses typically call
this method in their '__init__()' method.
Parameters:
aDatabaseContext -- the DatabaseContext from which the object's state
can be restored. Concrete subclasses use it to restore the object from
a database.
anEditingContext -- the editingContext the faulted object is registered
to. Note that the EditingContext is weak-referenced, thus a fault
handler does not prevent it to be garbage-collected. When this happens,
the fault is automatically notified and is invalidated as well --see
'_handleDeletion()'.
"""
self._dbContext=aDatabaseContext
self._weakref_ec=weakref.ref(anEditingContext, self._handleECDeletion)
def _handleECDeletion(self, ref):
"""
Automatically called when the EditingContext the FaultHandler (weak-)
references is deleted ; simply removes itself from the chain of handlers.
You should not call this method directly, nor should it be overriden.
See also: linkAfterHandler()
"""
self._weakref_ec=None
self._dbContext=None
self.generation=None
self._removeSelfFromChain()
def _removeSelfFromChain(self):
"""
Removes itself from the chain. You should never call this method, it is
automatically called when necessary.
Subclasses should not override this method.
See: faultWillFire()
"""
next=self.next()
previous=self.previous()
if not (previous or next):
return
if previous and next:
previous._next=next
next._previous=previous
elif previous:
previous._next=None
elif next:
next._previous=None
self._previous=self._next=None
##
## AccessArrayFaultHandler
##
class AccessArrayFaultHandler(AccessGenericFaultHandler):
"""
Handles to-many relationships
"""
def __init__(self, srcKeyGlobalID, relationshipName,
aDatabaseContext, anEditingContext, object):
"""
Initializes the fault handler with all the necessary informations needed
to fetch rows from a database when the corresponding faulted object needs
to be populated with its own data.
Parameters:
srcKeyGlobalID -- the 'KeyGlobalID' identifying the source object for
which the fault was created.
relationshipName -- the name of the toMany relationship this fault
handler services.
aDatabaseContext -- the 'DatabaseContext' object responsible for fetching
datas for this toMany fault
anEditingContext -- the 'EditingContext' holding the source object and
in which the objects
Implementation notes::
__TBD cf. besoin de violer la regle du fault qui ne contient pas son
__TBD objet, pour le cas des requetes sur __len__, etc. (sequence-like
__TBD messages)
__TBD It would be really better if faults could dynamically change
__TBD their class (python2.2: derive from lists) so that triggering the
__TBD fault automatically gives you the right thing
__TBD cf. e.g. EntityClassDescription.propagateDeleteForObject():
__TBD when processing toMany rels. it has to get the related objects
__TBD w/ storedValueForKey(), trigger the fault, and then re-get it
__TBD to get it right... That shouldnt be made that way. But is there
__TBD any way to do this right in python 2.1 ???
"""
self._setContext(aDatabaseContext, anEditingContext)
self._srcGlobalID=srcKeyGlobalID
self._relationshipName=relationshipName
self.__temporaryHoldFetchedArrayOfObjects=0
import weakref
self.__object=weakref.ref(object)
self.__list=None
# NOTE: python version<2.0 are not supported
#if sys.version_info >= (2, 2):
self.__sequenceTypeMethods=('__len__', '__contains__',
'__getitem__', '__setitem__', '__delitem__',
'__iter__',
'__add__', '__radd__', '__iadd__',
'__mul__', '__rmul__', '__imul__',
'__eq__', '__ne__',
'__gt_', '__ge__',
'__lt__', '__le__',
'__hash__')
def completeInitializationOfObject(self, anObject):
"""
See also: 'DatabaseContext.objectsForSourceGlobalID()'
"""
self.faultWillFire(anObject)
dbContext=self.databaseContext()
objects=dbContext.objectsForSourceGlobalID(self._srcGlobalID,
self._relationshipName,
self.editingContext())
anObject.takeStoredValueForKey(objects, self._relationshipName)
self.__list=self.__object().storedValueForKey(self._relationshipName)
#if self.__temporaryHoldFetchedArrayOfObjects:
# self.__fetchedArrayOfObjects=objects
def isFault(self):
"""
Returns true or false, depending on the object internal state.
When the object is still a fault, returns 1. When the fault was triggered,
this object acts as a proxy for the underlying sequence, and it returns 0
in this case.
This is needed during validation, when an object holds a toMany fault and
validation needs to know whether this is a fault or not.
See also: Relationship.validateValue()
"""
return not self.__list
def relationshipName(self):
"""
Returns the name of the source object's toMany relationship that fault
handler is reponsible for.
"""
return self._relationshipName
def sourceGlobalID(self):
"""
Returns the 'KeyGlobalID' identifying the source object for which that
fault handler was created.
"""
return self._srcGlobalID
# sequence_like API
#def __len__(self):
# self.__temporaryHoldFetchedArrayOfObjects=1
# self.completeInitializationOfObject(self.__object())
# l=len(self.__object().storedValueForKey(self._relationshipName))
# return l
if sys.version_info >= (2, 2): # python v2.2 and higher
def __getattr__(self, name):
if name in self.__sequenceTypeMethods:
if not self.__list:
self.completeInitializationOfObject(self.__object())
#trace(self.__object().storedValueForKey(self._relationshipName))
return getattr(self.__object().storedValueForKey(self._relationshipName), name)
else:
raise AttributeError
else: # python v2.0/v2.1 -> proxy behaviour
## We could also call willChange on the object, but this is no use
## In any case the developper should be aware that it is its responsability
## to call it.
def __len__(self):
if not self.__list:
self.completeInitializationOfObject(self.__object())
l=len(self.__list)
return l
def __getitem__(self, key):
if not self.__list:
self.completeInitializationOfObject(self.__object())
return self.__list[key]
def __setitem__(self, key, value):
if not self.__list:
self.completeInitializationOfObject(self.__object())
self.__list[key]=value
def __delitem__(self, key):
if not self.__list:
self.completeInitializationOfObject(self.__object())
del self.__list[key]
#def __iter__(self):
# if not self.__list:
# self.completeInitializationOfObject(self.__object())
# l=self.__object().storedValueForKey(self._relationshipName)
def __contains__(self, item):
if not self.__list:
self.completeInitializationOfObject(self.__object())
return item in self.__list
def append(self, object):
if not self.__list:
self.completeInitializationOfObject(self.__object())
self.__list.append(object)
def count(self, value):
if not self.__list:
self.completeInitializationOfObject(self.__object())
return self.__list.count(value)
def index(self, value):
if not self.__list:
self.completeInitializationOfObject(self.__object())
return self.__list.index(value)
def insert(self, index, object):
if not self.__list:
self.completeInitializationOfObject(self.__object())
self.__list.insert(index, object)
def pop(self, index=None):
if not self.__list:
self.completeInitializationOfObject(self.__object())
if index: return self.__list.pop(index)
else: return self.__list.pop()
def remove(self, value):
if not self.__list:
self.completeInitializationOfObject(self.__object())
self.__list.remove(value)
def reverse(self):
if not self.__list:
self.completeInitializationOfObject(self.__object())
self.__list.reverse()
def sort(self, cmpfunc=None):
if not self.__list:
self.completeInitializationOfObject(self.__object())
if cmpfunc: self.__list.sort(cmpfunc)
else: self.__list.sort()
# etc. (slice, etc.) cf python doc.
##
## AccessFaultHandler
##
class AccessFaultHandler(AccessGenericFaultHandler):
"""
Handles to-one relationships
"""
def __init__(self, keyGlobalID, aDatabaseContext, anEditingContext):
"""
Initializes the 'AccessFaultHandler' with the objects it needs to get
the data back from the Database.
Parameters:
keyGlobalID -- the KeyGlobalID for the faulted object
aDatabaseContext -- the 'DatabaseContext' object responsible for fetching
the data for the object identified by 'keyGlobalID'
anEditingContext -- the 'EditingContext' object managing the graph of
objects in which the object identified by 'keyGlobalID' participates.
"""
if keyGlobalID.isTemporary():
raise ValueError, 'Cannot initialize a FaultHandler with a temporary'\
'GlobalID'
self._setContext(aDatabaseContext, anEditingContext)
self._globalID=keyGlobalID
def completeInitializationOfObject(self, anObject):
"""
"""
#import pdb; pdb.set_trace()
self.faultWillFire(anObject)
dbContext=self.databaseContext()
# First check that the corresponding data were not already fetched
if dbContext.database().snapshotForGlobalID(self._globalID):
dbContext.initializeObject(anObject, self._globalID,
self.editingContext())
anObject.clearFault()
return
else:
pass # TBD it is possible not to get the snapshot while it actually
# TBD exists, when db is asked for a root_keyGlobalID and not
# TBD for the real one
# Let's fetch the data: first, we need to build the qualifier & fetchSpec
dbContext=self.databaseContext()
entity=dbContext.database().entityNamed(self._globalID.entityName())
qualifier=entity.qualifierForPrimaryKey(self._globalID.keyValues())
fetchSpec=FetchSpecification.FetchSpecification(entity.name(),
qualifier=qualifier,
deepFlag=1) ## !!!
# sends objectsWithFetchSpec. to dbContext
objects=dbContext.objectsWithFetchSpecification(fetchSpec,
self.editingContext())
# get the result back, check existence and unicity
if len(objects)!=1:
raise RuntimeError, 'Error: Unexpected: Unable to complete '\
'initialization of object: fetched %i object%s'%\
(len(objects), len(objects)>1 and 's' or '')
# NB: fault is cleared within DatabaseChannel.fetchObject()
# hence, we do not need to clear it explicitly
#def descriptionForObject(self, anObject):
# """
# """
def globalID(self):
"""
Returns the 'KeyGlobalID' of the object this FaultHandler is responsible
for.
"""
return self._globalID
def __abstract__():
raise 'AbstractMethod', 'Left intentionally unimplemented in this class, '\
'subclass should override this method'
|