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 SCOPE_CONSTRUCT = {'module': 'program-unit',
210 'func': 'program-unit',
211 'sub': 'program-unit',
212 'type': 'T-construct',
213 'prog': 'program-unit',
214 'interface': 'interface-construct'}
215
216 def __init__(self, xml, scopePath='/', parentScope=None,
217 enableCache=False, tree=None, excludeContains=False):
218 """
219 :param xml: xml corresponding to this PYFTscope
220 :param scopePath: scope path ('/' separated string) of this node
221 :param parentScope: parent PYFTscope instance
222 :param enableCache: True to cache node parents
223 :param tree: an optional Tree instance
224 :param excludeContains: do not take into account the CONTAINS part
225 """
226 super().__init__(xml=xml, excludeContains=excludeContains)
227 self._mainScope = self if parentScope is None else parentScope._mainScope
228 self._path = scopePath
229 self._parentScope = parentScope
230 self.tree = Tree() if tree is None else tree
231 self._cacheParent = {}
232
233 if enableCache and parentScope is None:
234 # parent cache associated to the main scope
235 for node in self.iter():
236 for subNode in node:
237 self._cacheParent[id(subNode)] = node
238
239 def __copy__(self):
240 cls = self.__class__
241 result = cls.__new__(cls)
242 result.__dict__.update(self.__dict__)
243 return result
244
245 def __deepcopy__(self, memo):
246 cls = self.__class__
247 result = cls.__new__(cls)
248 memo[id(self)] = result
249 for key, val in self.__dict__.items():
250 setattr(result, key, copy.deepcopy(val, memo))
251 return result
252
253 def __getattr__(self, attr):
254 """
255 Get some attributes defined in the PYFT class
256 """
257 if attr in ('SHARED_TREE', 'NO_PARALLEL_LOCK', 'PARALLEL_FILE_LOCKS '):
258 return getattr(self._parentScope, attr)
259 raise AttributeError(f"{attr} doesn't exist")
260
261 @property
262 def path(self):
263 """
264 Return the current path
265 """
266 return self._path
267
268 @property
269 def mainScope(self):
270 """
271 Return the main scope (the upper scope in the file)
272 """
273 return self._mainScope
274
275 @property
276 def parentScope(self):
277 """
278 Return the parent of the current scope
279 """
280 return self._parentScope
281
282 # No @debugDecor for this low-level method
283 def getParent(self, item, level=1):
284 """
285 :param item: item whose parent is to be searched
286 :param level: number of degrees (1 to get the parent, 2 to get
287 the parent of the parent...)
288 """
289 # pylint: disable=protected-access
290 def check(node):
291 # We check if the registered parent is still the right one
292 # node must be in its parent, and the parent chain must go to the root node
293 return node in self.mainScope._cacheParent.get(id(node), []) and \
294 (self.mainScope._cacheParent[id(node)] == self.mainScope._xml or
295 check(self.mainScope._cacheParent[id(node)]))
296
297 assert level >= 1
298 parent = None
299 if check(item):
300 parent = self.mainScope._cacheParent[id(item)]
301 else:
302 for node in self.mainScope.iter():
303 if item in list(node):
304 parent = node
305 if self.mainScope._cacheParent:
306 # _cacheParent not empty means that we want to use the caching system
307 self.mainScope._cacheParent[id(item)] = node
308 break
309 return parent if level == 1 else self.getParent(parent, level - 1)
310
311 def getSiblings(self, item, before=True, after=True):
312 """
313 :param item: item whose siblings are to be searched
314 :param before: returns siblings before
315 :param after: returns siblings after
316 By default before and after are True so that all siblings are returned
317 """
318
319 siblings = self.getParent(item).findall('./{*}*')
320 if not after:
321 siblings = siblings[:siblings.index(item)]
322 if not before:
323 siblings = siblings[siblings.index(item) + 1:]
324 return [s for s in siblings if s != item]
325
326 # No @debugDecor for this low-level method
327 @staticmethod
328 def _getNodeName(node):
329 """
330 Internal methode to compute the name of a scope
331 :param node: program-unit node
332 :return: name
333 """
334 # If there was no interface bloc, code could be n2name(node[0].find('.//{*}N'))
335 # But (as of 7 Jul 2023), interface have two nested N
336 nodeN = node.find('.//{*}N')
337 if nodeN is not None and nodeN.find('.//{*}N') is not None:
338 # As of 7 Jul 2023, this is the case for interface statements
339 nodeN = nodeN.find('.//{*}N')
340 if nodeN is not None:
341 name = n2name(nodeN).upper()
342 else:
343 name = '--UNKNOWN--'
344 return name
345
346 # No @debugDecor for this low-level method
347 def _getNodePath(self, node):
348 """
349 Internal methode to compute a path part from a node
350 :param node: program-unit node
351 :return: path part (e.g. module:MODU)
352 """
353 stmt = tag(node[0])
354 name = self._getNodeName(node[0])
355 return {v: k for (k, v) in self.SCOPE_STMT.items()}[stmt] + ':' + name
356
357 # No @debugDecor for this low-level method
358 @staticmethod
359 def normalizeScope(scopePath):
360 """
361 Method to normalize a scope path
362 """
363 return '/'.join([(k.lower() + ':' + w.upper())
364 for (k, w) in [component.split(':')
365 for component in scopePath.split('/')]])
366
367 @debugDecor
368 def showScopesList(self, includeItself=False):
369 """
370 Shows the list of scopes found in the source code
371 :param includeItself: include itself if self represent a "valid" scope (not a file)
372 """
373 print("These scopes have been found in the source code:")
374 print("\n".join([' - ' + scope.path
375 for scope in self.getScopes(includeItself=includeItself)]))
376
377 @debugDecor
378 def getScopes(self, level=-1, excludeContains=True, excludeKinds=None, includeItself=True):
379 """
380 :param level: -1 to get all child scopes
381 1 to get only direct child scopes
382 2 to get direct and direct of direct ...
383 :param excludeContains: if True, each PYFTscope which is a module, function or subroutine
384 that contain (after a 'CONTAINS' statement) other subroutines or
385 functions, those contained subroutines or functions are excluded
386 from the result; but the PYFTscope contains the 'END' statement
387 of the module/subroutine or function.
388 :param excludeKinds: if not None, is a list of scope kinds to exclude
389 :param includeItself: include itself if self represent a "valid" scope (not a file)
390 :return: list of PYFTscope found in the current scope
391 """
392 assert level == -1 or level > 0, 'level must be -1 or a positive int'
393
394 def _getRecur(node, level, basePath=''):
395 # If node is the entire xml
396 if tag(node) == 'object':
397 usenode = node.find('./{*}file')
398 else:
399 usenode = node
400 results = []
401 for child in [child for child in usenode
402 if tag(child) in self.SCOPE_CONSTRUCT.values()]:
403 nodePath = self._getNodePath(child)
404 if excludeKinds is None or nodePath.split(':')[0] not in excludeKinds:
405 scopePath = nodePath if basePath in ('', '/') \
406 else basePath + '/' + nodePath
407 results.append(PYFTscope(child,
408 scopePath=scopePath, parentScope=self,
409 enableCache=False, tree=self.tree,
410 excludeContains=excludeContains))
411 if level != 1:
412 results.extend(_getRecur(child, level - 1, scopePath))
413 return results
414
415 if includeItself and tag(self) in self.SCOPE_CONSTRUCT.values():
416 itself = [self]
417 else:
418 itself = []
419
420 return _getRecur(self, level, self.pathpathpath) + itself
421
422 @debugDecor
423 def getScopeNode(self, scopePath, excludeContains=True, includeItself=True):
424 """
425 :param scopePath: scope path to search for
426 :param excludeContains: see getScopes
427 :param includeItself: include itself if self represent a "valid" scope (not a file)
428 :return: PYFTscope whose path is the path asked for
429 """
430 scope = [scope for scope in self.getScopes(excludeContains=excludeContains,
431 includeItself=includeItself)
432 if scope.path == scopePath]
433 if len(scope) == 0:
434 raise PYFTError(f'{scopePath} not found')
435 if len(scope) > 1:
436 raise PYFTError(f'{scopePath} found several times')
437 return scope[0]
438
439 @debugDecor
440 def isScopeNode(self, node):
441 """
442 :param node: node to test
443 :return: True if node is a scope node (construct node around a
444 module, subroutine, function or type declaration)
445 """
446 return tag(node) in self.SCOPE_CONSTRUCT.values()
447
448 @debugDecor
449 def getParentScopeNode(self, item, mustRaise=True):
450 """
451 :param item: item whose scope parent is to be searched
452 :param mustRaise: True to raise an exception if parent is not found
453 :return: the scope parent node of item
454 Example: if item is a call statement, result is the program-unit node
455 in which the call statement is
456 """
457 result = self.getParent(item)
458 while result is not None and not self.isScopeNode(result):
459 result = self.getParent(result)
460 if result is None and mustRaise:
461 raise PYFTError("The scope parent has not been found.")
462 return result
463
464 @debugDecor
465 def getScopePath(self, item, includeItself=True):
466 """
467 :param item: item whose path must be determined
468 :param includeItself: include the item if it is a scope node
469 :return: the full path of the structure containing item
470 """
471 if includeItself and self.isScopeNode(item):
472 result = [self._getNodePath(item)]
473 else:
474 result = []
475 item = self.getParentScopeNode(item, mustRaise=False)
476 while item is not None:
477 result = [self._getNodePath(item)] + result
478 item = self.getParentScopeNode(item, mustRaise=False)
479 return '/'.join(result)
480
481 def getFileName(self):
482 """
483 :return: the name of the input file name or 'unknown' if not available
484 in the xml fragment provided
485 """
486 return os.path.normpath(self.mainScope.find('.//{*}file').attrib['name'])
487
488 @updateTree()
489 @updateVarList
490 def empty(self, addStmt=None, simplify=False):
491 """
492 Empties the scope by removing all statements except dummy arguments declaration
493 and USE statements (because they can be useful for the dummy argument declarations).
494 :param addStmt: add this statement in the body of the emptied scopes
495 :param simplify: try to simplify code
496 """
497 scopes = [] # list of scopes to empty
498 for scope in self.getScopes(level=1, excludeContains=False):
499 if scope.path.split('/')[-1].split(':')[0] == 'module':
500 scopes.extend(scope.getScopes(level=1, excludeContains=False, includeItself=False))
501 else:
502 scopes.append(scope)
503 tagExcluded = (list(self.SCOPE_STMT.values()) +
504 ['end-' + decl for decl in self.SCOPE_STMT.values()] +
505 ['T-decl-stmt', 'use-stmt', 'C'])
506 for scope in scopes:
507 for node in list(scope):
508 if tag(node) not in tagExcluded:
509 scope.remove(node)
510 scope.removeUnusedLocalVar(simplify=simplify)
511 if addStmt is not None:
512 if isinstance(addStmt, str):
513 addStmt = createExpr(addStmt)
514 elif not isinstance(addStmt, list):
515 addStmt = [addStmt]
516 for stmt in addStmt:
517 scope.insertStatement(stmt, False)
518 if simplify:
519 # Apllied on self (and not scope) to remove lines before the first scope
520 # in case self represents an entire file
521 self.removeComments()
522 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:423
empty(self, addStmt=None, simplify=False)
Definition scope.py:490
__init__(self, xml, scopePath='/', parentScope=None, enableCache=False, tree=None, excludeContains=False)
Definition scope.py:217
getScopes(self, level=-1, excludeContains=True, excludeKinds=None, includeItself=True)
Definition scope.py:378
getScopePath(self, item, includeItself=True)
Definition scope.py:465
isScopeNode(self, node)
Definition scope.py:440
normalizeScope(scopePath)
Definition scope.py:359
getParent(self, item, level=1)
Definition scope.py:283
getSiblings(self, item, before=True, after=True)
Definition scope.py:311
_getNodePath(self, node)
Definition scope.py:347
getParentScopeNode(self, item, mustRaise=True)
Definition scope.py:449
showScopesList(self, includeItself=False)
Definition scope.py:368
__getattr__(self, attr)
Definition scope.py:253