From 3e58857d43f5bf0f15eefb4d3999cfdd6631570f Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Fri, 29 Mar 2019 14:43:08 +0100 Subject: [PATCH] refactor link default config variables --- config.yaml | 4 +- dotdrop/config.py | 157 +++++++++++++++++++++++++--------- dotdrop/dotdrop.py | 6 +- dotdrop/options.py | 20 +++-- tests-ng/deprecated-link.sh | 142 ++++++++++++++++++++++++++++++ tests-ng/inst-link-default.sh | 12 +-- tests/helpers.py | 3 +- tests/test_config.py | 76 +++++++++++++++- tests/test_import.py | 4 +- 9 files changed, 362 insertions(+), 62 deletions(-) create mode 100755 tests-ng/deprecated-link.sh diff --git a/config.yaml b/config.yaml index f201f6f..cca9aab 100644 --- a/config.yaml +++ b/config.yaml @@ -5,7 +5,7 @@ config: banner: true longkey: false keepdot: false - link_import_default: false - link_install_default: nolink + link_import_default: nolink + link_dotfile_default: nolink dotfiles: profiles: diff --git a/dotdrop/config.py b/dotdrop/config.py index d4157d4..de1ec3f 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -32,16 +32,10 @@ class Cfg: key_ignoreempty = 'ignoreempty' key_showdiff = 'showdiff' key_imp_link = 'link_import_default' - key_inst_link = 'link_install_default' + key_dotfile_link = 'link_dotfile_default' key_workdir = 'workdir' key_include_vars = 'import_variables' - # below entries will be automatically transformed - # to their new counterpart - key_deprecated = { - 'link_by_default': key_imp_link, - } - # actions keys key_actions = 'actions' key_actions_pre = 'pre' @@ -89,8 +83,8 @@ class Cfg: default_keepdot = False default_showdiff = False default_ignoreempty = False - default_link_imp = False - default_link_inst = 'nolink' + default_link_imp = lnk_nolink + default_link = lnk_nolink default_workdir = '~/.config/dotdrop' def __init__(self, cfgpath, profile=None, debug=False): @@ -106,6 +100,7 @@ class Cfg: # make sure to have an absolute path to config file self.cfgpath = os.path.abspath(cfgpath) self.debug = debug + self._modified = False # init the logger self.log = Logger() @@ -196,9 +191,13 @@ class Cfg: return False return True - def _get_def_inst_link(self): + def _get_def_link(self): """get dotfile link entry when not specified""" - string = self.lnk_settings[self.key_inst_link].lower() + string = self.lnk_settings[self.key_dotfile_link].lower() + return self._string_to_linktype(string) + + def _string_to_linktype(self, string): + """translate string to linktype""" if string == self.lnk_parent.lower(): return LinkTypes.PARENT elif string == self.lnk_children.lower(): @@ -209,7 +208,8 @@ class Cfg: """parse config file""" # parse the settings self.lnk_settings = self.content[self.key_settings] - self._complete_settings() + if not self._complete_settings(): + return False # parse the profiles self.lnk_profiles = self.content[self.key_profiles] @@ -278,8 +278,9 @@ class Cfg: if not self.content[self.key_dotfiles]: # ensures the dotfiles entry is a dict self.content[self.key_dotfiles] = {} - def_link_val = self._get_def_inst_link() - for k, v in self.content[self.key_dotfiles].items(): + + for k in self.content[self.key_dotfiles].keys(): + v = self.content[self.key_dotfiles][k] src = os.path.normpath(v[self.key_dotfiles_src]) dst = os.path.normpath(v[self.key_dotfiles_dst]) @@ -291,17 +292,21 @@ class Cfg: self.log.err(msg.format(k)) return False - # Otherwise, get link type - link = def_link_val - if self.key_dotfiles_link in v and v[self.key_dotfiles_link]: - link = LinkTypes.PARENT - if self.key_dotfiles_link_children in v \ - and v[self.key_dotfiles_link_children]: - link = LinkTypes.CHILDREN + # fix it + v = self._fix_dotfile_link(k, v) + self.content[self.key_dotfiles][k] = v + # get link type + link = self._get_def_link() + if self.key_dotfiles_link in v: + link = self._string_to_linktype(v[self.key_dotfiles_link]) + + # get ignore empty noempty = v[self.key_dotfiles_noempty] if \ self.key_dotfiles_noempty \ in v else self.lnk_settings[self.key_ignoreempty] + + # parse actions itsactions = v[self.key_dotfiles_actions] if \ self.key_dotfiles_actions in v else [] actions = self._parse_actions(itsactions) @@ -563,7 +568,7 @@ class Cfg: def _complete_settings(self): """set settings defaults if not present""" - self._deprecated() + self._fix_deprecated() if self.key_dotpath not in self.lnk_settings: self.lnk_settings[self.key_dotpath] = self.default_dotpath if self.key_backup not in self.lnk_settings: @@ -576,32 +581,99 @@ class Cfg: self.lnk_settings[self.key_long] = self.default_longkey if self.key_keepdot not in self.lnk_settings: self.lnk_settings[self.key_keepdot] = self.default_keepdot - if self.key_imp_link not in self.lnk_settings: - self.lnk_settings[self.key_imp_link] = self.default_link_imp if self.key_workdir not in self.lnk_settings: self.lnk_settings[self.key_workdir] = self.default_workdir if self.key_showdiff not in self.lnk_settings: self.lnk_settings[self.key_showdiff] = self.default_showdiff if self.key_ignoreempty not in self.lnk_settings: self.lnk_settings[self.key_ignoreempty] = self.default_ignoreempty - if self.key_inst_link not in self.lnk_settings: - self.lnk_settings[self.key_inst_link] = self.default_link_inst - def _deprecated(self): + if self.key_dotfile_link not in self.lnk_settings: + self.lnk_settings[self.key_dotfile_link] = self.default_link + else: + key = self.lnk_settings[self.key_dotfile_link] + if key != self.lnk_parent and \ + key != self.lnk_children and \ + key != self.lnk_nolink: + self.log.err('bad value for {}'.format(self.key_dotfile_link)) + return False + + if self.key_imp_link not in self.lnk_settings: + self.lnk_settings[self.key_imp_link] = self.default_link_imp + elif self.lnk_settings[self.key_imp_link] == self.lnk_children: + msg = '\"{}\" not supported in {}'.format(self.lnk_children, + self.key_imp_link) + self.log.err(msg) + return False + else: + key = self.lnk_settings[self.key_imp_link] + if key != self.lnk_parent and \ + key != self.lnk_children and \ + key != self.lnk_nolink: + self.log.err('bad value for {}'.format(self.key_dotfile_link)) + return False + return True + + def _fix_deprecated(self): """fix deprecated entries""" - for k, v in self.key_deprecated.items(): - if k in self.lnk_settings: - # replace - entry = self.lnk_settings[k] - self.lnk_settings[v] = entry - del self.lnk_settings[k] + # link_by_default + key = 'link_by_default' + newkey = self.key_imp_link + if key in self.lnk_settings: + if self.lnk_settings[key]: + self.lnk_settings[newkey] = self.lnk_parent + else: + self.lnk_settings[newkey] = self.lnk_nolink + del self.lnk_settings[key] + self._modified = True + + def _fix_dotfile_link(self, key, entry): + """fix deprecated link usage in dotfile entry""" + v = entry + + if self.key_dotfiles_link not in v \ + and self.key_dotfiles_link_children not in v: + # nothing defined + return v + + new = self.lnk_nolink + if self.key_dotfiles_link in v \ + and type(v[self.key_dotfiles_link]) is bool: + # patch link: + if v[self.key_dotfiles_link]: + new = self.lnk_parent + else: + new = self.lnk_nolink + self._modified = True + if self.debug: + self.log.dbg('link updated for {} to {}'.format(key, new)) + elif self.key_dotfiles_link_children in v \ + and type(v[self.key_dotfiles_link_children]) is bool: + # patch link_children: + if v[self.key_dotfiles_link_children]: + new = self.lnk_children + else: + new = self.lnk_nolink + del v[self.key_dotfiles_link_children] + self._modified = True + if self.debug: + self.log.dbg('link updated for {} to {}'.format(key, new)) + else: + # no change + new = v[self.key_dotfiles_link] + + v[self.key_dotfiles_link] = new + return v def _save(self, content, path): """writes the config to file""" ret = False with open(path, 'w') as f: ret = yaml.dump(content, f, - default_flow_style=False, indent=2) + default_flow_style=False, + indent=2) + if ret: + self._modified = False return ret def _norm_key_elem(self, elem): @@ -729,10 +801,7 @@ class Cfg: } # set the link flag - if link == LinkTypes.PARENT: - dots[dotfile.key][self.key_dotfiles_link] = True - elif link == LinkTypes.CHILDREN: - dots[dotfile.key][self.key_dotfiles_link_children] = True + dots[dotfile.key][self.key_dotfiles_link] = link.name # link it to this profile in the yaml file pro = self.content[self.key_profiles][profile] @@ -759,7 +828,13 @@ class Cfg: def get_settings(self): """return all defined settings""" - return self.lnk_settings.copy() + settings = self.lnk_settings.copy() + # patch link entries + key = self.key_imp_link + settings[key] = self._string_to_linktype(settings[key]) + key = self.key_dotfile_link + settings[key] = self._string_to_linktype(settings[key]) + return settings def get_variables(self, profile, debug=False): """return the variables for this profile""" @@ -865,6 +940,10 @@ class Cfg: self.lnk_settings[self.key_workdir] = workdir return ret + def is_modified(self): + """need the db to be saved""" + return self._modified + def save(self): """save the config to file""" # temporary reset paths diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 45c0aca..cc66e55 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -239,7 +239,7 @@ def cmd_importer(o): # create a new dotfile dotfile = Dotfile('', dst, src) - linktype = LinkTypes(o.link) + linktype = LinkTypes(o.import_link) if o.debug: LOG.dbg('new dotfile: {}'.format(dotfile)) @@ -453,6 +453,10 @@ def main(): LOG.err('interrupted') ret = False + if ret and o.conf.is_modified(): + LOG.log('config file updated') + o.conf.save() + return ret diff --git a/dotdrop/options.py b/dotdrop/options.py index b53b6af..409f6fe 100644 --- a/dotdrop/options.py +++ b/dotdrop/options.py @@ -63,7 +63,7 @@ Options: -t --temp Install to a temporary directory for review. -T --template Only template dotfiles. -D --showdiff Show a diff before overwriting. - -l --inv-link Invert "link_import_default" when importing. + -l --inv-link Invert "link_import_default". -P --show-patch Provide a one-liner to manually patch template. -f --force Do not warn if exists. -k --key Treat as a dotfile key. @@ -160,6 +160,8 @@ class Options(AttrMonitor): self.conf = Cfg(self.confpath, profile=profile, debug=self.debug) # transform the configs in attribute for k, v in self.conf.get_settings().items(): + if self.debug: + self.log.dbg('setting: {}={}'.format(k, v)) setattr(self, k, v) def _apply_args(self): @@ -176,16 +178,16 @@ class Options(AttrMonitor): # adapt attributes based on arguments self.dry = self.args['--dry'] self.safe = not self.args['--force'] - self.link = LinkTypes.NOLINK - if self.link_import_default: - self.link = LinkTypes.PARENT + # import link default value + self.import_link = self.link_import_default if self.args['--inv-link']: - # Only invert link type from NOLINK to PARENT and vice-versa - if self.link == LinkTypes.NOLINK: - self.link = LinkTypes.PARENT - elif self.link == LinkTypes.PARENT: - self.link = LinkTypes.NOLINK + if self.import_link == LinkTypes.NOLINK: + self.import_link = LinkTypes.PARENT + elif self.import_link == LinkTypes.PARENT: + self.import_link = LinkTypes.NOLINK + elif self.import_link == LinkTypes.CHILDREN: + self.import_link = LinkTypes.NOLINK # "listfiles" specifics self.listfiles_templateonly = self.args['--template'] diff --git a/tests-ng/deprecated-link.sh b/tests-ng/deprecated-link.sh new file mode 100755 index 0000000..584413c --- /dev/null +++ b/tests-ng/deprecated-link.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2017, deadc0de6 +# +# test migration from link/link_children to single entry +# returns 1 in case of error +# + +# exit on first error +set -e + +# all this crap to get current path +rl="readlink -f" +if ! ${rl} "${0}" >/dev/null 2>&1; then + rl="realpath" + + if ! hash ${rl}; then + echo "\"${rl}\" not found !" && exit 1 + fi +fi +cur=$(dirname "$(${rl} "${0}")") + +#hash dotdrop >/dev/null 2>&1 +#[ "$?" != "0" ] && echo "install dotdrop to run tests" && exit 1 + +#echo "called with ${1}" + +# dotdrop path can be pass as argument +ddpath="${cur}/../" +[ "${1}" != "" ] && ddpath="${1}" +[ ! -d ${ddpath} ] && echo "ddpath \"${ddpath}\" is not a directory" && exit 1 + +export PYTHONPATH="${ddpath}:${PYTHONPATH}" +bin="python3 -m dotdrop.dotdrop" + +echo "dotdrop path: ${ddpath}" +echo "pythonpath: ${PYTHONPATH}" + +# get the helpers +source ${cur}/helpers + +echo -e "\e[96m\e[1m==> RUNNING $(basename $BASH_SOURCE) <==\e[0m" + +################################################################ +# this is the test +################################################################ + +# the dotfile source +tmps=`mktemp -d --suffix='-dotdrop-tests'` +mkdir -p ${tmps}/dotfiles +# the dotfile destination +tmpd=`mktemp -d --suffix='-dotdrop-tests'` + +# create the config file +cfg="${tmps}/config.yaml" + +cat > ${cfg} << _EOF +config: + backup: false + create: true + dotpath: dotfiles + link_by_default: true +dotfiles: + f_link: + dst: ${tmpd}/abc + src: abc + link: true + f_link1: + dst: ${tmpd}/abc + src: abc + link: true + f_nolink: + dst: ${tmpd}/abc + src: abc + link: false + f_nolink1: + dst: ${tmpd}/abc + src: abc + link: false + f_children: + dst: ${tmpd}/abc + src: abc + link_children: true + f_children2: + dst: ${tmpd}/abc + src: abc + link_children: true + f_children3: + dst: ${tmpd}/abc + src: abc + link_children: false +profiles: + p1: + dotfiles: + - f_link + - f_nolink + - f_nolink1 + - f_children + - f_children2 + - f_children3 +_EOF +cat ${cfg} + +# create the dotfiles +echo "test" > ${tmps}/dotfiles/abc +echo "test" > ${tmpd}/abc + +# compare +cd ${ddpath} | ${bin} compare -c ${cfg} -p p1 +# install +#cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V + +cat ${cfg} + +# TODO test settings +# TODO change to safe_dump +# TODO yaml order + +# fail if find some of these entries +set +e +grep 'link_children: true' ${cfg} >/dev/null && exit 1 +grep 'link_children: false' ${cfg} >/dev/null && exit 1 +grep 'link: true' ${cfg} >/dev/null && exit 1 +grep 'link: false' ${cfg} >/dev/null && exit 1 +grep 'link_by_default: true' ${cfg} >/dev/null && exit 1 +grep 'link_by_default: false' ${cfg} >/dev/null && exit 1 +set -e + +# test values have been correctly updated +dotfiles=`cd ${ddpath} | ${bin} listfiles -c ${cfg} -p p1 | grep -v '^ '` +echo "${dotfiles}" | grep '^f_link ' | grep ', link: parent)' +echo "${dotfiles}" | grep '^f_nolink ' | grep ', link: nolink)' +echo "${dotfiles}" | grep '^f_nolink1 ' | grep ', link: nolink)' +echo "${dotfiles}" | grep '^f_children ' | grep ', link: children)' +echo "${dotfiles}" | grep '^f_children2 ' | grep ', link: children)' +echo "${dotfiles}" | grep '^f_children3 ' | grep ', link: nolink)' + +## CLEANING +rm -rf ${tmps} ${tmpd} + +echo "OK" +exit 0 diff --git a/tests-ng/inst-link-default.sh b/tests-ng/inst-link-default.sh index bc77706..a6811a6 100755 --- a/tests-ng/inst-link-default.sh +++ b/tests-ng/inst-link-default.sh @@ -2,7 +2,7 @@ # author: deadc0de6 (https://github.com/deadc0de6) # Copyright (c) 2017, deadc0de6 # -# test link_install_default +# test link_dotfile_default # returns 1 in case of error # @@ -54,8 +54,8 @@ tmpd=`mktemp -d --suffix='-dotdrop-tests'` # create the dotfile mkdir -p ${tmps}/dotfiles/abc -echo "test link_install_default 1" > ${tmps}/dotfiles/abc/file1 -echo "test link_install_default 2" > ${tmps}/dotfiles/abc/file2 +echo "test link_dotfile_default 1" > ${tmps}/dotfiles/abc/file1 +echo "test link_dotfile_default 2" > ${tmps}/dotfiles/abc/file2 # create a shell script # create the config file @@ -66,7 +66,7 @@ config: backup: true create: true dotpath: dotfiles - link_install_default: nolink + link_dotfile_default: nolink dotfiles: d_abc: dst: ${tmpd}/abc @@ -96,7 +96,7 @@ config: backup: true create: true dotpath: dotfiles - link_install_default: link + link_dotfile_default: link dotfiles: d_abc: dst: ${tmpd}/abc @@ -125,7 +125,7 @@ config: backup: true create: true dotpath: dotfiles - link_install_default: link_children + link_dotfile_default: link_children dotfiles: d_abc: dst: ${tmpd}/abc diff --git a/tests/helpers.py b/tests/helpers.py index d43d1f2..7eac3b1 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -118,14 +118,13 @@ def load_options(confpath, profile): args['--cfg'] = confpath args['--profile'] = profile # and get the options - # TODO need to patch options o = Options(args=args) o.profile = profile o.dry = False o.profile = profile o.safe = True o.install_diff = True - o.link = LinkTypes.NOLINK.value + o.import_link = LinkTypes.NOLINK o.install_showdiff = True o.debug = True if ENV_NODEBUG in os.environ: diff --git a/tests/test_config.py b/tests/test_config.py index 460d6c7..4b45013 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -6,11 +6,15 @@ basic unittest for the config parser import unittest +from unittest.mock import patch import os import yaml from dotdrop.config import Cfg -from tests.helpers import get_tempdir, clean, create_fake_config +from dotdrop.options import Options +from dotdrop.linktypes import LinkTypes +from tests.helpers import get_tempdir, clean, \ + create_fake_config, _fake_args class TestConfig(unittest.TestCase): @@ -45,6 +49,76 @@ class TestConfig(unittest.TestCase): self.assertTrue(conf._is_valid()) self.assertTrue(conf.dump() != '') + def test_def_link(self): + self._test_link_import('nolink', LinkTypes.NOLINK, False) + self._test_link_import('link', LinkTypes.PARENT, False) + self._test_link_import('nolink', LinkTypes.PARENT, True) + self._test_link_import('link', LinkTypes.NOLINK, True) + self._test_link_import_fail('whatever') + self._test_link_import_fail('link_children') + + @patch('dotdrop.config.open', create=True) + @patch('dotdrop.config.os.path.exists', create=True) + def _test_link_import(self, cfgstring, expected, + invert, mock_exists, mock_open): + data = ''' +config: + backup: true + create: true + dotpath: dotfiles + banner: true + longkey: false + keepdot: false + link_import_default: {} + link_dotfile_default: nolink +dotfiles: +profiles: + '''.format(cfgstring) + + mock_open.side_effect = [ + unittest.mock.mock_open(read_data=data).return_value + ] + mock_exists.return_value = True + + args = _fake_args() + args['--profile'] = 'p1' + args['--cfg'] = 'mocked' + if invert: + args['--inv-link'] = True + o = Options(args=args) + + self.assertTrue(o.import_link == expected) + + @patch('dotdrop.config.open', create=True) + @patch('dotdrop.config.os.path.exists', create=True) + def _test_link_import_fail(self, value, mock_exists, mock_open): + data = ''' +config: + backup: true + create: true + dotpath: dotfiles + banner: true + longkey: false + keepdot: false + link_import_default: {} + link_dotfile_default: nolink +dotfiles: +profiles: + '''.format(value) + + mock_open.side_effect = [ + unittest.mock.mock_open(read_data=data).return_value + ] + mock_exists.return_value = True + + args = _fake_args() + args['--profile'] = 'p1' + args['--cfg'] = 'mocked' + + with self.assertRaisesRegex(ValueError, 'config is not valid'): + o = Options(args=args) + print(o.import_link) + def test_include(self): tmp = get_tempdir() self.assertTrue(os.path.exists(tmp)) diff --git a/tests/test_import.py b/tests/test_import.py index 28ae7b6..7116120 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -112,11 +112,11 @@ class TestImport(unittest.TestCase): o.import_path = dfiles cmd_importer(o) # import symlink - o.link = LinkTypes.PARENT + o.import_link = LinkTypes.PARENT sfiles = [dotfile6, dotfile7] o.import_path = sfiles cmd_importer(o) - o.link = LinkTypes.NOLINK + o.import_link = LinkTypes.NOLINK # reload the config o = load_options(confpath, profile)