diff options
-rw-r--r-- | dev-requirements.txt | 4 | ||||
-rw-r--r-- | dotfiles/cli.py | 2 | ||||
-rw-r--r-- | dotfiles/dotfile.py | 51 | ||||
-rw-r--r-- | dotfiles/pathutils.py | 21 | ||||
-rw-r--r-- | dotfiles/repository.py | 15 | ||||
-rw-r--r-- | setup.py | 54 | ||||
-rw-r--r-- | tests/conftest.py | 4 | ||||
-rw-r--r-- | tests/test_dotfile.py | 108 |
8 files changed, 158 insertions, 101 deletions
diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..95bea23 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,4 @@ +-e . +pytest +pytest-flake8 +pytest-runner diff --git a/dotfiles/cli.py b/dotfiles/cli.py index a99d098..9a8fd17 100644 --- a/dotfiles/cli.py +++ b/dotfiles/cli.py @@ -142,6 +142,8 @@ def status(repos, all, color): state_info['missing']['color'] = 'yellow' state_info['conflict']['color'] = 'magenta' + # XXX: could display tree [https://realpython.com/python-pathlib/] + for repo in repos: if len(repos) > 1: click.secho('%s:' % repo.path) diff --git a/dotfiles/dotfile.py b/dotfiles/dotfile.py index e539c68..3932d2a 100644 --- a/dotfiles/dotfile.py +++ b/dotfiles/dotfile.py @@ -1,4 +1,5 @@ from click import echo +from hashlib import md5 from pathlib import Path from .exceptions import \ @@ -33,7 +34,7 @@ class Dotfile(object): if debug: echo('MKDIR %s' % dir) else: - dir.mkdir() + dir.mkdir(parents=True) ensure(self.name.parent, debug) ensure(self.target.parent, debug) @@ -42,20 +43,20 @@ class Dotfile(object): """Create a symlink from name to target, no error checking.""" source = self.name target = self.target - if self.name.islink(): + if self.name.is_symlink(): source = self.target target = self.name.realpath() if debug: echo('LINK %s -> %s' % (source, target)) else: - source.mksymlinkto(target, absolute=0) + source.symlink_to(target) def _unlink(self, debug): """Remove a symlink in the home directory, no error checking.""" if debug: echo('UNLINK %s' % self.name) else: - self.name.remove() + self.name.unlink() def short_name(self, homedir): """A shorter, more readable name given a home directory.""" @@ -64,24 +65,29 @@ class Dotfile(object): def is_present(self): """Is this dotfile present in the repository?""" - return self.name.islink() and (self.name.realpath() == self.target) + # return self.name.islink() and (self.name.realpath() == self.target) + return self.name.is_symlink() and (self.name.resolve() == self.target) @property def state(self): """The current state of this dotfile.""" - if self.target.check(exists=0): + if not self.target.exists(): # only for testing, cli should never reach this state return 'error' - elif self.name.check(exists=0): + # elif self.name.check(exists=0): + elif not self.name.exists(): # no $HOME file or symlink return 'missing' - elif self.name.islink(): + # elif self.name.islink(): + elif self.name.is_symlink(): # name exists, is a link, but isn't a link to the target if not self.name.samefile(self.target): return 'conflict' else: # name exists, is a file, but differs from the target - if self.name.computehash() != self.target.computehash(): + # if self.name.computehash() != self.target.computehash(): + if md5(self.name.read_bytes()).hexdigest() != \ + md5(self.target.read_bytes()).hexdigest(): return 'conflict' return 'ok' @@ -89,42 +95,49 @@ class Dotfile(object): """Move a dotfile to it's target and create a symlink.""" if self.is_present(): raise IsSymlink(self.name) - if self.target.check(exists=1): + # if self.target.check(exists=1): + if self.target.exists(): raise TargetExists(self.name) self._ensure_dirs(debug) - if not self.name.islink(): + if not self.name.is_symlink(): if debug: echo('MOVE %s -> %s' % (self.name, self.target)) else: - self.name.move(self.target) + # self.name.move(self.target) + self.name.replace(self.target) self._link(debug) def remove(self, debug=False): """Remove a symlink and move the target back to its name.""" - if self.name.check(link=0): + # if self.name.check(link=0): + if not self.name.is_symlink(): raise NotASymlink(self.name) - if self.target.check(exists=0): + # if self.target.check(exists=0): + if not self.target.is_file(): raise TargetMissing(self.name) self._unlink(debug) if debug: echo('MOVE %s -> %s' % (self.target, self.name)) else: - self.target.move(self.name) + self.target.replace(self.name) def link(self, debug=False): """Create a symlink from name to target.""" - if self.name.check(exists=1): + # if self.name.check(exists=1): + if self.name.exists(): raise Exists(self.name) - if self.target.check(exists=0): + # if self.target.check(exists=0): + if not self.target.exists(): raise TargetMissing(self.name) self._ensure_dirs(debug) self._link(debug) def unlink(self, debug=False): """Remove a symlink from name to target.""" - if self.name.check(link=0): + # if self.name.check(link=0): + if not self.name.is_symlink(): raise NotASymlink(self.name) - if self.target.check(exists=0): + if not self.target.exists(): raise TargetMissing(self.name) if not self.name.samefile(self.target): raise RuntimeError diff --git a/dotfiles/pathutils.py b/dotfiles/pathutils.py new file mode 100644 index 0000000..0b051b1 --- /dev/null +++ b/dotfiles/pathutils.py @@ -0,0 +1,21 @@ +# TODO: docstrings + + +def is_file(path): + return path.is_file() and not path.is_symlink() + + +def is_link(path): + return path.is_file() and path.is_symlink() + + +def mkdir(path): + try: + path.mkdir(parents=True) + except FileExistsError: + pass + + +def touch(path): + mkdir(path.parent) + path.touch() diff --git a/dotfiles/repository.py b/dotfiles/repository.py index 9197c2a..b06d250 100644 --- a/dotfiles/repository.py +++ b/dotfiles/repository.py @@ -1,5 +1,12 @@ import os +# import sys +# if sys.version_info < (3, 4): +# from pathlib2 import Path +# #import pathlib2 as pathlib +# else: +# from pathlib import Path + from click import echo from pathlib import Path from fnmatch import fnmatch @@ -65,7 +72,7 @@ class Repository(object): def _ignore(self, path): for pattern in self.ignore_patterns: - if fnmatch(path, '*/%s' % pattern): + if fnmatch(str(path), '*/%s' % pattern): return True return False @@ -96,9 +103,9 @@ class Repository(object): target = self._dotfile_target(path) - if not fnmatch(path, '%s/*' % self.homedir): + if not fnmatch(str(path), '%s/*' % self.homedir): raise NotRootedInHome(path) - if fnmatch(path, '%s/*' % self.path): + if fnmatch(str(path), '%s/*' % self.path): raise InRepository(path) if self._ignore(target): raise TargetIgnored(path) @@ -162,7 +169,7 @@ class Repository(object): return self._ignore(path) or path == str(self.path) dirs = reversed([dir for dir, subdirs, files in - os.walk(self.path) if not skip(dir)]) + os.walk(str(self.path)) if not skip(dir)]) for dir in dirs: if not len(os.listdir(dir)): @@ -1,42 +1,44 @@ -from setuptools import setup, find_packages -from dotfiles import __version__ +import io +import re +from setuptools import setup + +with io.open("README.md", "rt", encoding="utf8") as f: + readme = f.read() + +with io.open("dotfiles/__init__.py", "rt", encoding="utf8") as f: + version = re.search(r"__version__ = \'(.*?)\'", f.read()).group(1) setup( name='dotfiles', - version=__version__, - description='Easily manage your dotfiles', - long_description=open('README.md', encoding='utf-8').read(), - long_description_content_type='text/markdown', - author='Jon Bernard', - author_email='jbernard@jbernard.io', - license='ISC', + version=version, url='https://github.com/jbernard/dotfiles', - packages=find_packages(), - tests_require=[ - 'pytest', - 'pytest-flake8', - ], - setup_requires=['pytest-runner'], - entry_points={ - 'console_scripts': [ - 'dotfiles=dotfiles.cli:cli', - ], + project_urls={ + 'Code': 'https://github.com/jbernard/dotfiles', + 'Issues': 'https://github.com/jbernard/dotfiles/issues', }, - install_requires=['click'], + license='ISC', + author='Jon Bernard', + author_email='jbernard@jbernard.io', + description='Easily manage your dotfiles', + long_description=readme, + long_description_content_type='text/markdown', + packages=["dotfiles"], + python_requires=">=3.6", classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: ISC License (ISCL)' 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Topic :: Utilities', ], - project_urls={ - 'Bug Reports': 'https://github.com/jbernard/dotfiles/issues', - 'Source': 'https://github.com/jbernard/dotfiles', + install_requires=['click'], + setup_requires=['pytest-runner'], + tests_require=['pytest', 'pytest-flake8'], + entry_points={ + 'console_scripts': [ + 'dotfiles=dotfiles.cli:cli', + ], }, ) diff --git a/tests/conftest.py b/tests/conftest.py index f5beb21..41f8995 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,8 +6,8 @@ from dotfiles.repository import Repository @pytest.fixture(scope='function', params=['', 'home']) def repo(request, tmpdir): - path = tmpdir.ensure_dir('repo') - home = tmpdir.ensure_dir(request.param) + path = str(tmpdir.ensure_dir('repo')) + home = str(tmpdir.ensure_dir(request.param)) return Repository(path, home) diff --git a/tests/test_dotfile.py b/tests/test_dotfile.py index 5672c53..d992bdb 100644 --- a/tests/test_dotfile.py +++ b/tests/test_dotfile.py @@ -2,26 +2,27 @@ import pytest from pathlib import Path from dotfiles.dotfile import Dotfile -from dotfiles.exceptions import IsSymlink, NotASymlink, TargetExists, \ - TargetMissing, Exists +from dotfiles.pathutils import is_file, is_link, touch, mkdir +from dotfiles.exceptions import TargetExists, IsSymlink, \ + TargetMissing, NotASymlink, Exists def _make_dotfile(repo, name, target=None): - return Dotfile(repo.homedir.join(name), - repo.path.join(target if target is not None else name)) + return Dotfile(repo.homedir.joinpath(name), + repo.path.joinpath(target if target else name)) @pytest.mark.parametrize('name', ['.a']) def test_str(repo, name): dotfile = _make_dotfile(repo, name, '.b') - assert str(dotfile) == repo.homedir.join(name) + assert dotfile.name == repo.homedir / name @pytest.mark.parametrize('name', ['.foo']) def test_short_name(repo, name): dotfile = _make_dotfile(repo, name) - assert dotfile.name == repo.homedir.join(name) - assert dotfile.short_name(repo.homedir) == name + assert dotfile.name == repo.homedir / name + assert dotfile.short_name(repo.homedir) == Path(name) def test_is_present(repo): @@ -30,77 +31,81 @@ def test_is_present(repo): # TODO: more -# {{{1 state def test_state(repo): dotfile = _make_dotfile(repo, '.vimrc', 'vimrc') assert dotfile.state == 'error' - dotfile.target.ensure() - dotfile.name.mksymlinkto(dotfile.target) + dotfile.target.touch() + dotfile.name.symlink_to(dotfile.target) assert dotfile.state == 'ok' - dotfile.name.remove() + dotfile.name.unlink() assert dotfile.state == 'missing' - dotfile.name.ensure() + dotfile.name.touch() assert dotfile.state == 'ok' - dotfile.name.write('test content') + dotfile.name.write_text('test content') assert dotfile.state == 'conflict' - dotfile.target.write('test content') + dotfile.target.write_text('test content') assert dotfile.state == 'ok' -# {{{1 add @pytest.mark.parametrize('path', ['.foo', '.foo/bar/baz']) def test_add(repo, path): dotfile = _make_dotfile(repo, path) - dotfile.target.ensure() - dotfile.name.ensure() + + touch(dotfile.name) + touch(dotfile.target) with pytest.raises(TargetExists): dotfile.add() + dotfile.target.unlink() - dotfile.target.remove() dotfile.add() - - assert dotfile.target.check(file=1, link=0) - assert dotfile.name.check(file=1, link=1) + assert is_link(dotfile.name) + assert is_file(dotfile.target) assert dotfile.name.samefile(dotfile.target) with pytest.raises(IsSymlink): dotfile.add() - - assert dotfile.target.check(file=1, link=0) - assert dotfile.name.check(file=1, link=1) + assert is_file(dotfile.target) assert dotfile.name.samefile(dotfile.target) -# {{{1 remove @pytest.mark.parametrize('path', ['.foo', '.foo/bar/baz']) def test_remove(repo, path): dotfile = _make_dotfile(repo, path) - py.path.local(dotfile.name.dirname).ensure_dir() - dotfile.name.mksymlinkto(dotfile.target) + + # py.path.local(dotfile.name.dirname).ensure_dir() + # dotfile.name.mksymlinkto(dotfile.target) + # dotfile.name.parent.mkdir(parents=True) + mkdir(dotfile.name.parent) + dotfile.name.symlink_to(dotfile.target) with pytest.raises(TargetMissing): dotfile.remove() - dotfile.target.ensure() + # dotfile.target.ensure() + # dotfile.target.touch() + touch(dotfile.target) dotfile.remove() - assert dotfile.target.check(exists=0) - assert dotfile.name.check(file=1, link=0) + # assert dotfile.target.check(exists=0) + # assert dotfile.name.check(file=1, link=0) + assert not dotfile.target.exists() + assert is_file(dotfile.name) with pytest.raises(NotASymlink): dotfile.remove() - assert dotfile.target.check(exists=0) - assert dotfile.name.check(file=1, link=0) + # assert dotfile.target.check(exists=0) + # assert dotfile.name.check(file=1, link=0) + assert not dotfile.target.exists() + assert is_file(dotfile.name) -# {{{1 link @pytest.mark.parametrize('path', ['.foo', '.foo/bar/baz']) def test_link(repo, path): dotfile = _make_dotfile(repo, path) @@ -108,22 +113,21 @@ def test_link(repo, path): with pytest.raises(TargetMissing): dotfile.link() - dotfile.target.ensure() + touch(dotfile.target) dotfile.link() - assert dotfile.target.check(file=1, link=0) - assert dotfile.name.check(file=1, link=1) + assert is_file(dotfile.target) + assert is_link(dotfile.name) assert dotfile.name.samefile(dotfile.target) with pytest.raises(Exists): dotfile.link() - assert dotfile.target.check(file=1, link=0) - assert dotfile.name.check(file=1, link=1) + assert is_file(dotfile.target) + assert is_link(dotfile.name) assert dotfile.name.samefile(dotfile.target) -# {{{1 unlink @pytest.mark.parametrize('path', ['.foo', '.foo/bar/baz']) def test_unlink(repo, path): dotfile = _make_dotfile(repo, path) @@ -131,26 +135,30 @@ def test_unlink(repo, path): with pytest.raises(NotASymlink): dotfile.unlink() - py.path.local(dotfile.name.dirname).ensure_dir() - dotfile.name.mksymlinkto(dotfile.target) + # py.path.local(dotfile.name.dirname).ensure_dir() + # dotfile.name.mksymlinkto(dotfile.target) + mkdir(dotfile.name.parent) + dotfile.name.symlink_to(dotfile.target) with pytest.raises(TargetMissing): dotfile.unlink() - dotfile.target.ensure() + # dotfile.target.ensure() + touch(dotfile.target) dotfile.unlink() - assert dotfile.target.check(file=1, link=0) - assert dotfile.name.check(exists=0) + assert is_file(dotfile.target) + assert not dotfile.name.exists() with pytest.raises(NotASymlink): dotfile.unlink() - assert dotfile.target.check(file=1, link=0) - assert dotfile.name.check(exists=0) + assert is_file(dotfile.target) + assert not dotfile.name.exists() -# {{{1 copy -# @pytest.mark.parametrize('path', ['.foo', '.foo/bar/baz']) -# def test_unlink(repo, path): -# dotfile = _make_dotfile(repo, path) +@pytest.mark.parametrize('path', ['.foo', '.foo/bar/baz']) +def test_copy(repo, path): + pass + # dotfile = _make_dotfile(repo, path) + # TODO |