mirror of
1
0
Fork 0

Drop support for Python 2 and Python < 3.6

This patch also deletes all of the compatibility code that we no longer
need.
This commit is contained in:
Anish Athalye 2023-09-09 20:39:45 -04:00
parent 712b30a445
commit 81d4a434ef
21 changed files with 98 additions and 203 deletions

View File

@ -5,14 +5,14 @@ on:
schedule: schedule:
- cron: '0 8 * * 6' - cron: '0 8 * * 6'
jobs: jobs:
test-py3: test:
env: env:
PIP_DISABLE_PIP_VERSION_CHECK: 1 PIP_DISABLE_PIP_VERSION_CHECK: 1
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: ["ubuntu-20.04", "macos-latest"] os: ["ubuntu-20.04", "macos-latest"]
python: ["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", "pypy-3.9"]
include: include:
- os: "windows-latest" - os: "windows-latest"
python: "3.8" python: "3.8"
@ -38,28 +38,6 @@ jobs:
python -m tox python -m tox
python -m tox -e coverage_report python -m tox -e coverage_report
- uses: codecov/codecov-action@v3 - 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: fmt:
name: Format name: Format
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04

View File

@ -1,49 +1,52 @@
Note: this changelog only lists feature additions, not bugfixes. For details on Note: this changelog only lists feature additions, not bugfixes. For details on
those, see the Git history. those, see the Git history.
* v1.19 - v1.20
* Add `mode:` option for `create` - Drop support for Python 2 and old versions of Python 3: the minimum
* Add `exclude:` option for `link` version supported is now Python 3.6
* v1.18 - v1.19
* Add `--only` and `--except` flags - Add `mode:` option for `create`
* Add support to run with `python -m dotbot` - Add `exclude:` option for `link`
* Add `--force-color` option - v1.18
* v1.17 - Add `--only` and `--except` flags
* Add `canonicalize-path:` option for `link` - Add support to run with `python -m dotbot`
* v1.16 - Add `--force-color` option
* Add `create` plugin - v1.17
* v1.15 - Add `canonicalize-path:` option for `link`
* Add `quiet:` option for `shell` - v1.16
* v1.14 - Add `create` plugin
* Add `if:` option for `link` - v1.15
* v1.13 - Add `quiet:` option for `shell`
* Add `--no-color` flag - v1.14
* v1.12 - Add `if:` option for `link`
* Add globbing support to `link` - v1.13
* v1.11 - Add `--no-color` flag
* Add force option to `clean` to remove all broken symlinks - v1.12
* v1.10 - Add globbing support to `link`
* Update `link` to support shorthand syntax for links - v1.11
* v1.9 - Add force option to `clean` to remove all broken symlinks
* Add support for default options for commands - v1.10
* v1.8 - Update `link` to support shorthand syntax for links
* Update `link` to be able to create relative links - v1.9
* v1.7 - Add support for default options for commands
* Add support for plugins - v1.8
* v1.6 - Update `link` to be able to create relative links
* Update `link` to expand environment variables in paths - v1.7
* v1.5 - Add support for plugins
* Update `link` to be able to automatically overwrite broken symlinks - v1.6
* v1.4 - Update `link` to expand environment variables in paths
* Update `shell` to allow for selectively enabling/disabling stdin, stdout, - 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 and stderr
* v1.3 - v1.3
* Add support for YAML format configs - Add support for YAML format configs
* v1.2 - v1.2
* Update `link` to be able to force create links (deleting things that were - Update `link` to be able to force create links (deleting things that were
previously there) previously there)
* Update `link` to be able to create parent directories - Update `link` to be able to create parent directories
* v1.1 - v1.1
* Update `clean` to remove old broken symlinks - Update `clean` to remove old broken symlinks
* v1.0 - v1.0
* Initial commit - Initial commit

View File

