diff options
-rw-r--r-- | dotfiles/cli.py | 21 | ||||
-rw-r--r-- | dotfiles/repository.py | 110 | ||||
-rw-r--r-- | tests/test_dotfile.py | 2 | ||||
-rw-r--r-- | tests/test_repository.py | 8 |
4 files changed, 76 insertions, 65 deletions
diff --git a/dotfiles/cli.py b/dotfiles/cli.py index fc6e0d3..13a1bad 100644 --- a/dotfiles/cli.py +++ b/dotfiles/cli.py @@ -5,9 +5,10 @@ from .repository import Repository from .exceptions import DotfileException -DEFAULT_REPODIR = os.path.expanduser('~/Dotfiles') -DEFAULT_IGNORE = ['.git', '.hg'] -DEFAULT_DOT = True +# Defaults +REPOSITORY_PATH = os.path.expanduser('~/Dotfiles') +IGNORE_PATTERNS = ['.git', '.hg', '*~'] +PRESERVE_LEADING_DOT = False def confirm(method, files, repo): @@ -17,7 +18,7 @@ def confirm(method, files, repo): # user has specified specific files, so we are not assuming all return files - # no files provided, so we assume all files after confimration + # no files provided, so we assume all files after confirmation message = 'Are you sure you want to %s all dotfiles?' % method click.confirm(message, abort=True) return str(repo).split() @@ -40,9 +41,9 @@ pass_repo = click.make_pass_decorator(Repository) @click.group(context_settings=dict(help_option_names=['-h', '--help'])) @click.option('-r', '--repo', type=click.Path(), show_default=True, - default=DEFAULT_REPODIR, envvar='DOTFILES_REPO') -@click.option('--dot/--no-dot', default=True, show_default=DEFAULT_DOT, - envvar='DOTFILES_DOT') + default=REPOSITORY_PATH, envvar='DOTFILES_REPO') +@click.option('--dot/--dot', show_default=True, + default=PRESERVE_LEADING_DOT, envvar='DOTFILES_DOT') @click.version_option() @click.pass_context def cli(ctx, repo, dot): @@ -52,11 +53,13 @@ def cli(ctx, repo, dot): The following environment variables are recognized at runtime: \b - DOTFILES_DOT: Set this to 'False' to remove the leading dot. DOTFILES_REPO: Set this to the location of your repository. DOTFILES_COLOR: Set this to 'True' to enable color output. + DOTFILES_DOT: Set this to 'True' to preserve the leading dot. """ - ctx.obj = Repository(repodir=repo, ignore=DEFAULT_IGNORE, dot=dot) + ctx.obj = Repository(path=repo, + ignore_patterns=IGNORE_PATTERNS, + preserve_leading_dot=dot) @cli.command() diff --git a/dotfiles/repository.py b/dotfiles/repository.py index 86b975c..b36d77d 100644 --- a/dotfiles/repository.py +++ b/dotfiles/repository.py @@ -1,5 +1,5 @@ -import py -from click import echo +import click +import py.path from operator import attrgetter from .dotfile import Dotfile @@ -10,67 +10,84 @@ from .exceptions import NotRootedInHome, InRepository, IsDirectory class Repository(object): """A repository is a directory that contains dotfiles. - :param repodir: the location of the repository directory - :param homedir: the location of the home directory (primarily for testing) - :param ignore: a list of targets to ignore - :param dot: wether to preserve the target's leading dot + :param path: the location of the repository directory + :param ignore_patterns: a list of glob patterns to ignore + :param preserve_leading_dot: whether to preserve the target's leading dot + :param home_directory: the location of the home directory """ - homedir = py.path.local('~/', expanduser=True) + home_directory = py.path.local('~/', expanduser=True) - def __init__(self, repodir, homedir=homedir, ignore=[], dot=True): - self.repodir = py.path.local(repodir).ensure_dir() - self.homedir = py.path.local(homedir) - self.ignore = ignore - self.dot = dot + def __init__(self, path, + ignore_patterns=[], + preserve_leading_dot=False, + home_directory=home_directory): + + # create repository directory if not found + self.path = py.path.local(path).ensure_dir() + + self.home_directory = home_directory + self.ignore_patterns = ignore_patterns + self.preserve_leading_dot = preserve_leading_dot def __str__(self): """Return human-readable repository contents.""" return ''.join('%s\n' % item for item in self.contents()).rstrip() def __repr__(self): - return '<Repository %r>' % self.repodir + return '<Repository %r>' % self.path - def _target_to_name(self, target): + def _ignore(self, path): + for pattern in self.ignore_patterns: + if path.fnmatch(pattern): + return True + return False + + def _dotfile_path(self, target): """Return the expected symlink for the given repository target.""" - relpath = self.repodir.bestrelpath(target) - if self.dot: - return self.homedir.join(relpath) + relpath = self.path.bestrelpath(target) + if self.preserve_leading_dot: + return self.home_directory.join(relpath) else: - return self.homedir.join('.%s' % relpath) + return self.home_directory.join('.%s' % relpath) - def _name_to_target(self, name): + def _dotfile_target(self, path): """Return the expected repository target for the given symlink.""" - relpath = self.homedir.bestrelpath(name) - if self.dot: - return self.repodir.join(relpath) + relpath = self.home_directory.bestrelpath(path) + if self.preserve_leading_dot: + return self.path.join(relpath) else: - return self.repodir.join(relpath[1:]) + return self.path.join(relpath[1:]) - def _dotfile(self, name): + def _dotfile(self, path): """Return a valid dotfile for the given path.""" - target = self._name_to_target(name) + target = self._dotfile_target(path) - if not name.fnmatch('%s/*' % self.homedir): - raise NotRootedInHome(name) - if name.fnmatch('%s/*' % self.repodir): - raise InRepository(name) - if target.basename in self.ignore: - raise TargetIgnored(name) - if name.check(dir=1): - raise IsDirectory(name) + if not path.fnmatch('%s/*' % self.home_directory): + raise NotRootedInHome(path) + if path.fnmatch('%s/*' % self.path): + raise InRepository(path) + if self._ignore(target): + raise TargetIgnored(path) + if path.check(dir=1): + raise IsDirectory(path) - return Dotfile(name, target) + return Dotfile(path, target) def _contents(self, dir): """Return all unignored files contained below a directory.""" def filter(node): - return node.check(dir=0) and node.basename not in self.ignore + return node.check(dir=0) and not self._ignore(node) + + return dir.visit(filter, lambda x: not self._ignore(x)) - def recurse(node): - return node.basename not in self.ignore + def contents(self): + """Return a list of dotfiles for each file in the repository.""" + def construct(target): + return Dotfile(self._dotfile_path(target), target) - return dir.visit(filter, recurse) + contents = self._contents(self.path) + return sorted(map(construct, contents), key=attrgetter('path')) def dotfiles(self, paths): """Return a collection of dotfiles given a list of paths. @@ -92,19 +109,11 @@ class Repository(object): try: return self._dotfile(path) except DotfileException as err: - echo(err) + click.echo(err) return None return [d for d in map(construct, paths) if d is not None] - def contents(self): - """Return a list of dotfiles for each file in the repository.""" - def construct(target): - return Dotfile(self._target_to_name(target), target) - - contents = self._contents(self.repodir) - return sorted(map(construct, contents), key=attrgetter('name')) - def prune(self): """Remove any empty directories in the repository. @@ -113,11 +122,8 @@ class Repository(object): so pruning must take place explicitly after such operations occur. """ def filter(node): - return node.check(dir=1) and node.basename not in self.ignore - - def recurse(node): - return node.basename not in self.ignore + return node.check(dir=1) and not self._ignore(node) - for dir in self.repodir.visit(filter, recurse): + for dir in self.path.visit(filter, lambda x: not self._ignore(x)): if not len(dir.listdir()): dir.remove() diff --git a/tests/test_dotfile.py b/tests/test_dotfile.py index a913743..ff19d9d 100644 --- a/tests/test_dotfile.py +++ b/tests/test_dotfile.py @@ -1,5 +1,5 @@ -import py import pytest +import py.path from dotfiles.dotfile import Dotfile from dotfiles.exceptions import IsSymlink, NotASymlink diff --git a/tests/test_repository.py b/tests/test_repository.py index 4cf694f..05328e6 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -1,5 +1,5 @@ -import py import pytest +import py.path from dotfiles.repository import Repository from dotfiles.exceptions import NotRootedInHome, InRepository, TargetIgnored, \ @@ -48,9 +48,11 @@ def test_dotfile(repo, home): with pytest.raises(NotRootedInHome): Repository(repo, home)._dotfile(py.path.local('/tmp/foo')) with pytest.raises(TargetIgnored): - Repository(repo, home, ignore=['.foo'])._dotfile(home.join('.foo')) + Repository(repo, home, + ignore_patterns=['.foo'])._dotfile(home.join('.foo')) with pytest.raises(TargetIgnored): - Repository(repo, home, ignore=['foo'])._dotfile(home.join('.bar/foo')) + Repository(repo, home, + ignore_patterns=['foo'])._dotfile(home.join('.bar/foo')) with pytest.raises(IsDirectory): Repository(repo, home)._dotfile(home.ensure_dir('.config')) |