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
248 """
249 Remove parallel processing configuration
250 """
251 cls.NO_PARALLEL_LOCK = None
252 cls.SHARED_TREE = None
253 cls.PARALLEL_FILE_LOCKS = None
254
255 @classmethod
256 def lockFile(cls, filename):
257 """
258 Acquire file lock for parallel processing.
259
260 Parameters
261 ----------
262 filename : str
263 Path to the file to lock.
264 """
265 filename = os.path.normpath(filename)
266 # pylint: disable-next=unsupported-membership-test
267 if cls.PARALLEL_FILE_LOCKS is not None and filename in cls.PARALLEL_FILE_LOCKS:
268 # pylint: disable-next=unsubscriptable-object
269 cls.PARALLEL_FILE_LOCKS[filename].acquire()
270
271 @classmethod
272 def unlockFile(cls, filename, silence=False):
273 """
274 Release file lock for parallel processing.
275
276 Parameters
277 ----------
278 filename : str
279 Path to the file to unlock.
280 silence : bool, optional
281 If True, suppress ValueError when file is not locked.
282 """
283 filename = os.path.normpath(filename)
284 # pylint: disable-next=unsupported-membership-test
285 if cls.PARALLEL_FILE_LOCKS is not None and filename in cls.PARALLEL_FILE_LOCKS:
286 try:
287 # pylint: disable-next=unsubscriptable-object
288 cls.PARALLEL_FILE_LOCKS[filename].release()
289 except ValueError:
290 if not silence:
291 raise
292
293 def __enter__(self):
294 """
295 Enter context manager.
296
297 Returns
298 -------
299 PYFT
300 Self reference for use in with statement.
301 """
302 return self
303
304 def __exit__(self, excType, excVal, excTb):
305 """
306 Exit context manager and close file.
307 """
308 self.close()
309
310 def close(self):
311 """
312 Close the FORTRAN file and release resources.
313
314 Prints debug statistics and releases file locks.
315 Automatically called when exiting context manager.
316 """
317 printInfos()
318 self.__class__.unlockFile(self.getFileName())
319
320 @property
321 def xml(self):
322 """
323 Get the XML representation of the parsed code.
324
325 Returns
326 -------
327 str
328 XML string representation of the FORTRAN source.
329 """
330 return tostring(self)
331
332 @property
333 def fortran(self):
334 """
335 Get the FORTRAN source code representation.
336
337 Returns
338 -------
339 str
340 FORTRAN source code string.
341 """
342 return tofortran(self)
343
344 def renameUpper(self):
345 """
346 Set output file extension to uppercase.
347
348 Examples
349 --------
350 >>> pft = PYFT('file.F90')
351 >>> pft.renameUpper() # Output will be file.F90
352 """
353 self._rename(str.upper)
354
355 def renameLower(self):
356 """
357 Set output file extension to lowercase.
358
359 Examples
360 --------
361 >>> pft = PYFT('file.F90')
362 >>> pft.renameLower() # Output will be file.f90
363 """
364 self._rename(str.lower)
365
366 def _rename(self, mod):
367 """
368 Apply a transformation function to the file extension.
369
370 Parameters
371 ----------
372 mod : callable
373 Function to apply to file extension (e.g., str.upper, str.lower).
374 """
375 def _transExt(path, mod):
376 filename, ext = os.path.splitext(path)
377 return filename + mod(ext)
378 if self._output is None:
379 self._filename = _transExt(self._filename, mod)
380 else:
381 self._output = _transExt(self._output, mod)
382
383 def write(self):
384 """
385 Write the transformed FORTRAN source to file.
386
387 Writes the current state of the code tree as FORTRAN source
388 to the output file (or overwrites input if no output specified).
389 """
390 with open(self._filename if self._output is None else self._output, 'w',
391 encoding='utf-8') as fo:
392 fo.write(self.fortranfortran)
393 fo.flush() # ensuring all the existing buffers are written
394 os.fsync(fo.fileno()) # forcefully writing to the file
395
396 if self._output is None and self._filename != self._originalName:
397 # We must perform an in-place update of the file, but the output file
398 # name has been updated. Then, we must remove the original file.
399 os.unlink(self._originalName)
400
401 def writeXML(self, filename):
402 """
403 Write the internal XML representation to a file.
404
405 Parameters
406 ----------
407 filename : str
408 Path for the output XML file.
409
410 Examples
411 --------
412 >>> pft = PYFT('input.F90')
413 >>> pft.writeXML('output.xml')
414 """
415 with open(filename, 'w', encoding='utf-8') as fo:
416 fo.write(self.xmlxml)
unlockFile(cls, filename, silence=False)
Definition pyfortool.py:272
writeXML(self, filename)
Definition pyfortool.py:401
lockFile(cls, filename)
Definition pyfortool.py:256
__exit__(self, excType, excVal, excTb)
Definition pyfortool.py:304
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