[Modeling-cvs] SF.net SVN: modeling: [988] trunk/ProjectModeling
Status: Abandoned
Brought to you by:
sbigaret
From: <sbi...@us...> - 2006-04-09 13:53:33
|
Revision: 988 Author: sbigaret Date: 2006-04-09 06:53:19 -0700 (Sun, 09 Apr 2006) ViewCVS: http://svn.sourceforge.net/modeling/?rev=988&view=rev Log Message: ----------- Fixed bug #621210: defining __cmp__ leads to infinite loop Modified Paths: -------------- trunk/ProjectModeling/CHANGES trunk/ProjectModeling/Modeling/DatabaseContext.py trunk/ProjectModeling/Modeling/RelationshipManipulation.py trunk/ProjectModeling/Modeling/tests/testPackages/AuthorBooks/Book.py trunk/ProjectModeling/Modeling/tests/testPackages/AuthorBooks/Writer.py trunk/ProjectModeling/Modeling/tests/test_EditingContext_Global.py Modified: trunk/ProjectModeling/CHANGES =================================================================== --- trunk/ProjectModeling/CHANGES 2006-02-26 19:37:08 UTC (rev 987) +++ trunk/ProjectModeling/CHANGES 2006-04-09 13:53:19 UTC (rev 988) @@ -7,6 +7,12 @@ ** Distributed under a 3-clause BSD-style license, see LICENSE for details ** ----------------------------------------------------------------------------- + * Fixed bug #621210: when __cmp__ (and/or, possibly, other special methods + such as __eq__ or __ne__) was defined in a CustomObject, it was possible + to enter a infinite loop. The fix consisted in making sure that the + framework does not trigger those special methods, since in all cases what + we really want is comparing class instances by object identity. + 0.9 (2006/02/26) ---------------- Modified: trunk/ProjectModeling/Modeling/DatabaseContext.py =================================================================== --- trunk/ProjectModeling/Modeling/DatabaseContext.py 2006-02-26 19:37:08 UTC (rev 987) +++ trunk/ProjectModeling/Modeling/DatabaseContext.py 2006-04-09 13:53:19 UTC (rev 988) @@ -1588,7 +1588,7 @@ aGlobalID -- KeyGlobalID (non temporary) """ - if anEditingContext.objectForGlobalID(aGlobalID)!=aDatabaseObject: + if id(anEditingContext.objectForGlobalID(aGlobalID))!=id(aDatabaseObject): raise ValueError, 'aDatabaseObject %s is not registered within the '\ 'EditingContext %s for globalID: %s'%(repr(aDatabaseObject), repr(anEditingContext), Modified: trunk/ProjectModeling/Modeling/RelationshipManipulation.py =================================================================== --- trunk/ProjectModeling/Modeling/RelationshipManipulation.py 2006-02-26 19:37:08 UTC (rev 987) +++ trunk/ProjectModeling/Modeling/RelationshipManipulation.py 2006-04-09 13:53:19 UTC (rev 988) @@ -77,7 +77,7 @@ # toOne relationship: is it already set? _backRelObj=anObject.valueForKey(_backRelKey) #if _backRelObj and _backRelObj.persistentID()!=self.persistentID(): - if _backRelObj and _backRelObj!=self: + if _backRelObj and id(_backRelObj)!=id(self): # Yes: now remove it _removeObjectFromBothSidesOfRelationshipWithKey(anObject, _backRelObj, _backRelKey, otoOnes, otoManys) _addObjectToPropertyWithKey(anObject, self, _backRelKey, otoOnes, otoManys) @@ -164,7 +164,7 @@ _removeObjectFromPropertyWithKey(self,anObject, aKey, toOnes, toManys) else: # toOne: check this is the one! - if self.valueForKey(aKey) != anObject: + if id(self.valueForKey(aKey)) != id(anObject): raise ValueError, 'anObject %s is not set for key %s'%(repr(anObject), aKey) _addObjectToPropertyWithKey(self,None, aKey, toOnes,toManys) @@ -205,7 +205,7 @@ if aKey in toOnes: # toOne: Simply uses KVC - if self.valueForKey(aKey)!=anObject: + if id(self.valueForKey(aKey)) != id(anObject): raise ValueError, 'anObject %s is not set for key %s'%(repr(anObject), aKey) self.takeValueForKey(None, aKey) Modified: trunk/ProjectModeling/Modeling/tests/testPackages/AuthorBooks/Book.py =================================================================== --- trunk/ProjectModeling/Modeling/tests/testPackages/AuthorBooks/Book.py 2006-02-26 19:37:08 UTC (rev 987) +++ trunk/ProjectModeling/Modeling/tests/testPackages/AuthorBooks/Book.py 2006-04-09 13:53:19 UTC (rev 988) @@ -94,4 +94,26 @@ self.willChange() self._author=object + + def check_cmp_allowed(self): + # see comments in class Writer, same method + import AuthorBooks + from inspect import stack + if not AuthorBooks.allow_cmp and stack()[3][0].f_code.co_name not in ('__observersForObject', '__setObjectObservers', 'notifyObserversObjectWillChange','removeObjectFromAlreadyNotifiedObjects'): + raise 'Invalid call of %s in %s'%(stack()[1][0].f_code.co_name, + repr(self)) + + def __cmp__(self, o): + self.check_cmp_allowed() + return cmp( id(self), id(o) ) + + def __eq__(self, o): + self.check_cmp_allowed() + return id(self) == id(o) + def __ne__(self, o): + self.check_cmp_allowed() + return id(self) != id(o) + + def __hash__(self): + return hash(id(self)) Modified: trunk/ProjectModeling/Modeling/tests/testPackages/AuthorBooks/Writer.py =================================================================== --- trunk/ProjectModeling/Modeling/tests/testPackages/AuthorBooks/Writer.py 2006-02-26 19:37:08 UTC (rev 987) +++ trunk/ProjectModeling/Modeling/tests/testPackages/AuthorBooks/Writer.py 2006-04-09 13:53:19 UTC (rev 988) @@ -13,6 +13,7 @@ """ Writers are objects ... """ + allow_cmp=1 __implements__ = CustomObject.__implements__ @@ -137,18 +138,26 @@ def addToBooks(self,object): "Add the books relationship to many" + import AuthorBooks + _saved_value = AuthorBooks.allow_cmp + AuthorBooks.allow_cmp = 1 if object not in self._books: self.willChange() _books=list(self._books) _books.append(object) self._books=tuple(_books) + AuthorBooks.allow_cmp = _saved_value def removeFromBooks(self,object): "Remove the books relationship to many" self.willChange() _books=list(self._books) + import AuthorBooks + _saved_value = AuthorBooks.allow_cmp + AuthorBooks.allow_cmp = 1 _books.remove(object) self._books=tuple(_books) + AuthorBooks.allow_cmp = _saved_value @@ -164,3 +173,32 @@ def __str__(self): return '('+repr(self)+')'+' '+str(self._firstName)+' '+str(self._lastName) + + def check_cmp_allowed(self): + # part of the tests for bug #621210 + import AuthorBooks + from inspect import stack + + # excluded funcs are part of the Notification framework. It uses + # WeakDict. whose method get calls __cmp__ + # technically, these means that at some points, the framework does + # actually call __cmp__. for the moment we leave it as-is, since it + # is not triggering the infinite loop bug. + if not AuthorBooks.allow_cmp and stack()[3][0].f_code.co_name not in ('__observersForObject', '__setObjectObservers', 'notifyObserversObjectWillChange','removeObjectFromAlreadyNotifiedObjects'): + raise 'Invalid call of %s in %s'%(stack()[1][0].f_code.co_name, + repr(self)) + + def __cmp__(self, o): + self.check_cmp_allowed() + return cmp( id(self), id(o) ) + + def __eq__(self, o): + self.check_cmp_allowed() + return id(self) == id(o) + + def __ne__(self, o): + self.check_cmp_allowed() + return id(self) != id(o) + + def __hash__(self): + return hash(id(self)) Modified: trunk/ProjectModeling/Modeling/tests/test_EditingContext_Global.py =================================================================== --- trunk/ProjectModeling/Modeling/tests/test_EditingContext_Global.py 2006-02-26 19:37:08 UTC (rev 987) +++ trunk/ProjectModeling/Modeling/tests/test_EditingContext_Global.py 2006-04-09 13:53:19 UTC (rev 988) @@ -41,7 +41,6 @@ from AuthorBooks.Writer import Writer from AuthorBooks.Book import Book - from Modeling.EditingContext import EditingContext from Modeling.FetchSpecification import FetchSpecification from Modeling.Qualifier import qualifierWithQualifierFormat @@ -50,6 +49,18 @@ from Modeling import Adaptor from Modeling import Database +# The following is part of the global test for bug #621210 +# Note that the tests only works when using the testPackages/AuthorBooks +# modules, not the ones that are generated on the fly +# (see ref. to utils.dynamically_build_test_packages, above) +# The principle of the test is simple: the framework should not call any +# of the special funcs __cmp__ , __eq__, __ne__, etc. +import AuthorBooks +AuthorBooks.allow_cmp = 0 +def allow_cmp(): + AuthorBooks.allow_cmp = 1 +def forbid_cmp(): + AuthorBooks.allow_cmp = 0 class Writer_test07(Writer): refusesValidateForDelete=0 @@ -76,7 +87,6 @@ class TestEditingContext_Global(unittest.TestCase): "Global tests for EditingContext" - def test_00_insertObject_loads_databaseContexts(self): "[EditingContext] ..." # drop any existing default ObjectStoreCoordinator @@ -148,7 +158,9 @@ fetchSpec=FetchSpecification(entityName='Writer') objects=ec.objectsWithFetchSpecification(fetchSpec) + allow_cmp() self.failUnless(w in objects) + allow_cmp() def test_01c_objectsWithFetchSpecification_closes_adaptorChannel(self): "[EditingContext] objectsWithFetchSpecification closes adaptorChannel" @@ -170,10 +182,15 @@ objects_1=ec.objectsWithFetchSpecification(fetchSpec) objects_2=ec.fetch('Writer', 'lastName like "?a*"') self.failIf(len(objects_1)!=len(objects_2)) + allow_cmp() self.failIf([o for o in objects_1 if o not in objects_2]) + forbid_cmp() + objects_3=ec.fetch('Writer', qualifier) self.failIf(len(objects_1)!=len(objects_3)) + allow_cmp() self.failIf([o for o in objects_1 if o not in objects_3]) + forbid_cmp() def test_02_toOneFaultTrigger(self): "[EditingContext] toOneFaultTrigger" @@ -191,11 +208,12 @@ self.failUnless(dard_pygmalion.isFault()) dard_pygmalion.getLastName() #trigger self.failIf(dard_pygmalion.isFault()) + allow_cmp() self.failUnless(dard._pygmalion==dard_pygmalion) - # Last, check that the original object containing the toMany fault was not # marked as changed self.failIf(dard in ec.updatedObjects()) + forbid_cmp() self.failIf(ec.hasChanges()) def test_02b_toOneFaultOrNone(self): @@ -249,8 +267,10 @@ ec.processRecentChanges() self.failIf(ec.hasUnprocessedChanges(), 'no unprocessed changes anymore') self.failUnless(ec.hasChanges(), 'has changes') + allow_cmp() self.assertEqual(ec.insertedObjects(), [w]) - + forbid_cmp() + # new EC ec=EditingContext() fetchSpec=FetchSpecification(entityName='Writer') @@ -263,7 +283,9 @@ ec.processRecentChanges() self.failIf(ec.hasUnprocessedChanges(), 'no unprocessed changes anymore') self.failUnless(ec.hasChanges(), 'has changes') + allow_cmp() self.assertEqual(ec.updatedObjects(), [ws[0]]) + forbid_cmp() # new EC ec=EditingContext() @@ -299,11 +321,14 @@ dard_books=dard.getBooks() ldb=len(dard_books) # trigger dard_books=dard.getBooks() + allow_cmp() self.failIf(dard_books[0] in ec.deletedObjects()) + forbid_cmp() ec.processRecentChanges() self.failUnless(len(ec.deletedObjects())==ldb+1) # Check that the author was also deleted [cascade] + allow_cmp() self.failUnless(dard in ec.deletedObjects()) # AND that neither the author nor the book are marked as updated # [Bug #599602] @@ -312,6 +337,7 @@ for book in dard_books: self.failUnless(book in ec.deletedObjects()) + forbid_cmp() def test_06_deleteRule_nullify(self): "[EditingContext] processRecentChanges: deleteRule: NULLIFY" @@ -354,7 +380,9 @@ # Now the dard.getBooks() should have been refreshed and the book # that was deleted should be removed from dard's books. self.failIf(len(dard.getBooks())!=2) + allow_cmp() self.failIf(dard_book1 in dard.getBooks()) + forbid_cmp() def test_07_savechanges_01(self): @@ -447,8 +475,9 @@ ## return objects marked for deletion fetchSpec=FetchSpecification(entityName='Writer') writers=ec.objectsWithFetchSpecification(fetchSpec) + allow_cmp() self.failIf(hugo in writers) - + forbid_cmp() ## 4rd test: now deleting it for real should be OK hugo.refusesValidateForDelete=0 try: @@ -457,7 +486,9 @@ self.fail("Error: deletion failed") ## 5th test: check that the object has been removed from the uniquing table + allow_cmp() self.failIf(hugo in ec.registeredObjects()) + forbid_cmp() def test_09_savechanges_03(self): "[EditingContext] saveChanges part 03: change + validation" @@ -815,8 +846,10 @@ cleese.willChange() ec.setPropagatesInsertionForRelatedObjects(1) ec.processRecentChanges() + allow_cmp() self.failUnless(book_cleese in ec.insertedObjects()) self.failUnless(pygmalion_cleese in ec.insertedObjects()) + forbid_cmp() # Last: check that subsequent changes are notified after # ec.processRecentChanges() @@ -833,8 +866,10 @@ cleese.setPygmalion(pygmalion_cleese) cleese.addToBooks(book_cleese) ec.processRecentChanges() + allow_cmp() self.failUnless(book_cleese in ec.insertedObjects()) self.failUnless(pygmalion_cleese in ec.insertedObjects()) + forbid_cmp() def test_15_propagatesInsertionForRelatedObjects_02(self): "[EditingContext] propagatesInsertionForRelatedObjects (inserted objects)" @@ -849,13 +884,17 @@ hugo.addObjectToBothSidesOfRelationshipWithKey(b1, 'books') ec.processRecentChanges() + allow_cmp() self.failUnless(b1 in ec.insertedObjects()) + forbid_cmp() #Check that subsequent changes are notified after ec.processRecentChanges() b2=Book() ; b2.setTitle("Les travailleurs de la mer") hugo.addObjectToBothSidesOfRelationshipWithKey(b2, 'books') ec.processRecentChanges() + allow_cmp() self.failUnless(b2 in ec.insertedObjects()) + forbid_cmp() def test_16_toManySnapshotsUpdated(self): "[EditingContext] checks that toManySnapshots are correctly updated" @@ -1072,7 +1111,9 @@ self.assertEqual(adaptorChannel._count_for_execute, fetchCount+1) # fault was cleared for book in w.getBooks(): + allow_cmp() self.failIf(book not in tomany_fault) + forbid_cmp() def test_21_snapshot_raw(self): "[EditingContext] CustomObject.snapshot_raw()" @@ -1139,23 +1180,36 @@ ec=EditingContext() rabelais=ec.fetch('Writer', 'lastName == "Rabelais"')[0] ec.deleteObject(rabelais) + allow_cmp() self.failUnless(rabelais in ec.allDeletedObjects()) self.failIf(rabelais in ec.deletedObjects()) # unprocessed + forbid_cmp() ec.insertObject(rabelais) # cancel the deletion self.failIf(len(ec.fetch('Writer', 'lastName == "Rabelais"'))!=1) + allow_cmp() self.failIf(rabelais in ec.allDeletedObjects()) self.failIf(rabelais in ec.allInsertedObjects()) - + forbid_cmp() + ## check: delete / processRecentChanges / insert ec=EditingContext() rabelais=ec.fetch('Writer', 'lastName == "Rabelais"')[0] ec.deleteObject(rabelais) + allow_cmp() self.failUnless(rabelais in ec.allDeletedObjects()) + forbid_cmp() + ec.processRecentChanges() + + allow_cmp() self.failUnless(rabelais in ec.deletedObjects()) + forbid_cmp() + ec.insertObject(rabelais) # cancel the deletion + allow_cmp() self.failIf(rabelais in ec.allDeletedObjects()) self.failIf(rabelais in ec.allInsertedObjects()) + forbid_cmp() ## check: delete / saveChanges / insert ec=EditingContext() @@ -1167,12 +1221,16 @@ new=ec.fetch('Writer', 'lastName == "test_22"')[0] new_gid=new.globalID() ec.deleteObject(new) + allow_cmp() self.failUnless(new in ec.allDeletedObjects()) self.failIf(new in ec.deletedObjects()) + forbid_cmp() ec.saveChanges() ec.insertObject(new) # cancel the deletion? No, insertion of a new object + allow_cmp() self.failIf(new in ec.allDeletedObjects()) self.failUnless(new in ec.allInsertedObjects()) + forbid_cmp() self.failIf(new.globalID()==new_gid) self.failIf(not new.globalID().isTemporary()) @@ -1186,18 +1244,32 @@ new=ec.fetch('Writer', 'lastName == "test_22"')[0] new_gid=new.globalID() new.setLastName('test_22 alternate value') + allow_cmp() self.failUnless(new in ec.allUpdatedObjects()) + forbid_cmp() + ec.deleteObject(new) + + allow_cmp() self.failUnless(new in ec.allDeletedObjects()) self.failIf(new in ec.deletedObjects()) self.failIf(new in ec.allUpdatedObjects()) + forbid_cmp() + ec.processRecentChanges() + + allow_cmp() self.failUnless(new in ec.deletedObjects()) self.failIf(new in ec.allUpdatedObjects()) + forbid_cmp() + ec.insertObject(new) # cancel the deletion + + allow_cmp() self.failIf(new in ec.allDeletedObjects()) self.failUnless(new in ec.allUpdatedObjects()) self.failUnless(new.globalID()==new_gid) + forbid_cmp() ## check: modify / delete / saveChanges / insert @@ -1262,8 +1334,10 @@ # explicitely look for duplicates for w in ws: _ws=list(ws) #[:-1]) + allow_cmp() _ws.remove(w) self.failIf(w in _ws, 'Duplicate found: %s'%w) + forbid_cmp() def test_25_fetchCount_is_not_affected_by_duplicates(self): "[EditingContext] fetchCount is not affected by duplicates" @@ -1320,6 +1394,47 @@ self.failUnless(len(objects)==1) self.failUnless('Dard' in objects_names) + def test_29_bug621210(self): + "[EditingContext] Infinite loop when defining __cmp__" + # Note: bug #621210 is in fact tested not only w/ this test, but also + # globally. See notes at the beginning of this file, and changes + # introduced in revision 988. + ec=EditingContext() + from new import instancemethod + def invalid_call_fct(msg): + def invalid_call(self, o, msg): + raise 'Invalid call of %s in %s'%(msg,self) + return lambda s,o,fct=invalid_call,msg=msg: fct(s,o,msg) + Writer.__cmp__ = instancemethod(invalid_call_fct('__cmp__'), None, Writer) + Writer.__eq__ = instancemethod(invalid_call_fct('__eq__'), None, Writer) + Writer.__ne__ = instancemethod(invalid_call_fct('__ne__'), None, Writer) + + gargantua=ec.fetch('Book', 'title == "Gargantua"')[0] + rabelais = gargantua.getAuthor() + self.failUnless(rabelais.isFault()) + rabelais.getLastName() + + def _test_29_fetch_backslash(self): + "[EditingContext] fetch real backslash" + # bug # + ec=EditingContext() + b1=Book(); b1.setTitle(r'abc\n') + b2=Book(); b2.setTitle('abc\\\\') + b3=Book(); b3.setTitle('\\') + ec.insert(b1); ec.insert(b2); ec.insert(b3) + ec.saveChanges() + + res=ec.fetch('Book', 'title == "abc\\n"') + self.assertEqual(len(res), 1) + self.assertEqual(res[0].getTitle(), r"abc\n") + res=ec.fetch('Book', 'title == "\\"') + self.assertEqual(len(res), 1) + self.assertEqual(res[0].getTitle(), "\\") + #res=ec.fetch('Book', 'title like "*abc%d*"') + #self.assertEqual(len(res), 1) + #self.assertEqual(res[0].getTitle(), "abc%d") + # TBD: with LIKE and ILIKE + def test_999_customSQLQuery(self): "[EditingContext] custom SQL Query" fs=FetchSpecification(entityName='Writer') This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |