4Scope-level operations for FORTRAN code.
6Provides PYFTscope class for navigating and manipulating FORTRAN scopes
7(modules, subroutines, functions, types) with integrated support for
8variables, statements, cosmetics, applications, C++ directives, and OpenACC.
12- Scope path navigation (e.g., 'module:MOD/sub:SUB')
13- XML tree traversal with CONTAINS section filtering
14- Parent/sibling element lookup with caching
15- Integration of Variables, Statements, Cosmetics, Applications, Cpp, Openacc
19PYFTscope : Core scope wrapper extending ElementView
20ElementView : XML tree view with optional CONTAINS exclusion
24>>> pft = PYFT('input.F90')
25>>> scopes = pft.getScopes() # Get all scopes
26>>> sub = pft.getScopeNode('module:MOD/sub:SUB') # Get specific scope
27>>> for scope in pft.getScopes(excludeKinds=['type']):
47 View of an ElementTree exposing a subset of subelements.
49 Provides filtering capabilities for XML trees, particularly for
50 excluding CONTAINS sections from scope traversal.
53 def __init__(self, xml, excludeContains=False):
55 Initialize ElementView.
60 Root XML element for this view.
61 excludeContains : bool, optional
62 If True, ignore elements after CONTAINS statement.
71 :param xml: xml corresponding to a scope
72 :return: a node (possibly the xml node) containing only the relevant subelements
75 contains = self.
_xml.
find(
'./{*}contains-stmt')
78 indexContains = list(self.
_xml).index(contains)
79 childNode = createElem(
'virtual')
80 childNode.extend(self.
_xml[:indexContains] + [self.
_xml[-1]])
89 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.tag
96 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.tail
103 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.text
105 return self.
_xml.text
111 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.findtext
117 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.iterfind
123 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.itertext
127 def __getitem__(self, *args, **kwargs):
130 def __len__(self, *args, **kwargs):
136 def find(self, *args, **kwargs):
138 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.find
144 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.findall
148 def iter(self, *args, **kwargs):
150 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.iter
156 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.items
165 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.clear
174 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.append
181 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.extend
188 :param index: index in the virtual node
189 :return: index in the _xml node
193 contains = self.
_xml.
find(
'./{*}contains-stmt')
196 indexContains = list(self.
_xml).index(contains)
198 if index > indexContains
or index < -indexContains - 1:
199 raise IndexError(
'list index out of range')
205 index = indexContains + index + 1
207 return len(self.
_xml)
if index == indexContains
else index
209 def __setitem__(self, index, item):
213 def __delitem__(self, index):
218 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.insert
225 Remove node from the xml
227 if isinstance(node, ElementView):
229 self.getParent(node).
remove(node)
234 Wrap an XML node representing a FORTRAN scope.
236 PYFTscope provides methods to navigate, query, and modify FORTRAN
237 source code at the scope level (modules, subroutines, functions, types).
241 Scope paths are '/' separated strings identifying the location in the
242 source tree. Examples:
243 - 'module:MODULE' - a module
244 - 'module:MOD/sub:SUB' - subroutine SUB in module MODULE
245 - 'module:MOD/type:TYPE' - type TYPE in module MODULE
246 - 'module:MOD/sub:SUB/func:FUNC' - function FUNC in subroutine SUB
250 >>> pft = PYFT('myfile.F90')
251 >>> scopes = pft.getScopes() # Get all scopes
252 >>> sub = pft.getScopeNode('module:MOD/sub:SUB') # Get specific scope
253 >>> for scope in pft.getScopes(excludeKinds=['type']):
254 ... print(scope.path)
256 SCOPE_STMT = {
'module':
'module-stmt',
257 'func':
'function-stmt',
258 'sub':
'subroutine-stmt',
260 'prog':
'program-stmt',
261 'interface':
'interface-stmt',
262 'submodule':
'submodule-stmt'}
263 SCOPE_CONSTRUCT = {
'module':
'program-unit',
264 'func':
'program-unit',
265 'sub':
'program-unit',
266 'type':
'T-construct',
267 'prog':
'program-unit',
268 'interface':
'interface-construct',
269 'submodule':
'program-unit'}
271 def __init__(self, xml, scopePath='/', parentScope=None,
272 enableCache=False, tree=None, excludeContains=False):
274 Initialize a PYFTscope instance.
279 XML element representing the scope.
280 scopePath : str, optional
281 Path string identifying this scope (e.g., 'module:MOD/sub:SUB').
282 parentScope : PYFTscope, optional
283 Parent scope instance.
284 enableCache : bool, optional
285 If True, cache parent nodes for faster traversal.
286 tree : Tree, optional
287 Tree instance for cross-file analysis.
288 excludeContains : bool, optional
289 If True, ignore CONTAINS sections in scope traversal.
291 super().
__init__(xml=xml, excludeContains=excludeContains)
292 self.
_mainScope = self
if parentScope
is None else parentScope._mainScope
293 self.
_path = scopePath
295 self.
tree =
Tree()
if tree
is None else tree
298 if enableCache
and parentScope
is None:
300 for node
in self.
iter():
306 result = cls.__new__(cls)
307 result.__dict__.update(self.
__dict__)
310 def __deepcopy__(self, memo):
312 result = cls.__new__(cls)
313 memo[id(self)] = result
315 setattr(result, key, copy.deepcopy(val, memo))
320 Get some attributes defined in the PYFT class
322 if attr
in (
'SHARED_TREE',
'NO_PARALLEL_LOCK',
'PARALLEL_FILE_LOCKS '):
324 raise AttributeError(f
"{attr} doesn't exist")
334 Scope path string (e.g., 'module:MOD/sub:SUB').
341 Get the main (root) scope.
346 The top-level scope in the file.
353 Get the parent scope.
358 Parent scope, or None if this is the root scope.
365 Get the parent element of an XML node.
370 Element whose parent to find.
371 level : int, optional
372 Number of levels to traverse (1 = direct parent, 2 = grandparent, etc.).
377 Parent element at the specified level.
383 return node
in self.
mainScope._cacheParent.get(id(node), [])
and \
385 check(self.
mainScope._cacheParent[id(node)]))
390 parent = self.
mainScope._cacheParent[id(item)]
393 if item
in list(node):
397 self.
mainScope._cacheParent[id(item)] = node
399 return parent
if level == 1
else self.
getParent(parent, level - 1)
403 Get sibling elements.
408 Element whose siblings to find.
409 before : bool, optional
410 If True, include siblings before the item. Default is True.
411 after : bool, optional
412 If True, include siblings after the item. Default is True.
417 List of sibling elements.
421 >>> siblings = scope.getSiblings(node) # All siblings
422 >>> before = scope.getSiblings(node, after=False) # Only before
427 siblings = siblings[:siblings.index(item)]
429 siblings = siblings[siblings.index(item) + 1:]
430 return [s
for s
in siblings
if s != item]
436 Internal methode to compute the name of a scope
437 :param node: program-unit node
440 if tag(node) ==
'T-stmt':
442 nodeN = node.find(
'./{*}T-N/{*}N')
443 elif tag(node) ==
'submodule-stmt':
444 nodeN = node.find(
'./{*}submodule-module-N')
446 nodeN = node.find(
'.//{*}N')
447 if nodeN
is not None and nodeN.find(
'.//{*}N')
is not None:
449 nodeN = nodeN.find(
'.//{*}N')
450 if nodeN
is not None:
451 name = n2name(nodeN).upper()
459 Internal methode to compute a path part from a node
460 :param node: program-unit node
461 :return: path part (e.g. module:MODU)
471 Normalize a scope path to standard format.
473 Converts scope path to lowercase prefix and uppercase names.
478 Scope path to normalize.
483 Normalized scope path.
487 >>> PYFTscope.normalizeScope('module:Test/sub:Sub')
488 'module:TEST/sub:SUB'
490 return '/'.join([(k.lower() +
':' + w.upper())
491 for (k, w)
in [component.split(
':')
492 for component
in scopePath.split(
'/')]])
497 Display all scopes found in the source code.
501 includeItself : bool, optional
502 If True, include the current scope in the output.
507 >>> pft = PYFT('myfile.F90')
508 >>> pft.showScopesList()
509 These scopes have been found in the source code:
511 - /module:MOD/sub:SUB
512 - /module:MOD/func:FUNC
514 print(
"These scopes have been found in the source code:")
515 print(
"\n".join([
' - ' + scope.path
516 for scope
in self.
getScopes(includeItself=includeItself)]))
519 def getScopes(self, level=-1, excludeContains=True, excludeKinds=None, includeItself=True):
521 Get child scopes from the current scope.
525 level : int, optional
526 Depth of scope traversal:
527 - -1 (default): All child scopes recursively.
528 - 1: Direct children only.
529 - 2: Children and grandchildren, etc.
530 excludeContains : bool, optional
531 If True (default), exclude scopes from CONTAINS sections
532 (nested subroutines/functions).
533 excludeKinds : list of str, optional
534 Scope kinds to exclude. Options: 'module', 'sub', 'func',
535 'type', 'prog', 'interface', 'submodule'.
536 includeItself : bool, optional
537 If True (default), include the current scope in results.
542 List of scope instances matching the criteria.
546 >>> pft = PYFT('myfile.F90')
547 >>> all_scopes = pft.getScopes() # All scopes
548 >>> subs = pft.getScopes(excludeKinds=['module', 'func', 'type'])
549 >>> direct = pft.getScopes(level=1)
551 assert level == -1
or level > 0,
'level must be -1 or a positive int'
553 def _getRecur(node, level, basePath=''):
555 if tag(node) ==
'object':
556 usenode = node.find(
'./{*}file')
560 for child
in [child
for child
in usenode
563 if excludeKinds
is None or nodePath.split(
':')[0]
not in excludeKinds:
564 scopePath = nodePath
if basePath
in (
'',
'/') \
565 else basePath +
'/' + nodePath
567 scopePath=scopePath, parentScope=self,
568 enableCache=
False, tree=self.
tree,
569 excludeContains=excludeContains))
571 results.extend(_getRecur(child, level - 1, scopePath))
579 return _getRecur(self, level, self.
pathpathpath) + itself
582 def getScopeNode(self, scopePath, excludeContains=True, includeItself=True):
584 Get a specific scope by path.
589 Scope path to search for (e.g., 'module:MOD/sub:SUB').
590 excludeContains : bool, optional
591 See getScopes. Default is True.
592 includeItself : bool, optional
593 See getScopes. Default is True.
598 Scope instance matching the path.
603 If scope not found or found multiple times.
607 >>> pft = PYFT('myfile.F90')
608 >>> sub = pft.getScopeNode('module:MOD/sub:SUB')
609 >>> func = pft.getScopeNode('/module:MOD/func:FUNC')
611 scope = [scope
for scope
in self.
getScopes(excludeContains=excludeContains,
612 includeItself=includeItself)
613 if scope.path == scopePath]
615 raise PYFTError(f
'{scopePath} not found')
617 raise PYFTError(f
'{scopePath} found several times')
623 Check if a node is a scope-level construct.
633 True if node is a scope construct (program-unit, interface-construct,
638 >>> node = pft.find('.//{*}subroutine-stmt/..')
639 >>> pft.isScopeNode(node)
647 Get the scope containing an element.
652 Element whose containing scope to find.
653 mustRaise : bool, optional
654 If True (default), raise PYFTError if scope not found.
659 The scope node containing the item.
663 >>> call = pft.find('.//{*}call-stmt')
664 >>> scope = pft.getParentScopeNode(call)
667 while result
is not None and not self.
isScopeNode(result):
669 if result
is None and mustRaise:
670 raise PYFTError(
"The scope parent has not been found.")
676 Get the scope path for an element.
681 Element whose scope path to determine.
682 includeItself : bool, optional
683 If True (default) and item is a scope node, include it.
688 Full scope path of the element's containing scope.
692 >>> node = pft.find('.//{*}a-stmt')
693 >>> pft.getScopePath(node)
694 '/module:MOD/sub:SUB'
701 while item
is not None:
704 return '/'.join(result)
708 Get the source filename.
713 Normalized path to the source file.
717 >>> pft = PYFT('/path/to/file.F90')
718 >>> pft.getFileName()
721 return os.path.normpath(self.
mainScope.
find(
'.//{*}file').attrib[
'name'])
725 def empty(self, addStmt=None, simplify=False):
727 Remove all statements from scopes.
729 Removes all executable statements from scopes while preserving:
730 - Dummy argument declarations
732 - Scope declarations and endings
736 addStmt : str or list, optional
737 Statement(s) to insert into the body of emptied scopes.
738 simplify : bool, optional
739 If True, also remove unused local variables.
743 >>> pft = PYFT('myfile.F90')
744 >>> pft.empty() # Remove all statements
746 Empty and add a comment:
747 >>> pft.empty(addStmt='! TODO: Implement')
750 for scope
in self.
getScopes(level=1, excludeContains=
False):
751 if scope.path.split(
'/')[-1].split(
':')[0] ==
'module':
752 scopes.extend(scope.getScopes(level=1, excludeContains=
False, includeItself=
False))
755 tagExcluded = (list(self.
SCOPE_STMT.values()) +
756 [
'end-' + decl
for decl
in self.
SCOPE_STMT.values()] +
757 [
'T-decl-stmt',
'use-stmt',
'C'])
759 for node
in list(scope):
760 if tag(node)
not in tagExcluded:
762 scope.removeUnusedLocalVar(simplify=simplify)
763 if addStmt
is not None:
764 if isinstance(addStmt, str):
765 addStmt = createExpr(addStmt)
766 elif not isinstance(addStmt, list):
769 scope.insertStatement(stmt,
False)