PyForTool
Python-fortran-tool
Loading...
Searching...
No Matches
util.py
1"""
2Utility functions and classes for XML manipulation and debugging.
3
4Provides helper functions for parsing, converting, and manipulating
5FORTRAN source code represented as XML.
6"""
7
8import xml.etree.ElementTree as ET
9from functools import wraps
10import logging
11import tempfile
12import os
13import time
14import re
15import pyfxtran
16
17from pyfortool import NAMESPACE
18
19
21
22debugStats = {}
23
24
25def debugDecor(func):
26 """
27 Decorator to trace function calls with timing and argument logging.
28
29 When logging is enabled at DEBUG level, logs function calls with arguments.
30 When logging is enabled at INFO level, tracks call count and execution time.
31 """
32 @wraps(func)
33 def wrapper(*args, **kwargs):
34 # Logging call
35 logger = logging.getLogger()
36 if logger.isEnabledFor(logging.DEBUG):
37 callstr = func.__name__ + \
38 '(' + ', '.join([str(a) for a in args] +
39 [k + '=' + str(v) for (k, v) in kwargs.items()]) + ')'
40 logging.debug('%s --> ...', callstr)
41 else:
42 callstr = None
43
44 # Count and time
45 if logger.isEnabledFor(logging.INFO):
46 t0 = time.time()
47 else:
48 t0 = None
49
50 # effective call
51 result = func(*args, **kwargs)
52
53 # Count and time
54 if t0 is not None:
55 # We test with t0 instead of the log level in case level evolved during func call
56 if func.__name__ not in debugStats:
57 debugStats[func.__name__] = {'nb': 0, 'totalTime': 0}
58 debugStats[func.__name__]['nb'] += 1
59 duration = time.time() - t0
60 debugStats[func.__name__]['totalTime'] += duration
61 debugStats[func.__name__]['min'] = \
62 min(duration, debugStats[func.__name__].get('min', duration))
63 debugStats[func.__name__]['max'] = \
64 max(duration, debugStats[func.__name__].get('max', duration))
65
66 # logging result
67 if callstr is not None:
68 # We test with callstr instead of the log level in case level evolved during func call
69 logging.debug('%s --> %s', callstr, str(result))
70
71 return result
72 return wrapper
73
74
75def noParallel(func):
76 """
77 Decorator to prevent parallel execution of a method.
78
79 Used for methods that modify the XML tree and need to prevent
80 concurrent execution across multiple files.
81 """
82 @wraps(func)
83 def wrapper(self, *args, **kwargs):
84 if self.NO_PARALLEL_LOCK is not None:
85 # Acquire lock, if any
86 with self.NO_PARALLEL_LOCK:
87 # We cannot use directly the shared version because the update
88 # tries to send a whole PYFT object to the Manager and there are
89 # issues with that (at least about the locks attached to the instance)
90 # The code here allows to synchronize when there is a lock mechanism.
91 # All the routines updating the tree must be decorated by noParallel
92 if self.SHARED_TREE is not None:
93 self.tree.copyFromOtherTree(self.SHARED_TREE)
94 result = func(self, *args, **kwargs)
95 if self.SHARED_TREE is not None:
96 self.tree.copyToOtherTree(self.SHARED_TREE)
97 return result
98 else:
99 return func(self, *args, **kwargs)
100 return wrapper
101
102
103def setVerbosity(level):
104 """
105 Set the logging verbosity level.
106
107 Parameters
108 ----------
109 level : str or int
110 Logging level: 'DEBUG', 'INFO', 'WARNING', 'ERROR', or numeric value.
111 """
112 logger = logging.getLogger()
113 if isinstance(level, str):
114 logger.setLevel(level=level.upper())
115 else:
116 logger.setLevel(level=level)
117
118
120 """
121 Print debug statistics on decorated function usage.
122
123 Displays a table with function names, call counts, min/max/total
124 execution times for functions decorated with @debugDecor.
125 """
126 logger = logging.getLogger()
127 if logger.isEnabledFor(logging.INFO):
128 def _print(name, nb, vmin, vmax, mean):
129 print('| ' + name.ljust(30) + '| ' + str(nb).ljust(14) + '| ' +
130 str(vmin).ljust(23) + '| ' + str(vmax).ljust(23) + '| ' +
131 str(mean).ljust(23) + '|')
132 _print('Name of the function', '# of calls', 'Min (s)', 'Max (s)', 'Total (s)')
133 for funcName, values in debugStats.items():
134 _print(funcName, values['nb'], values['min'], values['max'], values['totalTime'])
135
136
137class PYFTError(Exception):
138 """
139 Exceptions for PYFT
140 """
141
142
144
145
146def fortran2xml(fortranSource, parserOptions=None, wrapH=False):
147 """
148 Convert FORTRAN source code to XML using fxtran parser.
149
150 Parameters
151 ----------
152 fortranSource : str
153 FORTRAN source code string or path to a file.
154 parserOptions : list, optional
155 Options passed to fxtran parser.
156 wrapH : bool, optional
157 If True, wrap .h file content in a MODULE for free-form parsing.
158
159 Returns
160 -------
161 tuple
162 (includesRemoved, xml) where:
163 - includesRemoved (bool): True if include statements were processed.
164 - xml (Element): XML document tree.
165
166 Examples
167 --------
168 >>> includesRemoved, xml = fortran2xml("REAL :: X\nX = 1.0")
169 """
170 # Namespace registration
171 ET.register_namespace('f', NAMESPACE)
172
173 # Default options
174 if parserOptions is None:
175 import pyfortool
176 parserOptions = pyfortool.PYFT.DEFAULT_FXTRAN_OPTIONS
177
178 # Call to fxtran
179 renamed = False
180 moduleAdded = False
181 with tempfile.NamedTemporaryFile(buffering=0, suffix='.F90') as file:
182 if os.path.exists(fortranSource):
183 # tempfile not needed in this case if wrapH is False but I found easier to write code
184 # like this to have only one pyfxtran use and automatic
185 # deletion of the temporary file
186 filename = fortranSource
187 if wrapH and filename.endswith('.h'):
188 renamed = True
189 filename = file.name
190 with open(fortranSource, 'r', encoding='utf-8') as src:
191 content = src.read()
192 # renaming is enough for .h files containing SUBROUTINE or FUNCTION
193 # but if the file contains a code fragment, it must be included in a
194 # program-unit
195 firstLine = [line for line in content.split('\n')
196 if not (re.search(r'^[\t ]*!', line) or
197 re.search(r'^[\t ]*$', line))][0]
198 fisrtLine = firstLine.upper().split()
199 if not ('SUBROUTINE' in fisrtLine or 'FUNCTION' in firstLine):
200 # Needs to be wrapped in a program-unit
201 moduleAdded = True
202 content = 'MODULE FOO\n' + content + '\nEND MODULE FOO'
203 file.write(content.encode('UTF8'))
204 else:
205 filename = file.name
206 file.write(fortranSource.encode('UTF-8'))
207 xml = pyfxtran.run(filename, ['-o', '-'] + parserOptions)
208 xml = ET.fromstring(xml, parser=ET.XMLParser(encoding='UTF-8'))
209 if renamed:
210 xml.find('./{*}file').attrib['name'] = fortranSource
211 if moduleAdded:
212 file = xml.find('./{*}file')
213 programUnit = file.find('./{*}program-unit')
214 # all nodes inside program-unit except 'MODULE' and 'END MODULE'
215 for node in programUnit[1:-1]:
216 file.append(node)
217 # pylint: disable-next=undefined-loop-variable
218 node.tail = node.tail[:-1] # remove '\n' added before 'END MODULE'
219 file.remove(programUnit)
220
221 includesDone = False
222 if len(set(['-no-include', '-noinclude']).intersection(parserOptions)) == 0:
223 # fxtran has included the files but:
224 # - it doesn't have removed the INCLUDE "file.h" statement
225 # - it included the file with its file node
226 # This code section removes the INCLUDE statement and the file node
227
228 # Remove the include statement
229 includeStmts = xml.findall('.//{*}include')
230 for includeStmt in includeStmts:
231 par = [p for p in xml.iter() if includeStmt in p][0]
232 par.remove(includeStmt)
233 includesDone = True
234 # Remove the file node
235 mainfile = xml.find('./{*}file')
236 for file in mainfile.findall('.//{*}file'):
237 par = [p for p in xml.iter() if file in p][0]
238 index = list(par).index(file)
239 if file.tail is not None:
240 file[-1].tail = file.tail if file[-1].tail is None else (file[-1].tail + file.tail)
241 for node in file[::-1]:
242 par.insert(index, node)
243 par.remove(file)
244
245 return includesDone, xml
246
247
248def tostring(doc):
249 """
250 Convert XML Element to string.
251
252 Parameters
253 ----------
254 doc : Element
255 XML Element object.
256
257 Returns
258 -------
259 str
260 XML string representation.
261
262 Examples
263 --------
264 >>> xml_str = tostring(xml_element)
265 """
266 return ET.tostring(doc, method='xml', encoding='UTF-8').decode('UTF-8')
267
268
269def tofortran(doc):
270 """
271 Convert XML Element to FORTRAN source code.
272
273 Parameters
274 ----------
275 doc : Element
276 XML Element representing FORTRAN code.
277
278 Returns
279 -------
280 str
281 FORTRAN source code string.
282
283 Examples
284 --------
285 >>> fortran_code = tofortran(xml_element)
286 """
287 # When fxtran encounters an UTF-8 character, it replaces it by *2* entities
288 # We must first transform each of these entities to its corresponding binary value
289 # (this is done by tostring), then we must consider the result as bytes
290 # to decode these two bytes into UTF-8 (this is done by encode('raw_...').decode('UTF-8'))
291 result = ET.tostring(doc, method='text', encoding='UTF-8').decode('UTF-8')
292 try:
293 result = result.encode('raw_unicode_escape').decode('UTF-8')
294 except UnicodeDecodeError:
295 filename = doc.find('.//{*}file').attrib['name']
296 logging.warning("The file '%s' certainly contains a strange character", filename)
297 return result
298
299
301
302
303def isint(string):
304 """
305 Check if string represents an integer.
306
307 Parameters
308 ----------
309 string : str
310 String to test.
311
312 Returns
313 -------
314 bool
315 True if string is a valid integer.
316
317 Examples
318 --------
319 >>> isint('42')
320 True
321 >>> isint('3.14')
322 False
323 """
324 try:
325 int(string)
326 except ValueError:
327 return False
328 else:
329 return True
330
331
332def isfloat(string):
333 """
334 Check if string represents a floating-point number.
335
336 Parameters
337 ----------
338 string : str
339 String to test.
340
341 Returns
342 -------
343 bool
344 True if string is a valid float.
345
346 Examples
347 --------
348 >>> isfloat('3.14')
349 True
350 >>> isfloat('42')
351 True
352 """
353 try:
354 float(string)
355 except ValueError:
356 return False
357 else:
358 return True
359
360
362
363
364def tag(elem):
365 """
366 Get XML tag name without namespace.
367
368 Parameters
369 ----------
370 elem : Element
371 XML Element.
372
373 Returns
374 -------
375 str
376 Tag name without namespace prefix.
377
378 Examples
379 --------
380 >>> tag(xml_element) # '{http://fxtran.net}subroutine-stmt' -> 'subroutine-stmt'
381 """
382 return elem.tag.split('}')[1]
383
384
385def n2name(nodeN):
386 """
387 Extract entity name from N-tagged XML element.
388
389 Parameters
390 ----------
391 nodeN : Element
392 XML element with N tag containing 'n' child elements.
393
394 Returns
395 -------
396 str
397 Concatenated name from all 'n' child elements.
398
399 Examples
400 --------
401 >>> n2name(element) # <N><n>X</n><n>Y</n></N> -> 'XY'
402 """
403 return ''.join([e.text for e in nodeN.findall('./{*}n')])
404
405
406def alltext(doc):
407 """
408 Get all text content from an XML element.
409
410 Parameters
411 ----------
412 doc : Element
413 XML element or fragment.
414
415 Returns
416 -------
417 str
418 Concatenated text from element and all descendants.
419
420 Examples
421 --------
422 >>> alltext(element) # Gets all text including nested elements
423 """
424 return ''.join(doc.itertext())
425
426
427def nonCode(elem):
428 """
429 Check if element is non-code (comment, text, etc.).
430
431 Parameters
432 ----------
433 elem : Element
434 XML element.
435
436 Returns
437 -------
438 bool
439 True if element is non-code (cnt, C, cpp, filename, S).
440
441 Examples
442 --------
443 >>> nonCode(comment_element)
444 True
445 """
446 return tag(elem) in {'cnt', 'C', 'cpp', 'filename', 'S'}
447
448
449def isExecutable(elem):
450 """
451 Check if element is an executable statement.
452
453 Parameters
454 ----------
455 elem : Element
456 XML element.
457
458 Returns
459 -------
460 bool
461 True if element is a statement or construct (excluding declarations).
462
463 Examples
464 --------
465 >>> isExecutable(stmt_element)
466 True
467 """
468 return ((isStmt(elem) or isConstruct(elem)) and
469 tag(elem) not in ('subroutine-stmt', 'end-subroutine-stmt',
470 'function-stmt', 'end-function-stmt',
471 'use-stmt', 'T-decl-stmt', 'component-decl-stmt',
472 'T-stmt', 'end-T-stmt',
473 'data-stmt', 'save-stmt',
474 'implicit-none-stmt'))
475
476
477def isConstruct(elem):
478 """
479 Check if element is a construct.
480
481 Parameters
482 ----------
483 elem : Element
484 XML element.
485
486 Returns
487 -------
488 bool
489 True if element tag ends with '-construct'.
490
491 Examples
492 --------
493 >>> isConstruct(if_construct_element)
494 True
495 """
496 return tag(elem).endswith('-construct')
497
498
499def isStmt(elem):
500 """
501 Check if element is a statement.
502
503 Parameters
504 ----------
505 elem : Element
506 XML element.
507
508 Returns
509 -------
510 bool
511 True if element tag ends with '-stmt'.
512
513 Examples
514 --------
515 >>> isStmt(call_element)
516 True
517 """
518 return tag(elem).endswith('-stmt')
isConstruct(elem)
Definition util.py:477