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