diff --git a/README.md b/README.md index 73cfadc..37753f0 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,28 @@ -# Fortran namelist files in Python +# A Python module to parse Fortran namelist files Read in a namelist file: ``` -from namelist_python import read_namelist_file -namelist = read_namelist_file('SIM_CONFIG.nl') +import namelist_python +namelist = namelist_python.read_namelist_file('config.dat') +namelist.groups['foo']['bar'] ``` -`namelist` is an instance of `namelist_python.Namelist` and all groups are -stored in the attribute `groups` with each variable in a nested dictionary -structure (using `OrderedDict` so that the order will be remembered). +This creates an instance of `namelist_python.Namelist` whose attribute +`groups` holds the data in a nested ordered case-insensitive dictionary +structure. Write a `Namelist` object back to a file: ``` -with open('NEW_FILE.nl', 'w') as f: +with open('new_file.dat', 'w') as f: f.write(namelist.dump()) ``` `dump` takes an optional argument `array_inline` a boolean which sets whether arrays should be inline or given in index notation. -If you use ipython there is usefull attribute called `data` which allows you to -do tab completion on the group and variable names, and do assignment: +If you use ipython or a another interactive REPL prompt you may want to use +the `data` attribute which allows you to do tab completion on the group and +variable names: ``` In [7]: namelist.data.ATHAM_SETUP.dt @@ -38,17 +40,17 @@ Out[9]: 4.0 ## Features - Parses ints, floats, booleans, escaped strings and complex numbers. - - Parses arrays in index notation and inlined. - Can output in namelist format. - Tab-completion and variable assignment in interactive console ## Missing features - - Currently can't handle variable definitions across multiple lines + - Currently can't handle line continuations + - Currently can't handle lines with several parameters - Comments are not kept, and so won't exist in output. + - Module does not help to create a Namelist object from scratch ## Contribute Please send any namelist files that don't parse correctly or fix the code -yourself and send me a pull request :) +yourself and send a pull request :) -Thanks, -Leif +Thanks diff --git a/__init__.py b/__init__.py index 3c9c3a1..f270c60 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1,3 @@ -from namelist import read_namelist_file, Namelist, AttributeMapper + +__all__ = ['namelist'] +from .namelist import * diff --git a/namelist.py b/namelist.py old mode 100644 new mode 100755 index c5a0e4d..c530060 --- a/namelist.py +++ b/namelist.py @@ -1,21 +1,23 @@ +#!/usr/bin/python3 + import unittest try: from collections import OrderedDict except ImportError: - from utils import OrderedDict + from .utils import OrderedDict import re -class NoSingleValueFoundException(Exception): - pass - def read_namelist_file(filename): return Namelist(open(filename, 'r').read()) +# trick for py2/3 compatibility +if 'basestring' not in globals(): + basestring = str class AttributeMapper(): """ - Simple mapper to access dictionary items as attributes + Simple mapper to access dictionary items as attributes. """ def __init__(self, obj): @@ -40,197 +42,344 @@ def __setattr__(self, attr, value): def __dir__(self): return self.data.keys() -class Namelist(): +class CaseInsensitiveDict(OrderedDict): """ - Parses namelist files in Fortran 90 format, recognised groups are - available through 'groups' attribute. + This is an ordered dictionary which ignores the letter case of the + keys given (if the respective key is a string). + + Many thanks to user m000 who answered + https://stackoverflow.com/questions/2082152/case-insensitive-dictionary + so helpfully. """ + + @classmethod + def _k(cls, key): + return key.lower() if isinstance(key, basestring) else key + + def __init__(self, *args, **kwargs): + super(CaseInsensitiveDict, self).__init__(*args, **kwargs) + self._convert_keys() + def __getitem__(self, key): + return super(CaseInsensitiveDict, self).__getitem__(self.__class__._k(key)) + def __setitem__(self, key, value): + super(CaseInsensitiveDict, self).__setitem__(self.__class__._k(key), value) + def __delitem__(self, key): + return super(CaseInsensitiveDict, self).__delitem__(self.__class__._k(key)) + def __contains__(self, key): + return super(CaseInsensitiveDict, self).__contains__(self.__class__._k(key)) + def has_key(self, key): + return super(CaseInsensitiveDict, self).has_key(self.__class__._k(key)) + def pop(self, key, *args, **kwargs): + return super(CaseInsensitiveDict, self).pop(self.__class__._k(key), *args, **kwargs) + def get(self, key, *args, **kwargs): + return super(CaseInsensitiveDict, self).get(self.__class__._k(key), *args, **kwargs) + def setdefault(self, key, *args, **kwargs): + return super(CaseInsensitiveDict, self).setdefault(self.__class__._k(key), *args, **kwargs) + def update(self, E={}, **F): + super(CaseInsensitiveDict, self).update(self.__class__(E)) + super(CaseInsensitiveDict, self).update(self.__class__(**F)) + def _convert_keys(self): + for k in list(self.keys()): + v = super(CaseInsensitiveDict, self).pop(k) + self.__setitem__(k, v) + # def to_dict(self): + # import copy + # ret = {} + # for k in self.keys(): + # if isinstance(self[k], CaseInsensitiveDict): + # ret[k] = self[k].to_dict() + # else: + # ret[k] = copy.deepcopy(self[k]) + # return ret - def __init__(self, input_str): - self.groups = OrderedDict() +class Namelist(): + """ + Parses namelist files in Fortran 90 format. - group_re = re.compile(r'&([^&/]+)/', re.DOTALL) # allow blocks to span multiple lines - array_re = re.compile(r'(\w+)\((\d+)\)') - string_re = re.compile(r"\'\s*\w[^']*\'") - self._complex_re = re.compile(r'^\((\d+.?\d*),(\d+.?\d*)\)$') + Note that while Fortran speaks of "namelists", this module uses + the term "group" to refer to individual namelists within a file.. - # remove all comments, since they may have forward-slashes - # TODO: store position of comments so that they can be re-inserted when - # we eventually save - filtered_lines = [] - for line in input_str.split('\n'): - if line.strip().startswith('!'): - continue - else: - filtered_lines.append(line) + After parsing, - group_blocks = re.findall(group_re, "\n".join(filtered_lines)) + nlist = namelist_python.read_namelist_file('input.dat') - for group_block in group_blocks: - block_lines = group_block.split('\n') - group_name = block_lines.pop(0).strip() + recognised groups are accessible through + the 'groups' attribute (which is a case-insensitive ordered dictionary) - group = {} + nlist.groups['mode']['chin'] - for line in block_lines: - line = line.strip() - if line == "": - continue - if line.startswith('!'): - continue + or the data attribute - # commas at the end of lines seem to be optional - if line.endswith(','): - line = line[:-1] + nlist.data.mode.chin - k, v = line.split('=') - variable_name = k.strip() - variable_value = v.strip() + """ - variable_name_groups = re.findall(array_re, k) + def __init__(self, input_str="", parse_strings_unqoted=True): + self.groups = CaseInsensitiveDict() + self.parse_strings_unqoted = parse_strings_unqoted - variable_index = None - if len(variable_name_groups) == 1: - variable_name, variable_index = variable_name_groups[0] - variable_index = int(variable_index)-1 # python indexing starts at 0 + namelist_start_line_re = re.compile(r'^\s*&(\w+)\s*$') + # FIXME the end of the namelist does not necessarily have to + # be in a separate line + namelist_end_line_re = re.compile(r'^\s*/\s*$') - try: - parsed_value = self._parse_value(variable_value) + # a pattern matching an array of stuff + a_number = r'[0-9\.\+\-eE]+' - if variable_index is None: - group[variable_name] = parsed_value - else: - if not variable_name in group: - group[variable_name] = {'_is_list': True} - group[variable_name][variable_index] = parsed_value - - except NoSingleValueFoundException as e: - # see we have several values inlined - if variable_value.count("'") in [0, 2]: - variable_arr_entries = variable_value.split() - else: - # we need to be more careful with lines with escaped - # strings, since they might contained spaces - matches = re.findall(string_re, variable_value) - variable_arr_entries = [s.strip() for s in matches] + # a comma-separated list, of elements which either do not + # contain a comma, or may contain commas inside strings, or + # may contain commas inside paretheses. + # At the end of the line, the comma is optional. + # FIXME strings containing parentheses will cause problems with this expression + array_re = re.compile(r"(\s*(?:[0-9]+\*)?(?:[^,(\'\"]+|\'[^\']*\'|\"[^\']*\"|[(][^),]+,[^),]+[)])\s*)\s*(?: |,|,?\s*$)") + self._complex_re = re.compile(r'\s*\([^,]+,[^,]+\)\s*') + # a pattern to match the non-comment part of a line. This + # should be able to deal with ! signs inside strings. + comment_re = re.compile(r"((?:[^\'!]*(?:\'[^\']*\'))*)!.*") - for variable_index, inline_value in enumerate(variable_arr_entries): - parsed_value = self._parse_value(inline_value) + # match notation for Fortran logicals in namelist files: + self.logical_true_re = re.compile(r"[^tTfF\']*[tT].*") + self.logical_false_re = re.compile(r"[^tTfF\']*[fF].*") - if variable_index is None: - group[variable_name] = parsed_value - else: - if not variable_name in group: - group[variable_name] = {'_is_list': True} - group[variable_name][variable_index] = parsed_value + # match abbreviated lists of identical items, like + # 509*-1.0000000000000000 + # 253*0 + # NEO_EQUIL_PARSE_SP_SEQ= 1, 2, 2*3 , 28*-1 + # 60*" " + self.abbrev_list_re = re.compile(r"\s*([0-9]+)\*(.+)\s*") - self.groups[group_name] = group + # commas at the end of lines seem to be optional + keyval_line_re = re.compile(r"\s*([\w\(\)]+)\s*=\s*(.*),?") - self._check_lists() + # detect index notation for arrays + array_index_notation_re = re.compile(r"\s*(\w+)\(([0-9])+\)\s*") - def _parse_value(self, variable_value): - """ - Tries to parse a single value, raises an exception if no single value is matched - """ - try: - parsed_value = int(variable_value) - except ValueError: - try: - parsed_value = float(variable_value) - except ValueError: - # check for complex number - complex_values = re.findall(self._complex_re, variable_value) - if len(complex_values) == 1: - a, b = complex_values[0] - parsed_value = complex(float(a),float(b)) - elif variable_value in ['.true.', 'T']: - # check for a boolean - parsed_value = True - elif variable_value in ['.false.', 'F']: - parsed_value = False - else: - # see if we have an escaped string - if variable_value.startswith("'") and variable_value.endswith("'") and variable_value.count("'") == 2: - parsed_value = variable_value[1:-1] - elif variable_value.startswith('"') and variable_value.endswith('"') and variable_value.count('"') == 2: - parsed_value = variable_value[1:-1] + current_group = None + for line in input_str.split('\n'): + # remove comments and whitespaces + line_without_comment = comment_re.sub(r"\1",line).strip() + + if len(line_without_comment) == 0: + continue + + m = namelist_start_line_re.match(line_without_comment) + if(m): + found_group = m.group(1).lower() + if(current_group is None): + if(found_group in self.groups): + if(not isinstance(self.groups[found_group],list)): + self.groups[found_group] = [self.groups[found_group]] + + self.groups[found_group].append(CaseInsensitiveDict()) + current_group = self.groups[found_group][-1] + else: - raise NoSingleValueFoundException(variable_value) + self.groups[found_group] = CaseInsensitiveDict() + current_group = self.groups[found_group] + + continue + else: + raise SyntaxError('Namelist %s starts, but namelist %s is not yet complete.' % (found_group,current_group)) - return parsed_value + m = namelist_end_line_re.match(line_without_comment) + if(m): + if(current_group is not None): + current_group = None + continue + else: + raise SyntaxError('End of namelist encountered, but there is no corresponding open namelist.') + + # other lines: key = value, or a continuation line + m = keyval_line_re.match(line_without_comment) + if(m): + if(current_group is not None): + variable_name = m.group(1) + variable_value = m.group(2) + + # check if this is in array index notation + m_ind_notation = array_index_notation_re.match(variable_name) + if(m_ind_notation): + variable_name = m_ind_notation.group(1) + # Fortran indexing is 1-based, + # but used Python indexing here + index = int(m_ind_notation.group(2))-1 + #print("index notation: %s %i" % (variable_name, index)) + if(variable_name not in current_group): + current_group[variable_name] = (index+1)*[None] + elif(len(current_group[variable_name]) <= index): + current_group[variable_name].extend((index+1-len(current_group[variable_name]))*[None]) + + # parse the array with self-crafted regex + parsed_list = array_re.findall(variable_value) + parsed_list = [self._parse_value(elem) for elem in parsed_list] + parsed_list = self._flatten(parsed_list) + + # if it wasnt for special notations like .false. or 60*'' , one could + # simply use a parser from the python standard library for the right + # hand side as a whole: + #parsed_value = ast.literal_eval(variable_value) + try: + if(len(parsed_list) == 1): + if(m_ind_notation): + current_group[variable_name][index] = parsed_list[0] + else: + current_group[variable_name] = parsed_list[0] + else: + if(m_ind_notation): + current_group[variable_name][index] = parsed_list + else: + current_group[variable_name] = parsed_list + except TypeError: + if(m_ind_notation): + current_group[variable_name][index] = parsed_list + else: + current_group[variable_name] = parsed_list + else: + raise SyntaxError('Key %s encountered, but there is no enclosing namelist' % variable_name) + else: + warning_text = 'this line could not be parsed: %s' % line_without_comment + print("WARNING: %s" % warning_text) + #raise SyntaxError(warning_text) - def _check_lists(self): - for group in self.groups.values(): - for variable_name, variable_values in group.items(): - if isinstance(variable_values, dict): - if '_is_list' in variable_values and variable_values['_is_list']: - variable_data = variable_values - del(variable_data['_is_list']) + def _parse_value(self, variable_value_str): + """ + Tries to parse a single value, raises a SyntaxError if not successful. + """ + import ast + try: + parsed_value = ast.literal_eval(variable_value_str.strip()) - num_entries = len(variable_data.keys()) - variable_list = [None]*num_entries + # use a regex to check if value is a complex number: (1.2 , 3.4) + # this is needed, because literal_eval parses both "(1.2 , 3.4)" + # and "1.2 , 3.4" into a tupel with two elements and then one + # cannot distinguish between a list of two numbers and a single + # complex number. This makes a difference when it comes to + # dumping, though. + if(self._complex_re.match(variable_value_str)): + parsed_value = complex(parsed_value[0], parsed_value[1]) - for i, value in variable_data.items(): - if i >= num_entries: - raise Exception("The variable '%s' has an array index assignment that is inconsistent with the number of list values" % variable) - else: - variable_list[i] = value + try: + if(isinstance(parsed_value, basestring)): + # Fortran strings end with blanks + parsed_value = parsed_value.rstrip() + else: + parsed_value = [elem.rstrip() for elem in parsed_value] + except Exception as err: + # value is probably just not iterable, or is an iterable of numbers + pass + except (ValueError, SyntaxError): + + abbrev_list_match = self.abbrev_list_re.match(variable_value_str) + if(abbrev_list_match): + parsed_value = int(abbrev_list_match.group(1)) * [self._parse_value(abbrev_list_match.group(2))] + elif(self.logical_true_re.match(variable_value_str) and + (variable_value_str.lower() in ['true','.true','.true.','t'] or not self.parse_strings_unqoted)): + parsed_value = True + if(variable_value_str.lower() not in ['true','.true','.true.','t'] and not self.parse_strings_unqoted): + print("WARNING: value %s was parsed to boolean %s" % (variable_value_str, parsed_value)) + elif(self.logical_false_re.match(variable_value_str) and + (variable_value_str.lower() in ['false','.false','.false.','f'] or not self.parse_strings_unqoted)): + parsed_value = False + if(variable_value_str.lower() not in ['false','.false','.false.','f'] and not self.parse_strings_unqoted): + print("WARNING: value %s was parsed to boolean %s" % (variable_value_str, parsed_value)) + else: + quoted = "'" + variable_value_str.strip()+"'" + try: + parsed_value = ast.literal_eval(quoted) + print("WARNING: value %s was treated as %s" % (variable_value_str, quoted)) + except: + raise SyntaxError('Right hand side expression could not be parsed. The string is: %s' % (variable_value_str)) - group[variable_name] = variable_list + #FIXME distinguish complex scalar and a list of 2 reals + try: + if(len(parsed_value) == 1): + # one gets a list of length 1 if the line ends with a + # comma, because (4,) for python is a tuple with one + # element, and (4) is just the scalar 4. + return parsed_value[0] + else: + return parsed_value + except TypeError: + return parsed_value + - def dump(self, array_inline=True): + def dump(self, array_inline=True, float_format="%13.5e"): lines = [] - for group_name, group_variables in self.groups.items(): - lines.append("&%s" % group_name) - for variable_name, variable_value in group_variables.items(): - if isinstance(variable_value, list): - if array_inline: - lines.append("%s= %s" % (variable_name, " ".join([self._format_value(v) for v in variable_value]))) + for group_name, group_content in self.groups.items(): + + group_list = isinstance(group_content,list) and group_content or [group_content] + for group in group_list: + lines.append("&%s" % group_name.upper()) + for variable_name, variable_value in group.items(): + if(isinstance(variable_value, list) or isinstance(variable_value, tuple)): + if(array_inline and None not in variable_value): + lines.append("%s= %s" % (variable_name, ", ".join([self._format_value(elem, float_format) for elem in variable_value]))) + else: + for n, v in enumerate(variable_value): + if(v is not None): + lines.append("%s(%d)= %s" % (variable_name, n+1, self._format_value(v, float_format))) else: - for n, v in enumerate(variable_value): - lines.append("%s(%d)=%s" % (variable_name, n+1, self._format_value(v))) - else: - lines.append("%s=%s" % (variable_name, self._format_value(variable_value))) - lines.append("/") + lines.append("%s=%s" % (variable_name, self._format_value(variable_value, float_format))) + lines.append("/") + lines.append("") return "\n".join(lines) - def _format_value(self, value): + def _flatten(self, x): + result = [] + for elem in x: + if hasattr(elem, "__iter__") and not isinstance(elem, basestring): + result.extend(self._flatten(elem)) + else: + result.append(elem) + return result + + def _format_value(self, value, float_format): if isinstance(value, bool): return value and '.true.' or '.false.' elif isinstance(value, int): return "%d" % value elif isinstance(value, float): - return "%f" % value - elif isinstance(value, str): + return float_format % value + elif isinstance(value, basestring): return "'%s'" % value elif isinstance(value, complex): - return "(%s,%s)" % (self._format_value(value.real), self._format_value(value.imag)) + complex_format = "("+float_format+","+float_format+")" + return complex_format % (value.real,value.imag) else: - raise Exception("Variable type not understood: %s" % type(value)) + print(value) + raise Exception("Variable type not understood: type %s" % type(value)) + # create a read-only propery by using property() as a + # decorator. This function is then the getter function for the + # .data attribute: @property def data(self): return AttributeMapper(self.groups) class ParsingTests(unittest.TestCase): + + def __init__(self, methodName='runTest'): + super().__init__(methodName) + self.maxDiff = None + def test_single_value(self): input_str = """ &CCFMSIM_SETUP - CCFMrad=800.0 + ccfmrad=800.0 / """ namelist = Namelist(input_str) - expected_output = {'CCFMSIM_SETUP': { 'CCFMrad': 800. }} + expected_output = {'ccfmsim_setup': { 'ccfmrad': 800. }} self.assertEqual(namelist.groups, expected_output) def test_multigroup(self): input_str = """ &CCFMSIM_SETUP - CCFMrad=800.0 + ccfmrad=800.0 / &GROUP2 R=500.0 @@ -238,8 +387,8 @@ def test_multigroup(self): """ namelist = Namelist(input_str) - expected_output = {'CCFMSIM_SETUP': { 'CCFMrad': 800. }, - 'GROUP2': { 'R': 500. }} + expected_output = {'ccfmsim_setup': { 'ccfmrad': 800. }, + 'group2': { 'r': 500. }} self.assertEqual(namelist.groups, expected_output) @@ -247,17 +396,17 @@ def test_comment(self): input_str = """ ! Interesting comment at the start &CCFMSIM_SETUP - CCFMrad=800.0 + ccfmrad=800.0 ! And a comment some where in the middle / &GROUP2 - R=500.0 + r=500.0 / """ namelist = Namelist(input_str) - expected_output = {'CCFMSIM_SETUP': { 'CCFMrad': 800. }, - 'GROUP2': { 'R': 500. }} + expected_output = {'ccfmsim_setup': { 'ccfmrad': 800. }, + 'group2': { 'r': 500. }} self.assertEqual(namelist.groups, expected_output) @@ -278,7 +427,7 @@ def test_array(self): namelist = Namelist(input_str) expected_output = { - 'CCFMSIM_SETUP': { + 'ccfmsim_setup': { 'ntrac_picture': 4, 'var_trac_picture': [ 'watcnew', @@ -304,7 +453,7 @@ def test_boolean_sciformat(self): nz =300 zstart =0. ztotal =15000. - dzzoom =50. + dzzoom =50.012345e-15 kcenter =20 nztrans =0 nztrans_boundary =6 @@ -318,11 +467,11 @@ def test_boolean_sciformat(self): namelist = Namelist(input_str) expected_output = { - 'ATHAM_SETUP': { + 'atham_setup': { 'nz': 300, 'zstart': 0., 'ztotal': 15000., - 'dzzoom': 50., + 'dzzoom': 50.012345e-15, 'kcenter': 20, 'nztrans': 0, 'nztrans_boundary': 6, @@ -338,18 +487,18 @@ def test_comment_with_forwardslash(self): input_str = """ ! Interesting comment at the start &CCFMSIM_SETUP - CCFMrad=800.0 + ccfmrad=800.0 ! And a comment some where in the middle/halfway ! var2=40 / &GROUP2 - R=500.0 + r=500.0 / """ namelist = Namelist(input_str) - expected_output = {'CCFMSIM_SETUP': { 'CCFMrad': 800., 'var2': 40 }, - 'GROUP2': { 'R': 500. }} + expected_output = {'ccfmsim_setup': { 'ccfmrad': 800., 'var2': 40 }, + 'group2': { 'r': 500. }} self.assertEqual(namelist.groups, expected_output) @@ -366,42 +515,42 @@ def test_inline_array(self): !&BOGUS rko=1 / ! &TTDATA - TTREAL = 1., - TTINTEGER = 2, - TTCOMPLEX = (3.,4.), - TTCHAR = 'namelist', - TTBOOL = T/ + ttreal = 1., + ttinteger = 2, + ttcomplex = (3.,4.), + ttchar = 'namelist', + ttbool = T/ &AADATA - AAREAL = 1. 1. 2. 3., - AAINTEGER = 2 2 3 4, - AACOMPLEX = (3.,4.) (3.,4.) (5.,6.) (7.,7.), - AACHAR = 'namelist' 'namelist' 'array' ' the lot', - AABOOL = T T F F/ + aareal = 1. 1. 2. 3., + aainteger = 2 2 3 4, + aacomplex = (3.,4.) (3.,4.) (5.,6.) (7.,7.), + aachar = 'namelist' 'namelist' 'array' ' the lot', + aabool = T T F F/ &XXDATA - XXREAL = 1., - XXINTEGER = 2, - XXCOMPLEX = (3.,4.)/! can have blank lines and comments in the namelist input file + xxreal = 1., + xxinteger = 2, + xxcomplex = (3.,4.)/! can have blank lines and comments in the namelist input file """ expected_output = { - 'TTDATA': { - 'TTREAL': 1., - 'TTINTEGER': 2, - 'TTCOMPLEX': 3. + 4.j, - 'TTCHAR': 'namelist', - 'TTBOOL': True, + 'ttdata': { + 'ttreal': 1., + 'ttinteger': 2, + 'ttcomplex': 3. + 4.j, + 'ttchar': 'namelist', + 'ttbool': true, }, - 'AADATA': { - 'AAREAL': [1., 1., 2., 3.,], - 'AAINTEGER': [2, 2, 3, 4], - 'AACOMPLEX': [3.+4.j, 3.+4.j, 5.+6.j, 7.+7.j], - 'AACHAR': ['namelist', 'namelist', 'array', ' the lot'], - 'AABOOL': [True, True, False, False], + 'aadata': { + 'aareal': [1., 1., 2., 3.,], + 'aainteger': [2, 2, 3, 4], + 'aacomplex': [3.+4.j, 3.+4.j, 5.+6.j, 7.+7.j], + 'aachar': ['namelist', 'namelist', 'array', ' the lot'], + 'aabool': [True, True, False, False], }, - 'XXDATA': { - 'XXREAL': 1., - 'XXINTEGER': 2., - 'XXCOMPLEX': 3.+4.j, + 'xxdata': { + 'xxreal': 1., + 'xxinteger': 2., + 'xxcomplex': 3.+4.j, }, } @@ -409,22 +558,24 @@ def test_inline_array(self): self.assertEqual(dict(namelist.groups), expected_output) -class ParsingTests(unittest.TestCase): def test_single_value(self): input_str = """&CCFMSIM_SETUP -CCFMrad=800. -/""" +ccfmrad= 8.00000e+02 +/ +""" namelist = Namelist(input_str) self.assertEqual(namelist.dump(), input_str) def test_multigroup(self): input_str = """&CCFMSIM_SETUP -CCFMrad=800. +ccfmrad= 8.00000e+02 / + &GROUP2 -R=500. -/""" +r= 5.00000e+02 +/ +""" namelist = Namelist(input_str) self.assertEqual(namelist.dump(), input_str) @@ -432,30 +583,57 @@ def test_multigroup(self): def test_array(self): input_str = """&CCFMSIM_SETUP -var_trac_picture(1)='watcnew' -var_trac_picture(2)='watpnew' -var_trac_picture(3)='icecnew' -var_trac_picture(4)='granew' -des_trac_picture(1)='cloud_water' -des_trac_picture(2)='rain' -des_trac_picture(3)='cloud_ice' -des_trac_picture(4)='graupel' -/""" +var_trac_picture(1)= 'watcnew' +var_trac_picture(2)= 'watpnew' +var_trac_picture(3)= 'icecnew' +var_trac_picture(4)= 'granew' +des_trac_picture(1)= 'cloud_water' +des_trac_picture(2)= 'rain' +des_trac_picture(3)= 'cloud_ice' +des_trac_picture(4)= 'graupel' +/ +""" namelist = Namelist(input_str) self.assertEqual(namelist.dump(array_inline=False), input_str) def test_inline_array(self): input_str = """&AADATA -AACOMPLEX= (3.,4.) (3.,4.) (5.,6.) (7.,7.) -/""" +aacomplex= (3.000000,4.000000) (3.000000,4.000000) (5.000000,6.000000) (7.000000,7.000000) +bbcomplex= ( 3.00000e+00, 4.00000e+00), ( 3.00000e+00, 4.00000e+00), ( 5.00000e+00, 6.00000e+00), ( 7.00000e+00, 7.00000e+00) +/ +""" namelist = Namelist(input_str) + expected_output = { + 'aadata': { + 'aacomplex': [complex(3.,4.),complex(3.,4.),complex(5.,6.),complex(7.,7.),], + 'bbcomplex': [complex(3.,4.),complex(3.,4.),complex(5.,6.),complex(7.,7.),] + }} + self.assertEqual(dict(namelist.groups), expected_output) - print input_str - print namelist.dump() + def test_inline_array_dump(self): + input_str = """&DATA +temp_coef= 1.00000000, 5.00000000, 0.18077700, 0.28930000, 0.01450000, +dens_coef=2.00000000,3.00000000,4.18077700,5.28930000,6.01450000 +/ +""" + namelist = Namelist(input_str) + expected_output = { + 'data': { + 'temp_coef': [1.00000000, 5.00000000, 0.18077700, 0.28930000, 0.01450000,], + 'dens_coef': [2.00000000,3.00000000,4.18077700,5.28930000,6.01450000] + }} + self.assertEqual(dict(namelist.groups), expected_output) + + expected_output = """&DATA +temp_coef= 1.00000e+00, 5.00000e+00, 1.80777e-01, 2.89300e-01, 1.45000e-02 +dens_coef= 2.00000e+00, 3.00000e+00, 4.18078e+00, 5.28930e+00, 6.01450e+00 +/ +""" + + self.assertEqual(namelist.dump(), expected_output) - self.assertEqual(namelist.dump(), input_str) if __name__=='__main__': unittest.main()