1
0
Fork 0
mirror of synced 2024-06-02 15:31:09 -04:00

Compare commits

..

No commits in common. "7a24ded5a6423d5ccaf5a42493ae9b8beab44e91" and "712b30a44508dacba8d2f250753035f168e0812b" have entirely different histories.

23 changed files with 208 additions and 107 deletions

View file

@ -5,14 +5,14 @@ on:
schedule:
- cron: '0 8 * * 6'
jobs:
test:
test-py3:
env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
strategy:
fail-fast: false
matrix:
os: ["ubuntu-20.04", "macos-latest"]
python: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.9"]
python: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.9"]
include:
- os: "windows-latest"
python: "3.8"
@ -20,8 +20,6 @@ jobs:
python: "3.9"
- os: "windows-latest"
python: "3.10"
- os: "windows-latest"
python: "3.11"
runs-on: ${{ matrix.os }}
name: "Test: Python ${{ matrix.python }} on ${{ matrix.os }}"
steps:
@ -40,6 +38,28 @@ jobs:
python -m tox
python -m tox -e coverage_report
- uses: codecov/codecov-action@v3
test-py2:
env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
runs-on: ubuntu-20.04
container:
image: python:2.7.18-buster
name: "Test: Python 2.7 on ubuntu-20.04"
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: "Install dependencies"
run: |
python -m pip install --upgrade pip setuptools
python -m pip install tox tox-gh-actions
- name: "Run tests"
run: |
python -m tox
python -m tox -e coverage_report
- uses: codecov/codecov-action@v3
fmt:
name: Format
runs-on: ubuntu-22.04

View file

@ -1,52 +1,49 @@
Note: this changelog only lists feature additions, not bugfixes. For details on
those, see the Git history.
- 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,
* 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

View file

