"""
Fortran namelist parser. Converts namelists to Python dictionaries.
Based on https://gist.github.com/krischer/4943658.
Should be fairly robust. Cannot be used for verifying fortran namelists as it
is rather forgiving.
Error messages during parsing are kind of messy right now.
Usage
=====
>>> from namelist import namelist2dict
>>> namelist_dict = namelist2dict("fortran_list.txt")
Can deal with filenames, open files and file-like object (StringIO).
Works with Python 2.7 and has not further dependencies.
:copyright:
Lion Krischer (krischer@geophysik.uni-muenchen.de), 2013
:license:
GNU Lesser General Public License, Version 3
(http://www.gnu.org/copyleft/lesser.html)
"""
QUOTE_CHARS = ["'", '"']
class Token(object):
"""
Base class for all token types.
"""
def __str__(self):
name = self.__class__.__name__
if hasattr(self, "value"):
return "%s(%s)" % (name, str(self.value))
return name
def __repr__(self):
return self.__str__()
class StringToken(Token):
def __init__(self, value):
self.value = value
class AssignmentToken(Token):
pass
class GroupEndToken(Token):
pass
class GroupStartToken(Token):
def __init__(self, value):
self.value = value
class IntegerToken(Token):
def __init__(self, value):
self.value = int(value)
class FloatToken(Token):
def __init__(self, value):
self.value = float(value)
class BooleanToken(Token):
def __init__(self, value):
self.value = bool(value)
class NameToken(Token):
def __init__(self, value):
self.value = str(value)
class ComplexNumberToken(Token):
def __init__(self, real, imag):
self.value = complex(real, imag)
class ArrayIndexToken(Token):
def __init__(self, value):
self.value = int(value)
def auto_token(value):
"""
Instantiates the correct token type based on the passed value string.
"""
value = value.strip()
if value.startswith("&"):
return (
GroupEndToken() if value[1:] == 'end'
else GroupStartToken(value[1:]))
elif value.lower() == ".true.":
return BooleanToken(True)
elif value.lower() == ".false.":
return BooleanToken(False)
try:
return IntegerToken(int(value))
except:
pass
try:
return FloatToken(float(value))
except:
pass
return NameToken(value)
def tokenizer(file_object):
"""
The lexer - a generator yielding tokens.
"""
for line in file_object:
line = line.strip()
if not line:
continue
in_name = True
in_string = False
in_complex_number = False
current_token = []
for letter in line:
# Handle strings.
if letter in QUOTE_CHARS:
if in_string is True:
yield StringToken("".join(current_token))
current_token = []
in_string = False
else:
in_string = True
continue
elif in_string is True:
current_token.append(letter)
# Handle array indices and complex numbers.
elif letter == "(":
if current_token:
yield auto_token("".join(current_token))
current_token = []
if not in_name:
in_complex_number = True
elif letter == ")":
if in_name:
# Finished array element index
yield ArrayIndexToken("".join(current_token))
current_token = []
in_name = False
else:
# Parse the complex number.
real, imag = map(float, "".join(current_token).split(","))
yield ComplexNumberToken(real, imag)
current_token = []
in_complex_number = False
elif in_complex_number is True:
current_token.append(letter)
# Everything from now on is neither string nor complex number.
elif letter == "!":
break
elif not letter.strip():
if current_token:
yield auto_token("".join(current_token))
current_token = []
elif letter == ",":
if current_token:
yield auto_token("".join(current_token))
current_token = []
elif letter == "=":
if current_token:
yield auto_token("".join(current_token))
current_token = []
in_name = False
yield AssignmentToken()
elif letter == "/":
if current_token:
yield auto_token("".join(current_token))
current_token = []
yield GroupEndToken()
else:
current_token.append(letter)
if current_token:
yield auto_token("".join(current_token))
def group_generator(tokens):
"""
Generator yielding one dictionary per found group.
"""
current_group = {}
current_group_name = None
current_assignment = []
for token in tokens:
if isinstance(token, GroupStartToken):
if current_group_name:
msg = "Starting new group without ending old one."
raise ValueError(msg)
current_group_name = token.value
continue
elif isinstance(token, GroupEndToken):
if current_assignment:
parse_assignment(current_assignment, current_group)
current_assignment = []
if current_group and current_group_name:
yield (current_group_name, current_group)
current_group = {}
current_group_name = None
continue
elif isinstance(token, NameToken):
if current_assignment:
parse_assignment(current_assignment, current_group)
current_assignment = []
current_assignment.append(token)
def parse_assignment(assignment, group):
"""
Parses all tokens for one assignment. Will write the result to the passed
group dictionary.
"""
if len(assignment) < 3:
msg = "Invalid assignment."
raise ValueError(msg)
if not isinstance(assignment[0], NameToken):
msg = "Assignment must start with a name."
raise ValueError(msg)
if isinstance(assignment[1], AssignmentToken):
values = assignment[2:]
array_assignment = False
elif all((
isinstance(assignment[1], ArrayIndexToken),
isinstance(assignment[2], AssignmentToken),
)):
array_index = assignment[1].value - 1
values = assignment[3:]
array_assignment = True
else:
msg = "Assignment must contain an AssignmentToken."
raise ValueError(msg)
values = [_i.value for _i in values]
if len(values) == 1:
values = values[0]
if not array_assignment:
group[assignment[0].value] = values
else:
try:
group[assignment[0].value].insert(array_index, values)
except KeyError:
if array_index != 0:
msg = "Array element assignments must start at element 1"
raise IndexError(msg)
group[assignment[0].value] = [values]
except IndexError:
msg = 'Array elements must be asigned in order'
raise IndexError(msg)
[docs]
def namelist2dict(file_or_file_object):
"""
Thin wrapper to be able to deal with file-like objects and filenames.
"""
if hasattr(file_or_file_object, "read"):
return _namelist2dict(file_or_file_object)
with open(file_or_file_object, "r") as open_file:
return _namelist2dict(open_file)
def _namelist2dict(file_object):
"""
Converts a file_object containng a namelist to a dictionary.
"""
namelist_dict = {}
for group_name, group_values in group_generator(tokenizer(file_object)):
namelist_dict.setdefault(group_name, [])
namelist_dict[group_name].append(group_values)
return namelist_dict