PyForTool
Python-fortran-tool
Loading...
Searching...
No Matches
scope.py
1#!/usr/bin/env python3
2
3"""
4This module implements the scope stuff
5"""
6
7import copy
8import os
9
10from pyfortool.variables import Variables, updateVarList
11from pyfortool.cosmetics import Cosmetics
12from pyfortool.applications import Applications
13from pyfortool.statements import Statements
14from pyfortool.cpp import Cpp
15from pyfortool.openacc import Openacc
16from pyfortool.util import PYFTError, debugDecor, n2name, tag
17from pyfortool.tree import Tree, updateTree
18from pyfortool.expressions import createElem, createExpr
19
20
22 """
23 This class acts as a view of an ElementTree exposing only a part of the subelements
24 """
25
26 def __init__(self, xml, excludeContains=False):
27 """
28 :param xml: xml corresponding to a scope
29 :param excludeContains: do not take into account the CONTAINS part
30 """
31 super().__init__()
32 self._excludeContains = excludeContains
33 self._xml = xml
34
35 @property
36 def _virtual(self):
37 """
38 :param xml: xml corresponding to a scope
39 :return: a node (possibly the xml node) containing only the relevant subelements
40 """
41 if self._excludeContains:
42 contains = self._xml.find('./{*}contains-stmt')
43 if contains is None:
44 return self._xml
45 indexContains = list(self._xml).index(contains)
46 childNode = createElem('virtual')
47 childNode.extend(self._xml[:indexContains] + [self._xml[-1]])
48 return childNode
49 return self._xml
50
51 # PROPERTIES
52
53 @property
54 def tag(self):
55 """
56 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.tag
57 """
58 return self._xml.tag
59
60 @property
61 def tail(self):
62 """
63 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.tail
64 """
65 return self._xml.tail
66
67 @property
68 def text(self):
69 """
70 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.text
71 """
72 return self._xml.text
73
74 # READ-ONLY METHODS, they can always use the virtual approach
75
76 def findtext(self, *args, **kwargs):
77 """
78 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.findtext
79 """
80 return self._virtual_virtual.findtext(*args, **kwargs)
81
82 def iterfind(self, *args, **kwargs):
83 """
84 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.iterfind
85 """
86 return self._virtual_virtual.iterfind(*args, **kwargs)
87
88 def itertext(self, *args, **kwargs):
89 """
90 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.itertext
91 """
92 return self._virtual_virtual.itertext(*args, **kwargs)
93
94 def __getitem__(self, *args, **kwargs):
95 return self._virtual_virtual.__getitem__(*args, **kwargs)
96
97 def __len__(self, *args, **kwargs):
98 return self._virtual_virtual.__len__(*args, **kwargs)
99
100 def __iter__(self):
101 return list(self._virtual_virtual).__iter__()
102
103 def find(self, *args, **kwargs):
104 """
105 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.find
106 """
107 return self._virtual_virtual.find(*args, **kwargs)
108
109 def findall(self, *args, **kwargs):
110 """
111 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.findall
112 """
113 return self._virtual_virtual.findall(*args, **kwargs)
114
115 def iter(self, *args, **kwargs):
116 """
117 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.iter
118 """
119 return self._virtual_virtual.iter(*args, **kwargs)
120
121 def items(self, *args, **kwargs):
122 """
123 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.items
124 """
125 return self._virtual_virtual.items(*args, **kwargs)
126
127 # WRITE METHODS
128
129 @updateVarList
130 def clear(self):
131 """
132 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.clear
133 """
134 for item in self:
135 self.remove(item)
136 self.texttext = None
137 self.tailtail = None
138
139 def append(self, *args, **kwargs):
140 """
141 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.append
142 """
143 # Append after the 'END SUBROUTINE' statement
144 return self._xml.append(*args, **kwargs)
145
146 def extend(self, *args, **kwargs):
147 """
148 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.extend
149 """
150 # Extend after the 'END SUBROUTINE' statement
151 return self._xml.extend(*args, **kwargs)
152
153 def _getIndex(self, index):
154 """
155 :param index: index in the virtual node
156 :return: index in the _xml node
157 """
158 if not self._excludeContains:
159 return index
160 contains = self._xml.find('./{*}contains-stmt')
161 if contains is None:
162 return index
163 indexContains = list(self._xml).index(contains)
164 # Checks
165 if index > indexContains or index < -indexContains - 1:
166 raise IndexError('list index out of range')
167 # Converts negative index into positive index
168 if index == -1:
169 # END SUBROUTINE
170 return index
171 if index < -1:
172 index = indexContains + index + 1
173
174 return len(self._xml) if index == indexContains else index
175
176 def __setitem__(self, index, item):
177 return self._xml.__setitem__(self._getIndex(index), item)
178
179 @updateVarList
180 def __delitem__(self, index):
181 return self._xml.__delitem__(self._getIndex(index))
182
183 def insert(self, index, item):
184 """
185 https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.insert
186 """
187 return self._xml.insert(0 if index == 0 else (self._getIndex(index - 1) + 1), item)
188
189 @updateVarList
190 def remove(self, node):
191 """
192 Remove node from the xml
193 """
194 if isinstance(node, ElementView):
195 node = node._xml # pylint: disable=protected-access
196 self.getParent(node).remove(node)
197
198
200 """
201 This class wrapps the xml node representing a FORTRAN scope
202 """
203 SCOPE_STMT = {'module': 'module-stmt',
204 'func': 'function-stmt',
205 'sub': 'subroutine-stmt',
206 'type': 'T-stmt',
207 'prog': 'program-stmt',
208 'interface': 'interface-stmt',
209 'submodule': 'submodule-stmt'}
210 SCOPE_CONSTRUCT = {'module': 'program-unit',
211 'func': 'program-unit',
212 'sub': 'program-unit',
213 'type': 'T-construct',
214 'prog': 'program-unit',
215 'interface': 'interface-construct',
216 'submodule': 'program-unit'}
217
218 def __init__(self, xml, scopePath='/', parentScope=None,
219 enableCache=False, tree=None, excludeContains=False):
220 """
221 :param xml: xml corresponding to this PYFTscope
222 :param scopePath: scope path ('/' separated string) of this node
223 :param parentScope: parent PYFTscope instance
224 :param enableCache: True to cache node parents
225 :param tree: an optional Tree instance
226 :param excludeContains: do not take into account the CONTAINS part
227 """
228 super().__init__(xml=xml, excludeContains=excludeContains)
229 self._mainScope = self if parentScope is None else parentScope._mainScope
230 self._path = scopePath
231 self._parentScope = parentScope
232 self.tree = Tree() if tree is None else tree
233 self._cacheParent = {}
234
235 if enableCache and parentScope is None:
236 # parent cache associated to the main scope
237 for node in self.iter():
238 for subNode in node:
239 self._cacheParent[id(subNode)] = node
240
241 def __copy__(self):
242 cls = self.__class__
243 result = cls.__new__(cls)
244 result.__dict__.update(self.__dict__)
245 return result
246
247 def __deepcopy__(self, memo):
248 cls = self.__class__
249 result = cls.__new__(cls)
250 memo[id(self)] = result
251 for key, val in self.__dict__.items():
252 setattr(result, key, copy.deepcopy(val, memo))
253 return result
254
255 def __getattr__(self, attr):
256 """
257 Get some attributes defined in the PYFT class
258 """
259 if attr in ('SHARED_TREE', 'NO_PARALLEL_LOCK', 'PARALLEL_FILE_LOCKS '):
260 return getattr(self._parentScope, attr)
261 raise AttributeError(f"{attr} doesn't exist")
262
263 @property
264 def path(self):
265 """
266 Return the current path
267 """
268 return self._path
269
270 @property
271 def mainScope(self):
272 """
273 Return the main scope (the upper scope in the file)
274 """
275 return self._mainScope
276
277 @property
278 def parentScope(self):
279 """
280 Return the parent of the current scope
281 """
282 return self._parentScope
283
284 # No @debugDecor for this low-level method
285 def getParent(self, item, level=1):
286 """
287 :param item: item whose parent is to be searched
288 :param level: number of degrees (1 to get the parent, 2 to get
289 the parent of the parent...)
290 """
291 # pylint: disable=protected-access
292 def check(node):
293 # We check if the registered parent is still the right one
294 # node must be in its parent, and the parent chain must go to the root node
295 return node in self.mainScope._cacheParent.get(id(node), []) and \
296 (self.mainScope._cacheParent[id(node)] == self.mainScope._xml or
297 check(self.mainScope._cacheParent[id(node)]))
298
299 assert level >= 1
300 parent = None
301 if check(item):
302 parent = self.mainScope._cacheParent[id(item)]
303 else:
304 for node in self.mainScope.iter():
305 if item in list(node):
306 parent = node
307 if self.mainScope._cacheParent:
308 # _cacheParent not empty means that we want to use the caching system
309 self.mainScope._cacheParent[id(item)] = node
310 break
311 return parent if level == 1 else self.getParent(parent, level - 1)
312
313 def getSiblings(self, item, before=True, after=True):
314 """
315 :param item: item whose siblings are to be searched
316 :param before: returns siblings before
317 :param after: returns siblings after
318 By default before and after are True so that all siblings are returned
319 """
320
321 siblings = self.getParent(item).findall('./{*}*')
322 if not after:
323 siblings = siblings[:siblings.index(item)]
324 if not before:
325 siblings = siblings[siblings.index(item) + 1:]
326 return [s for s in siblings if s != item]
327
328 # No @debugDecor for this low-level method
329 @staticmethod
330 def _getNodeName(node):
331 """
332 Internal methode to compute the name of a scope
333 :param node: program-unit node
334 :return: name
335 """
336 if tag(node) == 'T-stmt':
337 # To not capture the extension name in "TYPE, EXTENDS(FOO) :: FOO2"
338 nodeN = node.find('./{*}T-N/{*}N')
339 elif tag(node) == 'submodule-stmt':
340 nodeN = node.find('./{*}submodule-module-N')
341 else:
342 nodeN = node.find('.//{*}N')
343 if nodeN is not None and nodeN.find('.//{*}N') is not None:
344 # As of 7 Jul 2023, this is the case for interface statements
345 nodeN = nodeN.find('.//{*}N')
346 if nodeN is not None:
347 name = n2name(nodeN).upper()
348 else:
349 name = '--UNKNOWN--'
350 return name
351
352 # No @debugDecor for this low-level method
353 def _getNodePath(self, node):
354 """
355 Internal methode to compute a path part from a node
356 :param node: program-unit node
357 :return: path part (e.g. module:MODU)
358 """
359 stmt = tag(node[0])
360 name = self._getNodeName(node[0])
361 return {v: k for (k, v) in self.SCOPE_STMT.items()}[stmt] + ':' + name
362
363 # No @debugDecor for this low-level method
364 @staticmethod
365 def normalizeScope(scopePath):
366 """
367 Method to normalize a scope path
368 """
369 return '/'.join([(k.lower() + ':' + w.upper())
370 for (k, w) in [component.split(':')
371 for component in scopePath.split('/')]])
372
373 @debugDecor
374 def showScopesList(self, includeItself=False):
375 """
376 Shows the list of scopes found in the source code
377 :param includeItself: include itself if self represent a "valid" scope (not a file)
378 """
379 print("These scopes have been found in the source code:")
380 print("\n".join([' - ' + scope.path
381 for scope in self.getScopes(includeItself=includeItself)]))
382
383 @debugDecor
384 def getScopes(self, level=-1, excludeContains=True, excludeKinds=None, includeItself=True):
385 """
386 :param level: -1 to get all child scopes
387 1 to get only direct child scopes
388 2 to get direct and direct of direct ...
389 :param excludeContains: if True, each PYFTscope which is a module, function or subroutine
390 that contain (after a 'CONTAINS' statement) other subroutines or
391 functions, those contained subroutines or functions are excluded
392 from the result; but the PYFTscope contains the 'END' statement
393 of the module/subroutine or function.
394 :param excludeKinds: if not None, is a list of scope kinds to exclude
395 :param includeItself: include itself if self represent a "valid" scope (not a file)
396 :return: list of PYFTscope found in the current scope
397 """
398 assert level == -1 or level > 0, 'level must be -1 or a positive int'
399
400 def _getRecur(node, level, basePath=''):
401 # If node is the entire xml
402 if tag(node) == 'object':
403 usenode = node.find('./{*}file')
404 else:
405 usenode = node
406 results = []
407 for child in [child for child in usenode
408 if tag(child) in self.SCOPE_CONSTRUCT.values()]:
409 nodePath = self._getNodePath(child)
410 if excludeKinds is None or nodePath.split(':')[0] not in excludeKinds:
411 scopePath = nodePath if basePath in ('', '/') \
412 else basePath + '/' + nodePath
413 results.append(PYFTscope(child,
414 scopePath=scopePath, parentScope=self,
415 enableCache=False, tree=self.tree,
416 excludeContains=excludeContains))
417 if level != 1:
418 results.extend(_getRecur(child, level - 1, scopePath))
419 return results
420
421 if includeItself and tag(self) in self.SCOPE_CONSTRUCT.values():
422 itself = [self]
423 else:
424 itself = []
425
426 return _getRecur(self, level, self.pathpathpath) + itself
427
428 @debugDecor
429 def getScopeNode(self, scopePath, excludeContains=True, includeItself=True):
430 """
431 :param scopePath: scope path to search for
432 :param excludeContains: see getScopes
433 :param includeItself: include itself if self represent a "valid" scope (not a file)
434 :return: PYFTscope whose path is the path asked for
435 """
436 scope = [scope for scope in self.getScopes(excludeContains=excludeContains,
437 includeItself=includeItself)
438 if scope.path == scopePath]
439 if len(scope) == 0:
440 raise PYFTError(f'{scopePath} not found')
441 if len(scope) > 1:
442 raise PYFTError(f'{scopePath} found several times')
443 return scope[0]
444
445 @debugDecor
446 def isScopeNode(self, node):
447 """
448 :param node: node to test
449 :return: True if node is a scope node (construct node around a
450 module, subroutine, function or type declaration)
451 """
452 return tag(node) in self.SCOPE_CONSTRUCT.values()
453
454 @debugDecor
455 def getParentScopeNode(self, item, mustRaise=True):
456 """
457 :param item: item whose scope parent is to be searched
458 :param mustRaise: True to raise an exception if parent is not found
459 :return: the scope parent node of item
460 Example: if item is a call statement, result is the program-unit node
461 in which the call statement is
462 """
463 result = self.getParent(item)
464 while result is not None and not self.isScopeNode(result):
465 result = self.getParent(result)
466 if result is None and mustRaise:
467 raise PYFTError("The scope parent has not been found.")
468 return result
469
470 @debugDecor
471 def getScopePath(self, item, includeItself=True):
472 """
473 :param item: item whose path must be determined
474 :param includeItself: include the item if it is a scope node
475 :return: the full path of the structure containing item
476 """
477 if includeItself and self.isScopeNode(item):
478 result = [self._getNodePath(item)]
479 else:
480 result = []
481 item = self.getParentScopeNode(item, mustRaise=False)
482 while item is not None:
483 result = [self._getNodePath(item)] + result
484 item = self.getParentScopeNode(item, mustRaise=False)
485 return '/'.join(result)
486
487 def getFileName(self):
488 """
489 :return: the name of the input file name or 'unknown' if not available
490 in the xml fragment provided
491 """
492 return os.path.normpath(self.mainScope.find('.//{*}file').attrib['name'])
493
494 @updateTree()
495 @updateVarList
496 def empty(self, addStmt=None, simplify=False):
497 """
498 Empties the scope by removing all statements except dummy arguments declaration
499 and USE statements (because they can be useful for the dummy argument declarations).
500 :param addStmt: add this statement in the body of the emptied scopes
501 :param simplify: try to simplify code
502 """
503 scopes = [] # list of scopes to empty
504 for scope in self.getScopes(level=1, excludeContains=False):
505 if scope.path.split('/')[-1].split(':')[0] == 'module':
506 scopes.extend(scope.getScopes(level=1, excludeContains=False, includeItself=False))
507 else:
508 scopes.append(scope)
509 tagExcluded = (list(self.SCOPE_STMT.values()) +
510 ['end-' + decl for decl in self.SCOPE_STMT.values()] +
511 ['T-decl-stmt', 'use-stmt', 'C'])
512 for scope in scopes:
513 for node in list(scope):
514 if tag(node) not in tagExcluded:
515 scope.remove(node)
516 scope.removeUnusedLocalVar(simplify=simplify)
517 if addStmt is not None:
518 if isinstance(addStmt, str):
519 addStmt = createExpr(addStmt)
520 elif not isinstance(addStmt, list):
521 addStmt = [addStmt]
522 for stmt in addStmt:
523 scope.insertStatement(stmt, False)
524 if simplify:
525 # Apllied on self (and not scope) to remove lines before the first scope
526 # in case self represents an entire file
527 self.removeComments()
528 self.removeEmptyLines()
removeComments(self, exclDirectives=None, pattern=None)
Definition cosmetics.py:166
iterfind(self, *args, **kwargs)
Definition scope.py:82
iter(self, *args, **kwargs)
Definition scope.py:115
itertext(self, *args, **kwargs)
Definition scope.py:88
findall(self, *args, **kwargs)
Definition scope.py:109
find(self, *args, **kwargs)
Definition scope.py:103
append(self, *args, **kwargs)
Definition scope.py:139
__init__(self, xml, excludeContains=False)
Definition scope.py:26
_getIndex(self, index)
Definition scope.py:153
extend(self, *args, **kwargs)
Definition scope.py:146
insert(self, index, item)
Definition scope.py:183
findtext(self, *args, **kwargs)
Definition scope.py:76
items(self, *args, **kwargs)
Definition scope.py:121
getScopeNode(self, scopePath, excludeContains=True, includeItself=True)
Definition scope.py:429
empty(self, addStmt=None, simplify=False)
Definition scope.py:496
__init__(self, xml, scopePath='/', parentScope=None, enableCache=False, tree=None, excludeContains=False)
Definition scope.py:219
getScopes(self, level=-1, excludeContains=True, excludeKinds=None, includeItself=True)
Definition scope.py:384
getScopePath(self, item, includeItself=True)
Definition scope.py:471
isScopeNode(self, node)
Definition scope.py:446
normalizeScope(scopePath)
Definition scope.py:365
getParent(self, item, level=1)
Definition scope.py:285
getSiblings(self, item, before=True, after=True)
Definition scope.py:313
_getNodePath(self, node)
Definition scope.py:353
getParentScopeNode(self, item, mustRaise=True)
Definition scope.py:455
showScopesList(self, includeItself=False)
Definition scope.py:374
__getattr__(self, attr)
Definition scope.py:255