PyForTool
Python-fortran-tool
Loading...
Searching...
No Matches
util.py
1"""
2This module implements some tools to manipulate the xml
3"""
4
5import xml.etree.ElementTree as ET
6from functools import wraps
7import logging
8import tempfile
9import os
10import subprocess
11import time
12import re
13
14from pyfortool import NAMESPACE
15
16
18
19debugStats = {}
20
21
22def debugDecor(func):
23 """
24 Defines a decorator to trace all function calling with arguments and results
25 and count number of calls and time spent
26 """
27 @wraps(func)
28 def wrapper(*args, **kwargs):
29 # Logging call
30 logger = logging.getLogger()
31 if logger.isEnabledFor(logging.DEBUG):
32 callstr = func.__name__ + \
33 '(' + ', '.join([str(a) for a in args] +
34 [k + '=' + str(v) for (k, v) in kwargs.items()]) + ')'
35 logging.debug('%s --> ...', callstr)
36 else:
37 callstr = None
38
39 # Count and time
40 if logger.isEnabledFor(logging.INFO):
41 t0 = time.time()
42 else:
43 t0 = None
44
45 # effective call
46 result = func(*args, **kwargs)
47
48 # Count and time
49 if t0 is not None:
50 # We test with t0 instead of the log level in case level evolved during func call
51 if func.__name__ not in debugStats:
52 debugStats[func.__name__] = dict(nb=0, totalTime=0)
53 debugStats[func.__name__]['nb'] += 1
54 duration = time.time() - t0
55 debugStats[func.__name__]['totalTime'] += duration
56 debugStats[func.__name__]['min'] = \
57 min(duration, debugStats[func.__name__].get('min', duration))
58 debugStats[func.__name__]['max'] = \
59 max(duration, debugStats[func.__name__].get('max', duration))
60
61 # logging result
62 if callstr is not None:
63 # We test with callstr instead of the log level in case level evolved during func call
64 logging.debug('%s --> %s', callstr, str(result))
65
66 return result
67 return wrapper
68
69
70def noParallel(func):
71 """
72 Defines a decorator that prevent this method to be executed in parallel on several files
73 """
74 @wraps(func)
75 def wrapper(self, *args, **kwargs):
76 if self.NO_PARALLEL_LOCK is not None:
77 # Acquire lock, if any
78 with self.NO_PARALLEL_LOCK:
79 # We cannot use directly the shared version because the update
80 # tries to send a whole PYFT object to the Manager and there are
81 # issues with that (at least about the locks attached to the instance)
82 # The code here allows to synchronize when there is a lock mechanism.
83 # All the routines updating the tree must be decorated by noParallel
84 if self.SHARED_TREE is not None:
85 self.tree.copyFromOtherTree(self.SHARED_TREE)
86 result = func(self, *args, **kwargs)
87 if self.SHARED_TREE is not None:
88 self.tree.copyToOtherTree(self.SHARED_TREE)
89 return result
90 else:
91 return func(self, *args, **kwargs)
92 return wrapper
93
94
95def setVerbosity(level):
96 """
97 Set the verbosity level
98 :param level: verbosity level used to set the logging module
99 """
100 logger = logging.getLogger()
101 if isinstance(level, str):
102 logger.setLevel(level=level.upper())
103 else:
104 logger.setLevel(level=level)
105
106
108 """
109 Print statistics on methods and function usage
110 """
111 logger = logging.getLogger()
112 if logger.isEnabledFor(logging.INFO):
113 def _print(name, nb, vmin, vmax, mean):
114 print('| ' + name.ljust(30) + '| ' + str(nb).ljust(14) + '| ' +
115 str(vmin).ljust(23) + '| ' + str(vmax).ljust(23) + '| ' +
116 str(mean).ljust(23) + '|')
117 _print('Name of the function', '# of calls', 'Min (s)', 'Max (s)', 'Total (s)')
118 for funcName, values in debugStats.items():
119 _print(funcName, values['nb'], values['min'], values['max'], values['totalTime'])
120
121
122class PYFTError(Exception):
123 """
124 Exceptions for PYFT
125 """
126
127
129
130
131def fortran2xml(fortranSource, parser='fxtran', parserOptions=None, wrapH=False):
132 """
133 :param fortranSource: a string containing a fortran source code
134 or a filename
135 :param parser: path to the fxtran parser
136 :param parserOptions: dictionnary holding the parser options
137 :param wrapH: if True, content of .h file is put in a .F90 file (to force
138 fxtran to recognize it as free form) inside a module (to
139 enable the reading of files containing only a code part)
140 :returns: (includesRemoved, xml) where includesRemoved indicates if an include
141 was replaced by fxtran and xml is an ET xml document
142 """
143 # Namespace registration
144 ET.register_namespace('f', NAMESPACE)
145
146 # Default options
147 if parserOptions is None:
148 import pyfortool
149 parserOptions = pyfortool.PYFT.DEFAULT_FXTRAN_OPTIONS
150
151 # Call to fxtran
152 renamed = False
153 moduleAdded = False
154 with tempfile.NamedTemporaryFile(buffering=0, suffix='.F90') as file:
155 if os.path.exists(fortranSource):
156 # tempfile not needed in this case if wrapH is False but I found easier to write code
157 # like this to have only one subprocess call and automatic
158 # deletion of the temporary file
159 filename = fortranSource
160 if wrapH and filename.endswith('.h'):
161 renamed = True
162 filename = file.name
163 with open(fortranSource, 'r', encoding='utf-8') as src:
164 content = src.read()
165 # renaming is enough for .h files containing SUBROUTINE or FUNCTION
166 # but if the file contains a code fragment, it must be included in a
167 # program-unit
168 firstLine = [line for line in content.split('\n')
169 if not (re.search(r'^[\t ]*!', line) or
170 re.search(r'^[\t ]*$', line))][0]
171 fisrtLine = firstLine.upper().split()
172 if not ('SUBROUTINE' in fisrtLine or 'FUNCTION' in firstLine):
173 # Needs to be wrapped in a program-unit
174 moduleAdded = True
175 content = 'MODULE FOO\n' + content + '\nEND MODULE FOO'
176 file.write(content.encode('UTF8'))
177 else:
178 filename = file.name
179 file.write(fortranSource.encode('UTF-8'))
180 xml = subprocess.run([parser, filename,
181 '-o', '-'] + parserOptions,
182 stdout=subprocess.PIPE, check=True,
183 encoding='UTF-8').stdout
184 xml = ET.fromstring(xml, parser=ET.XMLParser(encoding='UTF-8'))
185 if renamed:
186 xml.find('./{*}file').attrib['name'] = fortranSource
187 if moduleAdded:
188 file = xml.find('./{*}file')
189 programUnit = file.find('./{*}program-unit')
190 # all nodes inside program-unit except 'MODULE' and 'END MODULE'
191 for node in programUnit[1:-1]:
192 file.append(node)
193 # pylint: disable-next=undefined-loop-variable
194 node.tail = node.tail[:-1] # remove '\n' added before 'END MODULE'
195 file.remove(programUnit)
196
197 includesDone = False
198 if len(set(['-no-include', '-noinclude']).intersection(parserOptions)) == 0:
199 # fxtran has included the files but:
200 # - it doesn't have removed the INCLUDE "file.h" statement
201 # - it included the file with its file node
202 # This code section removes the INCLUDE statement and the file node
203
204 # Remove the include statement
205 includeStmts = xml.findall('.//{*}include')
206 for includeStmt in includeStmts:
207 par = [p for p in xml.iter() if includeStmt in p][0]
208 par.remove(includeStmt)
209 includesDone = True
210 # Remove the file node
211 mainfile = xml.find('./{*}file')
212 for file in mainfile.findall('.//{*}file'):
213 par = [p for p in xml.iter() if file in p][0]
214 index = list(par).index(file)
215 if file.tail is not None:
216 file[-1].tail = file.tail if file[-1].tail is None else (file[-1].tail + file.tail)
217 for node in file[::-1]:
218 par.insert(index, node)
219 par.remove(file)
220
221 return includesDone, xml
222
223
224def tostring(doc):
225 """
226 :param doc: an ET object
227 :return: xml as a string
228 """
229 return ET.tostring(doc, method='xml', encoding='UTF-8').decode('UTF-8')
230
231
232def tofortran(doc):
233 """
234 :param doc: an ET object
235 :return: a string representing the FORTRAN source code
236 """
237 # When fxtran encounters an UTF-8 character, it replaces it by *2* entities
238 # We must first transform each of these entities to its corresponding binary value
239 # (this is done by tostring), then we must consider the result as bytes
240 # to decode these two bytes into UTF-8 (this is done by encode('raw_...').decode('UTF-8'))
241 result = ET.tostring(doc, method='text', encoding='UTF-8').decode('UTF-8')
242 try:
243 result = result.encode('raw_unicode_escape').decode('UTF-8')
244 except UnicodeDecodeError:
245 filename = doc.find('.//{*}file').attrib['name']
246 logging.warning("The file '%s' certainly contains a strange character", filename)
247 return result
248
249
251
252
253def isint(string):
254 """
255 :param string: string to test for intergerness
256 :return: True if s represent an int
257 """
258 try:
259 int(string)
260 except ValueError:
261 return False
262 else:
263 return True
264
265
266def isfloat(string):
267 """
268 :param string: string to test for intergerness
269 :return: True if s represent a real
270 """
271 try:
272 float(string)
273 except ValueError:
274 return False
275 else:
276 return True
277
278
280
281
282def tag(elem):
283 """
284 :param elem: ET Element
285 :return: the tag without the namespace
286 """
287 return elem.tag.split('}')[1]
288
289
290def n2name(nodeN):
291 """
292 Helper function which returns the entity name enclosed in a N tag
293 """
294 return ''.join([e.text for e in nodeN.findall('./{*}n')])
295
296
297def alltext(doc):
298 """
299 Helper function to iterate on all text fragment and join them
300 :param doc: xml fragment
301 """
302 return ''.join(doc.itertext())
303
304
305def nonCode(elem):
306 """
307 :param e: element
308 :return: True if e is non code (comment, text...)
309 """
310 return tag(elem) in {'cnt', 'C', 'cpp'}
311
312
313def isExecutable(elem):
314 """
315 :param e: element
316 :return: True if element is executable
317 """
318 return ((isStmt(elem) or isConstruct(elem)) and
319 tag(elem) not in ('subroutine-stmt', 'end-subroutine-stmt',
320 'function-stmt', 'end-function-stmt',
321 'use-stmt', 'T-decl-stmt', 'component-decl-stmt',
322 'T-stmt', 'end-T-stmt',
323 'data-stmt', 'save-stmt',
324 'implicit-none-stmt'))
325
326
327def isConstruct(elem):
328 """
329 :param elem: element
330 :return: True if element is a construct
331 """
332 return tag(elem).endswith('-construct')
333
334
335def isStmt(elem):
336 """
337 :param elem: element
338 :return: True if element is a statement
339 """
340 return tag(elem).endswith('-stmt')
isConstruct(elem)
Definition util.py:327