Migrate code style to Black
This commit is contained in:
parent
ac5793ceb5
commit
769767c129
21 changed files with 395 additions and 316 deletions
|
@ -15,6 +15,3 @@ indent_size = 4
|
||||||
|
|
||||||
[*.yml]
|
[*.yml]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
135
dotbot/cli.py
135
dotbot/cli.py
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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", (), {})
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
12
pyproject.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[tool.black]
|
||||||
|
line-length = 100
|
||||||
|
exclude = '''
|
||||||
|
/(
|
||||||
|
\.git
|
||||||
|
| \.github
|
||||||
|
| .*\.egg-info
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
| lib
|
||||||
|
)/
|
||||||
|
'''
|
80
setup.py
80
setup.py
|
@ -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",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue