diff --git a/dotdrop/comparator.py b/dotdrop/comparator.py index 5449015..c930d3f 100644 --- a/dotdrop/comparator.py +++ b/dotdrop/comparator.py @@ -2,7 +2,7 @@ author: deadc0de6 (https://github.com/deadc0de6) Copyright (c) 2017, deadc0de6 -handle the comparison of dotfiles and local deployment +handle the comparison of two dotfiles """ import os @@ -17,7 +17,7 @@ class Comparator: def __init__(self, diffopts='', debug=False): """constructor - @diffopts: cli switches to pass to unix diff + @diffopts: switches to pass to unix diff @debug: enable debug """ self.diffopts = diffopts @@ -61,12 +61,14 @@ class Comparator: self.log.dbg('compare {} and {}'.format(left, right)) ret = [] comp = filecmp.dircmp(left, right) + # handle files only in deployed file for i in comp.left_only: if utils.must_ignore([os.path.join(left, i)], ignore, debug=self.debug): continue ret.append('=> \"{}\" does not exist on local\n'.format(i)) + # handle files only in dotpath file for i in comp.right_only: if utils.must_ignore([os.path.join(right, i)], @@ -107,6 +109,5 @@ class Comparator: opts=self.diffopts, debug=self.debug) if header: lshort = os.path.basename(left) - rshort = os.path.basename(right) diff = '=> diff \"{}\":\n{}'.format(lshort, diff) return diff diff --git a/dotdrop/config.py b/dotdrop/config.py index 12eb8fb..6c68718 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -14,7 +14,7 @@ from dotdrop.dotfile import Dotfile from dotdrop.templategen import Templategen from dotdrop.logger import Logger from dotdrop.action import Action, Transform -from dotdrop.utils import * +from dotdrop.utils import strip_home, shell from dotdrop.linktypes import LinkTypes @@ -126,7 +126,8 @@ class Cfg: def eval_dotfiles(self, profile, variables, debug=False): """resolve dotfiles src/dst/actions templating for this profile""" t = Templategen(variables=variables) - for d in self.get_dotfiles(profile): + dotfiles = self._get_dotfiles(profile) + for d in dotfiles: # src and dst path d.src = t.generate_string(d.src) d.dst = t.generate_string(d.dst) @@ -138,6 +139,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 dotfiles def _load_file(self): """load the yaml file""" @@ -335,14 +337,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 +439,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 @@ -501,37 +504,6 @@ class Cfg: cnt += 1 return key - def short_to_long(self): - """transform all short keys to long keys""" - if not self.content[self.key_dotfiles]: - return - match = {} - new = {} - # handle the entries in dotfiles - for oldkey, v in self.content[self.key_dotfiles].items(): - path = v[self.key_dotfiles_dst] - path = os.path.expanduser(path) - newkey = self._get_long_key(path) - new[newkey] = v - match[oldkey] = newkey - # replace with new keys - self.content[self.key_dotfiles] = new - - # handle the entries in profiles - for k, v in self.lnk_profiles.items(): - if self.key_profiles_dots not in v: - continue - if not v[self.key_profiles_dots]: - continue - new = [] - for oldkey in v[self.key_profiles_dots]: - if oldkey == self.key_all: - continue - newkey = match[oldkey] - new.append(newkey) - # replace with new keys - v[self.key_profiles_dots] = new - def _dotfile_exists(self, dotfile): """return True and the existing dotfile key if it already exists, False and a new unique key otherwise""" @@ -609,7 +581,7 @@ class Cfg: return True, dotfile - def get_dotfiles(self, profile): + def _get_dotfiles(self, profile): """return a list of dotfiles for a specific profile""" if profile not in self.prodots: return [] diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 1f0f02f..4a7ea1d 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,43 @@ 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.install_diff, debug=o.debug, + totemp=tmpdir, + showdiff=o.install_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 +67,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 +132,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 +144,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 +165,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, + dry=o.dry, safe=o.safe, 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 +188,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 +197,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 +213,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 +251,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 +295,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 +335,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 +350,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 +376,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/dotfile.py b/dotdrop/dotfile.py index 0957690..2b3b090 100644 --- a/dotdrop/dotfile.py +++ b/dotdrop/dotfile.py @@ -12,8 +12,8 @@ class Dotfile: def __init__(self, key, dst, src, actions={}, trans_r=None, trans_w=None, - link=LinkTypes.NOLINK, cmpignore=[], noempty=False, - upignore=[]): + link=LinkTypes.NOLINK, cmpignore=[], + noempty=False, upignore=[]): """constructor @key: dotfile key @dst: dotfile dst (in user's home usually) diff --git a/dotdrop/logger.py b/dotdrop/logger.py index bccd30e..fec1e07 100644 --- a/dotdrop/logger.py +++ b/dotdrop/logger.py @@ -2,7 +2,7 @@ author: deadc0de6 (https://github.com/deadc0de6) Copyright (c) 2017, deadc0de6 -provides logging functions +provide logging functions """ import sys diff --git a/dotdrop/options.py b/dotdrop/options.py new file mode 100644 index 0000000..995a6ca --- /dev/null +++ b/dotdrop/options.py @@ -0,0 +1,194 @@ +""" +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 [-VbtfndD] [-c ] [-p ] [...] + dotdrop import [-Vbld] [-c ] [-p ] ... + dotdrop compare [-Vb] [-c ] [-p ] + [-o ] [-C ...] [-i ...] + dotdrop update [-VbfdkP] [-c ] [-p ] + [-i ...] [...] + dotdrop listfiles [-VbT] [-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 AttrMonitor: + _set_attr_err = False + + def __setattr__(self, key, value): + """monitor attribute setting""" + if not hasattr(self, key) and self._set_attr_err: + self._attr_change(key) + super(AttrMonitor, self).__setattr__(key, value) + + def _attr_set(self, attr): + """do something when unexistent attr is set""" + pass + + +class Options(AttrMonitor): + + def __init__(self, args=None): + """constructor + @args: argument dictionary (if None use sys) + """ + 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() + self._print_attr() + # start monitoring for bad attribute + self._set_attr_err = True + + def _header(self): + """print the header""" + self.log.log(BANNER) + self.log.log('') + + def _read_config(self): + """read the config file""" + 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): + """apply cli args as attribute""" + # 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.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[''] + self.install_diff = not self.args['--nodiff'] + self.install_showdiff = self.showdiff or self.args['--showdiff'] + # "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): + """create attributes from conf""" + # 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() + # the profiles + self.profiles = self.conf.get_profiles() + + def _print_attr(self): + """print all of this class attributes""" + if not self.debug: + return + 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)) + + def _attr_set(self, attr): + """error when some inexistent attr is set""" + raise Exception('bad option: {}'.format(attr)) diff --git a/dotdrop/updater.py b/dotdrop/updater.py index 9891756..524fe15 100644 --- a/dotdrop/updater.py +++ b/dotdrop/updater.py @@ -20,27 +20,23 @@ TILD = '~' class Updater: - def __init__(self, conf, dotpath, profile, variables, dry, safe, - iskey=False, debug=False, ignore=[], showpatch=False): + def __init__(self, dotpath, dotfiles, variables, dry=False, safe=True, + 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 - @iskey: will the update be called on keys or path @debug: enable debug @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 - self.iskey = iskey self.debug = debug self.ignore = ignore self.showpatch = showpatch @@ -78,22 +74,22 @@ class Updater: if self.debug: 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.expanduser(right) + path = os.path.expanduser(path) + dtpath = os.path.join(self.dotpath, dotfile.src) + dtpath = os.path.expanduser(dtpath) - if self._ignore([left, right]): + if self._ignore([path, dtpath]): return True if dotfile.trans_w: # apply write transformation if any new_path = self._apply_trans_w(path, dotfile) if not new_path: return False - left = new_path - if os.path.isdir(left): - ret = self._handle_dir(left, right) + path = new_path + if os.path.isdir(path): + ret = self._handle_dir(path, dtpath) else: - ret = self._handle_file(left, right) + ret = self._handle_file(path, dtpath) # clean temporary files if new_path and os.path.exists(new_path): utils.remove(new_path) @@ -128,7 +124,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 +137,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)) @@ -154,11 +150,13 @@ class Updater: def _is_template(self, path): if not Templategen.is_template(path): + if self.debug: + self.log.dbg('{} is NO template'.format(path)) return False self.log.warn('{} uses template, update manually'.format(path)) return True - def _show_patch(self, tpath, fpath): + def _show_patch(self, fpath, tpath): """provide a way to manually patch the template""" content = self._resolve_template(tpath) tmp = utils.write_to_tmpfile(content) @@ -172,49 +170,49 @@ class Updater: debug=self.debug) return t.generate(tpath) - def _handle_file(self, left, right, compare=True): - """sync left (deployed file) and right (dotdrop dotfile)""" - if self._ignore([left, right]): + def _handle_file(self, path, dtpath, compare=True): + """sync path (deployed file) and dtpath (dotdrop dotfile path)""" + if self._ignore([path, dtpath]): return True if self.debug: - self.log.dbg('update for file {} and {}'.format(left, right)) - if self._is_template(right): + self.log.dbg('update for file {} and {}'.format(path, dtpath)) + if self._is_template(dtpath): # dotfile is a template if self.debug: - self.log.dbg('{} is a template'.format(right)) + self.log.dbg('{} is a template'.format(dtpath)) if self.showpatch: - self._show_patch(right, left) + self._show_patch(path, dtpath) return False - if compare and filecmp.cmp(left, right, shallow=True): + if compare and filecmp.cmp(path, dtpath, shallow=True): # no difference if self.debug: - self.log.dbg('identical files: {} and {}'.format(left, right)) + self.log.dbg('identical files: {} and {}'.format(path, dtpath)) return True - if not self._overwrite(left, right): + if not self._overwrite(path, dtpath): return False try: if self.dry: - self.log.dry('would cp {} {}'.format(left, right)) + self.log.dry('would cp {} {}'.format(path, dtpath)) else: if self.debug: - self.log.dbg('cp {} {}'.format(left, right)) - shutil.copyfile(left, right) + self.log.dbg('cp {} {}'.format(path, dtpath)) + shutil.copyfile(path, dtpath) except IOError as e: - self.log.warn('{} update failed, do manually: {}'.format(left, e)) + self.log.warn('{} update failed, do manually: {}'.format(path, e)) return False return True - def _handle_dir(self, left, right): - """sync left (deployed dir) and right (dotdrop dir)""" + def _handle_dir(self, path, dtpath): + """sync path (deployed dir) and dtpath (dotdrop dir path)""" if self.debug: - self.log.dbg('handle update for dir {} to {}'.format(left, right)) + self.log.dbg('handle update for dir {} to {}'.format(path, dtpath)) # paths must be absolute (no tildes) - left = os.path.expanduser(left) - right = os.path.expanduser(right) - if self._ignore([left, right]): + path = os.path.expanduser(path) + dtpath = os.path.expanduser(dtpath) + if self._ignore([path, dtpath]): return True # find the differences - diff = filecmp.dircmp(left, right, ignore=None) + diff = filecmp.dircmp(path, dtpath, ignore=None) # handle directories diff return self._merge_dirs(diff) diff --git a/dotdrop/utils.py b/dotdrop/utils.py index 55422a0..70b1a61 100644 --- a/dotdrop/utils.py +++ b/dotdrop/utils.py @@ -10,8 +10,6 @@ import tempfile import os import uuid import shlex -import functools -import operator import fnmatch from shutil import rmtree diff --git a/scripts/short-to-long-key.py b/scripts/short-to-long-key.py deleted file mode 100755 index 45dbde0..0000000 --- a/scripts/short-to-long-key.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -""" -author: deadc0de6 (https://github.com/deadc0de6) -Copyright (c) 2017, deadc0de6 - -transform all short dotfile keys -from the short format to the long -format - -For example ~/.config/awesome/rc.lua - short format: f_rc.lua - long format: f_config_awesome_rc.lua - -You need to have dotdrop installed for this script -to work (either with setup.py or pypi). -""" - -from docopt import docopt -import sys -import os -from dotdrop.config import Cfg - - -USAGE = """ -short-to-long-key.py - -Usage: - short-to-long-key.py - short-to-long-key.py --help - -Options: - -h --help Show this screen. - -""" - - -def main(): - args = docopt(USAGE) - path = os.path.expanduser(args['']) - - try: - - conf = Cfg(path) - except ValueError as e: - print('error: {}'.format(str(e))) - return False - - conf.short_to_long() - print(conf.dump()) - - -if __name__ == '__main__': - if main(): - sys.exit(0) - sys.exit(1) diff --git a/tests-requirements.txt b/tests-requirements.txt index 3aea72f..2172e29 100644 --- a/tests-requirements.txt +++ b/tests-requirements.txt @@ -2,3 +2,4 @@ pycodestyle; python_version >= '3.0' nose; python_version >= '3.0' coverage; python_version >= '3.0' coveralls; python_version >= '3.0' +pyflakes; python_version >= '3.0' diff --git a/tests.sh b/tests.sh index 7a58f99..96d5fe0 100755 --- a/tests.sh +++ b/tests.sh @@ -12,6 +12,10 @@ pycodestyle --ignore=W605 dotdrop/ pycodestyle tests/ pycodestyle scripts/ +# pyflakes tests +pyflakes dotdrop/ +pyflakes tests/ + # retrieve the nosetests binary set +e nosebin="nosetests" diff --git a/tests/helpers.py b/tests/helpers.py index d43bd18..13b712d 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 strip_home 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.install_diff = True + o.link = LinkTypes.NOLINK.value + o.install_showdiff = True + o.debug = True + o.compare_dopts = '' + o.variables = {} + return o def get_path_strip_version(path): diff --git a/tests/test_compare.py b/tests/test_compare.py index d78cdb0..c336698 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,15 +26,16 @@ 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: + path = os.path.expanduser(dotfile.dst) ret, insttmp = inst.install_to_temp(t, tmp, dotfile.src, dotfile.dst) if not ret: @@ -42,10 +43,6 @@ class TestCompare(unittest.TestCase): continue diff = comp.compare(insttmp, dotfile.dst, ignore=['whatever', 'whatelse']) - print('XXXX diff for {} and {}:\n{}'.format(dotfile.src, - dotfile.dst, - diff)) - path = os.path.expanduser(dotfile.dst) results[path] = diff == '' return results @@ -110,32 +107,33 @@ 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, d9] # 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, d9: 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, d9: 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, d9: True} - results = self.compare(opts, conf, tmp, len(dfiles)) + results = self.compare(o, tmp, len(dfiles)) self.assertTrue(results == expected) # add file in directory @@ -143,7 +141,7 @@ class TestCompare(unittest.TestCase): self.assertTrue(os.path.exists(d7)) expected = {d1: False, d2: True, d3: True, d4: False, d5: False, d9: True} - results = self.compare(opts, conf, tmp, len(dfiles)) + results = self.compare(o, tmp, len(dfiles)) self.assertTrue(results == expected) # modify all files @@ -151,21 +149,23 @@ class TestCompare(unittest.TestCase): edit_content(d3, get_string(21)) expected = {d1: False, d2: False, d3: False, d4: False, d5: False, d9: True} - results = self.compare(opts, conf, tmp, len(dfiles)) + results = self.compare(o, tmp, len(dfiles)) self.assertTrue(results == expected) # edit sub file edit_content(d9f1, get_string(12)) expected = {d1: False, d2: False, d3: False, d4: False, d5: False, d9: 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.compare_focus = d4 + self.assertFalse(cmd_compare(o, tmp)) + o.compare_focus = '/tmp/fake' + self.assertFalse(cmd_compare(o, tmp)) def main(): diff --git a/tests/test_config.py b/tests/test_config.py index d92b9f6..460d6c7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,11 +8,9 @@ basic unittest for the config parser import unittest import os import yaml -import tempfile -import shutil from dotdrop.config import Cfg -from tests.helpers import * +from tests.helpers import get_tempdir, clean, create_fake_config class TestConfig(unittest.TestCase): @@ -88,16 +86,15 @@ class TestConfig(unittest.TestCase): self.assertTrue(conf is not None) # test profile - opts = conf.get_settings() profiles = conf.get_profiles() self.assertTrue(pf1key in profiles) self.assertTrue(pf2key in profiles) # test dotfiles - dotfiles = conf.get_dotfiles(pf1key) + dotfiles = conf._get_dotfiles(pf1key) self.assertTrue(df1key in [x.key for x in dotfiles]) self.assertTrue(df2key in [x.key for x in dotfiles]) - dotfiles = conf.get_dotfiles(pf2key) + dotfiles = conf._get_dotfiles(pf2key) self.assertTrue(df1key in [x.key for x in dotfiles]) self.assertFalse(df2key in [x.key for x in dotfiles]) diff --git a/tests/test_import.py b/tests/test_import.py index f72fb86..57b370b 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -12,11 +12,13 @@ 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 +from dotdrop.linktypes import LinkTypes -from tests.helpers import * +from tests.helpers import get_path_strip_version, edit_content, \ + load_options, create_random_file, \ + clean, get_string, get_dotfile_from_yaml, \ + get_tempdir, create_fake_config, create_dir class TestImport(unittest.TestCase): @@ -34,11 +36,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 +71,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 +109,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 +197,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..158e413 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.install_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..e07c5a5 100644 --- a/tests/test_listings.py +++ b/tests/test_listings.py @@ -7,15 +7,15 @@ basic unittest for the compare function import unittest import os -import yaml -from dotdrop.config import Cfg from dotdrop.dotdrop import cmd_list_profiles from dotdrop.dotdrop import cmd_list_files from dotdrop.dotdrop import cmd_detail from dotdrop.dotdrop import cmd_importer -from tests.helpers import * +from tests.helpers import create_dir, get_string, get_tempdir, \ + create_random_file, load_options, \ + create_fake_config, clean class TestListings(unittest.TestCase): @@ -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..ba994e0 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -10,9 +10,10 @@ import os from dotdrop.dotdrop import cmd_update from dotdrop.dotdrop import cmd_importer +from dotdrop.action import Transform 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): @@ -53,6 +54,31 @@ class TestUpdate(unittest.TestCase): self.assertTrue(os.path.exists(d2)) self.addCleanup(clean, d2) + # template + d3t, c3t = create_random_file(fold_config) + self.assertTrue(os.path.exists(d3t)) + self.addCleanup(clean, d3t) + + # sub dirs + dsubstmp = get_tempdir() + self.assertTrue(os.path.exists(dsubstmp)) + self.addCleanup(clean, dsubstmp) + dirsubs = os.path.basename(dsubstmp) + + dir1string = 'somedir' + dir1 = os.path.join(dsubstmp, dir1string) + create_dir(dir1) + dir1sub1str = 'sub1' + sub1 = os.path.join(dir1, dir1sub1str) + create_dir(sub1) + dir1sub2str = 'sub2' + sub2 = os.path.join(dir1, dir1sub2str) + create_dir(sub2) + f1s1, f1s1c1 = create_random_file(sub1) + self.assertTrue(os.path.exists(f1s1)) + f1s2, f1s2c1 = create_random_file(sub2) + self.assertTrue(os.path.exists(f1s2)) + # create the directory to test dpath = os.path.join(fold_config, get_string(5)) dir1 = create_dir(dpath) @@ -67,12 +93,47 @@ class TestUpdate(unittest.TestCase): backup=self.CONFIG_BACKUP, create=self.CONFIG_CREATE) self.assertTrue(os.path.exists(confpath)) - conf, opts = load_config(confpath, profile) - dfiles = [d1, dir1, d2] + o = load_options(confpath, profile) + o.debug = True + o.update_showpatch = True + dfiles = [d1, dir1, d2, d3t, dsubstmp] # import the files - cmd_importer(opts, conf, dfiles) - conf, opts = load_config(confpath, profile) + o.import_path = dfiles + cmd_importer(o) + + # get new config + o = load_options(confpath, profile) + o.safe = False + o.debug = True + o.update_showpatch = True + trans = Transform('trans', 'cp -r {0} {1}') + d3tb = os.path.basename(d3t) + for dotfile in o.dotfiles: + if os.path.basename(dotfile.dst) == d3tb: + # patch the template + src = os.path.join(o.dotpath, dotfile.src) + src = os.path.expanduser(src) + edit_content(src, '{{@@ profile @@}}') + if os.path.basename(dotfile.dst) == dirsubs: + # retrieve the path of the sub in the dotpath + d1indotpath = os.path.join(o.dotpath, dotfile.src) + d1indotpath = os.path.expanduser(d1indotpath) + dotfile.trans_w = trans + + # update template + o.update_path = [d3t] + self.assertFalse(cmd_update(o)) + + # update sub dirs + gone = os.path.join(d1indotpath, dir1string) + gone = os.path.join(gone, dir1sub1str) + self.assertTrue(os.path.exists(gone)) + clean(sub1) # dir1sub1str + self.assertTrue(os.path.exists(gone)) + o.update_path = [dsubstmp] + cmd_update(o) + self.assertFalse(os.path.exists(gone)) # edit the files edit_content(d1, 'newcontent') @@ -87,9 +148,8 @@ class TestUpdate(unittest.TestCase): create_random_file(dpath) # update it - opts['safe'] = False - opts['debug'] = True - cmd_update(opts, conf, [d1, dir1]) + o.update_path = [d1, dir1] + cmd_update(o) # test content newcontent = open(d1, 'r').read() @@ -100,7 +160,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 +168,9 @@ 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.update_path = [d2key] + o.update_iskey = True + cmd_update(o) # test content newcontent = open(d2, 'r').read()