@ -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: 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 | | Pattern | Meaning |
|:---------|:-------------------------------------------------------| |:---------|:-----------------------------------|
| `*` | matches anything | | `*` | matches anything |
| `**` | matches any **file**, recursively (Python >= 3.5 only) | | `**` | matches any **file**, recursively |
| `?` | matches any single character | | `?` | matches any single character |
| `[seq]` | matches any character in `seq` | | `[seq]` | matches any character in `seq` |
| `[!seq]` | matches any character not 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/.*`. 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,9 +7,8 @@
# is useful because we don't know the name of the python binary. # is useful because we don't know the name of the python binary.
''':' # begin python string; this line is interpreted by the shell as `:` ''':' # 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 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" >&2 echo "error: cannot find python"
exit 1 exit 1
''' '''
@ -18,6 +17,14 @@ exit 1
import sys, os 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( PROJECT_ROOT_DIRECTORY = os.path.dirname(
os.path.dirname(os.path.realpath(__file__))) 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) path = os.path.join(PROJECT_ROOT_DIRECTORY, 'lib', lib_path)
sys.path.insert(0, path) sys.path.insert(0, path)
# version dependent libraries inject('pyyaml/lib3')
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 os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'dotbot')):
if PROJECT_ROOT_DIRECTORY not in sys.path: if PROJECT_ROOT_DIRECTORY not in sys.path:

View File

@ -6,7 +6,7 @@ import yaml
from .util import string from .util import string
class ConfigReader(object): class ConfigReader:
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)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,9 @@
from ..util.compat import with_metaclass
from ..util.singleton import Singleton from ..util.singleton import Singleton
from .color import Color from .color import Color
from .level import Level from .level import Level
class Messenger(with_metaclass(Singleton, object)): class Messenger(metaclass=Singleton):
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)

View File

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

View File

@ -137,13 +137,7 @@ class Link(Plugin):
""" """
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: found = glob.glob(path, recursive=True)
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 # normalize paths to ensure cross-platform compatibility
found = [os.path.normpath(p) for p in found] found = [os.path.normpath(p) for p in found]
# if using recursive glob (`**`), filter results to return only files: # if using recursive glob (`**`), filter results to return only files:

View File

@ -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", (), {})

View File

@ -23,23 +23,11 @@ def load(path):
return plugins 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): def load_module(module_name, path):
from importlib.machinery import SourceFileLoader spec = importlib.util.spec_from_file_location(module_name, path)
module = importlib.util.module_from_spec(spec)
def load_module(module_name, path): spec.loader.exec_module(module)
return SourceFileLoader(module_name, path).load_module() return module
else:
import imp
def load_module(module_name, path):
return imp.load_source(module_name, path)

View File

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

View File

@ -1,5 +1,4 @@
import re import re
from codecs import open # For a consistent encoding
from os import path from os import path
from setuptools import find_packages, setup from setuptools import find_packages, setup
@ -38,10 +37,7 @@ setup(
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",

View File

@ -1,9 +1,11 @@
import builtins
import ctypes import ctypes
import json import json
import os import os
import shutil import shutil
import sys import sys
import tempfile import tempfile
import unittest.mock as mock
from shutil import rmtree from shutil import rmtree
import pytest import pytest
@ -11,15 +13,6 @@ import yaml
import dotbot.cli 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): def get_long_path(path):
"""Get the long path for a given path.""" """Get the long path for a given path."""
@ -35,8 +28,7 @@ def get_long_path(path):
return buffer.value return buffer.value
# Python 2.7 compatibility: # On Linux, tempfile.TemporaryFile() requires unlink access.
# On Linux, Python 2.7's tempfile.TemporaryFile() requires unlink access.
# This list is updated by a tempfile._mkstemp_inner() wrapper, # This list is updated by a tempfile._mkstemp_inner() wrapper,
# and its contents are checked by wrapped functions. # and its contents are checked by wrapped functions.
allowed_tempfile_internal_unlink_calls = [] allowed_tempfile_internal_unlink_calls = []
@ -49,7 +41,6 @@ def wrap_function(function, function_path, arg_index, kwarg_key, root):
else: else:
value = args[arg_index] value = args[arg_index]
# Python 2.7 compatibility:
# Allow tempfile.TemporaryFile's internal unlink calls to work. # Allow tempfile.TemporaryFile's internal unlink calls to work.
if value in allowed_tempfile_internal_unlink_calls: if value in allowed_tempfile_internal_unlink_calls:
return function(*args, **kwargs) return function(*args, **kwargs)
@ -68,11 +59,7 @@ def wrap_function(function, function_path, arg_index, kwarg_key, root):
def wrap_open(root): def wrap_open(root):
try: wrapped = getattr(builtins, "open")
wrapped = getattr(builtins, "open")
except AttributeError:
# Python 2.7 compatibility
wrapped = getattr(__builtin__, "open")
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if "file" in kwargs: if "file" in kwargs:
@ -207,11 +194,7 @@ def root(standardize_tmp):
patches.append(mock.patch(function_path, wrapped)) patches.append(mock.patch(function_path, wrapped))
# open() must be separately wrapped. # open() must be separately wrapped.
if builtins is not None: function_path = "builtins.open"
function_path = "builtins.open"
else:
# Python 2.7 compatibility
function_path = "__builtin__.open"
wrapped = wrap_open(current_root) wrapped = wrap_open(current_root)
patches.append(mock.patch(function_path, wrapped)) patches.append(mock.patch(function_path, wrapped))
@ -256,7 +239,7 @@ def home(monkeypatch, root):
yield home yield home
class Dotfiles(object): class Dotfiles:
"""Create and manage a dotfiles directory for a test.""" """Create and manage a dotfiles directory for a test."""
def __init__(self, root): def __init__(self, root):

View File

@ -1,25 +1,15 @@
import os import os
import shutil
import subprocess import subprocess
import pytest 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( @pytest.mark.skipif(
"sys.platform[:5] == 'win32'", "sys.platform[:5] == 'win32'",
reason="The hybrid sh/Python dotbot script doesn't run on Windows platforms", 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): def test_find_python_executable(python_name, home, dotfiles):
"""Verify that the sh/Python hybrid dotbot executable can find Python.""" """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. # Create a link to sh.
tmp_bin = os.path.join(home, "tmp_bin") tmp_bin = os.path.join(home, "tmp_bin")
os.makedirs(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")) os.symlink(sh_path, os.path.join(tmp_bin, "sh"))
if python_name: if python_name:

View File

@ -537,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)) 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): def test_link_glob_recursive(home, dotfiles, run_dotbot):
"""Verify recursive link globbing and exclusions.""" """Verify recursive link globbing and exclusions."""
@ -773,19 +769,11 @@ def test_link_no_canonicalize(key, home, dotfiles, run_dotbot):
dotfiles.write("f", "apple") dotfiles.write("f", "apple")
dotfiles.write_config([{"defaults": {"link": {key: False}}}, {"link": {"~/.f": {"path": "f"}}}]) dotfiles.write_config([{"defaults": {"link": {key: False}}}, {"link": {"~/.f": {"path": "f"}}}])
try: os.symlink(
os.symlink( dotfiles.directory,
dotfiles.directory, os.path.join(home, "dotfiles-symlink"),
os.path.join(home, "dotfiles-symlink"), target_is_directory=True,
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( run_dotbot(
"-c", "-c",
os.path.join(home, "dotfiles-symlink", os.path.basename(dotfiles.config_filename)), os.path.join(home, "dotfiles-symlink", os.path.basename(dotfiles.config_filename)),

View File

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

View File

@ -4,8 +4,8 @@
envlist = envlist =
coverage_erase coverage_erase
py{38, 39, 310}-all_platforms py{38, 39, 310}-all_platforms
py{27, 35, 36, 37}-most_platforms py{36, 37}-most_platforms
pypy{2, 3}-most_platforms pypy3-most_platforms
coverage_report coverage_report
skip_missing_interpreters = true skip_missing_interpreters = true
@ -20,7 +20,6 @@ deps =
pytest pytest
pytest-randomly pytest-randomly
pyyaml pyyaml
mock; python_version == "2.7"
commands = commands =
coverage run -m pytest tests/ coverage run -m pytest tests/
@ -66,10 +65,7 @@ python =
3.10: py310-all_platforms 3.10: py310-all_platforms
; Run on most platforms (Linux and Mac) ; Run on most platforms (Linux and Mac)
pypy-2.7: pypy2-most_platforms
pypy-3.9: pypy3-most_platforms pypy-3.9: pypy3-most_platforms
2.7: py27-most_platforms
3.5: py35-most_platforms
3.6: py36-most_platforms 3.6: py36-most_platforms
3.7: py37-most_platforms 3.7: py37-most_platforms