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 time
11import re
12import pyfxtran
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, parserOptions=None, wrapH=False):
132 """
133 :param fortranSource: a string containing a fortran source code
134 or a filename
135 :param parserOptions: dictionnary holding the parser options
136 :param wrapH: if True, content of .h file is put in a .F90 file (to force
137 fxtran to recognize it as free form) inside a module (to
138 enable the reading of files containing only a code part)
139 :returns: (includesRemoved, xml) where includesRemoved indicates if an include
140 was replaced by fxtran and xml is an ET xml document
141 """
142 # Namespace registration
143 ET.register_namespace('f', NAMESPACE)
144
145 # Default options
146 if parserOptions is None:
147 import pyfortool
148 parserOptions = pyfortool.PYFT.DEFAULT_FXTRAN_OPTIONS
149
150 # Call to fxtran
151 renamed = False
152 moduleAdded = False
153 with tempfile.NamedTemporaryFile(buffering=0, suffix='.F90') as file:
154 if os.path.exists(fortranSource):
155 # tempfile not needed in this case if wrapH is False but I found easier to write code
156 # like this to have only one pyfxtran use and automatic
157 # deletion of the temporary file
158 filename = fortranSource
159 if wrapH and filename.endswith('.h'):
160 renamed = True
161 filename = file.name
162 with open(fortranSource, 'r', encoding='utf-8') as src:
163 content = src.read()
164 # renaming is enough for .h files containing SUBROUTINE or FUNCTION
165 # but if the file contains a code fragment, it must be included in a
166 # program-unit
167 firstLine = [line for line in content.split('\n')
168 if not (re.search(r'^[\t ]*!', line) or
169 re.search(r'^[\t ]*$', line))][0]
170 fisrtLine = firstLine.upper().split()
171 if not ('SUBROUTINE' in fisrtLine or 'FUNCTION' in firstLine):
172 # Needs to be wrapped in a program-unit
173 moduleAdded = True
174 content = 'MODULE FOO\n' + content + '\nEND MODULE FOO'
175 file.write(content.encode('UTF8'))
176 else:
177 filename = file.name
178 file.write(fortranSource.encode('UTF-8'))
179 xml = pyfxtran.run(filename, ['-o', '-'] + parserOptions)
180 xml = ET.fromstring(xml, parser=ET.XMLParser(encoding='UTF-8'))
181 if renamed:
182 xml.find('./{*}file').attrib['name'] = fortranSource
183 if moduleAdded:
184 file = xml.find('./{*}file')
185 programUnit = file.find('./{*}program-unit')
186 # all nodes inside program-unit except 'MODULE' and 'END MODULE'
187 for node in programUnit[1:-1]:
188 file.append(node)
189 # pylint: disable-next=undefined-loop-variable
190 node.tail = node.tail[:-1] # remove '\n' added before 'END MODULE'
191 file.remove(programUnit)
192
193 includesDone = False
194 if len(set(['-no-include', '-noinclude']).intersection(parserOptions)) == 0:
195 # fxtran has included the files but:
196 # - it doesn't have removed the INCLUDE "file.h" statement
197 # - it included the file with its file node
198 # This code section removes the INCLUDE statement and the file node
199
200 # Remove the include statement
201 includeStmts = xml.findall('.//{*}include')
202 for includeStmt in includeStmts:
203 par = [p for p in xml.iter() if includeStmt in p][0]
204 par.remove(includeStmt)
205 includesDone = True
206 # Remove the file node
207 mainfile = xml.find('./{*}file')
208 for file in mainfile.findall('.//{*}file'):
209 par = [p for p in xml.iter() if file in p][0]
210 index = list(par).index(file)
211 if file.tail is not None:
212 file[-1].tail = file.tail if file[-1].tail is None else (file[-1].tail + file.tail)
213 for node in file[::-1]:
214 par.insert(index, node)
215 par.remove(file)
216
217 return includesDone, xml
218
219
220def tostring(doc):
221 """
222 :param doc: an ET object
223 :return: xml as a string
224 """
225 return ET.tostring(doc, method='xml', encoding='UTF-8').decode('UTF-8')
226
227
228def tofortran(doc):
229 """
230 :param doc: an ET object
231 :return: a string representing the FORTRAN source code
232 """
233 # When fxtran encounters an UTF-8 character, it replaces it by *2* entities
234 # We must first transform each of these entities to its corresponding binary value
235 # (this is done by tostring), then we must consider the result as bytes
236 # to decode these two bytes into UTF-8 (this is done by encode('raw_...').decode('UTF-8'))
237 result = ET.tostring(doc, method='text', encoding='UTF-8').decode('UTF-8')
238 try:
239 result = result.encode('raw_unicode_escape').decode('UTF-8')
240 except UnicodeDecodeError:
241 filename = doc.find('.//{*}file').attrib['name']
242 logging.warning("The file '%s' certainly contains a strange character", filename)
243 return result
244
245
247
248
249def isint(string):
250 """
251 :param string: string to test for intergerness
252 :return: True if s represent an int
253 """
254 try:
255 int(string)
256 except ValueError:
257 return False
258 else:
259 return True
260
261
262def isfloat(string):
263 """
264 :param string: string to test for intergerness
265 :return: True if s represent a real
266 """
267 try:
268 float(string)
269 except ValueError:
270 return False
271 else:
272 return True
273
274
276
277
278def tag(elem):
279 """
280 :param elem: ET Element
281 :return: the tag without the namespace
282 """
283 return elem.tag.split('}')[1]
284
285
286def n2name(nodeN):
287 """
288 Helper function which returns the entity name enclosed in a N tag
289 """
290 return ''.join([e.text for e in nodeN.findall('./{*}n')])
291
292
293def alltext(doc):
294 """
295 Helper function to iterate on all text fragment and join them
296 :param doc: xml fragment
297 """
298 return ''.join(doc.itertext())
299
300
301def nonCode(elem):
302 """
303 :param e: element
304 :return: True if e is non code (comment, text...)
305 """
306 return tag(elem) in {'cnt', 'C', 'cpp'}
307
308
309def isExecutable(elem):
310 """
311 :param e: element
312 :return: True if element is executable
313 """
314 return ((isStmt(elem) or isConstruct(elem)) and
315 tag(elem) not in ('subroutine-stmt', 'end-subroutine-stmt',
316 'function-stmt', 'end-function-stmt',
317 'use-stmt', 'T-decl-stmt', 'component-decl-stmt',
318 'T-stmt', 'end-T-stmt',
319 'data-stmt', 'save-stmt',
320 'implicit-none-stmt'))
321
322
323def isConstruct(elem):
324 """
325 :param elem: element
326 :return: True if element is a construct
327 """
328 return tag(elem).endswith('-construct')
329
330
331def isStmt(elem):
332 """
333 :param elem: element
334 :return: True if element is a statement
335 """
336 return tag(elem).endswith('-stmt')
isConstruct(elem)
Definition util.py:323