diff --git a/dotdrop/cfg_yaml.py b/dotdrop/cfg_yaml.py index bab7896..3b8b868 100644 --- a/dotdrop/cfg_yaml.py +++ b/dotdrop/cfg_yaml.py @@ -18,6 +18,8 @@ the upper layer: Additionally a few methods are exported. """ +# pylint: disable=C0302 + import os import glob import io @@ -31,7 +33,7 @@ from dotdrop.settings import Settings from dotdrop.logger import Logger from dotdrop.templategen import Templategen from dotdrop.linktypes import LinkTypes -from dotdrop.utils import shell, uniq_list +from dotdrop.utils import shellrun, uniq_list from dotdrop.exceptions import YamlException, UndefinedException @@ -96,7 +98,7 @@ class CfgYaml: allowed_link_val = [lnk_nolink, lnk_link, lnk_children] top_entries = [key_dotfiles, key_settings, key_profiles] - def __init__(self, path, profile=None, addprofiles=[], debug=False): + def __init__(self, path, profile=None, addprofiles=None, debug=False): """ config parser @path: config file path @@ -115,7 +117,7 @@ class CfgYaml: # profile variables self._profilevarskeys = [] # included profiles - self._inc_profiles = addprofiles + self._inc_profiles = addprofiles or [] # init the dictionaries self.settings = {} @@ -180,11 +182,11 @@ class CfgYaml: # include the profile's variables/dynvariables last # as it overwrites existing ones - self._inc_profiles, pv, pvd = self._get_profile_included_vars() - self._add_variables(pv, prio=True) - self._add_variables(pvd, shell=True, prio=True) - self._profilevarskeys.extend(pv.keys()) - self._profilevarskeys.extend(pvd.keys()) + self._inc_profiles, pvar, pdvar = self._get_profile_included_vars() + self._add_variables(pvar, prio=True) + self._add_variables(pdvar, shell=True, prio=True) + self._profilevarskeys.extend(pvar.keys()) + self._profilevarskeys.extend(pdvar.keys()) # template variables self.variables = self._template_dict(self.variables) @@ -232,11 +234,11 @@ class CfgYaml: self._resolve_profile_includes() # add the current profile variables - _, pv, pvd = self._get_profile_included_vars() - self._add_variables(pv, prio=False) - self._add_variables(pvd, shell=True, prio=False) - self._profilevarskeys.extend(pv.keys()) - self._profilevarskeys.extend(pvd.keys()) + _, pvar, pdvar = self._get_profile_included_vars() + self._add_variables(pvar, prio=False) + self._add_variables(pdvar, shell=True, prio=False) + self._profilevarskeys.extend(pvar.keys()) + self._profilevarskeys.extend(pdvar.keys()) # resolve variables self._clear_profile_vars(newvars) @@ -322,21 +324,21 @@ class CfgYaml: """update an existing dotfile""" if key not in self.dotfiles.keys(): return False - df = self._yaml_dict[self.key_dotfiles][key] + dotfile = self._yaml_dict[self.key_dotfiles][key] old = None - if self.key_dotfile_chmod in df: - old = df[self.key_dotfile_chmod] + if self.key_dotfile_chmod in dotfile: + old = dotfile[self.key_dotfile_chmod] if old == chmod: return False if self._debug: self._dbg('update dotfile: {}'.format(key)) self._dbg('old chmod value: {}'.format(old)) self._dbg('new chmod value: {}'.format(chmod)) - df = self._yaml_dict[self.key_dotfiles][key] + dotfile = self._yaml_dict[self.key_dotfiles][key] if not chmod: - del df[self.key_dotfile_chmod] + del dotfile[self.key_dotfile_chmod] else: - df[self.key_dotfile_chmod] = str(format(chmod, 'o')) + dotfile[self.key_dotfile_chmod] = str(format(chmod, 'o')) self._dirty = True return True @@ -426,11 +428,12 @@ class CfgYaml: if self._debug: self._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)) + with open(self._path, 'w') as file: + self._yaml_dump(content, file) + except Exception as exc: + self._log.err(exc) + err = 'error saving config: {}'.format(self._path) + raise YamlException(err) from exc if self._dirty_deprecated: warn = 'your config contained deprecated entries' @@ -587,11 +590,11 @@ class CfgYaml: 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, []) + for _, val in self.profiles.items(): + entries = val.get(self.key_import_profile_dfs, []) new = self._template_list(entries) if new: - v[self.key_import_profile_dfs] = new + val[self.key_import_profile_dfs] = new def _norm_actions(self, actions): """ @@ -601,12 +604,12 @@ class CfgYaml: if not actions: return actions new = {} - for k, v in actions.items(): - if k == self.action_pre or k == self.action_post: - for key, action in v.items(): + for k, val in actions.items(): + if k in (self.action_pre, self.action_post): + for key, action in val.items(): new[key] = (k, action) else: - new[k] = (self.action_post, v) + new[k] = (self.action_post, val) return new def _norm_profiles(self, profiles): @@ -614,14 +617,14 @@ class CfgYaml: if not profiles: return profiles new = {} - for k, v in profiles.items(): - if not v: + for k, val in profiles.items(): + if not val: # no dotfiles continue # add dotfiles entry if not present - if self.key_profile_dotfiles not in v: - v[self.key_profile_dotfiles] = [] - new[k] = v + if self.key_profile_dotfiles not in val: + val[self.key_profile_dotfiles] = [] + new[k] = val return new def _norm_dotfiles(self, dotfiles): @@ -629,55 +632,56 @@ class CfgYaml: if not dotfiles: return dotfiles new = {} - for k, v in dotfiles.items(): + for k, val in dotfiles.items(): # add 'src' as key' if not present - if self.key_dotfile_src not in v: - v[self.key_dotfile_src] = k - new[k] = v + if self.key_dotfile_src not in val: + val[self.key_dotfile_src] = k + new[k] = val else: - new[k] = v + new[k] = val # fix deprecated trans key - if self.old_key_trans_r in v: + if self.old_key_trans_r in val: msg = '\"trans\" is deprecated, please use \"trans_read\"' 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 - if self.key_dotfile_link not in v: + val[self.key_trans_r] = val[self.old_key_trans_r] + del val[self.old_key_trans_r] + new[k] = val + if self.key_dotfile_link not in val: # apply link value if undefined - val = self.settings[self.key_settings_link_dotfile_default] - v[self.key_dotfile_link] = val + value = self.settings[self.key_settings_link_dotfile_default] + val[self.key_dotfile_link] = value # apply noempty if undefined - if self.key_dotfile_noempty not in v: - val = self.settings.get(self.key_settings_noempty, False) - v[self.key_dotfile_noempty] = val + if self.key_dotfile_noempty not in val: + value = self.settings.get(self.key_settings_noempty, False) + val[self.key_dotfile_noempty] = value # apply template if undefined - if self.key_dotfile_template not in v: - val = self.settings.get(self.key_settings_template, True) - v[self.key_dotfile_template] = val + if self.key_dotfile_template not in val: + value = self.settings.get(self.key_settings_template, True) + val[self.key_dotfile_template] = value # validate value of chmod if defined - if self.key_dotfile_chmod in v: - val = str(v[self.key_dotfile_chmod]) - if len(val) < 3: - err = 'bad format for chmod: {}'.format(val) + if self.key_dotfile_chmod in val: + value = str(val[self.key_dotfile_chmod]) + if len(value) < 3: + err = 'bad format for chmod: {}'.format(value) self._log.err(err) raise YamlException('config content error: {}'.format(err)) try: - int(val) - except Exception: - err = 'bad format for chmod: {}'.format(val) + int(value) + except Exception as exc: + err = 'bad format for chmod: {}'.format(value) self._log.err(err) - raise YamlException('config content error: {}'.format(err)) + err = 'config content error: {}'.format(err) + raise YamlException(err) from exc # normalize chmod value - for x in list(val): - y = int(x) - if y < 0 or y > 7: - err = 'bad format for chmod: {}'.format(val) + for chmodv in list(value): + chmodint = int(chmodv) + if chmodint < 0 or chmodint > 7: + err = 'bad format for chmod: {}'.format(value) self._log.err(err) raise YamlException( 'config content error: {}'.format(err) ) - v[self.key_dotfile_chmod] = int(val, 8) + val[self.key_dotfile_chmod] = int(value, 8) return new @@ -719,13 +723,13 @@ class CfgYaml: if profile: variables['profile'] = profile # add some more variables - p = self.settings.get(self.key_settings_dotpath) - p = self._norm_path(p) - variables['_dotdrop_dotpath'] = p + path = self.settings.get(self.key_settings_dotpath) + path = self._norm_path(path) + variables['_dotdrop_dotpath'] = 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 + path = self.settings.get(self.key_settings_workdir) + path = self._norm_path(path) + variables['_dotdrop_workdir'] = path return variables def _get_profile_included_item(self, keyitem): @@ -765,18 +769,18 @@ class CfgYaml: def _resolve_profile_all(self): """resolve some other parts of the config""" # profile -> ALL - for k, v in self.profiles.items(): - dfs = v.get(self.key_profile_dotfiles, None) + for k, val in self.profiles.items(): + dfs = val.get(self.key_profile_dotfiles, None) if not dfs: continue if self.key_all in dfs: if self._debug: self._dbg('add ALL to profile \"{}\"'.format(k)) - v[self.key_profile_dotfiles] = self.dotfiles.keys() + val[self.key_profile_dotfiles] = self.dotfiles.keys() def _resolve_profile_includes(self): """resolve profile(s) including other profiles""" - for k, v in self.profiles.items(): + for k, _ in self.profiles.items(): self._rec_resolve_profile_include(k) def _rec_resolve_profile_include(self, profile): @@ -860,7 +864,7 @@ class CfgYaml: """import external variables from paths""" paths = self.settings.get(self.key_import_variables, None) if not paths: - return + return None paths = self._resolve_paths(paths) newvars = {} for path in paths: @@ -899,18 +903,18 @@ class CfgYaml: def _import_profiles_dotfiles(self): """import profile dotfiles""" - for k, v in self.profiles.items(): - imp = v.get(self.key_import_profile_dfs, None) + for k, val in self.profiles.items(): + imp = val.get(self.key_import_profile_dfs, None) if not imp: continue if self._debug: self._dbg('import dotfiles for profile {}'.format(k)) paths = self._resolve_paths(imp) for path in paths: - current = v.get(self.key_dotfiles, []) + current = val.get(self.key_dotfiles, []) new = self._import_sub(path, self.key_dotfiles, mandatory=False) - v[self.key_dotfiles] = new + current + val[self.key_dotfiles] = new + current def _import_config(self, path): """import config from path""" @@ -999,7 +1003,6 @@ class CfgYaml: return 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""" @@ -1028,9 +1031,9 @@ class CfgYaml: return if not yamldict[self.key_dotfiles]: return - for k, dotfile in yamldict[self.key_dotfiles].items(): + for _, dotfile in yamldict[self.key_dotfiles].items(): if self.key_dotfile_link in dotfile and \ - type(dotfile[self.key_dotfile_link]) is bool: + isinstance(dotfile[self.key_dotfile_link], bool): # patch link: cur = dotfile[self.key_dotfile_link] new = self.lnk_nolink @@ -1042,7 +1045,7 @@ class CfgYaml: self._log.warn('deprecated \"link\" value') elif old_key in dotfile and \ - type(dotfile[old_key]) is bool: + isinstance(dotfile[old_key], bool): # patch link_children: cur = dotfile[old_key] new = self.lnk_nolink @@ -1076,16 +1079,17 @@ class CfgYaml: if self._debug: self._dbg('----------start:{}----------'.format(path)) cfg = '\n' - with open(path, 'r') as f: - for line in f: + with open(path, 'r') as file: + for line in file: cfg += line self._dbg(cfg.rstrip()) self._dbg('----------end:{}----------'.format(path)) try: content = self._yaml_load(path) - except Exception as e: - self._log.err(e) - raise YamlException('config yaml error: {}'.format(path)) + except Exception as exc: + self._log.err(exc) + err = 'config yaml error: {}'.format(path) + raise YamlException(err) from exc return content @@ -1095,9 +1099,9 @@ class CfgYaml: return # check top entries - for e in self.top_entries: - if e not in yamldict: - err = 'no {} entry found'.format(e) + for entry in self.top_entries: + if entry not in yamldict: + err = 'no {} entry found'.format(entry) self._log.err(err) raise YamlException('config format error: {}'.format(err)) @@ -1117,21 +1121,23 @@ class CfgYaml: self._log.err(err) raise YamlException('config content error: {}'.format(err)) - def _yaml_load(self, path): + @classmethod + def _yaml_load(cls, path): """load from yaml""" - with open(path, 'r') as f: - y = yaml() - y.typ = 'rt' - content = y.load(f) + with open(path, 'r') as file: + data = yaml() + data.typ = 'rt' + content = data.load(file) return content - def _yaml_dump(self, content, where): + @classmethod + def _yaml_dump(cls, content, where): """dump to yaml""" - y = yaml() - y.default_flow_style = False - y.indent = 2 - y.typ = 'rt' - y.dump(content, where) + data = yaml() + data.default_flow_style = False + data.indent = 2 + data.typ = 'rt' + data.dump(content, where) ######################################################## # templating @@ -1156,9 +1162,9 @@ class CfgYaml: val = item while Templategen.var_is_template(val): val = self._tmpl.generate_string(val) - except UndefinedException as e: + except UndefinedException as exc: if exc_if_fail: - raise e + raise exc return val def _template_list(self, entries): @@ -1459,7 +1465,7 @@ class CfgYaml: keys = dic.keys() for k in keys: val = dic[k] - ret, out = shell(val, debug=self._debug) + ret, out = shellrun(val, debug=self._debug) if not ret: err = 'var \"{}: {}\" failed: {}'.format(k, val, out) self._log.err(err) diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index ce5fafd..d6ef02a 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -333,6 +333,7 @@ def cmd_install(opts): # check result for fut in futures.as_completed(wait_for): tmpret, key, err = fut.result() + # check result if tmpret: installed.append(key) elif err: diff --git a/dotdrop/installer.py b/dotdrop/installer.py index d99a0b6..f834a4e 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -356,7 +356,9 @@ class Installer: - False, 'aborted' : user aborted """ overwrite = not self.safe + if os.path.lexists(dst): + # symlink exists if os.path.realpath(dst) == os.path.realpath(src): msg = 'ignoring "{}", link already exists'.format(dst) self.log.dbg(msg) @@ -369,22 +371,29 @@ class Installer: msg = 'Remove "{}" for link creation?'.format(dst) if self.safe and not self.log.ask(msg): return False, 'aborted' + + # remove symlink overwrite = True try: utils.removepath(dst) except OSError as exc: err = 'something went wrong with {}: {}'.format(src, exc) return False, err + if self.dry: self.log.dry('would link {} to {}'.format(dst, src)) return True, None + base = os.path.dirname(dst) if not self._create_dirs(base): err = 'error creating directory for {}'.format(dst) return False, err + + # execute pre-actions ret, err = self._exec_pre_actions(actionexec) if not ret: return False, err + # re-check in case action created the file if os.path.lexists(dst): msg = 'Remove "{}" for link creation?'.format(dst) @@ -395,6 +404,8 @@ class Installer: except OSError as exc: err = 'something went wrong with {}: {}'.format(src, exc) return False, err + + # create symlink os.symlink(src, dst) if not self.comparing: self.log.sub('linked {} to {}'.format(dst, src)) @@ -418,20 +429,17 @@ class Installer: self.log.dbg('is_template: {}'.format(is_template)) self.log.dbg('no empty: {}'.format(noempty)) + # ignore file + if utils.must_ignore([src, dst], ignore, debug=self.debug): + self.log.dbg('ignoring install of {} to {}'.format(src, dst)) + return False, None + # check no loop if utils.samefile(src, dst): err = 'dotfile points to itself: {}'.format(dst) return False, err - if utils.must_ignore([src, dst], ignore, debug=self.debug): - self.log.dbg('ignoring install of {} to {}'.format(src, dst)) - return False, None - - if utils.samefile(src, dst): - # loop - err = 'dotfile points to itself: {}'.format(dst) - return False, err - + # check source file exists if not os.path.exists(src): err = 'source dotfile does not exist: {}'.format(src) return False, err @@ -499,9 +507,9 @@ class Installer: is_template=is_template) if not res and err: # error occured - ret = res, err - break - elif res: + return res, err + + if res: # something got installed ret = True, None else: @@ -514,9 +522,9 @@ class Installer: is_template=is_template) if not res and err: # error occured - ret = res, err - break - elif res: + return res, err + + if res: # something got installed ret = True, None return ret @@ -563,6 +571,7 @@ class Installer: return True, None if os.path.lexists(dst): + # file/symlink exists try: os.stat(dst) except OSError as exc: @@ -575,6 +584,7 @@ class Installer: if not self._is_different(src, dst, content=content): self.log.dbg('{} is the same'.format(dst)) return False, None + if self.safe: self.log.dbg('change detected for {}'.format(dst)) if self.showdiff: @@ -585,8 +595,8 @@ class Installer: return False, 'aborted' overwrite = True - if self.backup and os.path.lexists(dst): - self._backup(dst) + if self.backup: + self._backup(dst) # create hierarchy base = os.path.dirname(dst) diff --git a/dotdrop/options.py b/dotdrop/options.py index 7d35da5..276f0e9 100644 --- a/dotdrop/options.py +++ b/dotdrop/options.py @@ -5,6 +5,7 @@ Copyright (c) 2017, deadc0de6 stores all options to use across dotdrop """ +# attribute-defined-outside-init # pylint: disable=W0201 import os @@ -164,7 +165,27 @@ class Options(AttrMonitor): # start monitoring for bad attribute self._set_attr_err = True -# pylint: disable=R0911 + @classmethod + def _get_config_from_fs(cls): + """get config from filesystem""" + # look in ~/.config/dotdrop + cfg = os.path.expanduser(HOMECFG) + path = os.path.join(cfg, CONFIG) + if os.path.exists(path): + return path + + # look in /etc/xdg/dotdrop + path = os.path.join(ETCXDGCFG, CONFIG) + if os.path.exists(path): + return path + + # look in /etc/dotdrop + path = os.path.join(ETCCFG, CONFIG) + if os.path.exists(path): + return path + + return '' + def _get_config_path(self): """get the config path""" # cli provided @@ -186,24 +207,7 @@ class Options(AttrMonitor): if os.path.exists(path): return path - # look in ~/.config/dotdrop - cfg = os.path.expanduser(HOMECFG) - path = os.path.join(cfg, CONFIG) - if os.path.exists(path): - return path - - # look in /etc/xdg/dotdrop - path = os.path.join(ETCXDGCFG, CONFIG) - if os.path.exists(path): - return path - - # look in /etc/dotdrop - path = os.path.join(ETCCFG, CONFIG) - if os.path.exists(path): - return path - - return '' -# pylint: enable=R0911 + return self._get_config_from_fs() def _header(self): """display the header""" diff --git a/dotdrop/utils.py b/dotdrop/utils.py index bfe959e..c3dcf1c 100644 --- a/dotdrop/utils.py +++ b/dotdrop/utils.py @@ -56,7 +56,7 @@ def write_to_tmpfile(content): return path -def shell(cmd, debug=False): +def shellrun(cmd, debug=False): """ run a command in the shell (expects a string) returns True|False, output @@ -256,8 +256,7 @@ def uniq_list(a_list): def patch_ignores(ignores, prefix, debug=False): """allow relative ignore pattern""" new = [] - if debug: - LOG.dbg('ignores before patching: {}'.format(ignores), force=True) + LOG.dbg('ignores before patching: {}'.format(ignores), force=debug) for ignore in ignores: negative = ignore.startswith('!') if negative: @@ -284,8 +283,7 @@ def patch_ignores(ignores, prefix, debug=False): new.append('!' + path) else: new.append(path) - if debug: - LOG.dbg('ignores after patching: {}'.format(new), force=True) + LOG.dbg('ignores after patching: {}'.format(new), force=debug) return new @@ -305,8 +303,12 @@ def get_module_from_path(path): if not path or not os.path.exists(path): return None module_name = os.path.basename(path).rstrip('.py') - loader = importlib.machinery.SourceFileLoader(module_name, path) - mod = loader.load_module() + # allow any type of files + importlib.machinery.SOURCE_SUFFIXES.append('') + # import module + spec = importlib.util.spec_from_file_location(module_name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) return mod diff --git a/scripts/change-link.py b/scripts/change-link.py index f61a419..ec75312 100755 --- a/scripts/change-link.py +++ b/scripts/change-link.py @@ -10,8 +10,9 @@ usage example: ./change-link.py --true ../config.yaml --ignore f_vimrc --ignore f_xinitrc """ -from docopt import docopt import os +import io +from docopt import docopt from ruamel.yaml import YAML as yaml USAGE = """ @@ -26,11 +27,12 @@ Options: """ -key = 'dotfiles' -entry = 'link' +KEY = 'dotfiles' +ENTRY = 'link' def main(): + """entry point""" args = docopt(USAGE) path = os.path.expanduser(args['']) if args['--true']: @@ -40,19 +42,19 @@ def main(): ignores = args['--ignore'] - with open(path, 'r') as f: - content = yaml(typ='safe').load(f) - for k, v in content[key].items(): + with open(path, 'r') as file: + content = yaml(typ='safe').load(file) + for k, val in content[KEY].items(): if k in ignores: continue - v[entry] = value + val[ENTRY] = value output = io.StringIO() - y = yaml() - y.default_flow_style = False - y.indent = 2 - y.typ = 'rt' - y.dump(content, output) + data = yaml() + data.default_flow_style = False + data.indent = 2 + data.typ = 'rt' + data.dump(content, output) print(output)