Source code for ccpnmodel.ccpncore.lib.MergeObjects

"""MOdule for generic merging of data model objects
Transfers simple and link attributes from the source object to target object
Does not transfer derived, automatic or immutable attributes
Links will be transferred where possible
Where necessary the Api is bypassed

Logical analysis and design by R.H. Fogh

Coding and testing by T.J. Stevens

Definitions:
Objects targetObj and sourceObj of class O
link O.a (a) to class A, with backlink A.o ( o)

A note on checks:
Where the API is bypassed, the function does validity checks at each step,
and rolls back the last step if the checks fail.
The checks are done on sourceObj, targetObj, objects on the other end of
links, and the parents of the latter. The check on parents is done because
this includes a check on the keys of the children - the merge cannot change
the keys of either source or target, but can change the key of linked-to objects.
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2021"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
               "Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See http://www.ccpn.ac.uk/v3-software/downloads/license")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
                 "CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
                 "J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Ed Brooksbank $"
__dateModified__ = "$dateModified: 2021-09-06 17:58:19 +0100 (Mon, September 06, 2021) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-04-07 10:28:48 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================



from ccpnmodel.ccpncore.memops.metamodel import Constants as metaConstants
from ccpnmodel.ccpncore.memops.ApiError import ApiError
from ccpnmodel.ccpncore.memops.metamodel import Util as metaUtil

[docs]def mergeObjects(sourceObj,targetObj, _useV3Delete=False, _mergeFunc=None): """Merges sourceObj into targetObj, deleting sourceObj. Attributes and links from sourceObj are added to targetObj provided 1) that they are not there already, and 2) that there is room. WARNING this function bypasses the API. WARNING Merging objects with child links or frozen links is NOT undoable and undo stack is cleared WARNING, integrated update operations, e.g. Chemical Shift averaging and notifiers are NOT reliably performed during merging and must be handled by the callign function WARNING This function just might leave the data in an illegal state The function performs a number of checks for each individual change. If a check fails, the latest change is undone before the error exit, in an attempt to leave the data in a state that is legal. Note that only the latest change is undone - in case of error the data state will not be brought back to the state from before the execution of the command. Note that sourceObj is likely to be in an illegal state during execution, so that an error may well leave sourceObj in an illegal state. If this happens, deleting sourceObj may bring the data back to a legal state, and is unlikely to cause further problems. In spite of the checks, some objects (not limited to sourceObj and targetObj) may be left in an illegal state, even if no error is raised. It is recommended to use this function with caution, and to run checkAllValid after it has been used. """ #same class check if sourceObj.qualifiedName != targetObj.qualifiedName: return #ATTRIBUTES: #Objects targetObj, sourceObj, with attribute a objClass = targetObj.metaclass for a in objClass.getAllAttributes(): attrName = a.name if a.isDerived or a.isAutomatic or a.changeability == metaConstants.frozen: continue elif a.hicard == a.locard: continue elif a.hicard == 1: if targetObj.__dict__[attrName] is None: setattr(targetObj,attrName,sourceObj.__dict__[attrName]) else: # find add operation addfunc = getattr(targetObj, 'add' + metaUtil.upperFirst(a.baseName)) # Thisd is OK, as we iterate over list2 but modify list1 attrList1 = targetObj.__dict__[attrName] attrList2 = sourceObj.__dict__[attrName] if a.isUnique: # no duplicates = might be list or set if a.hicard > 1: nSpaces = max(0, a.hicard - len(attrList1)) else: nSpaces = -1 for aVal in attrList2: if nSpaces == 0: break else: nSpaces -= 1 addfunc(aVal) else: # might have duplicates (and must be an internal list) for aVal in attrList2: if len(attrList1) >= a.hicard and a.hicard != metaConstants.infinity: break # keep adding while there is room if attrList1.count(aVal) < attrList2.count(aVal): addfunc(aVal) #LINKS: niceLinks = [] nastyLinks = [] childLinks = [] for a in objClass.getAllRoles(): # select links and how to treat them if a.hicard == a.locard or a.isDerived or a.isAutomatic: continue if a.changeability == metaConstants.frozen: continue # This is probably right; it could be changed if we bypassed the API. o = a.otherRole if a.hierarchy == metaConstants.child_hierarchy: childLinks.append(a) elif o is None or o.changeability != metaConstants.frozen: # links that can be handled without bypassing API niceLinks.append(a) else: # links that require bypassing API nastyLinks.append(a) for a in niceLinks: # links that can be handled without bypassing API linkName = a.name o = a.otherRole if o is not None: backName = o.name #print linkName, a.locard, a.hicard, o.locard, o.hicard if o is None or o.hicard != o.locard: # #print "C3", linkName # # NB this does NOT break API # # We are setting/adding/removing from the .a side. # if o is None there will be no problems. # If o.hicard == 1, attrObj.o can be overwritten # Otherwise, as o.hicard != o.locard it will always be possible either to # remove sourceObj from attrObj or to add targetObj to attrObj # regardless of the exact cardinalities and of len(attrObj.o) if a.hicard == 1: #assert a.locard == 0 # # there will be no problems on the source/target side as we only # make changes when the link is unset in the target # Whatever the number of objects, you will either be able to # remove sourceObj from attrObj or to add targetObj. # if getattr(targetObj,linkName) is None: attrObj = getattr(sourceObj,linkName) try: setattr(sourceObj,linkName,None) except: pass setattr(targetObj,linkName,attrObj) else: # There will be no problems on the attrObj side (see above). # On the source/target side we will get the desired result as # the API simply passes if you try to add an existing object, # It also deletes the old link to sourceObj where appropriate # find add operation ss = metaUtil.upperFirst(a.baseName) addfunc = getattr(targetObj, 'add' + ss) removefunc = getattr(sourceObj, 'remove' + ss) # NB we cannot use teh raw list as we modify it during the loop for attrObj in getattr(sourceObj,linkName): try: removefunc(attrObj) except ApiError: pass #print 'Failed to remove %s for %s' % (linkName,sourceObj.className) try: # Adds objects to targetObj.a as long as there is room # (a.hicard could be e.g. 2) addfunc(attrObj) except ApiError: pass #print 'Failed to add %s for %s' % (linkName,targetObj.className) break elif o.hicard == 1 and o.locard == 1: if a.hicard == 1: #assert a.locard == 0 oldVal = getattr(targetObj,linkName) if oldVal is None: newVal = getattr(sourceObj,linkName) setattr(newVal, backName, targetObj) else: # assert a.hicard != a.locard # asser a.hicard != 1 for attrObj in getattr(sourceObj,linkName): try: setattr(attrObj, backName, targetObj) except ApiError: pass else: # #print "C4", linkNam # # NB this does NOT break API # # we know that o.hicard == o.locard > 1 # The trick is that since o.hicard == o.locard > 1 and a.changeability != frozen, # it must be possible to set attrObj.o to an appropriate tuple without # getting into trouble. if a.hicard == 1: #assert a.locard == 0 attrObj = getattr(sourceObj,linkName) linkList = list(getattr(attrObj,backName)) linkList[linkList.index(sourceObj)] = targetObj setattr(attrObj,backName,linkList) else: # assert a.hicard != a.locard # asser a.hicard != 1 for attrObj in getattr(sourceObj,linkName): linkList = list(getattr(attrObj,backName)) linkList[linkList.index(sourceObj)] = targetObj setattr(attrObj, backName, linkList) # make sure we are valid before going into the tough part targetObj.checkValid() undo = sourceObj.root._undo if nastyLinks or childLinks: if undo is not None: # This is not undoable - at least it would be immense work to make it so undo.increaseBlocking() try: if nastyLinks: root = targetObj.root try: root.override = True for a in nastyLinks: # links that can *NOT* be handled without bypassing API linkName = a.name o = a.otherRole backName = o.name keyNames = o.container.keyNames attrObjClass = a.valueType downlink = attrObjClass.parentRole.otherRole.name #print linkName, a.locard, a.hicard, o.locard, o.hicard if a.hicard == 1: #print "C1", linkName if getattr(targetObj,linkName) is None: #do childDict = {} oldKey = None newKey = None attrObj = getattr(sourceObj,linkName) if backName in keyNames: oldKey = attrObj.getLocalKey() setattr(sourceObj, linkName, None) setattr(targetObj, linkName, attrObj) if backName in keyNames: newKey = attrObj.getLocalKey() # this changes key for attrObj - fix it. childDict = attrObj.parent.__dict__[downlink] if newKey in childDict: # key already taken - undo setattr(targetObj, linkName, None) setattr(sourceObj, linkName, attrObj) raise ApiError("Merge failure: %s key %s already in use" % (attrObj.qualifiedName(), newKey)) else: del childDict[oldKey] childDict[newKey] = attrObj # test try: attrObj.checkValid() targetObj.checkValid() # undo except: setattr(targetObj, linkName, None) setattr(sourceObj, linkName, attrObj) if backName in keyNames: del childDict[newKey] childDict[oldKey] = attrObj print ("Merge failure: %s, %s result is not valid" % (targetObj, attrObj)) raise else: # # assert a.hicard != 1 # # NB if a.locard > 0 the code below could create an illegal # sourceObj. Which would not be a problem if all went well, # but would render the final state illegal if the merge ran into an # error somewhere else later # We ignore this as links that are locard>0 in one direction and # frozen in the other direction would make both objects impossible # to create except under override conditions. The problem is *very* # unlikely ever to arise. # #print "C2", linkName # set up keepList = list(getattr(targetObj, linkName)) ll = list(getattr(sourceObj, linkName)) if a.hicard == metaConstants.infinity: moveList = ll ignoreList = [] else: nSpaces = a.hicard - len(keepList) if nSpaces > 0: moveList = ll[:nSpaces] ignoreList = ll[nSpaces:] else: continue # do oldKeys = [] newKeys = [] if backName in keyNames: oldKeys = [x.getLocalKey() for x in moveList] setattr(sourceObj, linkName, ignoreList) setattr(targetObj, linkName, keepList + moveList) if backName in keyNames: newKeys = [] for ii, attrObj in enumerate(moveList): childDict = attrObj.parent.__dict__[downlink] newKey = attrObj.getLocalKey() if newKey in childDict: # key already taken - undo setattr(targetObj, linkName, None) setattr(sourceObj, linkName, attrObj) for jj, nk in enumerate(newKeys): ao = moveList[jj] cd = ao.parent.__dict__[downlink] cd[oldKeys[jj]] = ao del cd[nk] raise ApiError("Merge failure: %s key %s already in use" % (attrObj.qualifiedName(), newKey)) else: newKeys.append(newKey) # del childDict[oldKey] # TJS edit: to be checked del childDict[oldKeys[ii]] childDict[newKey] = attrObj # test try: targetObj.checkValid() for attrObj in moveList: attrObj.checkValid() # undo except: setattr(targetObj, linkName, keepList) setattr(sourceObj, linkName, moveList + ignoreList) if backName in keyNames: for jj, nk in enumerate(newKeys): ao = moveList[jj] cd = ao.parent.__dict__[downlink] cd[oldKeys[jj]] = ao del cd[nk] raise finally: root.override = False if childLinks: # now move children. This is a full bypass, no overrides for a in childLinks: parentName = a.otherRole.name sourceDd = sourceObj.__dict__[a.name] targetDd = targetObj.__dict__[a.name] topObj = targetObj.topObject if a.hicard == 1: # single kid (rare case) targetObj.__dict__[a.name] = oo = sourceObj.__dict__[a.name] sourceObj.__dict__[a.name] = None oo.__dict__[parentName] = targetObj oo.__dict__['topObject'] = topObj elif a.valueType.keyNames == ['serial']: # multiple kid with serial key nextSerial = targetObj.__dict__['_serialDict'][a.name] + 1 for junk, oo in sorted(sourceDd.items()): targetDd[nextSerial] = oo del sourceDd[nextSerial] oo.__dict__[parentName] = targetObj oo.__dict__['topObject'] = topObj targetObj.__dict__['_serialDict'][a.name] = nextSerial else: # multiple kid with normal key for localKey,oo in sorted(sourceDd.items()): if localKey in targetDd: # key is taken - skip object continue else: targetDd[localKey] = oo del sourceDd[localKey] oo.__dict__[parentName] = targetObj oo.__dict__['topObject'] = topObj targetObj.checkValid() if _useV3Delete: if _mergeFunc: _mergeFunc(sourceObj, targetObj) _deleteFromV3(sourceObj) _notifyChangeV3(targetObj) else: sourceObj.delete() finally: if undo is not None: undo.clear() else: targetObj.checkValid() if _useV3Delete: if _mergeFunc: _mergeFunc(sourceObj, targetObj) _deleteFromV3(sourceObj) _notifyChangeV3(targetObj) else: sourceObj.delete() return targetObj
def _deleteFromV3(obj): """Method to delete an object from the v3 identifier Required because v3 notifiers are missed otherwise """ from ccpn.framework.Application import getApplication getApp = getApplication() if getApp: project = getApp.project if project and obj in project._data2Obj: v3obj = project._data2Obj[obj] v3obj.delete() return raise RuntimeError('trying to delete object {}'.format(obj)) def _notifyChangeV3(obj): """Method to notify an object from the v3 identifier Required because v3 notifiers are missed otherwise """ from ccpn.framework.Application import getApplication getApp = getApplication() if getApp: project = getApp.project if project and obj in project._data2Obj: v3obj = project._data2Obj[obj] v3obj._finaliseAction('change') return raise RuntimeError('trying to notify object {}'.format(obj)) def _mergeResonances(sourceObj, targetObj): from ccpn.framework.Application import getApplication getApp = getApplication() if getApp: project = getApp.project if project and sourceObj in project._data2Obj and targetObj in project._data2Obj: # v3 update of chemicalShift ids for shift in project._data2Obj[targetObj].chemShifts: shift._refreshPid() return raise RuntimeError('trying to merge objects {} -> {}'.format(sourceObj, targetObj))