diff --git a/docs/changelog/2350.bugfix.rst b/docs/changelog/2350.bugfix.rst new file mode 100644 index 000000000..87a66a29f --- /dev/null +++ b/docs/changelog/2350.bugfix.rst @@ -0,0 +1 @@ +Fix selected scheme on debian derivatives for python 3.10 when ``python3-distutils`` is not installed or the ``venv`` scheme is not avaiable - by :user:`asottile`. diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index 2a648e049..7c680eeaa 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -77,10 +77,23 @@ def abs_path(v): self.file_system_encoding = u(sys.getfilesystemencoding()) self.stdout_encoding = u(getattr(sys.stdout, "encoding", None)) - if "venv" in sysconfig.get_scheme_names(): + scheme_names = sysconfig.get_scheme_names() + + if "venv" in scheme_names: self.sysconfig_scheme = "venv" self.sysconfig_paths = { - u(i): u(sysconfig.get_path(i, expand=False, scheme="venv")) for i in sysconfig.get_path_names() + u(i): u(sysconfig.get_path(i, expand=False, scheme=self.sysconfig_scheme)) + for i in sysconfig.get_path_names() + } + # we cannot use distutils at all if "venv" exists, distutils don't know it + self.distutils_install = {} + # debian / ubuntu python 3.10 without `python3-distutils` will report + # mangled `local/bin` / etc. names for the default prefix + # intentionally select `posix_prefix` which is the unaltered posix-like paths + elif sys.version_info[:2] == (3, 10) and "deb_system" in scheme_names: + self.sysconfig_scheme = "posix_prefix" + self.sysconfig_paths = { + i: sysconfig.get_path(i, expand=False, scheme=self.sysconfig_scheme) for i in sysconfig.get_path_names() } # we cannot use distutils at all if "venv" exists, distutils don't know it self.distutils_install = {} diff --git a/tests/unit/discovery/py_info/test_py_info.py b/tests/unit/discovery/py_info/test_py_info.py index 9d3d762e5..f621224ee 100644 --- a/tests/unit/discovery/py_info/test_py_info.py +++ b/tests/unit/discovery/py_info/test_py_info.py @@ -1,4 +1,5 @@ import copy +import functools import itertools import json import logging @@ -375,3 +376,73 @@ def test_custom_venv_install_scheme_is_prefered(mocker): pyver = f"{pyinfo.version_info.major}.{pyinfo.version_info.minor}" assert pyinfo.install_path("scripts") == "bin" assert pyinfo.install_path("purelib").replace(os.sep, "/") == f"lib/python{pyver}/site-packages" + + +@pytest.mark.skipif(sys.version_info[:2] != (3, 10), reason="3.10 specific") +def test_uses_posix_prefix_on_debian_3_10_without_venv(mocker): + # this is taken from ubuntu 22.04 /usr/lib/python3.10/sysconfig.py + sysconfig_install_schemes = { + "posix_prefix": { + "stdlib": "{installed_base}/{platlibdir}/python{py_version_short}", + "platstdlib": "{platbase}/{platlibdir}/python{py_version_short}", + "purelib": "{base}/lib/python{py_version_short}/site-packages", + "platlib": "{platbase}/{platlibdir}/python{py_version_short}/site-packages", + "include": "{installed_base}/include/python{py_version_short}{abiflags}", + "platinclude": "{installed_platbase}/include/python{py_version_short}{abiflags}", + "scripts": "{base}/bin", + "data": "{base}", + }, + "posix_home": { + "stdlib": "{installed_base}/lib/python", + "platstdlib": "{base}/lib/python", + "purelib": "{base}/lib/python", + "platlib": "{base}/lib/python", + "include": "{installed_base}/include/python", + "platinclude": "{installed_base}/include/python", + "scripts": "{base}/bin", + "data": "{base}", + }, + "nt": { + "stdlib": "{installed_base}/Lib", + "platstdlib": "{base}/Lib", + "purelib": "{base}/Lib/site-packages", + "platlib": "{base}/Lib/site-packages", + "include": "{installed_base}/Include", + "platinclude": "{installed_base}/Include", + "scripts": "{base}/Scripts", + "data": "{base}", + }, + "deb_system": { + "stdlib": "{installed_base}/{platlibdir}/python{py_version_short}", + "platstdlib": "{platbase}/{platlibdir}/python{py_version_short}", + "purelib": "{base}/lib/python3/dist-packages", + "platlib": "{platbase}/{platlibdir}/python3/dist-packages", + "include": "{installed_base}/include/python{py_version_short}{abiflags}", + "platinclude": "{installed_platbase}/include/python{py_version_short}{abiflags}", + "scripts": "{base}/bin", + "data": "{base}", + }, + "posix_local": { + "stdlib": "{installed_base}/{platlibdir}/python{py_version_short}", + "platstdlib": "{platbase}/{platlibdir}/python{py_version_short}", + "purelib": "{base}/local/lib/python{py_version_short}/dist-packages", + "platlib": "{platbase}/local/lib/python{py_version_short}/dist-packages", + "include": "{installed_base}/local/include/python{py_version_short}{abiflags}", + "platinclude": "{installed_platbase}/local/include/python{py_version_short}{abiflags}", + "scripts": "{base}/local/bin", + "data": "{base}", + }, + } + # reset the default in case we're on a system which doesn't have this problem + sysconfig_get_path = functools.partial(sysconfig.get_path, scheme="posix_local") + + # make it look like python3-distutils is not available + mocker.patch.dict(sys.modules, {"distutils.command": None}) + mocker.patch("sysconfig._INSTALL_SCHEMES", sysconfig_install_schemes) + mocker.patch("sysconfig.get_path", sysconfig_get_path) + mocker.patch("sysconfig.get_default_scheme", return_value="posix_local") + + pyinfo = PythonInfo() + pyver = f"{pyinfo.version_info.major}.{pyinfo.version_info.minor}" + assert pyinfo.install_path("scripts") == "bin" + assert pyinfo.install_path("purelib").replace(os.sep, "/") == f"lib/python{pyver}/site-packages"