1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-05 17:18:53 +00:00
This commit is contained in:
deadc0de6
2021-04-30 21:29:12 +02:00
parent cb71bf299f
commit 6a51a7abad
6 changed files with 188 additions and 163 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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['<config.yaml>'])
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)