Source code for imsi.utils.nml_tools

"""
nml_tools
=========

This is a module to collect common namelist functions.

This currently exists because neither f90nml nor the rpn nml tools correctly parse
the nemo namelists. Ideally all these functions would be replaced with more
robust, community packages.

NCS, 10/2021
"""
import os
import re


[docs]def nml_update(nml_input_file, nml_output_file, nml_changes): """ An interface function to update namelist parameters. Inputs: nml_input_file : str path to the namelist file to load in nml_output_file : str path to the updated namelist file to write nml_changes : dict A nested dict containing. At the top level the keys are the names of the namelists in nml_input_file, the values are dicts containing key = value pairs to change in the namelist. This is a basic python implementation of the `mod_nl` routine. Ideally the work would be done by a standard fortran namelist parser, such as f90nml. However, no existing parsers work correctly on NEMO namelists. """ with open(nml_input_file, 'r') as infile: filedata = infile.read() # todo - it could be possible to preserve comments after the replacement by using a more # sophisticated re, or by doing a split on `!` like in the read function below. with open(nml_output_file, 'w') as outfile: for line in filedata.split('\n'): lineout = line for nam, namdict in nml_changes.items(): for k, v in namdict.items(): k0 = re.sub(r'(\(|\))',r'\\\1',k) lineout = re.sub(rf'^ *{k0} *=.*', f'{k} = {v}', lineout) outfile.write(f'{lineout}\n')
[docs]def nml_write(nml_output_file, nml): """ An interface function to write namelists from a dict. Inputs: nml_output_file : str path to the namelist file to write nml : dict A nested dict containing. At the top level the keys are the names of the namelists, the values are dicts containing key = value pairs of the namelist. Ideally the work would be done by a standard fortran namelist parser, such as f90nml. However, no existing parsers work correctly on NEMO namelists. """ with open(nml_output_file, 'w') as outfile: for nam, namdict in nml.items(): outfile.write(f'&{nam}\n') for k, v in namdict.items(): outfile.write(f'{k} = {v}\n') outfile.write(f'/\n')
[docs]def nml_read(nml_input_file): """ An interface function to read namelists into a dict. Inputs: nml_input_file : str path to the namelist file to read Ideally the work would be done by a standard fortran namelist parser, such as f90nml. However, no existing parsers work correctly on NEMO namelists. """ with open(nml_input_file, 'r') as infile: filedata = infile.read() nml = {} for line in filedata.split('\n'): if line.strip().startswith('&'): key = re.sub(r'!.*', '', line).replace('&','').strip() nml[key] = {} continue print(key) if line.strip().startswith('/'): key = 'ERROR' continue if line.strip().startswith('!'): continue if line: key_value_comment = line.split('!',1) key_value = key_value_comment[0] if len(key_value_comment)>1: comment = key_value_comment[1] else: comment = '' k = re.sub(r'=.*', '', key_value).strip() v = re.sub(r'.*=', '', key_value).strip() nml[key][k] = v.strip() #print(f'{k} = {v} !{comment}') return nml
[docs]def cpp_update(cpp_input_file, cpp_output_file, cpp_changes, verbose=False): """ An interface function to update cpp keys. Inputs: cpp_input_file : str path to the cpp file to load in cpp_output_file : str path to the updated cpp file to write cpp_changes : dict key = value pairs to change in the file, where key appears in the default cpp file, and value is the value to replace it with This is a basic python implementation of the `mod_nl` routine. Ideally the work would be done by a standard fortran namelist parser, such as f90nml. However, no existing parsers work correctly on NEMO namelists. """ # Note not yet tested on NEMO .fcm files with open(cpp_input_file, 'r') as infile: filedata = infile.read() with open(cpp_output_file, 'w') as outfile: for line in filedata.split('\n'): lineout = line if ('replace' in cpp_changes.keys()): for k, v in cpp_changes['replace'].items(): if verbose: print(f'in CPP replacing {k} {v}') # likely too general for fcm files, since # multiple keys appear on one line. lineout = re.sub(rf'.*{k}.*', f'{v}', line) outfile.write(f'{lineout}\n') if ('add' in cpp_changes.keys()): for k, v in cpp_changes['add'].items(): lineout = k outfile.write(f'{lineout}\n')
[docs]def update_env_file(infile: str, outfile: str = None, updates: dict = None, commment_char: str = '#', key_value_only: bool = True): """Update simple shell/environment files. Only replaces values of existing keys, i.e. does not add key-value pairs that are not already in the file. If `key_value_only` is True, then only lines that are in the key=value pattern (and commented lines) will be written to the output file. To keep all lines, set `key_value_only=False`. """ comment_char = "#" linepattern = re.compile(r'(\w+)\s*=\s*(.*)') if outfile is None: outfile = infile if not updates: raise ValueError('updates must be a dict with at least one key-value pair') # return with open(infile, 'r') as f: filedata = f.read() try: with open(outfile, 'w') as out: for line in filedata.split('\n'): ls = line.strip() lineout = line if not line or ls.startswith(comment_char): out.write(lineout+'\n') continue m = linepattern.search(ls) if m: param, val_current = m.groups() if param in updates: val_new = str(updates[param]).strip('"') if val_current: lineout = line.replace(val_current, val_new) else: # handles if the value was left unset (empty after =) lineout = f'{line}{val_new}' out.write(lineout+'\n') else: if not key_value_only: # keep the other lines out.write(lineout+'\n') except Exception as e: if infile != outfile and os.path.exists(outfile): os.remove(outfile) raise Exception(f'Error writing file {outfile}; no file produced') from e