mirror of
1
0
Fork 0

Migrate code style to Black

This commit is contained in:
Anish Athalye 2022-01-30 18:48:30 -05:00
parent ac5793ceb5
commit 769767c129
21 changed files with 395 additions and 316 deletions

View File

@ -15,6 +15,3 @@ indent_size = 4
[*.yml] [*.yml]
indent_size = 2 indent_size = 2
[*.md]
trim_trailing_whitespace = false

View File

@ -5,12 +5,12 @@ on:
schedule: schedule:
- cron: '0 8 * * 6' - cron: '0 8 * * 6'
jobs: jobs:
build: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python: [2.7, pypy2, 3.5, 3.6, 3.7, 3.8, 3.9, pypy3] python: ["2.7", "pypy2", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy3"]
name: Python ${{ matrix.python }} name: "Test: Python ${{ matrix.python }}"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
@ -19,3 +19,9 @@ jobs:
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
- run: ./test/test - run: ./test/test
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: psf/black@stable

View File

@ -1,4 +1,4 @@
from .cli import main from .cli import main
from .plugin import Plugin from .plugin import Plugin
__version__ = '1.19.0' __version__ = "1.19.0"

View File

@ -12,43 +12,76 @@ import dotbot
import os import os
import subprocess import subprocess
def add_options(parser): def add_options(parser):
parser.add_argument('-Q', '--super-quiet', action='store_true', parser.add_argument(
help='suppress almost all output') "-Q", "--super-quiet", action="store_true", help="suppress almost all output"
parser.add_argument('-q', '--quiet', action='store_true', )
help='suppress most output') parser.add_argument("-q", "--quiet", action="store_true", help="suppress most output")
parser.add_argument('-v', '--verbose', action='count', default=0, parser.add_argument(
help='enable verbose output\n' "-v",
'-v: typical verbose\n' "--verbose",
'-vv: also, set shell commands stderr/stdout to true') action="count",
parser.add_argument('-d', '--base-directory', default=0,
help='execute commands from within BASEDIR', help="enable verbose output\n"
metavar='BASEDIR') "-v: typical verbose\n"
parser.add_argument('-c', '--config-file', "-vv: also, set shell commands stderr/stdout to true",
help='run commands given in CONFIGFILE', metavar='CONFIGFILE') )
parser.add_argument('-p', '--plugin', action='append', dest='plugins', default=[], parser.add_argument(
help='load PLUGIN as a plugin', metavar='PLUGIN') "-d", "--base-directory", help="execute commands from within BASEDIR", metavar="BASEDIR"
parser.add_argument('--disable-built-in-plugins', )
action='store_true', help='disable built-in plugins') parser.add_argument(
parser.add_argument('--plugin-dir', action='append', dest='plugin_dirs', default=[], "-c", "--config-file", help="run commands given in CONFIGFILE", metavar="CONFIGFILE"
metavar='PLUGIN_DIR', help='load all plugins in PLUGIN_DIR') )
parser.add_argument('--only', nargs='+', parser.add_argument(
help='only run specified directives', metavar='DIRECTIVE') "-p",
parser.add_argument('--except', nargs='+', dest='skip', "--plugin",
help='skip specified directives', metavar='DIRECTIVE') action="append",
parser.add_argument('--force-color', dest='force_color', action='store_true', dest="plugins",
help='force color output') default=[],
parser.add_argument('--no-color', dest='no_color', action='store_true', help="load PLUGIN as a plugin",
help='disable color output') metavar="PLUGIN",
parser.add_argument('--version', action='store_true', )
help='show program\'s version number and exit') parser.add_argument(
parser.add_argument('-x', '--exit-on-failure', dest='exit_on_failure', action='store_true', "--disable-built-in-plugins", action="store_true", help="disable built-in plugins"
help='exit after first failed directive') )
parser.add_argument(
"--plugin-dir",
action="append",
dest="plugin_dirs",
default=[],
metavar="PLUGIN_DIR",
help="load all plugins in PLUGIN_DIR",
)
parser.add_argument(
"--only", nargs="+", help="only run specified directives", metavar="DIRECTIVE"
)
parser.add_argument(
"--except", nargs="+", dest="skip", help="skip specified directives", metavar="DIRECTIVE"
)
parser.add_argument(
"--force-color", dest="force_color", action="store_true", help="force color output"
)
parser.add_argument(
"--no-color", dest="no_color", action="store_true", help="disable color output"
)
parser.add_argument(
"--version", action="store_true", help="show program's version number and exit"
)
parser.add_argument(
"-x",
"--exit-on-failure",
dest="exit_on_failure",
action="store_true",
help="exit after first failed directive",
)
def read_config(config_file): def read_config(config_file):
reader = ConfigReader(config_file) reader = ConfigReader(config_file)
return reader.get_config() return reader.get_config()
def main(): def main():
log = Messenger() log = Messenger()
try: try:
@ -58,12 +91,15 @@ def main():
if options.version: if options.version:
try: try:
with open(os.devnull) as devnull: with open(os.devnull) as devnull:
git_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD'], git_hash = subprocess.check_output(
cwd=os.path.dirname(os.path.abspath(__file__)), stderr=devnull) ["git", "rev-parse", "HEAD"],
hash_msg = ' (git %s)' % git_hash[:10] cwd=os.path.dirname(os.path.abspath(__file__)),
stderr=devnull,
)
hash_msg = " (git %s)" % git_hash[:10]
except (OSError, subprocess.CalledProcessError): except (OSError, subprocess.CalledProcessError):
hash_msg = '' hash_msg = ""
print('Dotbot version %s%s' % (dotbot.__version__, hash_msg)) print("Dotbot version %s%s" % (dotbot.__version__, hash_msg))
exit(0) exit(0)
if options.super_quiet: if options.super_quiet:
log.set_level(Level.WARNING) log.set_level(Level.WARNING)
@ -87,38 +123,43 @@ def main():
from .plugins import Clean, Create, Link, Shell from .plugins import Clean, Create, Link, Shell
plugin_paths = [] plugin_paths = []
for directory in plugin_directories: for directory in plugin_directories:
for plugin_path in glob.glob(os.path.join(directory, '*.py')): for plugin_path in glob.glob(os.path.join(directory, "*.py")):
plugin_paths.append(plugin_path) plugin_paths.append(plugin_path)
for plugin_path in options.plugins: for plugin_path in options.plugins:
plugin_paths.append(plugin_path) plugin_paths.append(plugin_path)
for plugin_path in plugin_paths: for plugin_path in plugin_paths:
abspath = os.path.abspath(plugin_path) abspath = os.path.abspath(plugin_path)
module.load(abspath) module.load(abspath)
if not options.config_file: if not options.config_file:
log.error('No configuration file specified') log.error("No configuration file specified")
exit(1) exit(1)
tasks = read_config(options.config_file) tasks = read_config(options.config_file)
if tasks is None: if tasks is None:
log.warning('Configuration file is empty, no work to do') log.warning("Configuration file is empty, no work to do")
tasks = [] tasks = []
if not isinstance(tasks, list): if not isinstance(tasks, list):
raise ReadingError('Configuration file must be a list of tasks') raise ReadingError("Configuration file must be a list of tasks")
if options.base_directory: if options.base_directory:
base_directory = os.path.abspath(options.base_directory) base_directory = os.path.abspath(options.base_directory)
else: else:
# default to directory of config file # default to directory of config file
base_directory = os.path.dirname(os.path.abspath(options.config_file)) base_directory = os.path.dirname(os.path.abspath(options.config_file))
os.chdir(base_directory) os.chdir(base_directory)
dispatcher = Dispatcher(base_directory, only=options.only, skip=options.skip, dispatcher = Dispatcher(
exit_on_failure=options.exit_on_failure, options=options) base_directory,
only=options.only,
skip=options.skip,
exit_on_failure=options.exit_on_failure,
options=options,
)
success = dispatcher.dispatch(tasks) success = dispatcher.dispatch(tasks)
if success: if success:
log.info('\n==> All tasks executed successfully') log.info("\n==> All tasks executed successfully")
else: else:
raise DispatchError('\n==> Some tasks were not executed successfully') raise DispatchError("\n==> Some tasks were not executed successfully")
except (ReadingError, DispatchError) as e: except (ReadingError, DispatchError) as e:
log.error('%s' % e) log.error("%s" % e)
exit(1) exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
log.error('\n==> Operation aborted') log.error("\n==> Operation aborted")
exit(1) exit(1)

