From 9d967ad7e8f22c6426d559dd211f1cba766a687e Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Mon, 11 Jun 2018 21:14:10 -0400 Subject: Include built-in plugins in PyPI distribution --- dotbot/cli.py | 2 +- dotbot/plugins/__init__.py | 3 + dotbot/plugins/clean.py | 57 +++++++++ dotbot/plugins/link.py | 219 +++++++++++++++++++++++++++++++++ dotbot/plugins/shell.py | 61 +++++++++ plugins/clean.py | 57 --------- plugins/link.py | 219 --------------------------------- plugins/shell.py | 61 --------- test/tests/plugin-disable-builtin.bash | 17 +++ 9 files changed, 358 insertions(+), 338 deletions(-) create mode 100644 dotbot/plugins/__init__.py create mode 100644 dotbot/plugins/clean.py create mode 100644 dotbot/plugins/link.py create mode 100644 dotbot/plugins/shell.py delete mode 100644 plugins/clean.py delete mode 100644 plugins/link.py delete mode 100644 plugins/shell.py create mode 100644 test/tests/plugin-disable-builtin.bash diff --git a/dotbot/cli.py b/dotbot/cli.py index 0674cbe..aec6097 100644 --- a/dotbot/cli.py +++ b/dotbot/cli.py @@ -52,7 +52,7 @@ def main(): log.set_level(Level.DEBUG) plugin_directories = list(options.plugin_dirs) if not options.disable_built_in_plugins: - plugin_directories.append(os.path.join(os.path.dirname(__file__), '..', 'plugins')) + from .plugins import Clean, Link, Shell plugin_paths = [] for directory in plugin_directories: for plugin_path in glob.glob(os.path.join(directory, '*.py')): diff --git a/dotbot/plugins/__init__.py b/dotbot/plugins/__init__.py new file mode 100644 index 0000000..93bd981 --- /dev/null +++ b/dotbot/plugins/__init__.py @@ -0,0 +1,3 @@ +from .clean import Clean +from .link import Link +from .shell import Shell diff --git a/dotbot/plugins/clean.py b/dotbot/plugins/clean.py new file mode 100644 index 0000000..7e6cba1 --- /dev/null +++ b/dotbot/plugins/clean.py @@ -0,0 +1,57 @@ +import os, dotbot + +class Clean(dotbot.Plugin): + ''' + Cleans broken symbolic links. + ''' + + _directive = 'clean' + + def can_handle(self, directive): + return directive == self._directive + + def handle(self, directive, data): + if directive != self._directive: + raise ValueError('Clean cannot handle directive %s' % directive) + return self._process_clean(data) + + def _process_clean(self, targets): + success = True + defaults = self._context.defaults().get(self._directive, {}) + force = defaults.get('force', False) + for target in targets: + if isinstance(targets, dict): + force = targets[target].get('force', force) + success &= self._clean(target, force) + if success: + self._log.info('All targets have been cleaned') + else: + self._log.error('Some targets were not successfully cleaned') + return success + + def _clean(self, target, force): + ''' + Cleans all the broken symbolic links in target if they point to + a subdirectory of the base directory or if forced to clean. + ''' + if not os.path.isdir(os.path.expanduser(target)): + self._log.debug('Ignoring nonexistent directory %s' % target) + return True + for item in os.listdir(os.path.expanduser(target)): + path = os.path.join(os.path.expanduser(target), item) + if not os.path.exists(path) and os.path.islink(path): + points_at = os.path.join(os.path.dirname(path), os.readlink(path)) + if self._in_directory(path, self._context.base_directory()) or force: + self._log.lowinfo('Removing invalid link %s -> %s' % (path, points_at)) + os.remove(path) + else: + self._log.lowinfo('Link %s -> %s not removed.' % (path, points_at)) + return True + + def _in_directory(self, path, directory): + ''' + Returns true if the path is in the directory. + ''' + directory = os.path.join(os.path.realpath(directory), '') + path = os.path.realpath(path) + return os.path.commonprefix([path, directory]) == directory diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py new file mode 100644 index 0000000..5274e3b --- /dev/null +++ b/dotbot/plugins/link.py @@ -0,0 +1,219 @@ +import os +import glob +import shutil +import dotbot + + +class Link(dotbot.Plugin): + ''' + Symbolically links dotfiles. + ''' + + _directive = 'link' + + def can_handle(self, directive): + return directive == self._directive + + def handle(self, directive, data): + if directive != self._directive: + raise ValueError('Link cannot handle directive %s' % directive) + return self._process_links(data) + + def _process_links(self, links): + success = True + defaults = self._context.defaults().get('link', {}) + for destination, source in links.items(): + destination = os.path.expandvars(destination) + relative = defaults.get('relative', False) + force = defaults.get('force', False) + relink = defaults.get('relink', False) + create = defaults.get('create', False) + use_glob = defaults.get('glob', False) + if isinstance(source, dict): + # extended config + relative = source.get('relative', relative) + force = source.get('force', force) + relink = source.get('relink', relink) + create = source.get('create', create) + use_glob = source.get('glob', use_glob) + path = self._default_source(destination, source.get('path')) + else: + path = self._default_source(destination, source) + path = os.path.expandvars(os.path.expanduser(path)) + if use_glob: + self._log.debug("Globbing with path: " + str(path)) + glob_results = glob.glob(path) + if len(glob_results) is 0: + self._log.warning("Globbing couldn't find anything matching " + str(path)) + success = False + continue + glob_star_loc = path.find('*') + if glob_star_loc is -1 and destination[-1] is '/': + self._log.error("Ambiguous action requested.") + self._log.error("No wildcard in glob, directory use undefined: " + + destination + " -> " + str(glob_results)) + self._log.warning("Did you want to link the directory or into it?") + success = False + continue + elif glob_star_loc is -1 and len(glob_results) is 1: + # perform a normal link operation + if create: + success &= self._create(destination) + if force or relink: + success &= self._delete(path, destination, relative, force) + success &= self._link(path, destination, relative) + else: + self._log.lowinfo("Globs from '" + path + "': " + str(glob_results)) + glob_base = path[:glob_star_loc] + for glob_full_item in glob_results: + glob_item = glob_full_item[len(glob_base):] + glob_link_destination = os.path.join(destination, glob_item) + if create: + success &= self._create(glob_link_destination) + if force or relink: + success &= self._delete(glob_full_item, glob_link_destination, relative, force) + success &= self._link(glob_full_item, glob_link_destination, relative) + else: + if create: + success &= self._create(destination) + if not self._exists(os.path.join(self._context.base_directory(), path)): + success = False + self._log.warning('Nonexistent target %s -> %s' % + (destination, path)) + continue + if force or relink: + success &= self._delete(path, destination, relative, force) + success &= self._link(path, destination, relative) + if success: + self._log.info('All links have been set up') + else: + self._log.error('Some links were not successfully set up') + return success + + def _default_source(self, destination, source): + if source is None: + basename = os.path.basename(destination) + if basename.startswith('.'): + return basename[1:] + else: + return basename + else: + return source + + def _is_link(self, path): + ''' + Returns true if the path is a symbolic link. + ''' + return os.path.islink(os.path.expanduser(path)) + + def _link_destination(self, path): + ''' + Returns the destination of the symbolic link. + ''' + path = os.path.expanduser(path) + return os.readlink(path) + + def _exists(self, path): + ''' + Returns true if the path exists. + ''' + path = os.path.expanduser(path) + return os.path.exists(path) + + def _create(self, path): + success = True + parent = os.path.abspath(os.path.join(os.path.expanduser(path), os.pardir)) + if not self._exists(parent): + self._log.debug("Try to create parent: " + str(parent)) + try: + os.makedirs(parent) + except OSError: + self._log.warning('Failed to create directory %s' % parent) + success = False + else: + self._log.lowinfo('Creating directory %s' % parent) + return success + + def _delete(self, source, path, relative, force): + success = True + source = os.path.join(self._context.base_directory(), source) + fullpath = os.path.expanduser(path) + if relative: + source = self._relative_path(source, fullpath) + if ((self._is_link(path) and self._link_destination(path) != source) or + (self._exists(path) and not self._is_link(path))): + removed = False + try: + if os.path.islink(fullpath): + os.unlink(fullpath) + removed = True + elif force: + if os.path.isdir(fullpath): + shutil.rmtree(fullpath) + removed = True + else: + os.remove(fullpath) + removed = True + except OSError: + self._log.warning('Failed to remove %s' % path) + success = False + else: + if removed: + self._log.lowinfo('Removing %s' % path) + return success + + def _relative_path(self, source, destination): + ''' + Returns the relative path to get to the source file from the + destination file. + ''' + destination_dir = os.path.dirname(destination) + return os.path.relpath(source, destination_dir) + + def _link(self, source, link_name, relative): + ''' + Links link_name to source. + + Returns true if successfully linked files. + ''' + success = False + destination = os.path.expanduser(link_name) + absolute_source = os.path.join(self._context.base_directory(), source) + if relative: + source = self._relative_path(absolute_source, destination) + else: + source = absolute_source + if (not self._exists(link_name) and self._is_link(link_name) and + self._link_destination(link_name) != source): + self._log.warning('Invalid link %s -> %s' % + (link_name, self._link_destination(link_name))) + # we need to use absolute_source below because our cwd is the dotfiles + # directory, and if source is relative, it will be relative to the + # destination directory + elif not self._exists(link_name) and self._exists(absolute_source): + try: + os.symlink(source, destination) + except OSError: + self._log.warning('Linking failed %s -> %s' % (link_name, source)) + else: + self._log.lowinfo('Creating link %s -> %s' % (link_name, source)) + success = True + elif self._exists(link_name) and not self._is_link(link_name): + self._log.warning( + '%s already exists but is a regular file or directory' % + link_name) + elif self._is_link(link_name) and self._link_destination(link_name) != source: + self._log.warning('Incorrect link %s -> %s' % + (link_name, self._link_destination(link_name))) + # again, we use absolute_source to check for existence + elif not self._exists(absolute_source): + if self._is_link(link_name): + self._log.warning('Nonexistent target %s -> %s' % + (link_name, source)) + else: + self._log.warning('Nonexistent target for %s : %s' % + (link_name, source)) + else: + self._log.lowinfo('Link exists %s -> %s' % (link_name, source)) + success = True + return success diff --git a/dotbot/plugins/shell.py b/dotbot/plugins/shell.py new file mode 100644 index 0000000..b6f5184 --- /dev/null +++ b/dotbot/plugins/shell.py @@ -0,0 +1,61 @@ +import os, subprocess, dotbot + +class Shell(dotbot.Plugin): + ''' + Run arbitrary shell commands. + ''' + + _directive = 'shell' + + def can_handle(self, directive): + return directive == self._directive + + def handle(self, directive, data): + if directive != self._directive: + raise ValueError('Shell cannot handle directive %s' % + directive) + return self._process_commands(data) + + def _process_commands(self, data): + success = True + defaults = self._context.defaults().get('shell', {}) + with open(os.devnull, 'w') as devnull: + for item in data: + stdin = stdout = stderr = devnull + if defaults.get('stdin', False) == True: + stdin = None + if defaults.get('stdout', False) == True: + stdout = None + if defaults.get('stderr', False) == True: + stderr = None + if isinstance(item, dict): + cmd = item['command'] + msg = item.get('description', None) + if 'stdin' in item: + stdin = None if item['stdin'] == True else devnull + if 'stdout' in item: + stdout = None if item['stdout'] == True else devnull + if 'stderr' in item: + stderr = None if item['stderr'] == True else devnull + elif isinstance(item, list): + cmd = item[0] + msg = item[1] if len(item) > 1 else None + else: + cmd = item + msg = None + if msg is None: + self._log.lowinfo(cmd) + else: + self._log.lowinfo('%s [%s]' % (msg, cmd)) + executable = os.environ.get('SHELL') + ret = subprocess.call(cmd, shell=True, stdin=stdin, stdout=stdout, + stderr=stderr, cwd=self._context.base_directory(), + executable=executable) + if ret != 0: + success = False + self._log.warning('Command [%s] failed' % cmd) + if success: + self._log.info('All commands have been executed') + else: + self._log.error('Some commands were not successfully executed') + return success diff --git a/plugins/clean.py b/plugins/clean.py deleted file mode 100644 index 7e6cba1..0000000 --- a/plugins/clean.py +++ /dev/null @@ -1,57 +0,0 @@ -import os, dotbot - -class Clean(dotbot.Plugin): - ''' - Cleans broken symbolic links. - ''' - - _directive = 'clean' - - def can_handle(self, directive): - return directive == self._directive - - def handle(self, directive, data): - if directive != self._directive: - raise ValueError('Clean cannot handle directive %s' % directive) - return self._process_clean(data) - - def _process_clean(self, targets): - success = True - defaults = self._context.defaults().get(self._directive, {}) - force = defaults.get('force', False) - for target in targets: - if isinstance(targets, dict): - force = targets[target].get('force', force) - success &= self._clean(target, force) - if success: - self._log.info('All targets have been cleaned') - else: - self._log.error('Some targets were not successfully cleaned') - return success - - def _clean(self, target, force): - ''' - Cleans all the broken symbolic links in target if they point to - a subdirectory of the base directory or if forced to clean. - ''' - if not os.path.isdir(os.path.expanduser(target)): - self._log.debug('Ignoring nonexistent directory %s' % target) - return True - for item in os.listdir(os.path.expanduser(target)): - path = os.path.join(os.path.expanduser(target), item) - if not os.path.exists(path) and os.path.islink(path): - points_at = os.path.join(os.path.dirname(path), os.readlink(path)) - if self._in_directory(path, self._context.base_directory()) or force: - self._log.lowinfo('Removing invalid link %s -> %s' % (path, points_at)) - os.remove(path) - else: - self._log.lowinfo('Link %s -> %s not removed.' % (path, points_at)) - return True - - def _in_directory(self, path, directory): - ''' - Returns true if the path is in the directory. - ''' - directory = os.path.join(os.path.realpath(directory), '') - path = os.path.realpath(path) - return os.path.commonprefix([path, directory]) == directory diff --git a/plugins/link.py b/plugins/link.py deleted file mode 100644 index 5274e3b..0000000 --- a/plugins/link.py +++ /dev/null @@ -1,219 +0,0 @@ -import os -import glob -import shutil -import dotbot - - -class Link(dotbot.Plugin): - ''' - Symbolically links dotfiles. - ''' - - _directive = 'link' - - def can_handle(self, directive): - return directive == self._directive - - def handle(self, directive, data): - if directive != self._directive: - raise ValueError('Link cannot handle directive %s' % directive) - return self._process_links(data) - - def _process_links(self, links): - success = True - defaults = self._context.defaults().get('link', {}) - for destination, source in links.items(): - destination = os.path.expandvars(destination) - relative = defaults.get('relative', False) - force = defaults.get('force', False) - relink = defaults.get('relink', False) - create = defaults.get('create', False) - use_glob = defaults.get('glob', False) - if isinstance(source, dict): - # extended config - relative = source.get('relative', relative) - force = source.get('force', force) - relink = source.get('relink', relink) - create = source.get('create', create) - use_glob = source.get('glob', use_glob) - path = self._default_source(destination, source.get('path')) - else: - path = self._default_source(destination, source) - path = os.path.expandvars(os.path.expanduser(path)) - if use_glob: - self._log.debug("Globbing with path: " + str(path)) - glob_results = glob.glob(path) - if len(glob_results) is 0: - self._log.warning("Globbing couldn't find anything matching " + str(path)) - success = False - continue - glob_star_loc = path.find('*') - if glob_star_loc is -1 and destination[-1] is '/': - self._log.error("Ambiguous action requested.") - self._log.error("No wildcard in glob, directory use undefined: " + - destination + " -> " + str(glob_results)) - self._log.warning("Did you want to link the directory or into it?") - success = False - continue - elif glob_star_loc is -1 and len(glob_results) is 1: - # perform a normal link operation - if create: - success &= self._create(destination) - if force or relink: - success &= self._delete(path, destination, relative, force) - success &= self._link(path, destination, relative) - else: - self._log.lowinfo("Globs from '" + path + "': " + str(glob_results)) - glob_base = path[:glob_star_loc] - for glob_full_item in glob_results: - glob_item = glob_full_item[len(glob_base):] - glob_link_destination = os.path.join(destination, glob_item) - if create: - success &= self._create(glob_link_destination) - if force or relink: - success &= self._delete(glob_full_item, glob_link_destination, relative, force) - success &= self._link(glob_full_item, glob_link_destination, relative) - else: - if create: - success &= self._create(destination) - if not self._exists(os.path.join(self._context.base_directory(), path)): - success = False - self._log.warning('Nonexistent target %s -> %s' % - (destination, path)) - continue - if force or relink: - success &= self._delete(path, destination, relative, force) - success &= self._link(path, destination, relative) - if success: - self._log.info('All links have been set up') - else: - self._log.error('Some links were not successfully set up') - return success - - def _default_source(self, destination, source): - if source is None: - basename = os.path.basename(destination) - if basename.startswith('.'): - return basename[1:] - else: - return basename - else: - return source - - def _is_link(self, path): - ''' - Returns true if the path is a symbolic link. - ''' - return os.path.islink(os.path.expanduser(path)) - - def _link_destination(self, path): - ''' - Returns the destination of the symbolic link. - ''' - path = os.path.expanduser(path) - return os.readlink(path) - - def _exists(self, path): - ''' - Returns true if the path exists. - ''' - path = os.path.expanduser(path) - return os.path.exists(path) - - def _create(self, path): - success = True - parent = os.path.abspath(os.path.join(os.path.expanduser(path), os.pardir)) - if not self._exists(parent): - self._log.debug("Try to create parent: " + str(parent)) - try: - os.makedirs(parent) - except OSError: - self._log.warning('Failed to create directory %s' % parent) - success = False - else: - self._log.lowinfo('Creating directory %s' % parent) - return success - - def _delete(self, source, path, relative, force): - success = True - source = os.path.join(self._context.base_directory(), source) - fullpath = os.path.expanduser(path) - if relative: - source = self._relative_path(source, fullpath) - if ((self._is_link(path) and self._link_destination(path) != source) or - (self._exists(path) and not self._is_link(path))): - removed = False - try: - if os.path.islink(fullpath): - os.unlink(fullpath) - removed = True - elif force: - if os.path.isdir(fullpath): - shutil.rmtree(fullpath) - removed = True - else: - os.remove(fullpath) - removed = True - except OSError: - self._log.warning('Failed to remove %s' % path) - success = False - else: - if removed: - self._log.lowinfo('Removing %s' % path) - return success - - def _relative_path(self, source, destination): - ''' - Returns the relative path to get to the source file from the - destination file. - ''' - destination_dir = os.path.dirname(destination) - return os.path.relpath(source, destination_dir) - - def _link(self, source, link_name, relative): - ''' - Links link_name to source. - - Returns true if successfully linked files. - ''' - success = False - destination = os.path.expanduser(link_name) - absolute_source = os.path.join(self._context.base_directory(), source) - if relative: - source = self._relative_path(absolute_source, destination) - else: - source = absolute_source - if (not self._exists(link_name) and self._is_link(link_name) and - self._link_destination(link_name) != source): - self._log.warning('Invalid link %s -> %s' % - (link_name, self._link_destination(link_name))) - # we need to use absolute_source below because our cwd is the dotfiles - # directory, and if source is relative, it will be relative to the - # destination directory - elif not self._exists(link_name) and self._exists(absolute_source): - try: - os.symlink(source, destination) - except OSError: - self._log.warning('Linking failed %s -> %s' % (link_name, source)) - else: - self._log.lowinfo('Creating link %s -> %s' % (link_name, source)) - success = True - elif self._exists(link_name) and not self._is_link(link_name): - self._log.warning( - '%s already exists but is a regular file or directory' % - link_name) - elif self._is_link(link_name) and self._link_destination(link_name) != source: - self._log.warning('Incorrect link %s -> %s' % - (link_name, self._link_destination(link_name))) - # again, we use absolute_source to check for existence - elif not self._exists(absolute_source): - if self._is_link(link_name): - self._log.warning('Nonexistent target %s -> %s' % - (link_name, source)) - else: - self._log.warning('Nonexistent target for %s : %s' % - (link_name, source)) - else: - self._log.lowinfo('Link exists %s -> %s' % (link_name, source)) - success = True - return success diff --git a/plugins/shell.py b/plugins/shell.py deleted file mode 100644 index b6f5184..0000000 --- a/plugins/shell.py +++ /dev/null @@ -1,61 +0,0 @@ -import os, subprocess, dotbot - -class Shell(dotbot.Plugin): - ''' - Run arbitrary shell commands. - ''' - - _directive = 'shell' - - def can_handle(self, directive): - return directive == self._directive - - def handle(self, directive, data): - if directive != self._directive: - raise ValueError('Shell cannot handle directive %s' % - directive) - return self._process_commands(data) - - def _process_commands(self, data): - success = True - defaults = self._context.defaults().get('shell', {}) - with open(os.devnull, 'w') as devnull: - for item in data: - stdin = stdout = stderr = devnull - if defaults.get('stdin', False) == True: - stdin = None - if defaults.get('stdout', False) == True: - stdout = None - if defaults.get('stderr', False) == True: - stderr = None - if isinstance(item, dict): - cmd = item['command'] - msg = item.get('description', None) - if 'stdin' in item: - stdin = None if item['stdin'] == True else devnull - if 'stdout' in item: - stdout = None if item['stdout'] == True else devnull - if 'stderr' in item: - stderr = None if item['stderr'] == True else devnull - elif isinstance(item, list): - cmd = item[0] - msg = item[1] if len(item) > 1 else None - else: - cmd = item - msg = None - if msg is None: - self._log.lowinfo(cmd) - else: - self._log.lowinfo('%s [%s]' % (msg, cmd)) - executable = os.environ.get('SHELL') - ret = subprocess.call(cmd, shell=True, stdin=stdin, stdout=stdout, - stderr=stderr, cwd=self._context.base_directory(), - executable=executable) - if ret != 0: - success = False - self._log.warning('Command [%s] failed' % cmd) - if success: - self._log.info('All commands have been executed') - else: - self._log.error('Some commands were not successfully executed') - return success diff --git a/test/tests/plugin-disable-builtin.bash b/test/tests/plugin-disable-builtin.bash new file mode 100644 index 0000000..f469b0f --- /dev/null +++ b/test/tests/plugin-disable-builtin.bash @@ -0,0 +1,17 @@ +test_description='can disable built-in plugins' +. '../test-lib.bash' + +test_expect_success 'setup' ' +echo "apple" > ${DOTFILES}/f +' + +test_expect_failure 'run' ' +run_dotbot --disable-built-in-plugins <