Compare commits
22 commits
Author | SHA1 | Date | |
---|---|---|---|
720206578a | |||
ba1c0f0e9a | |||
3f9e409669 | |||
26720b8264 | |||
9bb82306f4 | |||
eba0b24331 | |||
840cd164d2 | |||
cd1ff70113 | |||
390d306284 | |||
7a24ded5a6 | |||
b7e54a9bb3 | |||
eace76f697 | |||
b732baf163 | |||
81d4a434ef | |||
712b30a445 | |||
b04a3f1844 | |||
416f32f5fe | |||
9f8fd76f32 | |||
4daa065dc9 | |||
ed60c62432 | |||
53b3781fbb | |||
328bcb3259 |
|
@ -12,7 +12,7 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-20.04", "macos-latest"]
|
||||
python: ["2.7", "pypy-2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.9"]
|
||||
python: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.9"]
|
||||
include:
|
||||
- os: "windows-latest"
|
||||
python: "3.8"
|
||||
|
@ -20,15 +20,25 @@ jobs:
|
|||
python: "3.9"
|
||||
- os: "windows-latest"
|
||||
python: "3.10"
|
||||
- os: "windows-latest"
|
||||
python: "3.11"
|
||||
- os: "windows-latest"
|
||||
python: "3.12"
|
||||
exclude:
|
||||
- os: "macos-latest"
|
||||
python: "3.6"
|
||||
- os: "macos-latest"
|
||||
python: "3.7"
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Test: Python ${{ matrix.python }} on ${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
allow-prereleases: true
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools
|
||||
|
@ -38,7 +48,6 @@ jobs:
|
|||
python -m tox
|
||||
python -m tox -e coverage_report
|
||||
- uses: codecov/codecov-action@v3
|
||||
|
||||
fmt:
|
||||
name: Format
|
||||
runs-on: ubuntu-22.04
|
91
CHANGELOG.md
91
CHANGELOG.md
|
@ -1,49 +1,52 @@
|
|||
Note: this changelog only lists feature additions, not bugfixes. For details on
|
||||
those, see the Git history.
|
||||
|
||||
* v1.19
|
||||
* Add `mode:` option for `create`
|
||||
* Add `exclude:` option for `link`
|
||||
* v1.18
|
||||
* Add `--only` and `--except` flags
|
||||
* Add support to run with `python -m dotbot`
|
||||
* Add `--force-color` option
|
||||
* v1.17
|
||||
* Add `canonicalize-path:` option for `link`
|
||||
* v1.16
|
||||
* Add `create` plugin
|
||||
* v1.15
|
||||
* Add `quiet:` option for `shell`
|
||||
* v1.14
|
||||
* Add `if:` option for `link`
|
||||
* v1.13
|
||||
* Add `--no-color` flag
|
||||
* v1.12
|
||||
* Add globbing support to `link`
|
||||
* v1.11
|
||||
* Add force option to `clean` to remove all broken symlinks
|
||||
* v1.10
|
||||
* Update `link` to support shorthand syntax for links
|
||||
* v1.9
|
||||
* Add support for default options for commands
|
||||
* v1.8
|
||||
* Update `link` to be able to create relative links
|
||||
* v1.7
|
||||
* Add support for plugins
|
||||
* v1.6
|
||||
* Update `link` to expand environment variables in paths
|
||||
* v1.5
|
||||
* Update `link` to be able to automatically overwrite broken symlinks
|
||||
* v1.4
|
||||
* Update `shell` to allow for selectively enabling/disabling stdin, stdout,
|
||||
- v1.20
|
||||
- Drop support for Python 2 and old versions of Python 3: the minimum
|
||||
version supported is now Python 3.6
|
||||
- v1.19
|
||||
- Add `mode:` option for `create`
|
||||
- Add `exclude:` option for `link`
|
||||
- v1.18
|
||||
- Add `--only` and `--except` flags
|
||||
- Add support to run with `python -m dotbot`
|
||||
- Add `--force-color` option
|
||||
- v1.17
|
||||
- Add `canonicalize-path:` option for `link`
|
||||
- v1.16
|
||||
- Add `create` plugin
|
||||
- v1.15
|
||||
- Add `quiet:` option for `shell`
|
||||
- v1.14
|
||||
- Add `if:` option for `link`
|
||||
- v1.13
|
||||
- Add `--no-color` flag
|
||||
- v1.12
|
||||
- Add globbing support to `link`
|
||||
- v1.11
|
||||
- Add force option to `clean` to remove all broken symlinks
|
||||
- v1.10
|
||||
- Update `link` to support shorthand syntax for links
|
||||
- v1.9
|
||||
- Add support for default options for commands
|
||||
- v1.8
|
||||
- Update `link` to be able to create relative links
|
||||
- v1.7
|
||||
- Add support for plugins
|
||||
- v1.6
|
||||
- Update `link` to expand environment variables in paths
|
||||
- v1.5
|
||||
- Update `link` to be able to automatically overwrite broken symlinks
|
||||
- v1.4
|
||||
- Update `shell` to allow for selectively enabling/disabling stdin, stdout,
|
||||
and stderr
|
||||
* v1.3
|
||||
* Add support for YAML format configs
|
||||
* v1.2
|
||||
* Update `link` to be able to force create links (deleting things that were
|
||||
- v1.3
|
||||
- Add support for YAML format configs
|
||||
- v1.2
|
||||
- Update `link` to be able to force create links (deleting things that were
|
||||
previously there)
|
||||
* Update `link` to be able to create parent directories
|
||||
* v1.1
|
||||
* Update `clean` to remove old broken symlinks
|
||||
* v1.0
|
||||
* Initial commit
|
||||
- Update `link` to be able to create parent directories
|
||||
- v1.1
|
||||
- Update `clean` to remove old broken symlinks
|
||||
- v1.0
|
||||
- Initial commit
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
**Copyright (c) 2014-2021 Anish Athalye (me@anishathalye.com)**
|
||||
**Copyright (c) Anish Athalye (me@anishathalye.com)**
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
|
20
README.md
20
README.md
|
@ -1,4 +1,4 @@
|
|||
# Dotbot [![Build Status](https://github.com/anishathalye/dotbot/workflows/CI/badge.svg)](https://github.com/anishathalye/dotbot/actions?query=workflow%3ACI)
|
||||
# Dotbot [![Build Status](https://github.com/anishathalye/dotbot/workflows/CI/badge.svg)](https://github.com/anishathalye/dotbot/actions?query=workflow%3ACI) [![Coverage](https://codecov.io/gh/anishathalye/dotbot/branch/master/graph/badge.svg)](https://app.codecov.io/gh/anishathalye/dotbot) [![PyPI](https://img.shields.io/pypi/v/dotbot.svg)](https://pypi.org/pypi/dotbot/) [![Python 3.6+](https://img.shields.io/badge/python-3.6%2B-blue)](https://pypi.org/pypi/dotbot/)
|
||||
|
||||
Dotbot makes installing your dotfiles as easy as `git clone $url && cd dotfiles
|
||||
&& ./install`, even on a freshly installed system!
|
||||
|
@ -191,16 +191,18 @@ mapped to extended configuration dictionaries.
|
|||
|
||||
When `glob: True`, Dotbot uses [glob.glob](https://docs.python.org/3/library/glob.html#glob.glob) to resolve glob paths, expanding Unix shell-style wildcards, which are **not** the same as regular expressions; Only the following are expanded:
|
||||
|
||||
| Pattern | Meaning |
|
||||
|:---------|:-------------------------------------------------------|
|
||||
| `*` | matches anything |
|
||||
| `**` | matches any **file**, recursively (Python >= 3.5 only) |
|
||||
| `?` | matches any single character |
|
||||
| `[seq]` | matches any character in `seq` |
|
||||
| `[!seq]` | matches any character not in `seq` |
|
||||
| Pattern | Meaning |
|
||||
|:---------|:-----------------------------------|
|
||||
| `*` | matches anything |
|
||||
| `**` | matches any **file**, recursively |
|
||||
| `?` | matches any single character |
|
||||
| `[seq]` | matches any character in `seq` |
|
||||
| `[!seq]` | matches any character not in `seq` |
|
||||
|
||||
However, due to the design of `glob.glob`, using a glob pattern such as `config/*`, will **not** match items that begin with `.`. To specifically capture items that being with `.`, you will need to include the `.` in the pattern, like this: `config/.*`.
|
||||
|
||||
When using glob with the `exclude:` option, the paths in the exclude paths should be relative to the base directory, same as the glob pattern itself. For example, if a glob pattern `vim/*` matches directories `vim/autoload`, `vim/ftdetect`, `vim/ftplugin`, and `vim/spell`, and you want to ignore the spell directory, then you should use `exclude: ["vim/spell"]` (not just `"spell"`).
|
||||
|
||||
#### Example
|
||||
|
||||
```yaml
|
||||
|
@ -461,7 +463,7 @@ Do you have a feature request, bug report, or patch? Great! See
|
|||
|
||||
## License
|
||||
|
||||
Copyright (c) 2014-2021 Anish Athalye. Released under the MIT License. See
|
||||
Copyright (c) Anish Athalye. Released under the MIT License. See
|
||||
[LICENSE.md][license] for details.
|
||||
|
||||
[PyPI]: https://pypi.org/project/dotbot/
|
||||
|
|
17
bin/dotbot
17
bin/dotbot
|
@ -7,9 +7,8 @@
|
|||
# is useful because we don't know the name of the python binary.
|
||||
|
||||
''':' # begin python string; this line is interpreted by the shell as `:`
|
||||
command -v python >/dev/null 2>&1 && exec python "$0" "$@"
|
||||
command -v python3 >/dev/null 2>&1 && exec python3 "$0" "$@"
|
||||
command -v python2 >/dev/null 2>&1 && exec python2 "$0" "$@"
|
||||
command -v python >/dev/null 2>&1 && exec python "$0" "$@"
|
||||
>&2 echo "error: cannot find python"
|
||||
exit 1
|
||||
'''
|
||||
|
@ -18,6 +17,14 @@ exit 1
|
|||
|
||||
import sys, os
|
||||
|
||||
# this file is syntactically valid Python 2; bail out if the interpreter is Python 2
|
||||
if sys.version_info[0] < 3:
|
||||
print('error: this version of Dotbot is not compatible with Python 2:\nhttps://github.com/anishathalye/dotbot/wiki/Troubleshooting#python-2')
|
||||
exit(1)
|
||||
if sys.version_info < (3, 6):
|
||||
print('error: this version of Dotbot requires Python 3.6+')
|
||||
exit(1)
|
||||
|
||||
PROJECT_ROOT_DIRECTORY = os.path.dirname(
|
||||
os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
|
@ -25,11 +32,7 @@ def inject(lib_path):
|
|||
path = os.path.join(PROJECT_ROOT_DIRECTORY, 'lib', lib_path)
|
||||
sys.path.insert(0, path)
|
||||
|
||||
# version dependent libraries
|
||||
if sys.version_info[0] >= 3:
|
||||
inject('pyyaml/lib3')
|
||||
else:
|
||||
inject('pyyaml/lib')
|
||||
inject('pyyaml/lib')
|
||||
|
||||
if os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'dotbot')):
|
||||
if PROJECT_ROOT_DIRECTORY not in sys.path:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from .cli import main
|
||||
from .plugin import Plugin
|
||||
|
||||
__version__ = "1.19.1"
|
||||
__version__ = "1.20.1"
|
||||
|
|
|
@ -95,7 +95,7 @@ def main():
|
|||
["git", "rev-parse", "HEAD"],
|
||||
cwd=os.path.dirname(os.path.abspath(__file__)),
|
||||
stderr=devnull,
|
||||
)
|
||||
).decode("ascii")
|
||||
hash_msg = " (git %s)" % git_hash[:10]
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
hash_msg = ""
|
||||
|
|
|
@ -6,7 +6,7 @@ import yaml
|
|||
from .util import string
|
||||
|
||||
|
||||
class ConfigReader(object):
|
||||
class ConfigReader:
|
||||
def __init__(self, config_file_path):
|
||||
self._config = self._read(config_file_path)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import os
|
|||
from argparse import Namespace
|
||||
|
||||
|
||||
class Context(object):
|
||||
class Context:
|
||||
"""
|
||||
Contextual data and information for plugins.
|
||||
"""
|
||||
|
|
|
@ -6,7 +6,7 @@ from .messenger import Messenger
|
|||
from .plugin import Plugin
|
||||
|
||||
|
||||
class Dispatcher(object):
|
||||
class Dispatcher:
|
||||
def __init__(
|
||||
self,
|
||||
base_directory,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Color(object):
|
||||
class Color:
|
||||
NONE = ""
|
||||
RESET = "\033[0m"
|
||||
RED = "\033[91m"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Level(object):
|
||||
class Level:
|
||||
NOTSET = 0
|
||||
DEBUG = 10
|
||||
LOWINFO = 15
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from ..util.compat import with_metaclass
|
||||
from ..util.singleton import Singleton
|
||||
from .color import Color
|
||||
from .level import Level
|
||||
|
||||
|
||||
class Messenger(with_metaclass(Singleton, object)):
|
||||
class Messenger(metaclass=Singleton):
|
||||
def __init__(self, level=Level.LOWINFO):
|
||||
self.set_level(level)
|
||||
self.use_color(True)
|
||||
|
|
|
@ -2,7 +2,7 @@ from .context import Context
|
|||
from .messenger import Messenger
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
class Plugin:
|
||||
"""
|
||||
Abstract base class for commands that process directives.
|
||||
"""
|
||||
|
|
|
@ -59,64 +59,39 @@ class Link(Plugin):
|
|||
self._log.lowinfo("Skipping %s" % destination)
|
||||
continue
|
||||
path = os.path.normpath(os.path.expandvars(os.path.expanduser(path)))
|
||||
if use_glob:
|
||||
if use_glob and self._has_glob_chars(path):
|
||||
glob_results = self._create_glob_results(path, exclude_paths)
|
||||
if len(glob_results) == 0:
|
||||
self._log.warning("Globbing couldn't find anything matching " + str(path))
|
||||
success = False
|
||||
continue
|
||||
if len(glob_results) == 1 and destination[-1] == "/":
|
||||
self._log.error("Ambiguous action requested.")
|
||||
self._log.error(
|
||||
"No wildcard in glob, directory use undefined: "
|
||||
+ destination
|
||||
+ " -> "
|
||||
+ str(glob_results)
|
||||
self._log.lowinfo("Globs from '" + path + "': " + str(glob_results))
|
||||
for glob_full_item in glob_results:
|
||||
# Find common dirname between pattern and the 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 :]
|
||||
)
|
||||
self._log.warning("Did you want to link the directory or into it?")
|
||||
success = False
|
||||
continue
|
||||
elif len(glob_results) == 1 and destination[-1] != "/":
|
||||
# perform a normal link operation
|
||||
# Add prefix to basepath, if provided
|
||||
if base_prefix:
|
||||
glob_item = base_prefix + glob_item
|
||||
# where is it going
|
||||
glob_link_destination = os.path.join(destination, glob_item)
|
||||
if create:
|
||||
success &= self._create(destination)
|
||||
success &= self._create(glob_link_destination)
|
||||
if force or relink:
|
||||
success &= self._delete(path, destination, relative, canonical_path, force)
|
||||
success &= self._link(
|
||||
path, destination, relative, canonical_path, ignore_missing
|
||||
)
|
||||
else:
|
||||
self._log.lowinfo("Globs from '" + path + "': " + str(glob_results))
|
||||
for glob_full_item in glob_results:
|
||||
# Find common dirname between pattern and the 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 :]
|
||||
)
|
||||
# Add prefix to basepath, if provided
|
||||
if base_prefix:
|
||||
glob_item = base_prefix + glob_item
|
||||
# where is it going
|
||||
glob_link_destination = os.path.join(destination, glob_item)
|
||||
if create:
|
||||
success &= self._create(glob_link_destination)
|
||||
if force or relink:
|
||||
success &= self._delete(
|
||||
glob_full_item,
|
||||
glob_link_destination,
|
||||
relative,
|
||||
canonical_path,
|
||||
force,
|
||||
)
|
||||
success &= self._link(
|
||||
success &= self._delete(
|
||||
glob_full_item,
|
||||
glob_link_destination,
|
||||
relative,
|
||||
canonical_path,
|
||||
ignore_missing,
|
||||
force,
|
||||
)
|
||||
success &= self._link(
|
||||
glob_full_item,
|
||||
glob_link_destination,
|
||||
relative,
|
||||
canonical_path,
|
||||
ignore_missing,
|
||||
)
|
||||
else:
|
||||
if create:
|
||||
success &= self._create(destination)
|
||||
|
@ -155,17 +130,14 @@ class Link(Plugin):
|
|||
else:
|
||||
return source
|
||||
|
||||
def _has_glob_chars(self, path):
|
||||
return any(i in path for i in "?*[")
|
||||
|
||||
def _glob(self, path):
|
||||
"""
|
||||
Wrap `glob.glob` in a python agnostic way, catching errors in usage.
|
||||
"""
|
||||
if sys.version_info < (3, 5) and "**" in path:
|
||||
self._log.error(
|
||||
'Link cannot handle recursive glob ("**") for Python < version 3.5: "%s"' % path
|
||||
)
|
||||
return []
|
||||
# call glob.glob; only python >= 3.5 supports recursive globs
|
||||
found = glob.glob(path) if (sys.version_info < (3, 5)) else glob.glob(path, recursive=True)
|
||||
found = glob.glob(path, recursive=True)
|
||||
# normalize paths to ensure cross-platform compatibility
|
||||
found = [os.path.normpath(p) for p in found]
|
||||
# if using recursive glob (`**`), filter results to return only files:
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
def with_metaclass(meta, *bases):
|
||||
class metaclass(meta):
|
||||
def __new__(cls, name, this_bases, d):
|
||||
return meta(name, bases, d)
|
||||
|
||||
return type.__new__(metaclass, "temporary_class", (), {})
|
|
@ -23,23 +23,11 @@ def load(path):
|
|||
return plugins
|
||||
|
||||
|
||||
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
|
||||
|
||||
elif sys.version_info >= (3, 3):
|
||||
from importlib.machinery import SourceFileLoader
|
||||
|
||||
def load_module(module_name, path):
|
||||
return SourceFileLoader(module_name, path).load_module()
|
||||
|
||||
else:
|
||||
import imp
|
||||
|
||||
def load_module(module_name, path):
|
||||
return imp.load_source(module_name, path)
|
||||
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
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 2f463cf5b0e98a52bc20e348d1e69761bf263b86
|
||||
Subproject commit c42fa3bff1eabdb64763bb1526d9ea1ccb708479
|
|
@ -10,3 +10,8 @@ exclude = '''
|
|||
| lib
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
filterwarnings = [
|
||||
"error",
|
||||
]
|
||||
|
|
8
setup.py
8
setup.py
|
@ -1,5 +1,4 @@
|
|||
import re
|
||||
from codecs import open # For a consistent encoding
|
||||
from os import path
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
@ -38,15 +37,14 @@ setup(
|
|||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Utilities",
|
||||
],
|
||||
keywords="dotfiles",
|
||||
|
@ -56,7 +54,7 @@ setup(
|
|||
"wheel>=0.31.0",
|
||||
],
|
||||
install_requires=[
|
||||
"PyYAML>=5.3,<6",
|
||||
"PyYAML>=6.0.1,<7",
|
||||
],
|
||||
extras_require={
|
||||
"dev": {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import builtins
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest.mock as mock
|
||||
from shutil import rmtree
|
||||
|
||||
import pytest
|
||||
|
@ -11,15 +13,6 @@ import yaml
|
|||
|
||||
import dotbot.cli
|
||||
|
||||
try:
|
||||
import builtins
|
||||
import unittest.mock as mock
|
||||
except ImportError:
|
||||
# Python 2.7 compatibility
|
||||
builtins = None
|
||||
import __builtin__
|
||||
import mock # noqa: module not found
|
||||
|
||||
|
||||
def get_long_path(path):
|
||||
"""Get the long path for a given path."""
|
||||
|
@ -35,8 +28,7 @@ def get_long_path(path):
|
|||
return buffer.value
|
||||
|
||||
|
||||
# Python 2.7 compatibility:
|
||||
# On Linux, Python 2.7's tempfile.TemporaryFile() requires unlink access.
|
||||
# On Linux, tempfile.TemporaryFile() requires unlink access.
|
||||
# This list is updated by a tempfile._mkstemp_inner() wrapper,
|
||||
# and its contents are checked by wrapped functions.
|
||||
allowed_tempfile_internal_unlink_calls = []
|
||||
|
@ -49,7 +41,6 @@ def wrap_function(function, function_path, arg_index, kwarg_key, root):
|
|||
else:
|
||||
value = args[arg_index]
|
||||
|
||||
# Python 2.7 compatibility:
|
||||
# Allow tempfile.TemporaryFile's internal unlink calls to work.
|
||||
if value in allowed_tempfile_internal_unlink_calls:
|
||||
return function(*args, **kwargs)
|
||||
|
@ -68,11 +59,7 @@ def wrap_function(function, function_path, arg_index, kwarg_key, root):
|
|||
|
||||
|
||||
def wrap_open(root):
|
||||
try:
|
||||
wrapped = getattr(builtins, "open")
|
||||
except AttributeError:
|
||||
# Python 2.7 compatibility
|
||||
wrapped = getattr(__builtin__, "open")
|
||||
wrapped = getattr(builtins, "open")
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
if "file" in kwargs:
|
||||
|
@ -207,11 +194,7 @@ def root(standardize_tmp):
|
|||
patches.append(mock.patch(function_path, wrapped))
|
||||
|
||||
# open() must be separately wrapped.
|
||||
if builtins is not None:
|
||||
function_path = "builtins.open"
|
||||
else:
|
||||
# Python 2.7 compatibility
|
||||
function_path = "__builtin__.open"
|
||||
function_path = "builtins.open"
|
||||
wrapped = wrap_open(current_root)
|
||||
patches.append(mock.patch(function_path, wrapped))
|
||||
|
||||
|
@ -236,7 +219,10 @@ def root(standardize_tmp):
|
|||
finally:
|
||||
[patch.stop() for patch in patches]
|
||||
os.chdir(current_working_directory)
|
||||
rmtree(current_root, onerror=rmtree_error_handler)
|
||||
if sys.version_info >= (3, 12):
|
||||
rmtree(current_root, onexc=rmtree_error_handler)
|
||||
else:
|
||||
rmtree(current_root, onerror=rmtree_error_handler)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -256,7 +242,7 @@ def home(monkeypatch, root):
|
|||
yield home
|
||||
|
||||
|
||||
class Dotfiles(object):
|
||||
class Dotfiles:
|
||||
"""Create and manage a dotfiles directory for a test."""
|
||||
|
||||
def __init__(self, root):
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def which(name):
|
||||
"""Find an executable.
|
||||
|
||||
Python 2.7 doesn't have shutil.which().
|
||||
"""
|
||||
|
||||
for path in os.environ["PATH"].split(os.pathsep):
|
||||
if os.path.isfile(os.path.join(path, name)):
|
||||
return os.path.join(path, name)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"sys.platform[:5] == 'win32'",
|
||||
reason="The hybrid sh/Python dotbot script doesn't run on Windows platforms",
|
||||
)
|
||||
@pytest.mark.parametrize("python_name", (None, "python", "python2", "python3"))
|
||||
@pytest.mark.parametrize("python_name", (None, "python", "python3"))
|
||||
def test_find_python_executable(python_name, home, dotfiles):
|
||||
"""Verify that the sh/Python hybrid dotbot executable can find Python."""
|
||||
|
||||
|
@ -31,7 +21,7 @@ def test_find_python_executable(python_name, home, dotfiles):
|
|||
# Create a link to sh.
|
||||
tmp_bin = os.path.join(home, "tmp_bin")
|
||||
os.makedirs(tmp_bin)
|
||||
sh_path = which("sh")
|
||||
sh_path = shutil.which("sh")
|
||||
os.symlink(sh_path, os.path.join(tmp_bin, "sh"))
|
||||
|
||||
if python_name:
|
||||
|
|
|
@ -270,7 +270,7 @@ def test_link_glob_4(home, dotfiles, run_dotbot):
|
|||
|
||||
|
||||
@pytest.mark.parametrize("path", ("foo", "foo/"))
|
||||
def test_link_glob_ambiguous_failure(path, home, dotfiles, run_dotbot):
|
||||
def test_link_glob_ignore_no_glob_chars(path, home, dotfiles, run_dotbot):
|
||||
"""Verify ambiguous link globbing fails."""
|
||||
|
||||
dotfiles.makedirs("foo")
|
||||
|
@ -286,28 +286,8 @@ def test_link_glob_ambiguous_failure(path, home, dotfiles, run_dotbot):
|
|||
}
|
||||
]
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
run_dotbot()
|
||||
assert not os.path.exists(os.path.join(home, "foo"))
|
||||
|
||||
|
||||
def test_link_glob_ambiguous_success(home, dotfiles, run_dotbot):
|
||||
"""Verify the case where ambiguous link globbing succeeds."""
|
||||
|
||||
dotfiles.makedirs("foo")
|
||||
dotfiles.write_config(
|
||||
[
|
||||
{
|
||||
"link": {
|
||||
"~/foo": {
|
||||
"path": "foo",
|
||||
"glob": True,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
run_dotbot()
|
||||
assert os.path.islink(os.path.join(home, "foo"))
|
||||
assert os.path.exists(os.path.join(home, "foo"))
|
||||
|
||||
|
||||
|
@ -557,10 +537,6 @@ def test_link_glob_patterns(pattern, expect_file, home, dotfiles, run_dotbot):
|
|||
assert not os.path.islink(os.path.join(home, "globtest", "." + fruit))
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"sys.version_info < (3, 5)",
|
||||
reason="Python 3.5 required for ** globbing",
|
||||
)
|
||||
def test_link_glob_recursive(home, dotfiles, run_dotbot):
|
||||
"""Verify recursive link globbing and exclusions."""
|
||||
|
||||
|
@ -587,6 +563,39 @@ def test_link_glob_recursive(home, dotfiles, run_dotbot):
|
|||
assert file.read() == "cherry"
|
||||
|
||||
|
||||
def test_link_glob_no_match(home, dotfiles, run_dotbot):
|
||||
"""Verify that a glob with no match doesn't raise an error."""
|
||||
|
||||
dotfiles.makedirs("foo")
|
||||
dotfiles.write_config(
|
||||
[
|
||||
{"defaults": {"link": {"glob": True, "create": True}}},
|
||||
{"link": {"~/.config/foo": "foo/*"}},
|
||||
]
|
||||
)
|
||||
run_dotbot()
|
||||
|
||||
|
||||
def test_link_glob_single_match(home, dotfiles, run_dotbot):
|
||||
"""Verify linking works even when glob matches exactly one file."""
|
||||
# regression test for https://github.com/anishathalye/dotbot/issues/282
|
||||
|
||||
dotfiles.write("foo/a", "apple")
|
||||
dotfiles.write_config(
|
||||
[
|
||||
{"defaults": {"link": {"glob": True, "create": True}}},
|
||||
{"link": {"~/.config/foo": "foo/*"}},
|
||||
]
|
||||
)
|
||||
run_dotbot()
|
||||
|
||||
assert not os.path.islink(os.path.join(home, ".config"))
|
||||
assert not os.path.islink(os.path.join(home, ".config", "foo"))
|
||||
assert os.path.islink(os.path.join(home, ".config", "foo", "a"))
|
||||
with open(os.path.join(home, ".config", "foo", "a")) as file:
|
||||
assert file.read() == "apple"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"sys.platform[:5] == 'win32'",
|
||||
reason="These if commands won't run on Windows",
|
||||
|
@ -760,19 +769,11 @@ def test_link_no_canonicalize(key, home, dotfiles, run_dotbot):
|
|||
|
||||
dotfiles.write("f", "apple")
|
||||
dotfiles.write_config([{"defaults": {"link": {key: False}}}, {"link": {"~/.f": {"path": "f"}}}])
|
||||
try:
|
||||
os.symlink(
|
||||
dotfiles.directory,
|
||||
os.path.join(home, "dotfiles-symlink"),
|
||||
target_is_directory=True,
|
||||
)
|
||||
except TypeError:
|
||||
# Python 2 compatibility:
|
||||
# target_is_directory is only consistently available after Python 3.3.
|
||||
os.symlink(
|
||||
dotfiles.directory,
|
||||
os.path.join(home, "dotfiles-symlink"),
|
||||
)
|
||||
os.symlink(
|
||||
dotfiles.directory,
|
||||
os.path.join(home, "dotfiles-symlink"),
|
||||
target_is_directory=True,
|
||||
)
|
||||
run_dotbot(
|
||||
"-c",
|
||||
os.path.join(home, "dotfiles-symlink", os.path.basename(dotfiles.config_filename)),
|
||||
|
|
|
@ -6,26 +6,11 @@ import sys
|
|||
import pytest
|
||||
|
||||
|
||||
def which(name):
|
||||
"""Find an executable.
|
||||
|
||||
Python 2.7 doesn't have shutil.which().
|
||||
shutil.which() is used, if possible, to handle Windows' case-insensitivity.
|
||||
"""
|
||||
|
||||
if hasattr(shutil, "which"):
|
||||
return shutil.which(name)
|
||||
|
||||
for path in os.environ["PATH"].split(os.pathsep):
|
||||
if os.path.isfile(os.path.join(path, name)):
|
||||
return os.path.join(path, name)
|
||||
|
||||
|
||||
def test_shim(root, home, dotfiles, run_dotbot):
|
||||
"""Verify install shim works."""
|
||||
|
||||
# Skip the test if git is unavailable.
|
||||
git = which("git")
|
||||
git = shutil.which("git")
|
||||
if git is None:
|
||||
pytest.skip("git is unavailable")
|
||||
|
||||
|
@ -52,7 +37,7 @@ def test_shim(root, home, dotfiles, run_dotbot):
|
|||
# Run the shim script.
|
||||
env = dict(os.environ)
|
||||
if sys.platform[:5] == "win32":
|
||||
args = [which("powershell"), "-ExecutionPolicy", "RemoteSigned", shim]
|
||||
args = [shutil.which("powershell"), "-ExecutionPolicy", "RemoteSigned", shim]
|
||||
env["USERPROFILE"] = home
|
||||
else:
|
||||
args = [shim]
|
||||
|
|
16
tox.ini
16
tox.ini
|
@ -3,9 +3,9 @@
|
|||
; All older versions, and PyPy, lack full symlink support.
|
||||
envlist =
|
||||
coverage_erase
|
||||
py{38, 39, 310}-all_platforms
|
||||
py{27, 35, 36, 37}-most_platforms
|
||||
pypy{2, 3}-most_platforms
|
||||
py{38, 39, 310, 311, 312}-all_platforms
|
||||
py{36, 37}-most_platforms
|
||||
pypy3-most_platforms
|
||||
coverage_report
|
||||
skip_missing_interpreters = true
|
||||
|
||||
|
@ -20,7 +20,6 @@ deps =
|
|||
pytest
|
||||
pytest-randomly
|
||||
pyyaml
|
||||
mock; python_version == "2.7"
|
||||
|
||||
commands =
|
||||
coverage run -m pytest tests/
|
||||
|
@ -64,11 +63,14 @@ python =
|
|||
3.8: py38-all_platforms
|
||||
3.9: py39-all_platforms
|
||||
3.10: py310-all_platforms
|
||||
3.11: py311-all_platforms
|
||||
3.12: py312-all_platforms
|
||||
|
||||
; Run on most platforms (Linux and Mac)
|
||||
pypy-2.7: pypy2-most_platforms
|
||||
pypy-3.9: pypy3-most_platforms
|
||||
2.7: py27-most_platforms
|
||||
3.5: py35-most_platforms
|
||||
3.6: py36-most_platforms
|
||||
3.7: py37-most_platforms
|
||||
|
||||
; Disable problem matcher because it causes issues when running in a container;
|
||||
; see https://github.com/ymyzk/tox-gh-actions/issues/126
|
||||
problem_matcher = False
|
||||
|
|
Loading…
Reference in a new issue