PCART  v1.4
Automated Repair of Python API Parameter Compatibility Issues
changeAnalyze.py
Go to the documentation of this file.
1 
10 
11 
12 
13 import os
14 import re
15 import copy
16 import subprocess
17 from API.LibApi import Parameter
18 from Tool.tool import getParameter,removeParameter,getFileName,resolvePythonExecutable
19 
20 
21 
22 
27 class Update():
28 
29 
31  def __init__(self):
32 
33 
35  self.pospos=-1
36 
37 
39  self.typetype=''
40 
41 
43  self.renamerename=''
44 
45 
47  self.reprep=''
48 
49 
55  self.valuevalue=''
56 
57 
59  self.deledele=''
60 
61 
63  self.addPosaddPos=''
64 
65 
67  self.addKeyaddKey=''
68 
69 
71  self.pos2keypos2key=''
72 
73 
75  self.key2poskey2pos=''
76 
77 
78 
79 
81  def __repr__(self):
82  s=''
83  if self.pospos:
84  s+=f"posChange:{self.pos}, "
85  if self.typetype:
86  s+=f"typeChange:{self.type}, "
87  if self.deledele:
88  s+=f"delete:{self.dele}, "
89  if self.add:
90  s+=f"add:{self.add}, "
91  s=s.rstrip(', ')
92  return s
93 
94 
95 
96 
101 def updateErrorLst(errorLog,errorLst):
102  with open(errorLog, 'a', encoding='utf-8') as fw:
103  for it in errorLst:
104  fw.write(it)
105  fw.write('\n')
106 
107 
108 
109 
114 def querySharedDict(callAPI,sharedDict):
115  ansDict={}
116  k=callAPI
117  if k in sharedDict:
118  ansDict['current']=sharedDict[k]['current']
119  ansDict['target']=sharedDict[k]['target']
120  return ansDict
121  return ansDict
122 
123 
124 
125 
132 def updateSharedDict(callAPI,currentDict,targetDict,sharedDict):
133  key=callAPI
134  if key not in sharedDict: #没有就添加
135  sharedDict[key]={}
136  innerDict=sharedDict[key]
137  innerDict['current']=currentDict
138  innerDict['target']=targetDict
139  sharedDict[key]=innerDict #将修改操作更新到共享字典中
140  else: #存在则再看是否需要修改
141  innerDict=sharedDict[key]
142  if sharedDict[key]['current']['matchMethod']=='static' and currentDict['matchMethod']=='dynamic':
143  innerDict['current']=currentDict
144  if sharedDict[key]['target']['matchMethod']=='static' and targetDict['matchMethod']=='dynamic':
145  innerDict['target']=targetDict
146  sharedDict[key]=innerDict #将修改操作更新到共享字典中
147 
148 
149 
150 
162 def isDifferType(oldType,newType):
163  #先从字面值上判断看是否一样
164  if oldType==newType:
165  return False
166  #若至少存在一个类型注释为空,则认为二者的类型是相同的
167  if oldType=="" or newType=="":
168  return False
169  #若都存在类型注释且注释不同时,才认为二者的类型不同
170  else:
171  oldLst=[]
172  newLst=[]
173  oldTypeSet=set() #将类型构造成集合进行比较
174  newTypeSet=set()
175  if oldType[0]=="'" and oldType[-1]=="'":
176  oldType=oldType[1:-1]
177 
178  if newType[0]=="'" and newType[-1]=="'":
179  newType=newType[1:-1]
180 
181  if 'Union' in oldType:
182  pattern=r'.*?Union\[(.*)\].*?'
183  result=re.findall(pattern,oldType)
184  oldLst=getParameter(result[0])
185  elif 'Optional' in oldType:
186  pattern=r'.*?Optional\[(.*)\].*?'
187  result=re.findall(pattern,oldType)
188  oldLst=getParameter(result[0])
189  elif '|' in oldType:
190  oldLst=oldType.split('|')
191  else: #当oldType就是一个具体的类型而不是集合时,比如int
192  oldLst=[oldType]
193 
194  for it in oldLst:
195  oldTypeSet.add(it.replace(' ',''))
196 
197 
198 
199  if 'Union' in newType:
200  pattern=r'.*?Union\[(.*)\].*?'
201  result=re.findall(pattern,newType)
202  newLst=getParameter(result[0])
203  elif 'Optional' in newType:
204  pattern=r'.*?Optional\[(.*)\].*?'
205  result=re.findall(pattern,newType)
206  newLst=getParameter(result[0])
207  elif '|' in newType:
208  newLst=newType.split('|')
209  else:
210  newLst=[newType]
211 
212  for it in newLst:
213  newTypeSet.add(it.replace(' ',''))
214 
215  #这里oldTypeSet>0是因为避免空集是任意集合的子集的情况
216  if len(oldTypeSet)>0 and oldTypeSet.issubset(newTypeSet):
217  return False
218  else:
219  return newType
220 
221 
222 
223 
228 def para2Obj(paraStr):
229  paraStr=paraStr.replace(' ','') #去空格
230  paraObjLst=[] #保存参数对象
231  if '->' in paraStr: #若有有返回值的话去掉返回值
232  paraStr=paraStr.split('->')[0]
233  if '(' in paraStr[0]:
234  paraStr=paraStr[1:-1]
235 
236  if paraStr:
237  lst=getParameter(paraStr)
238  else:
239  lst=[]
240  lst=[para for para in lst if para != '/'] # 2026/4/23 当前修复模型只区分args/kwargs,忽略仅位置参数分隔符
241  if len(lst)>0:
242  if 'self' in lst[0]: #self可能也存在类型注释
243  lst.remove(lst[0])
244  elif 'cls' in lst[0]:
245  lst.remove(lst[0])
246  for para in lst:
247  parameter=Parameter()
248  parameter.position=lst.index(para) #当列表中有相同元素时,lst.index会出现问题,但库定义中不会出现相同的参数
249  parameter.fullItem=para
250  flagMaohao=0
251  if ':' in para:
252  pos=para.find(':')
253  flagMaohao=1
254 
255  if flagMaohao and "'" not in para[0:pos] and '"' not in para[0:pos] and '<' not in para[0:pos]: #参数值为字符串时,字符串中也可能出现冒号
256  l=para.split(':')
257  parameter.name=l[0]
258  if '=' in l[1]:
259  ll=l[1].split('=')
260  parameter.type=ll[0]
261  parameter.value=ll[1]
262  else:
263  parameter.type=l[1]
264  elif '=' in para:
265  l=para.split('=')
266  parameter.name=l[0]
267  parameter.value=l[1]
268  else:
269  parameter.name=para
270  paraObjLst.append(parameter)
271 
272  pos=len(paraObjLst)
273  posStar=-1 #记录*的位置
274  pos2Star=-1 #记录**的位置, 防止出现(x, y, **kwargs)的形式
275  for para in paraObjLst:
276  if '**' in para.name:
277  pos2Star=para.position
278  elif '*' in para.name:
279  posStar=para.position
280  break
281  if posStar!=-1: #优先根据*号拆分
282  pos=posStar
283  elif pos2Star!=-1:
284  pos=pos2Star
285 
286  posParameters=paraObjLst[0:pos]
287  keyParameters=[]
288  for para in paraObjLst[pos+1:]:
289  if '**' not in para.name:
290  keyParameters.append(para)
291 
292  return posParameters,keyParameters
293 
294 
295 
296 
302 def findDiffer(oldPara,newPara):
303  oldPos,oldKey=para2Obj(oldPara)
304  newPos,newKey=para2Obj(newPara)
305  oldPosParaNum=len(oldPos)
306  dic={} #保存每个参数应该做哪些修改操作
307  #第一轮筛选,先根据名字来找对应关系
308  #处理位置参数
309  for oldPara in copy.deepcopy(oldPos):
310  sameFlag=0
311  for newPara in newPos:
312  if oldPara.name==newPara.name:
313  up=Update()
314  if oldPara.position!=newPara.position:
315  up.pos=newPara.position
316  ty=isDifferType(oldPara.type,newPara.type)
317  if ty:
318  up.type=ty
319  sameFlag=1
320  oldPos.remove(oldPara)
321  newPos.remove(newPara)
322  break
323  if sameFlag==1:
324  dic[(oldPara.name,oldPara.position)]=up
325 
326  #处理关键字参数,旧版中没有找到相同名字的参数可能是重命名或删除了
327  for oldPara in copy.deepcopy(oldKey):
328  sameFlag=0
329  for newPara in newKey:
330  if oldPara.name==newPara.name:
331  up=Update()
332  ty=isDifferType(oldPara.type,newPara.type)
333  if ty:
334  up.type=ty
335  sameFlag=1
336  oldKey.remove(oldPara)
337  newKey.remove(newPara)
338  break
339  if sameFlag==1:
340  dic[(oldPara.name,oldPara.position)]=up
341 
342 
343 
344  #第二轮筛选,根据名字找对应关系,判断是否存在位置参数变到了关键字参数
345  for oldPara in copy.deepcopy(oldPos):
346  sameFlag=0
347  for newPara in newKey:
348  if oldPara.name==newPara.name:
349  up=Update()
350  up.pos2key=oldPara.name #位置参数变成关键字参数
351  ty=isDifferType(oldPara.type,newPara.type)
352  if ty:
353  up.type=ty
354  sameFlag=1
355  oldPos.remove(oldPara)
356  newKey.remove(newPara)
357  break
358  if sameFlag==1:
359  dic[(oldPara.name,oldPara.position)]=up
360 
361 
362  #再判断是否有关键字参数(起始版本)变到了位置参数(目标版本)
363  for oldPara in copy.deepcopy(oldKey):
364  sameFlag=0
365  for newPara in copy.deepcopy(newPos):
366  if oldPara.name==newPara.name:
367  print(f"key2pos-->{oldPara.name}")
368  up=Update()
369  up.key2pos=oldPara.name
370  ty=isDifferType(oldPara.type, newPara.type)
371  if ty:
372  up.type=ty
373  sameFlag=1
374  oldKey.remove(oldPara)
375  newPos.remove(newPara)
376  break
377  if sameFlag==1:
378  dic[(oldPara.name,oldPara.position)]=up
379 
380 
381 
382 
383  #第三轮筛选,根据对应位置和类型来筛选剩余的位置参数,判断其是否发生了重命名
384  for oldPara in copy.deepcopy(oldPos):
385  sameFlag=0
386  for newPara in newPos:
387  if oldPara.position==newPara.position and not isDifferType(oldPara.type,newPara.type): #这里需要加一个类型相同约束吗
388  up=Update()
389  if oldPara.name!=newPara.name: #参数发生了重命名
390  up.rename=newPara.name
391  ty=isDifferType(oldPara.type,newPara.type)
392  if ty:
393  up.type=ty
394  sameFlag=1
395  oldPos.remove(oldPara)
396  newPos.remove(newPara)
397  break
398  if sameFlag==1:
399  dic[(oldPara.name,oldPara.position)]=up
400  else:#oldPos中剩下的就是删除的
401  oldPos.remove(oldPara)
402  up=Update()
403  up.dele=1
404  dic[(oldPara.name,oldPara.position)]=up
405 
406  #第四轮筛选,根据类型来判断,剩余的关键字参数是否发生了重命名
407  for oldPara in copy.deepcopy(oldKey):
408  sameFlag=0
409  for newPara in newKey:
410  if oldPara.type==newPara.type:
411  sameFlag=1
412  up=Update()
413  up.rename=newPara.name
414  oldKey.remove(oldPara)
415  newKey.remove(newPara)
416  break
417  if sameFlag==1:
418  dic[(oldPara.name,oldPara.position)]=up
419  else:
420  oldKey.remove(oldPara)
421  up=Update()
422  up.dele=1
423  dic[(oldPara.name,oldPara.position)]=up
424 
425 
426  #newPos中剩下的就是替换或新增的
427  for para in newPos:
428  s=f'{para.name}'
429  if para.value:
430  s+=f"={para.value}"
431  if para.position>=0 and para.position<oldPosParaNum: #如果剩余参数的下标在旧版本参数下标的范围内,则认为是替换操作,反之则认为是新增的
432  for key in dic:
433  if key[1]==para.position:
434  dic[key].rep=s
435  break
436  else:
437  up=Update()
438  up.addPos=s
439  dic[(para.name,para.position)]=up
440 
441  #newKey中剩下的就是新增的,新增的关键字参数往往都带有默认值,一般不会引起兼容性问题
442  for para in newKey:
443  s=para.name
444  if para.value:
445  s+=f"={para.value}"
446  up=Update()
447  up.addKey=s
448  dic[(para.name,para.position)]=up
449 
450 
451  #构建修改操作的字典
452  updateDict={}
453  for key,value in dic.items():
454  if key not in updateDict:
455  updateDict[key]={}
456  if value.dele:
457  updateDict[key]['delete']=value.dele
458  if value.type:
459  updateDict[key]['typeChange']=value.type
460  if value.rename:
461  updateDict[key]['rename']=value.rename
462  if value.pos!=-1:
463  updateDict[key]['posChange']=value.pos
464  if value.rep:
465  updateDict[key]['replace']=value.rep
466  if value.pos2key:
467  updateDict[key]['pos2key']=value.pos2key
468  if value.addPos:
469  updateDict[key]['addPos']=value.addPos
470  if value.addKey:
471  updateDict[key]['addKey']=value.addKey
472 
473  if value.key2pos:
474  updateDict[key]['key2pos']=value.key2pos
475 
476  #对字典按照参数的位置进行排序
477  ansDict=dict(sorted(updateDict.items(), key=lambda it: it[0][1])) #it[0]代表字典的键,it[0][1]代表键中的第二个元素,即位置
478  return ansDict
479 
480 
481 
482 
493 def analyzeCompatibility(oldPara,newPara):
494  #将其转化为参数对象
495  oldPos,oldKey=para2Obj(oldPara)
496  newPos,newKey=para2Obj(newPara)
497  #分析位置参数
498  for oldPara in oldPos:
499  flag=0
500  for newPara in newPos:
501  if oldPara.name==newPara.name and oldPara.position==newPara.position and not isDifferType(oldPara.type,newPara.type):
502  flag=1
503  break
504  if flag==0: #若旧版本的位置参数没有在新版本中找到对应的
505  return False
506 
507  #再分析新增的位置参数
508  for newPara in newPos:
509  if newPara.position>=len(oldPos):
510  if newPara.value=='': #若新增的位置参数不带默认值
511  return False
512 
513  #分析关键字参数
514  for oldPara in oldKey:
515  flag=0
516  for newPara in newKey:
517  if oldPara.name==newPara.name and not isDifferType(oldPara.type,newPara.type):
518  flag=1
519  break
520 
521  #再判断关键字参数是否变成了位置参数(这种改变是兼容的)
522  if flag==0:
523  for newPara in newPos:
524  if oldPara.name==newPara.name and not isDifferType(oldPara.type,newPara.type):
525  flag=1
526  print(f"key2pos-->{oldPara.name}")
527  break
528 
529  if flag==0:
530  return False
531 
532  #再分析新增的关键字参数
533  #比如(*,x,y) --> (*,x,y,z),其中z就是新增的
534  for newPara in newKey:
535  flag=0
536  for oldPara in oldKey:
537  if newPara.name==oldPara.name: #若找到同名的,则不是新增的
538  flag=1
539  break
540  if flag==0:
541  if newPara.value=='': #若新增的关键字参数不带默认值
542  return False
543 
544  return True
545 
546 
547 
548 
556 def isCompatible(current,target):
557  if len(current['match'])==0 or len(target['match'])==0:
558  return None
559  # if "*args" in target['match'] or "**kwargs" in target['match'] or "**" in target['match']: #默认此时是兼容的
560  # return []
561  #先将数据结构统一化,便于之后的统一操作
562  if isinstance(current['match'],str): #只要是str,一定是动态匹配
563  currentDict={current['internalPath']:[current['match']]}
564  else:
565  currentDict=current['match']
566 
567  if isinstance(target['match'],str):
568  targetDict={target['internalPath']:[target['match']]}
569  else:
570  targetDict=target['match']
571 
572  tempLst1=[] #保存找到了对应关系的配对的修复字典
573  tempLst2=[] #保存未确定对应关系的配对的修复字典
574  flag=0
575  for currentSameAPI,currentOvLst in currentDict.items():
576  for targetSameAPI,targetOvLst in targetDict.items():
577  if currentSameAPI==targetSameAPI or (currentSameAPI+'.' in targetSameAPI) or (targetSameAPI+'.' in currentSameAPI) or len(currentDict)==1 and len(targetDict)==1:
578  flag=1
579  #第一轮遍历,筛选掉两个同名API确定对应关系的重载,这里对应关系的规则是字符串完全一致
580  #只要能找到对应关系,就说明是兼容的
581  #新版中完全对应的可以删除,但兼容的不能删除,防止它还可以兼容旧版本的其它重载
582  for currentOvPara in copy.deepcopy(currentOvLst):
583  for targetOvPara in copy.deepcopy(targetOvLst):
584  if currentOvPara.split('->')[0]==targetOvPara.split('->')[0]:
585  currentOvLst.remove(currentOvPara)
586  targetOvLst.remove(targetOvPara)
587 
588  #第二轮遍历
589  #只要旧版本的API在新版本中找到一个和它兼容的,那就认为旧版本的API在新版本中是兼容的,无需再进行其它的判断。
590  #只有旧版本的API在新版本中都不兼容时,才认为它不兼容,不兼容就要依次尝试修复。
591  for currentOvPara in currentOvLst:
592  compatibleFlag=0
593  for targetOvPara in targetOvLst:
594  if analyzeCompatibility(currentOvPara,targetOvPara):
595  compatibleFlag=1
596  break
597 
598  if compatibleFlag==0: #如果都不兼容,则需要尝试修复
599  for targetOvPara in targetOvLst: #这里
600  repairDict=findDiffer(currentOvPara,targetOvPara)
601  tempLst1.append((repairDict,targetOvPara))
602 
603  else:
604  for currentOvPara in currentOvLst:
605  compatibleFlag=0
606  for targetOvPara in targetOvLst:
607  if analyzeCompatibility(currentOvPara,targetOvPara):
608  compatibleFlag=1
609  break
610 
611  if compatibleFlag==0: #如果都不兼容,则需要尝试修复
612  for targetOvPara in targetOvLst: #这里
613  repairDict=findDiffer(currentOvPara,targetOvPara)
614  tempLst2.append((repairDict,targetOvPara))
615 
616  if flag==1:
617  return tempLst1
618  else:
619  return tempLst2
620 
621 
622 
623 
636 def addValueForAPI(callAPI,projName,runPath,runCommand,currentEnv,targetEnv,errLst,callKey,*,runtimePaths):
637  copyRoot=runtimePaths['copy_root']
638  dynamicRoot=runtimePaths['dynamic_root']
639  pklKey = callKey
640  pklName=getFileName(pklKey,'.pkl')
641  flag=0
642  # 参数补值沿用动态匹配的候选顺序,并优先使用目标环境重新生成的pkl
643  pklCandidates=[
644  ('new_'+pklName[:-4]+'__object.pkl',1),
645  ('new_'+pklName[:-4]+'__expr.pkl',1),
646  ('new_'+pklName,1),
647  (pklName[:-4]+'__object.pkl',0),
648  (pklName[:-4]+'__expr.pkl',0),
649  (pklName,0),
650  ]
651  pklPath=''
652  for candidateName,candidateFlag in pklCandidates:
653  candidatePath=os.path.join(copyRoot,'pkl',candidateName)
654  if os.path.exists(candidatePath):
655  flag=candidateFlag
656  pklPath=candidatePath
657  break
658  if not pklPath:
659  return ''
660 
661  #当runPath不在runCommand中时,需要切换到运行文件所在的目录执行命令
662  #而文件操作的相对路径就是相对于命令执行的路径
663  # pythonPath=f"{virtualEnv}/bin/python"
664  if flag==0:
665  pythonPath = resolvePythonExecutable(currentEnv)
666  else:
667  pythonPath = resolvePythonExecutable(targetEnv)
668 
669  if runPath and runPath not in runCommand:
670  cwd = os.path.join(dynamicRoot, projName, runPath)
671  script = 'addValueForAPI.py'
672  elif runPath:
673  cwd = os.path.join(dynamicRoot, projName)
674  script = os.path.join(runPath, 'addValueForAPI.py')
675  else:
676  cwd = os.path.join(dynamicRoot, projName)
677  script = 'addValueForAPI.py'
678  matchResult = subprocess.run(
679  [pythonPath, script, pklPath, callAPI, pklKey],
680  cwd=cwd, capture_output=True, text=True, encoding='utf-8'
681  )
682  if matchResult.returncode!=0:
683  errLst.append(f"{callAPI}, addValueError: {matchResult.stderr}\n")
684  return ''
685 
686  output=matchResult.stdout
687  pattern="##(.*)##"
688  obj=re.compile(pattern,re.DOTALL)
689  lst=obj.findall(output)
690  if len(lst)==1:
691  return lst[0]
692 
693  return ''
Update class for parameter change analysis 参数变更分析类
pos2key
Conversion of positional parameter to keyword parameter 位置参数变更为关键字参数
rename
New name of the parameter 参数重命名
dele
Flag (1) of parameter deletion 参数删除标记 (1)
addPos
New positional parameter 新增位置参数
key2pos
Conversion of keyword parameter to positional parameter 关键字参数变更为位置参数
addKey
New keyword parameter 新增关键字参数
type
New type of the parameter 参数新类型
def __repr__(self)
Return the string representation of the parameter update 返回参数变更的字符串表示形式
rep
Replacement of the parameter 参数替换
pos
New position of the parameter 参数新位置
def __init__(self)
The constructor 构造函数
value
New default value of the parameter 参数新默认值
def analyzeCompatibility(oldPara, newPara)
Analyze the compatibility of two APIs 判断两个重载API是否兼容
def isCompatible(current, target)
Determine the compatibility of APIs from current and target versions 判断起始版本和目标版本API的兼容性
def updateErrorLst(errorLog, errorLst)
Save error messages 保存错误信息
def para2Obj(paraStr)
Convert parameter string to positional and keyword parameter objects 将参数字符串转换为位置和关键字参数对象
def isDifferType(oldType, newType)
Determine parameter type change: compatible type means no type change 判断两个参数的类型是否发生了变更: 只要兼容,就认为相同
def updateSharedDict(callAPI, currentDict, targetDict, sharedDict)
Update API mapping dictionary: add and revise 更新API映射字典: 添加和修改
def findDiffer(oldPara, newPara)
Determine difference of two parameters 输入两个api参数部分,判断参数部分有何不同
def querySharedDict(callAPI, sharedDict)
Query API mapping dictionary 查询API映射字典
def addValueForAPI(callAPI, projName, runPath, runCommand, currentEnv, targetEnv, errLst, callKey, *runtimePaths)
Add values stored by pkl file for API parameters 为API参数添加保存至pkl文件中的值
def getFileName(fileName, extension)
Normalize file name 给文件取名字
Definition: tool.py:479
def getParameter(p_string, separator=',', space=1)
Split parameter string into list of separated parameters 将参数字符串拆分成单个的参数
Definition: tool.py:85
def resolvePythonExecutable(envPath)
Resolve Python executable from a virtual environment root 从虚拟环境根目录解析 Python 解释器路径
Definition: tool.py:856