diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e45a4e4eb..0bd7c98ba 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -24,6 +24,7 @@ jobs: - windows-latest - macos-latest py: + - 3.10.0-beta.4 - 3.9 - 3.8 - 3.7 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18cda11a7..4dae7768d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: rev: v1.17.0 hooks: - id: setup-cfg-fmt - args: [--min-py3-version, "3.4"] + args: [--min-py3-version, "3.5", "--max-py-version", "3.10"] - repo: https://github.com/PyCQA/flake8 rev: "3.9.2" hooks: diff --git a/docs/changelog/1910.feature.rst b/docs/changelog/1910.feature.rst new file mode 100644 index 000000000..fd715ea13 --- /dev/null +++ b/docs/changelog/1910.feature.rst @@ -0,0 +1,2 @@ +Support Python interpreters without ``distutils`` (fallback to ``syconfig`` in these cases) - by :user:`gaborbernat`. + diff --git a/setup.cfg b/setup.cfg index 94734a5c1..9a66f5b71 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Libraries diff --git a/src/virtualenv/create/describe.py b/src/virtualenv/create/describe.py index 1e59aaeae..6f05ff1e2 100644 --- a/src/virtualenv/create/describe.py +++ b/src/virtualenv/create/describe.py @@ -30,15 +30,15 @@ def bin_dir(self): @property def script_dir(self): - return self.dest / Path(self.interpreter.distutils_install["scripts"]) + return self.dest / self.interpreter.install_path("scripts") @property def purelib(self): - return self.dest / self.interpreter.distutils_install["purelib"] + return self.dest / self.interpreter.install_path("purelib") @property def platlib(self): - return self.dest / self.interpreter.distutils_install["platlib"] + return self.dest / self.interpreter.install_path("platlib") @property def libs(self): diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython2.py b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython2.py index 555b0c50f..dc822bcb9 100644 --- a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython2.py +++ b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython2.py @@ -36,7 +36,7 @@ def host_include_marker(cls, interpreter): @property def include(self): # the pattern include the distribution name too at the end, remove that via the parent call - return (self.dest / self.interpreter.distutils_install["headers"]).parent + return (self.dest / self.interpreter.install_path("headers")).parent @classmethod def modules(cls): diff --git a/src/virtualenv/create/via_global_ref/builtin/pypy/pypy2.py b/src/virtualenv/create/via_global_ref/builtin/pypy/pypy2.py index fb5901505..924944247 100644 --- a/src/virtualenv/create/via_global_ref/builtin/pypy/pypy2.py +++ b/src/virtualenv/create/via_global_ref/builtin/pypy/pypy2.py @@ -41,7 +41,7 @@ def host_include_marker(cls, interpreter): @property def include(self): - return self.dest / self.interpreter.distutils_install["headers"] + return self.dest / self.interpreter.install_path("headers") @classmethod def modules(cls): diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index 30e13215e..9b41d13fe 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -12,9 +12,8 @@ import re import sys import sysconfig +import warnings from collections import OrderedDict, namedtuple -from distutils import dist -from distutils.command.install import SCHEME_KEYS from string import digits VersionInfo = namedtuple("VersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]) @@ -118,10 +117,28 @@ def _fast_get_system_executable(self): # note we must choose the original and not the pure executable as shim scripts might throw us off return self.original_executable + def install_path(self, key): + result = self.distutils_install.get(key) + if result is None: # use sysconfig if distutils is unavailable + # set prefixes to empty => result is relative from cwd + prefixes = self.prefix, self.exec_prefix, self.base_prefix, self.base_exec_prefix + config_var = {k: "" if v in prefixes else v for k, v in self.sysconfig_vars} + result = self.sysconfig_path(key, config_var=config_var).lstrip(os.sep) + return result + @staticmethod def _distutils_install(): - # follow https://github.com/pypa/pip/blob/main/src/pip/_internal/locations.py#L95 + # use distutils primarily because that's what pip does + # https://github.com/pypa/pip/blob/main/src/pip/_internal/locations.py#L95 # note here we don't import Distribution directly to allow setuptools to patch it + with warnings.catch_warnings(): # disable warning for PEP-632 + warnings.simplefilter("ignore") + try: + from distutils import dist + from distutils.command.install import SCHEME_KEYS + except ImportError: # if removed or not installed ignore + return {} + d = dist.Distribution({"script_args": "--no-user-cfg"}) # conf files not parsed so they do not hijack paths if hasattr(sys, "_framework"): sys._framework = None # disable macOS static paths for framework @@ -177,7 +194,7 @@ def system_include(self): ) if not os.path.exists(path): # some broken packaging don't respect the sysconfig, fallback to distutils path # the pattern include the distribution name too at the end, remove that via the parent call - fallback = os.path.join(self.prefix, os.path.dirname(self.distutils_install["headers"])) + fallback = os.path.join(self.prefix, os.path.dirname(self.install_path("headers"))) if os.path.exists(fallback): path = fallback return path diff --git a/tests/unit/activation/test_powershell.py b/tests/unit/activation/test_powershell.py index b2d4cdae3..2d6d9ebb6 100644 --- a/tests/unit/activation/test_powershell.py +++ b/tests/unit/activation/test_powershell.py @@ -9,7 +9,9 @@ @pytest.mark.slow -def test_powershell(activation_tester_class, activation_tester): +def test_powershell(activation_tester_class, activation_tester, monkeypatch): + monkeypatch.setenv("TERM", "xterm") + class PowerShell(activation_tester_class): def __init__(self, session): cmd = "powershell.exe" if sys.platform == "win32" else "pwsh" diff --git a/tests/unit/discovery/py_info/test_py_info_exe_based_of.py b/tests/unit/discovery/py_info/test_py_info_exe_based_of.py index 8e7fcff29..f232e7aca 100644 --- a/tests/unit/discovery/py_info/test_py_info_exe_based_of.py +++ b/tests/unit/discovery/py_info/test_py_info_exe_based_of.py @@ -17,7 +17,7 @@ def test_discover_empty_folder(tmp_path, monkeypatch, session_app_data): CURRENT.discover_exe(session_app_data, prefix=str(tmp_path)) -BASE = (CURRENT.distutils_install["scripts"], ".") +BASE = (CURRENT.install_path("scripts"), ".") @pytest.mark.skipif(not fs_supports_symlink(), reason="symlink is not supported") diff --git a/tests/unit/discovery/test_discovery.py b/tests/unit/discovery/test_discovery.py index 7d33b22a8..c04caea2b 100644 --- a/tests/unit/discovery/test_discovery.py +++ b/tests/unit/discovery/test_discovery.py @@ -27,7 +27,7 @@ def test_discovery_via_path(monkeypatch, case, tmp_path, caplog, session_app_dat elif case == "upper": name = name.upper() exe_name = "{}{}{}".format(name, current.version_info.major, ".exe" if sys.platform == "win32" else "") - target = tmp_path / current.distutils_install["scripts"] + target = tmp_path / current.install_path("scripts") target.mkdir(parents=True) executable = target / exe_name os.symlink(sys.executable, ensure_text(str(executable)))