From 24d933cd733bcc89375bc43b56eb37a559e9144c Mon Sep 17 00:00:00 2001 From: Jon Bernard Date: Sun, 30 Oct 2011 23:15:26 -0400 Subject: Import license content instead of duplicating it --- LICENSE | 14 -------------- LICENSE.rst | 19 +++++++++++++++++++ README.rst | 20 -------------------- setup.py | 1 + 4 files changed, 20 insertions(+), 34 deletions(-) delete mode 100644 LICENSE create mode 100644 LICENSE.rst diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 88f69aa..0000000 --- a/LICENSE +++ /dev/null @@ -1,14 +0,0 @@ -Copyright (C) 2011 Jon Bernard - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . diff --git a/LICENSE.rst b/LICENSE.rst new file mode 100644 index 0000000..c45726b --- /dev/null +++ b/LICENSE.rst @@ -0,0 +1,19 @@ +License +------- + +GPL License. :: + +Copyright (C) 2011 Jon Bernard and contributers + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/README.rst b/README.rst index 0509f1f..d73beae 100644 --- a/README.rst +++ b/README.rst @@ -131,26 +131,6 @@ I have the following in my ``~/.dotfilesrc``: :: Any file you list in ``ignore`` will be skipped. The ``ignore`` option supports glob file patterns. -License -------- - -GPL License. :: - - Copyright (C) 2011 Jon Bernard - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - Contribute ---------- diff --git a/setup.py b/setup.py index e11410b..098e351 100755 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ setup(name='dotfiles', version=__version__, description='Easily manage your dotfiles', long_description=open('README.rst').read() + '\n\n' + + open('LICENSE.rst').read() + '\n\n' + open('HISTORY.rst').read(), author='Jon Bernard', author_email='jbernard@tuxion.com', -- cgit v1.2.3 From 6aec9c0e3c3e8a5e7718f5c34bb620462cb724f3 Mon Sep 17 00:00:00 2001 From: Jon Bernard Date: Tue, 1 Nov 2011 09:16:58 -0400 Subject: Remove unnecessary contrib directory --- contrib/dotfilesrc | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 contrib/dotfilesrc diff --git a/contrib/dotfilesrc b/contrib/dotfilesrc deleted file mode 100644 index f26515e..0000000 --- a/contrib/dotfilesrc +++ /dev/null @@ -1,13 +0,0 @@ -[dotfiles] -repository = ~/Dotfiles -prefix = -ignore = [ - '.metadata', - '.git', - '.gitignore'] -externals = { - '.adobe': '/tmp', - '.bzr.log': '/dev/null', - '.lastpass': '/tmp', - '.macromedia': '/tmp', - '.uml': '/tmp'} -- cgit v1.2.3 From 4584eb5790d42092ba1d3965ae987fef7b4d8c43 Mon Sep 17 00:00:00 2001 From: Jon Bernard Date: Sun, 30 Oct 2011 23:27:17 -0400 Subject: Add support for in-repo configuration files In addition to parsing .dotfilesrc in the user's home directory, also look for that file inside the repository. This allows you to keep your repository and it's settings together. Closes: #1 --- AUTHORS.rst | 5 +- README.rst | 13 ++- dotfiles/cli.py | 268 ++++++++++++++++++++++++++++++++++--------------------- dotfiles/core.py | 24 ++--- test_dotfiles.py | 63 ++++++------- 5 files changed, 221 insertions(+), 152 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index a183cc8..dc83965 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,7 +1,7 @@ Dotfiles is written and maintained by Jon Bernard and various contributors: -Development Lead -```````````````` +Development +``````````` - Jon Bernard @@ -11,3 +11,4 @@ Patches and Suggestions - Anaƫl Beutot - Remco Wendt +- Sebastian Rahlf diff --git a/README.rst b/README.rst index d73beae..22813c5 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ Dotfile management made easy ``dotfiles`` is a tool to make managing your dotfile symlinks in ``$HOME`` easy, allowing you to keep all your dotfiles in a single directory. -Hosting is left to you. Using whatever VCS you prefer, or even rsync, you can +Hosting is up to you. Using whatever VCS you prefer, or even rsync, you can easily distribute your dotfiles repository across multiple hosts. Interface @@ -73,9 +73,9 @@ You get the idea. Type ``dotfiles --help`` to see the available options. Configuration ------------- -You can choose to create a configuration file to store personal -customizations. By default, ``dotfiles`` will look in ``~/.dotfilesrc``. An -example configuration file might look like: :: +You can choose to create a configuration file to store personal customizations. +By default, ``dotfiles`` will look for ``~/.dotfilesrc``. You can change this +with the ``-C`` flag. An example configuration file might look like: :: [dotfiles] repository = ~/Dotfiles @@ -87,6 +87,11 @@ example configuration file might look like: :: '.bzr.log': '/dev/null', '.uml': '/tmp'} +You can also store your configuration file inside your repository. Put your +settings in ``.dotfilesrc`` at the root of your repository and ``dotfiles`` will +find it. Note that ``ignore`` and ``externals`` are appended to any values +previously discovered. + Prefixes -------- diff --git a/dotfiles/cli.py b/dotfiles/cli.py index a056f70..3441e75 100644 --- a/dotfiles/cli.py +++ b/dotfiles/cli.py @@ -13,166 +13,226 @@ from . import core import ConfigParser from optparse import OptionParser, OptionGroup -DEFAULT_REPO = os.path.expanduser('~/Dotfiles') +defaults = { + 'prefix': '', + 'homedir': '~/', + 'repository': '~/Dotfiles', + 'config_file': '~/.dotfilesrc'} +settings = { + 'prefix': None, + 'homedir': None, + 'repository': None, + 'config_file': None, + 'ignore': set(['.dotfilesrc']), + 'externals': dict()} -def parse_args(): - parser = OptionParser(usage="%prog ACTION [OPTION...] [FILE...]") +def missing_default_repo(): + """Print a helpful message when the default repository is missing.""" + + print """ +If this is your first time running dotfiles, you must first create +a repository. By default, dotfiles will look for '{0}'. +Something like: + + $ mkdir {0} + +is all you need to do. If you don't like the default, you can put your +repository wherever you like. You have two choices once you've created your +repository. You can specify the path to the repository on the command line +using the '-R' flag. Alternatively, you can create a configuration file at +'~/.dotfilesrc' and place the path to your repository in there. The contents +would look like: - parser.set_defaults(config=os.path.expanduser("~/.dotfilesrc")) - parser.set_defaults(ignore=[]) - parser.set_defaults(externals={}) + [dotfiles] + repository = {0} + +Type 'dotfiles -h' to see detailed usage information.""".format( + defaults['repository']) - parser.add_option("-v", "--version", action="store_true", - dest="show_version", default=False, + +def add_global_flags(parser): + parser.add_option("-v", "--version", + action="store_true", dest="show_version", default=False, help="show version number and exit") - parser.add_option("-f", "--force", action="store_true", dest="force", - default=False, help="ignore unmanaged dotfiles (use with --sync)") + parser.add_option("-f", "--force", + action="store_true", dest="force", default=False, + help="ignore unmanaged dotfiles (use with --sync)") + + parser.add_option("-R", "--repo", + type="string", dest="repository", + help="set repository location (default: %s)" % ( + defaults['repository'])) - # OptionParser expands ~ constructions - parser.add_option("-R", "--repo", type="string", dest="repo", - help="set repository location (default is %s)" % DEFAULT_REPO) + parser.add_option("-p", "--prefix", + type="string", dest="prefix", + help="set prefix character (default: %s)" % ( + "None" if not defaults['prefix'] else defaults['prefix'])) - parser.add_option("-p", "--prefix", type="string", dest="prefix", - help="set prefix character (default is None)") + parser.add_option("-C", "--config", + type="string", dest="config_file", + help="set configuration file location (default: %s)" % ( + defaults['config_file'])) - parser.add_option("-C", "--config", type="string", dest="config", - help="set configuration file location (default is ~/.dotfilesrc)") + parser.add_option("-H", "--home", + type="string", dest="homedir", + help="set home directory location (default: %s)" % ( + defaults['homedir'])) - parser.add_option("-H", "--home", type="string", dest="home", - help="set home directory location (default is ~/)") +def add_action_group(parser): action_group = OptionGroup(parser, "Actions") - action_group.add_option("-a", "--add", action="store_const", dest="action", - const="add", help="add dotfile(s) to the repository") + action_group.add_option("-a", "--add", + action="store_const", dest="action", const="add", + help="add dotfile(s) to the repository") - action_group.add_option("-c", "--check", action="store_const", - dest="action", const="check", help="check dotfiles repository") + action_group.add_option("-c", "--check", + action="store_const", dest="action", const="check", + help="check for broken and unmanaged dotfiles") - action_group.add_option("-l", "--list", action="store_const", - dest="action", const="list", + action_group.add_option("-l", "--list", + action="store_const", dest="action", const="list", help="list currently managed dotfiles") - action_group.add_option("-r", "--remove", action="store_const", - dest="action", const="remove", + action_group.add_option("-r", "--remove", + action="store_const", dest="action", const="remove", help="remove dotfile(s) from the repository") - action_group.add_option("-s", "--sync", action="store_const", - dest="action", const="sync", help="update dotfile symlinks") + action_group.add_option("-s", "--sync", + action="store_const", dest="action", const="sync", + help="update dotfile symlinks") - action_group.add_option("-m", "--move", action="store_const", - dest="action", const="move", help="move dotfiles repository to " \ - "another location") + action_group.add_option("-m", "--move", + action="store_const", dest="action", const="move", + help="move dotfiles repository to another location") parser.add_option_group(action_group) - (opts, args) = parser.parse_args() - - # Skip checking if the repository exists here. The user may have specified - # a command line argument or a configuration file, which will be examined - # next. - return (opts, args) +def parse_args(): + parser = OptionParser(usage="%prog ACTION [OPTION...] [FILE...]") -def main(): + add_global_flags(parser) + add_action_group(parser) - (opts, args) = parse_args() + (opts, args) = parser.parse_args() if opts.show_version: print 'dotfiles v%s' % core.__version__ exit(0) - config_defaults = { - 'repository': opts.repo, - 'prefix': opts.prefix, - 'ignore': opts.ignore, - 'externals': opts.externals} + if not opts.action: + print "Error: An action is required. Type 'dotfiles -h' to see " \ + "detailed usage information." + exit(-1) - parser = ConfigParser.SafeConfigParser(config_defaults) - parser.read(opts.config) + return (opts, args) - if 'dotfiles' in parser.sections(): - if not opts.repo and parser.get('dotfiles', 'repository'): - opts.repo = parser.get('dotfiles', 'repository') - if opts.action == 'move': - # TODO: update the configuration file after the move - print 'Remember to update the repository location ' \ - 'in your configuration file (%s).' % (opts.config) +def parse_config(config_file): - if not opts.prefix and parser.get('dotfiles', 'prefix'): - opts.prefix = parser.get('dotfiles', 'prefix') + parser = ConfigParser.SafeConfigParser() + parser.read(config_file) - if not opts.ignore and parser.get('dotfiles', 'ignore'): - opts.ignore = eval(parser.get('dotfiles', 'ignore')) + opts = {'repository': None, + 'prefix': None, + 'ignore': set(), + 'externals': dict()} - if not opts.externals and parser.get('dotfiles', 'externals'): - opts.externals = eval(parser.get('dotfiles', 'externals')) + for entry in ('repository', 'prefix'): + try: + opts[entry] = parser.get('dotfiles', entry) + except ConfigParser.NoOptionError: + pass + except ConfigParser.NoSectionError: + break - if not opts.repo: - opts.repo = DEFAULT_REPO + for entry in ('ignore', 'externals'): + try: + opts[entry] = eval(parser.get('dotfiles', entry)) + except ConfigParser.NoOptionError: + pass + except ConfigParser.NoSectionError: + break - opts.repo = os.path.realpath(os.path.expanduser(opts.repo)) + return opts - if not opts.prefix: - opts.prefix = '' - if not os.path.exists(opts.repo): - print 'Error: Could not find dotfiles repository \"%s\"' % (opts.repo) - if opts.repo == DEFAULT_REPO: - missing_default_repo() +def dispatch(dotfiles, action, force, args): + if action in ['list', 'check']: + getattr(dotfiles, action)() + elif action in ['add', 'remove']: + getattr(dotfiles, action)(args) + elif action == 'sync': + dotfiles.sync(force) + elif action == 'move': + if len(args) > 1: + print "Error: Move cannot handle multiple targets." + exit(-1) + dotfiles.move(args[0]) + else: + print "Error: Something truly terrible has happened." exit(-1) - if not opts.action: - print "Error: An action is required. Type 'dotfiles -h' to see detailed usage information." + +def compare_path(path1, path2): + return (os.path.realpath(os.path.expanduser(path1)) == + os.path.realpath(os.path.expanduser(path2))) + + +def realpath(path): + return os.path.realpath(os.path.expanduser(path)) + + +def check_repository_exists(): + if not os.path.exists(settings['repository']): + print 'Error: Could not find dotfiles repository \"%s\"' % ( + settings['repository']) + if compare_path(settings['repository'], defaults['repository']): + missing_default_repo() exit(-1) - dotfiles = core.Dotfiles(home='~/', repo=opts.repo, prefix=opts.prefix, - ignore=opts.ignore, externals=opts.externals) +def update_settings(opts, key): + global settings - if opts.action in ['list', 'check']: - getattr(dotfiles, opts.action)() + settings[key].update(opts[key]) - elif opts.action in ['add', 'remove']: - getattr(dotfiles, opts.action)(args) - elif opts.action == 'sync': - dotfiles.sync(opts.force) +def main(): - elif opts.action == 'move': - if len(args) > 1: - print "Error: Move cannot handle multiple targets." - exit(-1) - if opts.repo != args[0]: - dotfiles.move(args[0]) + global settings - else: - print "Error: Something truly terrible has happened." - exit(-1) + (cli_opts, args) = parse_args() + settings['homedir'] = realpath(cli_opts.homedir or defaults['homedir']) + settings['config_file'] = realpath(cli_opts.config_file or + defaults['config_file']) -def missing_default_repo(): - """Print a helpful message when the default repository is missing.""" + config_opts = parse_config(settings['config_file']) - print """ -If this is your first time running dotfiles, you must first create -a repository. By default, dotfiles will look for '{0}'. -Something like: + settings['repository'] = realpath(cli_opts.repository or + config_opts['repository'] or defaults['repository']) - $ mkdir {0} + check_repository_exists() -is all you need to do. If you don't like the default, you can put your -repository wherever you like. You have two choices once you've created your -repository. You can specify the path to the repository on the command line -using the '-R' flag. Alternatively, you can create a configuration file at -'~/.dotfilesrc' and place the path to your repository in there. The contents -would look like: + update_settings(config_opts, 'ignore') + update_settings(config_opts, 'externals') - [dotfiles] - repository = {0} + repo_config_file = os.path.join(settings['repository'], '.dotfilesrc') + repo_config_opts = parse_config(repo_config_file) + + settings['prefix'] = (cli_opts.prefix or + repo_config_opts['prefix'] or + config_opts['prefix'] or + defaults['prefix']) + + update_settings(repo_config_opts, 'ignore') + update_settings(repo_config_opts, 'externals') + + dotfiles = core.Dotfiles(**settings) -Type 'dotfiles -h' to see detailed usage information.""".format(DEFAULT_REPO) + dispatch(dotfiles, cli_opts.action, cli_opts.force, args) diff --git a/dotfiles/core.py b/dotfiles/core.py index a1aec45..f6c68c1 100644 --- a/dotfiles/core.py +++ b/dotfiles/core.py @@ -23,7 +23,7 @@ class Dotfile(object): if name.startswith('/'): self.name = name else: - self.name = os.path.expanduser(home + '/.%s' % name.strip('.')) + self.name = home + '/.%s' % name.strip('.') self.basename = os.path.basename(self.name) self.target = target.rstrip('/') self.status = '' @@ -69,7 +69,7 @@ class Dotfile(object): class Dotfiles(object): """A Dotfiles Repository.""" - __attrs__ = ['home', 'repo', 'prefix', 'ignore', 'externals'] + __attrs__ = ['homedir', 'repository', 'prefix', 'ignore', 'externals'] def __init__(self, **kwargs): @@ -84,23 +84,25 @@ class Dotfiles(object): self.dotfiles = list() - all_repofiles = os.listdir(self.repo) + all_repofiles = os.listdir(self.repository) repofiles_to_symlink = set(all_repofiles) for pat in self.ignore: - repofiles_to_symlink.difference_update(fnmatch.filter(all_repofiles, pat)) + repofiles_to_symlink.difference_update( + fnmatch.filter(all_repofiles, pat)) for dotfile in repofiles_to_symlink: self.dotfiles.append(Dotfile(dotfile[len(self.prefix):], - os.path.join(self.repo, dotfile), self.home)) + os.path.join(self.repository, dotfile), self.homedir)) for dotfile in self.externals.keys(): - self.dotfiles.append(Dotfile(dotfile, self.externals[dotfile], self.home)) + self.dotfiles.append(Dotfile(dotfile, self.externals[dotfile], + self.homedir)) def _fqpn(self, dotfile): """Return the fully qualified path to a dotfile.""" - return os.path.join(self.repo, + return os.path.join(self.repository, self.prefix + os.path.basename(dotfile).strip('.')) def list(self, verbose=True): @@ -136,7 +138,7 @@ class Dotfiles(object): def _perform_action(self, action, files): for file in files: if os.path.basename(file).startswith('.'): - getattr(Dotfile(file, self._fqpn(file), self.home), action)() + getattr(Dotfile(file, self._fqpn(file), self.homedir), action)() else: print "Skipping \"%s\", not a dotfile" % file @@ -146,10 +148,10 @@ class Dotfiles(object): if os.path.exists(target): raise ValueError('Target already exists: %s' % (target)) - shutil.copytree(self.repo, target) - shutil.rmtree(self.repo) + shutil.copytree(self.repository, target) + shutil.rmtree(self.repository) - self.repo = target + self.repository = target self._load() self.sync(force=True) diff --git a/test_dotfiles.py b/test_dotfiles.py index 85cdce6..f9b6b46 100755 --- a/test_dotfiles.py +++ b/test_dotfiles.py @@ -21,16 +21,16 @@ class DotfilesTestCase(unittest.TestCase): def setUp(self): """Create a temporary home directory.""" - self.home = tempfile.mkdtemp() + self.homedir = tempfile.mkdtemp() # Create a repository for the tests to use. - self.repo = os.path.join(self.home, 'Dotfiles') - os.mkdir(self.repo) + self.repository = os.path.join(self.homedir, 'Dotfiles') + os.mkdir(self.repository) def tearDown(self): """Delete the temporary home directory and its contents.""" - shutil.rmtree(self.home) + shutil.rmtree(self.homedir) def assertPathEqual(self, path1, path2): self.assertEqual( @@ -47,41 +47,42 @@ class DotfilesTestCase(unittest.TestCase): a directory. """ - os.mkdir(os.path.join(self.home, '.lastpass')) + os.mkdir(os.path.join(self.homedir, '.lastpass')) externals = {'.lastpass': '/tmp'} - dotfiles = core.Dotfiles(home=self.home, repo=self.repo, prefix='', - ignore=[], externals=externals) + dotfiles = core.Dotfiles( + homedir=self.homedir, repository=self.repository, + prefix='', ignore=[], externals=externals) dotfiles.sync(force=True) self.assertPathEqual( - os.path.join(self.home, '.lastpass'), + os.path.join(self.homedir, '.lastpass'), '/tmp') def test_move_repository(self): """Test the move() method for a Dotfiles repository.""" - touch(os.path.join(self.repo, 'bashrc')) + touch(os.path.join(self.repository, 'bashrc')) dotfiles = core.Dotfiles( - home=self.home, repo=self.repo, prefix='', - ignore=[], force=True, externals={}) + homedir=self.homedir, repository=self.repository, + prefix='', ignore=[], force=True, externals={}) dotfiles.sync() # Make sure sync() did the right thing. self.assertPathEqual( - os.path.join(self.home, '.bashrc'), - os.path.join(self.repo, 'bashrc')) + os.path.join(self.homedir, '.bashrc'), + os.path.join(self.repository, 'bashrc')) - target = os.path.join(self.home, 'MyDotfiles') + target = os.path.join(self.homedir, 'MyDotfiles') dotfiles.move(target) self.assertTrue(os.path.exists(os.path.join(target, 'bashrc'))) self.assertPathEqual( - os.path.join(self.home, '.bashrc'), + os.path.join(self.homedir, '.bashrc'), os.path.join(target, 'bashrc')) def test_sync_unmanaged_directory_symlink(self): @@ -95,29 +96,29 @@ class DotfilesTestCase(unittest.TestCase): """ # Create a dotfile symlink to some directory - os.mkdir(os.path.join(self.home, 'vim')) - os.symlink(os.path.join(self.home, 'vim'), - os.path.join(self.home, '.vim')) + os.mkdir(os.path.join(self.homedir, 'vim')) + os.symlink(os.path.join(self.homedir, 'vim'), + os.path.join(self.homedir, '.vim')) # Create a vim directory in the repository. This will cause the above # symlink to be overwritten on sync. - os.mkdir(os.path.join(self.repo, 'vim')) + os.mkdir(os.path.join(self.repository, 'vim')) # Make sure the symlink points to the correct location. self.assertPathEqual( - os.path.join(self.home, '.vim'), - os.path.join(self.home, 'vim')) + os.path.join(self.homedir, '.vim'), + os.path.join(self.homedir, 'vim')) dotfiles = core.Dotfiles( - home=self.home, repo=self.repo, prefix='', - ignore=[], externals={}) + homedir=self.homedir, repository=self.repository, + prefix='', ignore=[], externals={}) dotfiles.sync(force=True) # The symlink should now point to the directory in the repository. self.assertPathEqual( - os.path.join(self.home, '.vim'), - os.path.join(self.repo, 'vim')) + os.path.join(self.homedir, '.vim'), + os.path.join(self.repository, 'vim')) def test_glob_ignore_pattern(self): """ Test that the use of glob pattern matching works in the ignores list. @@ -156,11 +157,11 @@ class DotfilesTestCase(unittest.TestCase): all_dotfiles = [f for f in all_repo_files if f[1] is not None] for original, symlink in all_repo_files: - touch(os.path.join(self.repo, original)) + touch(os.path.join(self.repository, original)) dotfiles = core.Dotfiles( - home=self.home, repo=self.repo, prefix='', - ignore=ignore, externals={}) + homedir=self.homedir, repository=self.repository, + prefix='', ignore=ignore, externals={}) dotfiles.sync() @@ -168,13 +169,13 @@ class DotfilesTestCase(unittest.TestCase): # point to the correct file and are the only files that # exist in the home dir. self.assertEqual( - sorted(os.listdir(self.home)), + sorted(os.listdir(self.homedir)), sorted([f[1] for f in all_dotfiles] + ['Dotfiles'])) for original, symlink in all_dotfiles: self.assertPathEqual( - os.path.join(self.repo, original), - os.path.join(self.home, symlink)) + os.path.join(self.repository, original), + os.path.join(self.homedir, symlink)) def suite(): suite = unittest.TestLoader().loadTestsFromTestCase(DotfilesTestCase) -- cgit v1.2.3