#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
# Copyright 2013 Marcus Popp marcus@popp.mx
# Copyright 2016 The SmartHomeNG team
#########################################################################
# This file is part of SmartHomeNG.
#
# SmartHomeNG is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SmartHomeNG is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SmartHomeNG. If not, see <http://www.gnu.org/licenses/>.
#########################################################################
"""
This library does the handling and parsing of the configuration of SmartHomeNG.
:Warning: This library is part of the core of SmartHomeNG. It **should not be called directly** from plugins!
"""
import copy
import logging
import collections
import keyword
import os
from lib.utils import Utils
import lib.shyaml as shyaml
from lib.constants import (YAML_FILE, CONF_FILE)
logger = logging.getLogger(__name__)
valid_item_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
valid_attr_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_@*'
digits = '0123456789'
reserved = ['set', 'get', 'property']
REMOVE_ATTR = 'attr'
REMOVE_PATH = 'path'
[Doku]def parse_basename(basename, configtype=''):
"""
Load and parse a single configuration and merge it to the configuration tree
The configuration is only specified by the basename.
At the moment it looks for a .yaml file or a .conf file
.yaml files take preference
:param basename: Name of the configuration
:param configtype: Optional string with config type (only used for log output)
:type basename: str
:type configtype: str
:return: The resulting merged OrderedDict tree
:rtype: OrderedDict
"""
config = parse(basename + YAML_FILE)
if config == {}:
config = parse(basename + CONF_FILE)
if config == {}:
if configtype == 'module':
logger.warning(f"No valid file '{basename}{YAML_FILE}' found with {configtype} configuration")
elif configtype != 'logics':
logger.error(f"No valid file '{basename}.*' found with {configtype} configuration")
return config
[Doku]def parse_itemsdir(itemsdir, item_conf, addfilenames=False, struct_dict={}):
"""
Load and parse item configurations and merge it to the configuration tree
The configuration is only specified by the name of the directory.
At the moment it looks for .yaml files and a .conf files
Both filetypes are read, even if they have the same basename
:param itemsdir: Name of folder containing the configuration files
:param item_conf: Optional OrderedDict tree, into which the configuration should be merged
:param addfilenames:
:param struct_dict: dict with all defined structs (from /etc/structs.yaml and from loaded plugins)
:type itemsdir: str
:type item_conf: OrderedDict
:type addfilenames:
:type struct_dict: dict / OrderedDict
:return: The resulting merged OrderedDict tree
:rtype: OrderedDict
"""
logger.info(f"parse_itemsdir: Beginning to parse items directory {itemsdir}")
for item_file in sorted(os.listdir(itemsdir)):
if not item_file.startswith('.'):
if item_file.endswith(CONF_FILE) or item_file.endswith(YAML_FILE):
if item_file == 'logic' + YAML_FILE and itemsdir.find(os.path.join('lib', 'env')) > -1:
logger.info(f"parse_itemsdir: skipping logic definition file = {itemsdir + item_file}")
else:
try:
item_conf = parse(itemsdir + item_file, item_conf, addfilenames, parseitems=True, struct_dict=struct_dict)
except Exception as e:
logger.exception(f"Problem reading {item_file}: {e}")
continue
logger.info(f"parse_itemsdir: Finished parsing items directory {itemsdir}")
return item_conf
[Doku]def parse(filename, config=None, addfilenames=False, parseitems=False, struct_dict={}):
"""
Load and parse a configuration file and merge it to the configuration tree
Depending on the extension of the filename, the apropriate parser is called
:param filename: Name of the configuration file
:param config: Optional OrderedDict tree, into which the configuration should be merged
:param struct_dict: dict with all defined structs (from /etc/structs.yaml and from loaded plugins)
:type filename: str
:type config: OrderedDict
:return: The resulting merged OrderedDict tree
:rtype: OrderedDict
"""
if not filename.startswith('.'):
if filename.endswith(YAML_FILE) and os.path.isfile(filename):
return parse_yaml(filename, config, addfilenames, parseitems, struct_dict)
elif filename.endswith(CONF_FILE) and os.path.isfile(filename):
return parse_conf(filename, config)
return {}
# --------------------------------------------------------------------------------------
[Doku]def remove_keys(ydata, func, remove=[REMOVE_ATTR], level=0, msg=None, key_prefix=''):
"""
Removes given keys from a dict or OrderedDict structure
:param ydata: configuration (sub)tree to work on
:param func: the function to call to check for removal (Example: lambda k: k.startswith('comment'))
:param level: optional subtree level (used for recursion)
:type ydata: OrderedDict
:type func: function
:type level: int
"""
try:
level_keys = list(ydata.keys())
for key in level_keys:
key_str = str(key)
key_is_dict = type(ydata[key]).__name__ in ['dict', 'OrderedDict']
if key_is_dict:
key_remove = REMOVE_PATH in remove and func(key_str)
else:
key_remove = REMOVE_ATTR in remove and func(key_str)
if key_remove:
if msg:
logger.warning(msg.format(key_prefix + key_str))
ydata.pop(key)
elif key_is_dict:
remove_keys(ydata[key], func, remove, level + 1, msg, key_prefix + key_str + '.')
except Exception as e:
logger.error(f"Problem removing key from '{str(ydata)}', probably invalid YAML file: {e}")
[Doku]def remove_digits(ydata, filename=''):
"""
Removes keys starting with digits from a dict or OrderedDict structure
:param ydata: configuration (sub)tree to work on
:type ydata: OrderedDict
"""
remove_keys(ydata, lambda k: k[0] in digits, [REMOVE_ATTR, REMOVE_PATH], msg="Problem parsing '{}' in file '" + filename + "': item starts with digits")
[Doku]def remove_reserved(ydata, filename=''):
"""
Removes keys that are reserved keywords from a dict or OrderedDict structure
:param ydata: configuration (sub)tree to work on
:type ydata: OrderedDict
"""
remove_keys(ydata, lambda k: k in reserved, [REMOVE_PATH], msg="Problem parsing '{}' in file '" + filename + "': item using reserved word set/get")
[Doku]def remove_keyword(ydata, filename=''):
"""
Removes keys that are reserved Python keywords from a dict or OrderedDict structure
:param ydata: configuration (sub)tree to work on
:type ydata: OrderedDict
"""
remove_keys(ydata, lambda k: keyword.iskeyword(k), [REMOVE_PATH], msg="Problem parsing '{}' in file '" + filename + "': item using reserved Python keyword")
[Doku]def remove_invalid(ydata, filename=''):
"""
Removes invalid chars in item from a dict or OrderedDict structure
:param ydata: configuration (sub)tree to work on
:type ydata: OrderedDict
"""
valid_chars = valid_item_chars + valid_attr_chars
remove_keys(ydata, lambda k: not all(k[i] in valid_chars for i in range(len(k))), [REMOVE_ATTR, REMOVE_PATH],
msg="Problem parsing '{}' in file '" + filename + "': Invalid character. Valid characters are: " + str(valid_chars))
[Doku]def sanitize_items(ydata, filename=''):
"""
Remove all invalid entries from OrderedDict structure
:param ydata: configuration (sub)tree to work on
:type ydata: OrderedDict
"""
remove_comments(ydata, filename)
remove_digits(ydata, filename)
remove_reserved(ydata, filename)
remove_keyword(ydata, filename)
remove_invalid(ydata, filename)
struct_merging_active = False
struct_merge_lists = True
special_listentry_found = False
[Doku]def merge_structlists(l1, l2, key=''):
if not struct_merging_active:
global special_listentry_found
# merge* or merge_unique*
if (len(l1) > 0 and l1[0] == 'merge_unique*') and (len(l2) > 0 and l2[0] == 'merge_unique*'):
logger.debug(f"merge_structlists: merge_unique* l1={l1}, l2={l2}")
logger.debug(f"merge_structlists: both lists contains 'merge_unique*' - l1={l1}, l2={l2}, key={key}")
special_listentry_found = True
l1 = list(collections.OrderedDict.fromkeys(l1))
return l1
if (len(l1) > 0 and l1[0] == 'merge*') and (len(l2) > 0 and l2[0] == 'merge*'):
logger.debug(f"merge_structlists: merge* l1={l1}, l2={l2}")
logger.debug(f"merge_structlists: both lists contains 'merge*' - l1={l1}, l2={l2}, key={key}")
special_listentry_found = True
return l1
if (len(l2) > 0 and l2[0] == 'merge*'):
logger.debug(f"merge_structlists: l2 contains merge* l1={l1}, l2={l2}")
logger.debug(f"merge_structlists: list l2 contains 'merge*' - l1={l1}, l2={l2}, key={key}")
del l2[0]
l1 = ['merge*'] + l1 + l2
l2 = ['merge*'] + l2
return l1
if (len(l1) > 0 and l1[0] == 'merge*') or (len(l2) > 0 and l2[0] == 'merge*'):
logger.debug(f"merge_structlists: l1 or l2 contain merge* l1={l1}, l2={l2}")
# logger.warning(f"merge_structlists: a list contains 'merge*' - l1={l1}, l2={l2}, key={key}")
pass
return l2 # Last wins
if not struct_merge_lists:
# logger.warning(f"merge_structlists: Not merging lists, key '{key}' value '{l2}' is ignored'")
return l1 # First wins
else:
if not isinstance(l1, list):
l1 = [l1]
if not isinstance(l2, list):
l2 = [l2]
return l1 + l2
[Doku]def merge(source, destination, source_name='', dest_name='', filename=''):
"""
Merges an OrderedDict Tree into another one
:param source: source tree to merge into another one
:param destination: destination tree to merge into
:type source: OrderedDict
:type destination: OrderedDict
:return: Merged configuration tree
:rtype: OrderedDict
:Example: Run me with nosetests --with-doctest file.py
.. code-block:: python
>>> a = { 'first' : { 'all_rows' : { 'pass' : 'dog', 'number' : '1' } } }
>>> b = { 'first' : { 'all_rows' : { 'fail' : 'cat', 'number' : '5' } } }
>>> merge(b, a) == { 'first' : { 'all_rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } }
True
"""
if source.get('_filename', '') == 'test_struct.yaml':
logger.info(f"merge {source.get('_filename', '')}: source={dict(source)}, destination={dict(destination)}, source_name={source_name}, dest_name={dest_name}")
ext_logging = True
else:
ext_logging = False
for key, value in source.items():
try:
if isinstance(value, collections.OrderedDict):
# get node or create one
node = destination.setdefault(key, collections.OrderedDict())
if node == 'None':
destination[key] = value
else:
merge(value, node, source_name, dest_name)
else:
# if struct_merging_active:
# logger.warning(f"merge: - value={value}, destination.get(key, None)={destination.get(key, None)}")
if isinstance(value, list) or isinstance(destination.get(key, None), list):
if destination.get(key, None) is None:
destination[key] = value
else:
if ext_logging:
logger.info(f"merge: call merge_structlists - key={key}, value={value}, destination.get(key, None)={destination.get(key, None)}")
destination[key] = merge_structlists(destination[key], value, key)
else:
# convert to string and remove newlines from multiline attributes
destination[key] = str(value).replace('\n', '')
# if destination.get(key, None) is None:
# destination[key] = str(value).replace('\n', '')
# if type(value).__name__ == 'list':
# destination[key] = value
# else:
# # convert to string and remove newlines from multiline attributes
# destination[key] = str(value).replace('\n','')
# if struct_merging_active:
# logger.warning(f"merge: - destination={dict(destination)}")
except Exception as e:
logger.error(f"Problem merging subtrees (key={key}), probably invalid YAML file '{source_name}' with entry '{destination}'. Error: {e}")
return destination
# -------------------------------------------------------------------------------------
# Handling of structs while loading item tree from yaml files
#
[Doku]def nested_get(input_dict, path):
internal_dict_value = input_dict
nested_key = path.split('.')
for k in nested_key:
internal_dict_value = internal_dict_value.get(k, None)
if internal_dict_value is None:
return None
return internal_dict_value
[Doku]def nested_put(output_dict, path, value):
"""
:param output_dict: dict structure to write to
:param path: path to write to
:param value: value to write to the nested key
:return:
"""
internal_dict_value = output_dict
nested_key = path.split('.')
internal_last_dict_value = None
# if struct_merging_active:
# logger.warning(f"nested_put: path = {path}, value = {value} - nested_key = {nested_key}")
# logger.warning(f"nested_put: - output_dict = {dict(output_dict)}")
for k in nested_key:
if internal_dict_value.get(k, None) is None:
if isinstance(output_dict, collections.OrderedDict):
internal_dict_value[k] = collections.OrderedDict()
else:
internal_dict_value[k] = {}
internal_last_dict_value = internal_dict_value
internal_dict_value = internal_dict_value.get(k, None)
if internal_last_dict_value is not None:
# if struct_merging_active:
# logger.warning(f"nested_put: - dest subtree = {dict(internal_last_dict_value[nested_key[len(nested_key)-1]])}")
# logger.warning(f"nested_put: - merge struct = {dict(value)}")
# internal_last_dict_value[nested_key[len(nested_key)-1]] = value
merge(value, internal_last_dict_value[nested_key[len(nested_key) - 1]], 'struct-tree', 'sub-tree')
# if struct_merging_active:
# logger.warning(f"nested_put: - dest result = {dict(internal_last_dict_value[nested_key[len(nested_key)-1]])}")
# if struct_merging_active:
# logger.warning(f"nested_put: - internal_last_dict_value = {internal_last_dict_value}")
return
[Doku]def search_for_struct_in_items(items, struct_dict, config, source_name='', parent='', level=0):
"""
Test if the loaded file contains items with 'struct' attribute.
This function is (recursively) called before merging the loaded file into the item tree
:param items: tree content of a single items.yaml file (or part of it during recursion)
:param struct_dict: dict with all defined structs (from /etc/structs.yaml and from loaded plugins)
:param config: tree, into which the configuration should be merged
:param parent:
:type items: OrderedDict
:type config: OrderedDict
:return: True, if a struct attribute was expanded
"""
if source_name.startswith('test_struct'):
logger.info(f"search_for_struct_in_items: items.keys()={list(dict(items).keys())}, source_name={source_name}, parent={parent}")
for key in items:
value = items[key]
if source_name.startswith('test_struct'):
if isinstance(value, collections.OrderedDict):
logger.info(f"search_for_struct_in_items: - items[{key}]={dict(value)}")
else:
logger.info(f"search_for_struct_in_items: - items[{key}]={value}")
if key == 'struct':
# item is a struct
struct_names = value
# ensure, struct_names is a list
if isinstance(struct_names, str):
struct_names = [struct_names]
instance = items.get('instance', '')
template = collections.OrderedDict()
global struct_merging_active
struct_merging_active = True
for struct_name in struct_names:
wrk = struct_name.find('@')
if wrk > -1:
add_struct_to_item_template(parent, struct_name[:wrk], template, struct_dict, struct_name[wrk + 1:])
else:
add_struct_to_item_template(parent, struct_name, template, struct_dict, instance)
if template != {}:
config = merge(template, config, source_name, 'Item-Tree')
struct_merging_active = False
else:
# item is no struct
if isinstance(value, collections.OrderedDict):
# treat value as node
if parent == '':
path = key
else:
path = parent + '.' + key
# test if a aub-item is a struct
search_for_struct_in_items(value, struct_dict, config, source_name, parent=path, level=level + 1)
template = collections.OrderedDict()
nested_put(template, path, value)
config = merge(template, config, source_name, 'Item-Tree')
return
[Doku]def remove_special_listentries(config, filename=''):
for k, v in config.items():
if isinstance(v, dict):
remove_special_listentries(v, filename)
else:
if isinstance(v, list):
if len(v) > 0 and v[0] in ['merge*', 'merge_unique*']:
# logger.warning(f"remove_special_listentries: a list={k} -> {v} - {filename}")
del v[0]
[Doku]def set_attr_for_subtree(subtree, attr, value, indent=0):
"""
:param subtree: dict (subtree) to operate on
:param attr: Attribute to set for every item
:param value: Value to set the attribute to
:param indent: indent level (only for debug-logging)
:return:
"""
for k, v in subtree.items():
if isinstance(v, dict):
v[attr] = value
spc = " " * 2 * indent
logger.debug(f"set_attr_for_subtree:{spc} node: {k} => {v}")
set_attr_for_subtree(v, attr, value, indent + 1)
return
[Doku]def add_struct_to_item_template(path, struct_name, template, struct_dict, instance):
"""
Add the referenced struct to the items_template subtree
:param path: Path of the item which references a struct (template)
:param struct_name: Name of the to use for the item
:param template: Template dict to be merged into the item tree
:param struct_dict: dict with all defined structs (from /etc/structs.yaml and from loaded plugins)
:param instance: For multi instance plugins: instance for which the items work (is derived from item with struct attribute)
:return:
"""
logger.info(f"add_struct_to_item_template: path (parent)={path}, struct_name={struct_name}, template={dict(template)}")
struct = struct_dict.get(struct_name, None)
if struct is None:
# no struct/template with this name
nf = collections.OrderedDict()
nf['name'] = "ERROR: struct '" + struct_name + "' not found!"
# nf['value'] = nf['name']
nested_put(template, path, nf)
logger.error(f"add_struct_to_item_template: Struct definition for '{struct_name}' not found (referenced in item {path})")
else:
# add struct/template to temporary item(template) tree
#logger.debug("- add_struct_to_item_template: struct_dict = {}".format(dict(struct_dict)))
#logger.debug("- add_struct_to_item_template: struct '{}' to item '{}'".format(struct_name, path))
tmp_struct = copy.deepcopy(struct)
if 'name' in tmp_struct and isinstance(struct["name"], str):
from lib.smarthome import SmartHome
_sh = SmartHome.get_instance()
if Utils.to_bool(getattr(_sh, '_struct_strip_name', False)):
del tmp_struct['name']
logger.debug(f'removed "name" attribute from struct {struct_name}')
nested_put(template, path, tmp_struct)
if instance != '' or True:
# add instance to items added by template struct
subtree = nested_get(template, path)
# logger.info(f"add_struct_to_item_template: Adding 'instance: {instance}' to template for subtree '{path}'")
# add instance name to attributes which carry '@instance'
logger.debug(f"- add_struct_to_item_template: Add instance={instance} to subtree={subtree}")
replace_struct_instance(path, subtree, instance)
logger.info(f"- add_struct_to_item_template: - after add - template={dict(template)}")
return
[Doku]def replace_struct_instance(path, subtree, instance):
"""
Replace the constant string '@instance' in attribute names with the real instance
(or remove the constant string '@instance', if the struct has no instace reference)
:param path:
:param subtree:
:param instance:
:return:
"""
keys = list(subtree.keys())
# logger.info(f"replace_struct_instance: Setting instance to {instance} for subtree {subtree}")
for key in keys:
# replace recursively
if Utils.get_type(subtree[key]) == 'collections.OrderedDict':
replace_struct_instance(path, subtree[key], instance)
if key.endswith('@instance'):
if instance == '':
newkey = key[:-9]
else:
newkey = key[:-9] + '@' + instance
# logger.debug(f"replace_struct_instance: - path {path}: key '{key}' --> newkey '{newkey}'")
subtree[newkey] = subtree.pop(key)
# logger.info(f"replace_struct_instance: Done set instance to {instance} for subtree {subtree}")
return
[Doku]def parse_yaml(filename, config=None, addfilenames=False, parseitems=False, struct_dict={}):
"""
Load and parse a yaml configuration file and merge it to the configuration tree
:param filename: Name of the configuration file
:param config: Optional OrderedDict tree, into which the configuration should be merged
:param addfilenames: x
:param parseitems: x
:param struct_dict: dictionary with stuct definitions (templates) for reading item tree
:type filename: str
:type config: bool
:type addfilenames: bool
:type parseitems: bool
:type struct_dict: dict
:return: The resulting merged OrderedDict tree
:rtype: OrderedDict
The config file should stick to the following setup:
.. code-block:: yaml
firstlevel:
attribute1: xyz
attribute2: foo
attribute3: bar
secondlevel:
attribute1: abc
attribute2: bar
attribute3: foo
thirdlevel:
attribute1: def
attribute2: barfoo
attribute3: foobar
anothersecondlevel:
attribute1: and so on
where firstlevel, secondlevel, thirdlevel and anothersecondlevel are defined as items and attribute are their respective attribute - value pairs
Valid characters for the items are a-z and A-Z plus any digit and underscore as second or further characters.
Valid characters for the attributes are the same as for an item plus @ and *
"""
if os.path.basename(filename).startswith('test_'):
logger.info(f"parse_yaml: Parsing file {os.path.basename(filename)}")
if config is None:
config = collections.OrderedDict()
items = shyaml.yaml_load(filename, ordered=True)
if items is not None:
sanitize_items(items, filename)
if addfilenames:
# logger.debug(f"parse_yaml: Add filename = {os.path.basename(filename)} to items")
_add_filenames_to_config(items, os.path.basename(filename))
if parseitems:
# test if file contains 'struct' attribute and merge all items into config
# logger.debug(f"parse_yaml: Checking if file {os.path.basename(filename)} contains 'struct' attribute")
search_for_struct_in_items(items, struct_dict, config, os.path.basename(filename))
global special_listentry_found
if special_listentry_found:
remove_special_listentries(config, os.path.basename(filename))
special_listentry_found = False
if not parseitems:
# if not parsing items
config = merge(items, config, os.path.basename(filename), 'Config-Tree')
return config
def _add_filenames_to_config(items, filename, level=0):
"""
Adds the name of the config file to the config items
This routine is used to add the source filename to:
- be able to display the file an item is defined in (backend page items)
- to enable editing and storing back of item definitions
This function calls itself recurselively
"""
for attr, value in items.items():
if isinstance(value, dict):
child_path = dict(value)
if (filename != ''):
value['_filename'] = filename
_add_filenames_to_config(child_path, filename, level + 1)
return
# --------------------------------------------------------------------------------------
[Doku]def strip_quotes(string):
"""
Strip single-quotes or double-quotes from string beggining and end
:param string: String to strip the quotes from
:type string: str
:return: Stripped string
:rtype: str
"""
string = string.strip()
if len(string) > 0:
if string[0] in ['"', "'"]: # check if string starts with ' or "
if string[0] == string[-1]: # and end with it
if string.count(string[0]) == 2: # if they are the only one
string = string[1:-1] # remove them
return string
[Doku]def parse_conf(filename, config=None):
"""
Load and parse a configuration file which is in the old .conf format of smarthome.py
and merge it to the configuration tree
:param filename: Name of the configuration file
:param config: Optional OrderedDict tree, into which the configuration should be merged
:type filename: str
:type config: bool
:return: The resulting merged OrderedDict tree
:rtype: OrderedDict
The config file should stick to the following setup:
.. code-block:: ini
[firstlevel]
attribute1 = xyz
attribute2 = foo
attribute3 = bar
[[secondlevel]]
attribute1 = abc
attribute2 = bar
attribute3 = foo
[[[thirdlevel]]]
attribute1 = def
attribute2 = barfoo
attribute3 = foobar
[[anothersecondlevel]]
attribute1 = and so on
where firstlevel, secondlevel, thirdlevel and anothersecondlevel are defined as items and attribute are their respective attribute - value pairs
Valid characters for the items are a-z and A-Z plus any digit and underscore as second or further characters.
Valid characters for the attributes are the same as for an item plus @ and *
"""
valid_set = set(valid_attr_chars)
if config is None:
config = collections.OrderedDict()
item = config
with open(filename, 'r', encoding='UTF-8') as f:
linenu = 0
parent = collections.OrderedDict()
lines = iter(f.readlines())
for raw in lines:
linenu += 1
line = raw.lstrip('\ufeff') # remove BOM
while line.rstrip().endswith('\\'):
linenu += 1
line = line.rstrip().rstrip('\\') + next(lines, '').lstrip()
line = line.partition('#')[0].strip()
if line == '':
continue
if line[0] == '[': # item
brackets = 0
level = 0
closing = False
for index in range(len(line)):
if line[index] == '[' and not closing:
brackets += 1
level += 1
elif line[index] == ']':
closing = True
brackets -= 1
else:
closing = True
if line[index] not in valid_item_chars + "'":
logger.error(f"Problem parsing '{filename}' invalid character in line {linenu}: {line}. Valid characters are: {valid_item_chars}")
return config
if brackets != 0:
logger.error(f"Problem parsing '{filename}' unbalanced brackets in line {linenu}: {line}")
return config
name = line.strip("[]")
name = strip_quotes(name)
if len(name) == 0:
logger.error(f"Problem parsing '{filename}' tried to use an empty item name in line {linenu}: {line}")
return config
elif name[0] in digits:
logger.error(f"Problem parsing '{filename}': item starts with digit '{name[0]}' in line {linenu}: {line}")
return config
elif name in reserved:
logger.error(f"Problem parsing '{filename}': item using reserved word set/get in line {linenu}: {line}")
return config
elif keyword.iskeyword(name):
logger.error(f"Problem parsing '{filename}': item using reserved Python keyword {name} in line {linenu}: {line}")
return config
if level == 1:
if name not in config:
config[name] = collections.OrderedDict()
item = config[name]
parents = collections.OrderedDict()
parents[level] = item
else:
if level - 1 not in parents:
logger.error(f"Problem parsing '{filename}' no parent item defined for item in line {linenu}: {line}")
return config
parent = parents[level - 1]
if name not in parent:
parent[name] = collections.OrderedDict()
item = parent[name]
parents[level] = item
else: # attribute
attr, __, value = line.partition('=')
if not value:
continue
attr = attr.strip()
if not set(attr).issubset(valid_set):
logger.error(f"Problem parsing '{filename}' invalid character in line {linenu}: {attr}. Valid characters are: {valid_attr_chars}")
continue
if len(attr) > 0:
if attr[0] in digits:
logger.error(f"Problem parsing '{filename}' attrib starts with a digit '{attr[0]}' in line {linenu}: {attr }.")
continue
if '|' in value:
item[attr] = [strip_quotes(x) for x in value.split('|')]
else:
item[attr] = strip_quotes(value)
return config