View File

@ -3,6 +3,7 @@ import json
import os.path import os.path
from .util import string from .util import string
class ConfigReader(object): class ConfigReader(object):
def __init__(self, config_file_path): def __init__(self, config_file_path):
self._config = self._read(config_file_path) self._config = self._read(config_file_path)
@ -11,17 +12,18 @@ class ConfigReader(object):
try: try:
_, ext = os.path.splitext(config_file_path) _, ext = os.path.splitext(config_file_path)
with open(config_file_path) as fin: with open(config_file_path) as fin:
if ext == '.json': if ext == ".json":
data = json.load(fin) data = json.load(fin)
else: else:
data = yaml.safe_load(fin) data = yaml.safe_load(fin)
return data return data
except Exception as e: except Exception as e:
msg = string.indent_lines(str(e)) msg = string.indent_lines(str(e))
raise ReadingError('Could not read config file:\n%s' % msg) raise ReadingError("Could not read config file:\n%s" % msg)
def get_config(self): def get_config(self):
return self._config return self._config
class ReadingError(Exception): class ReadingError(Exception):
pass pass

View File

@ -2,10 +2,11 @@ import copy
import os import os
from argparse import Namespace from argparse import Namespace
class Context(object): class Context(object):
''' """
Contextual data and information for plugins. Contextual data and information for plugins.
''' """
def __init__(self, base_directory, options=Namespace()): def __init__(self, base_directory, options=Namespace()):
self._base_directory = base_directory self._base_directory = base_directory

View File

@ -4,9 +4,11 @@ from .plugin import Plugin
from .messenger import Messenger from .messenger import Messenger
from .context import Context from .context import Context
class Dispatcher(object): class Dispatcher(object):
def __init__(self, base_directory, only=None, skip=None, exit_on_failure=False, def __init__(
options=Namespace()): self, base_directory, only=None, skip=None, exit_on_failure=False, options=Namespace()
):
self._log = Messenger() self._log = Messenger()
self._setup_context(base_directory, options) self._setup_context(base_directory, options)
self._load_plugins() self._load_plugins()
@ -15,24 +17,26 @@ class Dispatcher(object):
self._exit = exit_on_failure self._exit = exit_on_failure
def _setup_context(self, base_directory, options): def _setup_context(self, base_directory, options):
path = os.path.abspath( path = os.path.abspath(os.path.expanduser(base_directory))
os.path.expanduser(base_directory))
if not os.path.exists(path): if not os.path.exists(path):
raise DispatchError('Nonexistent base directory') raise DispatchError("Nonexistent base directory")
self._context = Context(path, options) self._context = Context(path, options)
def dispatch(self, tasks): def dispatch(self, tasks):
success = True success = True
for task in tasks: for task in tasks:
for action in task: for action in task:
if (self._only is not None and action not in self._only \ if (
or self._skip is not None and action in self._skip) \ self._only is not None
and action != 'defaults': and action not in self._only
self._log.info('Skipping action %s' % action) or self._skip is not None
and action in self._skip
) and action != "defaults":
self._log.info("Skipping action %s" % action)
continue continue
handled = False handled = False
if action == 'defaults': if action == "defaults":
self._context.set_defaults(task[action]) # replace, not update self._context.set_defaults(task[action]) # replace, not update
handled = True handled = True
# keep going, let other plugins handle this if they want # keep going, let other plugins handle this if they want
for plugin in self._plugins: for plugin in self._plugins:
@ -41,29 +45,29 @@ class Dispatcher(object):
local_success = plugin.handle(action, task[action]) local_success = plugin.handle(action, task[action])
if not local_success and self._exit: if not local_success and self._exit:
# The action has failed exit # The action has failed exit
self._log.error('Action %s failed' % action) self._log.error("Action %s failed" % action)
return False return False
success &= local_success success &= local_success
handled = True handled = True
except Exception as err: except Exception as err:
self._log.error( self._log.error(
'An error was encountered while executing action %s' % "An error was encountered while executing action %s" % action
action) )
self._log.debug(err) self._log.debug(err)
if self._exit: if self._exit:
# There was an execption exit # There was an execption exit
return False return False
if not handled: if not handled:
success = False success = False
self._log.error('Action %s not handled' % action) self._log.error("Action %s not handled" % action)
if self._exit: if self._exit:
# Invalid action exit # Invalid action exit
return False return False
return success return success
def _load_plugins(self): def _load_plugins(self):
self._plugins = [plugin(self._context) self._plugins = [plugin(self._context) for plugin in Plugin.__subclasses__()]
for plugin in Plugin.__subclasses__()]
class DispatchError(Exception): class DispatchError(Exception):
pass pass

View File

@ -1,8 +1,8 @@
class Color(object): class Color(object):
NONE = '' NONE = ""
RESET = '\033[0m' RESET = "\033[0m"
RED = '\033[91m' RED = "\033[91m"
GREEN = '\033[92m' GREEN = "\033[92m"
YELLOW = '\033[93m' YELLOW = "\033[93m"
BLUE = '\033[94m' BLUE = "\033[94m"
MAGENTA = '\033[95m' MAGENTA = "\033[95m"

View File

@ -3,8 +3,9 @@ from ..util.compat import with_metaclass
from .color import Color from .color import Color
from .level import Level from .level import Level
class Messenger(with_metaclass(Singleton, object)): class Messenger(with_metaclass(Singleton, object)):
def __init__(self, level = Level.LOWINFO): def __init__(self, level=Level.LOWINFO):
self.set_level(level) self.set_level(level)
self.use_color(True) self.use_color(True)
@ -15,8 +16,8 @@ class Messenger(with_metaclass(Singleton, object)):
self._use_color = yesno self._use_color = yesno
def log(self, level, message): def log(self, level, message):
if (level >= self._level): if level >= self._level:
print('%s%s%s' % (self._color(level), message, self._reset())) print("%s%s%s" % (self._color(level), message, self._reset()))
def debug(self, message): def debug(self, message):
self.log(Level.DEBUG, message) self.log(Level.DEBUG, message)
@ -34,13 +35,13 @@ class Messenger(with_metaclass(Singleton, object)):
self.log(Level.ERROR, message) self.log(Level.ERROR, message)
def _color(self, level): def _color(self, level):
''' """
Get a color (terminal escape sequence) according to a level. Get a color (terminal escape sequence) according to a level.
''' """
if not self._use_color: if not self._use_color:
return '' return ""
elif level < Level.DEBUG: elif level < Level.DEBUG:
return '' return ""
elif Level.DEBUG <= level < Level.LOWINFO: elif Level.DEBUG <= level < Level.LOWINFO:
return Color.YELLOW return Color.YELLOW
elif Level.LOWINFO <= level < Level.INFO: elif Level.LOWINFO <= level < Level.INFO:
@ -53,10 +54,10 @@ class Messenger(with_metaclass(Singleton, object)):
return Color.RED return Color.RED
def _reset(self): def _reset(self):
''' """
Get a reset color (terminal escape sequence). Get a reset color (terminal escape sequence).
''' """
if not self._use_color: if not self._use_color:
return '' return ""
else: else:
return Color.RESET return Color.RESET

