Package treeconfigparser
Custom configuration parser based on a tree.
This package provides the TreeConfigParser
class, which
can be used to parse configuration files.
The options are organised using a hierarchy of sections
and can be accessed using the TreeConfigParser.get()
method.
Main Usage
A configuration file can be read and parsed as follows:
>>> config = TreeConfigParser()
>>> config.read_file(file_name)
Equivalently, one can use the fromfile()
function as follows:
>>> config = fromfile(file_name)
Supported Formats
Currently, three formats are supported for configuration files: the 'py' format, the 'flat' format, and the 'cpp' format. The 'py' and 'cpp' formats support an arbitrary hierarchy depth, while the 'flat' format only supports one level of hierarchy.
The following option hierarchy:
section1
|
- option1 = val1
section2
|
- option2 = val2
can be constructed with this file under the 'flat' format:
[section1]
option1 = val1
[section2]
option2 = val2
The following option hierarchy:
section1
|
- option1 = val1
|
- subsection11
| |
| - option2 = val2
|
- subsection12
| |
| - option3 = val3
can be constructed with this file under the 'py' format:
[section1]
option1 = val1
[subsection11]
option2 = val2
[subsection12]
option3 = val3
with this file under the 'cpp' format:
section1.option1 = val1
section1.subsection11.option2 = val2
section1.subsection12.option3 = val3
and cannot be constructed under the 'flat' format.
Comments can be included in the configuration file using a dedicated character (usually '#'). Everything which is on the right of this comment character is ignored by the parser.
The parser supports cross-references in the file. For example, the following ('py'-formatted) file:
[section1]
option1 = a
option2 = %section1.option1%b
is translated into the following option hierarchy:
section1
|
- option1 = a
|
- option2 = ab
as long as the cross-reference character is set to '%'.
Access To Options
Once the file has been parsed, the options can be accessed using the section and option names. For example, if the option hierarchy is
section1
|
- option1 = 1
|
- subsection11
| |
| - option2 = 2
|
- subsection12
| |
| - option3 = 3
then individual options can be accessed as follows:
>>> config.get(['section1', 'option1'])
'1'
>>> config.get(['section1', 'subsection11', 'option2'])
'2'
>>> config.get(['section1', 'subsection12', 'option3'])
'3'
Optionnaly, an option can be converted to a different type
using the string_conversion()
function:
>>> config.get(['section1', 'option1'], to_type='int')
1
Programmatic Usage
Alternatively, options can be added to a configuration using
the TreeConfigParser.set()
method. For example, the
following option hierarchy:
section1
|
- option1 = 1
|
- subsection11
| |
| - option2 = 2
|
- subsection12
| |
| - option3 = 3
can be constructed as follows:
>>> config = TreeConfigParser()
>>> config.set(['section1', 'option1'],
... value='1', update_tree=True)
>>> config.set(['section1', 'subsection11', 'option2'],
... value='2', update_tree=True)
>>> config.set(['section1', 'subsection12', 'option3'],
... value='3', update_tree=True)
Many more manipulations are possible using the TreeConfigParser
methods. Finally, the configuration can be written to a file using
the TreeConfigParser.tofile()
method.
Expand source code
"""Custom configuration parser based on a tree.
.. include:: ./documentation.md
"""
from collections import OrderedDict
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
from datetime import datetime
from path import Path
import numpy as np
class TreeConfigParser:
"""Configuration parser class.
Attributes
----------
tree : OrderedDict
The tree of options. Each element is either a
`TreeConfigParser` instance (for a subsection)
or directly a string (for an option).
"""
def __init__(self):
self.tree = OrderedDict()
def get(self, keylist, **kwargs):
"""Returns the option value.
Uses the `string_conversion` function to convert the
value to any type.
Parameters
----------
keylist : list of str
The list of names which identify the option.
**kwargs : dict
Keyword arguments passed to the `string_conversion`
function.
Returns
-------
value
The option value, with the requested type.
Examples
--------
>>> config = TreeConfigParser()
>>> config.set(['section1', 'subsection11', 'option1'],
... value='1', update_tree=True)
>>> config.get(['section1', 'subsection11', 'option1'])
'1'
>>> config.get(['section1', 'subsection11', 'option1'],
... to_type='int')
1
"""
key = keylist[0]
if len(keylist) > 1:
return self.tree[key].get(keylist[1:], **kwargs)
return string_conversion(self.tree[key], **kwargs)
def set(self, keylist, value='', update_tree=False):
"""Sets an option value.
Parameters
----------
keylist : list of str
The list of names which identify the option.
value : str, optional
The new value for the option.
update_tree : bool, optional
If *True*, new (sub)sections are created if necessary.
Notes
-----
This method can only create new options. New sections can
be created if they are necessary for a new option, but empty
(sub)sections cannot be created.
"""
key = keylist[0]
if len(keylist) > 1:
if update_tree and key not in self.tree:
self.tree[key] = TreeConfigParser()
self.tree[key].set(keylist[1:], value, update_tree)
else:
self.tree[key] = value
def options(self, keylist=None):
"""Returns the list of options.
Parameters
----------
keylist : list of str, optional
The list of names which identify the section for
which we list the options (default: root).
Returns
-------
option_list : odict_keys
The list of options and subsections for the section.
Examples
--------
>>> config = TreeConfigParser()
>>> config.set(['option1'], value='1')
>>> config.set(['section1', 'option2'],
... value='2', update_tree=True)
>>> config.set(['section1', 'subsection11', 'option3'],
... value='3', update_tree=True)
>>> config.options()
odict_keys(['option1', 'section1'])
>>> config.options(['section1'])
odict_keys(['option2', 'subsection11'])
"""
if keylist:
return self.tree[keylist[0]].options(keylist[1:])
return self.tree.keys()
def remove_option(self, keylist):
"""Removes an option.
Parameters
----------
keylist : list of str
The list of names which identify the option or
subsection to remove.
Notes
-----
After this option is removed, the parent section may
be empty, but it is not removed.
"""
key = keylist[0]
if len(keylist) > 1:
self.tree[key].remove_option(keylist[1:])
else:
del self.tree[key]
def suboptions(self):
"""Returns the list of all (sub)options as keylists.
Returns
-------
keylistlist : list of list of str
The list of all available keylists.
Each keylist identifies one option.
Examples
--------
>>> config = TreeConfigParser()
>>> config.set(['option1'], value='1')
>>> config.set(['section1', 'option2'],
... value='2', update_tree=True)
>>> config.set(['section1', 'subsection11', 'option3'],
... value='3', update_tree=True)
>>> config.suboptions()
[['option1'], ['section1', 'option2'], \
['section1', 'subsection11', 'option3']]
"""
keylistlist = []
for key in self.tree:
if isinstance(self.tree[key], TreeConfigParser):
for subkeylist in self.tree[key].suboptions():
keylistlist.append([key] + subkeylist)
else:
keylistlist.append([key])
return keylistlist
def merge(self, config, keylist=None, update_tree=False):
"""Merges an other configuration.
The other configuration's options are inserted in
the current tree, in a given section.
Parameters
----------
config : TreeConfigParser instance
The other configuration.
keylist : list of str, optional
The list of names which identify the section
in which the other configuration should be
merged (default: root).
update_tree : bool, optional
If *True*, new (sub)sections are created if necessary.
Notes
-----
Ensure that the section names of both configuration
instances are compatible before merging. In case of
conflict, the updated values are those of *config*.
Examples
--------
>>> config = TreeConfigParser()
>>> config.set(['option1'], value='1')
>>> config.set(['section1', 'option2'], value='2',
... update_tree=True)
>>> other = TreeConfigParser()
>>> other.set(['option2'], value='3')
>>> other.set(['option3'], value='3')
>>> config.merge(other, keylist=['section1'])
>>> config.options()
odict_keys(['option1', 'section1'])
>>> config.options(['section1'])
odict_keys(['option2', 'option3'])
>>> config.get(['section1', 'option2'])
'3'
"""
if keylist:
key = keylist[0]
if update_tree and key not in self.tree:
self.tree[key] = TreeConfigParser()
self.tree[key].merge(config, keylist[1:], update_tree)
else:
for key in config.tree:
if (key in self.tree
and isinstance(config.tree[key], TreeConfigParser)
and isinstance(self.tree[key], TreeConfigParser)):
self.tree[key].merge(config.tree[key],
update_tree=update_tree)
elif isinstance(config.tree[key], TreeConfigParser):
self.tree[key] = TreeConfigParser()
self.tree[key].merge(config.tree[key],
update_tree=update_tree)
else:
self.tree[key] = config.tree[key]
def substitution(self, old_string, new_string, keylist=None):
"""Performs a string substitution.
Replaces *old_string* by *new_string*. If *keylist*
identifies an option, the substitution is applied
only to that option. If *keylist* identifies a
section, the substitution is applied to all
options and subsections in that section.
Uses the `str.replace` method.
Parameters
----------
old_string : str
The old `str` pattern.
new_string : str
The new `str` pattern.
keylist : list of str
The list of names which identify the option
or the section in which the substitution is applied
(default: root).
"""
if keylist:
self.tree[keylist[0]].substitution(old_string, new_string,
keylist[1:])
else:
for key in self.tree:
if isinstance(self.tree[key], TreeConfigParser):
self.tree[key].substitution(old_string, new_string)
else:
self.tree[key] = self.tree[key].replace(
old_string, new_string)
def clone(self, keylist=None):
"""Return a copy of the (sub)configuration.
Parameters
----------
keylist : list of str, optional
The list of names which identify the (sub)configuration
to copy (default: root).
Returns
-------
copy : TreeConfigParser instance
The copy of the (sub)configuration.
Examples
--------
>>> config = TreeConfigParser()
>>> config.set(['section1', 'option1'], value='1',
... update_tree=True)
>>> other = config.clone(keylist=['section1'])
>>> other.set(['option2'], value='2')
>>> other.options()
odict_keys(['option1', 'option2'])
>>> config.options(['section1'])
odict_keys(['option1'])
"""
if keylist:
return self.tree[keylist[0]].clone(keylist[1:])
clone_cfg = TreeConfigParser()
for key in self.tree:
if isinstance(self.tree[key], TreeConfigParser):
clone_cfg.tree[key] = self.tree[key].clone()
else:
clone_cfg.tree[key] = self.tree[key]
return clone_cfg
def subconfig(self, keylist=None):
"""Return a reference to the (sub)configuration.
Parameters
----------
keylist : list of str, optional
The list of names which identify the (sub)configuration
to reference (default: root).
Returns
-------
copy : TreeConfigParser instance
The reference to the (sub)configuration.
Examples
--------
>>> config = TreeConfigParser()
>>> config.set(['section1', 'option1'], value='1',
... update_tree=True)
>>> other = config.subconfig(keylist=['section1'])
>>> other.set(['option2'], value='2')
>>> other.options()
odict_keys(['option1', 'option2'])
>>> config.options(['section1'])
odict_keys(['option1', 'option2'])
"""
if keylist:
return self.tree[keylist[0]].subconfig(keylist[1:])
return self
# pylint: disable=too-many-arguments,too-many-locals,too-many-statements
def read_file(self,
file_name,
convention='py',
comment_char='#',
reference_char='%',
indentation=4):
"""Reads and parses *file_name*.
This is the main method of the class.
The options are inserted in the current tree.
Parameters
----------
file_name : str
The name of the file to parse.
convention : {'py', 'flat', 'cpp'}, optional
The format of the file.
comment_char : str, optional
The character used to delimit comments in the file.
reference_char : str, optional
The character used to delimit cross-references
in the file.
indentation : int, optional
The indentation used to add suboptions or
subsections in the file when using the 'py' format.
Raises
------
CrossReferenceError
If the cross-references cannot be solved.
"""
def is_tree_node(line):
line = line.strip()
return len(line) > 2 and line[0] == '[' and line[-1] == ']'
def update_keylist(keylist, depth, indentation):
if depth % indentation:
raise IndentationError(line)
depth = depth // indentation
if depth > len(keylist):
raise IndentationError(line)
return keylist[:depth]
def extract_tree_node_py(keylist, line, indentation):
key = line.strip()[1:-1]
keylist = update_keylist(keylist, line.find(key) - 1, indentation)
return keylist + [key]
def extract_tree_node_flat(line):
return line.strip()[1:-1]
def extract_tree_leaf_py(keylist, line, indentation):
if '=' in line:
split = line.split('=', 1)
key = split[0].strip()
value = split[1].strip()
else:
key = line.strip()
value = ''
keylist = update_keylist(keylist, line.find(key), indentation)
self.set(keylist + [key], value, update_tree=True)
return keylist
def extract_tree_leaf_flat(section, line):
if '=' in line:
split = line.split('=', 1)
key = split[0].strip()
value = split[1].strip()
else:
key = line.strip()
value = ''
self.set([section, key], value, update_tree=True)
return section
def extract_tree_leaf_cpp(line):
if '=' in line:
split = line.split('=', 1)
key = split[0].strip()
value = split[1].strip()
else:
key = line.strip()
value = ''
keylist = key.split('.')
self.set(keylist, value, update_tree=True)
def read_line_py(line, keylist, comment_char, indentation):
line = line.split(comment_char)[0]
if not line or line.isspace():
return keylist
if is_tree_node(line):
return extract_tree_node_py(keylist, line, indentation)
return extract_tree_leaf_py(keylist, line, indentation)
def read_line_flat(line, section, comment_char):
line = line.split(comment_char)[0]
if not line or line.isspace():
return section
if is_tree_node(line):
return extract_tree_node_flat(line)
return extract_tree_leaf_flat(section, line)
def read_line_cpp(line, comment_char):
line = line.split(comment_char)[0].strip()
if line and not line.isspace():
extract_tree_leaf_cpp(line)
def has_reference(value, reference_char):
return len(value) > 2 and reference_char in value
def extract_references(value, reference_char):
return [
ref for (i, ref) in enumerate(value.split(reference_char))
if ref and i % 2
]
def solve_references_string(value, max_rec, reference_char):
if not has_reference(value, reference_char):
return value
if max_rec == 0:
raise CrossReferenceError
reflist = extract_references(value, reference_char)
for ref in reflist:
keylist = ref.split('.')
subvalue = solve_references_keylist(keylist, max_rec - 1,
reference_char)
value = value.replace(reference_char + ref + reference_char,
subvalue)
return value
def solve_references_keylist(keylist, max_rec, reference_char):
value = self.get(keylist)
value = solve_references_string(value, max_rec, reference_char)
self.set(keylist, value, update_tree=False)
return value
# read and parse file content
lines = Path(file_name).lines(retain=False)
if convention == 'py':
keylist = []
for line in lines:
keylist = read_line_py(line, keylist, comment_char,
indentation)
elif convention == 'flat':
section = None
for line in lines:
section = read_line_flat(line, section, comment_char)
elif convention == 'cpp':
for line in lines:
read_line_cpp(line, comment_char)
# solve references
keylistlist = self.suboptions()
max_rec = len(keylistlist) - 1
for keylist in keylistlist:
solve_references_keylist(keylist, max_rec, reference_char)
def tofile(self, file_name, convention='py', indentation=4):
"""Writes the configuration options to a file.
Once written, the file can be read and parsed by an other
`TreeConfigParser` instance.
Parameters
----------
file_name : str
The name of the file to write.
convention : {'py', 'flat', 'cpp'}, optional
The format of the file.
indentation : int, optional
The indentation used to add suboptions or
subsections in the file when using the 'py' format.
"""
def write_tree_node_line_py(depth, indentation, key):
return [' ' * depth * indentation + f'[{key}]']
def write_tree_leaf_line_py(depth, indentation, key, value):
return [' ' * depth * indentation + f'{key} = {value}']
def write_tree_leaf_line_cpp(keylist, value):
return ['.'.join(keylist) + f' = {value}']
def write_tree_py(tree, depth, indentation):
lines = []
for key in tree:
if isinstance(tree[key], TreeConfigParser):
lines.extend(
write_tree_node_line_py(depth, indentation, key))
lines.extend(
write_tree_py(tree[key].tree, depth + 1, indentation))
else:
lines.extend(
write_tree_leaf_line_py(depth, indentation, key,
tree[key]))
return lines
def write_tree_cpp(tree, keylist):
lines = []
for key in tree:
if isinstance(tree[key], TreeConfigParser):
lines.extend(
write_tree_cpp(tree[key].tree, keylist + [key]))
else:
lines.extend(
write_tree_leaf_line_cpp(keylist + [key], tree[key]))
return lines
# write lines
if convention == 'py':
lines = write_tree_py(self.tree, 0, indentation)
elif convention == 'flat':
lines = write_tree_py(self.tree, 0, 0)
elif convention == 'cpp':
lines = write_tree_cpp(self.tree, [])
Path(file_name).write_lines(lines)
class CrossReferenceError(Exception):
"""Exception class for cross-reference errors in config files."""
def string_conversion(value, to_type='string', **kwargs):
"""Converts a string to the given type.
Parameters
----------
value : string
The string to convert.
to_type : {'string', 'path', 'bool', 'int', 'float', 'stringlist', \
'log_level', 'np_fromfile', 'datetime'}, optional
The conversion to use.
**kwargs : dict
Keyword arguments passed to the conversion function.
See the details below.
Other parameters
----------------
use_eval : bool, optional
For the **string to int** and **string to float** conversions:
enables the use of the `eval` function (default: *False*).
sep : str, optional
For the **string to stringlist** conversion:
delimiter for the `split` function (default: *None*).
maxsplit : int, optional
For the **string to stringlist** conversion:
maximum number of splits for the `split` function (default: -1).
dtype : data-type, optional
For the **string to np_fromfile** conversion:
data type of the returned array (default `float`).
count : int, optional
For the **string to np_fromfile** conversion:
number of items to read (default -1).
offset : int, optional
For the **string to np_fromfile** conversion:
the offset (in bytes) from the file's current position (default 0).
convention : str, optional
For the **string to datetime** conversion:
format used in the `datetime.strptime` function
(default: '%Y-%m-%d').
Special cases are created for 'polyphemus_date'
('%Y-%m-%d'), 'polyphemus_datetime' ('%Y-%m-%d_%H-%M'),
and 'ecmwf_datetime' ('%Y-%m-%dT%H:%M:%SZ').
Returns
-------
converted_value
The converted string.
Notes
-----
**String to string**: no conversion is performed, directly returns
*value*.
**String to path**: converts the value to a `path.Path` instance.
**String to bool**: converts the value to a boolean.
- Returns *True* if *value* is 'true', 'yes', 'on', '1'.
- Returns *False* if *value* is 'false', 'no', 'off', or '0'.
- Raises a `ValueError` otherwise.
The comparison is case-insensitive.
**String to int**: converts the value to an integer. Use the `eval`
function if *use_eval* is True.
**String to float**: converts the value to a real. Use the `eval`
function if *use_eval* is True.
**String to stringlist**: converts the value to a list of strings
using the `split` function.
**String to log_level**: converts the value to a `logging` level.
- Returns `logging.DEBUG` if value is 'debug'.
- Returns `logging.INFO` if value is 'info'.
- Returns `logging.WARNING` if value is 'warning'.
- Returns `logging.ERROR` if value is 'error'.
- Returns `logging.CRITICAL` if value is 'critical'.
- Raises `ValueError` otherwise.
The comparison is case-insensitive.
**String to np_fromfile** The value is assumed to be the name of
a binary file. It is converted to a `numpy.ndarray` using the
`numpy.fromfile` function.
**String to datetime**: converts the value to a
`datetime.datetime` instance using the `datetime.strptime` function.
Examples
--------
>>> string_conversion('abc')
'abc'
>>> string_conversion('/usr/bin', to_type='path')
Path('/usr/bin')
>>> string_conversion('true', to_type='bool')
True
>>> string_conversion('On', to_type='bool')
True
>>> string_conversion('0', to_type='bool')
False
>>> string_conversion('-10', to_type='int')
-10
>>> string_conversion('3/2', to_type='float', use_eval=True)
1.5
>>> string_conversion('a bc def g hi', to_type='stringlist')
['a', 'bc', 'def', 'g', 'hi']
"""
def string_to_string(value):
return value
def string_to_path(value):
return Path(value)
def string_to_bool(value):
if value.lower() in ['true', 'yes', 'on', '1']:
return True
if value.lower() in ['false', 'no', 'off', '0']:
return False
return ValueError(f'"{value}" cannot be converted to bool')
def string_to_int(value, use_eval=False):
return int(eval(value)) if use_eval else int(value) # pylint: disable=eval-used
def string_to_float(value, use_eval=False):
return float(eval(value)) if use_eval else float(value) # pylint: disable=eval-used
def string_to_stringlist(value, sep=None, maxsplit=-1):
return value.split(sep, maxsplit)
def string_to_log_level(value):
levels = {
'debug': DEBUG,
'info': INFO,
'warning': WARNING,
'error': ERROR,
'critical': CRITICAL
}
if value.lower() in levels:
return levels[value.lower()]
raise ValueError(f'"{value}" cannot be converted to logging level')
def string_to_np_fromfile(value, dtype=float, count=-1, offset=0):
return np.fromfile(value, dtype=dtype, count=count, offset=offset)
def string_to_datetime(value, convention='%Y-%m-%d'):
if convention.lower() in ['polyphemus_date']:
convention = '%Y-%m-%d'
elif convention.lower() in ['polyphemus_datetime']:
convention = '%Y-%m-%d_%H-%M'
elif convention.lower() in ['ecmwf_datetime']:
convention = '%Y-%m-%dT%H:%M:%SZ'
return datetime.strptime(value, convention)
conversions = {
'string': string_to_string,
'path': string_to_path,
'bool': string_to_bool,
'int': string_to_int,
'float': string_to_float,
'stringlist': string_to_stringlist,
'log_level': string_to_log_level,
'np_fromfile': string_to_np_fromfile,
'datetime': string_to_datetime,
}
if to_type in conversions:
return conversions[to_type](value, **kwargs)
raise TypeError(f'unkown conversion "string to {to_type}"')
def fromfile(file_name, **kwargs):
"""Reads and parses a configuration file.
Parameters
----------
file_name : string
The name of the file to parse.
**kwargs : dict
Keyword arguments passed to the `TreeConfigParser.read_file`
method.
Returns
-------
config : TreeConfigParser
The configuration.
Notes
-----
This function creates an empty `TreeConfigParser` instance and
uses the `TreeConfigParser.read_file` method to parse the file.
"""
config = TreeConfigParser()
config.read_file(file_name, **kwargs)
return config
Functions
def fromfile(file_name, **kwargs)
-
Reads and parses a configuration file.
Parameters
file_name
:string
- The name of the file to parse.
**kwargs
:dict
- Keyword arguments passed to the
TreeConfigParser.read_file()
method.
Returns
config
:TreeConfigParser
- The configuration.
Notes
This function creates an empty
TreeConfigParser
instance and uses theTreeConfigParser.read_file()
method to parse the file.Expand source code
def fromfile(file_name, **kwargs): """Reads and parses a configuration file. Parameters ---------- file_name : string The name of the file to parse. **kwargs : dict Keyword arguments passed to the `TreeConfigParser.read_file` method. Returns ------- config : TreeConfigParser The configuration. Notes ----- This function creates an empty `TreeConfigParser` instance and uses the `TreeConfigParser.read_file` method to parse the file. """ config = TreeConfigParser() config.read_file(file_name, **kwargs) return config
def string_conversion(value, to_type='string', **kwargs)
-
Converts a string to the given type.
Parameters
value
:string
- The string to convert.
to_type
:{'string', 'path', 'bool', 'int', 'float', 'stringlist', 'log_level', 'np_fromfile', 'datetime'}
, optional- The conversion to use.
**kwargs
:dict
- Keyword arguments passed to the conversion function. See the details below.
Other Parameters
use_eval
:bool
, optional- For the string to int and string to float conversions:
enables the use of the
eval
function (default: False). sep
:str
, optional- For the string to stringlist conversion:
delimiter for the
split
function (default: None). maxsplit
:int
, optional- For the string to stringlist conversion:
maximum number of splits for the
split
function (default: -1). dtype
:data-type
, optional- For the string to np_fromfile conversion:
data type of the returned array (default
float
). count
:int
, optional- For the string to np_fromfile conversion: number of items to read (default -1).
offset
:int
, optional- For the string to np_fromfile conversion: the offset (in bytes) from the file's current position (default 0).
convention
:str
, optional- For the string to datetime conversion:
format used in the
datetime.strptime
function (default: '%Y-%m-%d'). Special cases are created for 'polyphemus_date' ('%Y-%m-%d'), 'polyphemus_datetime' ('%Y-%m-%d_%H-%M'), and 'ecmwf_datetime' ('%Y-%m-%dT%H:%M:%SZ').
Returns
converted_value
- The converted string.
Notes
String to string: no conversion is performed, directly returns value.
String to path: converts the value to a
path.Path
instance.String to bool: converts the value to a boolean.
- Returns True if value is 'true', 'yes', 'on', '1'.
- Returns False if value is 'false', 'no', 'off', or '0'.
- Raises a
ValueError
otherwise.
The comparison is case-insensitive.
String to int: converts the value to an integer. Use the
eval
function if use_eval is True.String to float: converts the value to a real. Use the
eval
function if use_eval is True.String to stringlist: converts the value to a list of strings using the
split
function.String to log_level: converts the value to a
logging
level.- Returns
logging.DEBUG
if value is 'debug'. - Returns
logging.INFO
if value is 'info'. - Returns
logging.WARNING
if value is 'warning'. - Returns
logging.ERROR
if value is 'error'. - Returns
logging.CRITICAL
if value is 'critical'. - Raises
ValueError
otherwise.
The comparison is case-insensitive.
String to np_fromfile The value is assumed to be the name of a binary file. It is converted to a
numpy.ndarray
using thenumpy.fromfile
function.String to datetime: converts the value to a
datetime.datetime
instance using thedatetime.strptime
function.Examples
>>> string_conversion('abc') 'abc' >>> string_conversion('/usr/bin', to_type='path') Path('/usr/bin') >>> string_conversion('true', to_type='bool') True >>> string_conversion('On', to_type='bool') True >>> string_conversion('0', to_type='bool') False >>> string_conversion('-10', to_type='int') -10 >>> string_conversion('3/2', to_type='float', use_eval=True) 1.5 >>> string_conversion('a bc def g hi', to_type='stringlist') ['a', 'bc', 'def', 'g', 'hi']
Expand source code
def string_conversion(value, to_type='string', **kwargs): """Converts a string to the given type. Parameters ---------- value : string The string to convert. to_type : {'string', 'path', 'bool', 'int', 'float', 'stringlist', \ 'log_level', 'np_fromfile', 'datetime'}, optional The conversion to use. **kwargs : dict Keyword arguments passed to the conversion function. See the details below. Other parameters ---------------- use_eval : bool, optional For the **string to int** and **string to float** conversions: enables the use of the `eval` function (default: *False*). sep : str, optional For the **string to stringlist** conversion: delimiter for the `split` function (default: *None*). maxsplit : int, optional For the **string to stringlist** conversion: maximum number of splits for the `split` function (default: -1). dtype : data-type, optional For the **string to np_fromfile** conversion: data type of the returned array (default `float`). count : int, optional For the **string to np_fromfile** conversion: number of items to read (default -1). offset : int, optional For the **string to np_fromfile** conversion: the offset (in bytes) from the file's current position (default 0). convention : str, optional For the **string to datetime** conversion: format used in the `datetime.strptime` function (default: '%Y-%m-%d'). Special cases are created for 'polyphemus_date' ('%Y-%m-%d'), 'polyphemus_datetime' ('%Y-%m-%d_%H-%M'), and 'ecmwf_datetime' ('%Y-%m-%dT%H:%M:%SZ'). Returns ------- converted_value The converted string. Notes ----- **String to string**: no conversion is performed, directly returns *value*. **String to path**: converts the value to a `path.Path` instance. **String to bool**: converts the value to a boolean. - Returns *True* if *value* is 'true', 'yes', 'on', '1'. - Returns *False* if *value* is 'false', 'no', 'off', or '0'. - Raises a `ValueError` otherwise. The comparison is case-insensitive. **String to int**: converts the value to an integer. Use the `eval` function if *use_eval* is True. **String to float**: converts the value to a real. Use the `eval` function if *use_eval* is True. **String to stringlist**: converts the value to a list of strings using the `split` function. **String to log_level**: converts the value to a `logging` level. - Returns `logging.DEBUG` if value is 'debug'. - Returns `logging.INFO` if value is 'info'. - Returns `logging.WARNING` if value is 'warning'. - Returns `logging.ERROR` if value is 'error'. - Returns `logging.CRITICAL` if value is 'critical'. - Raises `ValueError` otherwise. The comparison is case-insensitive. **String to np_fromfile** The value is assumed to be the name of a binary file. It is converted to a `numpy.ndarray` using the `numpy.fromfile` function. **String to datetime**: converts the value to a `datetime.datetime` instance using the `datetime.strptime` function. Examples -------- >>> string_conversion('abc') 'abc' >>> string_conversion('/usr/bin', to_type='path') Path('/usr/bin') >>> string_conversion('true', to_type='bool') True >>> string_conversion('On', to_type='bool') True >>> string_conversion('0', to_type='bool') False >>> string_conversion('-10', to_type='int') -10 >>> string_conversion('3/2', to_type='float', use_eval=True) 1.5 >>> string_conversion('a bc def g hi', to_type='stringlist') ['a', 'bc', 'def', 'g', 'hi'] """ def string_to_string(value): return value def string_to_path(value): return Path(value) def string_to_bool(value): if value.lower() in ['true', 'yes', 'on', '1']: return True if value.lower() in ['false', 'no', 'off', '0']: return False return ValueError(f'"{value}" cannot be converted to bool') def string_to_int(value, use_eval=False): return int(eval(value)) if use_eval else int(value) # pylint: disable=eval-used def string_to_float(value, use_eval=False): return float(eval(value)) if use_eval else float(value) # pylint: disable=eval-used def string_to_stringlist(value, sep=None, maxsplit=-1): return value.split(sep, maxsplit) def string_to_log_level(value): levels = { 'debug': DEBUG, 'info': INFO, 'warning': WARNING, 'error': ERROR, 'critical': CRITICAL } if value.lower() in levels: return levels[value.lower()] raise ValueError(f'"{value}" cannot be converted to logging level') def string_to_np_fromfile(value, dtype=float, count=-1, offset=0): return np.fromfile(value, dtype=dtype, count=count, offset=offset) def string_to_datetime(value, convention='%Y-%m-%d'): if convention.lower() in ['polyphemus_date']: convention = '%Y-%m-%d' elif convention.lower() in ['polyphemus_datetime']: convention = '%Y-%m-%d_%H-%M' elif convention.lower() in ['ecmwf_datetime']: convention = '%Y-%m-%dT%H:%M:%SZ' return datetime.strptime(value, convention) conversions = { 'string': string_to_string, 'path': string_to_path, 'bool': string_to_bool, 'int': string_to_int, 'float': string_to_float, 'stringlist': string_to_stringlist, 'log_level': string_to_log_level, 'np_fromfile': string_to_np_fromfile, 'datetime': string_to_datetime, } if to_type in conversions: return conversions[to_type](value, **kwargs) raise TypeError(f'unkown conversion "string to {to_type}"')
Classes
class CrossReferenceError (*args, **kwargs)
-
Exception class for cross-reference errors in config files.
Expand source code
class CrossReferenceError(Exception): """Exception class for cross-reference errors in config files."""
Ancestors
- builtins.Exception
- builtins.BaseException
class TreeConfigParser
-
Configuration parser class.
Attributes
tree
:OrderedDict
- The tree of options. Each element is either a
TreeConfigParser
instance (for a subsection) or directly a string (for an option).
Expand source code
class TreeConfigParser: """Configuration parser class. Attributes ---------- tree : OrderedDict The tree of options. Each element is either a `TreeConfigParser` instance (for a subsection) or directly a string (for an option). """ def __init__(self): self.tree = OrderedDict() def get(self, keylist, **kwargs): """Returns the option value. Uses the `string_conversion` function to convert the value to any type. Parameters ---------- keylist : list of str The list of names which identify the option. **kwargs : dict Keyword arguments passed to the `string_conversion` function. Returns ------- value The option value, with the requested type. Examples -------- >>> config = TreeConfigParser() >>> config.set(['section1', 'subsection11', 'option1'], ... value='1', update_tree=True) >>> config.get(['section1', 'subsection11', 'option1']) '1' >>> config.get(['section1', 'subsection11', 'option1'], ... to_type='int') 1 """ key = keylist[0] if len(keylist) > 1: return self.tree[key].get(keylist[1:], **kwargs) return string_conversion(self.tree[key], **kwargs) def set(self, keylist, value='', update_tree=False): """Sets an option value. Parameters ---------- keylist : list of str The list of names which identify the option. value : str, optional The new value for the option. update_tree : bool, optional If *True*, new (sub)sections are created if necessary. Notes ----- This method can only create new options. New sections can be created if they are necessary for a new option, but empty (sub)sections cannot be created. """ key = keylist[0] if len(keylist) > 1: if update_tree and key not in self.tree: self.tree[key] = TreeConfigParser() self.tree[key].set(keylist[1:], value, update_tree) else: self.tree[key] = value def options(self, keylist=None): """Returns the list of options. Parameters ---------- keylist : list of str, optional The list of names which identify the section for which we list the options (default: root). Returns ------- option_list : odict_keys The list of options and subsections for the section. Examples -------- >>> config = TreeConfigParser() >>> config.set(['option1'], value='1') >>> config.set(['section1', 'option2'], ... value='2', update_tree=True) >>> config.set(['section1', 'subsection11', 'option3'], ... value='3', update_tree=True) >>> config.options() odict_keys(['option1', 'section1']) >>> config.options(['section1']) odict_keys(['option2', 'subsection11']) """ if keylist: return self.tree[keylist[0]].options(keylist[1:]) return self.tree.keys() def remove_option(self, keylist): """Removes an option. Parameters ---------- keylist : list of str The list of names which identify the option or subsection to remove. Notes ----- After this option is removed, the parent section may be empty, but it is not removed. """ key = keylist[0] if len(keylist) > 1: self.tree[key].remove_option(keylist[1:]) else: del self.tree[key] def suboptions(self): """Returns the list of all (sub)options as keylists. Returns ------- keylistlist : list of list of str The list of all available keylists. Each keylist identifies one option. Examples -------- >>> config = TreeConfigParser() >>> config.set(['option1'], value='1') >>> config.set(['section1', 'option2'], ... value='2', update_tree=True) >>> config.set(['section1', 'subsection11', 'option3'], ... value='3', update_tree=True) >>> config.suboptions() [['option1'], ['section1', 'option2'], \ ['section1', 'subsection11', 'option3']] """ keylistlist = [] for key in self.tree: if isinstance(self.tree[key], TreeConfigParser): for subkeylist in self.tree[key].suboptions(): keylistlist.append([key] + subkeylist) else: keylistlist.append([key]) return keylistlist def merge(self, config, keylist=None, update_tree=False): """Merges an other configuration. The other configuration's options are inserted in the current tree, in a given section. Parameters ---------- config : TreeConfigParser instance The other configuration. keylist : list of str, optional The list of names which identify the section in which the other configuration should be merged (default: root). update_tree : bool, optional If *True*, new (sub)sections are created if necessary. Notes ----- Ensure that the section names of both configuration instances are compatible before merging. In case of conflict, the updated values are those of *config*. Examples -------- >>> config = TreeConfigParser() >>> config.set(['option1'], value='1') >>> config.set(['section1', 'option2'], value='2', ... update_tree=True) >>> other = TreeConfigParser() >>> other.set(['option2'], value='3') >>> other.set(['option3'], value='3') >>> config.merge(other, keylist=['section1']) >>> config.options() odict_keys(['option1', 'section1']) >>> config.options(['section1']) odict_keys(['option2', 'option3']) >>> config.get(['section1', 'option2']) '3' """ if keylist: key = keylist[0] if update_tree and key not in self.tree: self.tree[key] = TreeConfigParser() self.tree[key].merge(config, keylist[1:], update_tree) else: for key in config.tree: if (key in self.tree and isinstance(config.tree[key], TreeConfigParser) and isinstance(self.tree[key], TreeConfigParser)): self.tree[key].merge(config.tree[key], update_tree=update_tree) elif isinstance(config.tree[key], TreeConfigParser): self.tree[key] = TreeConfigParser() self.tree[key].merge(config.tree[key], update_tree=update_tree) else: self.tree[key] = config.tree[key] def substitution(self, old_string, new_string, keylist=None): """Performs a string substitution. Replaces *old_string* by *new_string*. If *keylist* identifies an option, the substitution is applied only to that option. If *keylist* identifies a section, the substitution is applied to all options and subsections in that section. Uses the `str.replace` method. Parameters ---------- old_string : str The old `str` pattern. new_string : str The new `str` pattern. keylist : list of str The list of names which identify the option or the section in which the substitution is applied (default: root). """ if keylist: self.tree[keylist[0]].substitution(old_string, new_string, keylist[1:]) else: for key in self.tree: if isinstance(self.tree[key], TreeConfigParser): self.tree[key].substitution(old_string, new_string) else: self.tree[key] = self.tree[key].replace( old_string, new_string) def clone(self, keylist=None): """Return a copy of the (sub)configuration. Parameters ---------- keylist : list of str, optional The list of names which identify the (sub)configuration to copy (default: root). Returns ------- copy : TreeConfigParser instance The copy of the (sub)configuration. Examples -------- >>> config = TreeConfigParser() >>> config.set(['section1', 'option1'], value='1', ... update_tree=True) >>> other = config.clone(keylist=['section1']) >>> other.set(['option2'], value='2') >>> other.options() odict_keys(['option1', 'option2']) >>> config.options(['section1']) odict_keys(['option1']) """ if keylist: return self.tree[keylist[0]].clone(keylist[1:]) clone_cfg = TreeConfigParser() for key in self.tree: if isinstance(self.tree[key], TreeConfigParser): clone_cfg.tree[key] = self.tree[key].clone() else: clone_cfg.tree[key] = self.tree[key] return clone_cfg def subconfig(self, keylist=None): """Return a reference to the (sub)configuration. Parameters ---------- keylist : list of str, optional The list of names which identify the (sub)configuration to reference (default: root). Returns ------- copy : TreeConfigParser instance The reference to the (sub)configuration. Examples -------- >>> config = TreeConfigParser() >>> config.set(['section1', 'option1'], value='1', ... update_tree=True) >>> other = config.subconfig(keylist=['section1']) >>> other.set(['option2'], value='2') >>> other.options() odict_keys(['option1', 'option2']) >>> config.options(['section1']) odict_keys(['option1', 'option2']) """ if keylist: return self.tree[keylist[0]].subconfig(keylist[1:]) return self # pylint: disable=too-many-arguments,too-many-locals,too-many-statements def read_file(self, file_name, convention='py', comment_char='#', reference_char='%', indentation=4): """Reads and parses *file_name*. This is the main method of the class. The options are inserted in the current tree. Parameters ---------- file_name : str The name of the file to parse. convention : {'py', 'flat', 'cpp'}, optional The format of the file. comment_char : str, optional The character used to delimit comments in the file. reference_char : str, optional The character used to delimit cross-references in the file. indentation : int, optional The indentation used to add suboptions or subsections in the file when using the 'py' format. Raises ------ CrossReferenceError If the cross-references cannot be solved. """ def is_tree_node(line): line = line.strip() return len(line) > 2 and line[0] == '[' and line[-1] == ']' def update_keylist(keylist, depth, indentation): if depth % indentation: raise IndentationError(line) depth = depth // indentation if depth > len(keylist): raise IndentationError(line) return keylist[:depth] def extract_tree_node_py(keylist, line, indentation): key = line.strip()[1:-1] keylist = update_keylist(keylist, line.find(key) - 1, indentation) return keylist + [key] def extract_tree_node_flat(line): return line.strip()[1:-1] def extract_tree_leaf_py(keylist, line, indentation): if '=' in line: split = line.split('=', 1) key = split[0].strip() value = split[1].strip() else: key = line.strip() value = '' keylist = update_keylist(keylist, line.find(key), indentation) self.set(keylist + [key], value, update_tree=True) return keylist def extract_tree_leaf_flat(section, line): if '=' in line: split = line.split('=', 1) key = split[0].strip() value = split[1].strip() else: key = line.strip() value = '' self.set([section, key], value, update_tree=True) return section def extract_tree_leaf_cpp(line): if '=' in line: split = line.split('=', 1) key = split[0].strip() value = split[1].strip() else: key = line.strip() value = '' keylist = key.split('.') self.set(keylist, value, update_tree=True) def read_line_py(line, keylist, comment_char, indentation): line = line.split(comment_char)[0] if not line or line.isspace(): return keylist if is_tree_node(line): return extract_tree_node_py(keylist, line, indentation) return extract_tree_leaf_py(keylist, line, indentation) def read_line_flat(line, section, comment_char): line = line.split(comment_char)[0] if not line or line.isspace(): return section if is_tree_node(line): return extract_tree_node_flat(line) return extract_tree_leaf_flat(section, line) def read_line_cpp(line, comment_char): line = line.split(comment_char)[0].strip() if line and not line.isspace(): extract_tree_leaf_cpp(line) def has_reference(value, reference_char): return len(value) > 2 and reference_char in value def extract_references(value, reference_char): return [ ref for (i, ref) in enumerate(value.split(reference_char)) if ref and i % 2 ] def solve_references_string(value, max_rec, reference_char): if not has_reference(value, reference_char): return value if max_rec == 0: raise CrossReferenceError reflist = extract_references(value, reference_char) for ref in reflist: keylist = ref.split('.') subvalue = solve_references_keylist(keylist, max_rec - 1, reference_char) value = value.replace(reference_char + ref + reference_char, subvalue) return value def solve_references_keylist(keylist, max_rec, reference_char): value = self.get(keylist) value = solve_references_string(value, max_rec, reference_char) self.set(keylist, value, update_tree=False) return value # read and parse file content lines = Path(file_name).lines(retain=False) if convention == 'py': keylist = [] for line in lines: keylist = read_line_py(line, keylist, comment_char, indentation) elif convention == 'flat': section = None for line in lines: section = read_line_flat(line, section, comment_char) elif convention == 'cpp': for line in lines: read_line_cpp(line, comment_char) # solve references keylistlist = self.suboptions() max_rec = len(keylistlist) - 1 for keylist in keylistlist: solve_references_keylist(keylist, max_rec, reference_char) def tofile(self, file_name, convention='py', indentation=4): """Writes the configuration options to a file. Once written, the file can be read and parsed by an other `TreeConfigParser` instance. Parameters ---------- file_name : str The name of the file to write. convention : {'py', 'flat', 'cpp'}, optional The format of the file. indentation : int, optional The indentation used to add suboptions or subsections in the file when using the 'py' format. """ def write_tree_node_line_py(depth, indentation, key): return [' ' * depth * indentation + f'[{key}]'] def write_tree_leaf_line_py(depth, indentation, key, value): return [' ' * depth * indentation + f'{key} = {value}'] def write_tree_leaf_line_cpp(keylist, value): return ['.'.join(keylist) + f' = {value}'] def write_tree_py(tree, depth, indentation): lines = [] for key in tree: if isinstance(tree[key], TreeConfigParser): lines.extend( write_tree_node_line_py(depth, indentation, key)) lines.extend( write_tree_py(tree[key].tree, depth + 1, indentation)) else: lines.extend( write_tree_leaf_line_py(depth, indentation, key, tree[key])) return lines def write_tree_cpp(tree, keylist): lines = [] for key in tree: if isinstance(tree[key], TreeConfigParser): lines.extend( write_tree_cpp(tree[key].tree, keylist + [key])) else: lines.extend( write_tree_leaf_line_cpp(keylist + [key], tree[key])) return lines # write lines if convention == 'py': lines = write_tree_py(self.tree, 0, indentation) elif convention == 'flat': lines = write_tree_py(self.tree, 0, 0) elif convention == 'cpp': lines = write_tree_cpp(self.tree, []) Path(file_name).write_lines(lines)
Methods
def clone(self, keylist=None)
-
Return a copy of the (sub)configuration.
Parameters
keylist
:list
ofstr
, optional- The list of names which identify the (sub)configuration to copy (default: root).
Returns
copy
:TreeConfigParser instance
- The copy of the (sub)configuration.
Examples
>>> config = TreeConfigParser() >>> config.set(['section1', 'option1'], value='1', ... update_tree=True) >>> other = config.clone(keylist=['section1']) >>> other.set(['option2'], value='2') >>> other.options() odict_keys(['option1', 'option2']) >>> config.options(['section1']) odict_keys(['option1'])
Expand source code
def clone(self, keylist=None): """Return a copy of the (sub)configuration. Parameters ---------- keylist : list of str, optional The list of names which identify the (sub)configuration to copy (default: root). Returns ------- copy : TreeConfigParser instance The copy of the (sub)configuration. Examples -------- >>> config = TreeConfigParser() >>> config.set(['section1', 'option1'], value='1', ... update_tree=True) >>> other = config.clone(keylist=['section1']) >>> other.set(['option2'], value='2') >>> other.options() odict_keys(['option1', 'option2']) >>> config.options(['section1']) odict_keys(['option1']) """ if keylist: return self.tree[keylist[0]].clone(keylist[1:]) clone_cfg = TreeConfigParser() for key in self.tree: if isinstance(self.tree[key], TreeConfigParser): clone_cfg.tree[key] = self.tree[key].clone() else: clone_cfg.tree[key] = self.tree[key] return clone_cfg
def get(self, keylist, **kwargs)
-
Returns the option value.
Uses the
string_conversion()
function to convert the value to any type.Parameters
keylist
:list
ofstr
- The list of names which identify the option.
**kwargs
:dict
- Keyword arguments passed to the
string_conversion()
function.
Returns
value
- The option value, with the requested type.
Examples
>>> config = TreeConfigParser() >>> config.set(['section1', 'subsection11', 'option1'], ... value='1', update_tree=True) >>> config.get(['section1', 'subsection11', 'option1']) '1' >>> config.get(['section1', 'subsection11', 'option1'], ... to_type='int') 1
Expand source code
def get(self, keylist, **kwargs): """Returns the option value. Uses the `string_conversion` function to convert the value to any type. Parameters ---------- keylist : list of str The list of names which identify the option. **kwargs : dict Keyword arguments passed to the `string_conversion` function. Returns ------- value The option value, with the requested type. Examples -------- >>> config = TreeConfigParser() >>> config.set(['section1', 'subsection11', 'option1'], ... value='1', update_tree=True) >>> config.get(['section1', 'subsection11', 'option1']) '1' >>> config.get(['section1', 'subsection11', 'option1'], ... to_type='int') 1 """ key = keylist[0] if len(keylist) > 1: return self.tree[key].get(keylist[1:], **kwargs) return string_conversion(self.tree[key], **kwargs)
def merge(self, config, keylist=None, update_tree=False)
-
Merges an other configuration.
The other configuration's options are inserted in the current tree, in a given section.
Parameters
config
:TreeConfigParser instance
- The other configuration.
keylist
:list
ofstr
, optional- The list of names which identify the section in which the other configuration should be merged (default: root).
update_tree
:bool
, optional- If True, new (sub)sections are created if necessary.
Notes
Ensure that the section names of both configuration instances are compatible before merging. In case of conflict, the updated values are those of config.
Examples
>>> config = TreeConfigParser() >>> config.set(['option1'], value='1') >>> config.set(['section1', 'option2'], value='2', ... update_tree=True) >>> other = TreeConfigParser() >>> other.set(['option2'], value='3') >>> other.set(['option3'], value='3') >>> config.merge(other, keylist=['section1']) >>> config.options() odict_keys(['option1', 'section1']) >>> config.options(['section1']) odict_keys(['option2', 'option3']) >>> config.get(['section1', 'option2']) '3'
Expand source code
def merge(self, config, keylist=None, update_tree=False): """Merges an other configuration. The other configuration's options are inserted in the current tree, in a given section. Parameters ---------- config : TreeConfigParser instance The other configuration. keylist : list of str, optional The list of names which identify the section in which the other configuration should be merged (default: root). update_tree : bool, optional If *True*, new (sub)sections are created if necessary. Notes ----- Ensure that the section names of both configuration instances are compatible before merging. In case of conflict, the updated values are those of *config*. Examples -------- >>> config = TreeConfigParser() >>> config.set(['option1'], value='1') >>> config.set(['section1', 'option2'], value='2', ... update_tree=True) >>> other = TreeConfigParser() >>> other.set(['option2'], value='3') >>> other.set(['option3'], value='3') >>> config.merge(other, keylist=['section1']) >>> config.options() odict_keys(['option1', 'section1']) >>> config.options(['section1']) odict_keys(['option2', 'option3']) >>> config.get(['section1', 'option2']) '3' """ if keylist: key = keylist[0] if update_tree and key not in self.tree: self.tree[key] = TreeConfigParser() self.tree[key].merge(config, keylist[1:], update_tree) else: for key in config.tree: if (key in self.tree and isinstance(config.tree[key], TreeConfigParser) and isinstance(self.tree[key], TreeConfigParser)): self.tree[key].merge(config.tree[key], update_tree=update_tree) elif isinstance(config.tree[key], TreeConfigParser): self.tree[key] = TreeConfigParser() self.tree[key].merge(config.tree[key], update_tree=update_tree) else: self.tree[key] = config.tree[key]
def options(self, keylist=None)
-
Returns the list of options.
Parameters
keylist
:list
ofstr
, optional- The list of names which identify the section for which we list the options (default: root).
Returns
option_list
:odict_keys
- The list of options and subsections for the section.
Examples
>>> config = TreeConfigParser() >>> config.set(['option1'], value='1') >>> config.set(['section1', 'option2'], ... value='2', update_tree=True) >>> config.set(['section1', 'subsection11', 'option3'], ... value='3', update_tree=True) >>> config.options() odict_keys(['option1', 'section1']) >>> config.options(['section1']) odict_keys(['option2', 'subsection11'])
Expand source code
def options(self, keylist=None): """Returns the list of options. Parameters ---------- keylist : list of str, optional The list of names which identify the section for which we list the options (default: root). Returns ------- option_list : odict_keys The list of options and subsections for the section. Examples -------- >>> config = TreeConfigParser() >>> config.set(['option1'], value='1') >>> config.set(['section1', 'option2'], ... value='2', update_tree=True) >>> config.set(['section1', 'subsection11', 'option3'], ... value='3', update_tree=True) >>> config.options() odict_keys(['option1', 'section1']) >>> config.options(['section1']) odict_keys(['option2', 'subsection11']) """ if keylist: return self.tree[keylist[0]].options(keylist[1:]) return self.tree.keys()
def read_file(self, file_name, convention='py', comment_char='#', reference_char='%', indentation=4)
-
Reads and parses file_name.
This is the main method of the class. The options are inserted in the current tree.
Parameters
file_name
:str
- The name of the file to parse.
convention
:{'py', 'flat', 'cpp'}
, optional- The format of the file.
comment_char
:str
, optional- The character used to delimit comments in the file.
reference_char
:str
, optional- The character used to delimit cross-references in the file.
indentation
:int
, optional- The indentation used to add suboptions or subsections in the file when using the 'py' format.
Raises
CrossReferenceError
- If the cross-references cannot be solved.
Expand source code
def read_file(self, file_name, convention='py', comment_char='#', reference_char='%', indentation=4): """Reads and parses *file_name*. This is the main method of the class. The options are inserted in the current tree. Parameters ---------- file_name : str The name of the file to parse. convention : {'py', 'flat', 'cpp'}, optional The format of the file. comment_char : str, optional The character used to delimit comments in the file. reference_char : str, optional The character used to delimit cross-references in the file. indentation : int, optional The indentation used to add suboptions or subsections in the file when using the 'py' format. Raises ------ CrossReferenceError If the cross-references cannot be solved. """ def is_tree_node(line): line = line.strip() return len(line) > 2 and line[0] == '[' and line[-1] == ']' def update_keylist(keylist, depth, indentation): if depth % indentation: raise IndentationError(line) depth = depth // indentation if depth > len(keylist): raise IndentationError(line) return keylist[:depth] def extract_tree_node_py(keylist, line, indentation): key = line.strip()[1:-1] keylist = update_keylist(keylist, line.find(key) - 1, indentation) return keylist + [key] def extract_tree_node_flat(line): return line.strip()[1:-1] def extract_tree_leaf_py(keylist, line, indentation): if '=' in line: split = line.split('=', 1) key = split[0].strip() value = split[1].strip() else: key = line.strip() value = '' keylist = update_keylist(keylist, line.find(key), indentation) self.set(keylist + [key], value, update_tree=True) return keylist def extract_tree_leaf_flat(section, line): if '=' in line: split = line.split('=', 1) key = split[0].strip() value = split[1].strip() else: key = line.strip() value = '' self.set([section, key], value, update_tree=True) return section def extract_tree_leaf_cpp(line): if '=' in line: split = line.split('=', 1) key = split[0].strip() value = split[1].strip() else: key = line.strip() value = '' keylist = key.split('.') self.set(keylist, value, update_tree=True) def read_line_py(line, keylist, comment_char, indentation): line = line.split(comment_char)[0] if not line or line.isspace(): return keylist if is_tree_node(line): return extract_tree_node_py(keylist, line, indentation) return extract_tree_leaf_py(keylist, line, indentation) def read_line_flat(line, section, comment_char): line = line.split(comment_char)[0] if not line or line.isspace(): return section if is_tree_node(line): return extract_tree_node_flat(line) return extract_tree_leaf_flat(section, line) def read_line_cpp(line, comment_char): line = line.split(comment_char)[0].strip() if line and not line.isspace(): extract_tree_leaf_cpp(line) def has_reference(value, reference_char): return len(value) > 2 and reference_char in value def extract_references(value, reference_char): return [ ref for (i, ref) in enumerate(value.split(reference_char)) if ref and i % 2 ] def solve_references_string(value, max_rec, reference_char): if not has_reference(value, reference_char): return value if max_rec == 0: raise CrossReferenceError reflist = extract_references(value, reference_char) for ref in reflist: keylist = ref.split('.') subvalue = solve_references_keylist(keylist, max_rec - 1, reference_char) value = value.replace(reference_char + ref + reference_char, subvalue) return value def solve_references_keylist(keylist, max_rec, reference_char): value = self.get(keylist) value = solve_references_string(value, max_rec, reference_char) self.set(keylist, value, update_tree=False) return value # read and parse file content lines = Path(file_name).lines(retain=False) if convention == 'py': keylist = [] for line in lines: keylist = read_line_py(line, keylist, comment_char, indentation) elif convention == 'flat': section = None for line in lines: section = read_line_flat(line, section, comment_char) elif convention == 'cpp': for line in lines: read_line_cpp(line, comment_char) # solve references keylistlist = self.suboptions() max_rec = len(keylistlist) - 1 for keylist in keylistlist: solve_references_keylist(keylist, max_rec, reference_char)
def remove_option(self, keylist)
-
Removes an option.
Parameters
keylist
:list
ofstr
- The list of names which identify the option or subsection to remove.
Notes
After this option is removed, the parent section may be empty, but it is not removed.
Expand source code
def remove_option(self, keylist): """Removes an option. Parameters ---------- keylist : list of str The list of names which identify the option or subsection to remove. Notes ----- After this option is removed, the parent section may be empty, but it is not removed. """ key = keylist[0] if len(keylist) > 1: self.tree[key].remove_option(keylist[1:]) else: del self.tree[key]
def set(self, keylist, value='', update_tree=False)
-
Sets an option value.
Parameters
keylist
:list
ofstr
- The list of names which identify the option.
value
:str
, optional- The new value for the option.
update_tree
:bool
, optional- If True, new (sub)sections are created if necessary.
Notes
This method can only create new options. New sections can be created if they are necessary for a new option, but empty (sub)sections cannot be created.
Expand source code
def set(self, keylist, value='', update_tree=False): """Sets an option value. Parameters ---------- keylist : list of str The list of names which identify the option. value : str, optional The new value for the option. update_tree : bool, optional If *True*, new (sub)sections are created if necessary. Notes ----- This method can only create new options. New sections can be created if they are necessary for a new option, but empty (sub)sections cannot be created. """ key = keylist[0] if len(keylist) > 1: if update_tree and key not in self.tree: self.tree[key] = TreeConfigParser() self.tree[key].set(keylist[1:], value, update_tree) else: self.tree[key] = value
def subconfig(self, keylist=None)
-
Return a reference to the (sub)configuration.
Parameters
keylist
:list
ofstr
, optional- The list of names which identify the (sub)configuration to reference (default: root).
Returns
copy
:TreeConfigParser instance
- The reference to the (sub)configuration.
Examples
>>> config = TreeConfigParser() >>> config.set(['section1', 'option1'], value='1', ... update_tree=True) >>> other = config.subconfig(keylist=['section1']) >>> other.set(['option2'], value='2') >>> other.options() odict_keys(['option1', 'option2']) >>> config.options(['section1']) odict_keys(['option1', 'option2'])
Expand source code
def subconfig(self, keylist=None): """Return a reference to the (sub)configuration. Parameters ---------- keylist : list of str, optional The list of names which identify the (sub)configuration to reference (default: root). Returns ------- copy : TreeConfigParser instance The reference to the (sub)configuration. Examples -------- >>> config = TreeConfigParser() >>> config.set(['section1', 'option1'], value='1', ... update_tree=True) >>> other = config.subconfig(keylist=['section1']) >>> other.set(['option2'], value='2') >>> other.options() odict_keys(['option1', 'option2']) >>> config.options(['section1']) odict_keys(['option1', 'option2']) """ if keylist: return self.tree[keylist[0]].subconfig(keylist[1:]) return self
def suboptions(self)
-
Returns the list of all (sub)options as keylists.
Returns
keylistlist
:list
oflist
ofstr
- The list of all available keylists. Each keylist identifies one option.
Examples
>>> config = TreeConfigParser() >>> config.set(['option1'], value='1') >>> config.set(['section1', 'option2'], ... value='2', update_tree=True) >>> config.set(['section1', 'subsection11', 'option3'], ... value='3', update_tree=True) >>> config.suboptions() [['option1'], ['section1', 'option2'], ['section1', 'subsection11', 'option3']]
Expand source code
def suboptions(self): """Returns the list of all (sub)options as keylists. Returns ------- keylistlist : list of list of str The list of all available keylists. Each keylist identifies one option. Examples -------- >>> config = TreeConfigParser() >>> config.set(['option1'], value='1') >>> config.set(['section1', 'option2'], ... value='2', update_tree=True) >>> config.set(['section1', 'subsection11', 'option3'], ... value='3', update_tree=True) >>> config.suboptions() [['option1'], ['section1', 'option2'], \ ['section1', 'subsection11', 'option3']] """ keylistlist = [] for key in self.tree: if isinstance(self.tree[key], TreeConfigParser): for subkeylist in self.tree[key].suboptions(): keylistlist.append([key] + subkeylist) else: keylistlist.append([key]) return keylistlist
def substitution(self, old_string, new_string, keylist=None)
-
Performs a string substitution.
Replaces old_string by new_string. If keylist identifies an option, the substitution is applied only to that option. If keylist identifies a section, the substitution is applied to all options and subsections in that section.
Uses the
str.replace
method.Parameters
old_string
:str
- The old
str
pattern. new_string
:str
- The new
str
pattern. keylist
:list
ofstr
- The list of names which identify the option or the section in which the substitution is applied (default: root).
Expand source code
def substitution(self, old_string, new_string, keylist=None): """Performs a string substitution. Replaces *old_string* by *new_string*. If *keylist* identifies an option, the substitution is applied only to that option. If *keylist* identifies a section, the substitution is applied to all options and subsections in that section. Uses the `str.replace` method. Parameters ---------- old_string : str The old `str` pattern. new_string : str The new `str` pattern. keylist : list of str The list of names which identify the option or the section in which the substitution is applied (default: root). """ if keylist: self.tree[keylist[0]].substitution(old_string, new_string, keylist[1:]) else: for key in self.tree: if isinstance(self.tree[key], TreeConfigParser): self.tree[key].substitution(old_string, new_string) else: self.tree[key] = self.tree[key].replace( old_string, new_string)
def tofile(self, file_name, convention='py', indentation=4)
-
Writes the configuration options to a file.
Once written, the file can be read and parsed by an other
TreeConfigParser
instance.Parameters
file_name
:str
- The name of the file to write.
convention
:{'py', 'flat', 'cpp'}
, optional- The format of the file.
indentation
:int
, optional- The indentation used to add suboptions or subsections in the file when using the 'py' format.
Expand source code
def tofile(self, file_name, convention='py', indentation=4): """Writes the configuration options to a file. Once written, the file can be read and parsed by an other `TreeConfigParser` instance. Parameters ---------- file_name : str The name of the file to write. convention : {'py', 'flat', 'cpp'}, optional The format of the file. indentation : int, optional The indentation used to add suboptions or subsections in the file when using the 'py' format. """ def write_tree_node_line_py(depth, indentation, key): return [' ' * depth * indentation + f'[{key}]'] def write_tree_leaf_line_py(depth, indentation, key, value): return [' ' * depth * indentation + f'{key} = {value}'] def write_tree_leaf_line_cpp(keylist, value): return ['.'.join(keylist) + f' = {value}'] def write_tree_py(tree, depth, indentation): lines = [] for key in tree: if isinstance(tree[key], TreeConfigParser): lines.extend( write_tree_node_line_py(depth, indentation, key)) lines.extend( write_tree_py(tree[key].tree, depth + 1, indentation)) else: lines.extend( write_tree_leaf_line_py(depth, indentation, key, tree[key])) return lines def write_tree_cpp(tree, keylist): lines = [] for key in tree: if isinstance(tree[key], TreeConfigParser): lines.extend( write_tree_cpp(tree[key].tree, keylist + [key])) else: lines.extend( write_tree_leaf_line_cpp(keylist + [key], tree[key])) return lines # write lines if convention == 'py': lines = write_tree_py(self.tree, 0, indentation) elif convention == 'flat': lines = write_tree_py(self.tree, 0, 0) elif convention == 'cpp': lines = write_tree_cpp(self.tree, []) Path(file_name).write_lines(lines)