from .iniparse import read_config
from .utilities import mk_lst, get_relative_to_cfg
from .entries import ConfigEntry, RecipeSection
from . import __recipe_keywords__
from collections import OrderedDict
from os.path import abspath
from os.path import join as pjoin
import copy
import importlib
import copy
DEBUG = False
FULL_DEBUG = False
[docs]class UserConfig():
"""
Class meant for managing the the users config, here we operate on the
config repeatedly making it available through the attribute self.cfg
Attributes:
raw_cfg: Untouched original OrderedDict that inicheck read from file
cfg: OrderedDict of the config file that inicheck will check, cast,
list, etc
recipes: List of entries.recipes.RecipesSection that apply to this
config
sections: List of strings that represent the unique sections for the
whole config file
items: List of strings that represent the unique items for the whole
config file
values: List of strings that represent the unique values for the whole
config file
mcfg: config.MasterConfig object that represents the standard the cfg
is checked against
"""
def __init__(self, filename, mcfg=None):
"""
Args:
filename: String to path containing config in .ini format
mcfg: Object of the master config
changelog: Object of changes.ChangeLog representing config file
entry changes by developers
"""
self.filename = filename
self.recipes = []
self.raw_cfg = OrderedDict()
# Hang on to the original
if self.filename != None:
self.raw_cfg = read_config(filename)
# The version of the config that inicheck will mess with
self.cfg = copy.deepcopy(self.raw_cfg)
self.sections, self.items, self.values = \
self.get_unique_entries(self.cfg)
if mcfg != None:
self.mcfg = mcfg
[docs] def apply_recipes(self):
"""
Look through the users config file and section by section add in
missing parameters to add defaults
Returns:
user_cfg: User config dictionary with defaults added.
"""
# Add this in case the user has added anything to the config obj
self.cfg = copy.deepcopy(self.raw_cfg)
# Start fresh with recipes to avoid over populating the recipes list
self.recipes = []
for r in self.mcfg.recipes:
for trigger, recipe_entry in r.triggers.items():
# Full conditions met to handle mulitple triggers
conditions_met = 0
triggered = False
# All conditions must be met if to be applied
for condition in recipe_entry.conditions:
conditions_triggered = []
for section in self.cfg.keys():
# Is it a valid section
if section in self.mcfg.cfg.keys():
# Watch out for empty sections
if len(self.cfg[section].keys()) == 0:
items = [None]
else:
items = self.cfg[section].keys()
for item in items:
# Confirm its a registered item
if item in self.mcfg.cfg[section].keys():
vals = mk_lst(self.cfg[section][item])
# Watch for empties
elif item == None:
vals = [None]
else:
vals = []
for v in vals:
# Sections
if (condition[0] == 'any' or
condition[0] == section):
if FULL_DEBUG:
print("Section Gate {0} == {1}"
"".format(condition[0],
section))
# Items
if (condition[1] == 'any' or
condition[1] == item):
if FULL_DEBUG:
print("\t\tItem Gate {0} == {1}"
"".format(condition[1],
item))
# Values
if (condition[2] == 'any' or
condition[2] == v):
if FULL_DEBUG:
print("\t\t\t\tValue Gate {0}"
" == {1}"
"".format(condition[2],
v))
# No conditions == [any any any]
conditions_triggered.append(
(section, item, v))
triggered = True
# Determine if the condition was met.
if triggered:
conditions_met += 1
triggered = False
if (conditions_met == len(recipe_entry.conditions) and
len(recipe_entry.conditions) != 0):
conditions_met = 0
self.recipes.append(r)
# Check recipes for sections not in the master
invalid_r_sections = [s for s in r.adj_config.keys()
if (s not in self.mcfg.cfg.keys()
and s != 'any')]
if len(invalid_r_sections) > 0:
raise ValueError("The recipe {} attempts to modify"
" section(s) {} not recognized by Master Config."
"".format(r.name,",".join(invalid_r_sections)))
if DEBUG:
print("\nDEBUG: Trigger: {0} {1} was met!"
"".format(trigger, condition))
# Iterate through the conditions found and apply
for situation in conditions_triggered:
# Insert the recipe into the users config
self.cfg = self.interpret_recipes(r.adj_config,
situation)
else:
if DEBUG:
print("\nDEBUG: Trigger: {0} not met. gates = {1}"
" and gates_passed = {2}"
"".format(trigger, condition,
conditions_met))
print('\n\n')
[docs] def interpret_recipes(self, partial_cfg, situation):
"""
User inserts a partial config by using each situation that
triggered a recipe. A triggering situation consists of a tuple of
(section, item, value) that represent the specific settings that
trigger the recipe. All default references should avoid overwriting the
users selection.
Args:
partial_cfg: dictionary of edits to be applied to the cfg
situation: List of len=3 describing the trigger mechanism
Return:
result: Modified dictionary
"""
result = copy.deepcopy(self.cfg)
for section in partial_cfg.keys():
for item in partial_cfg[section].keys():
remove = False
value = partial_cfg[section][item]
values = mk_lst(value)
for value in values:
# Defaults Keyword
if item == 'apply_defaults':
if str(value).lower() == 'true':
result = self.add_defaults(result,
sections=section)
# Keyword removal
elif item == 'remove_section':
if value.lower() == 'true':
if section in result.keys():
if DEBUG:
print("removed section: {0}"
"".format(section))
del result[section]
# Normal operation
else:
# Handle the any keyword for sections
if section == 'any':
s = situation[0]
else:
s = section
# Handle the any keyword for items
if item == 'any':
i = situation[1]
# Enable removing and defaulting
elif item in ['remove_item','default_item']:
i = value
if item == "default_item":
value = "default"
#value = "default"
# Nothing special asusme its a valid item name
else:
i = item
# Replace ANYs with values from the situation
if value == 'any':
v = situation[2]
elif value == 'default':
# Confirm existence in master
if i in self.mcfg.cfg[s].keys():
# Prefer user selection over default
if i not in self.cfg[s]:
v = self.mcfg.cfg[s][i].default
else:
raise Exception('{0} is not a valid item for '
'the master config section {1}, check your '
'recipes for a situation triggering on {2}, '
'{3}, {4}'.format(i, s, situation[0],
situation[1],
situation[2]))
# default items were sepcified.
else:
v = value
# Delete items
if item == 'remove_item':
if s in result.keys():
if i in result[s].keys():
if DEBUG:
print("Removed: {0} {1}".format(s, i))
del result[s][i]
elif s in result.keys():
# Check for empty dictionaries
if not bool(result[s]):
if DEBUG:
print("Adding section {0}".format(s))
result[s] = OrderedDict()
# Dictionary exists
else:
# Handle a item not provided by automatically
# adding it
if i not in result[s].keys():
if DEBUG:
print("Adding {0} {1} {2}"
"".format(s, i, v))
result[s][i] = v
# If the item was provided we don't want to
# overide the user with defaults
elif value != 'default':
if DEBUG:
print("Changing {0} {1} {2}"
"".format(s, i, v))
result[s][i] = v
return result
[docs] def get_unique_entries(self, cfg):
"""
Appends all the values in the user config to respectives lists of
section names, item names, and values. Afterwards any copy is
removed so all is left is a unique list of names and values
Args:
cfg: OrderedDict of the config file
Returns:
tuple: tuple of len=3 of sets of unique sections items and values
"""
unique_sections = []
unique_items = []
unique_values = []
for section in cfg.keys():
for item, value in cfg[section].items():
if type(value) != list:
vals = [value]
else:
vals = value
for v in vals:
unique_sections.append(section)
unique_items.append(item)
unique_values.append(v)
return set(unique_sections), set(unique_items), set(unique_values)
[docs] def add_defaults(self, cfg, sections=None,items=None):
"""
Look through the users config file and section by section add in
missing parameters to add defaults
Args:
sections: Single section name or a list of sections to apply
(optional) otherwise uses all sections in users
config
Returns:
user_cfg: User config dictionary with defaults added.
"""
master = self.mcfg.cfg
result = copy.deepcopy(cfg)
#Either go through specified sections or all sections provided by user.
if sections == None:
sections = result.keys()
else:
#Accounts for single items not entered as a list
sections = mk_lst(sections)
for section in sections:
configured = result[section]
for k, v in master[section].items():
if v.name not in configured.keys():
result[section][k] = v.default
return result
[docs] def update_config_paths(self, user_cfg_path=None):
"""
Sets all paths so that they are always relative to the config
file or absolute.
"""
if user_cfg_path == None:
user_cfg_path = self.filename
mcfg = self.mcfg.cfg
cfg = self.cfg
# Cycle thru users config
for section in cfg.keys():
for item in cfg[section].keys():
d = cfg[section][item]
# Does master have this and is it not none
if item in mcfg[section].keys() and d != None:
m = mcfg[section][item]
# Any paths
if m.type == 'filename' or m.type == 'directory':
cfg[section][item] = \
get_relative_to_cfg(cfg[section][item],
self.filename)
return cfg
[docs]class MasterConfig():
def __init__(self, path=None, modules=None, checkers=None, titles=None,
header=None, changelogs=None):
self.paths = []
self.recipes = []
self.titles = {}
self.header = header
self.checker_modules = []
self.changelogs = []
# Paths were manually provided
if path != None and modules == None:
for p in mk_lst(path):
self.paths.append(abspath(p))
# If a module was passed
if modules != None and self.paths == []:
for m in mk_lst(modules):
i = importlib.import_module(m)
self.paths.append(abspath(pjoin(i.__file__,
i.__core_config__)))
# Search for possible recipes provided in the module
if hasattr(i, '__recipes__'):
self.paths.append(i.__recipes__)
# Search for possible section titles provided in the module
if hasattr(i, '__config_titles__'):
self.titles.update(getattr(i, '__config_titles__'))
# Search for config headers provided in the module
if hasattr(i, '__config_header__'):
self.header = getattr(i, '__config_header__')
# Search for custom checkers
if hasattr(i, '__config_checkers__'):
self.checker_modules.append(m + '.' +
getattr(i,
'__config_checkers__'))
# Grab ayny change logs
if hasattr(i,"__config_changelog__"):
self.changelogs.append(abspath(pjoin(i.__file__,
i.__config_changelog__)))
# Add any extra ones provided
if checkers != None:
for c in mk_lst(checkers):
self.checker_modules.append(c)
if titles != None:
self.titles.update(titles)
if header != None:
self.header = header
if len(self.paths) == 0 and modules == None:
raise ValueError("No file was either provided or found when"
" initiating a master config file.")
self.cfg = self.add_files(self.paths)
[docs] def add_files(self, paths):
"""
Designed to add to the master config file if the user has split
up files to reduce the amount of info in a master file. e.g.
recipes are stored in another file.
Args:
paths: list of real path to another cfg.ini
Returns:
config: Original config with appended information found in the
extra cfg
"""
result = OrderedDict()
for f in paths:
if f != None:
extra_cfg = self._read(f)
result.update(extra_cfg)
return result
[docs] def merge(self, mcfg):
"""
Merges the a master config object into the current master config object
in place
"""
self.recipes+=mcfg.recipes
self.checker_modules += mcfg.checker_modules
self.titles.update(mcfg.titles)
self.cfg.update(mcfg.cfg)
def _read(self, master_config_file):
"""
Reads in the core config file which has special syntax for
specifying options
Args:
master_config_file: String path to the master config file.
Returns:
config: Dictionary of dictionaries representing the defaults
and available options. Based on the Core Config file.
"""
cfg = OrderedDict()
# Read in will automatically get the configurable key added
raw_config = read_config(master_config_file)
for section in raw_config.keys():
sec = OrderedDict()
# Look for keywords in section name e.g. recipe
for word in __recipe_keywords__:
if word in section:
self.recipes.append(RecipeSection(raw_config[section],
name=section))
break
# Look for master properties
else:
for item in raw_config[section].keys():
sec[item] = ConfigEntry(name=item,
parseable_line=raw_config[section][item])
cfg[section] = sec
return cfg
[docs]def check_types(cfg, checkers):
"""
Iterates through all the master config items and confirm all type are valid
Args:
cfg: dict of master config entries listing out properties
checkers: dict of checker names and class as Key Value pairs
Returns:
Bool: True if no error is raised
Raises:
ValueError: raises error if a type is specified not in the lists of
checkers
"""
for s in cfg.keys():
for i in cfg[s].keys():
type_value = cfg[s][i].type
# Is the specified type recognized?
if type_value not in checkers.keys():
raise ValueError("\n\nIn master config, SECTION: {0} at ITEM:"
" {1} attempts to use undefined type name "
" '{2}' which has no checker associated."
"\nAvailable checkers are:\n\n{3}"
"".format(s, i, type_value, checkers.keys()))
return True