New URL for NEMO forge!   http://forge.nemo-ocean.eu

Since March 2022 along with NEMO 4.2 release, the code development moved to a self-hosted GitLab.
This present forge is now archived and remained online for history.
nml.py in branches/2013/dev_r3987_UKMO4_OBS/NEMOGCM/TOOLS/OBSTOOLS/OOO/ooo – NEMO

source: branches/2013/dev_r3987_UKMO4_OBS/NEMOGCM/TOOLS/OBSTOOLS/OOO/ooo/nml.py @ 4126

Last change on this file since 4126 was 4126, checked in by andrewryan, 11 years ago

added latest version of OOO namelist editor and test suite

File size: 12.2 KB
Line 
1"""Collection of Fortran 90 namelist helper functions.
2
3    A common way to interface with a Fortran executable is via
4    an input file called a namelist. This module defines
5    functions which simplify the process of updating and
6    extending namelist data.
7
8    .. note:: This module is especially lightweight and follows the
9              batteries included philosophy. As such, only standard
10              library modules are required to use this code.
11
12    Walkthrough
13    ===========
14
15    New namelist
16    ------------
17
18    A typical usage is to create and update a Fortran namelist on the fly.
19
20    >>> import nml
21    >>> namid = "namfoo"
22    >>> text = nml.new(namid)
23    >>> data = {"x": nml.tostring([1, 2, 3])}
24    >>> text = nml.update(namid, text, data)
25    >>> print text
26    <BLANKLINE>
27    &namfoo
28       x = 1 2 3
29    /
30
31    In the above snippet :func:`tostring` has been used to sanitize the input
32    Python list. This function cleverly maps string data and numeric data to
33    the correct Fortran syntax.
34
35    However, the :func:`new` function takes care of many of the above steps automatically.
36    Where appropriate :func:`sanitize` has been embedded to reduce the need
37    to worry about data format problems. Take for example,
38
39    >>> print nml.new("namfoo", data={"x": range(3)})
40    <BLANKLINE>
41    &namfoo
42       x = 0 1 2
43    /
44
45    Parse existing namelist
46    -----------------------
47
48    In order to update a namelist it is necessary to convert the namelist text into
49    a dictionary of *key, value* pairs which can be manipulated in the usual Pythonic
50    fashion before being piped back out to disk.
51
52    In everyday usage text will be read from files, here however for illustration
53    purposes I have hand written a namelist.
54
55    >>> text = '''
56    ... &namfoo
57    ...     x = y  ! A description of the variables
58    ... /
59    ... &nambar
60    ...     ln_on = .TRUE. ! A description of the variables
61    ... /
62    ... '''
63
64    This can be parsed by invoking the :func:`variables` command.
65
66    >>> nml.variables(text)
67    {'x': 'y', 'ln_on': '.TRUE.'}
68
69    Or by using the :func:`namelists` function to split the file into sub-lists.
70
71    >>> nml.namelists(text)
72    {'namfoo': '&namfoo\\n    x = y  ! A description of the variables\\n/', 'nambar': '&nambar\\n    ln_on = .TRUE. ! A description of the variables\\n/'}
73    >>> sublists = nml.namelists(text)
74    >>> print sublists["nambar"]
75    &nambar
76        ln_on = .TRUE. ! A description of the variables
77    /
78
79    Which can be parsed into a dictionary as before.
80
81    >>> print nml.variables(sublists["nambar"])
82    {'ln_on': '.TRUE.'}
83
84
85    Update/replace data
86    -------------------
87
88    There are two ways of modifying values inside a Fortran namelist.
89
90    Replace
91        The first is to simply replace a set of variables with new values. This behaviour is accomplished
92        via the :func:`replace` function. This approach simply overwrites existing variables. No knowledge
93        of sub-namelist structure is required to modify a string of text.
94
95    .. note:: Additional variables will not be added to a namelist via this approach
96
97    Update
98        The second is to extend the set of variables contained within a namelist. This functionality is
99        controlled by the :func:`update` function. Here, variables which are not already specified are
100        added using a templated namelist line.
101
102    .. note:: It is essential to specify which sub-namelist is to be updated before modification takes place
103
104    Pipe to/from file
105    -----------------
106
107    As typical NEMO namelists are no larger than a few tens of kilobytes
108    it makes sense to process namelists as single strings instead of
109    line by line.
110
111    >>> path = "foo.nml"
112    >>> text = nml.new("namfoo")
113
114    To write to a file simply invoke the writer.
115
116    >>> # Write to file
117    >>> nml.writer(path, text)
118
119    To read from a file specify the path to be read.
120
121    >>> # Read from file
122    >>> text = nml.reader(path)
123
124    Join multiple namelists
125    -----------------------
126
127    Since the namelists are regular Python strings there is no need for a
128    specific *join* function. Namelists can be combined in whatever manner
129    is most pleasing to the eye.
130
131    >>> namoff = nml.new("namoff")
132    >>> namcl4 = nml.new("namcl4")
133    >>> # new line join
134    >>> print "\\n".join([namoff, namcl4])
135    <BLANKLINE>
136    &namoff
137    /
138    <BLANKLINE>
139    &namcl4
140    /
141
142    >>> # Or addition
143    >>> print namoff + namcl4
144    <BLANKLINE>
145    &namoff
146    /
147    &namcl4
148    /
149
150    Module functions
151    ================
152
153"""
154__version__ = "0.1.0"
155import re
156
157def reader(path):
158    """Reads a file into a string
159
160    Reads whole file into single string. Typically,
161    namelists are small enough to be stored in memory
162    while updates and edits are being performed.
163
164    :param path: Path to input file
165    :returns: entire file as a single string
166
167    """
168    with open(path, "r") as handle:
169        text = handle.read()
170    return text
171
172def writer(path, text):
173    """Writes to a file from a string
174
175    Handy way of piping a processed namelist into
176    a file.
177
178    :param path: Path to output file
179    :param text: Input text to process
180
181    """
182    with open(path, "w") as handle:
183        handle.write(text)
184
185def update(namid, text, data, convert=True):
186    """Extends namelist definition.
187
188    Similar to replace this function alters the values
189    of variables defined within a namelist. In addition to
190    replacing values it also creates definitions if the
191    variable is not found in the namelist. As such, the
192    namelist id must be specified.
193
194    :param namid: Namelist id
195    :param text: Input text to process
196    :param data: Dictionary of variables
197    :keyword convert: Sanitizes input data before replacement takes place
198
199    :returns: Text
200
201    .. seealso:: :func:`replace` :func:`sanitize`
202    """
203    sublists = namelists(text)
204    assert namid in sublists, "Warning: invalid namid specified!"
205
206    # Sanitize inputs
207    if convert:
208        data = sanitize(data)
209
210    # Parse subsection
211    namtext = sublists[namid]
212    subdata = variables(namtext)
213    subvars = subdata.keys()
214
215    # Replace existing variables in namtext
216    tmptext = replace(namtext, data)
217    text = text.replace(namtext, tmptext)
218    namtext = tmptext
219
220    # Identify new variables
221    vars = data.keys()
222    newvars = list(set(vars) - set(subvars))
223    newvars.sort()
224
225    # Append new vars to namid
226    lines = namtext.split("\n")
227    for v in newvars:
228        newline = "   %s = %s" % (v, data[v])
229        lines.insert(-1, newline)
230    newtext = "\n".join(lines)
231
232    # Replace old namtext with new namtext
233    text = text.replace(namtext, newtext)
234    return text
235
236def replace(text, data, convert=True):
237    """Edits existing variables.
238
239    Pattern matches and substitutes variables inside
240    a string of text. This is independent of namid and
241    as such is useful for modifying existing variables.
242    To append new variables the :func:`update` function
243    is required.
244
245    >>> text = '''
246    ... &namobs
247    ...    ln_sst = .TRUE. ! Logical switch for SST observations
248    ... /
249    ... '''
250    >>> data = {"ln_sst": ".FALSE."}
251    >>> print replace(text, data)
252    <BLANKLINE>
253    &namobs
254       ln_sst = .FALSE. ! Logical switch for SST observations
255    /
256    <BLANKLINE>
257
258    .. note :: This does not append new variables to a namelist
259
260    :param text: string to process
261    :param data: dictionary with which to modify **text**
262    :keyword convert: Sanitizes input data before replacement takes place
263
264    :returns: string with new data values
265
266    .. seealso:: :func:`update`, :func:`sanitize`
267    """
268    if convert:
269        data = sanitize(data)
270    for k, v in data.iteritems():
271        pat = r"(%s\s*=\s*).+?(\s*[!\n])" % (k,)
272        repl = r"\g<1>%s\g<2>" % (v,)
273        text = re.sub(pat, repl, text)
274    return text
275
276def variables(text):
277    """Retrieves dictionary of variables in text.
278
279    >>> text = '''
280    ... &namobs
281    ...    ln_sst = .TRUE. ! Logical switch for SST observations
282    ... /
283    ... '''
284    >>> variables(text)
285    {'ln_sst': '.TRUE.'}
286
287    :param text: Input text to process
288
289    :returns: A dictionary of variable, value pairs.
290
291    """
292    data = {}
293    pairs = re.findall(r"\n\s*(\w+)\s*=\s*(.+?)\s*(?=[!\n])", text)
294    for key, value in pairs:
295        data[key] = value
296    return data
297
298def namelists(text):
299    """Retrieves dictionary of namelists in text.
300
301    Useful for isolating sub-namelists.
302
303    >>> text = '''
304    ... &namobs
305    ...    ln_sst = .TRUE. ! Logical switch for SST observations
306    ... /
307    ... '''
308    >>> namelists(text)
309    {'namobs': '&namobs\\n   ln_sst = .TRUE. ! Logical switch for SST observations\\n/'}
310
311    :param text: Input text to process
312
313    :returns: A dictionary of id, text block key, value pairs
314
315    """
316    # Boundary case
317    if text.startswith("&"):
318        text = "\n" + text
319    # Regular expression
320    results = re.findall(r"\n(&(\w+).*?\n/)", text, re.DOTALL)
321    data = {}
322    for content, namid in results:
323        data[namid] = content
324    return data
325
326def tostring(data):
327    """Maps standard Python data to Fortran namelist format.
328
329    >>> tostring([1, 2, 3])
330    '1 2 3'
331    >>> tostring(["foo.nc", "bar.nc"])
332    "'foo.nc' 'bar.nc'"
333    >>> tostring(True)
334    '.TRUE.'
335
336    :param data: Input Python data
337
338    :returns: Namelist formatted string
339
340    .. seealso:: :func:`sanitize`
341    """
342    if isinstance(data, list):
343        if same_type(data):
344            delim = " "
345        else:
346            delim = ", "
347        text = delim.join([convert(item) for item in data])
348    else:
349        text = convert(data)
350    return text
351
352def numeric(word):
353    # Tests input string is numeric data
354    parts = word.split(" ")
355    try:
356        map(float, parts)
357        flag = True
358    except ValueError:
359        flag = False
360    return flag
361
362def logical(word):
363    # Tests input string is numeric data
364    if word.upper() in [".FALSE.", ".TRUE."]:
365        flag = True
366    else:
367        flag = False
368    return flag
369
370def listed(word):
371    # Tests input string is not a list
372    if ("," in word) or (" " in word):
373        flag = True
374    else:
375        flag = False
376    return flag
377
378def convert(word):
379    # Conversion function
380    if isinstance(word, str):
381        if (quoted(word) or numeric(word) 
382            or logical(word) or listed(word)):
383            result = "%s" % (word,)
384        else:
385            result = "'%s'" % (word,)
386    elif isinstance(word, bool):
387        if word:
388            result = ".TRUE."
389        else:
390            result = ".FALSE."
391    else:
392        result = str(word)
393    return result
394
395def quoted(word):
396    # Checks if string begins/ends with quotation marks
397    if (word.startswith("'") and word.endswith("'")):
398        flag = True
399    elif (word.startswith('"') and word.endswith('"')):
400        flag = True
401    else: 
402        flag = False
403    return flag
404
405def same_type(data):
406    # True if all entries are the same type
407    types = map(type, data)
408    if len(set(types)) == 1:
409        flag = True
410    else:
411        flag = False
412    return flag
413
414def sanitize(data):
415    """Converts dictionary values into Fortran namelist format.
416
417    This is a more typical way to prepare data for inclusion in
418    a Fortran namelist. Instead of manually applying :func:`tostring`
419    to every element of the input data, **sanitize** fixes the entire
420    data set.
421
422    >>> sanitize({"x": True})
423    {'x': '.TRUE.'}
424    >>>
425
426    :param data: Dictionary to convert
427
428    :returns: Dictionary whose values are in Fortran namelist format
429
430    .. seealso:: :func:`tostring`
431    """
432    replacements = [(k, tostring(v)) for k, v in data.items()]
433    data.update(replacements)
434    return data
435
436def new(namid, data=None, convert=True):
437    """Creates a new Fortran namelist
438
439    >>> new("namobs")
440    '\\n&namobs\\n/'
441    >>> print new("namobs")
442    <BLANKLINE>
443    &namobs
444    /
445
446    :param namid: Name for the new namelist
447
448    :keyword data: Specifies an initial dictionary with which to
449                   populate the namelist
450    :type data: dict
451    :keyword convert: Sanitizes input data before replacement takes place
452
453    :returns: string representation of a Fortran namelist
454    """
455    text = "\n&{namid}\n/".format(namid=namid)
456    if data is not None:
457        text = update(namid, text, data, convert=convert)
458    return text
459
460if __name__ == '__main__':
461    import doctest
462    doctest.testmod()
463
Note: See TracBrowser for help on using the repository browser.