diff --git a/completion/_dotdrop-completion.zsh b/completion/_dotdrop-completion.zsh index 2f694da..7062241 100644 --- a/completion/_dotdrop-completion.zsh +++ b/completion/_dotdrop-completion.zsh @@ -38,6 +38,7 @@ _dotdrop () 'import' 'compare' 'update' + 'remove' 'listfiles' 'detail' 'list' @@ -59,6 +60,9 @@ _dotdrop () update) _dotdrop-update ;; + remove) + _dotdrop-remove + ;; listfiles) _dotdrop-listfiles ;; @@ -97,7 +101,7 @@ _dotdrop-install () '(-D)-D' \ '(--showdiff)--showdiff' \ '(-a)-a' \ - '(--force-action)--force-action' \ + '(--force-actions)--force-actions' \ '(-c=-)-c=-' \ '(--cfg=-)--cfg=-' \ '(-p=-)-p=-' \ @@ -193,6 +197,35 @@ _dotdrop-update () fi } +_dotdrop-remove () +{ + local context state state_descr line + typeset -A opt_args + + if [[ $words[$CURRENT] == -* ]] ; then + _arguments -C \ + ':command:->command' \ + '(-V)-V' \ + '(--verbose)--verbose' \ + '(-b)-b' \ + '(--no-banner)--no-banner' \ + '(-f)-f' \ + '(--force)--force' \ + '(-d)-d' \ + '(--dry)--dry' \ + '(-k)-k' \ + '(--key)--key' \ + '(-c=-)-c=-' \ + '(--cfg=-)--cfg=-' \ + '(-p=-)-p=-' \ + '(--profile=-)--profile=-' \ + + else + myargs=('') + _message_next_arg + fi +} + _dotdrop-listfiles () { local context state state_descr line diff --git a/completion/_dotdrop.sh-completion.zsh b/completion/_dotdrop.sh-completion.zsh index 3357e75..a123621 100644 --- a/completion/_dotdrop.sh-completion.zsh +++ b/completion/_dotdrop.sh-completion.zsh @@ -38,6 +38,7 @@ _dotdrop.sh () 'import' 'compare' 'update' + 'remove' 'listfiles' 'detail' 'list' @@ -59,6 +60,9 @@ _dotdrop.sh () update) _dotdrop.sh-update ;; + remove) + _dotdrop.sh-remove + ;; listfiles) _dotdrop.sh-listfiles ;; @@ -97,7 +101,7 @@ _dotdrop.sh-install () '(-D)-D' \ '(--showdiff)--showdiff' \ '(-a)-a' \ - '(--force-action)--force-action' \ + '(--force-actions)--force-actions' \ '(-c=-)-c=-' \ '(--cfg=-)--cfg=-' \ '(-p=-)-p=-' \ @@ -193,6 +197,35 @@ _dotdrop.sh-update () fi } +_dotdrop.sh-remove () +{ + local context state state_descr line + typeset -A opt_args + + if [[ $words[$CURRENT] == -* ]] ; then + _arguments -C \ + ':command:->command' \ + '(-V)-V' \ + '(--verbose)--verbose' \ + '(-b)-b' \ + '(--no-banner)--no-banner' \ + '(-f)-f' \ + '(--force)--force' \ + '(-d)-d' \ + '(--dry)--dry' \ + '(-k)-k' \ + '(--key)--key' \ + '(-c=-)-c=-' \ + '(--cfg=-)--cfg=-' \ + '(-p=-)-p=-' \ + '(--profile=-)--profile=-' \ + + else + myargs=('') + _message_next_arg + fi +} + _dotdrop.sh-listfiles () { local context state state_descr line diff --git a/completion/dotdrop-completion.bash b/completion/dotdrop-completion.bash index 261a2be..92f2ffb 100644 --- a/completion/dotdrop-completion.bash +++ b/completion/dotdrop-completion.bash @@ -5,7 +5,7 @@ _dotdrop() cur="${COMP_WORDS[COMP_CWORD]}" if [ $COMP_CWORD -eq 1 ]; then - COMPREPLY=( $( compgen -W '-h --help -v --version install import compare update listfiles detail list' -- $cur) ) + COMPREPLY=( $( compgen -W '-h --help -v --version install import compare update remove listfiles detail list' -- $cur) ) else case ${COMP_WORDS[1]} in install) @@ -19,6 +19,9 @@ _dotdrop() ;; update) _dotdrop_update + ;; + remove) + _dotdrop_remove ;; listfiles) _dotdrop_listfiles @@ -40,7 +43,7 @@ _dotdrop_install() cur="${COMP_WORDS[COMP_CWORD]}" if [ $COMP_CWORD -ge 2 ]; then - COMPREPLY=( $( compgen -fW '-V --verbose -b --no-banner -t --temp -f --force -n --nodiff -d --dry -D --showdiff -a --force-action -c= --cfg= -p= --profile= ' -- $cur) ) + COMPREPLY=( $( compgen -fW '-V --verbose -b --no-banner -t --temp -f --force -n --nodiff -d --dry -D --showdiff -a --force-actions -c= --cfg= -p= --profile= ' -- $cur) ) fi } @@ -74,6 +77,16 @@ _dotdrop_update() fi } +_dotdrop_remove() +{ + local cur + cur="${COMP_WORDS[COMP_CWORD]}" + + if [ $COMP_CWORD -ge 2 ]; then + COMPREPLY=( $( compgen -fW '-V --verbose -b --no-banner -f --force -d --dry -k --key -c= --cfg= -p= --profile= ' -- $cur) ) + fi +} + _dotdrop_listfiles() { local cur diff --git a/completion/dotdrop.sh-completion.bash b/completion/dotdrop.sh-completion.bash index 98ece6c..c6e0760 100644 --- a/completion/dotdrop.sh-completion.bash +++ b/completion/dotdrop.sh-completion.bash @@ -5,7 +5,7 @@ _dotdropsh() cur="${COMP_WORDS[COMP_CWORD]}" if [ $COMP_CWORD -eq 1 ]; then - COMPREPLY=( $( compgen -W '-h --help -v --version install import compare update listfiles detail list' -- $cur) ) + COMPREPLY=( $( compgen -W '-h --help -v --version install import compare update remove listfiles detail list' -- $cur) ) else case ${COMP_WORDS[1]} in install) @@ -19,6 +19,9 @@ _dotdropsh() ;; update) _dotdropsh_update + ;; + remove) + _dotdropsh_remove ;; listfiles) _dotdropsh_listfiles @@ -40,7 +43,7 @@ _dotdropsh_install() cur="${COMP_WORDS[COMP_CWORD]}" if [ $COMP_CWORD -ge 2 ]; then - COMPREPLY=( $( compgen -fW '-V --verbose -b --no-banner -t --temp -f --force -n --nodiff -d --dry -D --showdiff -a --force-action -c= --cfg= -p= --profile= ' -- $cur) ) + COMPREPLY=( $( compgen -fW '-V --verbose -b --no-banner -t --temp -f --force -n --nodiff -d --dry -D --showdiff -a --force-actions -c= --cfg= -p= --profile= ' -- $cur) ) fi } @@ -74,6 +77,16 @@ _dotdropsh_update() fi } +_dotdropsh_remove() +{ + local cur + cur="${COMP_WORDS[COMP_CWORD]}" + + if [ $COMP_CWORD -ge 2 ]; then + COMPREPLY=( $( compgen -fW '-V --verbose -b --no-banner -f --force -d --dry -k --key -c= --cfg= -p= --profile= ' -- $cur) ) + fi +} + _dotdropsh_listfiles() { local cur diff --git a/dotdrop/cfg_aggregator.py b/dotdrop/cfg_aggregator.py index 142d98d..07a4407 100644 --- a/dotdrop/cfg_aggregator.py +++ b/dotdrop/cfg_aggregator.py @@ -19,6 +19,9 @@ from dotdrop.logger import Logger from dotdrop.utils import strip_home +TILD = '~' + + class CfgAggregator: file_prefix = 'f' @@ -99,9 +102,9 @@ class CfgAggregator: # patch trans_w/trans_r in dotfiles self._patch_keys_to_objs(self.dotfiles, - "trans_r", self.get_trans_r) + "trans_r", self._get_trans_r) self._patch_keys_to_objs(self.dotfiles, - "trans_w", self.get_trans_w) + "trans_w", self._get_trans_w) def _patch_keys_to_objs(self, containers, keys, get_by_key): """ @@ -128,6 +131,14 @@ class CfgAggregator: self.log.dbg('patching {}.{} with {}'.format(c, keys, objects)) setattr(c, keys, objects) + def del_dotfile(self, dotfile): + """remove this dotfile from the config""" + return self.cfgyaml.del_dotfile(dotfile.key) + + def del_dotfile_from_profile(self, dotfile, profile): + """remove this dotfile from this profile""" + return self.cfgyaml.del_dotfile_from_profile(dotfile.key, profile.key) + def new(self, src, dst, link, profile_key): """ import a new dotfile @@ -136,10 +147,9 @@ class CfgAggregator: @link: LinkType @profile_key: to which profile """ - home = os.path.expanduser('~') - dst = dst.replace(home, '~', 1) + dst = self.path_to_dotfile_dst(dst) - dotfile = self._get_dotfile_by_dst(dst) + dotfile = self.get_dotfile_by_dst(dst) if not dotfile: # get a new dotfile with a unique key key = self._get_new_dotfile_key(dst) @@ -228,8 +238,22 @@ class CfgAggregator: cnt += 1 return newkey - def _get_dotfile_by_dst(self, dst): + def path_to_dotfile_dst(self, path): + """normalize the path to match dotfile dst""" + path = os.path.expanduser(path) + path = os.path.expandvars(path) + path = os.path.abspath(path) + home = os.path.expanduser(TILD) + os.sep + + # normalize the path + if path.startswith(home): + path = path[len(home):] + path = os.path.join(TILD, path) + return path + + def get_dotfile_by_dst(self, dst): """get a dotfile by dst""" + dst = self.path_to_dotfile_dst(dst) try: return next(d for d in self.dotfiles if d.dst == dst) except StopIteration: @@ -262,12 +286,24 @@ class CfgAggregator: except StopIteration: return None + def get_profiles_by_dotfile_key(self, key): + """return all profiles having this dotfile""" + res = [] + for p in self.profiles: + keys = [d.key for d in p.dotfiles] + if key in keys: + res.append(p) + return res + def get_dotfiles(self, profile=None): """return dotfiles dict for this profile key""" if not profile: return self.dotfiles try: - return next(x.dotfiles for x in self.profiles if x.key == profile) + pro = self.get_profile(profile) + if not pro: + return [] + return pro.dotfiles except StopIteration: return [] @@ -278,7 +314,7 @@ class CfgAggregator: except StopIteration: return None - def get_action(self, key): + def _get_action(self, key): """return action by key""" try: return next(x for x in self.actions if x.key == key) @@ -293,19 +329,19 @@ class CfgAggregator: key, *args = fields if self.debug: self.log.dbg('action with parm: {} and {}'.format(key, args)) - action = self.get_action(key).copy(args) + action = self._get_action(key).copy(args) else: - action = self.get_action(key) + action = self._get_action(key) return action - def get_trans_r(self, key): + def _get_trans_r(self, key): """return the trans_r with this key""" try: return next(x for x in self.trans_r if x.key == key) except StopIteration: return None - def get_trans_w(self, key): + def _get_trans_w(self, key): """return the trans_w with this key""" try: return next(x for x in self.trans_w if x.key == key) diff --git a/dotdrop/cfg_yaml.py b/dotdrop/cfg_yaml.py index 4199391..e7bea99 100644 --- a/dotdrop/cfg_yaml.py +++ b/dotdrop/cfg_yaml.py @@ -8,6 +8,7 @@ handle lower level of the config file import os import yaml import glob +from copy import deepcopy # local imports from dotdrop.settings import Settings @@ -97,9 +98,10 @@ class CfgYaml: def _parse_main_yaml(self, dic): """parse the different blocks""" - self.ori_settings = self._get_entry(self.yaml_dict, self.key_settings) + 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) + # resolve settings paths p = self._resolve_path(self.settings[self.key_settings_dotpath]) self.settings[self.key_settings_dotpath] = p @@ -109,50 +111,60 @@ class CfgYaml: self.log.dbg('settings: {}'.format(self.settings)) # dotfiles - self.dotfiles = self._get_entry(self.yaml_dict, self.key_dotfiles) + self.ori_dotfiles = self._get_entry(dic, self.key_dotfiles) + self.dotfiles = deepcopy(self.ori_dotfiles) + keys = self.dotfiles.keys() + if len(keys) != len(list(set(keys))): + dups = [x for x in keys if x not in list(set(keys))] + raise Exception('duplicate dotfile keys found: {}'.format(dups)) self.dotfiles = self._norm_dotfiles(self.dotfiles) if self.debug: self.log.dbg('dotfiles: {}'.format(self.dotfiles)) # profiles - self.profiles = self._get_entry(self.yaml_dict, self.key_profiles) + self.ori_profiles = self._get_entry(dic, self.key_profiles) + self.profiles = deepcopy(self.ori_profiles) if self.debug: self.log.dbg('profiles: {}'.format(self.profiles)) # actions - self.actions = self._get_entry(self.yaml_dict, self.key_actions, - mandatory=False) + 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: self.log.dbg('actions: {}'.format(self.actions)) # trans_r - if self.old_key_trans_r in self.yaml_dict: + key = self.key_trans_r + if self.old_key_trans_r in dic: self.log.warn('\"trans\" is deprecated, please use \"trans_read\"') - self.yaml_dict[self.key_trans_r] = self.yaml_dict.pop( - self.old_key_trans_r - ) - self.dirty = True - self.trans_r = self._get_entry(self.yaml_dict, self.key_trans_r, - mandatory=False) + key = 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: self.log.dbg('trans_r: {}'.format(self.trans_r)) # trans_w - self.trans_w = self._get_entry(self.yaml_dict, self.key_trans_w, - mandatory=False) + 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: self.log.dbg('trans_w: {}'.format(self.trans_w)) # variables - self.variables = self._get_entry(self.yaml_dict, self.key_variables, - mandatory=False) + self.ori_variables = self._get_entry(dic, + self.key_variables, + mandatory=False) + self.variables = deepcopy(self.ori_variables) if self.debug: self.log.dbg('variables: {}'.format(self.variables)) # dynvariables - self.dvariables = self._get_entry(self.yaml_dict, self.key_dvariables, - mandatory=False) + self.ori_dvariables = self._get_entry(dic, + self.key_dvariables, + mandatory=False) + self.dvariables = deepcopy(self.ori_dvariables) if self.debug: self.log.dbg('dvariables: {}'.format(self.dvariables)) @@ -493,7 +505,7 @@ class CfgYaml: values[self.key_profiles_dotfiles] = uniq_list(current) if self.debug: dfs = values[self.key_profiles_dotfiles] - self.log.dbg('profile dfs after include: {}'.format(dfs)) + self.log.dbg('{} dfs after include: {}'.format(profile, dfs)) return values.get(self.key_profiles_dotfiles, []) def _resolve_path(self, path): @@ -538,21 +550,21 @@ class CfgYaml: """merge low into high""" # won't work in python3.4 # return {**low, **high} - new = low.copy() + new = deepcopy(low) new.update(high) return new - def _get_entry(self, yaml_dict, key, mandatory=True): + def _get_entry(self, dic, key, mandatory=True): """return entry from yaml dictionary""" - if key not in yaml_dict: + if key not in dic: if mandatory: raise Exception('invalid config: no {} found'.format(key)) - yaml_dict[key] = {} - return yaml_dict[key] - if mandatory and not yaml_dict[key]: + dic[key] = {} + return dic[key] + if mandatory and not dic[key]: # ensure is not none - yaml_dict[key] = {} - return yaml_dict[key] + dic[key] = {} + return dic[key] def _load_yaml(self, path): """load a yaml file to a dict""" @@ -609,6 +621,40 @@ class CfgYaml: 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 profiles: {}'.format(pro_key)) + return False + profiles = self.yaml_dict[self.key_profiles][pro_key] + if self.debug: + dfs = profiles[self.key_profiles_dotfiles] + self.log.dbg('{} profile dotfiles: {}'.format(pro_key, dfs)) + self.log.dbg('remove {} from profile {}'.format(df_key, pro_key)) + profiles[self.key_profiles_dotfiles].remove(df_key) + if self.debug: + dfs = profiles[self.key_profiles_dotfiles] + self.log.dbg('{} profile dotfiles: {}'.format(pro_key, dfs)) + self.dirty = True + return True + def _fix_deprecated(self, yamldict): """fix deprecated entries""" self._fix_deprecated_link_by_default(yamldict) @@ -671,9 +717,9 @@ class CfgYaml: newv = v if isinstance(v, dict): newv = self._clear_none(v) - if v is None: + if newv is None: continue - if not v: + if not newv: continue new[k] = newv return new diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index b95cc64..1721d46 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -268,7 +268,10 @@ def cmd_update(o): if o.debug: LOG.dbg('dotfile to update: {}'.format(paths)) - updater = Updater(o.dotpath, o.dotfiles, o.variables, + updater = Updater(o.dotpath, o.variables, + o.conf.get_dotfile, + o.conf.get_dotfile_by_dst, + o.conf.path_to_dotfile_dst, dry=o.dry, safe=o.safe, debug=o.debug, ignore=ignore, showpatch=showpatch) if not iskey: @@ -403,6 +406,67 @@ def cmd_detail(o): LOG.log('') +def cmd_remove(o): + """remove dotfile from dotpath and from config""" + paths = o.remove_path + iskey = o.remove_iskey + + if not paths: + LOG.log('no dotfile to remove') + return False + if o.debug: + LOG.dbg('dotfile to remove: {}'.format(paths)) + + removed = [] + for key in paths: + if o.debug: + LOG.dbg('removing {}'.format(key)) + if not iskey: + # by path + dotfile = o.conf.get_dotfile_by_dst(key) + if not dotfile: + LOG.warn('{} ignored, does not exist'.format(key)) + continue + k = dotfile.key + else: + # by key + dotfile = o.conf.get_dotfile(key) + k = key + # make sure is part of the profile + if dotfile.key not in [d.key for d in o.dotfiles]: + LOG.warn('{} ignored, not associated to this profile'.format(key)) + continue + profiles = o.conf.get_profiles_by_dotfile_key(k) + pkeys = ','.join([p.key for p in profiles]) + if o.dry: + LOG.dry('would remove {} from {}'.format(dotfile, pkeys)) + continue + msg = 'Remove dotfile from all these profiles: {}'.format(pkeys) + if o.safe and not LOG.ask(msg): + return False + if o.debug: + LOG.dbg('remove dotfile: {}'.format(dotfile)) + + for profile in profiles: + if not o.conf.del_dotfile_from_profile(dotfile, profile): + return False + if not o.conf.del_dotfile(dotfile): + return False + + # remove dotfile from dotpath + dtpath = os.path.join(o.dotpath, dotfile.src) + remove(dtpath) + removed.append(dotfile.key) + + if o.dry: + LOG.dry('new config file would be:') + LOG.raw(o.conf.dump()) + else: + o.conf.save() + LOG.log('\ndotfile(s) removed: {}'.format(','.join(removed))) + return True + + ########################################################### # helpers ########################################################### @@ -444,8 +508,10 @@ def _select(selections, dotfiles): def apply_trans(dotpath, dotfile, debug=False): - """apply the read transformation to the dotfile - return None if fails and new source if succeed""" + """ + apply the read transformation to the dotfile + return None if fails and new source if succeed + """ src = dotfile.src new_src = '{}.{}'.format(src, TRANS_SUFFIX) for trans in dotfile.trans_r: @@ -523,6 +589,12 @@ def main(): LOG.dbg('running cmd: detail') cmd_detail(o) + elif o.cmd_remove: + # remove dotfile + if o.debug: + LOG.dbg('running cmd: remove') + cmd_remove(o) + except KeyboardInterrupt: LOG.err('interrupted') ret = False diff --git a/dotdrop/options.py b/dotdrop/options.py index 8e69670..6ba7d6c 100644 --- a/dotdrop/options.py +++ b/dotdrop/options.py @@ -57,6 +57,7 @@ Usage: [-o ] [-C ...] [-i ...] dotdrop update [-VbfdkP] [-c ] [-p ] [-i ...] [...] + dotdrop remove [-Vbfdk] [-c ] [-p ] [...] dotdrop listfiles [-VbT] [-c ] [-p ] dotdrop detail [-Vb] [-c ] [-p ] [...] dotdrop list [-Vb] [-c ] @@ -193,6 +194,7 @@ class Options(AttrMonitor): self.cmd_import = self.args['import'] self.cmd_update = self.args['update'] self.cmd_detail = self.args['detail'] + self.cmd_remove = self.args['remove'] # adapt attributes based on arguments self.dry = self.args['--dry'] @@ -236,6 +238,9 @@ class Options(AttrMonitor): self.update_showpatch = self.args['--show-patch'] # "detail" specifics self.detail_keys = self.args[''] + # "remove" specifics + self.remove_path = self.args[''] + self.remove_iskey = self.args['--key'] def _fill_attr(self): """create attributes from conf""" diff --git a/dotdrop/updater.py b/dotdrop/updater.py index 5524d5c..1ee2148 100644 --- a/dotdrop/updater.py +++ b/dotdrop/updater.py @@ -20,12 +20,17 @@ TILD = '~' class Updater: - def __init__(self, dotpath, dotfiles, variables, dry=False, safe=True, + def __init__(self, dotpath, variables, + dotfile_key_getter, dotfile_dst_getter, + dotfile_path_normalizer, + dry=False, safe=True, debug=False, ignore=[], showpatch=False): """constructor @dotpath: path where dotfiles are stored - @dotfiles: dotfiles for this profile @variables: dictionary of variables for the templates + @dotfile_key_getter: func to get a dotfile by key + @dotfile_dst_getter: func to get a dotfile by dst + @dotfile_path_normalizer: func to normalize dotfile dst @dry: simulate @safe: ask for overwrite if True @debug: enable debug @@ -33,8 +38,10 @@ class Updater: @showpatch: show patch if dotfile to update is a template """ self.dotpath = dotpath - self.dotfiles = dotfiles self.variables = variables + self.dotfile_key_getter = dotfile_key_getter + self.dotfile_dst_getter = dotfile_dst_getter + self.dotfile_path_normalizer = dotfile_path_normalizer self.dry = dry self.safe = safe self.debug = debug @@ -48,8 +55,7 @@ class Updater: if not os.path.lexists(path): self.log.err('\"{}\" does not exist!'.format(path)) return False - path = self._normalize(path) - dotfile = self._get_dotfile_by_path(path) + dotfile = self.dotfile_dst_getter(path) if not dotfile: return False if self.debug: @@ -58,12 +64,12 @@ class Updater: def update_key(self, key): """update the dotfile referenced by key""" - dotfile = self._get_dotfile_by_key(key) + dotfile = self.dotfile_key_getter(key) if not dotfile: return False if self.debug: self.log.dbg('updating {} from key \"{}\"'.format(dotfile, key)) - path = self._normalize(dotfile.dst) + path = self.dotfile_path_normalizer(dotfile.dst) return self._update(path, dotfile) def _update(self, path, dotfile): @@ -111,45 +117,6 @@ class Updater: return None return tmp - def _normalize(self, path): - """normalize the path to match dotfile""" - path = os.path.expanduser(path) - path = os.path.expandvars(path) - path = os.path.abspath(path) - home = os.path.expanduser(TILD) + os.sep - - # normalize the path - if path.startswith(home): - path = path[len(home):] - path = os.path.join(TILD, path) - return path - - def _get_dotfile_by_key(self, key): - """get the dotfile matching this key""" - dotfiles = self.dotfiles - subs = [d for d in dotfiles if d.key == key] - if not subs: - self.log.err('key \"{}\" not found!'.format(key)) - return None - if len(subs) > 1: - found = ','.join([d.src for d in dotfiles]) - self.log.err('multiple dotfiles found: {}'.format(found)) - return None - return subs[0] - - def _get_dotfile_by_path(self, path): - """get the dotfile matching this path""" - dotfiles = self.dotfiles - subs = [d for d in dotfiles if d.dst == path] - if not subs: - self.log.err('\"{}\" is not managed!'.format(path)) - return None - if len(subs) > 1: - found = ','.join([d.src for d in dotfiles]) - self.log.err('multiple dotfiles found: {}'.format(found)) - return None - return subs[0] - def _is_template(self, path): if not Templategen.is_template(path): if self.debug: diff --git a/tests-ng/remove.sh b/tests-ng/remove.sh new file mode 100755 index 0000000..01d4966 --- /dev/null +++ b/tests-ng/remove.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2019, deadc0de6 +# +# test remove +# 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 +################################################################ + +# dotdrop directory +tmps=`mktemp -d --suffix='-dotdrop-tests'` +mkdir -p ${tmps}/dotfiles +# the dotfile to be imported +tmpd=`mktemp -d --suffix='-dotdrop-tests'` + +# create the config file +cfg="${tmps}/config.yaml" +cat > ${cfg} << _EOF +config: + backup: true + create: true + dotpath: dotfiles +dotfiles: + f_abc: + dst: ${tmpd}/abc + src: abc + f_def: + dst: ${tmpd}/def + src: def + f_last: + dst: ${tmpd}/last + src: last +profiles: + p1: + dotfiles: + - f_abc + - f_def + p2: + dotfiles: + - f_def + last: + dotfiles: + - f_last +_EOF +cfgbak="${tmps}/config.yaml.bak" +cp ${cfg} ${cfgbak} + +# create the dotfile +echo "abc" > ${tmps}/dotfiles/abc +echo "abc" > ${tmpd}/abc + +echo "def" > ${tmps}/dotfiles/def +echo "def" > ${tmpd}/def + +# remove with bad profile +cd ${ddpath} | ${bin} remove -f -k -p empty -c ${cfg} f_abc -V +[ ! -e ${tmps}/dotfiles/abc ] && echo "dotfile in dotpath deleted" && exit 1 +[ ! -e ${tmpd}/abc ] && echo "source dotfile deleted" && exit 1 +[ ! -e ${tmps}/dotfiles/def ] && echo "dotfile in dotpath deleted" && exit 1 +[ ! -e ${tmpd}/def ] && echo "source dotfile deleted" && exit 1 +# ensure config not altered +diff ${cfg} ${cfgbak} + +# remove by key +echo "[+] remove f_abc by key" +cd ${ddpath} | ${bin} remove -p p1 -f -k -c ${cfg} f_abc -V +cat ${cfg} +echo "[+] remove f_def by key" +cd ${ddpath} | ${bin} remove -p p2 -f -k -c ${cfg} f_def -V +cat ${cfg} + +# checks +[ -e ${tmps}/dotfiles/abc ] && echo "dotfile in dotpath not deleted" && exit 1 +[ ! -e ${tmpd}/abc ] && echo "source dotfile deleted" && exit 1 + +[ -e ${tmps}/dotfiles/def ] && echo "dotfile in dotpath not deleted" && exit 1 +[ ! -e ${tmpd}/def ] && echo "source dotfile deleted" && exit 1 + +echo "[+] =========" + +# create the config file +cfg="${tmps}/config.yaml" +cat > ${cfg} << _EOF +config: + backup: true + create: true + dotpath: dotfiles +dotfiles: + f_abc: + dst: ${tmpd}/abc + src: abc + f_def: + dst: ${tmpd}/def + src: def + f_last: + dst: ${tmpd}/last + src: last +profiles: + p1: + dotfiles: + - f_abc + - f_def + p2: + dotfiles: + - f_def + last: + dotfiles: + - f_last +_EOF +cat ${cfg} + +# create the dotfile +echo "abc" > ${tmps}/dotfiles/abc +echo "abc" > ${tmpd}/abc + +echo "def" > ${tmps}/dotfiles/def +echo "def" > ${tmpd}/def + +# remove by key +echo "[+] remove f_abc by path" +cd ${ddpath} | ${bin} remove -p p1 -f -c ${cfg} ${tmpd}/abc -V +cat ${cfg} +echo "[+] remove f_def by path" +cd ${ddpath} | ${bin} remove -p p2 -f -c ${cfg} ${tmpd}/def -V +cat ${cfg} + +# checks +[ -e ${tmps}/dotfiles/abc ] && echo "(2) dotfile in dotpath not deleted" && exit 1 +[ ! -e ${tmpd}/abc ] && echo "(2) source dotfile deleted" && exit 1 + +[ -e ${tmps}/dotfiles/def ] && echo "(2) dotfile in dotpath not deleted" && exit 1 +[ ! -e ${tmpd}/def ] && echo "(2) source dotfile deleted" && exit 1 + + +cat ${cfg} + +## CLEANING +rm -rf ${tmps} ${tmpd} + +echo "OK" +exit 0 diff --git a/tests/helpers.py b/tests/helpers.py index 45ab61e..7bd57f6 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -136,6 +136,7 @@ def _fake_args(): args['import'] = False args['update'] = False args['detail'] = False + args['remove'] = False return args @@ -172,8 +173,13 @@ def get_dotfile_from_yaml(dic, path): # path is not the file in dotpath but on the FS dotfiles = dic['dotfiles'] # src = get_path_strip_version(path) - dotfile = [d for d in dotfiles.values() if d['dst'] == path][0] - return dotfile + home = os.path.expanduser('~') + if path.startswith(home): + path = path.replace(home, '~') + dotfile = [d for d in dotfiles.values() if d['dst'] == path] + if dotfile: + return dotfile[0] + return None def yaml_dashed_list(items, indent=0): @@ -261,6 +267,11 @@ def file_in_yaml(yaml_file, path, link=False): in_dst = path in (os.path.expanduser(x['dst']) for x in dotfiles) if link: - has_link = 'link' in get_dotfile_from_yaml(yaml_conf, path) + df = get_dotfile_from_yaml(yaml_conf, path) + has_link = False + if df: + has_link = 'link' in df + else: + return False return in_src and in_dst and has_link return in_src and in_dst diff --git a/tests/test_import.py b/tests/test_import.py index bbbfb75..98efdea 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -352,7 +352,7 @@ class TestImport(unittest.TestCase): self.assertFalse(any(a.endswith('ing') for a in actions)) # testing transformations - transformations = y['trans_read'].keys() + transformations = y['trans'].keys() self.assertTrue(all(t.endswith('ed') for t in transformations)) self.assertFalse(any(t.endswith('ing') for t in transformations)) transformations = y['trans_write'].keys() @@ -394,7 +394,7 @@ class TestImport(unittest.TestCase): self.assertFalse(any(action.endswith('ed') for action in actions)) # testing transformations - transformations = y['trans_read'].keys() + transformations = y['trans'].keys() self.assertTrue(all(t.endswith('ing') for t in transformations)) self.assertFalse(any(t.endswith('ed') for t in transformations)) transformations = y['trans_write'].keys() diff --git a/tests/test_install.py b/tests/test_install.py index 988ae3a..af68d97 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -486,7 +486,6 @@ exec bspwm # ensure dst is link self.assertTrue(os.path.islink(dst)) # ensure dst not directly linked to src - # TODO: maybe check that its actually linked to template folder self.assertNotEqual(os.path.realpath(dst), src)