From 262dd45823d0b59db6d77537ea8a96848f756546 Mon Sep 17 00:00:00 2001 From: moyiz Date: Wed, 3 May 2017 10:04:14 +0300 Subject: [PATCH 1/9] Add link support to config file and dotfile --- dotdrop/config.py | 8 +++++--- dotdrop/dotfile.py | 9 +++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/dotdrop/config.py b/dotdrop/config.py index 5cd02b2..9757dad 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -18,6 +18,7 @@ class Cfg: key_dotpath = 'dotpath' key_dotfiles_src = 'src' key_dotfiles_dst = 'dst' + key_dotfiles_link = 'link' def __init__(self, cfgpath): if not os.path.exists(cfgpath): @@ -61,7 +62,9 @@ class Cfg: for k, v in self.content[self.key_dotfiles].items(): src = v[self.key_dotfiles_src] dst = v[self.key_dotfiles_dst] - self.dotfiles[k] = Dotfile(k, dst, src) + link = v[self.key_dotfiles_link] if self.key_dotfiles_link in v + else False + self.dotfiles[k] = Dotfile(k, dst, src, link) # contains a list of dotfiles defined for each profile for k, v in self.profiles.items(): self.prodots[k] = [] @@ -115,8 +118,7 @@ class Cfg: """ returns a list of dotfiles for a specific profile """ if profile not in self.prodots: return [] - tmp = sorted(self.prodots[profile], key=lambda x: x.key, reverse=True) - return tmp + return sorted(self.prodots[profile], key=lambda x: x.key, reverse=True) def get_profiles(self): """ returns all defined profiles """ diff --git a/dotdrop/dotfile.py b/dotdrop/dotfile.py index c696631..a7e53b0 100644 --- a/dotdrop/dotfile.py +++ b/dotdrop/dotfile.py @@ -7,15 +7,16 @@ represents a dotfile in dotdrop class Dotfile: - def __init__(self, key, dst, src): + def __init__(self, key, dst, src, link=False): # key of dotfile in the config self.key = key # where to install this dotfile self.dst = dst # stored dotfile in dotdrop self.src = src + # should be a link + self.link = link def __str__(self): - string = 'key:%s, src: %s, dst: %s' % (self.key, - self.src, self.dst) - return string + return 'key:%s, src: %s, dst: %s, link: %s' % (self.key, self.src, + self.dst, self.link) From d40ea36187baf94455e1e0ac363f7c3ce3402492 Mon Sep 17 00:00:00 2001 From: moyiz Date: Wed, 3 May 2017 10:04:39 +0300 Subject: [PATCH 2/9] Extend .gitignore --- .gitignore | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0d20b64..ff91c97 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,99 @@ -*.pyc +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + From c4aab509f7d4022080046f1b8eb5a49c37f966a5 Mon Sep 17 00:00:00 2001 From: moyiz Date: Wed, 3 May 2017 11:00:52 +0300 Subject: [PATCH 3/9] Add support for soft links --- README.md | 3 ++- dotdrop/config.py | 4 ++-- dotdrop/dotdrop.py | 5 ++++- dotdrop/installer.py | 28 +++++++++++++++++++++++++++- dotdrop/utils.py | 15 +++++++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a00f1c5..30b4bc6 100644 --- a/README.md +++ b/README.md @@ -177,11 +177,12 @@ the following entries: * `dotpath`: path to the folder containing the dotfiles to be managed by dotdrop (absolute path or relative to the config file location) * **dotfiles** entry: a list of dotfiles in the form - + * When `link` is true, dotdrop will create a link instead of copying. Template generation (as in [template](#template)) is not supported when `link` is true. ``` : dst: src: + link: # Optional ``` * **profiles** entry: a list of profiles with a sublist diff --git a/dotdrop/config.py b/dotdrop/config.py index 9757dad..597fdf8 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -62,8 +62,8 @@ class Cfg: for k, v in self.content[self.key_dotfiles].items(): src = v[self.key_dotfiles_src] dst = v[self.key_dotfiles_dst] - link = v[self.key_dotfiles_link] if self.key_dotfiles_link in v - else False + link = v[self.key_dotfiles_link] if self.key_dotfiles_link \ + in v else False self.dotfiles[k] = Dotfile(k, dst, src, link) # contains a list of dotfiles defined for each profile for k, v in self.profiles.items(): diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index dad1ca1..a6096d4 100755 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -66,7 +66,10 @@ def install(opts, conf): diff=opts['installdiff']) installed = [] for dotfile in dotfiles: - r = inst.install(t, opts['profile'], dotfile.src, dotfile.dst) + if hasattr(dotfile, "link") and dotfile.link: + r = inst.link(dotfile.src, dotfile.dst) + else: + r = inst.install(t, opts['profile'], dotfile.src, dotfile.dst) installed.extend(r) LOG.log('\n%u dotfile(s) installed.' % (len(installed))) return True diff --git a/dotdrop/installer.py b/dotdrop/installer.py index 658cb53..35502ec 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -32,6 +32,32 @@ class Installer: return self._handle_dir(templater, profile, src, dst) return self._handle_file(templater, profile, src, dst) + def link(self, src, dst): + '''Sets src as the link target of dst''' + src = os.path.join(self.base, os.path.expanduser(src)) + dst = os.path.join(self.base, os.path.expanduser(dst)) + if os.path.exists(dst): + if os.path.realpath(dst) == os.path.realpath(src): + self.log.sub('ignoring "{}", link exists'.format(dst)) + return [] + if self.dry: + self.log.dry('would remove {} and link it to {}' + .format(dst, src)) + return [] + if self.safe and not self.log.ask('Remove "{}" for link creation?' + .format(dst)): + self.log.warn('ignoring "{}", link was not created' + .format(dst)) + return [] + utils.remove(dst) + if self.dry: + self.log.dry('would link {} to {}'.format(dst, src)) + return [] + os.symlink(src, dst) + self.log.sub('linked %s to %s' % (dst, src)) + # Follows original developer's behavior + return [(src, dst)] + def _handle_file(self, templater, profile, src, dst): '''Install a file using templater for "profile"''' content = templater.generate(src, profile) @@ -47,7 +73,7 @@ class Installer: self.log.sub('ignoring \"%s\", same content' % (dst)) return [] if ret == 0: - if not self.quiet: + if not self.quiet and not self.dry: self.log.sub('copied %s to %s' % (src, dst)) return [(src, dst)] return [] diff --git a/dotdrop/utils.py b/dotdrop/utils.py index e48e7bc..5d78efe 100644 --- a/dotdrop/utils.py +++ b/dotdrop/utils.py @@ -6,7 +6,10 @@ utilities import subprocess import tempfile +import os from logger import Logger +from shutil import rmtree + LOG = Logger() @@ -29,3 +32,15 @@ def diff(src, dst, log=False, raw=True): def get_tmpdir(): return tempfile.mkdtemp(prefix='dotdrop-') + + +def remove(path): + ''' Remove a file / directory / symlink ''' + if not os.path.exists(path): + raise Exception("ERROR in remove: File not found: {}".format(path)) + if os.path.islink(path) or os.path.isfile(path): + os.unlink(path) + elif os.path.isdir(path): + rmtree(path) + else: + raise Exception("Unsupported file type for deletion: {}".format(path)) From 6a178d746d15df29bfd81b762ab5664d470b527a Mon Sep 17 00:00:00 2001 From: moyiz Date: Fri, 5 May 2017 16:33:55 +0300 Subject: [PATCH 4/9] Changed string formatting, raise OSError instead of Exception, and catch it in link to skip the current dotfile --- dotdrop/installer.py | 21 ++++++++++++--------- dotdrop/utils.py | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/dotdrop/installer.py b/dotdrop/installer.py index 35502ec..23a8dfb 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -38,20 +38,23 @@ class Installer: dst = os.path.join(self.base, os.path.expanduser(dst)) if os.path.exists(dst): if os.path.realpath(dst) == os.path.realpath(src): - self.log.sub('ignoring "{}", link exists'.format(dst)) + self.log.sub('ignoring "%s", link exists' % dst) return [] if self.dry: - self.log.dry('would remove {} and link it to {}' - .format(dst, src)) + self.log.dry('would remove %s and link it to %s'\ + % (dst, src)) return [] - if self.safe and not self.log.ask('Remove "{}" for link creation?' - .format(dst)): - self.log.warn('ignoring "{}", link was not created' - .format(dst)) + if self.safe and \ + not self.log.ask('Remove "%s" for link creation?' % dst): + self.log.warn('ignoring "%s", link was not created' % dst) + return [] + try: + utils.remove(dst) + except OSError: + self.log.err('something went wrong with %s' % src) return [] - utils.remove(dst) if self.dry: - self.log.dry('would link {} to {}'.format(dst, src)) + self.log.dry('would link %s to %s' % (dst, src)) return [] os.symlink(src, dst) self.log.sub('linked %s to %s' % (dst, src)) diff --git a/dotdrop/utils.py b/dotdrop/utils.py index 5d78efe..8c2f450 100644 --- a/dotdrop/utils.py +++ b/dotdrop/utils.py @@ -37,10 +37,10 @@ def get_tmpdir(): def remove(path): ''' Remove a file / directory / symlink ''' if not os.path.exists(path): - raise Exception("ERROR in remove: File not found: {}".format(path)) + raise OSError("File not found: %s" % path) if os.path.islink(path) or os.path.isfile(path): os.unlink(path) elif os.path.isdir(path): rmtree(path) else: - raise Exception("Unsupported file type for deletion: {}".format(path)) + raise OSError("Unsupported file type for deletion: %s" % path) From 05b78ca5fe43345f35b0ea1a6fbb5c4c8fc666c9 Mon Sep 17 00:00:00 2001 From: moyiz Date: Fri, 5 May 2017 17:02:18 +0300 Subject: [PATCH 5/9] Add support for importing dotfiles as links --- dotdrop/config.py | 4 +++- dotdrop/dotdrop.py | 16 +++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/dotdrop/config.py b/dotdrop/config.py index 597fdf8..213463c 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -87,7 +87,7 @@ class Cfg: return absconf return dotpath - def new(self, dotfile, profile): + def new(self, dotfile, profile, link=False): """ import new dotfile """ dots = self.content[self.key_dotfiles] if dots is None: @@ -103,6 +103,8 @@ class Cfg: self.key_dotfiles_dst: dotfile.dst, self.key_dotfiles_src: dotfile.src } + if link: + dots[dotfile.key][self.key_dotfiles_link] = True profiles = self.profiles if profile in profiles and profiles[profile] != [self.key_all]: if self.content[self.key_profiles][profile] is None: diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index a6096d4..23f108d 100755 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -34,7 +34,7 @@ Usage: [(-f | --force)] [--nodiff] [--dry] dotdrop.py compare [--profile=] [--cfg=] dotdrop.py list [--cfg=] - dotdrop.py import [--cfg=] [--profile=] [--dry] ... + dotdrop.py import [(-l | --link)] [--cfg=] [--profile=] [--dry] ... dotdrop.py (-h | --help) dotdrop.py (-v | --version) @@ -43,6 +43,7 @@ Options: --cfg= Path to the config [default: %s/config.yaml]. --dry Dry run. --nodiff Do not diff when installing [default: False]. + -l --link Import the file and add a link to it [default: False]. -f --force Do not warn if exists [default: False]. -v --version Show version. -h --help Show this screen. @@ -101,8 +102,7 @@ def importer(opts, conf, paths): key = dst.split(os.sep)[-1] if key == 'config': key = '_'.join(dst.split(os.sep)[-2:]) - key = key.lstrip('.') - key = key.lower() + key = key.lstrip('.').lower() if os.path.isdir(dst): key = 'd_%s' % (key) else: @@ -116,17 +116,22 @@ def importer(opts, conf, paths): if os.path.exists(srcf): LOG.err('\"%s\" already exists, ignored !' % (srcf)) continue - conf.new(dotfile, opts['profile']) + conf.new(dotfile, opts['profile'], opts['link']) cmd = ['mkdir', '-p', '%s' % (os.path.dirname(srcf))] if opts['dry']: LOG.dry('would run: %s' % (' '.join(cmd))) else: utils.run(cmd, raw=False, log=False) - cmd = ['cp', '-r', '%s' % (dst), '%s' % (srcf)] + if opts['link']: + cmd = ['mv', '%s' % (dst), '%s' % (srcf)] + else: + cmd = ['cp', '-r', '%s' % (dst), '%s' % (srcf)] if opts['dry']: LOG.dry('would run: %s' % (' '.join(cmd))) else: utils.run(cmd, raw=False, log=False) + if opts['link']: + os.symlink(srcf, dst) LOG.sub('\"%s\" imported' % (path)) cnt += 1 if opts['dry']: @@ -159,6 +164,7 @@ if __name__ == '__main__': opts['profile'] = args['--profile'] opts['safe'] = not args['--force'] opts['installdiff'] = not args['--nodiff'] + opts['link'] = args['--link'] header() From befd1848ac37ff90f4c0efbb228c3f531f808962 Mon Sep 17 00:00:00 2001 From: moyiz Date: Fri, 5 May 2017 17:42:55 +0300 Subject: [PATCH 6/9] pep8 --- dotdrop/dotdrop.py | 5 +++-- dotdrop/installer.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 23f108d..176a6f0 100755 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -34,7 +34,8 @@ Usage: [(-f | --force)] [--nodiff] [--dry] dotdrop.py compare [--profile=] [--cfg=] dotdrop.py list [--cfg=] - dotdrop.py import [(-l | --link)] [--cfg=] [--profile=] [--dry] ... + dotdrop.py import [--profile=] [--cfg=] + [(-l | --link)] [--dry] ... dotdrop.py (-h | --help) dotdrop.py (-v | --version) @@ -43,7 +44,7 @@ Options: --cfg= Path to the config [default: %s/config.yaml]. --dry Dry run. --nodiff Do not diff when installing [default: False]. - -l --link Import the file and add a link to it [default: False]. + -l --link Import and link [default: False]. -f --force Do not warn if exists [default: False]. -v --version Show version. -h --help Show this screen. diff --git a/dotdrop/installer.py b/dotdrop/installer.py index 23a8dfb..7581c3a 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -41,11 +41,11 @@ class Installer: self.log.sub('ignoring "%s", link exists' % dst) return [] if self.dry: - self.log.dry('would remove %s and link it to %s'\ + self.log.dry('would remove %s and link it to %s' % (dst, src)) return [] if self.safe and \ - not self.log.ask('Remove "%s" for link creation?' % dst): + not self.log.ask('Remove "%s" for link creation?' % dst): self.log.warn('ignoring "%s", link was not created' % dst) return [] try: From 550011cb8980143f89842aee93fcfa763d8be588 Mon Sep 17 00:00:00 2001 From: moyiz Date: Fri, 5 May 2017 17:54:39 +0300 Subject: [PATCH 7/9] Add new opt parameter (link) to load_config --- tests/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/helpers.py b/tests/helpers.py index 35c7c1b..c6cc804 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -68,6 +68,7 @@ def load_config(confpath, profile): opts['profile'] = profile opts['safe'] = True opts['installdiff'] = True + opts['link'] = False return conf, opts From 1271c978f9b783109a375bcd038484135500d5ee Mon Sep 17 00:00:00 2001 From: moyiz Date: Fri, 5 May 2017 20:32:14 +0300 Subject: [PATCH 8/9] Add common editors to gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0ce4f10..0db4888 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *.pyc -.coverage \ No newline at end of file +.coverage +.idea/ +.vscode/ +.atom/ From fa6fabbd9008c97fe4dbb895eb1ac532c23a2ddb Mon Sep 17 00:00:00 2001 From: moyiz Date: Fri, 5 May 2017 20:32:32 +0300 Subject: [PATCH 9/9] Add a basic link test --- tests/test_install.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_install.py b/tests/test_install.py index 5c17be0..f0a0bee 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -42,6 +42,7 @@ exec bspwm f.write(' %s:\n' % (d.key)) f.write(' dst: %s\n' % (d.dst)) f.write(' src: %s\n' % (d.src)) + f.write(' link: %s\n' % str(d.link).lower()) f.write('profiles:\n') f.write(' %s:\n' % (profile)) for d in dotfiles: @@ -78,10 +79,15 @@ exec bspwm with open(dst4, 'w') as f: f.write(get_string(16)) + # to test link + f5, c5 = create_random_file(tmp) + dst5 = os.path.join(dst, get_string(6)) + d5 = Dotfile(get_string(6), dst5, os.path.basename(f5), link=True) + # generate the config and stuff profile = get_string(5) confpath = os.path.join(tmp, self.CONFIG_NAME) - self.fake_config(confpath, [d1, d2, d3, d4], profile, tmp) + self.fake_config(confpath, [d1, d2, d3, d4, d5], profile, tmp) conf = Cfg(confpath) self.assertTrue(conf is not None) @@ -94,6 +100,11 @@ exec bspwm self.assertTrue(os.path.exists(dst1)) self.assertTrue(os.path.exists(dst2)) self.assertTrue(os.path.exists(dst3)) + self.assertTrue(os.path.exists(dst5)) + + # check if 'dst5' is a link whose target is 'f5' + self.assertTrue(os.path.islink(dst5)) + self.assertTrue(os.path.realpath(dst5) == os.path.realpath(f5)) # make sure backup is there b = dst4 + Installer.BACKUP_SUFFIX