PCART  v1.4
Automated Repair of Python API Parameter Compatibility Issues
repair.py
Go to the documentation of this file.
1 
13 
14 
15 
16 import os
17 import ast
18 import subprocess
19 from Tool.tool import getAst,getFileName,getParameter,getLastAPIParameter,resolvePythonExecutable
20 from API.LibApi import Parameter
21 from Change.changeAnalyze import para2Obj
22 
23 
24 
25 
34 def mapPos(pos,dic):
35  for key in dic:
36  try:
37  if key[1]==pos:
38  return dic[key]
39  except:
40  print(f"mapPos error, repairDict:{dic}")
41  return {}
42 
43 
44 
45 
54 def mapName(name,dic):
55  for key in dic:
56  if key[0]==name:
57  return dic[key]
58  return {}
59 
60 
61 
62 
68 def findName(pos,dic):
69  for key in dic.keys():
70  if key[1]==pos:
71  return key[0] #返回参数名
72  print(f"findName error:{pos}")
73  return ''
74 
75 
76 
77 
83 def mirrorAPI(fixedAPI,dic):
84  paraStr=getLastAPIParameter(fixedAPI)
85  paraObjLst=[] #保存参数对象
86  paraStr=paraStr.replace(' ','') #去空格
87  if paraStr:
88  lst=getParameter(paraStr,space=0)
89  else:
90  lst=[]
91  for i in range(len(lst)):
92  para=lst[i]
93  nameFlag=0
94  parameter=Parameter()
95  parameter.position=i #修复后参数的真实位置
96  if '=' in para:
97  pos=para.find('=')
98  if '(' not in para[0:pos] and "'" not in para[0:pos] and '"' not in para[0:pos]: #等号前面也不能出现引号,比如f('x=2',y=1)
99  nameFlag=1
100  parameter.name=para[0:pos]
101  else:
102  nameFlag=0
103  else:
104  nameFlag=0
105 
106  if not nameFlag: #到修复字典中找到修复后的参数名
107  flag=0
108  for k,subDict in dic.items():
109  if 'posChange' in subDict:
110  if subDict['posChange']==parameter.position: #(a,b)-->(b,a)
111  parameter.name=k[0]
112  flag=1
113  break
114  if flag==0:
115  for k,subDict in dic.items():
116  if parameter.position==k[1]:
117  if 'rename' in subDict:
118  parameter.name=subDict['rename']
119  elif 'replace' in subDict:
120  pos=subDict['replace'].find('=')
121  parameter.name=subDict['replace'][0:pos]
122  else:
123  parameter.name=k[0]
124 
125 
126  if nameFlag:
127  paraObjLst.append((parameter,1)) #1表示使用时带了参数名
128  else:
129  paraObjLst.append((parameter,0)) #0表示使用时没有带参数名
130  return paraObjLst
131 
132 
133 
134 
142 def fix(callAPI,repairDict,node,starFlag,twoStarFlag):
143  for n in ast.iter_child_nodes(node):
144  fix(callAPI,repairDict,n,starFlag,twoStarFlag)
145 
146  if isinstance(node,ast.Call):
147  #因为ast在对代码还原时会自动把原本的双引号解析为单引号,所以这里修正一下
148  callState=ast.unparse(node).replace(' ','').replace('"','').replace("'",'') #确保函数名和参数都要一致
149  if callState==callAPI.replace(' ','').replace('"','').replace("'",''):
150  posLst=node.args
151  keyLst=node.keywords
152  newPosLst=[]
153  newKeyLst=[]
154  index=0
155  insertParas=[] #记录待插入的参数,(insertPos, moveFlag, para)
156  #step1: 先对位置参数进行处理
157  for para in posLst:
158  opDict=mapPos(index,repairDict)
159  for op,v in opDict.items():
160  s=ast.unparse(para)
161  if op=='delete':
162  if starFlag:
163  if len(opDict)==1:
164  insertParas.append((index,-1,para))
165  break
166 
167  if op=='typeChange': #若有类型变化的话就无法修复
168  if len(opDict)==1: #typeChange的同时,可能还有其它变更,避免参数重复加入
169  insertParas.append((index,-1,para))
170  break
171 
172  if op=='rename': #当位置参数不带名字使用发生了rename,则无需改动
173  insertParas.append((index,-1,para))
174  break
175 
176  if op=='pos2key':
177  paraNode = ast.keyword(arg=v,value=ast.Name(id=s))
178  newKeyLst.append(paraNode)
179  break
180 
181  if op=='replace':
182  insertParas.append((index,-1,ast.Name(id=v.split('=')[-1])))
183 
184  if op=='posChange':
185  insertParas.append((v,index,para))
186 
187  if len(opDict)==0:
188  insertParas.append((index,-1,para))
189  index+=1
190  #最后还要判断是否有新增的位置参数,新增的位置参数也需要具体的值,新增带默认值的位置参数可以不填
191  # for key,value in repairDict.items():
192  # if 'addPos' in value:
193  # insertParas.append((key[1],ast.Name(id=value['addPos'].split('=')[-1])))
194  insertParas.sort(key=lambda it:it[0]) #按照插入的位置进行从小到大排序
195  for pos,moveFlag,para in insertParas:
196  if pos<=len(newPosLst):
197  newPosLst.insert(pos,para)
198  else:
199  if moveFlag==-1:
200  paraName=findName(pos,repairDict) #即使一个参数移到了其它位置,也要用它原本位置的名字
201  else:
202  paraName=findName(moveFlag,repairDict)
203  s=ast.unparse(para)
204  if paraName:
205  paraNode=ast.keyword(arg=paraName,value=ast.Name(id=s))
206  newKeyLst.append(paraNode)
207 
208  #保留API调用中存在的单独self或者cls参数
209  try:
210  if 'self' == posLst[0].id or 'cls' == posLst[0].id:
211  newPosLst.insert(0,posLst[0])
212  except Exception as e:
213  pass
214  node.args=newPosLst
215 
216  #再对带参数名使用的参数进行处理(位置参数或关键字参数)
217  for para in keyLst:
218  k=para.arg
219  opDict=mapName(k,repairDict)
220  for op,v in opDict.items():
221 
222  if op=='delete':
223  if twoStarFlag:
224  if len(opDict)==1:
225  newKeyLst.append(para)
226  break
227 
228  if op=='typeChange':
229  if len(opDict)==1:#typeChange的同时,还可能有其他变更(posChange),避免重复加入
230  newKeyLst.append(para)
231  break
232 
233  if op=='posChange': #若带参数名使用的参数位置发生改变,则无需修改,直接添加到newKeyLst中即可
234  newKeyLst.append(para)
235 
236  # if op=='replace':
237  # paraName=v.split('=',1)[0]
238  # if '=' in v:
239  # paraNode=ast.keyword(arg=paraName,value=ast.Name(id=v.split('=',1)[-1]))
240  # else:
241  # paraVal=ast.unparse(para).split('=',1)[-1]
242  # paraNode=ast.keyword(arg=paraName,value=ast.Name(id=paraVal))
243  # newKeyLst.append(paraNode)
244 
245  if op=='rename':
246  if not twoStarFlag:
247  para.arg=v #para.arg存放的就是参数名
248  newKeyLst.append(para)
249 
250 
251  if op=='pos2key' or op=='key2pos':
252  newKeyLst.append(para)
253 
254 
255  if len(opDict)==0:
256  newKeyLst.append(para)
257 
258  #最后再判断新增关键字,但新增的关键字参数一般不会导致兼容性问题,所以暂不添加
259  # for key,value in repairDict.items():
260  # if 'addKey' in value:
261  # newKeyLst.append(ast.keyword(arg=k,value=ast.Name(id=val)))
262  node.keywords=newKeyLst #更新修改后的关键字参数
263 
264 
265 
266 
278 def validateByRun(callAPI,apiWithValue,projName,virtualEnv,runPath,runCommand,callKey,*,runtimePaths):
279  if apiWithValue=='':
280  return None
281  copyRoot=runtimePaths['copy_root']
282  dynamicRoot=runtimePaths['dynamic_root']
283  pklName=getFileName(callKey,'.pkl')
284  # 验证阶段复用候选pkl顺序,保证修复验证和动态匹配读取的是同一类接收者
285  pklCandidates=[
286  'new_'+pklName[:-4]+'__object.pkl',
287  'new_'+pklName[:-4]+'__expr.pkl',
288  'new_'+pklName,
289  pklName[:-4]+'__object.pkl',
290  pklName[:-4]+'__expr.pkl',
291  pklName,
292  ]
293  pklPath=''
294  for candidateName in pklCandidates:
295  candidatePath=os.path.join(copyRoot,'pkl',candidateName)
296  if os.path.exists(candidatePath):
297  pklPath=candidatePath
298  break
299  if not pklPath:
300  return None
301 
302  #当runPath不在runCommand中时,需要切换到运行文件所在的目录执行命令
303  #而文件操作的相对路径就是相对于命令执行的路径
304  pythonPath = resolvePythonExecutable(virtualEnv)
305 
306  if runPath and runPath not in runCommand:
307  cwd = os.path.join(dynamicRoot, projName, runPath)
308  script = 'verifySingle.py'
309  elif runPath:
310  cwd = os.path.join(dynamicRoot, projName)
311  script = os.path.join(runPath, 'verifySingle.py')
312  else:
313  cwd = os.path.join(dynamicRoot, projName)
314  script = 'verifySingle.py'
315  result = subprocess.run(
316  [pythonPath, script, pklPath, apiWithValue],
317  cwd=cwd, capture_output=True, text=True, encoding='utf-8'
318  )
319  return result
320 
321 
322 
323 
332 def validateByStr(fixedAPI,repairDict,targetAPIDefinition,starFlag,twoStarFlag):
333  paraObjLst=mirrorAPI(fixedAPI,repairDict)
334  posLst,keyLst=para2Obj(targetAPIDefinition)
335  targetPara=posLst+keyLst
336 
337  for para,nameFlag in paraObjLst:
338  compatible=0 #假设每个参数都不兼容
339  for it in targetPara:
340  if nameFlag: #带参数名使用的,要求修复完之后能在目标版本的参数定义中找到相同名字的参数
341  if para.name==it.name:
342  compatible=1
343  break
344  else: #不带参数使用的要求修复完之后,位置和参数名都要能找到对应的
345  if para.position==it.position and para.name==it.name:
346  compatible=1
347  break
348 
349 
350  if not compatible:
351  if nameFlag and not twoStarFlag:
352  return False
353  if not nameFlag and not starFlag:
354  return False
355 
356  return True
357 
358 
359 
360 
383 def repairTask(root,callAPI,apiWithValue,projName,runPath,runCommand,repairLst,virtualEnv,errLst,callKey,*,runtimePaths):
384  #静态修复,pkl加载失败,只能进入静态修复
385  repairCandidates=[]
386  if apiWithValue=='':
387  for repairDict,targetPara in repairLst:
388  starFlag=0 #判断目标版本的参数定义中是否含有*args
389  twoStarFlag=0 #判断目标版本的参数定义中是否含有*kwargs
390  if '*args' in targetPara:
391  starFlag=1
392  if '**' in targetPara:
393  twoStarFlag=1
394  apiRoot=getAst(callAPI,1)
395  fix(callAPI,repairDict,apiRoot,starFlag,twoStarFlag)
396  fixedAPI=ast.unparse(apiRoot)
397  if validateByStr(fixedAPI,repairDict,targetPara,starFlag,twoStarFlag):
398  if fixedAPI not in repairCandidates:
399  repairCandidates.append(fixedAPI)
400 
401  # repairCandidates=list(set(repairCandidates))
402  if len(repairCandidates)==0:
403  return str(repairCandidates), 'Unknown', 'Unknown'
404  elif len(repairCandidates)==1:
405  fixedAPI=repairCandidates[0]
406  if callAPI.replace(' ','').replace('"','').replace("'",'')==fixedAPI.replace(' ','').replace('"','').replace("'",''):
407  return repairCandidates[0],"Compatible","Unknown"
408  else:
409  return repairCandidates[0],"Incompatible","Unknown"
410  else:
411  return str(repairCandidates),"Unknown", "Unknown"
412 
413  #pkl加载成功,但匹配的结果也可能是多个,内置API只能静态匹配
414  failedLst=[]
415  for repairDict,targetPara in repairLst:
416  starFlag=0 #判断目标版本的参数定义中是否含有*args
417  twoStarFlag=0 #判断目标版本的参数定义中是否含有*kwargs
418  # 1.先进行静态验证
419  if '*args' in targetPara:
420  starFlag=1
421  if '**' in targetPara:
422  twoStarFlag=1
423  apiRoot=getAst(callAPI,1)
424  fix(callAPI,repairDict,apiRoot,starFlag,twoStarFlag)
425  fixedAPI=ast.unparse(apiRoot)
426  if not validateByStr(fixedAPI,repairDict,targetPara,starFlag,twoStarFlag):
427  continue
428  elif len(repairLst)==1:
429  repairCandidates.append(fixedAPI)
430 
431  #2. 动态验证
432  fixFlag='Failed'
433  apiRoot=getAst(apiWithValue,1)
434  fix(apiWithValue,repairDict,apiRoot,starFlag,twoStarFlag)
435  apiWithValueFixed=ast.unparse(apiRoot)
436  #1先通过动态运行,判断其是否修复成功
437  result=validateByRun(callAPI,apiWithValueFixed,projName,virtualEnv,runPath,runCommand,callKey,runtimePaths=runtimePaths)
438  if result==None:
439  if fixedAPI not in repairCandidates:
440  repairCandidates.append(fixedAPI)
441  fixFlag='Unknown'
442  elif result.returncode!=0:
443  errLst.append(f"{callAPI}, validate error: {result.stderr}\n")
444  failedLst.append(f"{callAPI}, validate error: {result.stderr}\n")
445  if 'dill' in result.stderr:
446  fixFlag='Unknown'
447  if fixedAPI not in repairCandidates:
448  repairCandidates.append(fixedAPI)
449  elif validateByStr(fixedAPI,repairDict,targetPara,starFlag,twoStarFlag):
450  fixFlag='Unknown'
451  if fixedAPI not in repairCandidates:
452  repairCandidates.append(fixedAPI)
453 
454  else:
455  fixFlag='Successful'
456  if fixedAPI not in repairCandidates:
457  repairCandidates.append(fixedAPI)
458  break
459 
460  # repairCandidates=list(set(repairCandidates))
461  if len(repairCandidates)==0:
462  return str(repairCandidates), 'Unknown' , 'Unknown'
463  elif len(repairCandidates)==1:
464  fixedAPI=repairCandidates[0]
465  if callAPI.replace(' ','').replace('"','').replace("'",'')==fixedAPI.replace(' ','').replace('"','').replace("'",''):
466  return repairCandidates[0],'Compatible',fixFlag
467  else:
468  return repairCandidates[0],'Incompatible',fixFlag
469  else:
470  return str(repairCandidates),'Unknown','Unknown'
def para2Obj(paraStr)
Convert parameter string to positional and keyword parameter objects 将参数字符串转换为位置和关键字参数对象
def mirrorAPI(fixedAPI, dic)
Create mirror for the fixed API by adding all parameter names 给修复后API的参数填上参数名
Definition: repair.py:83
def mapPos(pos, dic)
Get repair operation dictionary of positional parameter 获取位置参数的修复操作字典
Definition: repair.py:34
def repairTask(root, callAPI, apiWithValue, projName, runPath, runCommand, repairLst, virtualEnv, errLst, callKey, *runtimePaths)
Task of repairing parameter compatibility issues 参数兼容性问题修复任务
Definition: repair.py:383
def fix(callAPI, repairDict, node, starFlag, twoStarFlag)
Perform parameter repair operations through AST 通过AST执行参数修复操作
Definition: repair.py:142
def findName(pos, dic)
Find parameter name by its position 通过位置查找参数名
Definition: repair.py:68
def validateByRun(callAPI, apiWithValue, projName, virtualEnv, runPath, runCommand, callKey, *runtimePaths)
Dynamic validation 动态验证
Definition: repair.py:278
def validateByStr(fixedAPI, repairDict, targetAPIDefinition, starFlag, twoStarFlag)
Static validation 静态验证
Definition: repair.py:332
def mapName(name, dic)
Get repair operation dictionary of keyword parameter 获取关键字参数的修复操作字典
Definition: repair.py:54
def getAst(filePath, strFlag=0)
Get AST for code 将代码转化为Ast树
Definition: tool.py:173
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
def getLastAPIParameter(apiStr)
Get parameter(s) of the last API from the API call string 获取最后一个API参数
Definition: tool.py:262