mirror of
https://github.com/weewx/weewx.git
synced 2026-04-19 00:56:54 -04:00
266 lines
9.2 KiB
Python
266 lines
9.2 KiB
Python
#
|
|
# Copyright (c) 2018-2023 Tom Keffer <tkeffer@gmail.com>
|
|
#
|
|
# See the file LICENSE.txt for your full rights.
|
|
#
|
|
|
|
"""Convenience functions for ConfigObj"""
|
|
|
|
import configobj
|
|
from configobj import Section
|
|
|
|
|
|
def search_up(d, k, *default):
|
|
"""Search a ConfigObj dictionary for a key. If it's not found, try my parent, and so on
|
|
to the root.
|
|
|
|
d: An instance of configobj.Section
|
|
|
|
k: A key to be searched for. If not found in d, it's parent will be searched
|
|
|
|
default: If the key is not found, then the default is returned. If no default is given,
|
|
then an AttributeError exception is raised.
|
|
|
|
Example:
|
|
|
|
>>> c = configobj.ConfigObj({"color":"blue", "size":10, "robin":{"color":"red", "sound": {"volume": "loud"}}})
|
|
>>> print(search_up(c['robin'], 'size'))
|
|
10
|
|
>>> print(search_up(c, 'color'))
|
|
blue
|
|
>>> print(search_up(c['robin'], 'color'))
|
|
red
|
|
>>> print(search_up(c['robin'], 'flavor', 'salty'))
|
|
salty
|
|
>>> try:
|
|
... print(search_up(c['robin'], 'flavor'))
|
|
... except AttributeError:
|
|
... print('not found')
|
|
not found
|
|
>>> print(search_up(c['robin'], 'sound'))
|
|
{'volume': 'loud'}
|
|
>>> print(search_up(c['robin'], 'smell', {}))
|
|
{}
|
|
"""
|
|
if k in d:
|
|
return d[k]
|
|
if d.parent is d:
|
|
if len(default):
|
|
return default[0]
|
|
else:
|
|
raise AttributeError(k)
|
|
else:
|
|
return search_up(d.parent, k, *default)
|
|
|
|
|
|
def accumulateLeaves(d, max_level=99):
|
|
"""Merges leaf options above a ConfigObj section with itself, accumulating the results.
|
|
|
|
This routine is useful for specifying defaults near the root node,
|
|
then having them overridden in the leaf nodes of a ConfigObj.
|
|
|
|
d: instance of a configobj.Section (i.e., a section of a ConfigObj)
|
|
|
|
Returns: a dictionary with all the accumulated scalars, up to max_level deep,
|
|
going upwards
|
|
|
|
Example: Supply a default color=blue, size=10. The section "dayimage" overrides the former:
|
|
|
|
>>> c = configobj.ConfigObj({"color":"blue", "size":10, "dayimage":{"color":"red", "position":{"x":20, "y":30}}})
|
|
>>> accumulateLeaves(c["dayimage"]) == {"color":"red", "size": 10}
|
|
True
|
|
>>> accumulateLeaves(c["dayimage"], max_level=0) == {'color': 'red'}
|
|
True
|
|
>>> accumulateLeaves(c["dayimage"]["position"]) == {'color': 'red', 'size': 10, 'y': 30, 'x': 20}
|
|
True
|
|
>>> accumulateLeaves(c["dayimage"]["position"], max_level=1) == {'color': 'red', 'y': 30, 'x': 20}
|
|
True
|
|
"""
|
|
|
|
# Use recursion. If I am the root object, then there is nothing above
|
|
# me to accumulate. Start with a virgin ConfigObj
|
|
if d.parent is d:
|
|
cum_dict = configobj.ConfigObj()
|
|
else:
|
|
if max_level:
|
|
# Otherwise, recursively accumulate scalars above me
|
|
cum_dict = accumulateLeaves(d.parent, max_level - 1)
|
|
else:
|
|
cum_dict = configobj.ConfigObj()
|
|
|
|
# Now merge my scalars into the results:
|
|
merge_dict = {k: d[k] for k in d.scalars}
|
|
cum_dict.merge(merge_dict)
|
|
return cum_dict
|
|
|
|
|
|
def merge_config(self_config, indict):
|
|
"""Merge and patch a config file"""
|
|
|
|
self_config.merge(indict)
|
|
patch_config(self_config, indict)
|
|
|
|
|
|
def patch_config(self_config, indict):
|
|
"""The ConfigObj merge does not transfer over parentage, nor comments. This function
|
|
fixes these limitations.
|
|
|
|
Example:
|
|
>>> import sys
|
|
>>> from io import StringIO
|
|
>>> c = configobj.ConfigObj(StringIO('''[Section1]
|
|
... option1 = bar'''))
|
|
>>> d = configobj.ConfigObj(StringIO('''[Section1]
|
|
... # This is a Section2 comment
|
|
... [[Section2]]
|
|
... option2 = foo
|
|
... '''))
|
|
>>> c.merge(d)
|
|
>>> # First do accumulateLeaves without a patch
|
|
>>> print(accumulateLeaves(c['Section1']['Section2']))
|
|
{'option2': 'foo'}
|
|
>>> # Now patch and try again
|
|
>>> patch_config(c, d)
|
|
>>> print(accumulateLeaves(c['Section1']['Section2']))
|
|
{'option1': 'bar', 'option2': 'foo'}
|
|
>>> c.write()
|
|
['[Section1]', 'option1 = bar', '# This is a Section2 comment', '[[Section2]]', 'option2 = foo']
|
|
"""
|
|
for key in self_config:
|
|
if isinstance(self_config[key], Section) \
|
|
and key in indict and isinstance(indict[key], Section):
|
|
self_config[key].parent = self_config
|
|
self_config[key].main = self_config.main
|
|
self_config.comments[key] = indict.comments[key]
|
|
self_config.inline_comments[key] = indict.inline_comments[key]
|
|
patch_config(self_config[key], indict[key])
|
|
|
|
|
|
def comment_scalar(a_dict, key):
|
|
"""Comment out a scalar in a ConfigObj object.
|
|
|
|
Convert an entry into a comment, sticking it at the beginning of the section.
|
|
|
|
Returns: 0 if nothing was done.
|
|
1 if the ConfigObj object was changed.
|
|
"""
|
|
|
|
# If the key is not in the list of scalars there is no need to do anything.
|
|
if key not in a_dict.scalars:
|
|
return 0
|
|
|
|
# Save the old comments
|
|
comment = a_dict.comments[key]
|
|
inline_comment = a_dict.inline_comments[key]
|
|
if inline_comment is None:
|
|
inline_comment = ''
|
|
# Build a new inline comment holding the key and value, as well as the old inline comment
|
|
new_inline_comment = "%s = %s %s" % (key, a_dict[key], inline_comment)
|
|
|
|
# Delete the old key
|
|
del a_dict[key]
|
|
|
|
# If that was the only key, there's no place to put the comments. Do nothing.
|
|
if len(a_dict.scalars):
|
|
# Otherwise, put the comments before the first entry
|
|
first_key = a_dict.scalars[0]
|
|
a_dict.comments[first_key] += comment
|
|
a_dict.comments[first_key].append(new_inline_comment)
|
|
|
|
return 1
|
|
|
|
|
|
def delete_scalar(a_dict, key):
|
|
"""Delete a scalar in a ConfigObj object.
|
|
|
|
Returns: 0 if nothing was done.
|
|
1 if the scalar was deleted
|
|
"""
|
|
|
|
if key not in a_dict.scalars:
|
|
return 0
|
|
|
|
del a_dict[key]
|
|
return 1
|
|
|
|
|
|
def conditional_merge(a_dict, b_dict):
|
|
"""Merge fields from b_dict into a_dict, but only if they do not yet
|
|
exist in a_dict"""
|
|
# Go through each key in b_dict
|
|
for k in b_dict:
|
|
if isinstance(b_dict[k], dict):
|
|
if k not in a_dict:
|
|
# It's a new section. Initialize it...
|
|
a_dict[k] = {}
|
|
# ... and transfer over the section comments, if available
|
|
try:
|
|
a_dict.comments[k] = b_dict.comments[k]
|
|
except AttributeError:
|
|
pass
|
|
conditional_merge(a_dict[k], b_dict[k])
|
|
elif k not in a_dict:
|
|
# It's a scalar. Transfer over the value...
|
|
a_dict[k] = b_dict[k]
|
|
# ... then its comments, if available:
|
|
try:
|
|
a_dict.comments[k] = b_dict.comments[k]
|
|
except AttributeError:
|
|
pass
|
|
|
|
|
|
def config_from_str(input_str):
|
|
"""Return a ConfigObj from a string. Values will be in Unicode."""
|
|
from io import StringIO
|
|
config = configobj.ConfigObj(StringIO(input_str), encoding='utf-8', default_encoding='utf-8')
|
|
return config
|
|
|
|
|
|
def deep_copy(old_dict, parent=None, depth=None, main=None):
|
|
"""Return a deep copy of a ConfigObj"""
|
|
|
|
# Is this a copy starting from the top level?
|
|
if isinstance(old_dict, configobj.ConfigObj):
|
|
new_dict = configobj.ConfigObj('',
|
|
encoding=old_dict.encoding,
|
|
default_encoding=old_dict.default_encoding,
|
|
interpolation=old_dict.interpolation,
|
|
indent_type=old_dict.indent_type)
|
|
new_dict.initial_comment = list(old_dict.initial_comment)
|
|
else:
|
|
# No. It's a copy of something deeper down. If no parent or main is given, then
|
|
# adopt the parent and main of the incoming dictionary.
|
|
new_dict = configobj.Section(parent if parent is not None else old_dict.parent,
|
|
depth if depth is not None else old_dict.depth,
|
|
main if main is not None else old_dict.main)
|
|
for entry in old_dict:
|
|
# Avoid interpolation by using the version of __getitem__ from dict
|
|
old_value = dict.__getitem__(old_dict, entry)
|
|
if isinstance(old_value, configobj.Section):
|
|
new_value = deep_copy(old_value, new_dict, new_dict.depth + 1, new_dict.main)
|
|
elif isinstance(old_value, list):
|
|
# Make a copy
|
|
new_value = list(old_value)
|
|
elif isinstance(old_value, tuple):
|
|
# Make a copy
|
|
new_value = tuple(old_value)
|
|
else:
|
|
# It's a scalar, possibly a string
|
|
new_value = old_value
|
|
new_dict[entry] = new_value
|
|
# A comment is a list of strings. We need to make a copy of the list, but the strings
|
|
# themselves are immutable, so we don't need to copy them. That means a simple shallow
|
|
# copy will do:
|
|
new_dict.comments[entry] = list(old_dict.comments[entry])
|
|
# An inline comment is either None, or a string. Either way, they are immutable, so
|
|
# a simple assignment will work:
|
|
new_dict.inline_comments[entry] = old_dict.inline_comments[entry]
|
|
return new_dict
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
|
|
if not doctest.testmod().failed:
|
|
print("PASSED")
|