@ -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) [![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 [![Build Status](https://github.com/anishathalye/dotbot/workflows/CI/badge.svg)](https://github.com/anishathalye/dotbot/actions?query=workflow%3ACI)
Dotbot makes installing your dotfiles as easy as `git clone $url && cd dotfiles
&& ./install`, even on a freshly installed system!
@ -191,13 +191,13 @@ 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 |
| `?` | 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 (Python >= 3.5 only) |
| `?` | 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/.*`.

View file

@ -7,8 +7,9 @@
# 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 python3 >/dev/null 2>&1 && exec python3 "$0" "$@"
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" "$@"
>&2 echo "error: cannot find python"
exit 1
'''
@ -17,14 +18,6 @@ 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__)))
@ -32,7 +25,11 @@ def inject(lib_path):
path = os.path.join(PROJECT_ROOT_DIRECTORY, 'lib', lib_path)
sys.path.insert(0, path)
inject('pyyaml/lib3')
# version dependent libraries
if sys.version_info[0] >= 3:
inject('pyyaml/lib3')
else:
inject('pyyaml/lib')
if os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'dotbot')):
if PROJECT_ROOT_DIRECTORY not in sys.path:

View file

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

View file

@ -6,7 +6,7 @@ import yaml
from .util import string
class ConfigReader:
class ConfigReader(object):
def __init__(self, config_file_path):
self._config = self._read(config_file_path)

View file

@ -3,7 +3,7 @@ import os
from argparse import Namespace
class Context:
class Context(object):
"""
Contextual data and information for plugins.
"""

View file

@ -6,7 +6,7 @@ from .messenger import Messenger
from .plugin import Plugin
class Dispatcher:
class Dispatcher(object):
def __init__(
self,
base_directory,

View file

@ -1,4 +1,4 @@
class Color:
class Color(object):
NONE = ""
RESET = "\033[0m"
RED = "\033[91m"

View file

@ -1,4 +1,4 @@
class Level:
class Level(object):
NOTSET = 0
DEBUG = 10
LOWINFO = 15

View file

@ -1,9 +1,10 @@
from ..util.compat import with_metaclass
from ..util.singleton import Singleton
from .color import Color
from .level import Level
class Messenger(metaclass=Singleton):
class Messenger(with_metaclass(Singleton, object)):
def __init__(self, level=Level.LOWINFO):
self.set_level(level)
self.use_color(True)

View file

@ -2,7 +2,7 @@ from .context import Context
from .messenger import Messenger
class Plugin:
class Plugin(object):
"""
Abstract base class for commands that process directives.
"""

View file

@ -137,7 +137,13 @@ class Link(Plugin):
"""
Wrap `glob.glob` in a python agnostic way, catching errors in usage.
"""
found = glob.glob(path, recursive=True)
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)
# 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:

6
dotbot/util/compat.py Normal file
View file

@ -0,0 +1,6 @@
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", (), {})

View file

@ -23,11 +23,23 @@ def load(path):
return plugins
import importlib.util
if sys.version_info >= (3, 5):
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):
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)

@ -1 +1 @@
Subproject commit c42fa3bff1eabdb64763bb1526d9ea1ccb708479
Subproject commit 2f463cf5b0e98a52bc20e348d1e69761bf263b86

2
setup.cfg Normal file
View file

@ -0,0 +1,2 @@
[bdist_wheel]
universal=1

View file

@ -1,4 +1,5 @@
import re
from codecs import open # For a consistent encoding
from os import path
from setuptools import find_packages, setup
@ -37,13 +38,15 @@ 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",
"Topic :: Utilities",
],
keywords="dotfiles",
@ -53,7 +56,7 @@ setup(
"wheel>=0.31.0",
],
install_requires=[
"PyYAML>=6.0.1,<7",
"PyYAML>=5.3,<6",
],
extras_require={
"dev": {

View file

@ -1,11 +1,9 @@
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
@ -13,6 +11,15 @@ 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."""
@ -28,7 +35,8 @@ def get_long_path(path):
return buffer.value
# On Linux, tempfile.TemporaryFile() requires unlink access.
# Python 2.7 compatibility:
# On Linux, Python 2.7's 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 = []
@ -41,6 +49,7 @@ 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)
@ -59,7 +68,11 @@ def wrap_function(function, function_path, arg_index, kwarg_key, root):
def wrap_open(root):
wrapped = getattr(builtins, "open")
try:
wrapped = getattr(builtins, "open")
except AttributeError:
# Python 2.7 compatibility
wrapped = getattr(__builtin__, "open")
def wrapper(*args, **kwargs):
if "file" in kwargs:
@ -194,7 +207,11 @@ def root(standardize_tmp):
patches.append(mock.patch(function_path, wrapped))
# open() must be separately wrapped.
function_path = "builtins.open"
if builtins is not None:
function_path = "builtins.open"
else:
# Python 2.7 compatibility
function_path = "__builtin__.open"
wrapped = wrap_open(current_root)
patches.append(mock.patch(function_path, wrapped))
@ -239,7 +256,7 @@ def home(monkeypatch, root):
yield home
class Dotfiles:
class Dotfiles(object):
"""Create and manage a dotfiles directory for a test."""
def __init__(self, root):

View file

@ -1,15 +1,25 @@
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", "python3"))
@pytest.mark.parametrize("python_name", (None, "python", "python2", "python3"))
def test_find_python_executable(python_name, home, dotfiles):
"""Verify that the sh/Python hybrid dotbot executable can find Python."""
@ -21,7 +31,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 = shutil.which("sh")
sh_path = which("sh")
os.symlink(sh_path, os.path.join(tmp_bin, "sh"))
if python_name:

View file

@ -537,6 +537,10 @@ 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."""
@ -769,11 +773,19 @@ 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"}}}])
os.symlink(
dotfiles.directory,
os.path.join(home, "dotfiles-symlink"),
target_is_directory=True,
)
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"),
)
run_dotbot(
"-c",
os.path.join(home, "dotfiles-symlink", os.path.basename(dotfiles.config_filename)),

View file

@ -6,11 +6,26 @@ 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 = shutil.which("git")
git = which("git")
if git is None:
pytest.skip("git is unavailable")
@ -37,7 +52,7 @@ def test_shim(root, home, dotfiles, run_dotbot):
# Run the shim script.
env = dict(os.environ)
if sys.platform[:5] == "win32":
args = [shutil.which("powershell"), "-ExecutionPolicy", "RemoteSigned", shim]
args = [which("powershell"), "-ExecutionPolicy", "RemoteSigned", shim]
env["USERPROFILE"] = home
else:
args = [shim]

11
tox.ini
View file

@ -3,9 +3,9 @@
; All older versions, and PyPy, lack full symlink support.
envlist =
coverage_erase
py{38, 39, 310, 311}-all_platforms
py{36, 37}-most_platforms
pypy3-most_platforms
py{38, 39, 310}-all_platforms
py{27, 35, 36, 37}-most_platforms
pypy{2, 3}-most_platforms
coverage_report
skip_missing_interpreters = true
@ -20,6 +20,7 @@ deps =
pytest
pytest-randomly
pyyaml
mock; python_version == "2.7"
commands =
coverage run -m pytest tests/
@ -63,10 +64,12 @@ python =
3.8: py38-all_platforms
3.9: py39-all_platforms
3.10: py310-all_platforms
3.11: py311-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