View File

@ -1,25 +1,26 @@
from .messenger import Messenger from .messenger import Messenger
from .context import Context from .context import Context
class Plugin(object): class Plugin(object):
''' """
Abstract base class for commands that process directives. Abstract base class for commands that process directives.
''' """
def __init__(self, context): def __init__(self, context):
self._context = context self._context = context
self._log = Messenger() self._log = Messenger()
def can_handle(self, directive): def can_handle(self, directive):
''' """
Returns true if the Plugin can handle the directive. Returns true if the Plugin can handle the directive.
''' """
raise NotImplementedError raise NotImplementedError
def handle(self, directive, data): def handle(self, directive, data):
''' """
Executes the directive. Executes the directive.
Returns true if the Plugin successfully handled the directive. Returns true if the Plugin successfully handled the directive.
''' """
raise NotImplementedError raise NotImplementedError

View File

@ -3,43 +3,43 @@ import dotbot
class Clean(dotbot.Plugin): class Clean(dotbot.Plugin):
''' """
Cleans broken symbolic links. Cleans broken symbolic links.
''' """
_directive = 'clean' _directive = "clean"
def can_handle(self, directive): def can_handle(self, directive):
return directive == self._directive return directive == self._directive
def handle(self, directive, data): def handle(self, directive, data):
if directive != self._directive: if directive != self._directive:
raise ValueError('Clean cannot handle directive %s' % directive) raise ValueError("Clean cannot handle directive %s" % directive)
return self._process_clean(data) return self._process_clean(data)
def _process_clean(self, targets): def _process_clean(self, targets):
success = True success = True
defaults = self._context.defaults().get(self._directive, {}) defaults = self._context.defaults().get(self._directive, {})
for target in targets: for target in targets:
force = defaults.get('force', False) force = defaults.get("force", False)
recursive = defaults.get('recursive', False) recursive = defaults.get("recursive", False)
if isinstance(targets, dict) and isinstance(targets[target], dict): if isinstance(targets, dict) and isinstance(targets[target], dict):
force = targets[target].get('force', force) force = targets[target].get("force", force)
recursive = targets[target].get('recursive', recursive) recursive = targets[target].get("recursive", recursive)
success &= self._clean(target, force, recursive) success &= self._clean(target, force, recursive)
if success: if success:
self._log.info('All targets have been cleaned') self._log.info("All targets have been cleaned")
else: else:
self._log.error('Some targets were not successfully cleaned') self._log.error("Some targets were not successfully cleaned")
return success return success
def _clean(self, target, force, recursive): def _clean(self, target, force, recursive):
''' """
Cleans all the broken symbolic links in target if they point to Cleans all the broken symbolic links in target if they point to
a subdirectory of the base directory or if forced to clean. a subdirectory of the base directory or if forced to clean.
''' """
if not os.path.isdir(os.path.expandvars(os.path.expanduser(target))): if not os.path.isdir(os.path.expandvars(os.path.expanduser(target))):
self._log.debug('Ignoring nonexistent directory %s' % target) self._log.debug("Ignoring nonexistent directory %s" % target)
return True return True
for item in os.listdir(os.path.expandvars(os.path.expanduser(target))): for item in os.listdir(os.path.expandvars(os.path.expanduser(target))):
path = os.path.join(os.path.expandvars(os.path.expanduser(target)), item) path = os.path.join(os.path.expandvars(os.path.expanduser(target)), item)
@ -51,16 +51,16 @@ class Clean(dotbot.Plugin):
if not os.path.exists(path) and os.path.islink(path): if not os.path.exists(path) and os.path.islink(path):
points_at = os.path.join(os.path.dirname(path), os.readlink(path)) points_at = os.path.join(os.path.dirname(path), os.readlink(path))
if self._in_directory(path, self._context.base_directory()) or force: if self._in_directory(path, self._context.base_directory()) or force:
self._log.lowinfo('Removing invalid link %s -> %s' % (path, points_at)) self._log.lowinfo("Removing invalid link %s -> %s" % (path, points_at))
os.remove(path) os.remove(path)
else: else:
self._log.lowinfo('Link %s -> %s not removed.' % (path, points_at)) self._log.lowinfo("Link %s -> %s not removed." % (path, points_at))
return True return True
def _in_directory(self, path, directory): def _in_directory(self, path, directory):
''' """
Returns true if the path is in the directory. Returns true if the path is in the directory.
''' """
directory = os.path.join(os.path.realpath(directory), '') directory = os.path.join(os.path.realpath(directory), "")
path = os.path.realpath(path) path = os.path.realpath(path)
return os.path.commonprefix([path, directory]) == directory return os.path.commonprefix([path, directory]) == directory

