2This module contains the Tree class to browse the tree
11from functools
import wraps
18def updateTree(method='file'):
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
25 - 'signal': analyse files (if any) signaled using
26 the signal method of the tree object
28 assert method
in (
'file',
'scan',
'signal')
32 def wrapper(self, *args, **kwargs):
33 result = func(self, *args, **kwargs)
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())
50 Class to browse the Tree
52 def __init__(self, tree=None, descTreeFile=None,
53 parser=None, parserOptions=None, wrapH=False,
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 parser, parserOptions, wrapH: see the PYFT class
59 :param verbosity: if not None, sets the verbosity level
62 self.
_tree = []
if tree
is None else tree
73 self.
_cwd = os.getcwd()
83 if descTreeFile
is not None and os.path.exists(descTreeFile):
85 elif tree
is not None:
87 if descTreeFile
is not None:
92 :return: a dict containing the full content of the instance
94 return {
'tree': self.
_tree,
113 :param content: Fill the current instance with this dict
115 self.
_tree = content[
'tree']
117 self.
_parser = content[
'parser']
119 self.
_wrapH = content[
'wrapH']
121 self.
_cwd = content[
'cwd']
122 self.
_scopes = content[
'scopes']
134 Sets self to be a copy of other
140 Sets other to be a copy of self
146 Method used for signaling a modified file which needs to be analized
147 :param filename: file name or PYFTscope object
153 :return: the list of file signaled for update and empties the list
161 :return: the list of analysez file names
163 return list(self.
_scopes.keys())
167 """Is the Tree object valid"""
172 """List of directories"""
178 :param tree: list of directories composing the tree or None
179 :return: list of directories and subdirectories
182 if self.
tree is not None:
183 for tDir
in self.
tree:
184 result += glob.glob(tDir +
'/**/', recursive=
True)
190 :param tree: list of directories composing the tree or None
191 :return: list of directories and subdirectories
194 for tDir
in self.
tree:
195 for filename
in glob.glob(tDir +
'/**/*', recursive=
True):
196 if os.path.splitext(filename)[1]
not in (
'',
'.json',
'.fypp',
'.txt'):
198 filenames.append(filename)
204 Builds the self._* variable
213 Updates the object when a file has changed
214 :param file: name of the file (or list of names) with updated content
218 if not isinstance(file, (list, set)):
226 """Empties cached values"""
233 """Fill and return the self._cacheIncInScope cached value"""
241 def _compilationTree(self):
242 """Fill and return the self._cacheCompilationTree cached value"""
247 for filename, incScopePaths
in self.
_includeList.items():
249 for scopePath, incList
in incScopePaths.items():
259 if os.path.normpath(inc) == os.path.normpath(file):
262 elif ((
not os.path.isabs(file))
and
263 os.path.realpath(inc) == os.path.realpath(os.path.join(
264 os.path.dirname(inc), file))):
268 elif os.path.basename(inc) == os.path.basename(file):
270 basename.append(file)
272 same = subdir = basename = []
274 subdir = basename = []
275 if len(basename) > 1:
279 incFilename = same[0]
280 elif len(subdir) > 0:
281 incFilename = subdir[0]
282 elif len(basename) > 0:
283 incFilename = basename[0]
293 for filename, uList
in self.
_useList.items():
295 for modName, _
in [use
for li
in uList.values()
for use
in li]:
296 moduleScopePath =
'module:' + modName
299 for file, scopes
in self.
_scopes.items():
300 if moduleScopePath
in scopes:
305 logging.info(
'Several or none file containing the scope path ' +
306 '%s have been found for file %s',
307 moduleScopePath, filename)
317 def _executionTree(self):
318 """Fill and return the self._cacheCompilationTree cached value"""
322 allScopes = [scopePath
for _, l
in self.
_scopes.items()
for scopePath
in l]
325 for filename, callScopes
in progList.items():
327 for scopePath, cList
in callScopes.items():
329 for call
in set(cList):
334 foundInSameScope = []
337 for kind
in (canonicKind,
'interface'):
339 uList = [self.
_useList[filename][sc]
341 if (sc == scopePath
or scopePath.startswith(sc +
'/'))]
342 for modName, only
in [use
for li
in uList
for use
in li]:
343 moduleScope =
'module:' + modName
344 callScope = moduleScope +
'/' + kind +
':' + call
347 if call
in only
and callScope
in allScopes:
348 foundInUse.append(callScope)
351 for _, scopes
in self.
_scopes.items():
352 if callScope
in scopes:
353 foundInUse.append(callScope)
356 callScope = kind +
':' + call
357 for _, scopes
in self.
_scopes.items():
358 if callScope
in scopes:
359 foundElsewhere.append(callScope)
362 callScope = kind +
':' + call
364 if callScope
in self.
_scopes[incFile]:
365 foundInInclude.append(callScope)
368 callScope = scopePath +
'/' + kind +
':' + call
369 if callScope
in self.
_scopes[filename]:
370 foundInContains.append(callScope)
374 callScope = scopePath.rsplit(
'/', 1)[0] +
'/' + kind + \
377 callScope = kind +
':' + call
378 if callScope
in self.
_scopes[filename]:
379 foundInSameScope.append(callScope)
382 foundInUse = list(set(foundInUse))
383 if len(foundInUse + foundInInclude +
384 foundInContains + foundInSameScope) > 1:
385 logging.error(
'Several definition of the program unit found for '
386 '%s called in %s:', call, scopePath)
387 logging.error(
' found %i time(s) in USE statements',
389 logging.error(
' found %i time(s) in include files',
391 logging.error(
' found %i time(s) in CONTAINS block',
392 len(foundInContains))
393 logging.error(
' found %i time(s) in the same scope',
394 len(foundInSameScope))
396 elif len(foundInUse + foundInInclude +
397 foundInContains + foundInSameScope) == 1:
398 rr = (foundInUse + foundInInclude +
399 foundInContains + foundInSameScope)[0]
400 if canonicKind !=
'func' or rr
in allScopes:
402 elif len(foundElsewhere) > 1:
403 logging.info(
'Several definition of the program unit found for '
404 '%s called in %s', call, scopePath)
405 elif len(foundElsewhere) == 1:
408 if canonicKind !=
'func':
409 logging.info(
'No definition of the program unit found for '
410 '%s called in %s', call, scopePath)
416 for item
in list(execList):
417 itemSplt = item.split(
'/')[-1].split(
':')
418 if itemSplt[0] ==
'interface' and itemSplt[1] !=
'--UNKNOWN--':
420 filenames = [k
for (k, v)
in self.
_scopes.items()
if item
in v]
421 if len(filenames) == 1:
423 execList.remove(item)
424 for sub
in [sub
for sub
in self.
_scopes[filenames[0]]
425 if sub.startswith(item +
'/')]:
426 subscopeIn = sub.rsplit(
'/', 2)[0] +
'/' + sub.split(
'/')[-1]
427 if subscopeIn
in self.
_scopes[filenames[0]]:
429 execList.append(subscopeIn)
431 execList.append(sub.split(
'/')[-1])
442 :param file: Name of the file to explore, or PYFTscope object
443 :return: dict of use, include, call, function and scope list
445 def extractString(text):
447 if text[0]
in (
'"',
"'"):
448 assert text[-1] == text[0]
456 filename = pft.getFileName()
463 filename = filename[2:]
if filename.startswith(
'./')
else filename
471 scopes = pft.getScopes()
474 self.
_scopes[filename].append(scope.path)
476 if scope.path.split(
'/')[-1].split(
':')[0] ==
'interface':
477 for name
in [n2name(nodeN).upper()
478 for moduleproc
in scope.findall(
'./{*}procedure-stmt')
479 for nodeN
in moduleproc.findall(
'./{*}module-procedure-N-LT/' +
482 if re.search(scope.path.rsplit(
'/', 1)[0] +
'/[a-zA-Z]*:' + name,
484 self.
_scopes[filename].append(scope.path +
'/' +
485 sc.path.split(
'/')[-1])
492 [file.text
for file
in scope.findall(
'.//{*}include/{*}filename')]
494 [extractString(file.text)
495 for file
in scope.findall(
'.//{*}include/{*}filename/{*}S')])
499 self.
_useList[filename][scope.path] = []
500 for use
in scope.findall(
'.//{*}use-stmt'):
501 modName = n2name(use.find(
'./{*}module-N/{*}N')).upper()
502 only = [n2name(n).upper()
for n
in use.findall(
'.//{*}use-N//{*}N')]
503 self.
_useList[filename][scope.path].append((modName, only))
508 list(set(n2name(call.find(
'./{*}procedure-designator/{*}named-E/{*}N')).upper()
509 for call
in scope.findall(
'.//{*}call-stmt')))
511 self.
_funcList[filename][scope.path] = set()
512 for name
in [n2name(call.find(
'./{*}N')).upper()
513 for call
in scope.findall(
'.//{*}named-E/{*}R-LT/{*}parens-R/../..')]:
515 var = scope.varList.findVar(name)
516 if var
is None or var[
'as']
is None:
517 self.
_funcList[filename][scope.path].add(name)
530 with open(filename,
'r', encoding=
'utf-8')
as file:
531 descTree = json.load(file)
532 self.
_cwd = descTree[
'cwd']
533 self.
_scopes = descTree[
'scopes']
542 descTree = {
'cwd': self.
_cwd,
550 descTree[
'scopes'] = {k: sorted(descTree[
'scopes'][k])
for k
in sorted(descTree[
'scopes'])}
551 for cat
in (
'useList',
'includeList',
'callList',
'funcList'):
552 descTree[cat] = {file: {scope: sorted(descTree[cat][file][scope])
553 for scope
in sorted(descTree[cat][file])}
554 for file
in sorted(descTree[cat])}
556 with open(filename,
'w', encoding=
'utf-8')
as file:
557 json.dump(descTree, file, indent=2)
562 Return the name of the file defining the scope
563 :param scopePath: scope path to search for
564 :return: list file names in which scope is defined
566 return [filename
for filename, scopes
in self.
_scopes.items()
if scopePath
in scopes]
571 Return the scopes contained in the file
572 :param filename: name of the file tn inspect
573 :return: list of scopes defined in the file
580 :param node: initial node
581 :param descTreePart: 'compilation_tree' or 'execution_tree' part of a descTree object
582 :param level: number of levels (0 to get only the initial node, None to get all nodes)
583 :param down: True to get the nodes lower in the tree, False to get the upper ones
584 :return: list of nodes lower or upper tahn initial node (recursively)
586 def recur(nnn, level, currentList):
588 result = descTreePart.get(nnn, [])
590 result = [item
for (item, l)
in descTreePart.items()
if nnn
in l]
591 if level
is None or level > 1:
592 for res
in list(result):
593 if res
not in currentList:
594 result.extend(recur(res,
None if level
is None else level - 1, result))
596 return recur(node, level, [])
601 :param filename: initial file name
602 :param level: number of levels (0 to get only the initial file, None to get all files)
603 :return: list of file names needed by the initial file (recursively)
610 :param filename: initial file name
611 :param level: number of levels (0 to get only the initial file, None to get all files)
612 :return: list of file names that needs the initial file (recursively)
619 :param scopePath: initial scope path
620 :param level: number of levels (0 to get only the initial scope path,
621 None to get all scopes)
622 :return: list of scopes called by the initial scope path (recursively)
629 :param scopePath: initial scope path
630 :param level: number of levels (0 to get only the initial scope path,
631 None to get all scopes)
632 :return: list of scopes that calls the initial scope path (recursively)
638 includeInterfaces=False, includeStopScopes=False):
640 :param scopePath: scope path to test
641 :param stopScopes: list of scopes
642 :param includeInterfaces: if True, interfaces of positive scopes are also positive
643 :param includeInterfaces: if True, scopes that are in stopScopes return True
644 :return: True if the scope path is called directly or indirectly by one of the scope
645 paths listed in stopScopes
647 scopeSplt = scopePath.split(
'/')
648 if includeInterfaces
and len(scopeSplt) >= 2
and scopeSplt[-2].split(
':')[0] ==
'interface':
651 scopeI = scopeSplt[-1]
655 includeStopScopes=includeStopScopes)
659 return (any(scp
in upperScopes
for scp
in stopScopes)
or
660 (includeStopScopes
and scopePath
in stopScopes))
663 def plotTree(self, centralNodeList, output, plotMaxUpper, plotMaxLower, kind, frame=False):
665 Compute a dependency graph
666 :param centralNodeList: file, scope path, list of files or list of scope paths
667 :param output: output file name (.dot or .png extension)
668 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
669 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
670 :param kind: must be 'compilation_tree' or 'execution_tree'
671 :param frame: True to plot a frame grouping the central nodes
673 assert kind
in (
'compilation_tree',
'execution_tree')
679 if objHash
not in hashValues:
681 hashValues[objHash] = hashValues[0]
682 return str(hashValues[objHash])
684 def createNode(node, label=None):
686 if label
is not None:
687 result +=
"subgraph cluster_" + myHash(node) +
" {\n"
688 result += f
'label="{label}"\n'
689 if kind ==
'execution_tree':
690 color =
'blue' if node.split(
'/')[-1].split(
':')[0] ==
'func' else 'green'
693 result += myHash(node) + f
' [label="{node}" color="{color}"]\n'
694 if label
is not None:
698 def createLink(file1, file2):
699 return myHash(file1) +
' -> ' + myHash(file2) +
'\n'
701 def createCluster(nodes, label=None):
702 result =
"subgraph cluster_R {\n"
703 result +=
"{rank=same " + (
' '.join([myHash(node)
for node
in nodes])) +
"}\n"
704 if label
is not None:
705 result += f
'label="{label}"\n'
713 def filename(scopePath):
714 if kind ==
'compilation_tree':
716 return [f
for f, l
in self.
_scopes.items()
if scopePath
in l][0]
718 def recur(node, level, down, var):
719 if level
is None or level > 0:
721 result = var.get(node, [])
723 result = [f
for f, l
in var.items()
if node
in l]
725 add(createNode(res, filename(res)))
726 add(createLink(node, res)
if down
else createLink(res, node))
727 if level
is None or level > 1:
728 recur(res,
None if level
is None else level - 1, down, var)
732 if kind ==
'execution_tree':
733 centralScopeFilenames = []
734 for scopePath
in centralNodeList:
735 centralScopeFilenames.append(filename(scopePath))
736 centralScopeFilenames = list(set(centralScopeFilenames))
737 if len(centralScopeFilenames) == 1:
745 var = {k: sorted(var[k])
for k
in sorted(var)}
747 dot = [
"digraph D {\n"]
748 if not isinstance(centralNodeList, list):
749 centralNodeList = [centralNodeList]
750 for centralNode
in centralNodeList:
751 add(createNode(centralNode,
None if printInFrame
else filename(centralNode)))
752 recur(centralNode, plotMaxLower,
True, var)
753 recur(centralNode, plotMaxUpper,
False, var)
755 if kind ==
'compilation_tree':
758 frameText = centralScopeFilenames[0]
if printInFrame
else None
759 add(createCluster(centralNodeList, frameText))
762 fmt = os.path.splitext(output)[1].lower()[1:]
764 with open(output,
'w', encoding=
'utf-8')
as file:
767 dotCommand = [
'dot',
'-T' + fmt,
'-o', output]
768 logging.info(
'Dot command: %s',
' '.join(dotCommand))
769 subprocess.run(dotCommand, input=dot.encode(
'utf8'), check=
True)
774 Compute the compilation dependency graph
775 :param filename: central file
776 :param output: output file name (.dot or .png extension)
777 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
778 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
780 return self.
plotTree(filename, output, plotMaxUpper, plotMaxLower,
'compilation_tree',
True)
785 Compute the execution dependency graph
786 :param scopePath: central scope path
787 :param output: output file name (.dot or .png extension)
788 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
789 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
791 return self.
plotTree(scopePath, output, plotMaxUpper, plotMaxLower,
'execution_tree')
796 Compute the compilation dependency graph
797 :param scopePath: central scope path
798 :param output: output file name (.dot or .png extension)
799 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
800 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
808 Compute the execution dependency graph
809 :param filename: central filename
810 :param output: output file name (.dot or .png extension)
811 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
812 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
815 'execution_tree',
True)
820 Return the file name containing an interface for the scope path
821 :param scopePath: scope path for which an interface is searched
822 :return: (file name, interface scope) or (None, None) if not found
824 for filename, scopes
in self.
_scopes.items():
825 for scopeInterface
in scopes:
826 if re.search(
r'interface:[a-zA-Z0-9_-]*/' + scopePath, scopeInterface):
827 return filename, scopeInterface