diff --git a/dotdrop/cfg_aggregator.py b/dotdrop/cfg_aggregator.py index 958af8a..2be9297 100644 --- a/dotdrop/cfg_aggregator.py +++ b/dotdrop/cfg_aggregator.py @@ -78,7 +78,7 @@ class CfgAggregator: self._debug_list('trans_w', self.trans_w) # variables - self.variables = self.cfgyaml.get_variables() + self.variables = self.cfgyaml.variables if self.debug: self._debug_dict('variables', self.variables) diff --git a/dotdrop/cfg_yaml.py b/dotdrop/cfg_yaml.py index 5dfa50a..c2bf973 100644 --- a/dotdrop/cfg_yaml.py +++ b/dotdrop/cfg_yaml.py @@ -3,6 +3,19 @@ author: deadc0de6 (https://github.com/deadc0de6) Copyright (c) 2019, deadc0de6 handle lower level of the config file + +will provide the following dictionaries to +the upper layer: + +* self.settings +* self.dotfiles +* self.profiles +* self.actions +* self.trans_r +* self.trans_w +* self.variables + +Additionally a few methods are exported. """ import os @@ -19,7 +32,7 @@ from dotdrop.logger import Logger from dotdrop.templategen import Templategen from dotdrop.linktypes import LinkTypes from dotdrop.utils import shell, uniq_list -from dotdrop.exceptions import YamlException +from dotdrop.exceptions import YamlException, UndefinedException class CfgYaml: @@ -76,6 +89,44 @@ class CfgYaml: lnk_link = LinkTypes.LINK.name.lower() lnk_children = LinkTypes.LINK_CHILDREN.name.lower() + # TODO + # + # "include" entries: + # - import_actions + # - import_configs + # - import_variables + # - profile's import + # - profile's include + # + # variable precedence: + # 1) profile variable + # 2) "import_variables" variables + # 3) "import_configs" variables + # 4) other variables + # + # parse a config file + # - parse settings + # - parse variables + # - interprete dynvariables + # - template the include entries + # - parse and integrate included elements (see below) + # - parse profiles + # - parse dotfiles + # + # parse "include" entry + # - same as parse config file + # - add new entry to the top dict + # + # TODO + # - properly handle the included profile + # - document precedence in wiki + # - document parsing in CONTRIBUTING.md + # - document dvars are executed in their own config file + # - remove unused functions/methods + # - coverage + # - remove ori_* + # + def __init__(self, path, profile=None, debug=False): """ config parser @@ -83,92 +134,302 @@ class CfgYaml: @profile: the selected profile @debug: debug flag """ - self.path = os.path.abspath(path) - self.profile = profile - self.debug = debug - self.log = Logger() + self._path = os.path.abspath(path) + self._profile = profile + self._debug = debug + self._log = Logger() # config needs to be written - self.dirty = False + self._dirty = False # indicates the config has been updated - self.dirty_deprecated = False + self._dirty_deprecated = False - if not os.path.exists(path): + # init the dictionaries + self.settings = {} + self.dotfiles = {} + self.profiles = {} + self.actions = {} + self.trans_r = {} + self.trans_w = {} + self.variables = {} + + if not os.path.exists(self._path): err = 'invalid config path: \"{}\"'.format(path) - if self.debug: - self.log.dbg(err) + if self._debug: + self._log.dbg(err) raise YamlException(err) - self.yaml_dict = self._load_yaml(self.path) - # live patch deprecated entries - self._fix_deprecated(self.yaml_dict) - # parse to self variables - self._parse_main_yaml(self.yaml_dict) - if self.debug: - self.log.dbg('BEFORE normalization: {}'.format(self.yaml_dict)) + self._yaml_dict = self._load_yaml(self._path) - # resolve variables - self.variables, self.prokeys = self._merge_variables() + # parse the "config" block + self.settings = self._parse_blk_settings(self._yaml_dict) - # apply variables - self._apply_variables() + # parse the "variables" block + self.variables = self._parse_blk_variables(self._yaml_dict) + self._redefine_templater() + + # parse the "dynvariables" block + dvariables = self._parse_blk_dynvariables(self._yaml_dict) + self._add_variables(dvariables) + + # interprete dynvariables + dvariables = self._template_dict(dvariables) + dvariables = self._shell_exec_dvars(dvariables) + + # merge variables and dynvariables + self.variables = self._merge_dict(dvariables, self.variables) + self._redefine_templater() + + # TODO template variables + self.variables = self._template_dict(self.variables) + if self._debug: + self._debug_dict('variables', self.variables) + + # template the "include" entries + self._template_include_entry() # process imported variables (import_variables) - self._import_variables() - # process imported actions (import_actions) + newvars = self._import_variables() + self._add_variables(newvars) + + # TODO process imported actions (import_actions) self._import_actions() - # process imported profile dotfiles (import) + # TODO process imported profile dotfiles (import) self._import_profiles_dotfiles() - # process imported configs (import_configs) + # TODO process imported configs (import_configs) self._import_configs() # process profile include self._resolve_profile_includes() + + # ===== + # TODO below + # ==== + if self._debug: + self._log.dbg('BEFORE normalization: {}'.format(self._yaml_dict)) + + # resolve variables + # TODO + # self.variables, self.prokeys = self._merge_variables() + + # apply variables + # self._apply_variables() + # process profile ALL self._resolve_profile_all() # patch dotfiles paths self._resolve_dotfile_paths() - if self.debug: - self.log.dbg('AFTER normalization: {}'.format(self.yaml_dict)) + # TODO ensure no element is left un-templated at the end - def get_variables(self): - """retrieve all variables""" - return self.variables + if self._debug: + self._log.dbg('AFTER normalization: {}'.format(self._yaml_dict)) + + def _add_variables(self, ext_variables): + """add new variables from external file""" + # TODO move me + # merge + self.variables = self._merge_dict(self.variables, ext_variables) + # ensure enriched variables are relative to this config + self.variables = self._enrich_vars(self.variables, self._profile) + # re-create the templater + self._redefine_templater() + # rec resolve variables with new ones + self.variables = self._template_dict(self.variables) + + def _redefine_templater(self): + """create templater based on current variables""" + fufile = self.settings[Settings.key_func_file] + fifile = self.settings[Settings.key_filter_file] + self._tmpl = Templategen(variables=self.variables, + func_file=fufile, + filter_file=fifile) ######################################################## - # parsing + # outside available methods ######################################################## - def _parse_main_yaml(self, dic): - """parse the different blocks""" - self.ori_settings = self._get_entry(dic, self.key_settings) - self.settings = Settings(None).serialize().get(self.key_settings) - self.settings.update(self.ori_settings) + def resolve_dotfile_src(self, src, templater=None): + """resolve dotfile src path""" + newsrc = '' + if src: + new = src + if templater: + new = templater.generate_string(src) + if new != src and self._debug: + msg = 'dotfile src: \"{}\" -> \"{}\"'.format(src, new) + self._log.dbg(msg) + src = new + src = os.path.join(self.settings[self.key_settings_dotpath], + src) + newsrc = self._norm_path(src) + return newsrc + + def resolve_dotfile_dst(self, dst, templater=None): + """resolve dotfile dst path""" + newdst = '' + if dst: + new = dst + if templater: + new = templater.generate_string(dst) + if new != dst and self._debug: + msg = 'dotfile dst: \"{}\" -> \"{}\"'.format(dst, new) + self._log.dbg(msg) + dst = new + newdst = self._norm_path(dst) + return newdst + + def add_dotfile_to_profile(self, dotfile_key, profile_key): + """add an existing dotfile key to a profile_key""" + self._new_profile(profile_key) + profile = self._yaml_dict[self.key_profiles][profile_key] + if self.key_profile_dotfiles not in profile or \ + profile[self.key_profile_dotfiles] is None: + profile[self.key_profile_dotfiles] = [] + pdfs = profile[self.key_profile_dotfiles] + if self.key_all not in pdfs and \ + dotfile_key not in pdfs: + profile[self.key_profile_dotfiles].append(dotfile_key) + if self._debug: + msg = 'add \"{}\" to profile \"{}\"'.format(dotfile_key, + profile_key) + msg.format(dotfile_key, profile_key) + self._log.dbg(msg) + self._dirty = True + return self._dirty + + def get_all_dotfile_keys(self): + """return all existing dotfile keys""" + return self.dotfiles.keys() + + def add_dotfile(self, key, src, dst, link): + """add a new dotfile""" + if key in self.dotfiles.keys(): + return False + if self._debug: + self._log.dbg('adding new dotfile: {}'.format(key)) + + df_dict = { + self.key_dotfile_src: src, + self.key_dotfile_dst: dst, + } + dfl = self.settings[self.key_settings_link_dotfile_default] + if str(link) != dfl: + df_dict[self.key_dotfile_link] = str(link) + self._yaml_dict[self.key_dotfiles][key] = df_dict + self._dirty = True + + def del_dotfile(self, key): + """remove this dotfile from config""" + if key not in self._yaml_dict[self.key_dotfiles]: + self._log.err('key not in dotfiles: {}'.format(key)) + return False + if self._debug: + self._log.dbg('remove dotfile: {}'.format(key)) + del self._yaml_dict[self.key_dotfiles][key] + if self._debug: + dfs = self._yaml_dict[self.key_dotfiles] + self._log.dbg('new dotfiles: {}'.format(dfs)) + self._dirty = True + return True + + def del_dotfile_from_profile(self, df_key, pro_key): + """remove this dotfile from that profile""" + if df_key not in self.dotfiles.keys(): + self._log.err('key not in dotfiles: {}'.format(df_key)) + return False + if pro_key not in self.profiles.keys(): + self._log.err('key not in profile: {}'.format(pro_key)) + return False + # get the profile dictionary + profile = self._yaml_dict[self.key_profiles][pro_key] + if df_key not in profile[self.key_profile_dotfiles]: + return True + if self._debug: + dfs = profile[self.key_profile_dotfiles] + self._log.dbg('{} profile dotfiles: {}'.format(pro_key, dfs)) + self._log.dbg('remove {} from profile {}'.format(df_key, pro_key)) + profile[self.key_profile_dotfiles].remove(df_key) + if self._debug: + dfs = profile[self.key_profile_dotfiles] + self._log.dbg('{} profile dotfiles: {}'.format(pro_key, dfs)) + self._dirty = True + return True + + def save(self): + """save this instance and return True if saved""" + if not self._dirty: + return False + + content = self._prepare_to_save(self._yaml_dict) + + if self._dirty_deprecated: + # add minversion + settings = content[self.key_settings] + settings[self.key_settings_minversion] = VERSION + + # save to file + if self._debug: + self._log.dbg('saving to {}'.format(self._path)) + try: + with open(self._path, 'w') as f: + self._yaml_dump(content, f) + except Exception as e: + self._log.err(e) + raise YamlException('error saving config: {}'.format(self._path)) + + if self._dirty_deprecated: + warn = 'your config contained deprecated entries' + warn += ' and was updated' + self._log.warn(warn) + + self._dirty = False + self.cfg_updated = False + return True + + def dump(self): + """dump the config dictionary""" + output = io.StringIO() + content = self._prepare_to_save(self._yaml_dict.copy()) + self._yaml_dump(content, output) + return output.getvalue() + + ######################################################## + # block parsing + ######################################################## + + def _parse_blk_settings(self, dic): + """parse the "config" block""" + block = self._get_entry(dic, self.key_settings) + # set defaults + settings = Settings(None).serialize().get(self.key_settings) + settings.update(block) # resolve minimum version - if self.key_settings_minversion in self.settings: - minversion = self.settings[self.key_settings_minversion] + if self.key_settings_minversion in settings: + minversion = settings[self.key_settings_minversion] self._check_minversion(minversion) - # resolve settings paths - p = self._norm_path(self.settings[self.key_settings_dotpath]) - self.settings[self.key_settings_dotpath] = p - p = self._norm_path(self.settings[self.key_settings_workdir]) - self.settings[self.key_settings_workdir] = p + # normalize paths + p = self._norm_path(settings[self.key_settings_dotpath]) + settings[self.key_settings_dotpath] = p + p = self._norm_path(settings[self.key_settings_workdir]) + settings[self.key_settings_workdir] = p p = [ self._norm_path(p) - for p in self.settings[Settings.key_filter_file] + for p in settings[Settings.key_filter_file] ] - self.settings[Settings.key_filter_file] = p + settings[Settings.key_filter_file] = p p = [ self._norm_path(p) - for p in self.settings[Settings.key_func_file] + for p in settings[Settings.key_func_file] ] - self.settings[Settings.key_func_file] = p - if self.debug: - self._debug_dict('settings', self.settings) + settings[Settings.key_func_file] = p + if self._debug: + self._debug_dict('settings', settings) + return settings - # dotfiles + def _parse_blk_dotfiles(self, dic): + """parse the "dotfiles" block""" self.ori_dotfiles = self._get_entry(dic, self.key_dotfiles) self.dotfiles = deepcopy(self.ori_dotfiles) keys = self.dotfiles.keys() @@ -177,58 +438,72 @@ class CfgYaml: err = 'duplicate dotfile keys found: {}'.format(dups) raise YamlException(err) self.dotfiles = self._norm_dotfiles(self.dotfiles) - if self.debug: + if self._debug: self._debug_dict('dotfiles', self.dotfiles) - # profiles + def _parse_blk_profiles(self, dic): + """parse the "profiles" block""" self.ori_profiles = self._get_entry(dic, self.key_profiles) self.profiles = deepcopy(self.ori_profiles) self.profiles = self._norm_profiles(self.profiles) - if self.debug: + if self._debug: self._debug_dict('profiles', self.profiles) - # actions + def _parse_blk_actions(self, dic): + """parse the "actions" block""" self.ori_actions = self._get_entry(dic, self.key_actions, mandatory=False) self.actions = deepcopy(self.ori_actions) self.actions = self._norm_actions(self.actions) - if self.debug: + if self._debug: self._debug_dict('actions', self.actions) - # trans_r + def _parse_blk_trans_r(self, dic): + """parse the "trans_r" block""" key = self.key_trans_r if self.old_key_trans_r in dic: - self.log.warn('\"trans\" is deprecated, please use \"trans_read\"') + msg = '\"trans\" is deprecated, please use \"trans_read\"' + self._log.warn(msg) dic[self.key_trans_r] = dic[self.old_key_trans_r] del dic[self.old_key_trans_r] self.ori_trans_r = self._get_entry(dic, key, mandatory=False) self.trans_r = deepcopy(self.ori_trans_r) - if self.debug: + if self._debug: self._debug_dict('trans_r', self.trans_r) - # trans_w + def _parse_blk_trans_w(self, dic): + """parse the "trans_w" block""" self.ori_trans_w = self._get_entry(dic, self.key_trans_w, mandatory=False) self.trans_w = deepcopy(self.ori_trans_w) - if self.debug: + if self._debug: self._debug_dict('trans_w', self.trans_w) - # variables - self.ori_variables = self._get_entry(dic, - self.key_variables, - mandatory=False) - if self.debug: - self._debug_dict('variables', self.ori_variables) + def _parse_blk_variables(self, dic): + """parse the "variables" block""" + variables = self._get_entry(dic, + self.key_variables, + mandatory=False) + if self._debug: + self._debug_dict('variables', variables) + return variables - # dynvariables - self.ori_dvariables = self._get_entry(dic, - self.key_dvariables, - mandatory=False) - if self.debug: - self._debug_dict('dynvariables', self.ori_dvariables) + def _parse_blk_dynvariables(self, dic): + """parse the "dynvariables" block""" + dvariables = self._get_entry(dic, + self.key_dvariables, + mandatory=False) + if self._debug: + self._debug_dict('dynvariables', dvariables) + return dvariables + + ######################################################## + # parsing helpers + ######################################################## def _resolve_dotfile_paths(self): """resolve dotfiles paths""" + # TODO remove t = Templategen(variables=self.variables, func_file=self.settings[Settings.key_func_file], filter_file=self.settings[Settings.key_filter_file]) @@ -243,39 +518,10 @@ class CfgYaml: newdst = self.resolve_dotfile_dst(dst, templater=t) dotfile[self.key_dotfile_dst] = newdst - def resolve_dotfile_src(self, src, templater=None): - """resolve dotfile src path""" - newsrc = '' - if src: - new = src - if templater: - new = templater.generate_string(src) - if new != src and self.debug: - msg = 'dotfile src: \"{}\" -> \"{}\"'.format(src, new) - self.log.dbg(msg) - src = new - src = os.path.join(self.settings[self.key_settings_dotpath], - src) - newsrc = self._norm_path(src) - return newsrc - - def resolve_dotfile_dst(self, dst, templater=None): - """resolve dotfile dst path""" - newdst = '' - if dst: - new = dst - if templater: - new = templater.generate_string(dst) - if new != dst and self.debug: - msg = 'dotfile dst: \"{}\" -> \"{}\"'.format(dst, new) - self.log.dbg(msg) - dst = new - newdst = self._norm_path(dst) - return newdst - def _rec_resolve_vars(self, variables): """recursive resolve variables""" - default = self._get_variables_dict(self.profile) + # TODO remove this and any call + default = self._get_variables_dict(self._profile) t = Templategen(variables=self._merge_dict(default, variables), func_file=self.settings[Settings.key_func_file], filter_file=self.settings[Settings.key_filter_file]) @@ -289,6 +535,7 @@ class CfgYaml: def _get_profile_included_vars(self, tvars): """resolve profile included variables/dynvariables""" + # TODO remove t = Templategen(variables=tvars, func_file=self.settings[Settings.key_func_file], filter_file=self.settings[Settings.key_filter_file]) @@ -301,12 +548,12 @@ class CfgYaml: v[self.key_profile_include] = new # now get the included ones - pro_var = self._get_profile_included_item(self.profile, + pro_var = self._get_profile_included_item(self._profile, self.key_profile_variables, - seen=[self.profile]) - pro_dvar = self._get_profile_included_item(self.profile, + seen=[self._profile]) + pro_dvar = self._get_profile_included_item(self._profile, self.key_profile_dvariables, - seen=[self.profile]) + seen=[self._profile]) # exec incl dynvariables self._shell_exec_dvars(pro_dvar.keys(), pro_dvar) @@ -318,11 +565,11 @@ class CfgYaml: apply them to any needed entries and return the full list of variables """ - if self.debug: - self.log.dbg('get local variables') + if self._debug: + self._log.dbg('get local variables') # get all variables from local and resolve - var = self._get_variables_dict(self.profile) + var = self._get_variables_dict(self._profile) # get all dynvariables from local and resolve dvar = self._get_dvariables_dict() @@ -330,13 +577,13 @@ class CfgYaml: # temporarly resolve all variables for "include" merged = self._merge_dict(dvar, var) merged = self._rec_resolve_vars(merged) - if self.debug: + if self._debug: self._debug_dict('variables', merged) # exec dynvariables self._shell_exec_dvars(dvar.keys(), merged) - if self.debug: - self.log.dbg('local variables resolved') + if self._debug: + self._log.dbg('local variables resolved') self._debug_dict('variables', merged) # resolve profile included variables/dynvariables @@ -347,42 +594,38 @@ class CfgYaml: merged = self._merge_dict(pro_dvar, merged) merged = self._rec_resolve_vars(merged) - if self.debug: - self.log.dbg('resolve all uses of variables in config') + if self._debug: + self._log.dbg('resolve all uses of variables in config') self._debug_dict('variables', merged) prokeys = list(pro_var.keys()) + list(pro_dvar.keys()) return merged, prokeys - def _apply_variables(self): - """template any needed parts of the config""" - t = Templategen(variables=self.variables, - func_file=self.settings[Settings.key_func_file], - filter_file=self.settings[Settings.key_filter_file]) - + def _template_include_entry(self): + """template all "include" entries""" # import_actions new = [] entries = self.settings.get(self.key_import_actions, []) - new = self._template_list(t, entries) + new = self._template_list(entries) if new: self.settings[self.key_import_actions] = new # import_configs entries = self.settings.get(self.key_import_configs, []) - new = self._template_list(t, entries) + new = self._template_list(entries) if new: self.settings[self.key_import_configs] = new # import_variables entries = self.settings.get(self.key_import_variables, []) - new = self._template_list(t, entries) + new = self._template_list(entries) if new: self.settings[self.key_import_variables] = new # profile's import for k, v in self.profiles.items(): entries = v.get(self.key_import_profile_dfs, []) - new = self._template_list(t, entries) + new = self._template_list(entries) if new: v[self.key_import_profile_dfs] = new @@ -432,7 +675,7 @@ class CfgYaml: # fix deprecated trans key if self.old_key_trans_r in v: msg = '\"trans\" is deprecated, please use \"trans_read\"' - self.log.warn(msg) + self._log.warn(msg) v[self.key_trans_r] = v[self.old_key_trans_r] del v[self.old_key_trans_r] new[k] = v @@ -446,9 +689,8 @@ class CfgYaml: v[self.key_dotfile_noempty] = val return new - def _get_variables_dict(self, profile): + def _enrich_vars(self, variables, profile): """return enriched variables""" - variables = deepcopy(self.ori_variables) # add profile variable if profile: variables['profile'] = profile @@ -456,7 +698,7 @@ class CfgYaml: p = self.settings.get(self.key_settings_dotpath) p = self._norm_path(p) variables['_dotdrop_dotpath'] = p - variables['_dotdrop_cfgpath'] = self._norm_path(self.path) + variables['_dotdrop_cfgpath'] = self._norm_path(self._path) p = self.settings.get(self.key_settings_workdir) p = self._norm_path(p) variables['_dotdrop_workdir'] = p @@ -483,9 +725,9 @@ class CfgYaml: seen.append(inherited_profile) new = self._get_profile_included_item(inherited_profile, item, seen) - if self.debug: + if self._debug: msg = 'included {} from {}: {}' - self.log.dbg(msg.format(item, inherited_profile, new)) + self._log.dbg(msg.format(item, inherited_profile, new)) items.update(new) cur = pentry.get(item, {}) @@ -499,8 +741,8 @@ class CfgYaml: if not dfs: continue if self.key_all in dfs: - if self.debug: - self.log.dbg('add ALL to profile {}'.format(k)) + if self._debug: + self._log.dbg('add ALL to profile {}'.format(k)) v[self.key_profile_dotfiles] = self.dotfiles.keys() def _resolve_profile_includes(self): @@ -532,22 +774,22 @@ class CfgYaml: # nothing to include return dotfiles, actions, pvars, pdvars - if self.debug: - self.log.dbg('{} includes {}'.format(profile, ','.join(includes))) - self.log.dbg('{} dotfiles before include: {}'.format(profile, - dotfiles)) - self.log.dbg('{} actions before include: {}'.format(profile, - actions)) - self.log.dbg('{} variables before include: {}'.format(profile, - pvars)) - self.log.dbg('{} dynvariables before include: {}'.format(profile, - pdvars)) + if self._debug: + self._log.dbg('{} includes {}'.format(profile, ','.join(includes))) + self._log.dbg('{} dotfiles before include: {}'.format(profile, + dotfiles)) + self._log.dbg('{} actions before include: {}'.format(profile, + actions)) + self._log.dbg('{} variables before include: {}'.format(profile, + pvars)) + self._log.dbg('{} dynvariables before include: {}'.format(profile, + pdvars)) seen = [] for i in uniq_list(includes): - if self.debug: - self.log.dbg('resolving includes "{}" <- "{}"' - .format(profile, i)) + if self._debug: + self._log.dbg('resolving includes "{}" <- "{}"' + .format(profile, i)) # ensure no include loop occurs if i in seen: @@ -555,41 +797,41 @@ class CfgYaml: seen.append(i) # included profile even exists if i not in self.profiles.keys(): - self.log.warn('include unknown profile: {}'.format(i)) + self._log.warn('include unknown profile: {}'.format(i)) continue # recursive resolve - if self.debug: - self.log.dbg('recursively resolving includes for profile "{}"' - .format(i)) + if self._debug: + self._log.dbg('recursively resolving includes for profile "{}"' + .format(i)) o_dfs, o_actions, o_v, o_dv = self._rec_resolve_profile_include(i) # merge dotfile keys - if self.debug: - self.log.dbg('Merging dotfiles {} <- {}: {} <- {}' - .format(profile, i, dotfiles, o_dfs)) + if self._debug: + self._log.dbg('Merging dotfiles {} <- {}: {} <- {}' + .format(profile, i, dotfiles, o_dfs)) dotfiles.extend(o_dfs) this_profile[self.key_profile_dotfiles] = uniq_list(dotfiles) # merge actions keys - if self.debug: - self.log.dbg('Merging actions {} <- {}: {} <- {}' - .format(profile, i, actions, o_actions)) + if self._debug: + self._log.dbg('Merging actions {} <- {}: {} <- {}' + .format(profile, i, actions, o_actions)) actions.extend(o_actions) this_profile[self.key_profile_actions] = uniq_list(actions) # merge variables - if self.debug: - self.log.dbg('Merging variables {} <- {}: {} <- {}' - .format(profile, i, dict(pvars), dict(o_v))) + if self._debug: + self._log.dbg('Merging variables {} <- {}: {} <- {}' + .format(profile, i, dict(pvars), dict(o_v))) pvars = self._merge_dict(o_v, pvars) this_profile[self.key_profile_variables] = pvars # merge dynvariables - if self.debug: - self.log.dbg('Merging dynamic variables {} <- {}: {} <- {}' - .format(profile, i, dict(pdvars), - dict(o_dv))) + if self._debug: + self._log.dbg('Merging dynamic variables {} <- {}: {} <- {}' + .format(profile, i, dict(pdvars), + dict(o_dv))) pdvars = self._merge_dict(o_dv, pdvars) this_profile[self.key_profile_dvariables] = pdvars @@ -598,17 +840,17 @@ class CfgYaml: pvars = this_profile.get(self.key_profile_variables, {}) or {} pdvars = this_profile.get(self.key_profile_dvariables, {}) or {} - if self.debug: - self.log.dbg('{} dotfiles after include: {}'.format(profile, - dotfiles)) - self.log.dbg('{} actions after include: {}'.format(profile, - actions)) - self.log.dbg('{} variables after include: {}'.format(profile, - pvars)) - self.log.dbg('{} dynvariables after include: {}'.format(profile, - pdvars)) + if self._debug: + self._log.dbg('{} dotfiles after include: {}'.format(profile, + dotfiles)) + self._log.dbg('{} actions after include: {}'.format(profile, + actions)) + self._log.dbg('{} variables after include: {}'.format(profile, + pvars)) + self._log.dbg('{} dynvariables after include: {}'.format(profile, + pdvars)) - if profile == self.profile: + if profile == self._profile: # Only for the selected profile, we execute dynamic variables and # we merge variables/dynvariables into the global variables self._shell_exec_dvars(pdvars.keys(), pdvars) @@ -630,30 +872,254 @@ class CfgYaml: if not paths: return paths = self._resolve_paths(paths) + newvars = {} for path in paths: - if self.debug: - self.log.dbg('import variables from {}'.format(path)) + if self._debug: + self._log.dbg('import variables from {}'.format(path)) var = self._import_sub(path, self.key_variables, mandatory=False) - if self.debug: - self.log.dbg('import dynvariables from {}'.format(path)) + if self._debug: + self._log.dbg('import dynvariables from {}'.format(path)) dvar = self._import_sub(path, self.key_dvariables, mandatory=False) merged = self._merge_dict(dvar, var) - merged = self._rec_resolve_vars(merged) - # execute dvar - self._shell_exec_dvars(dvar.keys(), merged) - self._clear_profile_vars(merged) - self.variables = self._merge_dict(merged, self.variables) + merged = self._template_dict(merged) + newvars = self._merge_dict(newvars, merged) + # TODO needed? + # self._clear_profile_vars(merged) + return newvars + + def _import_actions(self): + """import external actions from paths""" + paths = self.settings.get(self.key_import_actions, None) + if not paths: + return + paths = self._resolve_paths(paths) + for path in paths: + if self._debug: + self._log.dbg('import actions from {}'.format(path)) + new = self._import_sub(path, self.key_actions, + mandatory=False, + patch_func=self._norm_actions) + self.actions = self._merge_dict(new, self.actions) + + def _import_profiles_dotfiles(self): + """import profile dotfiles""" + for k, v in self.profiles.items(): + imp = v.get(self.key_import_profile_dfs, None) + if not imp: + continue + if self._debug: + self._log.dbg('import dotfiles for profile {}'.format(k)) + paths = self._resolve_paths(imp) + for path in paths: + current = v.get(self.key_dotfiles, []) + new = self._import_sub(path, self.key_dotfiles, + mandatory=False) + v[self.key_dotfiles] = new + current + + def _import_config(self, path): + """import config from path""" + if self._debug: + self._log.dbg('import config from {}'.format(path)) + sub = CfgYaml(path, profile=self._profile, debug=self._debug) + + # settings are ignored from external file + # except for filter_file and func_file + self.settings[Settings.key_func_file] += [ + self._norm_path(func_file) + for func_file in sub.settings[Settings.key_func_file] + ] + self.settings[Settings.key_filter_file] += [ + self._norm_path(func_file) + for func_file in sub.settings[Settings.key_filter_file] + ] + + # merge top entries + self.dotfiles = self._merge_dict(self.dotfiles, sub.dotfiles) + self.profiles = self._merge_dict(self.profiles, sub.profiles) + self.actions = self._merge_dict(self.actions, sub.actions) + self.trans_r = self._merge_dict(self.trans_r, sub.trans_r) + self.trans_w = self._merge_dict(self.trans_w, sub.trans_w) + self._clear_profile_vars(sub.variables) + + if self._debug: + self._debug_dict('add import_configs var', sub.variables) + self.variables = self._merge_dict(sub.variables, self.variables) + + def _import_configs(self): + """import configs from external files""" + # settings -> import_configs + imp = self.settings.get(self.key_import_configs, None) + if not imp: + return + paths = self._resolve_paths(imp) + for path in paths: + self._import_config(path) + + def _import_sub(self, path, key, mandatory=False, patch_func=None): + """ + import the block "key" from "path" + patch_func is applied to each element if defined + """ + if self._debug: + self._log.dbg('import \"{}\" from \"{}\"'.format(key, path)) + extdict = self._load_yaml(path) + new = self._get_entry(extdict, key, mandatory=mandatory) + if patch_func: + if self._debug: + self._log.dbg('calling patch: {}'.format(patch_func)) + new = patch_func(new) + if not new and mandatory: + err = 'no \"{}\" imported from \"{}\"'.format(key, path) + self._log.warn(err) + raise YamlException(err) + if self._debug: + self._log.dbg('imported \"{}\": {}'.format(key, new)) + return new + + ######################################################## + # add/remove entries + ######################################################## + + def _new_profile(self, key): + """add a new profile if it doesn't exist""" + if key not in self.profiles.keys(): + # update yaml_dict + self._yaml_dict[self.key_profiles][key] = { + self.key_profile_dotfiles: [] + } + if self._debug: + self._log.dbg('adding new profile: {}'.format(key)) + self._dirty = True + + ######################################################## + # handle deprecated entries + ######################################################## + + def _fix_deprecated(self, yamldict): + """fix deprecated entries""" + self._fix_deprecated_link_by_default(yamldict) + self._fix_deprecated_dotfile_link(yamldict) + return yamldict + + def _fix_deprecated_link_by_default(self, yamldict): + """fix deprecated link_by_default""" + key = 'link_by_default' + newkey = self.key_imp_link + if self.key_settings not in yamldict: + return + if not yamldict[self.key_settings]: + return + config = yamldict[self.key_settings] + if key not in config: + return + if config[key]: + config[newkey] = self.lnk_link + else: + config[newkey] = self.lnk_nolink + del config[key] + self._log.warn('deprecated \"link_by_default\"') + self._dirty = True + self._dirty_deprecated = True + + def _fix_deprecated_dotfile_link(self, yamldict): + """fix deprecated link in dotfiles""" + if self.key_dotfiles not in yamldict: + return + if not yamldict[self.key_dotfiles]: + return + for k, dotfile in yamldict[self.key_dotfiles].items(): + new = self.lnk_nolink + if self.key_dotfile_link in dotfile and \ + type(dotfile[self.key_dotfile_link]) is bool: + # patch link: + cur = dotfile[self.key_dotfile_link] + new = self.lnk_nolink + if cur: + new = self.lnk_link + dotfile[self.key_dotfile_link] = new + self._dirty = True + self._dirty_deprecated = True + self._log.warn('deprecated \"link\" value') + + elif self.key_dotfile_link_children in dotfile and \ + type(dotfile[self.key_dotfile_link_children]) is bool: + # patch link_children: + cur = dotfile[self.key_dotfile_link_children] + new = self.lnk_nolink + if cur: + new = self.lnk_children + del dotfile[self.key_dotfile_link_children] + dotfile[self.key_dotfile_link] = new + self._dirty = True + self._dirty_deprecated = True + self._log.warn('deprecated \"link_children\" value') + + ######################################################## + # yaml utils + ######################################################## + + def _prepare_to_save(self, content): + content = self._clear_none(content) + + # make sure we have the base entries + if self.key_settings not in content: + content[self.key_settings] = None + if self.key_dotfiles not in content: + content[self.key_dotfiles] = None + if self.key_profiles not in content: + content[self.key_profiles] = None + return content + + def _load_yaml(self, path): + """load a yaml file to a dict""" + content = {} + if self._debug: + self._log.dbg('----------start:{}----------'.format(path)) + cfg = '\n' + with open(path, 'r') as f: + for line in f: + cfg += line + self._log.dbg(cfg.rstrip()) + self._log.dbg('----------end:{}----------'.format(path)) + try: + content = self._yaml_load(path) + except Exception as e: + self._log.err(e) + raise YamlException('invalid config: {}'.format(path)) + # live patch deprecated entries + return self._fix_deprecated(content) + + def _yaml_load(self, path): + """load from yaml""" + with open(path, 'r') as f: + y = yaml() + y.typ = 'rt' + content = y.load(f) + return content + + def _yaml_dump(self, content, where): + """dump to yaml""" + y = yaml() + y.default_flow_style = False + y.indent = 2 + y.typ = 'rt' + y.dump(content, where) + + ######################################################## + # helpers + ######################################################## def _clear_profile_vars(self, dic): """remove profile variables from dic if found""" + # TODO check why this is used at all [dic.pop(k, None) for k in self.prokeys] def _parse_extended_import_path(self, path_entry): """Parse an import path in a tuple (path, fatal_not_found).""" - if self.debug: - self.log.dbg('parsing path entry {}'.format(path_entry)) + if self._debug: + self._log.dbg('parsing path entry {}'.format(path_entry)) path, _, attribute = path_entry.rpartition(self.key_import_sep) fatal_not_found = attribute != self.key_import_ignore_key @@ -666,14 +1132,14 @@ class CfgYaml: # attribute is set to whatever comes after the separator by # str.rpartition # In both cases, path_entry is the path we're looking for. - if self.debug: - self.log.dbg('using attribute default values for path {}' - .format(path_entry)) + if self._debug: + self._log.dbg('using attribute default values for path {}' + .format(path_entry)) path = path_entry fatal_not_found = self.key_import_fatal_not_found - elif self.debug: - self.log.dbg('path entry {} has fatal_not_found flag set to {}' - .format(path_entry, fatal_not_found)) + elif self._debug: + self._log.dbg('path entry {} has fatal_not_found flag set to {}' + .format(path_entry, fatal_not_found)) return path, fatal_not_found def _handle_non_existing_path(self, path, fatal_not_found=True): @@ -681,13 +1147,13 @@ class CfgYaml: error = 'bad path {}'.format(path) if fatal_not_found: raise YamlException(error) - self.log.warn(error) + self._log.warn(error) def _check_path_existence(self, path, fatal_not_found=True): """Check if a path exists, raising if necessary.""" if os.path.exists(path): - if self.debug: - self.log.dbg('path {} exists'.format(path)) + if self._debug: + self._log.dbg('path {} exists'.format(path)) return path self._handle_non_existing_path(path, fatal_not_found) @@ -714,8 +1180,8 @@ class CfgYaml: path = self._norm_path(path) paths = self._glob_path(path) if self._is_glob(path) else [path] if not paths: - if self.debug: - self.log.dbg("glob path {} didn't expand".format(path)) + if self._debug: + self._log.dbg("glob path {} didn't expand".format(path)) self._handle_non_existing_path(path, fatal_not_found) return [] @@ -738,341 +1204,22 @@ class CfgYaml: processed_paths = (self._process_path(p) for p in paths) return list(chain.from_iterable(processed_paths)) - def _import_actions(self): - """import external actions from paths""" - paths = self.settings.get(self.key_import_actions, None) - if not paths: - return - paths = self._resolve_paths(paths) - for path in paths: - if self.debug: - self.log.dbg('import actions from {}'.format(path)) - new = self._import_sub(path, self.key_actions, - mandatory=False, - patch_func=self._norm_actions) - self.actions = self._merge_dict(new, self.actions) - - def _import_profiles_dotfiles(self): - """import profile dotfiles""" - for k, v in self.profiles.items(): - imp = v.get(self.key_import_profile_dfs, None) - if not imp: - continue - if self.debug: - self.log.dbg('import dotfiles for profile {}'.format(k)) - paths = self._resolve_paths(imp) - for path in paths: - current = v.get(self.key_dotfiles, []) - new = self._import_sub(path, self.key_dotfiles, - mandatory=False) - v[self.key_dotfiles] = new + current - - def _import_config(self, path): - """import config from path""" - if self.debug: - self.log.dbg('import config from {}'.format(path)) - sub = CfgYaml(path, profile=self.profile, debug=self.debug) - - # settings are ignored from external file - # except for filter_file and func_file - self.settings[Settings.key_func_file] += [ - self._norm_path(func_file) - for func_file in sub.settings[Settings.key_func_file] - ] - self.settings[Settings.key_filter_file] += [ - self._norm_path(func_file) - for func_file in sub.settings[Settings.key_filter_file] - ] - - # merge top entries - self.dotfiles = self._merge_dict(self.dotfiles, sub.dotfiles) - self.profiles = self._merge_dict(self.profiles, sub.profiles) - self.actions = self._merge_dict(self.actions, sub.actions) - self.trans_r = self._merge_dict(self.trans_r, sub.trans_r) - self.trans_w = self._merge_dict(self.trans_w, sub.trans_w) - self._clear_profile_vars(sub.variables) - - if self.debug: - self._debug_dict('add import_configs var', sub.variables) - self.variables = self._merge_dict(sub.variables, self.variables) - - def _import_configs(self): - """import configs from external files""" - # settings -> import_configs - imp = self.settings.get(self.key_import_configs, None) - if not imp: - return - paths = self._resolve_paths(imp) - for path in paths: - self._import_config(path) - - def _import_sub(self, path, key, mandatory=False, patch_func=None): + def _template_item(self, item, exc_if_fail=True): """ - import the block "key" from "path" - patch_func is applied to each element if defined + template an item using the templategen + will raise an exception if template failed and exc_if_fail """ - if self.debug: - self.log.dbg('import \"{}\" from \"{}\"'.format(key, path)) - extdict = self._load_yaml(path) - new = self._get_entry(extdict, key, mandatory=mandatory) - if patch_func: - if self.debug: - self.log.dbg('calling patch: {}'.format(patch_func)) - new = patch_func(new) - if not new and mandatory: - err = 'no \"{}\" imported from \"{}\"'.format(key, path) - self.log.warn(err) - raise YamlException(err) - if self.debug: - self.log.dbg('imported \"{}\": {}'.format(key, new)) - return new - - ######################################################## - # add/remove entries - ######################################################## - - def _new_profile(self, key): - """add a new profile if it doesn't exist""" - if key not in self.profiles.keys(): - # update yaml_dict - self.yaml_dict[self.key_profiles][key] = { - self.key_profile_dotfiles: [] - } - if self.debug: - self.log.dbg('adding new profile: {}'.format(key)) - self.dirty = True - - def add_dotfile_to_profile(self, dotfile_key, profile_key): - """add an existing dotfile key to a profile_key""" - self._new_profile(profile_key) - profile = self.yaml_dict[self.key_profiles][profile_key] - if self.key_profile_dotfiles not in profile or \ - profile[self.key_profile_dotfiles] is None: - profile[self.key_profile_dotfiles] = [] - pdfs = profile[self.key_profile_dotfiles] - if self.key_all not in pdfs and \ - dotfile_key not in pdfs: - profile[self.key_profile_dotfiles].append(dotfile_key) - if self.debug: - msg = 'add \"{}\" to profile \"{}\"'.format(dotfile_key, - profile_key) - msg.format(dotfile_key, profile_key) - self.log.dbg(msg) - self.dirty = True - return self.dirty - - def get_all_dotfile_keys(self): - """return all existing dotfile keys""" - return self.dotfiles.keys() - - def add_dotfile(self, key, src, dst, link): - """add a new dotfile""" - if key in self.dotfiles.keys(): - return False - if self.debug: - self.log.dbg('adding new dotfile: {}'.format(key)) - - df_dict = { - self.key_dotfile_src: src, - self.key_dotfile_dst: dst, - } - dfl = self.settings[self.key_settings_link_dotfile_default] - if str(link) != dfl: - df_dict[self.key_dotfile_link] = str(link) - self.yaml_dict[self.key_dotfiles][key] = df_dict - self.dirty = True - - def del_dotfile(self, key): - """remove this dotfile from config""" - if key not in self.yaml_dict[self.key_dotfiles]: - self.log.err('key not in dotfiles: {}'.format(key)) - return False - if self.debug: - self.log.dbg('remove dotfile: {}'.format(key)) - del self.yaml_dict[self.key_dotfiles][key] - if self.debug: - dfs = self.yaml_dict[self.key_dotfiles] - self.log.dbg('new dotfiles: {}'.format(dfs)) - self.dirty = True - return True - - def del_dotfile_from_profile(self, df_key, pro_key): - """remove this dotfile from that profile""" - if df_key not in self.dotfiles.keys(): - self.log.err('key not in dotfiles: {}'.format(df_key)) - return False - if pro_key not in self.profiles.keys(): - self.log.err('key not in profile: {}'.format(pro_key)) - return False - # get the profile dictionary - profile = self.yaml_dict[self.key_profiles][pro_key] - if df_key not in profile[self.key_profile_dotfiles]: - return True - if self.debug: - dfs = profile[self.key_profile_dotfiles] - self.log.dbg('{} profile dotfiles: {}'.format(pro_key, dfs)) - self.log.dbg('remove {} from profile {}'.format(df_key, pro_key)) - profile[self.key_profile_dotfiles].remove(df_key) - if self.debug: - dfs = profile[self.key_profile_dotfiles] - self.log.dbg('{} profile dotfiles: {}'.format(pro_key, dfs)) - self.dirty = True - return True - - ######################################################## - # handle deprecated entries - ######################################################## - - def _fix_deprecated(self, yamldict): - """fix deprecated entries""" - self._fix_deprecated_link_by_default(yamldict) - self._fix_deprecated_dotfile_link(yamldict) - - def _fix_deprecated_link_by_default(self, yamldict): - """fix deprecated link_by_default""" - key = 'link_by_default' - newkey = self.key_imp_link - if self.key_settings not in yamldict: - return - if not yamldict[self.key_settings]: - return - config = yamldict[self.key_settings] - if key not in config: - return - if config[key]: - config[newkey] = self.lnk_link - else: - config[newkey] = self.lnk_nolink - del config[key] - self.log.warn('deprecated \"link_by_default\"') - self.dirty = True - self.dirty_deprecated = True - - def _fix_deprecated_dotfile_link(self, yamldict): - """fix deprecated link in dotfiles""" - if self.key_dotfiles not in yamldict: - return - if not yamldict[self.key_dotfiles]: - return - for k, dotfile in yamldict[self.key_dotfiles].items(): - new = self.lnk_nolink - if self.key_dotfile_link in dotfile and \ - type(dotfile[self.key_dotfile_link]) is bool: - # patch link: - cur = dotfile[self.key_dotfile_link] - new = self.lnk_nolink - if cur: - new = self.lnk_link - dotfile[self.key_dotfile_link] = new - self.dirty = True - self.dirty_deprecated = True - self.log.warn('deprecated \"link\" value') - - elif self.key_dotfile_link_children in dotfile and \ - type(dotfile[self.key_dotfile_link_children]) is bool: - # patch link_children: - cur = dotfile[self.key_dotfile_link_children] - new = self.lnk_nolink - if cur: - new = self.lnk_children - del dotfile[self.key_dotfile_link_children] - dotfile[self.key_dotfile_link] = new - self.dirty = True - self.dirty_deprecated = True - self.log.warn('deprecated \"link_children\" value') - - ######################################################## - # yaml utils - ######################################################## - - def _prepare_to_save(self, content): - content = self._clear_none(content) - - # make sure we have the base entries - if self.key_settings not in content: - content[self.key_settings] = None - if self.key_dotfiles not in content: - content[self.key_dotfiles] = None - if self.key_profiles not in content: - content[self.key_profiles] = None - - return content - - def save(self): - """save this instance and return True if saved""" - if not self.dirty: - return False - - content = self._prepare_to_save(self.yaml_dict) - - if self.dirty_deprecated: - # add minversion - settings = content[self.key_settings] - settings[self.key_settings_minversion] = VERSION - - # save to file - if self.debug: - self.log.dbg('saving to {}'.format(self.path)) + # TODO use this across the entire file + if not Templategen.var_is_template(item): + return item try: - with open(self.path, 'w') as f: - self._yaml_dump(content, f) - except Exception as e: - self.log.err(e) - raise YamlException('error saving config: {}'.format(self.path)) - - if self.dirty_deprecated: - warn = 'your config contained deprecated entries' - warn += ' and was updated' - self.log.warn(warn) - - self.dirty = False - self.cfg_updated = False - return True - - def dump(self): - """dump the config dictionary""" - output = io.StringIO() - content = self._prepare_to_save(self.yaml_dict.copy()) - self._yaml_dump(content, output) - return output.getvalue() - - def _load_yaml(self, path): - """load a yaml file to a dict""" - content = {} - if self.debug: - self.log.dbg('----------start:{}----------'.format(path)) - cfg = '\n' - with open(path, 'r') as f: - for line in f: - cfg += line - self.log.dbg(cfg.rstrip()) - self.log.dbg('----------end:{}----------'.format(path)) - try: - content = self._yaml_load(path) - except Exception as e: - self.log.err(e) - raise YamlException('invalid config: {}'.format(path)) - return content - - def _yaml_load(self, path): - """load from yaml""" - with open(path, 'r') as f: - y = yaml() - y.typ = 'rt' - content = y.load(f) - return content - - def _yaml_dump(self, content, where): - """dump to yaml""" - y = yaml() - y.default_flow_style = False - y.indent = 2 - y.typ = 'rt' - y.dump(content, where) - - ######################################################## - # helpers - ######################################################## + val = item + while Templategen.var_is_template(val): + val = self._tmpl.generate_string(val) + except UndefinedException as e: + if exc_if_fail: + raise e + return val def _merge_dict(self, high, low): """merge high and low dict""" @@ -1119,8 +1266,8 @@ class CfgYaml: def _glob_path(self, path): """Expand a glob.""" - if self.debug: - self.log.dbg('expanding glob {}'.format(path)) + if self._debug: + self._log.dbg('expanding glob {}'.format(path)) expanded_path = os.path.expanduser(path) return glob.glob(expanded_path, recursive=True) @@ -1130,41 +1277,56 @@ class CfgYaml: return path path = os.path.expanduser(path) if not os.path.isabs(path): - d = os.path.dirname(self.path) + d = os.path.dirname(self._path) ret = os.path.join(d, path) - if self.debug: + if self._debug: msg = 'normalizing relative to cfg: {} -> {}' - self.log.dbg(msg.format(path, ret)) + self._log.dbg(msg.format(path, ret)) return ret ret = os.path.normpath(path) - if self.debug and path != ret: - self.log.dbg('normalizing: {} -> {}'.format(path, ret)) + if self._debug and path != ret: + self._log.dbg('normalizing: {} -> {}'.format(path, ret)) return ret - def _shell_exec_dvars(self, keys, variables): + def _shell_exec_dvars(self, dic): """shell execute dynvariables""" - for k in list(keys): - ret, out = shell(variables[k], debug=self.debug) + # TODO remove other calls outside initial setup of dvars + executed = {} + for k, v in dic.items(): + ret, out = shell(v, debug=self._debug) if not ret: - err = 'var \"{}: {}\" failed: {}'.format(k, variables[k], out) - self.log.err(err) + err = 'var \"{}: {}\" failed: {}'.format(k, v, out) + self._log.err(err) raise YamlException(err) - if self.debug: - self.log.dbg('\"{}\": {} -> {}'.format(k, variables[k], out)) - variables[k] = out + if self._debug: + self._log.dbg('\"{}\": {} -> {}'.format(k, v, out)) + executed[k] = out + return executed - def _template_list(self, t, entries): + def _template_list(self, entries): """template a list of entries""" new = [] if not entries: return new for e in entries: - et = t.generate_string(e) - if self.debug and e != et: - self.log.dbg('resolved: {} -> {}'.format(e, et)) + et = self._template_item(e) + if self._debug and e != et: + self._log.dbg('resolved: {} -> {}'.format(e, et)) new.append(et) return new + def _template_dict(self, entries): + """template a dictionary of entries""" + new = {} + if not entries: + return new + for k, v in entries.items(): + vt = self._template_item(v) + if self._debug and v != vt: + self._log.dbg('resolved: {} -> {}'.format(v, vt)) + new[k] = vt + return new + def _check_minversion(self, minversion): if not minversion: return @@ -1181,10 +1343,10 @@ class CfgYaml: def _debug_dict(self, title, elems): """pretty print dict""" - if not self.debug: + if not self._debug: return - self.log.dbg('{}:'.format(title)) + self._log.dbg('{}:'.format(title)) if not elems: return for k, v in elems.items(): - self.log.dbg('\t- \"{}\": {}'.format(k, v)) + self._log.dbg('\t- \"{}\": {}'.format(k, v))