aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--dotfiles/cli.py126
-rw-r--r--dotfiles/dotfile.py113
-rw-r--r--dotfiles/dotfiles.py365
-rw-r--r--dotfiles/exceptions.py75
-rw-r--r--dotfiles/repository.py95
-rw-r--r--setup.py2
-rw-r--r--tests/test_dotfiles.py6
7 files changed, 414 insertions, 368 deletions
diff --git a/dotfiles/cli.py b/dotfiles/cli.py
new file mode 100644
index 0000000..1cdac89
--- /dev/null
+++ b/dotfiles/cli.py
@@ -0,0 +1,126 @@
+import os
+import py
+import click
+
+from .repository import Repository
+from .exceptions import DotfileException
+
+
+DEFAULT_HOMEDIR = os.path.expanduser('~/')
+DEFAULT_REPO_PATH = os.path.expanduser('~/Dotfiles')
+DEFAULT_REPO_IGNORE = ['.git']
+
+pass_repo = click.make_pass_decorator(Repository)
+
+
+@click.group(context_settings=dict(help_option_names=['-h', '--help']))
+@click.option('-r', '--repository', type=click.Path(), show_default=True,
+ default=DEFAULT_REPO_PATH)
+@click.version_option()
+@click.pass_context
+def cli(ctx, repository):
+ """Dotfiles is a tool to make managing your dotfile symlinks in $HOME easy,
+ allowing you to keep all your dotfiles in a single directory.
+ """
+ ctx.obj = Repository(py.path.local(repository),
+ py.path.local(DEFAULT_HOMEDIR),
+ DEFAULT_REPO_IGNORE)
+
+
+@cli.command()
+@click.option('-d', '--debug', is_flag=True,
+ help='Show commands that would be executed.')
+@click.argument('files', nargs=-1, type=click.Path(exists=True))
+@pass_repo
+def add(repo, debug, files):
+ """Replace file with symlink."""
+ for dotfile in repo.dotfiles(files):
+ dotfile.add(debug)
+
+ # try:
+ # repo.dotfile(py.path.local(filename)).add(debug)
+ # if not debug:
+ # click.echo('added \'%s\'' % filename)
+ # except DotfileException as err:
+ # click.echo(err)
+
+
+@cli.command()
+@click.option('-d', '--debug', is_flag=True,
+ help='Show commands that would be executed.')
+@click.argument('files', nargs=-1, type=click.Path(exists=True))
+@pass_repo
+def remove(repo, debug, files):
+ """Replace symlink with file."""
+ for filename in files:
+ try:
+ repo.dotfile(py.path.local(filename)).remove(debug)
+ if not debug:
+ click.echo('removed \'%s\'' % filename)
+ except DotfileException as err:
+ click.echo(err)
+
+
+def show_dotfile(homedir, char, dotfile, color):
+ display_name = homedir.bestrelpath(dotfile.name)
+ click.secho('%c %s' % (char, display_name), fg=color)
+
+
+@cli.command()
+@click.option('-a', '--all', is_flag=True, help='Show all dotfiles.')
+@click.option('-c', '--color', is_flag=True, help='Enable color output.')
+@pass_repo
+def status(repo, all, color):
+ """Show all dotfiles in a non-OK state."""
+
+ state_info = {
+ 'error': {'char': 'E', 'color': None},
+ 'missing': {'char': '?', 'color': None},
+ 'conflict': {'char': '!', 'color': None},
+ }
+
+ if all:
+ state_info['ok'] = {'char': ' ', 'color': None}
+
+ if color:
+ state_info['error']['color'] = 'red'
+ state_info['missing']['color'] = 'yellow'
+ state_info['conflict']['color'] = 'magenta'
+
+ for dotfile in repo.contents():
+ try:
+ char = state_info[dotfile.state]['char']
+ color = state_info[dotfile.state]['color']
+ show_dotfile(repo.homedir, char, dotfile, color)
+ except KeyError:
+ continue
+
+
+@cli.command()
+@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.')
+@click.argument('files', nargs=-1, type=click.Path())
+@pass_repo
+def link(repo, verbose, files):
+ """Create missing symlinks."""
+ # TODO: no files should be interpreted as all files with confirmation
+ for filename in files:
+ try:
+ repo.dotfile(py.path.local(filename)).link(verbose)
+ click.echo('linked \'%s\'' % filename)
+ except DotfileException as err:
+ click.echo(err)
+
+
+@cli.command()
+@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.')
+@click.argument('files', nargs=-1, type=click.Path(exists=True))
+@pass_repo
+def unlink(repo, verbose, files):
+ """Remove existing symlinks."""
+ # TODO: no files should be interpreted as all files with confirmation
+ for filename in files:
+ try:
+ repo.dotfile(py.path.local(filename)).unlink(verbose)
+ click.echo('unlinked \'%s\'' % filename)
+ except DotfileException as err:
+ click.echo(err)
diff --git a/dotfiles/dotfile.py b/dotfiles/dotfile.py
new file mode 100644
index 0000000..bf1e9ac
--- /dev/null
+++ b/dotfiles/dotfile.py
@@ -0,0 +1,113 @@
+import py
+from click import echo
+from errno import EEXIST, ENOENT
+
+from .exceptions import IsDirectory, IsSymlink, NotASymlink, DoesNotExist, \
+ TargetExists, TargetMissing
+
+
+class Dotfile(object):
+ """An configuration file managed within a repository.
+
+ :param name: name of the symlink in the home directory (~/.vimrc)
+ :param target: where the symlink should point to (~/Dotfiles/vimrc)
+ """
+
+ def __init__(self, name, target):
+ self.name = name
+ self.target = target
+
+ def __str__(self):
+ return str(self.name)
+
+ def __repr__(self):
+ return '<Dotfile %r>' % self.name
+
+ def _ensure_subdirs(self, debug):
+ target_dir = py.path.local(self.target.dirname)
+ if not target_dir.check():
+ if debug:
+ echo('MKDIR %s' % self.target.dirname)
+ else:
+ target_dir.ensure_dir()
+
+ def _remove_subdirs(self, debug):
+ # TODO
+ pass
+
+ def _add(self, debug):
+ self._ensure_subdirs(debug)
+ if debug:
+ echo('MOVE %s -> %s' % (self.name, self.target))
+ else:
+ self.name.move(self.target)
+ self._link(debug)
+
+ def _remove(self, debug):
+ self._unlink(debug)
+ if debug:
+ echo('MOVE %s -> %s' % (self.target, self.name))
+ else:
+ self.target.move(self.name)
+ self._remove_subdirs(debug)
+
+ def _link(self, debug):
+ if debug:
+ echo('LINK %s -> %s' % (self.name, self.target))
+ else:
+ self.name.mksymlinkto(self.target, absolute=0)
+
+ def _unlink(self, debug):
+ if debug:
+ echo('UNLINK %s' % self.name)
+ else:
+ self.name.remove()
+
+ @property
+ def state(self):
+ if self.target.check(exists=0):
+ # only for testing, cli should never reach this state
+ return 'error'
+ elif self.name.check(exists=0):
+ # no $HOME symlink
+ return 'missing'
+ elif self.name.check(link=0) or not self.name.samefile(self.target):
+ # if name exists but isn't a link to the target
+ return 'conflict'
+ return 'ok'
+
+ def add(self, debug=False):
+ if self.name.check(file=0):
+ raise DoesNotExist(self.name)
+ if self.name.check(dir=1):
+ raise IsDirectory(self.name)
+ if self.name.check(link=1):
+ raise IsSymlink(self.name)
+ if self.target.check(exists=1):
+ raise TargetExists(self.name)
+ self._add(debug)
+
+ def remove(self, debug=False):
+ if not self.name.check(link=1):
+ raise NotASymlink(self.name)
+ if self.target.check(exists=0):
+ raise TargetMissing(self.name)
+ self._remove(debug)
+
+ # TODO: replace exceptions
+
+ def link(self, debug=False):
+ if self.name.check(exists=1):
+ raise OSError(EEXIST, self.name)
+ if self.target.check(exists=0):
+ raise OSError(ENOENT, self.target)
+ self._link(debug)
+
+ def unlink(self, debug=False):
+ if self.name.check(link=0):
+ raise Exception('%s is not a symlink' % self.name.basename)
+ if self.target.check(exists=0):
+ raise Exception('%s does not exist' % self.target)
+ if not self.name.samefile(self.target):
+ raise Exception('good lord')
+ self._unlink(debug)
diff --git a/dotfiles/dotfiles.py b/dotfiles/dotfiles.py
deleted file mode 100644
index 1805e97..0000000
--- a/dotfiles/dotfiles.py
+++ /dev/null
@@ -1,365 +0,0 @@
-import py
-import os
-import click
-import errno
-from operator import attrgetter
-
-
-DEFAULT_HOMEDIR = os.path.expanduser('~/')
-DEFAULT_REPO_PATH = os.path.expanduser('~/Dotfiles')
-DEFAULT_REPO_IGNORE = ['.git', '.gitignore']
-
-
-class DotfileException(Exception):
- """An exception the CLI can handle and show to the user."""
-
- def __init__(self, path, message='an unknown error occurred'):
- self.message = '\'%s\' %s' % (path, message)
- Exception.__init__(self, self.message)
-
- def __str__(self):
- return 'ERROR: %s' % self.message
-
-
-class TargetIgnored(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'targets an ignored file')
-
-
-class IsDirectory(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'is a directory')
-
-
-class IsSymlink(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'is a symlink')
-
-
-class NotASymlink(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'is not a symlink')
-
-
-class InRepository(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'is within the repository')
-
-
-class NotRootedInHome(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'not rooted in home directory')
-
-
-class IsNested(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'is nested')
-
-
-class NotADotfile(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'is not a dotfile')
-
-
-class DoesNotExist(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'doest not exist')
-
-
-class TargetExists(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'target already exists')
-
-
-class TargetMissing(DotfileException):
-
- def __init__(self, path):
- DotfileException.__init__(self, path, 'target is missing')
-
-
-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
- """
-
- def __init__(self, repodir, homedir, ignore=[]):
- self.ignore = ignore
- self.homedir = homedir
-
- # create repository if needed
- self.repodir = repodir.ensure(dir=1)
-
- 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
-
- def _target_to_name(self, target):
- """Return the expected symlink for the given repository target."""
- return self.homedir.join('.%s' % target.basename)
-
- def _name_to_target(self, name):
- """Return the expected repository target for the given symlink."""
- return self.repodir.join(self.homedir.bestrelpath(name)[1:])
-
- def dotfile(self, name):
- """Return a valid dotfile for the given path."""
-
- target = self._name_to_target(name)
- if target.basename in self.ignore:
- raise TargetIgnored(name)
- if name.check(dir=1):
- raise IsDirectory(name)
-
- for path in name.parts():
- try:
- if self.repodir.samefile(path):
- raise InRepository(name)
- except py.error.ENOENT:
- # this occurs when the symlink does not yet exist
- continue
-
- # if not self.homedir.samefile(name.dirname):
- # raise NotRootedInHome(name)
- # if name.dirname != self.homedir:
- # raise IsNested(name)
- if name.basename[0] != '.':
- raise NotADotfile(name)
-
- return Dotfile(name, target)
-
- def dotfiles(self, path):
- """Return a list of dotfiles given a path."""
-
- if path.check(dir=1):
- raise IsDirectory(path)
-
- return self.dotfile(path)
-
- def contents(self):
- """Return a list of all dotfiles in the repository path."""
- contents = []
- for target in self.repodir.listdir():
- target = py.path.local(target)
- if target.basename not in self.ignore:
- contents.append(Dotfile(self._target_to_name(target), target))
- return sorted(contents, key=attrgetter('name'))
-
-
-class Dotfile(object):
- """An configuration file managed within a repository.
-
- :param name: name of the symlink in the home directory (~/.vimrc)
- :param target: where the symlink should point to (~/Dotfiles/vimrc)
- """
-
- def __init__(self, name, target):
- self.name = name
- self.target = target
-
- def __str__(self):
- return self.name.basename
-
- def __repr__(self):
- return '<Dotfile %r>' % self.name
-
- def _ensure_target_dir(self, verbose):
- target_dir = py.path.local(self.target.dirname)
- if not target_dir.check():
- if verbose:
- click.echo('MKDIR %s' % self.target.dirname)
- target_dir.ensure(dir=1)
-
- def _add(self, verbose):
- self._ensure_target_dir(verbose)
- if verbose:
- click.echo('MOVE %s -> %s' % (self.name, self.target))
- self.name.move(self.target)
- self._link(verbose)
-
- def _remove(self, verbose):
- self._unlink(verbose)
- if verbose:
- click.echo('MOVE %s -> %s' % (self.target, self.name))
- self.target.move(self.name)
- # TODO: remove directory if empty
-
- def _link(self, verbose):
- if verbose:
- click.echo('LINK %s -> %s' % (self.name, self.target))
- self.name.mksymlinkto(self.target, absolute=0)
-
- def _unlink(self, verbose):
- if verbose:
- click.echo('UNLINK %s' % self.name)
- self.name.remove()
-
- @property
- def state(self):
- if self.target.check(exists=0):
- # only for testing, cli should never reach this state
- return 'error'
- elif self.name.check(exists=0):
- # no $HOME symlink
- return 'missing'
- elif self.name.check(link=0) or not self.name.samefile(self.target):
- # if name exists but isn't a link to the target
- return 'conflict'
- return 'ok'
-
- def add(self, verbose=False):
- if self.name.check(file=0):
- raise DoesNotExist(self.name)
- if self.name.check(dir=1):
- raise IsDirectory(self.name)
- if self.name.check(link=1):
- raise IsSymlink(self.name)
- if self.target.check(exists=1):
- raise TargetExists(self.name)
- self._add(verbose)
-
- def remove(self, verbose=False):
- if not self.name.check(link=1):
- raise NotASymlink(self.name)
- if self.target.check(exists=0):
- raise TargetMissing(self.name)
- self._remove(verbose)
-
- # TODO: replace exceptions
-
- def link(self, verbose=False):
- if self.name.check(exists=1):
- raise OSError(errno.EEXIST, self.name)
- if self.target.check(exists=0):
- raise OSError(errno.ENOENT, self.target)
- self._link(verbose)
-
- def unlink(self, verbose=False):
- if self.name.check(link=0):
- raise Exception('%s is not a symlink' % self.name.basename)
- if self.target.check(exists=0):
- raise Exception('%s does not exist' % self.target)
- if not self.name.samefile(self.target):
- raise Exception('good lord')
- self._unlink(verbose)
-
-
-pass_repo = click.make_pass_decorator(Repository)
-
-
-@click.group(context_settings=dict(help_option_names=['-h', '--help']))
-@click.option('--repository', type=click.Path(), show_default=True,
- default=DEFAULT_REPO_PATH)
-@click.version_option()
-@click.pass_context
-def cli(ctx, repository):
- """Dotfiles is a tool to make managing your dotfile symlinks in $HOME easy,
- allowing you to keep all your dotfiles in a single directory.
- """
- ctx.obj = Repository(py.path.local(repository),
- py.path.local(DEFAULT_HOMEDIR),
- DEFAULT_REPO_IGNORE)
-
-
-@cli.command()
-@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.')
-@click.argument('files', nargs=-1, type=click.Path(exists=True))
-@pass_repo
-def add(repo, verbose, files):
- """Replace file with symlink."""
- # TODO: repo.dotfiles() for directories
- for filename in files:
- try:
- repo.dotfile(py.path.local(filename)).add(verbose)
- click.echo('added \'%s\'' % filename)
- except DotfileException as err:
- click.echo(err)
-
-
-@cli.command()
-@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.')
-@click.argument('files', nargs=-1, type=click.Path(exists=True))
-@pass_repo
-def remove(repo, verbose, files):
- """Replace symlink with file."""
- for filename in files:
- try:
- repo.dotfile(py.path.local(filename)).remove(verbose)
- click.echo('removed \'%s\'' % filename)
- except DotfileException as err:
- click.echo(err)
-
-
-@cli.command()
-@click.option('-v', '--verbose', is_flag=True, help='Show all dotfiles.')
-@click.option('-c', '--color', is_flag=True, help='Enable color output.')
-@pass_repo
-def status(repo, verbose, color):
- """Show all dotfiles in a non-OK state."""
-
- state_info = {
- 'error': {'char': 'E', 'color': None},
- 'conflict': {'char': '!', 'color': None},
- 'missing': {'char': '?', 'color': None},
- }
-
- if verbose:
- state_info['ok'] = {'char': ' ', 'color': None}
-
- if color:
- state_info['error']['color'] = 'red'
- state_info['conflict']['color'] = 'magenta'
- state_info['missing']['color'] = 'yellow'
-
- for dotfile in repo.contents():
- try:
- char = state_info[dotfile.state]['char']
- fg = state_info[dotfile.state]['color']
- click.secho('%c %s' % (char, dotfile), fg=fg)
- except KeyError:
- continue
-
-
-@cli.command()
-@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.')
-@click.argument('files', nargs=-1, type=click.Path())
-@pass_repo
-def link(repo, verbose, files):
- """Create missing symlinks."""
- # TODO: no files should be interpreted as all files with confirmation
- for filename in files:
- try:
- repo.dotfile(py.path.local(filename)).link(verbose)
- click.echo('linked \'%s\'' % filename)
- except DotfileException as err:
- click.echo(err)
-
-
-@cli.command()
-@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.')
-@click.argument('files', nargs=-1, type=click.Path(exists=True))
-@pass_repo
-def unlink(repo, verbose, files):
- """Remove existing symlinks."""
- # TODO: no files should be interpreted as all files with confirmation
- for filename in files:
- try:
- repo.dotfile(py.path.local(filename)).unlink(verbose)
- click.echo('unlinked \'%s\'' % filename)
- except DotfileException as err:
- click.echo(err)
diff --git a/dotfiles/exceptions.py b/dotfiles/exceptions.py
new file mode 100644
index 0000000..2469b80
--- /dev/null
+++ b/dotfiles/exceptions.py
@@ -0,0 +1,75 @@
+class DotfileException(Exception):
+ """An exception the CLI can handle and show to the user."""
+
+ def __init__(self, path, message='an unknown error occurred'):
+ self.message = '\'%s\' %s' % (path, message)
+ Exception.__init__(self, self.message)
+
+ def __str__(self):
+ return 'ERROR: %s' % self.message
+
+
+class TargetIgnored(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'targets an ignored file')
+
+
+class IsDirectory(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'is a directory')
+
+
+class IsSymlink(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'is a symlink')
+
+
+class NotASymlink(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'is not a symlink')
+
+
+class InRepository(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'is within the repository')
+
+
+class NotRootedInHome(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'not rooted in home directory')
+
+
+class IsNested(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'is nested')
+
+
+class NotADotfile(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'is not a dotfile')
+
+
+class DoesNotExist(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'doest not exist')
+
+
+class TargetExists(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'target already exists')
+
+
+class TargetMissing(DotfileException):
+
+ def __init__(self, path):
+ DotfileException.__init__(self, path, 'target is missing')
diff --git a/dotfiles/repository.py b/dotfiles/repository.py
new file mode 100644
index 0000000..63fab23
--- /dev/null
+++ b/dotfiles/repository.py
@@ -0,0 +1,95 @@
+import py
+from click import echo
+
+from .dotfile import Dotfile
+from .exceptions import DotfileException, TargetIgnored, IsDirectory, \
+ InRepository
+
+
+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
+ """
+
+ def __init__(self, repodir, homedir, ignore=[]):
+ self.ignore = ignore
+ self.homedir = homedir
+
+ # create repository if needed
+ self.repodir = repodir.ensure(dir=1)
+
+ 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
+
+ def _target_to_name(self, target):
+ """Return the expected symlink for the given repository target."""
+ return self.homedir.join(self.repodir.bestrelpath(target))
+
+ def _name_to_target(self, name):
+ """Return the expected repository target for the given symlink."""
+ return self.repodir.join(self.homedir.bestrelpath(name))
+
+ def dotfile(self, name):
+ """Return a valid dotfile for the given path."""
+
+ # XXX: it must be below the home directory
+ # it cannot be contained in the repository
+ # it cannot be ignored
+ # it must be a file
+
+ target = self._name_to_target(name)
+ if target.basename in self.ignore:
+ raise TargetIgnored(name)
+ if name.check(dir=1):
+ raise IsDirectory(name)
+
+ for path in name.parts():
+ try:
+ if self.repodir.samefile(path):
+ raise InRepository(name)
+ except py.error.ENOENT:
+ # this occurs when the symlink does not yet exist
+ continue
+
+ # if not self.homedir.samefile(name.dirname):
+ # raise NotRootedInHome(name)
+ # if name.dirname != self.homedir:
+ # raise IsNested(name)
+ # if name.basename[0] != '.':
+ # raise NotADotfile(name)
+
+ return Dotfile(name, target)
+
+ def dotfiles(self, paths):
+ """Return a list of dotfiles given a path."""
+
+ paths = map(py.path.local, paths)
+
+ rv = []
+ for path in paths:
+ try:
+ rv.append(self.dotfile(path))
+ except DotfileException as err:
+ echo(err)
+
+ return rv
+
+ def contents(self):
+ """Return a list of all dotfiles in the repository path."""
+ def filter(node):
+ return node.check(dir=0) and node.basename not in self.ignore
+
+ def recurse(node):
+ return node.basename not in self.ignore
+
+ def construct(target):
+ return Dotfile(self._target_to_name(target), target)
+
+ return map(construct, self.repodir.visit(filter, recurse, sort=True))
diff --git a/setup.py b/setup.py
index eae2667..a41c837 100644
--- a/setup.py
+++ b/setup.py
@@ -27,7 +27,7 @@ setup(
],
entry_points={
'console_scripts': [
- 'dotfiles=dotfiles.dotfiles:cli',
+ 'dotfiles=dotfiles.cli:cli',
],
},
classifiers=[
diff --git a/tests/test_dotfiles.py b/tests/test_dotfiles.py
index ffc9073..8323385 100644
--- a/tests/test_dotfiles.py
+++ b/tests/test_dotfiles.py
@@ -1,7 +1,9 @@
import pytest
-from dotfiles.dotfiles import Repository, Dotfile, cli
-from dotfiles.dotfiles import IsSymlink
+from dotfiles.cli import cli
+from dotfiles.dotfile import Dotfile
+from dotfiles.repository import Repository
+from dotfiles.exceptions import IsSymlink
class TestCli(object):