View File

@ -3,54 +3,54 @@ import dotbot
class Create(dotbot.Plugin): class Create(dotbot.Plugin):
''' """
Create empty paths. Create empty paths.
''' """
_directive = 'create' _directive = "create"
def can_handle(self, directive): def can_handle(self, directive):
return directive == self._directive return directive == self._directive
def handle(self, directive, data): def handle(self, directive, data):
if directive != self._directive: if directive != self._directive:
raise ValueError('Create cannot handle directive %s' % directive) raise ValueError("Create cannot handle directive %s" % directive)
return self._process_paths(data) return self._process_paths(data)
def _process_paths(self, paths): def _process_paths(self, paths):
success = True success = True
defaults = self._context.defaults().get('create', {}) defaults = self._context.defaults().get("create", {})
for key in paths: for key in paths:
path = os.path.expandvars(os.path.expanduser(key)) path = os.path.expandvars(os.path.expanduser(key))
mode = defaults.get('mode', 0o777) # same as the default for os.makedirs mode = defaults.get("mode", 0o777) # same as the default for os.makedirs
if isinstance(paths, dict): if isinstance(paths, dict):
options = paths[key] options = paths[key]
if options: if options:
mode = options.get('mode', mode) mode = options.get("mode", mode)
success &= self._create(path, mode) success &= self._create(path, mode)
if success: if success:
self._log.info('All paths have been set up') self._log.info("All paths have been set up")
else: else:
self._log.error('Some paths were not successfully set up') self._log.error("Some paths were not successfully set up")
return success return success
def _exists(self, path): def _exists(self, path):
''' """
Returns true if the path exists. Returns true if the path exists.
''' """
path = os.path.expanduser(path) path = os.path.expanduser(path)
return os.path.exists(path) return os.path.exists(path)
def _create(self, path, mode): def _create(self, path, mode):
success = True success = True
if not self._exists(path): if not self._exists(path):
self._log.debug('Trying to create path %s with mode %o' % (path, mode)) self._log.debug("Trying to create path %s with mode %o" % (path, mode))
try: try:
self._log.lowinfo('Creating path %s' % path) self._log.lowinfo("Creating path %s" % path)
os.makedirs(path, mode) os.makedirs(path, mode)
except OSError: except OSError:
self._log.warning('Failed to create path %s' % path) self._log.warning("Failed to create path %s" % path)
success = False success = False
else: else:
self._log.lowinfo('Path exists %s' % path) self._log.lowinfo("Path exists %s" % path)
return success return success

View File

@ -8,53 +8,55 @@ import subprocess
class Link(dotbot.Plugin): class Link(dotbot.Plugin):
''' """
Symbolically links dotfiles. Symbolically links dotfiles.
''' """
_directive = 'link' _directive = "link"
def can_handle(self, directive): def can_handle(self, directive):
return directive == self._directive return directive == self._directive
def handle(self, directive, data): def handle(self, directive, data):
if directive != self._directive: if directive != self._directive:
raise ValueError('Link cannot handle directive %s' % directive) raise ValueError("Link cannot handle directive %s" % directive)
return self._process_links(data) return self._process_links(data)
def _process_links(self, links): def _process_links(self, links):
success = True success = True
defaults = self._context.defaults().get('link', {}) defaults = self._context.defaults().get("link", {})
for destination, source in links.items(): for destination, source in links.items():
destination = os.path.expandvars(destination) destination = os.path.expandvars(destination)
relative = defaults.get('relative', False) relative = defaults.get("relative", False)
# support old "canonicalize-path" key for compatibility # support old "canonicalize-path" key for compatibility
canonical_path = defaults.get('canonicalize', defaults.get('canonicalize-path', True)) canonical_path = defaults.get("canonicalize", defaults.get("canonicalize-path", True))
force = defaults.get('force', False) force = defaults.get("force", False)
relink = defaults.get('relink', False) relink = defaults.get("relink", False)
create = defaults.get('create', False) create = defaults.get("create", False)
use_glob = defaults.get('glob', False) use_glob = defaults.get("glob", False)
base_prefix = defaults.get('prefix', '') base_prefix = defaults.get("prefix", "")
test = defaults.get('if', None) test = defaults.get("if", None)
ignore_missing = defaults.get('ignore-missing', False) ignore_missing = defaults.get("ignore-missing", False)
exclude_paths = defaults.get('exclude', []) exclude_paths = defaults.get("exclude", [])
if isinstance(source, dict): if isinstance(source, dict):
# extended config # extended config
test = source.get('if', test) test = source.get("if", test)
relative = source.get('relative', relative) relative = source.get("relative", relative)
canonical_path = source.get('canonicalize', source.get('canonicalize-path', canonical_path)) canonical_path = source.get(
force = source.get('force', force) "canonicalize", source.get("canonicalize-path", canonical_path)
relink = source.get('relink', relink) )
create = source.get('create', create) force = source.get("force", force)
use_glob = source.get('glob', use_glob) relink = source.get("relink", relink)
base_prefix = source.get('prefix', base_prefix) create = source.get("create", create)
ignore_missing = source.get('ignore-missing', ignore_missing) use_glob = source.get("glob", use_glob)
exclude_paths = source.get('exclude', exclude_paths) base_prefix = source.get("prefix", base_prefix)
path = self._default_source(destination, source.get('path')) ignore_missing = source.get("ignore-missing", ignore_missing)
exclude_paths = source.get("exclude", exclude_paths)
path = self._default_source(destination, source.get("path"))
else: else:
path = self._default_source(destination, source) path = self._default_source(destination, source)
if test is not None and not self._test_success(test): if test is not None and not self._test_success(test):
self._log.lowinfo('Skipping %s' % destination) self._log.lowinfo("Skipping %s" % destination)
continue continue
path = os.path.expandvars(os.path.expanduser(path)) path = os.path.expandvars(os.path.expanduser(path))
if use_glob: if use_glob:
@ -63,26 +65,36 @@ class Link(dotbot.Plugin):
self._log.warning("Globbing couldn't find anything matching " + str(path)) self._log.warning("Globbing couldn't find anything matching " + str(path))
success = False success = False
continue continue
if len(glob_results) == 1 and destination[-1] == '/': if len(glob_results) == 1 and destination[-1] == "/":
self._log.error("Ambiguous action requested.") self._log.error("Ambiguous action requested.")
self._log.error("No wildcard in glob, directory use undefined: " + self._log.error(
destination + " -> " + str(glob_results)) "No wildcard in glob, directory use undefined: "
+ destination
+ " -> "
+ str(glob_results)
)
self._log.warning("Did you want to link the directory or into it?") self._log.warning("Did you want to link the directory or into it?")
success = False success = False
continue continue
elif len(glob_results) == 1 and destination[-1] != '/': elif len(glob_results) == 1 and destination[-1] != "/":
# perform a normal link operation # perform a normal link operation
if create: if create:
success &= self._create(destination) success &= self._create(destination)
if force or relink: if force or relink:
success &= self._delete(path, destination, relative, canonical_path, force) success &= self._delete(path, destination, relative, canonical_path, force)
success &= self._link(path, destination, relative, canonical_path, ignore_missing) success &= self._link(
path, destination, relative, canonical_path, ignore_missing
)
else: else:
self._log.lowinfo("Globs from '" + path + "': " + str(glob_results)) self._log.lowinfo("Globs from '" + path + "': " + str(glob_results))
for glob_full_item in glob_results: for glob_full_item in glob_results:
# Find common dirname between pattern and the item: # Find common dirname between pattern and the item:
glob_dirname = os.path.dirname(os.path.commonprefix([path, glob_full_item])) glob_dirname = os.path.dirname(os.path.commonprefix([path, glob_full_item]))
glob_item = (glob_full_item if len(glob_dirname) == 0 else glob_full_item[len(glob_dirname) + 1:]) glob_item = (
glob_full_item
if len(glob_dirname) == 0
else glob_full_item[len(glob_dirname) + 1 :]
)
# Add prefix to basepath, if provided # Add prefix to basepath, if provided
if base_prefix: if base_prefix:
glob_item = base_prefix + glob_item glob_item = base_prefix + glob_item
@ -91,39 +103,52 @@ class Link(dotbot.Plugin):
if create: if create:
success &= self._create(glob_link_destination) success &= self._create(glob_link_destination)
if force or relink: if force or relink:
success &= self._delete(glob_full_item, glob_link_destination, relative, canonical_path, force) success &= self._delete(
success &= self._link(glob_full_item, glob_link_destination, relative, canonical_path, ignore_missing) glob_full_item,
glob_link_destination,
relative,
canonical_path,
force,
)
success &= self._link(
glob_full_item,
glob_link_destination,
relative,
canonical_path,
ignore_missing,
)
else: else:
if create: if create:
success &= self._create(destination) success &= self._create(destination)
if not ignore_missing and not self._exists(os.path.join(self._context.base_directory(), path)): if not ignore_missing and not self._exists(
os.path.join(self._context.base_directory(), path)
):
# we seemingly check this twice (here and in _link) because # we seemingly check this twice (here and in _link) because
# if the file doesn't exist and force is True, we don't # if the file doesn't exist and force is True, we don't
# want to remove the original (this is tested by # want to remove the original (this is tested by
# link-force-leaves-when-nonexistent.bash) # link-force-leaves-when-nonexistent.bash)
success = False success = False
self._log.warning('Nonexistent source %s -> %s' % self._log.warning("Nonexistent source %s -> %s" % (destination, path))
(destination, path))
continue continue
if force or relink: if force or relink:
success &= self._delete(path, destination, relative, canonical_path, force) success &= self._delete(path, destination, relative, canonical_path, force)
success &= self._link(path, destination, relative, canonical_path, ignore_missing) success &= self._link(path, destination, relative, canonical_path, ignore_missing)
if success: if success:
self._log.info('All links have been set up') self._log.info("All links have been set up")
else: else:
self._log.error('Some links were not successfully set up') self._log.error("Some links were not successfully set up")
return success return success
def _test_success(self, command): def _test_success(self, command):
ret = dotbot.util.shell_command(command, cwd=self._context.base_directory()) ret = dotbot.util.shell_command(command, cwd=self._context.base_directory())
if ret != 0: if ret != 0:
self._log.debug('Test \'%s\' returned false' % command) self._log.debug("Test '%s' returned false" % command)
return ret == 0 return ret == 0
def _default_source(self, destination, source): def _default_source(self, destination, source):
if source is None: if source is None:
basename = os.path.basename(destination) basename = os.path.basename(destination)
if basename.startswith('.'): if basename.startswith("."):
return basename[1:] return basename[1:]
else: else:
return basename return basename
@ -131,20 +156,20 @@ class Link(dotbot.Plugin):
return source return source
def _glob(self, path): def _glob(self, path):
''' """
Wrap `glob.glob` in a python agnostic way, catching errors in usage. Wrap `glob.glob` in a python agnostic way, catching errors in usage.
''' """
if (sys.version_info < (3, 5) and '**' in path): if sys.version_info < (3, 5) and "**" in path:
self._log.error('Link cannot handle recursive glob ("**") for Python < version 3.5: "%s"' % path) self._log.error(
'Link cannot handle recursive glob ("**") for Python < version 3.5: "%s"' % path
)
return [] return []
# call glob.glob; only python >= 3.5 supports recursive globs # call glob.glob; only python >= 3.5 supports recursive globs
found = ( glob.glob(path) found = glob.glob(path) if (sys.version_info < (3, 5)) else glob.glob(path, recursive=True)
if (sys.version_info < (3, 5)) else
glob.glob(path, recursive=True) )
# if using recursive glob (`**`), filter results to return only files: # if using recursive glob (`**`), filter results to return only files:
if '**' in path and not path.endswith(str(os.sep)): if "**" in path and not path.endswith(str(os.sep)):
self._log.debug("Excluding directories from recursive glob: " + str(path)) self._log.debug("Excluding directories from recursive glob: " + str(path))
found = [f for f in found if os.path.isfile(f)] found = [f for f in found if os.path.isfile(f)]
# return matched results # return matched results
return found return found
@ -156,28 +181,28 @@ class Link(dotbot.Plugin):
exclude = [] exclude = []
for expat in exclude_paths: for expat in exclude_paths:
self._log.debug("Excluding globs with pattern: " + str(expat)) self._log.debug("Excluding globs with pattern: " + str(expat))
exclude.extend( self._glob(expat) ) exclude.extend(self._glob(expat))
self._log.debug("Excluded globs from '" + path + "': " + str(exclude)) self._log.debug("Excluded globs from '" + path + "': " + str(exclude))
ret = set(include) - set(exclude) ret = set(include) - set(exclude)
return list(ret) return list(ret)
def _is_link(self, path): def _is_link(self, path):
''' """
Returns true if the path is a symbolic link. Returns true if the path is a symbolic link.
''' """
return os.path.islink(os.path.expanduser(path)) return os.path.islink(os.path.expanduser(path))
def _link_destination(self, path): def _link_destination(self, path):
''' """
Returns the destination of the symbolic link. Returns the destination of the symbolic link.
''' """
path = os.path.expanduser(path) path = os.path.expanduser(path)
return os.readlink(path) return os.readlink(path)
def _exists(self, path): def _exists(self, path):
''' """
Returns true if the path exists. Returns true if the path exists.
''' """
path = os.path.expanduser(path) path = os.path.expanduser(path)
return os.path.exists(path) return os.path.exists(path)
@ -189,10 +214,10 @@ class Link(dotbot.Plugin):
try: try:
os.makedirs(parent) os.makedirs(parent)
except OSError: except OSError:
self._log.warning('Failed to create directory %s' % parent) self._log.warning("Failed to create directory %s" % parent)
success = False success = False
else: else:
self._log.lowinfo('Creating directory %s' % parent) self._log.lowinfo("Creating directory %s" % parent)
return success return success
def _delete(self, source, path, relative, canonical_path, force): def _delete(self, source, path, relative, canonical_path, force):
@ -201,8 +226,9 @@ class Link(dotbot.Plugin):
fullpath = os.path.expanduser(path) fullpath = os.path.expanduser(path)
if relative: if relative:
source = self._relative_path(source, fullpath) source = self._relative_path(source, fullpath)
if ((self._is_link(path) and self._link_destination(path) != source) or if (self._is_link(path) and self._link_destination(path) != source) or (
(self._exists(path) and not self._is_link(path))): self._exists(path) and not self._is_link(path)
):
removed = False removed = False
try: try:
if os.path.islink(fullpath): if os.path.islink(fullpath):
@ -216,27 +242,27 @@ class Link(dotbot.Plugin):
os.remove(fullpath) os.remove(fullpath)
removed = True removed = True
except OSError: except OSError:
self._log.warning('Failed to remove %s' % path) self._log.warning("Failed to remove %s" % path)
success = False success = False
else: else:
if removed: if removed:
self._log.lowinfo('Removing %s' % path) self._log.lowinfo("Removing %s" % path)
return success return success
def _relative_path(self, source, destination): def _relative_path(self, source, destination):
''' """
Returns the relative path to get to the source file from the Returns the relative path to get to the source file from the
destination file. destination file.
''' """
destination_dir = os.path.dirname(destination) destination_dir = os.path.dirname(destination)
return os.path.relpath(source, destination_dir) return os.path.relpath(source, destination_dir)
def _link(self, source, link_name, relative, canonical_path, ignore_missing): def _link(self, source, link_name, relative, canonical_path, ignore_missing):
''' """
Links link_name to source. Links link_name to source.
Returns true if successfully linked files. Returns true if successfully linked files.
''' """
success = False success = False
destination = os.path.expanduser(link_name) destination = os.path.expanduser(link_name)
base_directory = self._context.base_directory(canonical_path=canonical_path) base_directory = self._context.base_directory(canonical_path=canonical_path)
@ -245,10 +271,14 @@ class Link(dotbot.Plugin):
source = self._relative_path(absolute_source, destination) source = self._relative_path(absolute_source, destination)
else: else:
source = absolute_source source = absolute_source
if (not self._exists(link_name) and self._is_link(link_name) and if (
self._link_destination(link_name) != source): not self._exists(link_name)
self._log.warning('Invalid link %s -> %s' % and self._is_link(link_name)
(link_name, self._link_destination(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 # 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 # directory, and if source is relative, it will be relative to the
# destination directory # destination directory
@ -256,26 +286,23 @@ class Link(dotbot.Plugin):
try: try:
os.symlink(source, destination) os.symlink(source, destination)
except OSError: except OSError:
self._log.warning('Linking failed %s -> %s' % (link_name, source)) self._log.warning("Linking failed %s -> %s" % (link_name, source))
else: else:
self._log.lowinfo('Creating link %s -> %s' % (link_name, source)) self._log.lowinfo("Creating link %s -> %s" % (link_name, source))
success = True success = True
elif self._exists(link_name) and not self._is_link(link_name): elif self._exists(link_name) and not self._is_link(link_name):
self._log.warning( self._log.warning("%s already exists but is a regular file or directory" % link_name)
'%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: elif self._is_link(link_name) and self._link_destination(link_name) != source:
self._log.warning('Incorrect link %s -> %s' % self._log.warning(
(link_name, self._link_destination(link_name))) "Incorrect link %s -> %s" % (link_name, self._link_destination(link_name))
)
# again, we use absolute_source to check for existence # again, we use absolute_source to check for existence
elif not self._exists(absolute_source): elif not self._exists(absolute_source):
if self._is_link(link_name): if self._is_link(link_name):
self._log.warning('Nonexistent source %s -> %s' % self._log.warning("Nonexistent source %s -> %s" % (link_name, source))
(link_name, source))
else: else:
self._log.warning('Nonexistent source for %s : %s' % self._log.warning("Nonexistent source for %s : %s" % (link_name, source))
(link_name, source))
else: else:
self._log.lowinfo('Link exists %s -> %s' % (link_name, source)) self._log.lowinfo("Link exists %s -> %s" % (link_name, source))
success = True success = True
return success return success

View File

@ -5,11 +5,11 @@ import dotbot.util
class Shell(dotbot.Plugin): class Shell(dotbot.Plugin):
''' """
Run arbitrary shell commands. Run arbitrary shell commands.
''' """
_directive = 'shell' _directive = "shell"
_has_shown_override_message = False _has_shown_override_message = False
def can_handle(self, directive): def can_handle(self, directive):
@ -17,26 +17,25 @@ class Shell(dotbot.Plugin):
def handle(self, directive, data): def handle(self, directive, data):
if directive != self._directive: if directive != self._directive:
raise ValueError('Shell cannot handle directive %s' % raise ValueError("Shell cannot handle directive %s" % directive)
directive)
return self._process_commands(data) return self._process_commands(data)
def _process_commands(self, data): def _process_commands(self, data):
success = True success = True
defaults = self._context.defaults().get('shell', {}) defaults = self._context.defaults().get("shell", {})
options = self._get_option_overrides() options = self._get_option_overrides()
for item in data: for item in data:
stdin = defaults.get('stdin', False) stdin = defaults.get("stdin", False)
stdout = defaults.get('stdout', False) stdout = defaults.get("stdout", False)
stderr = defaults.get('stderr', False) stderr = defaults.get("stderr", False)
quiet = defaults.get('quiet', False) quiet = defaults.get("quiet", False)
if isinstance(item, dict): if isinstance(item, dict):
cmd = item['command'] cmd = item["command"]
msg = item.get('description', None) msg = item.get("description", None)
stdin = item.get('stdin', stdin) stdin = item.get("stdin", stdin)
stdout = item.get('stdout', stdout) stdout = item.get("stdout", stdout)
stderr = item.get('stderr', stderr) stderr = item.get("stderr", stderr)
quiet = item.get('quiet', quiet) quiet = item.get("quiet", quiet)
elif isinstance(item, list): elif isinstance(item, list):
cmd = item[0] cmd = item[0]
msg = item[1] if len(item) > 1 else None msg = item[1] if len(item) > 1 else None
@ -46,33 +45,33 @@ class Shell(dotbot.Plugin):
if msg is None: if msg is None:
self._log.lowinfo(cmd) self._log.lowinfo(cmd)
elif quiet: elif quiet:
self._log.lowinfo('%s' % msg) self._log.lowinfo("%s" % msg)
else: else:
self._log.lowinfo('%s [%s]' % (msg, cmd)) self._log.lowinfo("%s [%s]" % (msg, cmd))
stdout = options.get('stdout', stdout) stdout = options.get("stdout", stdout)
stderr = options.get('stderr', stderr) stderr = options.get("stderr", stderr)
ret = dotbot.util.shell_command( ret = dotbot.util.shell_command(
cmd, cmd,
cwd=self._context.base_directory(), cwd=self._context.base_directory(),
enable_stdin=stdin, enable_stdin=stdin,
enable_stdout=stdout, enable_stdout=stdout,
enable_stderr=stderr enable_stderr=stderr,
) )
if ret != 0: if ret != 0:
success = False success = False
self._log.warning('Command [%s] failed' % cmd) self._log.warning("Command [%s] failed" % cmd)
if success: if success:
self._log.info('All commands have been executed') self._log.info("All commands have been executed")
else: else:
self._log.error('Some commands were not successfully executed') self._log.error("Some commands were not successfully executed")
return success return success
def _get_option_overrides(self): def _get_option_overrides(self):
ret = {} ret = {}
options = self._context.options() options = self._context.options()
if options.verbose > 1: if options.verbose > 1:
ret['stderr'] = True ret["stderr"] = True
ret['stdout'] = True ret["stdout"] = True
if not self._has_shown_override_message: if not self._has_shown_override_message:
self._log.debug("Shell: Found cli option to force show stderr and stdout.") self._log.debug("Shell: Found cli option to force show stderr and stdout.")
self._has_shown_override_message = True self._has_shown_override_message = True

View File

@ -4,12 +4,12 @@ import platform
def shell_command(command, cwd=None, enable_stdin=False, enable_stdout=False, enable_stderr=False): def shell_command(command, cwd=None, enable_stdin=False, enable_stdout=False, enable_stderr=False):
with open(os.devnull, 'w') as devnull_w, open(os.devnull, 'r') as devnull_r: with open(os.devnull, "w") as devnull_w, open(os.devnull, "r") as devnull_r:
stdin = None if enable_stdin else devnull_r stdin = None if enable_stdin else devnull_r
stdout = None if enable_stdout else devnull_w stdout = None if enable_stdout else devnull_w
stderr = None if enable_stderr else devnull_w stderr = None if enable_stderr else devnull_w
executable = os.environ.get('SHELL') executable = os.environ.get("SHELL")
if platform.system() == 'Windows': if platform.system() == "Windows":
# We avoid setting the executable kwarg on Windows because it does # We avoid setting the executable kwarg on Windows because it does
# not have the desired effect when combined with shell=True. It # not have the desired effect when combined with shell=True. It
# will result in the correct program being run (e.g. bash), but it # will result in the correct program being run (e.g. bash), but it
@ -30,5 +30,5 @@ def shell_command(command, cwd=None, enable_stdin=False, enable_stdout=False, en
stdin=stdin, stdin=stdin,
stdout=stdout, stdout=stdout,
stderr=stderr, stderr=stderr,
cwd=cwd cwd=cwd,
) )

View File

@ -2,4 +2,5 @@ def with_metaclass(meta, *bases):
class metaclass(meta): class metaclass(meta):
def __new__(cls, name, this_bases, d): def __new__(cls, name, this_bases, d):
return meta(name, bases, d) return meta(name, bases, d)
return type.__new__(metaclass, 'temporary_class', (), {})
return type.__new__(metaclass, "temporary_class", (), {})

View File

@ -3,27 +3,31 @@ import sys, os.path
# We keep references to loaded modules so they don't get garbage collected. # We keep references to loaded modules so they don't get garbage collected.
loaded_modules = [] loaded_modules = []
def load(path): def load(path):
basename = os.path.basename(path) basename = os.path.basename(path)
module_name, extension = os.path.splitext(basename) module_name, extension = os.path.splitext(basename)
plugin = load_module(module_name, path) plugin = load_module(module_name, path)
loaded_modules.append(plugin) loaded_modules.append(plugin)
if sys.version_info >= (3, 5): if sys.version_info >= (3, 5):
import importlib.util import importlib.util
def load_module(module_name, path):
spec = importlib.util.spec_from_file_location(module_name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def load_module(module_name, path):
spec = importlib.util.spec_from_file_location(module_name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
elif sys.version_info >= (3, 3): elif sys.version_info >= (3, 3):
from importlib.machinery import SourceFileLoader from importlib.machinery import SourceFileLoader
def load_module(module_name, path):
return SourceFileLoader(module_name, path).load_module()
def load_module(module_name, path):
return SourceFileLoader(module_name, path).load_module()
else: else:
import imp import imp
def load_module(module_name, path): def load_module(module_name, path):
return imp.load_source(module_name, path) return imp.load_source(module_name, path)

View File

@ -1,5 +1,6 @@
class Singleton(type): class Singleton(type):
_instances = {} _instances = {}
def __call__(cls, *args, **kwargs): def __call__(cls, *args, **kwargs):
if cls not in cls._instances: if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)

View File

@ -1,4 +1,4 @@
def indent_lines(string, amount=2, delimiter='\n'): def indent_lines(string, amount=2, delimiter="\n"):
whitespace = ' ' * amount whitespace = " " * amount
sep = '%s%s' % (delimiter, whitespace) sep = "%s%s" % (delimiter, whitespace)
return '%s%s' % (whitespace, sep.join(string.split(delimiter))) return "%s%s" % (whitespace, sep.join(string.split(delimiter)))

12
pyproject.toml Normal file
View File

@ -0,0 +1,12 @@
[tool.black]
line-length = 100
exclude = '''
/(
\.git
| \.github
| .*\.egg-info
| build
| dist
| lib
)/
'''

View File

@ -1,5 +1,5 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
from codecs import open # For a consistent encoding from codecs import open # For a consistent encoding
from os import path from os import path
import re import re
@ -7,81 +7,63 @@ import re
here = path.dirname(__file__) here = path.dirname(__file__)
with open(path.join(here, 'README.md'), encoding='utf-8') as f: with open(path.join(here, "README.md"), encoding="utf-8") as f:
long_description = f.read() long_description = f.read()
def read(*names, **kwargs): def read(*names, **kwargs):
with open( with open(path.join(here, *names), encoding=kwargs.get("encoding", "utf8")) as fp:
path.join(here, *names),
encoding=kwargs.get("encoding", "utf8")
) as fp:
return fp.read() return fp.read()
def find_version(*file_paths): def find_version(*file_paths):
version_file = read(*file_paths) version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
version_file, re.M)
if version_match: if version_match:
return version_match.group(1) return version_match.group(1)
raise RuntimeError("Unable to find version string.") raise RuntimeError("Unable to find version string.")
setup( setup(
name='dotbot', name="dotbot",
version=find_version("dotbot", "__init__.py"),
version=find_version('dotbot', '__init__.py'), description="A tool that bootstraps your dotfiles",
description='A tool that bootstraps your dotfiles',
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type="text/markdown",
url="https://github.com/anishathalye/dotbot",
url='https://github.com/anishathalye/dotbot', author="Anish Athalye",
author_email="me@anishathalye.com",
author='Anish Athalye', license="MIT",
author_email='me@anishathalye.com',
license='MIT',
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
'Intended Audience :: Developers', "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 2",
'License :: OSI Approved :: MIT License', "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
'Programming Language :: Python :: 2', "Programming Language :: Python :: 3.5",
'Programming Language :: Python :: 2.7', "Programming Language :: Python :: 3.6",
'Programming Language :: Python :: 3', "Programming Language :: Python :: 3.7",
'Programming Language :: Python :: 3.2', "Programming Language :: Python :: 3.8",
'Programming Language :: Python :: 3.3', "Programming Language :: Python :: 3.9",
'Programming Language :: Python :: 3.4', "Programming Language :: Python :: 3.10",
'Programming Language :: Python :: 3.5', "Topic :: Utilities",
'Programming Language :: Python :: 3.6',
'Topic :: Utilities',
], ],
keywords="dotfiles",
keywords='dotfiles',
packages=find_packages(), packages=find_packages(),
setup_requires=[ setup_requires=[
'setuptools>=38.6.0', "setuptools>=38.6.0",
'wheel>=0.31.0', "wheel>=0.31.0",
], ],
install_requires=[ install_requires=[
'PyYAML>=5.3,<6', "PyYAML>=5.3,<6",
], ],
# To provide executable scripts, use entry points in preference to the # To provide executable scripts, use entry points in preference to the
# "scripts" keyword. Entry points provide cross-platform support and allow # "scripts" keyword. Entry points provide cross-platform support and allow
# pip to create the appropriate form of executable for the target platform. # pip to create the appropriate form of executable for the target platform.
entry_points={ entry_points={
'console_scripts': [ "console_scripts": [
'dotbot=dotbot:main', "dotbot=dotbot:main",
], ],
}, },
) )