-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replacement Options File Parser #97
Open
TomNicholas
wants to merge
20
commits into
master
Choose a base branch
from
boutoptions-newclass
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
6848e66
Created basic options file reader
TomNicholas 87b7007
Added blank lines between imports and definitions
TomNicholas 0a83776
example input file was empty
TomNicholas acfb401
Dealt with BOUT.inp's missing first section header
TomNicholas 8c6792c
Added (failing) tests to check nesting is handled
TomNicholas 4f2027a
Copied boutdata.BoutOptionsFile - file read tests pass
TomNicholas dbb0476
Added ToDos for all the changes I want to make
TomNicholas a107057
Wrote beginnings of Section class
TomNicholas b10cae3
Wrote basic attempt at whole BoutOptionsFile class
TomNicholas 4dbbeca
Added write methods
TomNicholas b71fc34
Section class passes get/set tests
TomNicholas c9df34c
Can now find parents
TomNicholas 5069c97
Now gets all sections, including lineages
TomNicholas bf13031
Now prints recursively
TomNicholas 59591e4
test for print method
TomNicholas 5a68243
Write to file
TomNicholas 3c3859f
string evaluation
TomNicholas ee9acf9
Generalised comment detector to find ';'
TomNicholas f3927f9
Added various ToDos discovered
TomNicholas e4cc1b2
Added some substitution tests
TomNicholas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,364 @@ | ||
from collections import UserDict | ||
from pathlib import Path | ||
|
||
|
||
# These are imported to be used by 'evaluate=True' in Section.get(). | ||
# Change the names to match those used by C++/BOUT++ | ||
from numpy import (pi, sin, cos, tan, arccos as acos, arcsin as asin, | ||
arctan as atan, arctan2 as atan2, sinh, cosh, tanh, | ||
arcsinh as asinh, arccosh as acosh, arctanh as atanh, | ||
exp, log, log10, power as pow, sqrt, ceil, floor, | ||
round, abs) | ||
|
||
# TODO file reader | ||
# TODO substitution of keys within strings | ||
# TODO Sanitise some of the weirder things BOUT's option parser does: | ||
# - Detect escaped arithmetic symbols (+-*/^), dot (.), or brackets ((){}[]) in option names | ||
# - Detect escaping through backquotes in option names (`2ndvalue`) | ||
# - Evaluate numletter as num*letter (and numbracket as num*bracket) | ||
# - Substitute unicode representation of pi | ||
# - Ensure option names don't contain ':' or '=' | ||
# - Check if all expressions are rounded to nearest integer?! | ||
# TODO ability to read from/write to nested dictionary | ||
|
||
|
||
SECTION_DELIM = ':' | ||
COMMENT_DELIM = ['#', ';'] | ||
INDENT_STR = '|-- ' | ||
BOOLEAN_STATES = {'true': True, 'on': True, | ||
'false': False, 'off': False} | ||
|
||
|
||
class Section(UserDict): | ||
""" | ||
Section of an options file. | ||
|
||
This section can have multiple sub-sections, and each section stores the | ||
options as multiple key-value pairs. | ||
|
||
Parameters | ||
---------- | ||
name : str | ||
Name of the section | ||
data : dict, optional | ||
parent : str, Section or None | ||
The parent Section | ||
""" | ||
|
||
# Inheriting from UserDict gives keys, items, values, iter, del, & len | ||
# methods already, and the data attribute for storing the contents | ||
|
||
def __init__(self, name, parent=None, data=None): | ||
if data is None: data = {} | ||
super().__init__(data) | ||
|
||
self.name = name | ||
# TODO check if parent has a section with the same name? | ||
self._parent = parent | ||
if isinstance(parent, Section): | ||
self.parent = parent.name | ||
else: | ||
self.parent = parent | ||
|
||
def __getitem__(self, key): | ||
return self.get(key, evaluate=False, keep_comments=False) | ||
|
||
def get(self, key, substitute=False, evaluate=False, keep_comments=False): | ||
""" | ||
Fetch the value stored under a certain key. | ||
|
||
Parameters | ||
---------- | ||
key : str | ||
substitute : bool, optional (default: False) | ||
If value contains other values, referenced as `section:key`, will | ||
substitute them in to the value string. | ||
evaluate : bool, optional (default: False) | ||
If true, attempts to evaluate the value as an expression by | ||
substituting in other values from the options file. Other values | ||
are specified by key, or by section:key. Also sets substitute=True. | ||
If false, will return value as an unmodified string. | ||
keep_comments : bool, optional (default: False) | ||
If false, will strip off any inline comments, delimited by '#', and | ||
any whitespace before returning. | ||
If true, will return the whole line. | ||
""" | ||
|
||
if evaluate and keep_comments: | ||
# TODO relax this? | ||
raise ValueError("Cannot keep comments and evaluate the contents.") | ||
|
||
line = self.data[key] | ||
if keep_comments or isinstance(line, Section): | ||
return line | ||
else: | ||
# any comment will be discarded, along with any trailing whitespace | ||
for delim in COMMENT_DELIM: | ||
line, *comments = line.split(delim) | ||
value = line.rstrip() | ||
|
||
if substitute or evaluate: | ||
value = self._substitute_keys_within(value) | ||
|
||
if evaluate: | ||
return self.evaluate(value) | ||
else: | ||
return value | ||
|
||
def __setitem__(self, key, value): | ||
self.set(key, value) | ||
|
||
def set(self, key, value): | ||
""" | ||
Store a value under a certain key. | ||
|
||
Will cast the value to a string (unless value is a new section). | ||
If a dictionary is passed, will create a new Section. | ||
|
||
Parameters | ||
---------- | ||
key : str | ||
value : str | ||
""" | ||
|
||
if isinstance(value, dict): | ||
value = Section(name=key, parent=self, data=value) | ||
if not isinstance(value, Section): | ||
value = str(value) | ||
self.data[key] = value | ||
|
||
def lineage(self): | ||
""" | ||
Returns the full route to this section by working back up the tree. | ||
|
||
Returns | ||
------- | ||
str | ||
""" | ||
if self._parent is None or self._parent == 'root': | ||
return self.name | ||
else: | ||
return self._parent.lineage() + SECTION_DELIM + self.name | ||
|
||
def _find_sections(self, sections): | ||
""" | ||
Recursively find a list of all the section objects below this one, and | ||
append them to the list passed before returning them all. | ||
""" | ||
for key in self.keys(): | ||
val = self[key] | ||
if isinstance(val, Section): | ||
sections.append(val) | ||
sections = val._find_sections(sections) | ||
return sections | ||
|
||
def sections(self): | ||
""" | ||
Returns a list of all sections contained, including nested ones. | ||
|
||
Returns | ||
------- | ||
list of Section objects | ||
""" | ||
return self._find_sections([self]) | ||
|
||
def __str__(self): | ||
depth = self.lineage().count(SECTION_DELIM) | ||
text = INDENT_STR * depth + f"[{self.name}]\n" | ||
for key, val in self.items(): | ||
if isinstance(val, Section): | ||
text += str(val) | ||
else: | ||
text += INDENT_STR * (depth+1) + f"{key} = {val}\n" | ||
return text | ||
|
||
def _write(self, file, substitute=False, evaluate=False, | ||
keep_comments=True): | ||
""" | ||
Writes out a single section to an open file object as | ||
[...:parent:section] | ||
key = value # comment | ||
... | ||
|
||
If it encounters a nested section it will recursively call this method | ||
on that nested section. | ||
""" | ||
|
||
if self.name is not None and self.name != 'root': | ||
section_header = f"[{self.lineage()}]\n" | ||
file.write(section_header) | ||
|
||
for key in self.keys(): | ||
entry = self.get(key, substitute, evaluate, keep_comments) | ||
|
||
if isinstance(entry, Section): | ||
# Recursively write sections | ||
file.write("\n") | ||
entry._write(file, substitute, evaluate, keep_comments) | ||
else: | ||
file.write(f"{key} = {entry}\n") | ||
|
||
def __repr__(self): | ||
return f"Section(name='{self.name}', parent='{self.parent}', " \ | ||
f"data={self.data})" | ||
|
||
@staticmethod | ||
def evaluate(value): | ||
""" | ||
Evaluates the string using eval, following the conventions of BOUT++. | ||
|
||
Parameters | ||
---------- | ||
value : str | ||
""" | ||
|
||
if value.lower() in BOOLEAN_STATES: | ||
# Treat booleans separately to cover lowercase 'true' etc. | ||
return BOOLEAN_STATES[value.lower()] | ||
else: | ||
if '^' in value: | ||
value = value.replace('^', '**') | ||
return eval(value) | ||
|
||
def _substitute_keys_within(self, value): | ||
# Detect if any colons | ||
|
||
# Parse candidate keys contained | ||
|
||
# Loop over candidate keys | ||
# Search for matches in whole tree | ||
# If found, replace, if not, throw error | ||
|
||
# If any keys left? | ||
# Check none of them point to this value (i.e. no cycles) | ||
# Call _substitute_keys again | ||
# Otherwise return ready for evaluation | ||
return value | ||
|
||
|
||
class OptionsTree(Section): | ||
""" | ||
Tree of options, with the same structure as a complete BOUT++ input file. | ||
|
||
This class represents a tree structure. Each section (Section object) can | ||
have multiple sub-sections, and each section stores the options as multiple | ||
key-value pairs. | ||
|
||
Examples | ||
-------- | ||
|
||
>>> opts = OptionsTree() # Create a root | ||
|
||
Specify value of a key in a section "test" | ||
|
||
>>> opts["test"]["key"] = 4 | ||
|
||
Get the value of a key in a section "test" | ||
If the section does not exist then a KeyError is raised | ||
|
||
>>> print(opts["test"]["key"]) | ||
4 | ||
|
||
To pretty print the options | ||
|
||
>>> print(opts) | ||
root | ||
|- test | ||
| |- key = 4 | ||
|
||
""" | ||
|
||
def __init__(self, data=None): | ||
super().__init__(name='root', data=data, parent=None) | ||
|
||
# TODO .as_dict() ? | ||
|
||
def write_to(self, file, substitute=False, evaluate=False, | ||
keep_comments=True, lower=False): | ||
""" | ||
Writes out contents to a file, following the format of a BOUT.inp file. | ||
|
||
Parameters | ||
---------- | ||
file | ||
evaluate : bool, optional (default: False) | ||
If true, attempts to evaluate the value as an expression by | ||
substituting in other values from the options file, before writing. | ||
Other values are specified by key, or by section:key. | ||
If false, will write value as an unmodified string. | ||
keep_comments : bool, optional (default: False) | ||
If false, will strip off any inline comments, delimited by '#', and | ||
any whitespace before writing. | ||
If true, will write out the whole line. | ||
|
||
Returns | ||
------- | ||
filepath : str | ||
Full path of output file written to | ||
""" | ||
|
||
with open(Path(file), 'w') as f: | ||
self._write(file=f, substitute=False, evaluate=False, | ||
keep_comments=True) | ||
return str(Path(file).resolve()) | ||
|
||
def _read_from(self, filepath, lower): | ||
with open(filepath, 'r') as f: | ||
# TODO add in first section header? | ||
#for l in f: | ||
# line = Line(l) | ||
# if line.is_section(): | ||
# | ||
# name | ||
# self.data[name] = Section(name, parent, data) | ||
# elif line.is_comment() or line.is_empty() | ||
# continue | ||
# else: | ||
data = None | ||
return data, str(filepath.resolve()) | ||
|
||
|
||
class OptionsFile(OptionsTree): | ||
""" | ||
The full contents of a particular BOUT++ input file. | ||
|
||
This class represents a tree structure of options, all loaded from `file`. | ||
Each section (Section object) can have multiple sub-sections, and each | ||
section stores the options as multiple key-value pairs. | ||
|
||
Parameters | ||
---------- | ||
file : str or path-like object | ||
Path to file from which to read input options. | ||
lower : bool, optional (default: False) | ||
If true, converts all strings to lowercase on reading. | ||
""" | ||
|
||
# TODO gridfile stuff? | ||
|
||
def __init__(self, file, lower=False): | ||
contents, self.file = self._read_from(Path(file), lower) | ||
super().__init__(data=contents) | ||
|
||
def write(self, substitute=False, evaluate=False, keep_comments=True): | ||
""" | ||
Writes out contents to the file, following the BOUT.inp format. | ||
|
||
Parameters | ||
---------- | ||
evaluate : bool, optional (default: False) | ||
If true, attempts to evaluate the value as an expression by | ||
substituting in other values from the options file, before writing. | ||
Other values are specified by key, or by section:key. | ||
If false, will write value as an unmodified string. | ||
keep_comments : bool, optional (default: False) | ||
If false, will strip off any inline comments, delimited by '#', and | ||
any whitespace before writing. | ||
If true, will write out the whole line. | ||
""" | ||
|
||
self.write_to(self.file, substitute, evaluate, keep_comments) | ||
|
||
def __repr__(self): | ||
# TODO Add grid-related options | ||
return f"OptionsFile(file='{self.file}')" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BOUT++ has dropped support for
;
as a comment character, I doubt you'll come across an input file that uses them