# -*- 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.
#-----------------------------------------------------------------------------
"""
EditingContext
Notifications
ObjectsChangedInStoreNotification -- declared in 'ObjectStore', refer to
its documentation for details
ObjectsChangedInEditingContextNotification -- Same as the previous one,
except that the values posted in the 'info' field along with the
notification are objects, not GlobalIDs.
*Important*: Listeners for these notification should take care not to hold
the transmitted objects or globalIDs ; if they need to store them, e.g. to
process things later, please keep weak-references on them.
Implementation note: (__TBD) maybe this ('weakref') should be done before
posting the notifications.
CVS information
$Id: EditingContext.py 932 2004-07-20 06:21:57Z sbigaret $
"""
__version__='$Revision: 932 $'[11:-2]
from Modeling import ObjectStoreCoordinator
from Modeling.ObjectStore import ObjectStore,\
InvalidatedAllObjectsInStoreNotification,\
ObjectsChangedInStoreNotification, \
DeletedKey, InsertedKey, InvalidatedKey, UpdatedKey
from Modeling.GlobalID import GlobalIDChangedNotification
from Modeling.GlobalID import globalIDWithEntityName
from Modeling.Qualifier import filteredArrayWithQualifier
from Modeling import ObserverCenter
from Modeling.utils import toBoolean
import ClassDescription
# interfaces
from interfaces.EditingContext import IEditingContext
from interfaces.ObservingInterface import ObservingInterface
from logging import info,warn,trace
import weakref
## Notifications
from NotificationFramework import NotificationCenter
NC=NotificationCenter
from ObjectStore import ObjectsChangedInStoreNotification
ObjectsChangedInEditingContextNotification='ObjectsChangedInEditingContextNotification'
## you should *NOT* directly access to module's variable
## '__defaultCoordinator' since it is considered private (MT-safety)
from threading import RLock
__defaultParentObjectStore=None
defaultParentObjectStore_lock=RLock()
lock=defaultParentObjectStore_lock.acquire
unlock=defaultParentObjectStore_lock.release
class ObjectNotRegisteredError(Exception):
"""
Raised whenever an object is accessed that should be inserted in an
EditingContext but is not.
"""
pass
def defaultParentObjectStore():
"See interfaces.EditingContext for details"
lock()
try:
global __defaultParentObjectStore
if __defaultParentObjectStore is None:
return ObjectStoreCoordinator.defaultCoordinator()
return __defaultParentObjectStore
finally:
unlock()
def setDefaultParentObjectStore(anObjectStore):
"See interfaces.EditingContext for details"
lock()
try:
__defaultParentObjectStore=anObjectStore
finally:
unlock()
## Implementation notes
## This module needs refactoring for sure ; for example, the internal states
## representing the managed graph of objects could be separated (UniquingTable
## is the very first step!), especially for everything concerning changes
## within the graph of objects
## [related methods: insertedObjects(), updatedObjects(), deletedObjects(),
## has(Unprocessed)Changes(), processRecentChanges(), insertObject(),
## recordObject(), deleteObject(), objectWillChange()]
from Modeling.utils import base_object
class EditingContext(base_object, ObjectStore): # ???
"""
EditingContext: an in-memory world/graph of objects, with the ability
to ensure its own persistence, to check its consistency, etc.
Parent/child EditingContext configuration -- this allow the developper to
get the notion of transactions for an in-memory graph of objects.
See: parentObjectStore, __init__()
Unsupported features:
Undo -- this is yet unsupported. However, this is now clear how this
should be done, given that there is an UndoManager or something like
that. The undo stack will grow each time 'processRecentChanges()' is
called (usually automatically, at some point where it make sense, for
example, in an HTTP-based application, when the HTTP request/response
loop is about to end).
"""
__implements__ = (IEditingContext,)
_is_an_editingcontext_=1 # remove this a.s.a. parent/child ECs
# configuration is supported
# see: __init__()
########
class UniquingTable(base_object):
"""
The uniquing table - encapsulate the dictionary structure
Defines the standard behaviour for methods: 'globalIDForObject()',
'objectForGlobalID', etc.
Its goal is to guarantee that the uniquing principle is enforced, i.e.
that one given row in the database cannot be represented by more than one
object within the EditingContext.
Since faults / ghosted object are usually identified by their so-called
``root GlobalID'' (a root GlobalID has exactly the same keyValues() than
the real GlobalID but has an entityName() referring to the root Entity,
not the object's actual Entity ; this is because the actual Entity for an
object can only be known when data for that object is actually fetched
against the database), the UniquingTable also keeps track of root
globalIDs in order to be able to serve objects from globalID being either
the root and the real GlobalID.
"""
def __init__(self):
self._table={} # GlobalIDs -> objects
self._invTable={} # objects -> GlobalIDs
self._rootGids={} # gIDs -> root_gIDs
self._invRootGids={} # root_gIDs -> gIDs
def globalIDs(self):
return self._table.keys()
def objects(self):
return self._table.values()
def addObjectForGlobalID(self, anObject, aGlobalID):
#trace('Adding globalID: %s'%str(aGlobalID))
if self._table.has_key(aGlobalID):
warn('Try to register an already registered object')
return # or raise?
# generate root_keyGlobalID
root_GlobalID=None
if not aGlobalID.isTemporary():
from Modeling import ClassDescription
from Modeling.GlobalID import KeyGlobalID
entityName=aGlobalID.entityName()
cd=ClassDescription.classDescriptionForName(entityName)
cd_root=cd.rootClassDescription()
if cd_root.entityName() != entityName:
root_GlobalID=KeyGlobalID(cd_root.entityName(),
aGlobalID.keyValues())
self._rootGids[aGlobalID]=root_GlobalID
self._invRootGids[root_GlobalID]=aGlobalID
### Additional test, to be removed
#def rootkGid(gid):
# from Modeling.GlobalID import KeyGlobalID
# from Modeling import ClassDescription
# cd=ClassDescription.classDescriptionForName(gid.entityName())
# cd_root=cd.rootClassDescription()
# return KeyGlobalID(cd_root.entityName(), gid.keyValues())
#
#if not aGlobalID.isTemporary():
# if root_GlobalID in [rootkGid(g) for g in self._table.keys()
# if not g.isTemporary()]:
# alreadyRegisteredGids=[g for g in self._table.keys()
# if not g.isTemporary() and \
# rootkGid(g)==root_aGlobalID]
# alreadyRegisteredObjs=[self._table[g] for g in alreadyRegisteredGids]
# alreadyRegisteredGids=[str(g) for g in alreadyRegisteredGids]
#
# raise ValueError, 'Inconsistency: trying to register in EC uniquing table has two different objects and gIDs corresponding to the same row: existing: (%s,%s) / new: (%s,%s)'%(alreadyRegisteredGids, alreadyRegisteredObjs, str(aGlobalID), anObject)
### /Additional test
self._table[aGlobalID]=anObject
self._invTable[anObject]=aGlobalID
def clear(self):
"Clear all entries"
self._table.clear()
self._invTable.clear()
self._rootGids.clear()
self._invRootGids.clear()
def forgetObject(self, object):
"""
Removes the object and the corresponding GlobalID from the uniquing
table
"""
gid=self._invTable[object]
del self._table[self._invTable[object]]
del self._invTable[object]
try: del self._rootGids[aGlobalID]
except: pass
try: del self._invRootGids[aGlobalID]
except: pass
def forgetObjectForGlobalID(self, aGlobalID):
"""
Removes the GlobalID and the corresponding object from the uniquing
table
"""
del self._invTable[self._table[aGlobalID]]
del self._table[aGlobalID]
try: del self._rootGids[aGlobalID]
except: pass
try: del self._invRootGids[aGlobalID]
except: pass
def globalIDForObject(self, anObject):
"""
Returns the GlobalId registered with that object, or 'None' if it cannot
be found
"""
return self._invTable.get(anObject, None)
#if self.hasObject(anObject):
# for key in self._table.keys():
# if self._table[key]==anObject:
# return key
#return None
def hasGlobalID(self, aGlobalID):
return aGlobalID in self._table.keys() or \
aGlobalID in self._rootGids.values()
def hasObject(self, anObject):
return anObject in self._table.values()
def objectForGlobalID(self, aGlobalID):
"""
Returns the object corresponding to the given 'aGlobalID', or 'None' if
it cannot be found in the receiver's uniquing table.
"""
#trace('Searching globalID: %s'%str(aGlobalID))
return self._table.get(aGlobalID, None) or \
self._table.get(self._invRootGids.get(aGlobalID, None),None)
########
# Class
invalidatesObjectsWhenFinalized_default=toBoolean(1)
def __init__(self, parentObjectStore=None):
"""
Initializes an 'EditingContext'.
Parameter 'parentObjectStore'
determines which 'ObjectStore' the receiver will use to fetch its data
and to save changes made in it ; the default (i.e. when the parameter is
omitted or equals to 'None') is to use the one returned by
'defaultParentObjectStore()' --which in turn defaults to the
application-wide shared 'CooperatingObjectStore.defaultCoordinator()'.
Note that the parent 'ObjectStore' of an 'EditingContext' cannot be
changed during the whole object's lifetime.
You will typically use the default behaviour, or provide an parent
'EditingContext' to make the newly created 'EditingContext' a child of
the former one (not supported yet __TBD).
Implementation Note:
The object graph is hold by 4 variables:
- the UniquingTable holds the whole graphs
- _insertedObjects
- _deletedObjects
- _updatedObjects: these three variables only reference GlobaldIDs
More than that: there should *only* be empty intersections
between each one and an other, or many methods could fail or
mis-behave in an uncontrolled way!!
Last, but not least, please note that the union of these 3 sets
is a subset of the whole graph (which additionally holds
objects that were fetched but are still unmodified).
- Additionally, each of these last three attributes has its equivalent
for changes that were not yet processed (e.g. _pendingUpdatedObjects).
These changes are moved to the
Additional notes::
__TBD This is not that good... in fact there should be some
__TBD 'ObjectGraph' object here to manage these.
__TBD Plus: the way it is implemented here is naive and certainly
__TBD sub-sub-optimal in terms of run-time efficiency (data structures
__TBD imply time-consuming treatments). refactoring will be done in
__TBD the future, for the moment I need to concentrate on features...
"""
self.__instanceLock=RLock()
self._pendingDeletedObjects=[] # only holds GlobalIDs
self._pendingInsertedObjects=[]
self._pendingUpdatedObjects=[]
self._deletedObjects=[] # only holds GlobalIDs
self._insertedObjects=[]
self._updatedObjects=[]
self._uniquingTable=EditingContext.UniquingTable()
self.__resolvedFaultsGIDs={} # rootGid -> resolved Gid (inheritance)
self._propagatesInsertionForRelatedObjects=0
if parentObjectStore is None:
parentObjectStore=defaultParentObjectStore()
# remove this statement a.s.a. parent/child ECs configuration is supported
#if getattr(parentObjectStore, '_is_an_editingcontext_', None):
# raise 'Unimplemented', 'Parent/child ECs configuration not supported yet'
self._parentObjectStore=parentObjectStore
# Registering for notifications __TBD
# Changes within the editing context
def allDeletedGlobalIDs(self):
"""
Returns a sequence made of GlobalIDs of objects that have been deleted
into the EditingContext and that have not been made persistent yet.
The GlobalIDs returned are those identifying objects who were marked as
deleted within the EditingContext, while deletedGlobalIDs() only returns
the GlobalIDs for objects that were marked as deleted before the last
invocation of processRecentChanges() (or saveChanges()).
See also: deletedGlobalIDs(), allDeletedObjects()
"""
return tuple(self._deletedObjects+self._pendingDeletedObjects)
def deletedGlobalIDs(self):
"""
Returns a sequence made of GlobalIDs of objects that were removed from the
graph of objects managed by the EditingContext.
The GlobalIDs returned are those indentifying objects who were marked as
deleted before the last invocation of processRecentChanges() (or
saveChanges()). If you want to get all deleted GlobalIDs, see
allDeletedGlobalIDs().
See also: deletedObjects()
"""
return tuple(self._deletedObjects)
def allDeletedObjects(self):
"""
Returns a sequence made of objects that were removed from the graph of
objects managed by the EditingContext.
allDeletedObjects() returns all the objects that have been marked as
deleted within the EditingContext, while deletedObjects() only returns
the objects that were marked as deleted before the last invocation of
processRecentChanges() (or saveChanges()).
See also: insertedObjects(), updatedObjects(), hasChanges
"""
return map(self.objectForGlobalID,
self._deletedObjects+self._pendingDeletedObjects)
def deletedObjects(self):
"""
Returns a sequence made of objects that were removed from the graph of
objects managed by the EditingContext.
The objects returned are those who were marked as deleted before the last
invocation of processRecentChanges() (typically after saveChanges()). If
you want to get all deleted objects, see allDeletedObjects().
See also: insertedObjects(), updatedObjects(), hasChanges
"""
return map(self.objectForGlobalID, self._deletedObjects)
def allInsertedGlobalIDs(self):
"""
Returns a sequence made of GlobalIDs of objects that have been inserted
into the EditingContext and that have not been made persistent yet.
The GlobalIDs returned are those identifying objects who were marked as
inserted within the EditingContext, while insertedGlobalIDs() only returns
the GlobalIDs for objects that were marked as inserted before the last
invocation of processRecentChanges() (or saveChanges()).
See also: insertedObjects(), allInsertedObjects()
"""
return tuple(self._insertedObjects+self._pendingInsertedObjects)
def insertedGlobalIDs(self):
"""
Returns a sequence made of GlobalIDs of objects that have been inserted
into the EditingContext and that have not been made persistent yet.
The GlobalIDs returned are those indentifying objects who were marked as
inserted before the last invocation of processRecentChanges() (or
saveChanges()). If you want to get all inserted GlobalIDs, see
allInsertedGlobalIDs().
See also: insertedObjects()
"""
return tuple(self._insertedObjects)
def allInsertedObjects(self):
"""
Returns a sequence made of the objects that have been inserted into the
EditingContext and that have not been made persistent yet.
allInsertedObjects() returns all the objects that have been marked as
inserted within the EditingContext, while insertedObjects() only returns
the objects that were marked as inserted before the last invocation of
processRecentChanges() (or saveChanges()).
See also: deletedObjects(), updatedObjects(), hasChanges()
"""
return map(self.objectForGlobalID,
self._insertedObjects+self._pendingInsertedObjects)
def insertedObjects(self):
"""
Returns a sequence made of the objects that have been inserted into the
EditingContext and that have not been made persistent yet.
The objects returned are those who were marked as inserted before the last
invocation of processRecentChanges() (typically, after saveChanges()). If
you want to get all inserted objects, see allInsertedObjects().
See also: deletedObjects(), updatedObjects(), hasChanges()
"""
return map(self.objectForGlobalID, self._insertedObjects)
def allUpdatedGlobalIDs(self):
"""
Returns a sequence made of GlobalIDs of objects that have been updated
into the EditingContext and that have not been made persistent yet.
The GlobalIDs returned are those identifying objects who were marked as
updated within the EditingContext, while updatedGlobalIDs() only returns
the GlobalIDs for objects that were marked as updated before the last
invocation of processRecentChanges() (or saveChanges()).
See also: updatedObjects(), allUpdatedObjects()
"""
return tuple(self._updatedObjects+self._pendingUpdatedObjects)
def updatedGlobalIDs(self):
"""
Returns a sequence made of GlobalIDs of objects that have changed since
the last saveChanges(), or since the EditingContext was created.
The GlobalIDs returned are those indentifying objects who were marked as
updated before the last invocation of processRecentChanges() (or
saveChanges()). If you want to get all updated GlobalIDs, see
allUpdatedGlobalIDs().
See also: allUpdatedGlobalIDs(), updatedObjects()
"""
return tuple(self._updatedObjects)
def allUpdatedObjects(self):
"""
Returns a sequence made of the objects that are marked for update.
allUpdatedObjects() returns all the objects that have been marked for
update within the EditingContext, while updatedObjects() only returns
the objects that were marked for update before the last invocation of
processRecentChanges() (typically, after saveChanges()).
See also: deletedObjects(), insertedObjects(), hasChanges()
"""
return map(self.objectForGlobalID,
self._updatedObjects+self._pendingUpdatedObjects)
def updatedObjects(self):
"""
Returns a sequence made of the objects that have changed since the last
invocation of processRecentChanges (typically, after the last
saveChanges()).
If you want to get all updated objects, including those marked after the
last invocation of processRecentChanges(), see allUpdatedObjects().
See also: deletedObjects(), insertedObjects(), hasChanges()
"""
return map(self.objectForGlobalID, self._updatedObjects)
def hasChanges(self):
"""
Tells whether some changes have been made in the graph of objects the
EditingContext manages, whose have not been made persistent yet
(i.e. newly inserted objects, some that have been modified or some that
have been removed from the graph of objects
This method does not make any difference between processed and unprocessed
changes ; to know if there are some unprocessed changes, use
'hasUnprocessedChanges()' instead.
See also: deletedObjects(), insertedObjects(), updatedObjects(),
hasUnprocessedChanges()
"""
return self._insertedObjects or self._updatedObjects or \
self._deletedObjects or \
self._pendingInsertedObjects or self._pendingUpdatedObjects or \
self._pendingDeletedObjects
def hasUnprocessedChanges(self):
"""
Tells whether the EditingContext knows that some changes were made to its
graph of objects that were not processed yet
See also: processRecentChanges()
"""
return self._pendingInsertedObjects or self._pendingUpdatedObjects or \
self._pendingDeletedObjects
# Processing changes
def processRecentChanges(self):
"""
Examines the graph of objects and take appropriate actions.
This includes:
- registers all unprocessed changes: inserted, updated and deleted
objects
- if propagatesInsertionForRelatedObjects() is true, it searches the
inserted and updated objects's relationships for newly created
objects, which are in turn inserted into the EditingContext, if
needed.
- for all deleted objects, cascade the delete if this needs to be done
- __TBD registers undo informations
- posts 'ObjectsChangedInEditingContextNotification' and
'ObjectsChangedInStoreNotification'
This method is also automatically called when the EditingContext is
instructed to save changes.
See also: saveChanges(), saveChangesInEditingContext()
ClassDescription.propagateDeleteForObject()
ClassDescription.propagateInsertionForObject()
"""
_changedGids={DeletedKey: [],
InsertedKey: [],
UpdatedKey: []}
_changedObjects={DeletedKey:[]}
while self.hasUnprocessedChanges():
for gID in self._pendingUpdatedObjects:
if gID not in self._updatedObjects:
self._updatedObjects.append(gID)
_changedGids[UpdatedKey].append(gID)
object=self.objectForGlobalID(gID)
#http://mail.python.org/pipermail-21/persistence-sig/2002-July/000152.html
if self.propagatesInsertionForRelatedObjects():
#may insert some objects
object.propagateInsertionWithEditingContext(self)
ObserverCenter.ensureSubsequentChangeWillBeNotifiedForObject(object)
self._pendingUpdatedObjects=[]
while self._pendingInsertedObjects:
# We suppose here that an inserted objects modified after its
# insertion has been moved from self._insertedObjects back to
# self._pendingInsertedObjects: examining the whole set of inserted
# objects could be really time-consuming, hence we use such a policy
# to reduce the number of objects to be checked.
# See objectWillChange() which is responsible for that policy.
_copy_pendingInsertedObjects=list(self._pendingInsertedObjects) # copy
self._pendingInsertedObjects=[]
for gID in _copy_pendingInsertedObjects:
if gID in self._deletedObjects:
self._deletedObjects.remove(gID)
self._insertedObjects.append(gID)
if gID not in _changedGids[InsertedKey]:
_changedGids[InsertedKey].append(gID)
if self.propagatesInsertionForRelatedObjects():
object=self.objectForGlobalID(gID)
object.propagateInsertionWithEditingContext(self)
ObserverCenter.ensureSubsequentChangeWillBeNotifiedForObject(object)
self._pendingInsertedObjects=[]
# Now check the deleted objects and propagate the deletes
_copy_pendingDeletedObjects=list(self._pendingDeletedObjects) # copy
self._pendingDeletedObjects=[]
for gID in _copy_pendingDeletedObjects:
try: self._insertedObjects.remove(gID)
except ValueError: pass
try: self._updatedObjects.remove(gID)
except ValueError: pass
try: _changedGids[InsertedKey].remove(gID)
except ValueError: pass
try: _changedGids[UpdatedKey].remove(gID)
except ValueError: pass
object=self.objectForGlobalID(gID)
# ClassDescription.propagateDeleteForObject(), called by
# propagateDeleteWithEditingContext() populates
# self._pendingDeletedObjects (when calling deleteObject())
object.propagateDeleteWithEditingContext(self)
try:
# The propagation of the deletion can modify the object, e.g. when
# one the relationship is marked as NULLIFY. Since
# self._pendingDeletedObjects is empty when
# propagateDeleteWithEditingContext is triggered, it will be marked
# as changed [cf. self.objectWillChange()], but we now this cannot
# be the case, so we remove it, unconditionally [Bug #599602].
self._pendingUpdatedObjects.remove(gID)
except ValueError:
pass
if gID not in self._deletedObjects: #
self._deletedObjects.append(gID)
_changedGids[DeletedKey].append(gID)
_changedObjects[DeletedKey].append(object)
# Notifications
# finish initializing dict (userInfo) for Obj.ChangedInEditingContext
if _changedGids[InsertedKey]:
_changedObjects[InsertedKey]=map(lambda o, s=self: s.objectForGlobalID,
_changedGids[InsertedKey])
if _changedGids[UpdatedKey]:
_changedObjects[UpdatedKey]=map(lambda o, s=self: s.objectForGlobalID,
_changedGids[UpdatedKey])
NC.postNotification(ObjectsChangedInEditingContextNotification,
self, _changedObjects)
NC.postNotification(ObjectsChangedInStoreNotification,
self, _changedGids)
def propagatesInsertionForRelatedObjects(self):
"""
If true, processRecentChanges() will search for newly created objects
related to updated&inserted objects in the EditingContext, and these new
objects will in turn be automatically inserted in the EditingContext.
Default is false.
"""
return self._propagatesInsertionForRelatedObjects
autoInsertion=propagatesInsertionForRelatedObjects
def setPropagatesInsertionForRelatedObjects(self, bool):
"""
See propagatesInsertionForRelatedObjects() for details.
If 'bool' is true and self.propagatesInsertionForRelatedObjects() is
false, all updated and inserted objects are sent the
'propagateInsertionWithEditingContext' message ; this can take some time.
We suggest that you do not change the behaviour after you started working
with objects within the EditingContext ; it is probably better to choose
to set it to true or false right after the EditingContext is created.
Parameter:
bool -- a boolean value
"""
if bool and not self._propagatesInsertionForRelatedObjects:
for object in self.updatedObjects()+self.insertedObjects():
object.propagateInsertionWithEditingContext(self)
self._propagatesInsertionForRelatedObjects=not not bool
setAutoInsertion=setPropagatesInsertionForRelatedObjects
def saveChanges(self):
"""
Tells the EditingContext to save the changes made within the graph of
objects it is responsible for. It first sends the 'processRecentChanges'
message to itself, then validates the changes against the constraints
defined both in the model and in possible custom validation methods, and
finally calls its parent objects store's 'saveChangesInEditingContext()'
method.
Raises Validation.ValidationException if the validation process failed, or
a RuntimeError if something failed during the process of saving changes in
the underlying database(s).
See also: saveChangesInEditingContext(), processRecentChanges(),
parentObjectStore(), validateChanges()
"""
self.processRecentChanges()
self.validateChanges()
self.parentObjectStore().saveChangesInEditingContext(self)
for obj in self._uniquingTable.objects():
ObserverCenter.ensureSubsequentChangeWillBeNotifiedForObject(obj)
# remove deleted objects
for obj in self.deletedObjects():
self.forgetObject(obj)
self._deletedObjects=[]
self._insertedObjects=[]
self._updatedObjects=[]
def validateChanges(self):
"""
Performs validation on all objects in the EditingContext.
This is automatically called by saveChanges() before changes are forwarded
to the database(s). You should rarely need to call it yourself. If you do,
you must make sure that processRecentChanges() is called before, since
this method does not process pending changes.
The validation process stops after the first error.
See also: processRecentChanges(), saveChanges()
"""
for gID in self._insertedObjects+self._updatedObjects:
self.objectForGlobalID(gID).validateForSave()
for gID in self._deletedObjects:
self.objectForGlobalID(gID).validateForDelete()
# Object identification
def globalIDForObject(self, anObject):
"""
Returns the GlobalID for 'anObject', or 'None' if 'anObject' is not
under the receiver's control.
"""
return self._uniquingTable.globalIDForObject(anObject)
def objectForGlobalID(self, aGlobalID):
"""
Returns the object corresponding to the given 'aGlobalID', or 'None' if
it cannot be found in the receiver's uniquing table.
"""
obj = self._uniquingTable.objectForGlobalID(aGlobalID)
possibleNewGid=self.__resolvedFaultsGIDs.get(aGlobalID)
if not obj and possibleNewGid:
obj=self._uniquingTable.objectForGlobalID(possibleNewGid)
return obj
# Object and graph of objects manipulation
def deleteObject(self, anObject):
"""
Informs the 'EditingContext' that the supplied object should be marked as
deleted, so that it will actually be deleted when the receiver saves
changes. This is not an error to mark an object as deleted more than
once: this will be silently ignored.
Parameter 'anObject' should already be registered within the
'EditingContext' ; if not, ObjectNotRegisteredError is raised.
Both methods deleteObject() and delete() are completely equivalent.
"""
gID=self.globalIDForObject(anObject)
if gID is None:
raise ObjectNotRegisteredError
if anObject.isFault():
# we need to trigger the fault, because of
# propagateDeleteWithEditingContext() triggered in
# processRecentChanges(). This propagation cannot work on a fault since
# a fault is, by definition, a non-initialized object, hence it has no
# idea on related objects.
# __TBD maybe this can be moved to EntityClassDescription, so that
# __TBD the fault is triggered only when needed??
anObject.willRead()
try: self._pendingInsertedObjects.remove(gID)
except ValueError: pass
else:
## TBD note: simply forgetting the object here prevents the
## TBD DELETE_ mechanisms to be triggered appropriately when
## TBD processRecentChanges() is called. This should be fixed!!
self.forgetObject(anObject)
return
try: self._insertedObjects.remove(gID)
except ValueError: pass
else:
## TBD: see notes below
self.forgetObject(anObject)
return
try: self._pendingUpdatedObjects.remove(gID)
except ValueError: pass
if gID not in self._deletedObjects and \
gID not in self._pendingDeletedObjects: # ignore
self._pendingDeletedObjects.append(gID)
delete=deleteObject
def forgetObject(self, anObject):
"""
Inverse for recordObject(): removes itself from the observers of
'anObject', removes the object and its GlobalID from the UniquingTable,
and removes the weakref for self stored in 'anObject'.
Parameter:
anObject -- a CustomObject instance
You should never call this method by yourself: it is for internal use only
(e.g. at the end of the process of saving changes, DatabaseContexts uses
this to inform the EditingContext of any changes that occured on objects'
globalIDs --which is always the case when an inserted object has been
saved in the DB)
"""
trace('Call')
gID=self.globalIDForObject(anObject)
self._uniquingTable.forgetObject(anObject)
ObserverCenter.removeObserver(self, anObject)
anObject._CustomObject__editingContext=lambda: None
if not gID.isTemporary() and not anObject.isFault():
db=self.rootObjectStore().objectStoreForObject(anObject).database()
db.decrementSnapshotCountForGlobalID(gID)
# [...] __TBD forgetObject()
#self.rootObjectStore().objectStoreForObject(anObject).editingContextDidForgetObjectWithGlobalID(self.globalIDForObject(anObject)
def insertObject(self, anObject):
"""
Inserts a new object within the graph of objects. The 'EditingContext'
generates a 'TemporaryGlobalID' for that object, registers the object as a
new one in its graph of objects and registers itself as an observer for
that object (see ObserverCenter).
An object should not be inserted more than once. The only valid situation
where an object can be re-inserted is when this object has previously been
marked as deleted (hence un-deleting the object), otherwise 'ValueError'
is raised.
However, if self.propagateInsertionWithEditingContext() is true, a
RuntimeWarning is issued, rather than raising ValueError. In that case,
the warning message will refer to the caller of 'insertObject'. If you
want to turn these warnings to real exceptions (e.g. for systematic
tracking of such situations), simply put the following statement in your
app.'s init. code::
import warnings
warnings.filterwarnings(action='error', category=RuntimeWarning)
or start 'python' with argument '-W error::RuntimeWarning::0'.
(see the documentation for standard module warnings for further details)
Both methods insertObject() and insert() are completely equivalent.
"""
gID=self._uniquingTable.globalIDForObject(anObject)
if not gID: # register the new object
tmpID=globalIDWithEntityName(anObject.entityName())
self.insertObjectWithGlobalID(anObject, tmpID)
else:
# Already registered: only deleted objects are allowed to re-insert
if not gID in self._pendingDeletedObjects and \
not gID in self._deletedObjects:
if self.propagatesInsertionForRelatedObjects():
import warnings
warnings.warn('Attempt to insert an already registered object',
RuntimeWarning, stacklevel=2)
else:
raise ValueError, 'Object already registered'
else: # cancel the deletion
if gID in self._pendingDeletedObjects:
self._pendingDeletedObjects.remove(gID)
if gID in self._deletedObjects:
self._deletedObjects.remove(gID)
# Mark it as updated, because we here have no way to know whether
# it was updated or not before it was deleted, or even between its
# deletion and the cancellation of its deletion!
self._pendingUpdatedObjects.append(gID)
insert=insertObject
def insertObjectWithGlobalID(self, anObject, aTemporaryGlobalID):
"""
You should normally not call this method directly. To insert an object,
use 'insertObject()' instead.
Parameter:
'aTemporaryGlobalID' -- must respond 'true' to message 'isTemporary'
(the method raises 'ValueError' otherwise)
"""
if not aTemporaryGlobalID.isTemporary():
raise ValueError, "(Parameter) aTemporaryGlobalID.isTemporary() should "\
"evaluate to false"
self.recordObject(anObject, aTemporaryGlobalID)
self._pendingInsertedObjects.append(self.globalIDForObject(anObject))
if not self.parentObjectStore().handlesObject(anObject):
raise ValueError, 'Parent Object Store is not able to deal with object'
anObject.awakeFromInsertion(self)
def recordObject(self, anObject, aGlobalID):
"""
Make the editing context aware of this object by adding itself as an
observer for it and by adding it to the uniquing table.
You should never need to invoke this method directly, it it automatically
called within the framework when it needs to. To insert an object into the
graph of objects, use 'insertObject()' instead.
See also: insertObject()
"""
ObserverCenter.addObserver(self, anObject)
anObject._CustomObject__editingContext=weakref.ref(self)
# + registers it in the uniquing table
self._uniquingTable.addObjectForGlobalID(anObject, aGlobalID)
NC.addObserver(self, EditingContext.handleNotification,
GlobalIDChangedNotification)
def registeredObjects(self):
"""
Returns all objects managed by the EditingContext
"""
return self._uniquingTable.objects()
# Behaviour the EditingContext is garbage-collected
def invalidatesObjectsWhenFinalized(self):
"""
default==true==when finalized, sends 'clearProperties' to all objects
"""
if hasattr(self, '_invalidatesObjectsWhenFinalized'):
return self._invalidatesObjectsWhenFinalized
return self.invalidatesObjectsWhenFinalized_default
def setInvalidatesObjectsWhenFinalized(self, aBool):
"""
-
See also: invalidatesObjectsWhenFinalized()
"""
self._invalidatesObjectsWhenFinalized=toBoolean(aBool)
def dispose(self):
"""
-
"""
trace('Call')
try: ## TBD check that objects' referneces are decr. whatever happens here
for object in self._uniquingTable.objects():
self.forgetObject(object)
if self.invalidatesObjectsWhenFinalized():
object.clearProperties()
except:
import cStringIO, traceback
exc=cStringIO.StringIO()
traceback.print_exc(file=exc)
trace(exc.getvalue())
del exc
def __del__(self):
"""
cf. invalidatesObjectsWhenFinalized()
"""
trace('Call')
self.dispose()
# ObjectStore hierarchy related methods
def parentObjectStore(self):
"""
Returns the parent ObjectStore for the EditingContext ; it is typically
an 'ObjectStoreCorrdinator'
"""
return self._parentObjectStore
def rootObjectStore(self):
"""
Returns the 'ObjectStoreCoordinator' in the underlying hierarchy of
'ObjectStore'.
Forwards the 'rootObjectStore' message to the 'parentObjectStore()', which
in turn does the same thing, until the 'ObjectStoreCoordinator' is found.
Conformance to the ObjectStore API
See also: ObjectStoreCoordinator.rootObjectStore()
"""
return self._parentObjectStore.rootObjectStore()
##
## Observing API
##
def objectWillChange(self, anObject):
"""
ObservingInterface implementation
Automatically invoked when an object is about to change its state.
Silently ignores changes if they occur on an inserted, deleted or already
updated objects.
Simply logs (at warning level) if the EditingContext receives a
notification of changes for an object which is not in its uniquing table
(hence, not in its graph of objects) -- __TBD this behaviour should
probably be changed, since it should never happen at all.
"""
gID=self.globalIDForObject(anObject)
#if not self._uniquingTable.hasObject(anObject):
if gID is None:
warn("Received 'objectWillChange()' for an object not registered")
return
if self.propagatesInsertionForRelatedObjects() \
and gID in self._insertedObjects:
# move the object from _insertedObjects back to _pendingInsertedObjects
# see comments in processRecentChanges() for a complete explanation
self._insertedObjects.remove(gID)
self._pendingInsertedObjects.append(gID)
if gID in \
self._insertedObjects + self._deletedObjects + \
self._pendingInsertedObjects + self._pendingDeletedObjects:
return
if gID not in self._pendingUpdatedObjects:
self._pendingUpdatedObjects.append(gID)
##
## ObjectStore API
##
def arrayFaultWithSourceGlobalID(self, aGlobalID, aRelationshipName,
anEditingContext):
"""
EditingContext implementation just forwards the message to its parent
object store and returns the result.
"""
return self.parentObjectStore().arrayFaultWithSourceGlobalID(aGlobalID, aRelationshipName, anEditingContext)
def isaChildOf(self, anEditingContext):
"""
Tells whether ``self`` is a child of ``anEditingContext``, either directly
(``anEditingContext`` is its parent) or indirectly (``anEditingContext``
is its grand-parent, or grand-grand-parent, etc.).
Note that ``self`` is not a child of ``self``.
"""
if anEditingContext==self:
return 0
try:
currentEC=self
while 1:
if currentEC.parentObjectStore()==anEditingContext:
return 1
currentEC=currentEC.parentObjectStore()
except AttributeError:
return 0
def faultForGlobalID(self, aGlobalID, anEditingContext=None):
"""
Searches for an object registered within the EditingContext with the
supplied GlobalID, and returns it if it is found. If not, forwards the
message to the parentObjectStore (usually an ObjectStoreCoordinator).
If anEditingContext is a child of self,
"""
if anEditingContext is None:
anEditingContext=self
if anEditingContext.isaChildOf(self):
# One of our child ECs forwarded the message
if aGlobalID.isTemporary():
# newly inserted object: we create a copy of it, record it in the
# child ec and finally we return it
object=self.objectForGlobalID(aGlobalID)
new_object=object.__class__()
propertiesKeys=new_object.attributesKeys()+\
new_object.toOneRelationshipKeys()+\
new_object.toManyRelationshipKeys()
new_object.prepareForInitializationWithKeys(propertiesKeys)
anEditingContext.recordObject(new_object, aGlobalID)
new_object.updateFromSnapshot(object.snapshot())
return new_object
else: # forward to the parentObjectStore
return self.parentObjectStore().faultForGlobalID(aGlobalID,
anEditingContext)
else:
obj=self.objectForGlobalID(aGlobalID)
if obj:
return obj
else:
return self.parentObjectStore().faultForGlobalID(aGlobalID,
anEditingContext)
def faultForRawRow(self, row, entityName, anEditingContext=None):
"""
Turns a row (dictionary) into a real object. Any row, such as the one
returned by a fetch when raw rows is actvated, can be turned into a
real object given that it contains the primary keys.
Parameters:
row -- a dictionary. This dictionary should have the entity's primary
keys in its keys (and their corresponding values)
entityName -- the name of the entity the row represents
anEditingContext -- (optional) The EditingContext in which the object
should be registered. Defaults to self if None or omitted.
"""
if anEditingContext is None:
anEditingContext=self
# no particular action for the child, faultForGlobalID() will handle
# everything automatically
return self.parentObjectStore().faultForRawRow(row, entityName,
anEditingContext)
def initializeObject(self, anObject, aGlobalID, anEditingContext):
"""
Forwards the message to the parentObjectStore(), with the following
exception: if 'anEditingContext' 's 'parentObjectStore()' is 'self', i.e.
it is a child of 'self', and if 'self' already has an real object (not a
fault) with that globalID, the values of this object is used to initialize
'anObject'.
Conformance to the ObjectStore API
Parameters:
anObject -- a CustomObject instance
aGlobalID -- the GlobalID of 'anObject'
anEditingContext -- the EditingContext which holds 'anObject'
"""
if anEditingContext.parentObjectStore()==self:
# do we have that one?
our_obj=self.objectForGlobalID(aGlobalID)
if our_obj and not our_obj.isFault():
# Override with our values
## snapshot() requires that all objects are inserted within ec
## (in fact, especially for toMany relationships)
## --> we choose here to propagate the insertion, if appropriate
if self.propagatesInsertionForRelatedObjects():
our_obj.propagateInsertionWithEditingContext(self)
ObserverCenter.ensureSubsequentChangeWillBeNotifiedForObject(our_obj)
snap=our_obj.snapshot()
db=self.rootObjectStore().objectStoreForObject(anObject).database()
db.incrementSnapshotCountForGlobalID(aGlobalID)
anObject.updateFromSnapshot(snap)
return
self.parentObjectStore().initializeObject(anObject, aGlobalID,
anEditingContext)
def objectsCountWithFetchSpecification(self, aFetchSpecification,
anEditingContext=None):
"""
Returns the approximate number of objects that would be returned by
objectsWithFetchSpecification() if called with the very same parameters.
About ``approximate'': the number returned is in fact the upper bound ; as
the objects are not actually fetched against the database, it is not
possible to determine whether some have already been deleted within
anEditingContext (in which case objectsWithFetchSpecification() would not
return them --see its documentation as well).
Forwards the message to the parentObjectStore(), usually an
ObjectStoreCoordinator.
Parameters:
aFetchSpecification -- a FetchSpecification object describing the
objects to be fetched
anEditingContext -- the EditingContext in which the objects would be
fetched. This is in fact an optional argument: since no objects are
fetched it is not really needed.
"""
if anEditingContext is None:
anEditingContext=self
return self.parentObjectStore().objectsCountWithFetchSpecification(aFetchSpecification, anEditingContext)
def fetchCount(self, entityName,
qualifier=None, # either a Qualifier instance or a string
#orderBy=None, # either a SortOrdering or a string
isDeep=0, # should subentities be fetched as well?
#refresh=0, # unsupported yet
#lock=0, # unsupported yet
#limit=None, # \
#page=None, # > slice parameters, not in core yet
#offset=None, # / (page/offset: mutually exclusive)
):
"""
Returns the approximate number of objects that would be returned by
fetch() if called with the very same parameters.
About ``approximate'': the number returned is in fact the upper bound ; as
the objects are not actually fetched against the database, it is not
possible to determine whether some have already been deleted within
anEditingContext (in which case objectsWithFetchSpecification() would not
return them --see its documentation as well).
Returns the objects identified by the following parameters:
entityName -- the corresponding entity's name (see also isDeep, below)
qualifier -- a string qualifying the objects to be fetched (a instance
of Qualifier can also be supplied)
isDeep -- shall the fetch be made against the entity and its
sub-entities?
"""
if type(qualifier)==type(''):
from Qualifier import qualifierWithQualifierFormat
qualifier=qualifierWithQualifierFormat(qualifier)
#if type(orderBy)==type(''):
# from SortOrdering import sortOrderingsWithString
# orderBy=sortOrderingsWithString(orderBy)
from Modeling.FetchSpecification import FetchSpecification
fs=FetchSpecification(entityName, qualifier=qualifier, deepFlag=isDeep)
#fs.setLocksObject(lock)
#fs.setRefreshesRefetchedObjects(refresh)
return self.objectsCountWithFetchSpecification(fs)
def objectsWithFetchSpecification(self, aFetchSpecification,
anEditingContext=None):
"""
Returns the objects matching the supplied FetchSpecification.
Note that returned objects are:
- objects that are already stored in the database. If some, if not all,
of these objects are already registered within the EditingContext,
they are returned as-is --meaning that, if they have been modified
after they have been fetched from the database, these modifications
are NOT overridden.
- new objects that are inserted in the EditingContext (see
insertObject()) but that are not saved yet.
Objects that are marked as deleted (cf. deleteObject()) in the
EditingContext are not returned.
When a nested EditingContext fetches objects, the result set will contain
the objects that are inserted in the parent (or the parent's parent,
etc.), and objects which are marked as deleted in parent will not be part
of the result set.
If parameter 'anEditingContext' is omitted or 'None', it defaults to
'self', then the message is forwarded down the hierarchy of ObjectStores,
i.e. to the 'parentObjectStore()'
Conformance to the ObjectStore API
See also: DatabaseContext.objectsWithFetchSpecifications()
"""
if anEditingContext is None:
anEditingContext=self
fs=aFetchSpecification
ec=anEditingContext
# +--> From this point we use 'ec' instead of 'anEditingContext'
objects=self.parentObjectStore().objectsWithFetchSpecification(fs, ec)
if ec.isaChildOf(self) and fs.fetchesRawRows():
# filter the returned raw rows: we'd better replace the modified objects
# with our own version
fs_entityName=fs.entityName()
copy_objects=list(objects)
for o in copy_objects:
self_obj=self.faultForRawRow(o, fs_entityName)
if not self_obj.isFault():
objects[objects.index(o)]=self_obj.snapshot_raw()
if ec is self or ec.isaChildOf(self):
entitiesNames=[fs.entityName()]
if fs.isDeep():
import ModelSet
defaultModelSet=ModelSet.defaultModelSet()
entity=defaultModelSet.entityNamed(fs.entityName())
entitiesNames+=entity.allSubEntitiesNames()
# do not include deleted objects in the returned set of objects
if not fs.fetchesRawRows():
#TBD Implementation note: the two blocks below (if isaChildOf/else)
#TBD are completely equivalent. Well, at least, the first one works
#TBD for both. Is it slower/better than
if ec.isaChildOf(self):
# If we're returning objects for a child's use, we remove the one
# that are marked for deletion in the parent (self) However, the
# deleted objects in self and the ones in the child-ec are distinct,
# but the GlobalIDs are the same and this is what we do here:
# compare the gIDs and remove the apropriate objects from the result
# set
ec_deletedGids=self._deletedObjects+self._pendingDeletedObjects
objects=[o for o in objects
if ec.globalIDForObject(o) not in ec_deletedGids]
else:
# We work for self, so we just remove the ones that are already
# marked as deleted
ec_deletedObjects = self.allDeletedObjects()
objects=[o for o in objects if o not in ec_deletedObjects]
else: # We're fetching raw rows. Wow, now we have to remove from the
# list the dictionaries corresponding to the deleted objects.
# Note: we use the exact same code when we're working for us (self)
# or for a child --this is basically manipulations of
# GlobalIDs
ec_deletedGids=self._deletedObjects+self._pendingDeletedObjects
ec_deleted_raw_pks=[gid.keyValues() for gid in ec_deletedGids]
#if ec_deletedGids: import pdb ; pdb.set_trace()
for deleted_raw_pk in ec_deleted_raw_pks:
for raw_obj in list(objects):
raw_obj_copy=raw_obj.copy()
raw_obj_copy.update(deleted_raw_pk)
if raw_obj_copy==raw_obj:
objects.remove(raw_obj)
# now append inserted objects
for entityName in entitiesNames:
ec_insertedObjects = self.allInsertedObjects()
objs=[o for o in ec_insertedObjects if o.entityName()==entityName]
if fs.qualifier():
objs=filteredArrayWithQualifier(objs,fs.qualifier())
# If we are returning objects for a child's use it is NOT POSSIBLE to
# return our (self's) objects, instead, we return a fault for the
# given object.
# Note that faultForGlobalID() is responsible for cloning inserted
# objects when they are requested by the nested EditingContext
if ec.isaChildOf(self):
fault_objs=[ec.faultForGlobalID(self.globalIDForObject(o), ec)
for o in objs]
objs=fault_objs
if fs.fetchesRawRows():
objs=[o.snapshot_raw() for o in objs]
objects.extend(objs)
return objects
def fetch(self, entityName,
qualifier=None, # either a Qualifier instance or a string
#orderBy=None, # either a SortOrdering or a string
isDeep=0, # should subentities be fetched as well?
#refresh=0, # unsupported yet
#lock=0, # unsupported yet
#limit=None, # \
#page=None, # > slice parameters, not in core yet
#offset=None, # / (page/offset: mutually exclusive)
rawRows=0 # should we return raw rows
):
"""
Returns the objects identified by the following parameters:
entityName -- the corresponding entity's name (see also isDeep, below)
qualifier -- a string qualifying the objects to be fetched (a instance
of Qualifier can also be supplied)
isDeep -- shall the fetch be made against the entity and its
sub-entities?
"""
if type(qualifier)==type(''):
from Qualifier import qualifierWithQualifierFormat
qualifier=qualifierWithQualifierFormat(qualifier)
#if type(orderBy)==type(''):
# from SortOrdering import sortOrderingsWithString
# orderBy=sortOrderingsWithString(orderBy)
from Modeling.FetchSpecification import FetchSpecification
fs=FetchSpecification(entityName, qualifier=qualifier, deepFlag=isDeep)
fs.setFetchesRawRows(rawRows)
#fs.setLocksObject(lock)
#fs.setRefreshesRefetchedObjects(refresh)
return self.objectsWithFetchSpecification(fs)
def ownsObject(self, anObject):
"""
Forwards the message to the parentObjectStore() ; it usually goes done the
ObjectStore hierarchy until an ObjectStoreCoordinator or a DatabaseContext
is hit and answers
"""
return self.parentObjectStore().ownsObject(anObject)
def saveChangesInEditingContext(self, anEditingContext):
"""
Called on the parent EditingContext when a child EditingContext saves
changes.
Exceptions that can be raised:
- ValueError if 'anEditingContext' is not a child of self
-
Note: this method also automatically locks 'self' before actually
saving the changes ; this makes it possible for two children of a
single EC to safely save their changes in two different threads
without having to worry about explicitly locking the parent EC.
See also: saveChanges(), processRecentChanges()
"""
self.lock()
try:
self._savesChangesInEditingContext(anEditingContext)
finally:
self.unlock()
def _savesChangesInEditingContext(self, anEditingContext):
"""
Private method called by saveChangesInEditingContext() after self has
been locked.
"""
if anEditingContext.parentObjectStore()!=self:
raise ValueError, "Parameter 'anEditingContext' should be a child of "\
"self"
childEC=anEditingContext
def ensureObjectsInRelationAreRegisteredInEC(object, ec):
# Hypothesis: anObject is NOT an newly inserted object
# and objects in relation are already registered
for key in object.toOneRelationshipKeys():
relObj=object.storedValueForKey(key)
if relObj:
relObjEC=relObj.editingContext()
if not relObjEC:
raise ObjectNotRegisteredError,\
"Object %s at %s references object %s at %s for key '%s' "\
"but the latter is not registered in any EditingContext"%\
(str(object), hex(id(object)),
str(relObj), hex(id(relObj)), key)
gID=relObjEC.globalIDForObject(relObj)
if not gID.isTemporary():
ec.faultForGlobalID(gID, ec)
for key in object.toManyRelationshipKeys():
relObjs=object.storedValueForKey(key)
for relObj in relObjs:
relObjEC=relObj.editingContext()
if not relObjEC:
raise ObjectNotRegisteredError,\
"Object %s at %s references object %s at %s for key '%s' "\
"but the latter is not registered in any EditingContext"%\
(str(object), hex(id(object)),
str(relObj), hex(id(relObj)), key)
gID=relObjEC.globalIDForObject(relObj)
if not gID.isTemporary():
ec.faultForGlobalID(gID, ec)
#end/def ensureObjectsInRelationAreRegisteredInEC()
# first, inserts objects that needs to be inserted
for insertedObject in anEditingContext.insertedObjects():
gID=childEC.globalIDForObject(insertedObject)
if not self.objectForGlobalID(gID):
new_inserted_object=insertedObject.__class__()
# Note: we could simply call self.insertObjectWithGlobalID() here
# BUT: we do not want awakeFromInsertion to be triggered
self.recordObject(new_inserted_object, gID)
self._pendingInsertedObjects.append(gID)
for updatedObject in anEditingContext.updatedObjects():
gID=childEC.globalIDForObject(updatedObject)
self.faultForGlobalID(gID, self)
# Then, update the objects that were already inserted within the parent
## TBD check that objects in relation still exists
for insertedObject in anEditingContext.insertedObjects():
gID=childEC.globalIDForObject(insertedObject)
self.objectForGlobalID(gID).updateFromSnapshot(insertedObject.snapshot())
ensureObjectsInRelationAreRegisteredInEC(new_inserted_object, self)
# Updated objects
for updatedObject in anEditingContext.updatedObjects():
gID=childEC.globalIDForObject(updatedObject)
self_obj=self.objectForGlobalID(gID)
if not self_obj:
self_obj=self.faultForGlobalID(gID, self)
if self_obj in self._deletedObjects+self._pendingDeletedObjects:
raise ParentChildECInconsistencyError, "Object %s at %s is marked as "\
"updated in the EditingContext %s but it was deleted in its "\
"parent EditingContext %s in the meantime"%\
(updatedObject, hex(id(updatedObject)), str(childEC), str(self))
ensureObjectsInRelationAreRegisteredInEC(updatedObject, self)
self_obj.willChange()
self_obj.updateFromSnapshot(updatedObject.snapshot())
# DeletedObject
for deletedObject in anEditingContext.deletedObjects():
gID=childEC.globalIDForObject(deletedObject)
self_obj=self.objectForGlobalID(gID)
if not self_obj:
self_obj=self.faultForGlobalID(gID, self)
self.deleteObject(self_obj)
# now we own the snapshot count: deleteObject has triggered the fault,
# hence the refCount was updated --> nothing more to do.
##
## Notifications
##
def handleNotification(self, notification):
"""
Handles notification
"""
notification_name=notification.name()
if notification_name == ObjectsChangedInStoreNotification:
raise 'Unimplemented', \
'EditingContext got ObjectsChangedInStoreNotification'
# UPDATED: refault objects for these GlobalIDs
# Others??
elif notification_name == InvalidatedAllObjectsInStoreNotification:
raise 'Unimplemented', 'EditingContext got InvalidatedAllObj.InStore'
elif notification_name == GlobalIDChangedNotification:
gid=notification.object()
if gid.isTemporary():
obj=self.objectForGlobalID(gid)
if obj:
key_gid=notification.userInfo().get(gid)
self.forgetObject(obj)
self.recordObject(obj, key_gid)
db=self.rootObjectStore().objectStoreForObject(obj).database()
db.incrementSnapshotCountForGlobalID(key_gid)
# Time to update the object if a PK is set as class properties
# Note: One might say this could have been done earlier, e.g. in
# DatabaseContext.prepareForSaveWithCoordinator() where PK values
# are requested and assigned to the inserted objects
# THIS IS INTENTIONAL: until finalizeCommitChanges() is called by
# the ObjectStoreCoordinator, we can't be sure that no exception
# will be raised in the process of saving changes. If an exception
# is actually raised, the transaction is rolled back and the object
# is not saved and as a consequence should NOT appear as if it
# received a valid PK value.
# Now, since DBContext.finalizeCommitChanges() does notify the EC
# that inserted objects did receive a valid GlobalID (thus a valid
# PK value) after all changes were supplied and committed to the
# database, we know it is safe here to assign the inserted & saved
# object its PK value --if the PK is a class property, obviously!
from ModelSet import defaultModelSet
entity=defaultModelSet().entityNamed(obj.entityName())
pk_values=key_gid.keyValues()
for pk in entity.primaryKeyAttributes():
if pk.isClassProperty():
obj.takeStoredValueForKey(pk_values[pk.name()], pk.name())
else:
obj=self.objectForGlobalID(gid)
new_gid=notification.userInfo().get(gid)
trace('Got GlobalIDChangedNotification w/ gid %s mapped to %s'%(gid, new_gid))
if obj and not obj.isFault():
warn('Got GlobalIDChangedNotification w/ a non temporary GlobalID '\
'but the corresponding object is not a fault')
return
if obj and new_gid:
self.__resolvedFaultsGIDs[gid] = new_gid
self.forgetObject(obj)
self.recordObject(obj, new_gid)
NC.removeObserver(self, GlobalIDChangedNotification, gid)
else:
info('EditingContext got an unhandled notification: %s'%notification_name, ERROR)
##
## Undo
##
def setUndoManager(self, anUndoManager):
"""
Unimplemented yet
See also: undoManager()
"""
def redo(self):
"""
Unimplemented yet
Raises if no UndoManager is assigned to the EditingContext
See also: redo(), setUndoManager()
"""
def undo(self):
"""
Unimplemented yet
Raises if no UndoManager is assigned to the EditingContext
See also: redo(), setUndoManager()
"""
def undoManager(self):
"""
Unimplemented yet: returns 'None'
"""
return None
## Instance's lock
def lock(self):
"""
Acquire the reentrant-lock for the EditingContext. Calls to 'lock()' should
be balanced with the same number of 'unlock()' for the lock to be released.
Use this if you need to use the EditingContext in a multi-threaded
environment.
See also: unlock()
"""
self.__instanceLock.acquire()
def unlock(self):
"""
Releases the lock for the EditingContext.
See also: lock()
"""
self.__instanceLock.release()
##
## Special methods
##
def __unimplemented__(self):
"Raises 'Unimplemented'"
raise 'Unimplemented'
|