PCART  v1.4
Automated Repair of Python API Parameter Compatibility Issues
getDef.py
Go to the documentation of this file.
1 
10 
11 
12 
13 import os
14 import re
15 from Path.getPath import *
16 from Extract.extractDef import *
17 from Extract.extractCall import *
18 from Tool.tool import getAst
19 
20 
21 
22 
26 class RegexMatch:
27 
28 
32  def __init__(self,code_text,pattern):
33  self._code_text_code_text=code_text
34  self._pattern_pattern=pattern
35  self._result_result=[]
36 
37 
40  def get_result(self):
41  return self._result_result
42 
43 
46  def regex_match(self):
47  obj=re.compile(self._pattern_pattern,re.DOTALL)
48  lst=obj.findall(self._code_text_code_text)
49  if len(lst)>0:
50  #对找到的所有参数字符串进行处理
51  for index in range(0,len(lst)):
52  if lst[index].find('\n')!=-1: #若参数字符串含有换行符'\n'
53  lst[index]=lst[index].replace('\n','') #去掉所有的换行符
54  lst[index]=lst[index].replace(' ','') #去掉所有的空格
55 
56  self._result_result=lst
57  return 1
58  else:
59  self._result_result=[]
60  return 0
61 
62 
63 
64 
68 def getAssign(root_node):
69  #找出树中所有的模块名
70  import_visitor=Import()
71  try:
72  import_visitor.visit(root_node)
73  except Exception as e:
74  print(f"import visit failed: {e}")
75  md_names=import_visitor.get_md_name() #dict
76 
77  #找出所有的Assign节点
78  assign_visitor=AssignVisitor()
79  assign_visitor.visit(root_node)
80  target_call=assign_visitor.get_target_call()
81 
82  for key,val in target_call.items():
83  name_parts=val.split('.')
84  if name_parts[0] in target_call:
85  target_call[key]=target_call[name_parts[0]]+'.'+'.'.join(name_parts[1:])
86 
87  for key,val in target_call.items():
88  name_parts=val.split('.')
89  if name_parts[0] in md_names:
90  target_call[key]=(md_names[name_parts[0]]+'.'+'.'.join(name_parts[1:])).rstrip('.')
91 
92  return target_call
93 
94 
95 
96 
104 def shortenPath(lst,fileDict,importCache=None): #lst是传入传出参数,保存修正之后的API路径
105  absolutePath=[k for k in fileDict.keys()][0] #/home/zhang/pkg/file.py
106  relativePath=[v for v in fileDict.values()][0] #pkg/file.py
107  norm_relative=relativePath.replace('\\','/')
108  norm_absolute=absolutePath.replace('\\','/')
109  pos1=norm_relative.rfind('/')
110  if pos1==-1:
111  return
112  relativePath=relativePath[0:pos1] #更新relativatePath
113  pos2=norm_absolute.rfind('/')
114  absolutePath=absolutePath[0:pos2] #更新absolutePath,使其和relativatePath保持一致
115  initPath=f"{absolutePath}/__init__.py"
116  api=lst[0]
117  if os.path.exists(initPath): #判断当前目录中是否有__init__.py
118  currentLevel=relativePath.replace('\\','/').split('/')[-1]
119  cacheKey=(initPath,currentLevel)
120  if importCache is not None and cacheKey in importCache:
121  importDict=importCache[cacheKey]
122  else:
123  try:
124  root=getAst(initPath)
125  except Exception as e:
126  print(f"shortenPath --> ast.parse failed: {e}")
127  return
128  obj=FromImport(currentLevel)
129  obj.visit(root)
130  importDict=obj.importDict
131  if importCache is not None:
132  importCache[cacheKey]=importDict
133  replaceKey1=''
134  replaceVal1=''
135  replaceKey2=''
136  replaceVal2=''
137  for key,value in importDict.items():
138  if key[-1]=='*':
139  key=key.rstrip('*')
140  if key in api:
141  replaceKey1=key
142  replaceVal1=''
143  elif key in api:
144  # if key.split('.')[-1]==api.split('.')[-1]: #key的最后一个字段要和api的最后一个字段相同
145  replaceKey2=key
146  replaceVal2=value
147  if replaceKey2: #优先使用第二种替换方式
148  api=api.replace(replaceKey2,replaceVal2)
149  elif replaceKey1:
150  api=api.replace(replaceKey1,replaceVal1)
151  lst[0]=api
152  shortenPath(lst,{absolutePath:relativePath},importCache)
153 
154 
155 
156 
165 def getClass(lst,root,prefix,fileDict, pyiFlag=0, importCache=None): #lst是传入传出参数
166  className=root.name
167  flagInit=0
168  flagNew=0
169  flagCall=0
170  #首先抽取当前类中的所有函数
171  for n in ast.iter_child_nodes(root):
172  #if isinstance(n,ast.FunctionDef):
173  # Add the support of extracting AsyncFunctionDef type node -- 2025/5/19
174  if isinstance(n,(ast.FunctionDef, ast.AsyncFunctionDef)):
175  if 'overload' in ast.unparse(n.decorator_list) and not pyiFlag: #非pyi文件中,遇到带有overload装饰器的就跳过
176  continue
177  funcName=n.name
178  arg=ast.unparse(n.args) #从args节点出解析出函数参数
179  arg=arg.replace(' ','') #去掉字符串中的空格
180  try:
181  ret='->'+ast.unparse(n.returns)
182  except:
183  ret=''
184  if funcName=='__init__': #一个class中也可能存在多个init,即class API的重载
185  init=arg
186  flagInit=1
187  elif funcName=='__new__':
188  new=arg
189  flagNew=1
190  elif funcName=='__call__':
191  call=arg
192  flagCall=1
193  else:
194  lst.append(f"{prefix}.{className}.{funcName}({arg}){ret}")
195  #尝试缩短API路径
196  apiPath=[f"{prefix}.{className}.{funcName}"]
197  shortenPath(apiPath,fileDict,importCache)
198 
199  if apiPath[0]!=f"{prefix}.{className}.{funcName}":
200  lst.append(f"{apiPath[0]}({arg}){ret}")
201 
202  if flagInit==1:
203  para=f"({init})"
204  lst.append(f"{prefix}.{className}.__init__{para}")
205  elif flagNew==1:#若class中不含init,再看是否有new
206  para=f"({new})"
207  lst.append(f"{prefix}.{className}.__new__{para}")
208  elif flagCall==1:
209  para=f"({call})"
210  lst.append(f"{prefix}.{className}.__call__{para}")
211  else: #若类中不含init,new,call,就将类的继承作为类的参数
212  pattern=fr"class {re.escape(className)}(\‍(.*?):"
213  codeText=ast.unparse(root)
214  R=RegexMatch(codeText,pattern)
215  flag=R.regex_match()
216  if flag==1:
217  args=R.get_result()
218  else:
219  args=['']
220  para=args[0]
221  lst.append(f"{prefix}.{className}{para}")
222 
223  #尝试缩短API路径
224  apiPath=[f"{prefix}.{className}"]
225  shortenPath(apiPath,fileDict,importCache)
226  if apiPath[0]!=f"{prefix}.{className}":
227  lst.append(f"{apiPath[0]}{para}")
228 
229  #然后再判断当前类节点下是否还有嵌套类,有的话就往下递归
230  prefix+=f".{className}" #更新前缀
231  for n in ast.iter_child_nodes(root):
232  if isinstance(n,ast.ClassDef):
233  getClass(lst,n,prefix,fileDict,pyiFlag,importCache)
234 
235 
236 
237 
246 def task(codeText,libApi,prefix,fileDict, pyiFlag=0, importCache=None): #这里的prefix只到文件名
247  try:
248  rootNode=ast.parse(codeText,filename='<unknown>',mode='exec')
249  except Exception as e:
250  file = list(fileDict.keys())[0]
251  print(f"{file} ast.parse falied: {e}")
252  return
253  for node in ast.iter_child_nodes(rootNode):
254  if isinstance(node, ast.ClassDef): #抽取类内API
255  getClass(libApi,node,prefix,fileDict,pyiFlag,importCache)
256 
257  #if isinstance(node,ast.FunctionDef): #再抽取类外的API
258  # Add the support of extracting AsyncFunctionDef type node -- 2025/5/19
259  if isinstance(node,(ast.FunctionDef, ast.AsyncFunctionDef)):
260  if 'overload' in ast.unparse(node.decorator_list) and not pyiFlag: #遇到含overload装饰器的就跳过
261  continue
262  funcName=node.name
263  arg=ast.unparse(node.args)
264  arg=arg.replace(' ','')
265  try:
266  ret='->'+ast.unparse(node.returns)
267  except:
268  ret=''
269  libApi.append(f"{prefix}.{funcName}({arg}){ret}")
270 
271  #尝试缩短API路径
272  lst=[f"{prefix}.{funcName}"]
273  shortenPath(lst,fileDict,importCache)
274  if lst[0]!=f"{prefix}.{funcName}":
275  libApi.append(f"{lst[0]}({arg}){ret}")
276 
277 
278 
279 
285 def getPublicAliasLine(line,sourceRoot,publicRoot):
286  if not sourceRoot or not publicRoot:
287  return None
288  sourcePrefix=sourceRoot+'.'
289  publicPrefix=publicRoot+'.'
290  assignSourcePrefix='A:'+sourcePrefix
291  assignPublicPrefix='A:'+publicPrefix
292  if line.startswith(sourcePrefix):
293  return publicPrefix+line[len(sourcePrefix):]
294  if line.startswith(assignSourcePrefix):
295  return assignPublicPrefix+line[len(assignSourcePrefix):]
296  return None
297 
298 
299 
300 
307 def writeApiLine(fw,line,sourceRoot='',publicRoot=''):
308  fw.write(f"{line}\n")
309  aliasLine=getPublicAliasLine(line,sourceRoot,publicRoot)
310  if aliasLine and aliasLine!=line:
311  fw.write(f"{aliasLine}\n")
312 
313 
314 
315 
319 def getDefFunction(args):
320  libName, version, libPath=args
321  fileObj=Path('DF')
322  fileObj.getPath(libPath)
323  #filePath是库下所有文件对应的路径
324  filePath=fileObj.path
325  if not os.path.exists(f"LibAPIExtraction/{libName}"):
326  try:
327  os.mkdir(f"LibAPIExtraction/{libName}") #多进程可能同时执行这句,所以这个结构需要修改
328  except:
329  pass
330  f=open(f'LibAPIExtraction/{libName}/{libName}{version}','w',encoding='UTF-8')
331 
332 
333  fileVisitLst=[]
334  importCache={}
335  publicAliasSource=''
336  publicAliasTarget=''
337  if libName=="tensorflow" and os.path.basename(os.path.normpath(libPath))=="tensorflow_core":
338  publicAliasSource="tensorflow_core"
339  publicAliasTarget="tensorflow"
340  for file in filePath:
341  pyLst=[] #保存每个.py文件中的API
342  pyiLst=[] #保存每个.pyi文件中的API
343  def2format=Def2format()
344  def2format.toFormat(file)
345  prefix=def2format.prefix #前缀,包名.文件名
346  relativePath=def2format.relativePath #相对路径,只从包名开始
347  fileDict={file:relativePath}
348  #对于每个file,首先判断一下他是.py文件还是.pyi文件
349  if file[-1]=='y' and file not in fileVisitLst:
350  fileVisitLst.append(file)
351  #对于每一个.py文件,首先看它有没有.pyi,若无,则直接以.py中的API定义为准
352  #若有,则再抽取.pyi中的API,最后保留.pyi和.py的差集
353  pyiFlag=0
354  if file+'i' not in fileVisitLst: #判断.pyi之前是否访问过
355  try:
356  with open(file+'i','r',encoding='UTF-8') as fr:
357  code_text=fr.read()
358  task(code_text,pyiLst,prefix,fileDict, 1, importCache) #抽取.pyi中的API
359  pyiFlag=1
360  fileVisitLst.append(file+'i')
361  except FileNotFoundError:
362  pass
363 
364  with open(file,'r',encoding='UTF-8') as fr:
365  try:
366  code_text=fr.read()
367  except Exception as e:
368  print(f"{file} read failed: {e}")
369  continue
370  try:
371  root_node=ast.parse(code_text,filename='<unknown>',mode='exec')
372  except Exception as e:
373  print(f'{file} ast.parse failed: {e}')
374  continue
375  assignDict=getAssign(root_node) #抽取.py中的所有Assign Node
376  f.write('\n'+'-' * 40 + f"{file}" + '-' * 40+'\n')
377  for key,val in assignDict.items():
378  writeApiLine(f,f'A:{prefix}.{key}->{val}',publicAliasSource,publicAliasTarget)
379  #抽取.py中的Definition Node
380  task(code_text,pyLst,prefix,fileDict,0,importCache)
381  pyLst.sort()
382  for it in pyLst:
383  writeApiLine(f,it,publicAliasSource,publicAliasTarget)
384  f.write('\n')
385 
386  if pyiFlag:
387  removeLst=[]
388  for it1 in pyiLst:
389  for it2 in pyLst:
390  if it2.split('(')[0]==it1.split('(')[0]:
391  removeLst.append(it1)
392  break
393  for it in removeLst:
394  pyiLst.remove(it)
395 
396  #此时.pyi中保存的都是内置的API注释
397  pyiLst.sort()
398  f.write('\n'+'-' * 40 + f"{file}"+'i' + '-' * 40+'\n')
399  for it in pyiLst:
400  writeApiLine(f,it,publicAliasSource,publicAliasTarget)
401  f.write('\n')
402 
403  elif file[-1]=='i' and file not in fileVisitLst:
404  fileVisitLst.append(file)
405  if file.rstrip('i') not in fileVisitLst:
406  try:
407  with open(file.rstrip('i'),'r',encoding='UTF-8') as fr:
408  code_text=fr.read()
409  task(code_text,pyLst,prefix,fileDict,0,importCache) #抽取.py中的API
410  fileVisitLst.append(file.rstrip('i'))
411  root_node=ast.parse(code_text,filename='<unknown>',mode='exec')
412  assignDict=getAssign(root_node)
413  f.write('\n'+'-' * 40 + f"{file.rstrip('i')}" + '-' * 40+'\n')
414  for key,value in assignDict.items():
415  writeApiLine(f,f'A:{prefix}.{key}->{value}',publicAliasSource,publicAliasTarget)
416  pyLst.sort()
417  for it in pyLst:
418  writeApiLine(f,it,publicAliasSource,publicAliasTarget)
419  f.write('\n')
420  except FileNotFoundError:
421  pass
422 
423  with open(file,'r',encoding='UTF-8') as fr:
424  code_text=fr.read()
425  task(code_text,pyiLst,prefix,fileDict,1,importCache) #抽取.pyi中的API
426  removeLst=[]
427  pyiLst.sort()
428  for it1 in pyiLst:
429  for it2 in pyLst:
430  if it2.split('(')[0]==it1.split('(')[0]:
431  removeLst.append(it1)
432  break
433  for it in removeLst:
434  pyiLst.remove(it)
435 
436  f.write('\n'+'-' * 40 + f"{file}" + '-' * 40+'\n')
437  for it in pyiLst:
438  writeApiLine(f,it,publicAliasSource,publicAliasTarget)
439  f.write('\n')
440 
441  f.close()
Regular expression match class 正则表达式匹配类
Definition: getDef.py:26
def regex_match(self)
Perform the regular expression match 执行正则表达式匹配
Definition: getDef.py:46
def __init__(self, code_text, pattern)
The constructor 构造函数
Definition: getDef.py:32
def get_result(self)
Return the match result 返回匹配结果
Definition: getDef.py:40
def getAssign(root_node)
Extract all assign node from a .py file's AST 通过AST获取.py文件的Assign语句
Definition: getDef.py:68
def writeApiLine(fw, line, sourceRoot='', publicRoot='')
Write one lib API line and its public alias if needed 写出一行库API,并按需写出公开路径别名
Definition: getDef.py:307
def getClass(lst, root, prefix, fileDict, pyiFlag=0, importCache=None)
Extract class method definitions from a give class.
Definition: getDef.py:165
def shortenPath(lst, fileDict, importCache=None)
Shorten the API path based on init.py and import alias 通过解析__init__.py和import别名,把源码中的部分API路径缩短
Definition: getDef.py:104
def task(codeText, libApi, prefix, fileDict, pyiFlag=0, importCache=None)
Extract all lib API definitions from a source file 抽取库源码API定义任务
Definition: getDef.py:246
def getDefFunction(args)
Extract all lib API definitions from a specified version 抽取给定版本的库API定义
Definition: getDef.py:319
def getPublicAliasLine(line, sourceRoot, publicRoot)
Return a public alias line for source-root API lines 为源码根路径API行生成公开路径别名
Definition: getDef.py:285
def getAst(filePath, strFlag=0)
Get AST for code 将代码转化为Ast树
Definition: tool.py:173