From c82cc56dcf80f1eb9e06b2294b0f35adfe3866b5 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Sun, 10 Feb 2019 20:59:14 +0100 Subject: [PATCH] wrap conf and args into a new Options class --- dotdrop/config.py | 24 +-- dotdrop/dotdrop.py | 321 ++++++++++++++--------------------------- dotdrop/options.py | 169 ++++++++++++++++++++++ dotdrop/updater.py | 14 +- tests/helpers.py | 66 +++++++-- tests/test_compare.py | 39 ++--- tests/test_import.py | 47 +++--- tests/test_install.py | 24 +-- tests/test_listings.py | 18 ++- tests/test_update.py | 26 ++-- 10 files changed, 435 insertions(+), 313 deletions(-) create mode 100644 dotdrop/options.py diff --git a/dotdrop/config.py b/dotdrop/config.py index 12eb8fb..aabf76e 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -138,6 +138,7 @@ class Cfg: if self.key_actions_post in d.actions: for action in d.actions[self.key_actions_post]: action.action = t.generate_string(action.action) + return self.get_dotfiles(profile) def _load_file(self): """load the yaml file""" @@ -335,14 +336,24 @@ class Cfg: # make sure we have an absolute dotpath self.curdotpath = self.lnk_settings[self.key_dotpath] - self.lnk_settings[self.key_dotpath] = self.abs_or_rel(self.curdotpath) + self.lnk_settings[self.key_dotpath] = \ + self._abs_path(self.curdotpath) # make sure we have an absolute workdir self.curworkdir = self.lnk_settings[self.key_workdir] - self.lnk_settings[self.key_workdir] = self.abs_or_rel(self.curworkdir) + self.lnk_settings[self.key_workdir] = \ + self._abs_path(self.curworkdir) return True + def _abs_path(self, path): + """return absolute path of path relative to the confpath""" + path = os.path.expanduser(path) + if not os.path.isabs(path): + d = os.path.dirname(self.cfgpath) + return os.path.join(d, path) + return path + def _get_included_dotfiles(self, profile): """find all dotfiles for a specific profile when using the include keyword""" @@ -427,15 +438,6 @@ class Cfg: if self.key_ignoreempty not in self.lnk_settings: self.lnk_settings[self.key_ignoreempty] = self.default_ignoreempty - def abs_or_rel(self, path): - """path is either absolute or relative to the config path""" - path = os.path.expanduser(path) - if not os.path.isabs(path): - absconf = os.path.join(os.path.dirname( - self.cfgpath), path) - return absconf - return path - def _save(self, content, path): """writes the config to file""" ret = False diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 1f0f02f..f2f0156 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -7,11 +7,9 @@ entry point import os import sys -import socket -from docopt import docopt # local imports -from dotdrop.version import __version__ as VERSION +from dotdrop.options import Options from dotdrop.logger import Logger from dotdrop.templategen import Templategen from dotdrop.installer import Installer @@ -23,91 +21,42 @@ from dotdrop.utils import get_tmpdir, remove, strip_home, run from dotdrop.linktypes import LinkTypes LOG = Logger() -ENV_PROFILE = 'DOTDROP_PROFILE' -ENV_NOBANNER = 'DOTDROP_NOBANNER' -PROFILE = socket.gethostname() -if ENV_PROFILE in os.environ: - PROFILE = os.environ[ENV_PROFILE] TRANS_SUFFIX = 'trans' -BANNER = """ _ _ _ - __| | ___ | |_ __| |_ __ ___ _ __ - / _` |/ _ \| __/ _` | '__/ _ \| '_ | - \__,_|\___/ \__\__,_|_| \___/| .__/ v{} - |_|""".format(VERSION) - -USAGE = """ -{} - -Usage: - dotdrop install [-tfndVbD] [-c ] [-p ] [...] - dotdrop import [-ldVb] [-c ] [-p ] ... - dotdrop compare [-Vb] [-c ] [-p ] - [-o ] [-C ...] [-i ...] - dotdrop update [-fdVbkP] [-c ] [-p ] - [-i ...] [...] - dotdrop listfiles [-VTb] [-c ] [-p ] - dotdrop detail [-Vb] [-c ] [-p ] [...] - dotdrop list [-Vb] [-c ] - dotdrop --help - dotdrop --version - -Options: - -p --profile= Specify the profile to use [default: {}]. - -c --cfg= Path to the config [default: config.yaml]. - -C --file= Path of dotfile to compare. - -i --ignore= Pattern to ignore. - -o --dopts= Diff options [default: ]. - -n --nodiff Do not diff when installing. - -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 the value of "link_by_default" when importing. - -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. - -V --verbose Be verbose. - -d --dry Dry run. - -b --no-banner Do not display the banner. - -v --version Show version. - -h --help Show this screen. - -""".format(BANNER, PROFILE) - ########################################################### # entry point ########################################################### -def cmd_install(opts, conf, temporary=False, keys=[]): +def cmd_install(o): """install dotfiles for this profile""" - dotfiles = conf.get_dotfiles(opts['profile']) - if keys: + dotfiles = o.dotfiles + if o.install_keys: # filtered dotfiles to install - dotfiles = [d for d in dotfiles if d.key in set(keys)] + dotfiles = [d for d in dotfiles if d.key in set(o.install_keys)] if not dotfiles: msg = 'no dotfile to install for this profile (\"{}\")' - LOG.warn(msg.format(opts['profile'])) + LOG.warn(msg.format(o.profile)) return False - t = Templategen(base=opts['dotpath'], variables=opts['variables'], - debug=opts['debug']) + t = Templategen(base=o.dotpath, variables=o.variables, + debug=o.debug) tmpdir = None - if temporary: + if o.install_temporary: tmpdir = get_tmpdir() - inst = Installer(create=opts['create'], backup=opts['backup'], - dry=opts['dry'], safe=opts['safe'], - base=opts['dotpath'], workdir=opts['workdir'], - diff=opts['installdiff'], debug=opts['debug'], - totemp=tmpdir, showdiff=opts['showdiff']) + inst = Installer(create=o.create, backup=o.backup, + dry=o.dry, safe=o.safe, + base=o.dotpath, workdir=o.workdir, + diff=o.installdiff, debug=o.debug, + totemp=tmpdir, showdiff=o.showdiff) installed = [] for dotfile in dotfiles: preactions = [] - if not temporary and dotfile.actions \ + if not o.install_temporary and dotfile.actions \ and Cfg.key_actions_pre in dotfile.actions: for action in dotfile.actions[Cfg.key_actions_pre]: preactions.append(action) - if opts['debug']: + if o.debug: LOG.dbg('installing {}'.format(dotfile)) if hasattr(dotfile, 'link') and dotfile.link == LinkTypes.PARENTS: r = inst.link(t, dotfile.src, dotfile.dst, actions=preactions) @@ -117,59 +66,60 @@ def cmd_install(opts, conf, temporary=False, keys=[]): src = dotfile.src tmp = None if dotfile.trans_r: - tmp = apply_trans(opts, dotfile) + tmp = apply_trans(o.dotpath, dotfile, debug=o.debug) if not tmp: continue src = tmp r = inst.install(t, src, dotfile.dst, actions=preactions, noempty=dotfile.noempty) if tmp: - tmp = os.path.join(opts['dotpath'], tmp) + tmp = os.path.join(o.dotpath, tmp) if os.path.exists(tmp): remove(tmp) if len(r) > 0: - if not temporary and Cfg.key_actions_post in dotfile.actions: + if not o.install_temporary and \ + Cfg.key_actions_post in dotfile.actions: actions = dotfile.actions[Cfg.key_actions_post] # execute action for action in actions: - if opts['dry']: + if o.dry: LOG.dry('would execute action: {}'.format(action)) else: - if opts['debug']: + if o.debug: LOG.dbg('executing post action {}'.format(action)) action.execute() installed.extend(r) - if temporary: + if o.install_temporary: LOG.log('\nInstalled to tmp \"{}\".'.format(tmpdir)) LOG.log('\n{} dotfile(s) installed.'.format(len(installed))) return True -def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): +def cmd_compare(o, tmp): """compare dotfiles and return True if all identical""" - dotfiles = conf.get_dotfiles(opts['profile']) + dotfiles = o.dotfiles if dotfiles == []: msg = 'no dotfile defined for this profile (\"{}\")' - LOG.warn(msg.format(opts['profile'])) + LOG.warn(msg.format(o.profile)) return True # compare only specific files same = True selected = dotfiles - if focus: - selected = _select(focus, dotfiles) + if o.compare_focus: + selected = _select(o.compare_focus, dotfiles) if len(selected) < 1: return False - t = Templategen(base=opts['dotpath'], variables=opts['variables'], - debug=opts['debug']) - inst = Installer(create=opts['create'], backup=opts['backup'], - dry=opts['dry'], base=opts['dotpath'], - workdir=opts['workdir'], debug=opts['debug']) - comp = Comparator(diffopts=opts['dopts'], debug=opts['debug']) + t = Templategen(base=o.dotpath, variables=o.variables, + debug=o.debug) + inst = Installer(create=o.create, backup=o.backup, + dry=o.dry, base=o.dotpath, + workdir=o.workdir, debug=o.debug) + comp = Comparator(diffopts=o.compare_dopts, debug=o.debug) for dotfile in selected: - if opts['debug']: + if o.debug: LOG.dbg('comparing {}'.format(dotfile)) src = dotfile.src if not os.path.lexists(os.path.expanduser(dotfile.dst)): @@ -181,7 +131,7 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): tmpsrc = None if dotfile.trans_r: # apply transformation - tmpsrc = apply_trans(opts, dotfile) + tmpsrc = apply_trans(o.dotpath, dotfile, debug=o.debug) if not tmpsrc: # could not apply trans same = False @@ -193,15 +143,15 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): # failed to install to tmp same = False continue - ignores = list(set(ignore + dotfile.cmpignore)) + ignores = list(set(o.compare_ignore + dotfile.cmpignore)) diff = comp.compare(insttmp, dotfile.dst, ignore=ignores) if tmpsrc: # clean tmp transformed dotfile if any - tmpsrc = os.path.join(opts['dotpath'], tmpsrc) + tmpsrc = os.path.join(o.dotpath, tmpsrc) if os.path.exists(tmpsrc): remove(tmpsrc) if diff == '': - if opts['debug']: + if o.debug: line = '=> compare {}: diffing with \"{}\"' LOG.dbg(line.format(dotfile.key, dotfile.dst)) LOG.dbg('same file') @@ -214,16 +164,20 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): return same -def cmd_update(opts, conf, paths, iskey=False, ignore=[], showpatch=False): +def cmd_update(o): """update the dotfile(s) from path(s) or key(s)""" ret = True - updater = Updater(conf, opts['dotpath'], opts['profile'], - opts['variables'], opts['dry'], opts['safe'], - iskey=iskey, debug=opts['debug'], ignore=[], - showpatch=showpatch) + paths = o.update_path + iskey = o.update_iskey + ignore = o.update_ignore + showpatch = o.update_showpatch + + updater = Updater(o.dotpath, o.dotfiles, o.variables, + o.dry, o.safe, iskey=iskey, debug=o.debug, + ignore=ignore, showpatch=showpatch) if not iskey: # update paths - if opts['debug']: + if o.debug: LOG.dbg('update by paths: {}'.format(paths)) for path in paths: if not updater.update_path(path): @@ -233,8 +187,8 @@ def cmd_update(opts, conf, paths, iskey=False, ignore=[], showpatch=False): keys = paths if not keys: # if not provided, take all keys - keys = [d.key for d in conf.get_dotfiles(opts['profile'])] - if opts['debug']: + keys = [d.key for d in o.dotfiles] + if o.debug: LOG.dbg('update by keys: {}'.format(keys)) for key in keys: if not updater.update_key(key): @@ -242,12 +196,13 @@ def cmd_update(opts, conf, paths, iskey=False, ignore=[], showpatch=False): return ret -def cmd_importer(opts, conf, paths): +def cmd_importer(o): """import dotfile(s) from paths""" ret = True cnt = 0 + paths = o.import_path for path in paths: - if opts['debug']: + if o.debug: LOG.dbg('trying to import {}'.format(path)) if not os.path.lexists(path): LOG.err('\"{}\" does not exist, ignored!'.format(path)) @@ -257,37 +212,37 @@ def cmd_importer(opts, conf, paths): dst = os.path.abspath(dst) src = strip_home(dst) strip = '.' + os.sep - if opts['keepdot']: + if o.keepdot: strip = os.sep src = src.lstrip(strip) # create a new dotfile dotfile = Dotfile('', dst, src) - linktype = LinkTypes(opts['link']) + linktype = LinkTypes(o.link) - if opts['debug']: + if o.debug: LOG.dbg('new dotfile: {}'.format(dotfile)) # prepare hierarchy for dotfile - srcf = os.path.join(opts['dotpath'], src) + srcf = os.path.join(o.dotpath, src) if not os.path.exists(srcf): cmd = ['mkdir', '-p', '{}'.format(os.path.dirname(srcf))] - if opts['dry']: + if o.dry: LOG.dry('would run: {}'.format(' '.join(cmd))) else: - r, _ = run(cmd, raw=False, debug=opts['debug'], checkerr=True) + r, _ = run(cmd, raw=False, debug=o.debug, checkerr=True) if not r: LOG.err('importing \"{}\" failed!'.format(path)) ret = False continue cmd = ['cp', '-R', '-L', dst, srcf] - if opts['dry']: + if o.dry: LOG.dry('would run: {}'.format(' '.join(cmd))) if linktype == LinkTypes.PARENTS: LOG.dry('would symlink {} to {}'.format(srcf, dst)) else: - r, _ = run(cmd, raw=False, debug=opts['debug'], checkerr=True) + r, _ = run(cmd, raw=False, debug=o.debug, checkerr=True) if not r: LOG.err('importing \"{}\" failed!'.format(path)) ret = False @@ -295,42 +250,42 @@ def cmd_importer(opts, conf, paths): if linktype == LinkTypes.PARENTS: remove(dst) os.symlink(srcf, dst) - retconf, dotfile = conf.new(dotfile, opts['profile'], - link=linktype, debug=opts['debug']) + retconf, dotfile = o.conf.new(dotfile, o.profile, + link=linktype, debug=o.debug) if retconf: LOG.sub('\"{}\" imported'.format(path)) cnt += 1 else: LOG.warn('\"{}\" ignored'.format(path)) - if opts['dry']: + if o.dry: LOG.dry('new config file would be:') - LOG.raw(conf.dump()) + LOG.raw(o.conf.dump()) else: - conf.save() + o.conf.save() LOG.log('\n{} file(s) imported.'.format(cnt)) return ret -def cmd_list_profiles(conf): +def cmd_list_profiles(o): """list all profiles""" LOG.log('Available profile(s):') - for p in conf.get_profiles(): + for p in o.profiles: LOG.sub(p) LOG.log('') -def cmd_list_files(opts, conf, templateonly=False): +def cmd_list_files(o): """list all dotfiles for a specific profile""" - if not opts['profile'] in conf.get_profiles(): - LOG.warn('unknown profile \"{}\"'.format(opts['profile'])) + if o.profile not in o.profiles: + LOG.warn('unknown profile \"{}\"'.format(o.profile)) return what = 'Dotfile(s)' - if templateonly: + if o.listfiles_templateonly: what = 'Template(s)' - LOG.emph('{} for profile \"{}\"\n'.format(what, opts['profile'])) - for dotfile in conf.get_dotfiles(opts['profile']): - if templateonly: - src = os.path.join(opts['dotpath'], dotfile.src) + LOG.emph('{} for profile \"{}\"\n'.format(what, o.profile)) + for dotfile in o.dotfiles: + if o.listfiles_templateonly: + src = os.path.join(o.dotpath, dotfile.src) if not Templategen.is_template(src): continue LOG.log('{} (src: \"{}\", link: {})'.format(dotfile.key, dotfile.src, @@ -339,18 +294,18 @@ def cmd_list_files(opts, conf, templateonly=False): LOG.log('') -def cmd_detail(opts, conf, keys=None): +def cmd_detail(o): """list details on all files for all dotfile entries""" - if not opts['profile'] in conf.get_profiles(): - LOG.warn('unknown profile \"{}\"'.format(opts['profile'])) + if o.profile not in o.profiles: + LOG.warn('unknown profile \"{}\"'.format(o.profile)) return - dotfiles = conf.get_dotfiles(opts['profile']) - if keys: + dotfiles = o.dotfiles + if o.detail_keys: # filtered dotfiles to install - dotfiles = [d for d in dotfiles if d.key in set(keys)] - LOG.emph('dotfiles details for profile \"{}\":\n'.format(opts['profile'])) + dotfiles = [d for d in dotfiles if d.key in set(o.details_keys)] + LOG.emph('dotfiles details for profile \"{}\":\n'.format(o.profile)) for d in dotfiles: - _detail(opts['dotpath'], d) + _detail(o.dotpath, d) LOG.log('') @@ -379,12 +334,6 @@ def _detail(dotpath, dotfile): LOG.sub('{} (template:{})'.format(p, template)) -def _header(): - """print the header""" - LOG.log(BANNER) - LOG.log('') - - def _select(selections, dotfiles): selected = [] for selection in selections: @@ -400,16 +349,16 @@ def _select(selections, dotfiles): return selected -def apply_trans(opts, dotfile): +def apply_trans(dotpath, dotfile, debug=False): """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) trans = dotfile.trans_r - if opts['debug']: + if debug: LOG.dbg('executing transformation {}'.format(trans)) - s = os.path.join(opts['dotpath'], src) - temp = os.path.join(opts['dotpath'], new_src) + s = os.path.join(dotpath, src) + temp = os.path.join(dotpath, new_src) if not trans.transform(s, temp): msg = 'transformation \"{}\" failed for {}' LOG.err(msg.format(trans.key, dotfile.key)) @@ -426,110 +375,64 @@ def apply_trans(opts, dotfile): def main(): """entry point""" - ret = True - args = docopt(USAGE, version=VERSION) - try: - conf = Cfg(os.path.expanduser(args['--cfg']), - debug=args['--verbose']) + o = Options() except ValueError as e: LOG.err('Config format error: {}'.format(str(e))) return False - opts = conf.get_settings() - opts['dry'] = args['--dry'] - opts['profile'] = args['--profile'] - opts['safe'] = not args['--force'] - opts['debug'] = args['--verbose'] - opts['installdiff'] = not args['--nodiff'] - opts['link'] = LinkTypes.NOLINK - if opts['link_by_default']: - opts['link'] = LinkTypes.PARENTS - - # Only invert link type from NOLINK to PARENTS and vice-versa - if args['--inv-link'] and opts['link'] == LinkTypes.NOLINK: - opts['link'] = LinkTypes.PARENTS - if args['--inv-link'] and opts['link'] == LinkTypes.PARENTS: - opts['link'] = LinkTypes.NOLINK - - opts['variables'] = conf.get_variables(opts['profile'], - debug=opts['debug']) - opts['showdiff'] = opts['showdiff'] or args['--showdiff'] - - if opts['debug']: - LOG.dbg('config file: {}'.format(args['--cfg'])) - LOG.dbg('options:\n{}'.format(opts)) - LOG.dbg('configs:\n{}'.format(conf.dump())) - - # resolve dynamic paths - conf.eval_dotfiles(opts['profile'], opts['variables'], - debug=opts['debug']) - - if ENV_NOBANNER not in os.environ \ - and opts['banner'] \ - and not args['--no-banner']: - _header() - + ret = True try: - if args['list']: + if o.cmd_list: # list existing profiles - if opts['debug']: + if o.debug: LOG.dbg('running cmd: list') - cmd_list_profiles(conf) + cmd_list_profiles(o) - elif args['listfiles']: + elif o.cmd_listfiles: # list files for selected profile - if opts['debug']: + if o.debug: LOG.dbg('running cmd: listfiles') - cmd_list_files(opts, conf, templateonly=args['--template']) + cmd_list_files(o) - elif args['install']: + elif o.cmd_install: # install the dotfiles stored in dotdrop - if opts['debug']: + if o.debug: LOG.dbg('running cmd: install') - ret = cmd_install(opts, conf, temporary=args['--temp'], - keys=args['']) + ret = cmd_install(o) - elif args['compare']: + elif o.cmd_compare: # compare local dotfiles with dotfiles stored in dotdrop - if opts['debug']: + if o.debug: LOG.dbg('running cmd: compare') tmp = get_tmpdir() - opts['dopts'] = args['--dopts'] - ret = cmd_compare(opts, conf, tmp, focus=args['--file'], - ignore=args['--ignore']) + ret = cmd_compare(o, tmp) # clean tmp directory remove(tmp) - elif args['import']: + elif o.cmd_import: # import dotfile(s) - if opts['debug']: + if o.debug: LOG.dbg('running cmd: import') - ret = cmd_importer(opts, conf, args['']) + ret = cmd_importer(o) - elif args['update']: + elif o.cmd_update: # update a dotfile - if opts['debug']: + if o.debug: LOG.dbg('running cmd: update') - iskey = args['--key'] - ret = cmd_update(opts, conf, args[''], - iskey=iskey, ignore=args['--ignore'], - showpatch=args['--show-patch']) + ret = cmd_update(o) - elif args['detail']: + elif o.cmd_detail: # detail files - if opts['debug']: + if o.debug: LOG.dbg('running cmd: update') - cmd_detail(opts, conf, keys=args['']) + cmd_detail(o) except KeyboardInterrupt: LOG.err('interrupted') ret = False - if opts['debug']: - LOG.dbg('configs:\n{}'.format(conf.dump())) - return ret diff --git a/dotdrop/options.py b/dotdrop/options.py new file mode 100644 index 0000000..7f1d223 --- /dev/null +++ b/dotdrop/options.py @@ -0,0 +1,169 @@ +""" +author: deadc0de6 (https://github.com/deadc0de6) +Copyright (c) 2017, deadc0de6 + +stores all options to use across dotdrop +""" + +import os +import socket +from docopt import docopt + +# local imports +from dotdrop.version import __version__ as VERSION +from dotdrop.linktypes import LinkTypes +from dotdrop.logger import Logger +from dotdrop.config import Cfg + + +PROFILE = socket.gethostname() +ENV_PROFILE = 'DOTDROP_PROFILE' +ENV_NOBANNER = 'DOTDROP_NOBANNER' +if ENV_PROFILE in os.environ: + PROFILE = os.environ[ENV_PROFILE] + +BANNER = """ _ _ _ + __| | ___ | |_ __| |_ __ ___ _ __ + / _` |/ _ \| __/ _` | '__/ _ \| '_ | + \__,_|\___/ \__\__,_|_| \___/| .__/ v{} + |_|""".format(VERSION) + +USAGE = """ +{} + +Usage: + dotdrop install [-tfndVbD] [-c ] [-p ] [...] + dotdrop import [-ldVb] [-c ] [-p ] ... + dotdrop compare [-Vb] [-c ] [-p ] + [-o ] [-C ...] [-i ...] + dotdrop update [-fdVbkP] [-c ] [-p ] + [-i ...] [...] + dotdrop listfiles [-VTb] [-c ] [-p ] + dotdrop detail [-Vb] [-c ] [-p ] [...] + dotdrop list [-Vb] [-c ] + dotdrop --help + dotdrop --version + +Options: + -p --profile= Specify the profile to use [default: {}]. + -c --cfg= Path to the config [default: config.yaml]. + -C --file= Path of dotfile to compare. + -i --ignore= Pattern to ignore. + -o --dopts= Diff options [default: ]. + -n --nodiff Do not diff when installing. + -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 the value of "link_by_default" when importing. + -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. + -V --verbose Be verbose. + -d --dry Dry run. + -b --no-banner Do not display the banner. + -v --version Show version. + -h --help Show this screen. + +""".format(BANNER, PROFILE) + + +class Options: + + def __init__(self, args=None): + """constructor + @key: action key + @action: action string + """ + self.args = args + if not args: + self.args = docopt(USAGE, version=VERSION) + self.log = Logger() + self.debug = self.args['--verbose'] + self.confpath = os.path.expanduser(self.args['--cfg']) + self.log.dbg('config file: {}'.format(self.confpath)) + + self._read_config() + self._apply_args() + self._fill_attr() + if ENV_NOBANNER not in os.environ \ + and self.banner \ + and not self.args['--no-banner']: + self._header() + if self.debug: + self._print_attr() + + def _header(): + """print the header""" + self.log.log(BANNER) + self.log.log('') + + def _read_config(self): + self.conf = Cfg(self.confpath, debug=self.debug) + # transform the configs in attribute + for k, v in self.conf.get_settings().items(): + setattr(self, k, v) + + def _apply_args(self): + # the commands + self.cmd_list = self.args['list'] + self.cmd_listfiles = self.args['listfiles'] + self.cmd_install = self.args['install'] + self.cmd_compare = self.args['compare'] + self.cmd_import = self.args['import'] + self.cmd_update = self.args['update'] + self.cmd_detail = self.args['detail'] + + # adapt attributes based on arguments + self.dry = self.args['--dry'] + self.profile = self.args['--profile'] + self.safe = not self.args['--force'] + self.installdiff = not self.args['--nodiff'] + self.showdiff = self.showdiff or self.args['--showdiff'] + self.link = LinkTypes.NOLINK + if self.link_by_default: + self.link = LinkTypes.PARENTS + + if self.args['--inv-link']: + # Only invert link type from NOLINK to PARENTS and vice-versa + if self.link == LinkTypes.NOLINK: + self.link = LinkTypes.PARENTS + elif self.link == LinkTypes.PARENTS: + self.link = LinkTypes.NOLINK + + # "listfiles" specifics + self.listfiles_templateonly = self.args['--template'] + # "install" specifics + self.install_temporary = self.args['--temp'] + self.install_keys = self.args[''] + # "compare" specifics + self.compare_dopts = self.args['--dopts'] + self.compare_focus = self.args['--file'] + self.compare_ignore = self.args['--ignore'] + # "import" specifics + self.import_path = self.args[''] + # "update" specifics + self.update_path = self.args[''] + self.update_iskey = self.args['--key'] + self.update_ignore = self.args['--ignore'] + self.update_showpatch = self.args['--show-patch'] + # "detail" specifics + self.detail_keys = self.args[''] + + def _fill_attr(self): + # variables + self.variables = self.conf.get_variables(self.profile, + debug=self.debug).copy() + # the dotfiles + self.dotfiles = self.conf.eval_dotfiles(self.profile, self.variables, + debug=self.debug).copy() + self.profiles = self.conf.get_profiles() + + def _print_attr(self): + self.log.dbg('options:') + for att in dir(self): + if att.startswith('_'): + continue + val = getattr(self, att) + if callable(val): + continue + self.log.dbg('- {}: \"{}\"'.format(att, val)) diff --git a/dotdrop/updater.py b/dotdrop/updater.py index 9891756..cffdc35 100644 --- a/dotdrop/updater.py +++ b/dotdrop/updater.py @@ -20,12 +20,11 @@ TILD = '~' class Updater: - def __init__(self, conf, dotpath, profile, variables, dry, safe, + def __init__(self, dotpath, dotfiles, variables, dry, safe, iskey=False, debug=False, ignore=[], showpatch=False): """constructor - @conf: configuration @dotpath: path where dotfiles are stored - @profile: profile selected + @dotfiles: dotfiles for this profile @variables: dictionary of variables for the templates @dry: simulate @safe: ask for overwrite if True @@ -34,9 +33,8 @@ class Updater: @ignore: pattern to ignore when updating @showpatch: show patch if dotfile to update is a template """ - self.conf = conf self.dotpath = dotpath - self.profile = profile + self.dotfiles = dotfiles self.variables = variables self.dry = dry self.safe = safe @@ -79,7 +77,7 @@ class Updater: self.log.dbg('ignore pattern(s): {}'.format(self.ignores)) left = os.path.expanduser(path) - right = os.path.join(self.conf.abs_or_rel(self.dotpath), dotfile.src) + right = os.path.join(self.dotpath, dotfile.src) right = os.path.expanduser(right) if self._ignore([left, right]): @@ -128,7 +126,7 @@ class Updater: def _get_dotfile_by_key(self, key): """get the dotfile matching this key""" - dotfiles = self.conf.get_dotfiles(self.profile) + dotfiles = self.dotfiles subs = [d for d in dotfiles if d.key == key] if not subs: self.log.err('key \"{}\" not found!'.format(key)) @@ -141,7 +139,7 @@ class Updater: def _get_dotfile_by_path(self, path): """get the dotfile matching this path""" - dotfiles = self.conf.get_dotfiles(self.profile) + dotfiles = self.dotfiles subs = [d for d in dotfiles if d.dst == path] if not subs: self.log.err('\"{}\" is not managed!'.format(path)) diff --git a/tests/helpers.py b/tests/helpers.py index d43bd18..4a8f195 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -10,9 +10,9 @@ import string import random import tempfile -from dotdrop.config import Cfg -from dotdrop.utils import * +from dotdrop.options import Options from dotdrop.linktypes import LinkTypes +from dotdrop.utils import * TMPSUFFIX = '.dotdrop' @@ -81,20 +81,56 @@ def create_dir(path): return path -def load_config(confpath, profile): +def _fake_args(): + args = {} + args['--verbose'] = False + args['--no-banner'] = False + args['--dry'] = False + args['--force'] = False + args['--nodiff'] = False + args['--showdiff'] = True + args['--inv-link'] = False + args['--template'] = False + args['--temp'] = False + args[''] = [] + args['--dopts'] = '' + args['--file'] = [] + args['--ignore'] = [] + args[''] = [] + args['--key'] = False + args['--ignore'] = [] + args['--show-patch'] = False + # cmds + args['list'] = False + args['listfiles'] = False + args['install'] = False + args['compare'] = False + args['import'] = False + args['update'] = False + args['detail'] = False + return args + + +def load_options(confpath, profile): """Load the config file from path""" - conf = Cfg(confpath) - opts = conf.get_settings() - opts['dry'] = False - opts['profile'] = profile - opts['safe'] = True - opts['installdiff'] = True - opts['link'] = LinkTypes.NOLINK.value - opts['showdiff'] = True - opts['debug'] = True - opts['dopts'] = '' - opts['variables'] = {} - return conf, opts + # create the fake args (bypass docopt) + args = _fake_args() + 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.installdiff = True + o.link = LinkTypes.NOLINK.value + o.showdiff = True + o.debug = True + o.dopts = '' + o.variables = {} + return o def get_path_strip_version(path): diff --git a/tests/test_compare.py b/tests/test_compare.py index 7ce8cda..554abc3 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -16,7 +16,7 @@ from dotdrop.templategen import Templategen # from tests.helpers import * from tests.helpers import create_dir, get_string, get_tempdir, clean, \ - create_random_file, create_fake_config, load_config, edit_content + create_random_file, create_fake_config, load_options, edit_content class TestCompare(unittest.TestCase): @@ -26,12 +26,12 @@ class TestCompare(unittest.TestCase): CONFIG_DOTPATH = 'dotfiles' CONFIG_NAME = 'config.yaml' - def compare(self, opts, conf, tmp, nbdotfiles): - dotfiles = conf.get_dotfiles(opts['profile']) + def compare(self, o, tmp, nbdotfiles): + dotfiles = o.dotfiles self.assertTrue(len(dotfiles) == nbdotfiles) - t = Templategen(base=opts['dotpath'], debug=True) - inst = Installer(create=opts['create'], backup=opts['backup'], - dry=opts['dry'], base=opts['dotpath'], debug=True) + t = Templategen(base=o.dotpath, debug=True) + inst = Installer(create=o.create, backup=o.backup, + dry=o.dry, base=o.dotpath, debug=True) comp = Comparator() results = {} for dotfile in dotfiles: @@ -98,50 +98,53 @@ class TestCompare(unittest.TestCase): backup=self.CONFIG_BACKUP, create=self.CONFIG_CREATE) self.assertTrue(os.path.exists(confpath)) - conf, opts = load_config(confpath, profile) - opts['longkey'] = True + o = load_options(confpath, profile) + o.longkey = True dfiles = [d1, d2, d3, d4, d5] # import the files - cmd_importer(opts, conf, dfiles) - conf, opts = load_config(confpath, profile) + o.import_path = dfiles + cmd_importer(o) + o = load_options(confpath, profile) # compare the files expected = {d1: True, d2: True, d3: True, d4: True, d5: True} - results = self.compare(opts, conf, tmp, len(dfiles)) + results = self.compare(o, tmp, len(dfiles)) self.assertTrue(results == expected) # modify file edit_content(d1, get_string(20)) expected = {d1: False, d2: True, d3: True, d4: True, d5: True} - results = self.compare(opts, conf, tmp, len(dfiles)) + results = self.compare(o, tmp, len(dfiles)) self.assertTrue(results == expected) # modify binary file edit_content(d4, bytes(get_string(20), 'ascii'), binary=True) expected = {d1: False, d2: True, d3: True, d4: False, d5: True} - results = self.compare(opts, conf, tmp, len(dfiles)) + results = self.compare(o, tmp, len(dfiles)) self.assertTrue(results == expected) # add file in directory d7, _ = create_random_file(d5) self.assertTrue(os.path.exists(d7)) expected = {d1: False, d2: True, d3: True, d4: False, d5: False} - results = self.compare(opts, conf, tmp, len(dfiles)) + results = self.compare(o, tmp, len(dfiles)) self.assertTrue(results == expected) # modify all files edit_content(d2, get_string(20)) edit_content(d3, get_string(21)) expected = {d1: False, d2: False, d3: False, d4: False, d5: False} - results = self.compare(opts, conf, tmp, len(dfiles)) + results = self.compare(o, tmp, len(dfiles)) self.assertTrue(results == expected) # test compare from dotdrop - self.assertFalse(cmd_compare(opts, conf, tmp)) + self.assertFalse(cmd_compare(o, tmp)) # test focus - self.assertFalse(cmd_compare(opts, conf, tmp, focus=d4)) - self.assertFalse(cmd_compare(opts, conf, tmp, focus='/tmp/fake')) + o.focus = d4 + self.assertFalse(cmd_compare(o, tmp)) + o.focus = '/tmp/fake' + self.assertFalse(cmd_compare(o, tmp)) def main(): diff --git a/tests/test_import.py b/tests/test_import.py index f72fb86..fd51769 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -12,7 +12,6 @@ import yaml from dotdrop.dotdrop import cmd_importer from dotdrop.dotdrop import cmd_list_profiles from dotdrop.dotdrop import cmd_list_files -from dotdrop.dotdrop import _header from dotdrop.dotdrop import cmd_update from dotdrop.config import Cfg @@ -34,11 +33,11 @@ class TestImport(unittest.TestCase): content = yaml.load(f) return content - def assert_file(self, path, conf, profile): + def assert_file(self, path, o, profile): """Make sure path has been inserted in conf for profile""" strip = get_path_strip_version(path) - self.assertTrue(strip in [x.src for x in conf.get_dotfiles(profile)]) - dsts = [os.path.expanduser(x.dst) for x in conf.get_dotfiles(profile)] + self.assertTrue(strip in [x.src for x in o.dotfiles]) + dsts = [os.path.expanduser(x.dst) for x in o.dotfiles] self.assertTrue(path in dsts) def assert_in_yaml(self, path, dic, link=False): @@ -69,7 +68,7 @@ class TestImport(unittest.TestCase): backup=self.CONFIG_BACKUP, create=self.CONFIG_CREATE) self.assertTrue(os.path.exists(confpath)) - conf, opts = load_config(confpath, profile) + o = load_options(confpath, profile) # create some random dotfiles dotfile1, content1 = create_random_file(src) @@ -107,25 +106,27 @@ class TestImport(unittest.TestCase): # import the dotfiles dfiles = [dotfile1, dotfile2, dotfile3, dotfile4, dotfile5] - cmd_importer(opts, conf, dfiles) + o.import_path = dfiles + cmd_importer(o) # import symlink - opts[Cfg.key_dotfiles_link] = True + o.link = LinkTypes.PARENTS sfiles = [dotfile6, dotfile7] - cmd_importer(opts, conf, sfiles) - opts[Cfg.key_dotfiles_link] = False + o.import_path = sfiles + cmd_importer(o) + o.link = LinkTypes.NOLINK # reload the config - conf, opts = load_config(confpath, profile) + o = load_options(confpath, profile) # test dotfiles in config class - self.assertTrue(profile in conf.get_profiles()) - self.assert_file(dotfile1, conf, profile) - self.assert_file(dotfile2, conf, profile) - self.assert_file(dotfile3, conf, profile) - self.assert_file(dotfile4, conf, profile) - self.assert_file(dotfile5, conf, profile) - self.assert_file(dotfile6, conf, profile) - self.assert_file(dotfile7, conf, profile) + self.assertTrue(profile in o.profiles) + self.assert_file(dotfile1, o, profile) + self.assert_file(dotfile2, o, profile) + self.assert_file(dotfile3, o, profile) + self.assert_file(dotfile4, o, profile) + self.assert_file(dotfile5, o, profile) + self.assert_file(dotfile6, o, profile) + self.assert_file(dotfile7, o, profile) # test dotfiles in yaml file y = self.load_yaml(confpath) @@ -193,15 +194,15 @@ class TestImport(unittest.TestCase): self.assertTrue(os.path.islink(dotfile7)) self.assertTrue(os.path.realpath(dotfile7) == indt7) - cmd_list_profiles(conf) - cmd_list_files(opts, conf) - _header() + cmd_list_profiles(o) + cmd_list_files(o) # fake test update editcontent = 'edited' edit_content(dotfile1, editcontent) - opts['safe'] = False - cmd_update(opts, conf, [dotfile1]) + o.safe = False + o.update_path = [dotfile1] + cmd_update(o) c2 = open(indt1, 'r').read() self.assertTrue(editcontent == c2) diff --git a/tests/test_install.py b/tests/test_install.py index 0d3b4a5..b107448 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -11,7 +11,7 @@ import filecmp from dotdrop.config import Cfg from tests.helpers import create_dir, get_string, get_tempdir, clean, \ - create_random_file, load_config + create_random_file, load_options from dotdrop.dotfile import Dotfile from dotdrop.installer import Installer from dotdrop.action import Action @@ -181,12 +181,12 @@ exec bspwm self.assertTrue(conf is not None) # install them - conf, opts = load_config(confpath, profile) - opts['safe'] = False - opts['debug'] = True - opts['showdiff'] = True - opts['variables'] = {} - cmd_install(opts, conf) + o = load_options(confpath, profile) + o.safe = False + o.debug = True + o.showdiff = True + o.variables = {} + cmd_install(o) # now compare the generated files self.assertTrue(os.path.exists(dst1)) @@ -233,7 +233,7 @@ exec bspwm self.assertTrue(tempcontent == header()) def test_link_children(self): - + """test the link children""" # create source dir src_dir = get_tempdir() self.assertTrue(os.path.exists(src_dir)) @@ -257,6 +257,7 @@ exec bspwm self.assertEqual(os.path.realpath(dst), src) def test_fails_without_src(self): + """test fails without src""" src = '/some/non/existant/file' installer = Installer() @@ -272,7 +273,7 @@ exec bspwm .format(src)) def test_fails_when_src_file(self): - + """test fails when src file""" # create source dir src_dir = get_tempdir() self.assertTrue(os.path.exists(src_dir)) @@ -296,6 +297,7 @@ exec bspwm .format(src)) def test_creates_dst(self): + """test creates dst""" src_dir = get_tempdir() self.assertTrue(os.path.exists(src_dir)) self.addCleanup(clean, src_dir) @@ -316,7 +318,7 @@ exec bspwm self.assertTrue(os.path.exists(dst_dir)) def test_prompts_to_replace_dst(self): - + """test prompts to replace dst""" # create source dir src_dir = get_tempdir() self.assertTrue(os.path.exists(src_dir)) @@ -354,7 +356,7 @@ exec bspwm @patch('dotdrop.installer.Templategen') def test_runs_templater(self, mocked_templategen): - + """test runs templater""" # create source dir src_dir = get_tempdir() self.assertTrue(os.path.exists(src_dir)) diff --git a/tests/test_listings.py b/tests/test_listings.py index 33d7291..e9022c9 100644 --- a/tests/test_listings.py +++ b/tests/test_listings.py @@ -74,22 +74,26 @@ class TestListings(unittest.TestCase): backup=self.CONFIG_BACKUP, create=self.CONFIG_CREATE) self.assertTrue(os.path.exists(confpath)) - conf, opts = load_config(confpath, profile) + o = load_options(confpath, profile) dfiles = [d1, d2, d3, d4, d5] # import the files - cmd_importer(opts, conf, dfiles) - conf, opts = load_config(confpath, profile) + o.import_path = dfiles + cmd_importer(o) + o = load_options(confpath, profile) # listfiles - cmd_list_profiles(conf) + cmd_list_profiles(o) # list files - cmd_list_files(opts, conf, templateonly=False) - cmd_list_files(opts, conf, templateonly=True) + o.listfiles_templateonly = False + cmd_list_files(o) + o.listfiles_templateonly = True + cmd_list_files(o) # details - cmd_detail(opts, conf, keys=None) + o.detail_keys = None + cmd_detail(o) def main(): diff --git a/tests/test_update.py b/tests/test_update.py index d7d0070..9701eb5 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -12,7 +12,7 @@ from dotdrop.dotdrop import cmd_update from dotdrop.dotdrop import cmd_importer from tests.helpers import create_dir, get_string, get_tempdir, clean, \ - create_random_file, create_fake_config, load_config, edit_content + create_random_file, create_fake_config, load_options, edit_content class TestUpdate(unittest.TestCase): @@ -67,12 +67,13 @@ class TestUpdate(unittest.TestCase): backup=self.CONFIG_BACKUP, create=self.CONFIG_CREATE) self.assertTrue(os.path.exists(confpath)) - conf, opts = load_config(confpath, profile) + o = load_options(confpath, profile) dfiles = [d1, dir1, d2] # import the files - cmd_importer(opts, conf, dfiles) - conf, opts = load_config(confpath, profile) + o.import_path = dfiles + cmd_importer(o) + o = load_options(confpath, profile) # edit the files edit_content(d1, 'newcontent') @@ -87,9 +88,10 @@ class TestUpdate(unittest.TestCase): create_random_file(dpath) # update it - opts['safe'] = False - opts['debug'] = True - cmd_update(opts, conf, [d1, dir1]) + o.safe = False + o.debug = True + o.update_path = [d1, dir1] + cmd_update(o) # test content newcontent = open(d1, 'r').read() @@ -100,7 +102,7 @@ class TestUpdate(unittest.TestCase): edit_content(d2, 'newcontentbykey') # update it by key - dfiles = conf.get_dotfiles(profile) + dfiles = o.dotfiles d2key = '' for ds in dfiles: t = os.path.expanduser(ds.dst) @@ -108,9 +110,11 @@ class TestUpdate(unittest.TestCase): d2key = ds.key break self.assertTrue(d2key != '') - opts['safe'] = False - opts['debug'] = True - cmd_update(opts, conf, [d2key], iskey=True) + o.safe = False + o.debug = True + o.update_path = [d2key] + o.iskey = True + cmd_update(o) # test content newcontent = open(d2, 'r').read()