PyForTool
Python-fortran-tool
Loading...
Searching...
No Matches
pyfortool.py
1#!/usr/bin/env python3
2
3"""
4This module contains the main PYFT class.
5
6PYFT (Python FORTRAN Tool) is the primary class for reading, manipulating,
7and writing FORTRAN source files. It provides file-level operations and
8wraps the scope-level functionality from PYFTscope.
9
10Examples
11--------
12>>> from pyfortool import PYFT
13
14# Read and modify a file
15>>> pft = PYFT('input.F90')
16>>> pft.removePrints()
17>>> pft.write()
18
19# Use context manager
20>>> with PYFT('input.F90') as pft:
21... pft.removeComments()
22... pft.write()
23"""
24
25import os
26import sys
27from multiprocessing import Lock, RLock
28
29from pyfortool.scope import PYFTscope
30from pyfortool.tree import Tree, updateTree
31from pyfortool.util import (debugDecor, tostring, tofortran, fortran2xml,
32 setVerbosity, printInfos, PYFTError)
33
34
35@debugDecor
36def conservativePYFT(filename, parserOptions, wrapH,
37 tree=None, verbosity=None, clsPYFT=None):
38 """
39 Create a PYFT object with conservative parsing options.
40
41 Similar to PYFT constructor but forces the '-no-include' option
42 to prevent automatic inclusion of header files.
43
44 Parameters
45 ----------
46 filename : str
47 Path to the FORTRAN source file.
48 parserOptions : list or None
49 Parser options passed to PYFT. If None, uses default options.
50 wrapH : bool
51 Whether to wrap .h file content. See PYFT class.
52 tree : Tree, optional
53 Tree instance for cross-file analysis.
54 verbosity : str or int, optional
55 Logging verbosity level.
56 clsPYFT : type, optional
57 PYFT subclass to use instead of default PYFT.
58
59 Returns
60 -------
61 PYFT
62 A PYFT instance configured for conservative tree manipulation.
63
64 See Also
65 --------
66 PYFT : The main PYFT class.
67 """
68 options = PYFT.DEFAULT_FXTRAN_OPTIONS if parserOptions is None else parserOptions
69 options = options.copy()
70 if len(set(options).intersection(('-no-include', '-noinclude'))) == 0:
71 # We add the option to not include 'include files' when analysing the tree
72 options.append('-no-include')
73 if clsPYFT is None:
74 clsPYFT = PYFT
75 pft = clsPYFT(filename, parserOptions=options, wrapH=wrapH,
76 tree=tree, verbosity=verbosity)
77 return pft
78
79
80def generateEmptyPYFT(filename, fortran=None, **kwargs):
81 """
82 Generate a new PYFT object from a file path, creating the file if needed.
83
84 Parameters
85 ----------
86 filename : str
87 Path for the new file.
88 fortran : str, optional
89 FORTRAN source code to write to the file.
90 If None, creates a stub subroutine "SUBROUTINE FOO\nEND".
91 **kwargs
92 Additional arguments passed to PYFT constructor.
93
94 Returns
95 -------
96 PYFT
97 PYFT instance for the newly created file.
98
99 Examples
100 --------
101 >>> pft = generateEmptyPYFT('new.F90', 'MODULE TEST\nEND MODULE')
102 >>> pft.addVar([('module:TEST', 'X', 'INTEGER :: X', None)])
103 >>> pft.write()
104 """
105 with open(filename, 'w', encoding='utf-8') as fo:
106 fo.write('SUBROUTINE FOO\nEND' if fortran is None else fortran)
107 pft = PYFT(filename, **kwargs)
108 if fortran is None:
109 pft.remove(pft.find('.//{*}program-unit'))
110 return pft
111
112
114 """
115 Main class for FORTRAN file manipulation.
116
117 PYFT extends PYFTscope with file-level operations (read/write) and
118 integrates with the fxtran parser for FORTRAN source code analysis.
119
120 Class Attributes
121 ----------------
122 DEFAULT_FXTRAN_OPTIONS : list
123 Default options for fxtran parser: ['-construct-tag', '-no-include',
124 '-no-cpp', '-line-length', '9999']
125 MANDATORY_FXTRAN_OPTIONS : list
126 Required options: ['-construct-tag']
127
128 Examples
129 --------
130 Basic usage:
131 >>> pft = PYFT('myfile.F90')
132 >>> pft.upperCase()
133 >>> pft.write()
134
135 Using context manager:
136 >>> with PYFT('myfile.F90', output='output.F90') as pft:
137 ... pft.removeComments()
138 ... pft.indent()
139
140 Parallel processing:
141 >>> tree = Tree(['/path/to/src'])
142 >>> PYFT.setParallel(tree)
143 """
144 DEFAULT_FXTRAN_OPTIONS = ['-construct-tag', '-no-include', '-no-cpp', '-line-length', '9999']
145 MANDATORY_FXTRAN_OPTIONS = ['-construct-tag']
146 SHARED_TREE = None # Can be updated by setParallel
147 NO_PARALLEL_LOCK = None # Can be updated by setParallel
148 PARALLEL_FILE_LOCKS = None # Can be updated by setParallel
149
150 @updateTree('signal')
151 def __init__(self, filename, output=None, parserOptions=None, verbosity=None,
152 wrapH=False, tree=None, enableCache=False):
153 """
154 Initialize a PYFT instance from a FORTRAN source file.
155
156 Parameters
157 ----------
158 filename : str
159 Path to the input FORTRAN file.
160 output : str, optional
161 Path for output file. If None, overwrites the input file.
162 parserOptions : list, optional
163 Options for the fxtran parser. See DEFAULT_FXTRAN_OPTIONS.
164 verbosity : str or int, optional
165 Logging level (e.g., 'DEBUG', 'INFO', 'WARNING').
166 wrapH : bool, optional
167 If True, wrap .h file content in a MODULE to enable parsing
168 as free-form FORTRAN.
169 tree : Tree, optional
170 Tree instance for cross-file analysis. If None, creates a new Tree.
171 enableCache : bool, optional
172 If True, cache parent nodes for faster traversal.
173
174 Raises
175 ------
176 PYFTError
177 If Python version < 3.8 or file does not exist.
178
179 Examples
180 --------
181 >>> pft = PYFT('myfile.F90')
182 >>> pft = PYFT('myfile.F90', output='newfile.F90')
183 >>> pft = PYFT('code.h', wrapH=True)
184 """
185 self.__class__.lockFile(filename)
186 if not sys.version_info >= (3, 8):
187 # At least version 3.7 for ordered dictionary
188 # At least verison 3.8 for namsepace wildcard (use of '{*}' in find or findall)
189 raise PYFTError("PyForTool needs at least version 3.8 of python")
190 self._filename = filename
191 self._originalName = filename
192 assert os.path.exists(filename), f'Input filename ({filename})must exist'
193 self._output = output
194 tree = Tree() if tree is None else tree
195 if self.SHARED_TREE is not None:
196 assert tree is not None, 'tree must be None if setParallel has been called'
197 tree.copyFromOtherTree(self.SHARED_TREE)
198 if parserOptions is None:
199 self._parserOptions = self.DEFAULT_FXTRAN_OPTIONS.copy()
200 else:
201 self._parserOptions = parserOptions.copy()
202 for tDir in tree.getDirs():
203 self._parserOptions.extend(['-I', tDir])
204 for option in self.MANDATORY_FXTRAN_OPTIONS:
205 if option not in self._parserOptions:
206 self._parserOptions.append(option)
207 includesRemoved, xml = fortran2xml(self._filename, self._parserOptions, wrapH)
208 super().__init__(xml, enableCache=enableCache, tree=tree)
209 if includesRemoved:
210 self.tree.signal(self)
211 if verbosity is not None:
212 setVerbosity(verbosity)
213
214 @classmethod
215 def setParallel(cls, tree, clsLock=None, clsRLock=None):
216 """
217 Configure PYFT for parallel processing.
218
219 Must be called before creating PYFT instances for parallel execution.
220 Sets up shared tree and file locking mechanisms.
221
222 Parameters
223 ----------
224 tree : Tree
225 Tree object shared among all processes.
226 clsLock : type, optional
227 Lock class to use. Defaults to multiprocessing.Lock.
228 clsRLock : type, optional
229 Recursive lock class. Defaults to multiprocessing.RLock.
230
231 Examples
232 --------
233 >>> tree = Tree(['/path/to/src'], descTreeFile='tree.json')
234 >>> PYFT.setParallel(tree)
235 >>> # Now create PYFT instances in parallel processes
236 """
237 if clsLock is None:
238 clsLock = Lock
239 if clsRLock is None:
240 clsRLock = RLock
241 cls.NO_PARALLEL_LOCK = clsRLock()
242 cls.SHARED_TREE = tree
243 cls.PARALLEL_FILE_LOCKS = {os.path.normpath(file): clsLock()
244 for file in tree.getFiles()}
245
246 @classmethod
247 def lockFile(cls, filename):
248 """
249 Acquire file lock for parallel processing.
250
251 Parameters
252 ----------
253 filename : str
254 Path to the file to lock.
255 """
256 filename = os.path.normpath(filename)
257 # pylint: disable-next=unsupported-membership-test
258 if cls.PARALLEL_FILE_LOCKS is not None and filename in cls.PARALLEL_FILE_LOCKS:
259 # pylint: disable-next=unsubscriptable-object
260 cls.PARALLEL_FILE_LOCKS[filename].acquire()
261
262 @classmethod
263 def unlockFile(cls, filename, silence=False):
264 """
265 Release file lock for parallel processing.
266
267 Parameters
268 ----------
269 filename : str
270 Path to the file to unlock.
271 silence : bool, optional
272 If True, suppress ValueError when file is not locked.
273 """
274 filename = os.path.normpath(filename)
275 # pylint: disable-next=unsupported-membership-test
276 if cls.PARALLEL_FILE_LOCKS is not None and filename in cls.PARALLEL_FILE_LOCKS:
277 try:
278 # pylint: disable-next=unsubscriptable-object
279 cls.PARALLEL_FILE_LOCKS[filename].release()
280 except ValueError:
281 if not silence:
282 raise
283
284 def __enter__(self):
285 """
286 Enter context manager.
287
288 Returns
289 -------
290 PYFT
291 Self reference for use in with statement.
292 """
293 return self
294
295 def __exit__(self, excType, excVal, excTb):
296 """
297 Exit context manager and close file.
298 """
299 self.close()
300
301 def close(self):
302 """
303 Close the FORTRAN file and release resources.
304
305 Prints debug statistics and releases file locks.
306 Automatically called when exiting context manager.
307 """
308 printInfos()
309 self.__class__.unlockFile(self.getFileName())
310
311 @property
312 def xml(self):
313 """
314 Get the XML representation of the parsed code.
315
316 Returns
317 -------
318 str
319 XML string representation of the FORTRAN source.
320 """
321 return tostring(self)
322
323 @property
324 def fortran(self):
325 """
326 Get the FORTRAN source code representation.
327
328 Returns
329 -------
330 str
331 FORTRAN source code string.
332 """
333 return tofortran(self)
334
335 def renameUpper(self):
336 """
337 Set output file extension to uppercase.
338
339 Examples
340 --------
341 >>> pft = PYFT('file.F90')
342 >>> pft.renameUpper() # Output will be file.F90
343 """
344 self._rename(str.upper)
345
346 def renameLower(self):
347 """
348 Set output file extension to lowercase.
349
350 Examples
351 --------
352 >>> pft = PYFT('file.F90')
353 >>> pft.renameLower() # Output will be file.f90
354 """
355 self._rename(str.lower)
356
357 def _rename(self, mod):
358 """
359 Apply a transformation function to the file extension.
360
361 Parameters
362 ----------
363 mod : callable
364 Function to apply to file extension (e.g., str.upper, str.lower).
365 """
366 def _transExt(path, mod):
367 filename, ext = os.path.splitext(path)
368 return filename + mod(ext)
369 if self._output is None:
370 self._filename = _transExt(self._filename, mod)
371 else:
372 self._output = _transExt(self._output, mod)
373
374 def write(self):
375 """
376 Write the transformed FORTRAN source to file.
377
378 Writes the current state of the code tree as FORTRAN source
379 to the output file (or overwrites input if no output specified).
380 """
381 with open(self._filename if self._output is None else self._output, 'w',
382 encoding='utf-8') as fo:
383 fo.write(self.fortranfortran)
384 fo.flush() # ensuring all the existing buffers are written
385 os.fsync(fo.fileno()) # forcefully writing to the file
386
387 if self._output is None and self._filename != self._originalName:
388 # We must perform an in-place update of the file, but the output file
389 # name has been updated. Then, we must remove the original file.
390 os.unlink(self._originalName)
391
392 def writeXML(self, filename):
393 """
394 Write the internal XML representation to a file.
395
396 Parameters
397 ----------
398 filename : str
399 Path for the output XML file.
400
401 Examples
402 --------
403 >>> pft = PYFT('input.F90')
404 >>> pft.writeXML('output.xml')
405 """
406 with open(filename, 'w', encoding='utf-8') as fo:
407 fo.write(self.xmlxml)
unlockFile(cls, filename, silence=False)
Definition pyfortool.py:263
writeXML(self, filename)
Definition pyfortool.py:392
lockFile(cls, filename)
Definition pyfortool.py:247
__exit__(self, excType, excVal, excTb)
Definition pyfortool.py:295
setParallel(cls, tree, clsLock=None, clsRLock=None)
Definition pyfortool.py:215
__init__(self, filename, output=None, parserOptions=None, verbosity=None, wrapH=False, tree=None, enableCache=False)
Definition pyfortool.py:152
append(self, *args, **kwargs)
Definition scope.py:172
extend(self, *args, **kwargs)
Definition scope.py:179
generateEmptyPYFT(filename, fortran=None, **kwargs)
Definition pyfortool.py:80