PyForTool
Python-fortran-tool
Loading...
Searching...
No Matches
tree.py
1"""
2This module contains the Tree class to browse the tree
3"""
4
5import glob
6import os
7import logging
8import json
9import subprocess
10import re
11from functools import wraps
12
13from pyfortool.util import debugDecor, n2name
14import pyfortool.scope
16
17
18def updateTree(method='file'):
19 """
20 Decorator factory to update the tree after having executed a PYFTscope method
21 :param method: method to use for updating
22 - 'file': analyze current file (default)
23 - 'scan': analyse new files and suppress tree information
24 for suppressed files
25 - 'signal': analyse files (if any) signaled using
26 the signal method of the tree object
27 """
28 assert method in ('file', 'scan', 'signal')
29
30 def decorator(func):
31 @wraps(func)
32 def wrapper(self, *args, **kwargs):
33 result = func(self, *args, **kwargs)
34 if self.tree.isValid:
35 if method == 'file':
36 self.tree.update(self)
37 elif method == 'scan':
38 current = set(self.tree.getFiles())
39 old = set(self.tree.knownFiles())
40 self.tree.update(current.symmetric_difference(old))
41 elif method == 'signal':
42 self.tree.update(self.tree.popSignaled())
43 return result
44 return wrapper
45 return decorator
46
47
48class Tree():
49 """
50 Class to browse the Tree
51 """
52 def __init__(self, tree=None, descTreeFile=None,
53 parserOptions=None, wrapH=False,
54 verbosity=None):
55 """
56 :param tree: list of directories composing the tree or None
57 :param descTreeFile: filename where the description of the tree will be stored
58 :param parserOptions, wrapH: see the PYFT class
59 :param verbosity: if not None, sets the verbosity level
60 """
61 # Options
62 self._tree = [] if tree is None else tree
63 self._descTreeFile = descTreeFile
64 self._parserOptions = parserOptions
65 self._wrapH = wrapH
66 self._verbosity = verbosity
67
68 # Files signaled for update
69 self._signaled = set()
70
71 # File analysis
72 self._cwd = os.getcwd()
73 self._emptyCache()
74 self._scopes = {}
75 self._useList = {}
76 self._includeList = {}
77 self._callList = {}
78 self._funcList = {}
79 self._cacheCompilationTree = None
80 self._cacheExecutionTree = None
81 self._cacheIncInScope = None
82 if descTreeFile is not None and os.path.exists(descTreeFile):
83 self.fromJson(descTreeFile)
84 elif tree is not None:
85 self._build()
86 if descTreeFile is not None:
87 self.toJson(descTreeFile)
88
89 def getFullContent(self):
90 """
91 :return: a dict containing the full content of the instance
92 """
93 return {'tree': self._tree,
94 'descTreeFile': self._descTreeFile,
95 'parserOptions': self._parserOptions,
96 'wrapH': self._wrapH,
97 'verbosity': self._verbosity,
98 'cwd': self._cwd,
99 'scopes': self._scopes,
100 'useList': self._useList,
101 'includeList': self._includeList,
102 'callList': self._callList,
103 'funcList': self._funcList,
104 'signaled': self._signaled,
105 'cache_compilationTree': self._cacheCompilationTree,
106 'cacheExecutionTree': self._cacheExecutionTree,
107 'cacheIncScope': self._cacheIncInScope}
108
109 def setFullContent(self, content):
110 """
111 :param content: Fill the current instance with this dict
112 """
113 self._tree = content['tree']
114 self._descTreeFile = content['descTreeFile']
115 self._parserOptions = content['parserOptions']
116 self._wrapH = content['wrapH']
117 self._verbosity = content['verbosity']
118 self._cwd = content['cwd']
119 self._scopes = content['scopes']
120 self._useList = content['useList']
121 self._includeList = content['includeList']
122 self._callList = content['callList']
123 self._funcList = content['funcList']
124 self._signaled = content['signaled']
125 self._cacheCompilationTree = content['cache_compilationTree']
126 self._cacheExecutionTree = content['cacheExecutionTree']
127 self._cacheIncInScope = content['cacheIncScope']
128
129 def copyFromOtherTree(self, other):
130 """
131 Sets self to be a copy of other
132 """
133 self.setFullContent(other.getFullContent())
134
135 def copyToOtherTree(self, other):
136 """
137 Sets other to be a copy of self
138 """
139 other.setFullContent(self.getFullContent())
140
141 def signal(self, file):
142 """
143 Method used for signaling a modified file which needs to be analized
144 :param filename: file name or PYFTscope object
145 """
146 self._signaled.add(file)
147
148 def popSignaled(self):
149 """
150 :return: the list of file signaled for update and empties the list
151 """
152 temp = self._signaled
153 self._signaled = set()
154 return temp
155
156 def knownFiles(self):
157 """
158 :return: the list of analysez file names
159 """
160 return list(self._scopes.keys())
161
162 @property
163 def isValid(self):
164 """Is the Tree object valid"""
165 return len(self._scopes) != 0
166
167 @property
168 def tree(self):
169 """List of directories"""
170 return self._tree
171
172 @debugDecor
173 def getDirs(self):
174 """
175 :param tree: list of directories composing the tree or None
176 :return: list of directories and subdirectories
177 """
178 result = []
179 if self.tree is not None:
180 for tDir in self.tree:
181 result += glob.glob(tDir + '/**/', recursive=True)
182 return result
183
184 @debugDecor
185 def getFiles(self):
186 """
187 :param tree: list of directories composing the tree or None
188 :return: list of directories and subdirectories
189 """
190 filenames = []
191 for tDir in self.tree:
192 for filename in glob.glob(tDir + '/**/*', recursive=True):
193 if os.path.splitext(filename)[1] not in ('', '.json', '.fypp', '.txt'):
194 # We only keep files with extension
195 filenames.append(filename)
196 return filenames
197
198 @debugDecor
199 def _build(self):
200 """
201 Builds the self._* variable
202 """
203 # Loop on directory and files
204 for filename in self.getFiles():
205 self._analyseFile(filename)
206
207 @debugDecor
208 def update(self, file):
209 """
210 Updates the object when a file has changed
211 :param file: name of the file (or list of names) with updated content
212 or PYFTscope object
213 """
214 if self.isValid:
215 if not isinstance(file, (list, set)):
216 file = [file]
217 if len(file) != 0:
218 for onefile in file:
219 self._analyseFile(onefile)
220 self._emptyCache()
221
222 def _emptyCache(self):
223 """Empties cached values"""
224 self._cacheCompilationTree = None
225 self._cacheExecutionTree = None
226 self._cacheIncInScope = None
227
228 @property
229 def _incInScope(self):
230 """Fill and return the self._cacheIncInScope cached value"""
231 if self.isValid and self._cacheIncInScope is None:
232 # pylint: disable-next=pointless-statement
233 self._compilationTree_compilationTree # self._cacheIncInScope computed at the same time
234 return self._cacheIncInScope
235
236 @property
237 @debugDecor
238 def _compilationTree(self):
239 """Fill and return the self._cacheCompilationTree cached value"""
240 if self.isValid and self._cacheCompilationTree is None:
241 self._cacheCompilationTree = {f: [] for f in self._scopes}
242 self._cacheIncInScope = {}
243 # Compilation_tree computation: include
244 for filename, incScopePaths in self._includeList.items():
245 # Loop on scopes
246 for scopePath, incList in incScopePaths.items():
247 # Loop on each included file
248 self._cacheIncInScope[scopePath] = []
249 for inc in incList:
250 # Try to guess the right file
251 same = []
252 subdir = []
253 basename = []
254 # Loop on each file found in the source tree
255 for file in self._cacheCompilationTree:
256 if os.path.normpath(inc) == os.path.normpath(file):
257 # Exactly the same file name (including directories)
258 same.append(file)
259 elif ((not os.path.isabs(file)) and
260 os.path.realpath(inc) == os.path.realpath(os.path.join(
261 os.path.dirname(inc), file))):
262 # The include statement refers to a file contained in the
263 # directory where inc is
264 subdir.append(file)
265 elif os.path.basename(inc) == os.path.basename(file):
266 # Same name excluding the directories
267 basename.append(file)
268 if len(same) > 1:
269 same = subdir = basename = []
270 if len(subdir) > 1:
271 subdir = basename = []
272 if len(basename) > 1:
273 basename = []
274 found = True
275 if len(same) > 0:
276 incFilename = same[0]
277 elif len(subdir) > 0:
278 incFilename = subdir[0]
279 elif len(basename) > 0:
280 incFilename = basename[0]
281 else:
282 # We haven't found the file in the tree, we keep the inc untouched
283 found = False
284 incFilename = inc
285 self._cacheCompilationTree[filename].append(incFilename)
286 if found:
287 self._cacheIncInScope[scopePath].append(incFilename)
288
289 # Compilation_tree computation: use
290 for filename, uList in self._useList.items():
291 # Loop on each use statement
292 for modName, _ in [use for li in uList.values() for use in li]:
293 moduleScopePath = 'module:' + modName
294 # Loop on scopes to find the module
295 found = []
296 for file, scopes in self._scopes.items():
297 if moduleScopePath in scopes:
298 found.append(file)
299 if len(found) == 1:
300 self._cacheCompilationTree[filename].append(found[0])
301 else:
302 logging.info('Several or none file containing the scope path ' +
303 '%s have been found for file %s',
304 moduleScopePath, filename)
305
306 # Compilation_tree: cleaning (uniq values)
307 for filename, depList in self._cacheCompilationTree.items():
308 self._cacheCompilationTree[filename] = list(set(depList))
309
310 return self._cacheCompilationTree
311
312 @property
313 @debugDecor
314 def _executionTree(self):
315 """Fill and return the self._cacheCompilationTree cached value"""
316 if self.isValid and self._cacheExecutionTree is None:
317 self._cacheExecutionTree = {}
318 # Execution_tree: call statements
319 allScopes = [scopePath for _, l in self._scopes.items() for scopePath in l]
320 self._cacheExecutionTree = {scopePath: [] for scopePath in allScopes}
321 for canonicKind, progList in (('sub', self._callList), ('func', self._funcList)):
322 for filename, callScopes in progList.items():
323 # Loop on scopes
324 for scopePath, cList in callScopes.items():
325 # Loop on calls
326 for call in set(cList):
327 foundInUse = []
328 foundElsewhere = []
329 foundInInclude = []
330 foundInContains = []
331 foundInSameScope = []
332
333 # We look for sub:c or interface:c
334 for kind in (canonicKind, 'interface'):
335 # Loop on each use statement in scope or in upper scopes
336 uList = [self._useList[filename][sc]
337 for sc in self._useList[filename]
338 if (sc == scopePath or scopePath.startswith(sc + '/'))]
339 for modName, only in [use for li in uList for use in li]:
340 moduleScope = 'module:' + modName
341 callScope = moduleScope + '/' + kind + ':' + call
342 if len(only) > 0:
343 # There is a "ONLY" keyword
344 if call in only and callScope in allScopes:
345 foundInUse.append(callScope)
346 else:
347 # There is no "ONLY"
348 for _, scopes in self._scopes.items():
349 if callScope in scopes:
350 foundInUse.append(callScope)
351
352 # Look for subroutine directly accessible
353 callScope = kind + ':' + call
354 for _, scopes in self._scopes.items():
355 if callScope in scopes:
356 foundElsewhere.append(callScope)
357
358 # Look for include files
359 callScope = kind + ':' + call
360 for incFile in self._incInScope[scopePath]:
361 if callScope in self._scopes[incFile]:
362 foundInInclude.append(callScope)
363
364 # Look for contained routines
365 callScope = scopePath + '/' + kind + ':' + call
366 if callScope in self._scopes[filename]:
367 foundInContains.append(callScope)
368
369 # Look for routine in the same scope
370 if '/' in scopePath:
371 callScope = scopePath.rsplit('/', 1)[0] + '/' + kind + \
372 ':' + call
373 else:
374 callScope = kind + ':' + call
375 if callScope in self._scopes[filename]:
376 foundInSameScope.append(callScope)
377
378 # Final selection
379 foundInUse = list(set(foundInUse)) # If a module is used several times
380 if len(foundInUse + foundInInclude +
381 foundInContains + foundInSameScope) > 1:
382 logging.error('Several definition of the program unit found for '
383 '%s called in %s:', call, scopePath)
384 logging.error(' found %i time(s) in USE statements',
385 len(foundInUse))
386 logging.error(' found %i time(s) in include files',
387 len(foundInInclude))
388 logging.error(' found %i time(s) in CONTAINS block',
389 len(foundInContains))
390 logging.error(' found %i time(s) in the same scope',
391 len(foundInSameScope))
392 self._cacheExecutionTree[scopePath].append('??')
393 elif len(foundInUse + foundInInclude +
394 foundInContains + foundInSameScope) == 1:
395 rr = (foundInUse + foundInInclude +
396 foundInContains + foundInSameScope)[0]
397 if canonicKind != 'func' or rr in allScopes:
398 self._cacheExecutionTree[scopePath].append(rr)
399 elif len(foundElsewhere) > 1:
400 logging.info('Several definition of the program unit found for '
401 '%s called in %s', call, scopePath)
402 elif len(foundElsewhere) == 1:
403 self._cacheExecutionTree[scopePath].append(foundElsewhere[0])
404 else:
405 if canonicKind != 'func':
406 logging.info('No definition of the program unit found for '
407 '%s called in %s', call, scopePath)
408
409 # Execution_tree: named interface
410 # We replace named interface by the list of routines declared in this interface
411 # This is not perfect because only one routine is called and not all
412 for _, execList in self._cacheExecutionTree.items():
413 for item in list(execList):
414 itemSplt = item.split('/')[-1].split(':')
415 if itemSplt[0] == 'interface' and itemSplt[1] != '--UNKNOWN--':
416 # This is a named interface
417 filenames = [k for (k, v) in self._scopes.items() if item in v]
418 if len(filenames) == 1:
419 # We have found in which file this interface is declared
420 execList.remove(item)
421 for sub in [sub for sub in self._scopes[filenames[0]]
422 if sub.startswith(item + '/')]:
423 subscopeIn = sub.rsplit('/', 2)[0] + '/' + sub.split('/')[-1]
424 if subscopeIn in self._scopes[filenames[0]]:
425 # Routine found in the same scope as the interface
426 execList.append(subscopeIn)
427 else:
428 execList.append(sub.split('/')[-1])
429
430 # Execution_tree: cleaning (uniq values)
431 for scopePath, execList in self._cacheExecutionTree.items():
432 self._cacheExecutionTree[scopePath] = list(set(execList))
433
434 return self._cacheExecutionTree
435
436 @debugDecor
437 def _analyseFile(self, file):
438 """
439 :param file: Name of the file to explore, or PYFTscope object
440 :return: dict of use, include, call, function and scope list
441 """
442 def extractString(text):
443 text = text.strip()
444 if text[0] in ('"', "'"):
445 assert text[-1] == text[0]
446 text = text[1, -1]
447 return text
448
449 # Loop on directory and files
450 if isinstance(file, pyfortool.scope.PYFTscope) or os.path.isfile(file):
451 if isinstance(file, pyfortool.scope.PYFTscope):
452 pft = file.mainScope
453 filename = pft.getFileName()
454 mustClose = False
455 else:
457 self._wrapH, verbosity=self._verbosity)
458 filename = file
459 mustClose = True
460 filename = filename[2:] if filename.startswith('./') else filename
461
462 # Loop on scopes
463 self._scopes[filename] = []
464 self._includeList[filename] = {}
465 self._useList[filename] = {}
466 self._callList[filename] = {}
467 self._funcList[filename] = {}
468 scopes = pft.getScopes()
469 for scope in scopes:
470 # Scope found in file
471 self._scopes[filename].append(scope.path)
472 # We add, to this list, the "MODULE PROCEDURE" declared in INTERFACE statements
473 if scope.path.split('/')[-1].split(':')[0] == 'interface':
474 for name in [n2name(nodeN).upper()
475 for moduleproc in scope.findall('./{*}procedure-stmt')
476 for nodeN in moduleproc.findall('./{*}module-procedure-N-LT/' +
477 '{*}N')]:
478 for sc in scopes:
479 if re.search(scope.path.rsplit('/', 1)[0] + '/[a-zA-Z]*:' + name,
480 sc.path):
481 self._scopes[filename].append(scope.path + '/' +
482 sc.path.split('/')[-1])
483
484 # include, use, call and functions
485 # Fill compilation_tree
486 # Includes give directly the name of the source file but possibly without
487 # the directory
488 self._includeList[filename][scope.path] = \
489 [file.text for file in scope.findall('.//{*}include/{*}filename')] # cpp
490 self._includeList[filename][scope.path].extend(
491 [extractString(file.text)
492 for file in scope.findall('.//{*}include/{*}filename/{*}S')]) # FORTRAN
493
494 # For use statements, we need to scan all the files to know which one
495 # contains the module
496 self._useList[filename][scope.path] = []
497 for use in scope.findall('.//{*}use-stmt'):
498 modName = n2name(use.find('./{*}module-N/{*}N')).upper()
499 only = [n2name(n).upper() for n in use.findall('.//{*}use-N//{*}N')]
500 self._useList[filename][scope.path].append((modName, only))
501
502 # Fill execution tree
503 # We need to scan all the files to find which one contains the subroutine/function
504 self._callList[filename][scope.path] = \
505 list(set(n2name(call.find('./{*}procedure-designator/{*}named-E/{*}N')).upper()
506 for call in scope.findall('.//{*}call-stmt')))
507 # We cannot distinguish function from arrays
508 self._funcList[filename][scope.path] = set()
509 for name in [n2name(call.find('./{*}N')).upper()
510 for call in scope.findall('.//{*}named-E/{*}R-LT/{*}parens-R/../..')]:
511 # But we can exclude some names if they are declared as arrays
512 var = scope.varList.findVar(name)
513 if var is None or var['as'] is None:
514 self._funcList[filename][scope.path].add(name)
515 self._funcList[filename][scope.path] = list(self._funcList[filename][scope.path])
516 if mustClose:
517 pft.close()
518 else:
519 if filename in self._scopes:
520 del self._scopes[filename], self._includeList[filename], \
521 self._useList[filename], self._callList[filename], \
522 self._funcList[filename]
523
524 @debugDecor
525 def fromJson(self, filename):
526 """read from json"""
527 with open(filename, 'r', encoding='utf-8') as file:
528 descTree = json.load(file)
529 self._cwd = descTree['cwd']
530 self._scopes = descTree['scopes']
531 self._useList = descTree['useList']
532 self._includeList = descTree['includeList']
533 self._callList = descTree['callList']
534 self._funcList = descTree['funcList']
535
536 @debugDecor
537 def toJson(self, filename):
538 """save to json"""
539 descTree = {'cwd': self._cwd,
540 'scopes': self._scopes,
541 'useList': self._useList,
542 'includeList': self._includeList,
543 'callList': self._callList,
544 'funcList': self._funcList,
545 }
546 # Order dict keys and list values
547 descTree['scopes'] = {k: sorted(descTree['scopes'][k]) for k in sorted(descTree['scopes'])}
548 for cat in ('useList', 'includeList', 'callList', 'funcList'):
549 descTree[cat] = {file: {scope: sorted(descTree[cat][file][scope])
550 for scope in sorted(descTree[cat][file])}
551 for file in sorted(descTree[cat])}
552 # Write json on disk with indentation
553 with open(filename, 'w', encoding='utf-8') as file:
554 json.dump(descTree, file, indent=2)
555
556 # No @debugDecor for this low-level method
557 def scopeToFiles(self, scopePath):
558 """
559 Return the name of the file defining the scope
560 :param scopePath: scope path to search for
561 :return: list file names in which scope is defined
562 """
563 return [filename for filename, scopes in self._scopes.items() if scopePath in scopes]
564
565 @debugDecor
566 def fileToScopes(self, filename):
567 """
568 Return the scopes contained in the file
569 :param filename: name of the file tn inspect
570 :return: list of scopes defined in the file
571 """
572 return self._scopes[filename]
573
574 @staticmethod
575 def _recurList(node, descTreePart, level, down):
576 """
577 :param node: initial node
578 :param descTreePart: 'compilation_tree' or 'execution_tree' part of a descTree object
579 :param level: number of levels (0 to get only the initial node, None to get all nodes)
580 :param down: True to get the nodes lower in the tree, False to get the upper ones
581 :return: list of nodes lower or upper tahn initial node (recursively)
582 """
583 def recur(nnn, level, currentList):
584 if down:
585 result = descTreePart.get(nnn, [])
586 else:
587 result = [item for (item, l) in descTreePart.items() if nnn in l]
588 if level is None or level > 1:
589 for res in list(result):
590 if res not in currentList: # for FORTRAN recursive calls
591 result.extend(recur(res, None if level is None else level - 1, result))
592 return result
593 return recur(node, level, [])
594
595 @debugDecor
596 def needsFile(self, filename, level=1):
597 """
598 :param filename: initial file name
599 :param level: number of levels (0 to get only the initial file, None to get all files)
600 :return: list of file names needed by the initial file (recursively)
601 """
602 return self._recurList(filename, self._compilationTree_compilationTree, level, True)
603
604 @debugDecor
605 def neededByFile(self, filename, level=1):
606 """
607 :param filename: initial file name
608 :param level: number of levels (0 to get only the initial file, None to get all files)
609 :return: list of file names that needs the initial file (recursively)
610 """
611 return self._recurList(filename, self._compilationTree_compilationTree, level, False)
612
613 @debugDecor
614 def callsScopes(self, scopePath, level=1):
615 """
616 :param scopePath: initial scope path
617 :param level: number of levels (0 to get only the initial scope path,
618 None to get all scopes)
619 :return: list of scopes called by the initial scope path (recursively)
620 """
621 return self._recurList(scopePath, self._executionTree_executionTree, level, True)
622
623 @debugDecor
624 def calledByScope(self, scopePath, level=1):
625 """
626 :param scopePath: initial scope path
627 :param level: number of levels (0 to get only the initial scope path,
628 None to get all scopes)
629 :return: list of scopes that calls the initial scope path (recursively)
630 """
631 return self._recurList(scopePath, self._executionTree_executionTree, level, False)
632
633 @debugDecor
634 def isUnderStopScopes(self, scopePath, stopScopes,
635 includeInterfaces=False, includeStopScopes=False):
636 """
637 :param scopePath: scope path to test
638 :param stopScopes: list of scopes
639 :param includeInterfaces: if True, interfaces of positive scopes are also positive
640 :param includeInterfaces: if True, scopes that are in stopScopes return True
641 :return: True if the scope path is called directly or indirectly by one of the scope
642 paths listed in stopScopes
643 """
644 scopeSplt = scopePath.split('/')
645 if includeInterfaces and len(scopeSplt) >= 2 and scopeSplt[-2].split(':')[0] == 'interface':
646 # This scope declares an interface, we look for the scope corresponding
647 # to this interface
648 scopeI = scopeSplt[-1]
649 if scopeI in self._executionTree_executionTree:
650 # The actual code for the routine exists
651 return self.isUnderStopScopes(scopeI, stopScopes,
652 includeStopScopes=includeStopScopes)
653 # No code found for this interface
654 return False
655 upperScopes = self.calledByScope(scopePath, None)
656 return (any(scp in upperScopes for scp in stopScopes) or
657 (includeStopScopes and scopePath in stopScopes))
658
659 @debugDecor
660 def plotTree(self, centralNodeList, output, plotMaxUpper, plotMaxLower, kind, frame=False):
661 """
662 Compute a dependency graph
663 :param centralNodeList: file, scope path, list of files or list of scope paths
664 :param output: output file name (.dot or .png extension)
665 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
666 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
667 :param kind: must be 'compilation_tree' or 'execution_tree'
668 :param frame: True to plot a frame grouping the central nodes
669 """
670 assert kind in ('compilation_tree', 'execution_tree')
671
672 hashValues = {0: 1}
673
674 def myHash(obj):
675 objHash = hash(obj)
676 if objHash not in hashValues:
677 hashValues[0] += 1
678 hashValues[objHash] = hashValues[0]
679 return str(hashValues[objHash])
680
681 def createNode(node, label=None):
682 result = ""
683 if label is not None:
684 result += "subgraph cluster_" + myHash(node) + " {\n"
685 result += f'label="{label}"\n'
686 if kind == 'execution_tree':
687 color = 'blue' if node.split('/')[-1].split(':')[0] == 'func' else 'green'
688 else:
689 color = 'black'
690 result += myHash(node) + f' [label="{node}" color="{color}"]\n'
691 if label is not None:
692 result += "}\n"
693 return result
694
695 def createLink(file1, file2):
696 return myHash(file1) + ' -> ' + myHash(file2) + '\n'
697
698 def createCluster(nodes, label=None):
699 result = "subgraph cluster_R {\n"
700 result += "{rank=same " + (' '.join([myHash(node) for node in nodes])) + "}\n"
701 if label is not None:
702 result += f'label="{label}"\n'
703 result += "}\n"
704 return result
705
706 def add(item):
707 if item not in dot:
708 dot.append(item)
709
710 def filename(scopePath):
711 if kind == 'compilation_tree':
712 return None
713 return [f for f, l in self._scopes.items() if scopePath in l][0]
714
715 def recur(node, level, down, var):
716 if level is None or level > 0:
717 if down:
718 result = var.get(node, [])
719 else:
720 result = [f for f, l in var.items() if node in l]
721 for res in result:
722 add(createNode(res, filename(res)))
723 add(createLink(node, res) if down else createLink(res, node))
724 if level is None or level > 1:
725 recur(res, None if level is None else level - 1, down, var)
726
727 # Are all the central scopes in the same file
728 printInFrame = False
729 if kind == 'execution_tree':
730 centralScopeFilenames = []
731 for scopePath in centralNodeList:
732 centralScopeFilenames.append(filename(scopePath))
733 centralScopeFilenames = list(set(centralScopeFilenames))
734 if len(centralScopeFilenames) == 1:
735 frame = True
736 printInFrame = True
737 else:
738 printInFrame = False
739
740 # Order the tree to obtain deterministic graphs
741 var = self._executionTree_executionTree if kind == 'execution_tree' else self._compilationTree_compilationTree
742 var = {k: sorted(var[k]) for k in sorted(var)}
743
744 dot = ["digraph D {\n"]
745 if not isinstance(centralNodeList, list):
746 centralNodeList = [centralNodeList]
747 for centralNode in centralNodeList:
748 add(createNode(centralNode, None if printInFrame else filename(centralNode)))
749 recur(centralNode, plotMaxLower, True, var)
750 recur(centralNode, plotMaxUpper, False, var)
751 if frame:
752 if kind == 'compilation_tree':
753 frameText = None
754 else:
755 frameText = centralScopeFilenames[0] if printInFrame else None
756 add(createCluster(centralNodeList, frameText))
757 add("}\n")
758 dot = ''.join(dot)
759 fmt = os.path.splitext(output)[1].lower()[1:]
760 if fmt == 'dot':
761 with open(output, 'w', encoding='utf-8') as file:
762 file.write(dot)
763 else:
764 dotCommand = ['dot', '-T' + fmt, '-o', output]
765 logging.info('Dot command: %s', ' '.join(dotCommand))
766 subprocess.run(dotCommand, input=dot.encode('utf8'), check=True)
767
768 @debugDecor
769 def plotCompilTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower):
770 """
771 Compute the compilation dependency graph
772 :param filename: central file
773 :param output: output file name (.dot or .png extension)
774 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
775 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
776 """
777 return self.plotTree(filename, output, plotMaxUpper, plotMaxLower, 'compilation_tree', True)
778
779 @debugDecor
780 def plotExecTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower):
781 """
782 Compute the execution dependency graph
783 :param scopePath: central scope path
784 :param output: output file name (.dot or .png extension)
785 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
786 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
787 """
788 return self.plotTree(scopePath, output, plotMaxUpper, plotMaxLower, 'execution_tree')
789
790 @debugDecor
791 def plotCompilTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower):
792 """
793 Compute the compilation dependency graph
794 :param scopePath: central scope path
795 :param output: output file name (.dot or .png extension)
796 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
797 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
798 """
799 return self.plotTree(self.scopeToFiles(scopePath), output, plotMaxUpper, plotMaxLower,
800 'compilation_tree')
801
802 @debugDecor
803 def plotExecTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower):
804 """
805 Compute the execution dependency graph
806 :param filename: central filename
807 :param output: output file name (.dot or .png extension)
808 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
809 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
810 """
811 return self.plotTree(self.fileToScopes(filename), output, plotMaxUpper, plotMaxLower,
812 'execution_tree', True)
813
814 @debugDecor
815 def findScopeInterface(self, scopePath):
816 """
817 Return the file name containing an interface for the scope path
818 :param scopePath: scope path for which an interface is searched
819 :return: (file name, interface scope) or (None, None) if not found
820 """
821 for filename, scopes in self._scopes.items():
822 for scopeInterface in scopes:
823 if re.search(r'interface:[a-zA-Z0-9_-]*/' + scopePath, scopeInterface):
824 return filename, scopeInterface
825 return None, None
plotTree(self, centralNodeList, output, plotMaxUpper, plotMaxLower, kind, frame=False)
Definition tree.py:660
fileToScopes(self, filename)
Definition tree.py:566
needsFile(self, filename, level=1)
Definition tree.py:596
getFullContent(self)
Definition tree.py:89
scopeToFiles(self, scopePath)
Definition tree.py:557
_executionTree(self)
Definition tree.py:314
copyFromOtherTree(self, other)
Definition tree.py:129
callsScopes(self, scopePath, level=1)
Definition tree.py:614
_analyseFile(self, file)
Definition tree.py:437
calledByScope(self, scopePath, level=1)
Definition tree.py:624
popSignaled(self)
Definition tree.py:148
plotExecTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower)
Definition tree.py:780
toJson(self, filename)
Definition tree.py:537
_compilationTree(self)
Definition tree.py:238
update(self, file)
Definition tree.py:208
_recurList(node, descTreePart, level, down)
Definition tree.py:575
_incInScope(self)
Definition tree.py:229
fromJson(self, filename)
Definition tree.py:525
__init__(self, tree=None, descTreeFile=None, parserOptions=None, wrapH=False, verbosity=None)
Definition tree.py:54
findScopeInterface(self, scopePath)
Definition tree.py:815
plotCompilTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower)
Definition tree.py:769
plotExecTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower)
Definition tree.py:803
copyToOtherTree(self, other)
Definition tree.py:135
neededByFile(self, filename, level=1)
Definition tree.py:605
signal(self, file)
Definition tree.py:141
isUnderStopScopes(self, scopePath, stopScopes, includeInterfaces=False, includeStopScopes=False)
Definition tree.py:635
_emptyCache(self)
Definition tree.py:222
plotCompilTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower)
Definition tree.py:791
setFullContent(self, content)
Definition tree.py:109
conservativePYFT(filename, parserOptions, wrapH, tree=None, verbosity=None, clsPYFT=None)
Definition pyfortool.py:20