1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-05 16:43:55 +00:00

adding "remove" option for #47 and fix a few issues/bugs

This commit is contained in:
deadc0de6
2019-06-02 18:18:52 +02:00
parent 44db88ce74
commit 4ed7b4d78c
13 changed files with 509 additions and 102 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -57,6 +57,7 @@ Usage:
[-o <opts>] [-C <file>...] [-i <pattern>...]
dotdrop update [-VbfdkP] [-c <path>] [-p <profile>]
[-i <pattern>...] [<path>...]
dotdrop remove [-Vbfdk] [-c <path>] [-p <profile>] [<path>...]
dotdrop listfiles [-VbT] [-c <path>] [-p <profile>]
dotdrop detail [-Vb] [-c <path>] [-p <profile>] [<key>...]
dotdrop list [-Vb] [-c <path>]
@@ -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['<key>']
# "remove" specifics
self.remove_path = self.args['<path>']
self.remove_iskey = self.args['--key']
def _fill_attr(self):
"""create attributes from conf"""

View File

@@ -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: