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