From 42e0698087f061d1d7db6fcb9469302bda5d44ca Mon Sep 17 00:00:00 2001 From: chrysle Date: Fri, 28 Apr 2023 01:36:03 +0200 Subject: [PATCH] 3.12 support and no setuptools/wheel on 3.12+ (#2558) Cherry-picked from fd93dd79be89b21e6e9d43ca2dd1b02b811f6d6f --- docs/changelog/2487.feature.rst | 6 +++++ docs/changelog/2558.feature.rst | 1 + docs/render_cli.py | 10 +------ docs/user_guide.rst | 5 ++-- src/virtualenv/activation/python/__init__.py | 3 ++- src/virtualenv/seed/embed/base_embed.py | 11 +++++--- src/virtualenv/util/path/_sync.py | 4 ++- tests/unit/config/test___main__.py | 2 +- tests/unit/create/test_creator.py | 26 ++++++++++++++++--- tests/unit/discovery/py_info/test_py_info.py | 2 +- tests/unit/discovery/windows/conftest.py | 2 +- tests/unit/seed/embed/test_base_embed.py | 12 +++++++++ .../embed/test_bootstrap_link_via_app_data.py | 4 +-- .../unit/seed/wheels/test_periodic_update.py | 17 ++++++++++-- 14 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 docs/changelog/2487.feature.rst create mode 100644 docs/changelog/2558.feature.rst diff --git a/docs/changelog/2487.feature.rst b/docs/changelog/2487.feature.rst new file mode 100644 index 0000000..12cc896 --- /dev/null +++ b/docs/changelog/2487.feature.rst @@ -0,0 +1,6 @@ +Do not install ``wheel`` and ``setuptools`` seed packages for Python 3.12+. To restore the old behaviour use: + +- for ``wheel`` use ``VIRTUALENV_WHEEL=bundle`` environment variable or ``--wheel=bundle`` CLI flag, +- for ``setuptools`` use ``VIRTUALENV_SETUPTOOLS=bundle`` environment variable or ``--setuptools=bundle`` CLI flag. + +By :user:`chrysle`. diff --git a/docs/changelog/2558.feature.rst b/docs/changelog/2558.feature.rst new file mode 100644 index 0000000..58b627a --- /dev/null +++ b/docs/changelog/2558.feature.rst @@ -0,0 +1 @@ +3.12 support - by :user:`gaborbernat`. diff --git a/src/virtualenv/activation/python/__init__.py b/src/virtualenv/activation/python/__init__.py index eb83504..a49444b 100644 --- a/src/virtualenv/activation/python/__init__.py +++ b/src/virtualenv/activation/python/__init__.py @@ -12,10 +12,11 @@ class PythonActivator(ViaTemplateActivator): def replacements(self, creator, dest_folder): replacements = super().replacements(creator, dest_folder) lib_folders = OrderedDict((os.path.relpath(str(i), str(dest_folder)), None) for i in creator.libs) + lib_folders = os.pathsep.join(lib_folders.keys()).replace("\\", "\\\\") # escape Windows path characters win_py2 = creator.interpreter.platform == "win32" and creator.interpreter.version_info.major == 2 replacements.update( { - "__LIB_FOLDERS__": os.pathsep.join(lib_folders.keys()), + "__LIB_FOLDERS__": lib_folders, "__DECODE_PATH__": ("yes" if win_py2 else ""), }, ) diff --git a/src/virtualenv/seed/embed/base_embed.py b/src/virtualenv/seed/embed/base_embed.py index f29110b..6782d6f 100644 --- a/src/virtualenv/seed/embed/base_embed.py +++ b/src/virtualenv/seed/embed/base_embed.py @@ -39,7 +39,7 @@ class BaseEmbed(Seeder, metaclass=ABCMeta): return { distribution: getattr(self, f"{distribution}_version") for distribution in self.distributions() - if getattr(self, f"no_{distribution}") is False + if getattr(self, f"no_{distribution}") is False and getattr(self, f"{distribution}_version") != "none" } @classmethod @@ -69,11 +69,13 @@ class BaseEmbed(Seeder, metaclass=ABCMeta): default=[], ) for distribution, default in cls.distributions().items(): + if interpreter.version_info[:2] >= (3, 12) and distribution in {"wheel", "setuptools"}: + default = "none" parser.add_argument( f"--{distribution}", dest=distribution, metavar="version", - help=f"version of {distribution} to install as seed: embed, bundle or exact version", + help=f"version of {distribution} to install as seed: embed, bundle, none or exact version", default=default, ) for distribution in cls.distributions(): @@ -101,7 +103,10 @@ class BaseEmbed(Seeder, metaclass=ABCMeta): for distribution in self.distributions(): if getattr(self, f"no_{distribution}"): continue - ver = f"={getattr(self, f'{distribution}_version', None) or 'latest'}" + version = getattr(self, f"{distribution}_version", None) + if version == "none": + continue + ver = f"={version or 'latest'}" result += f" {distribution}{ver}," return result[:-1] + ")" diff --git a/src/virtualenv/util/path/_sync.py b/src/virtualenv/util/path/_sync.py index 604379d..b0af1eb 100644 --- a/src/virtualenv/util/path/_sync.py +++ b/src/virtualenv/util/path/_sync.py @@ -1,6 +1,7 @@ import logging import os import shutil +import sys from stat import S_IWUSR @@ -56,7 +57,8 @@ def safe_delete(dest): else: raise - shutil.rmtree(str(dest), ignore_errors=True, onerror=onerror) + kwargs = {"onexc" if sys.version_info >= (3, 12) else "onerror": onerror} + shutil.rmtree(str(dest), ignore_errors=True, **kwargs) class _Debug: diff --git a/tests/unit/config/test___main__.py b/tests/unit/config/test___main__.py index 62228c9..d22ef7e 100644 --- a/tests/unit/config/test___main__.py +++ b/tests/unit/config/test___main__.py @@ -58,7 +58,7 @@ def test_fail_with_traceback(raise_on_session_done, tmp_path, capsys): @pytest.mark.usefixtures("session_app_data") def test_session_report_full(tmp_path, capsys): - run_with_catch([str(tmp_path)]) + run_with_catch([str(tmp_path), "--setuptools", "bundle", "--wheel", "bundle"]) out, err = capsys.readouterr() assert err == "" lines = out.splitlines() diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py index 0ec6d62..8b9d688 100644 --- a/tests/unit/create/test_creator.py +++ b/tests/unit/create/test_creator.py @@ -412,7 +412,19 @@ def test_create_long_path(tmp_path): @pytest.mark.parametrize("creator", sorted(set(PythonInfo.current_system().creators().key_to_class) - {"builtin"})) @pytest.mark.usefixtures("session_app_data") def test_create_distutils_cfg(creator, tmp_path, monkeypatch): - result = cli_run([str(tmp_path / "venv"), "--activators", "", "--creator", creator]) + result = cli_run( + [ + str(tmp_path / "venv"), + "--activators", + "", + "--creator", + creator, + "--setuptools", + "bundle", + "--wheel", + "bundle", + ], + ) app = Path(__file__).parent / "console_app" dest = tmp_path / "console_app" @@ -465,7 +477,9 @@ def list_files(path): def test_zip_importer_can_import_setuptools(tmp_path): """We're patching the loaders so might fail on r/o loaders, such as zipimporter on CPython<3.8""" - result = cli_run([str(tmp_path / "venv"), "--activators", "", "--no-pip", "--no-wheel", "--copies"]) + result = cli_run( + [str(tmp_path / "venv"), "--activators", "", "--no-pip", "--no-wheel", "--copies", "--setuptools", "bundle"], + ) zip_path = tmp_path / "site-packages.zip" with zipfile.ZipFile(str(zip_path), "w", zipfile.ZIP_DEFLATED) as zip_handler: lib = str(result.creator.purelib) @@ -499,6 +513,7 @@ def test_no_preimport_threading(tmp_path): out = subprocess.check_output( [str(session.creator.exe), "-c", r"import sys; print('\n'.join(sorted(sys.modules)))"], text=True, + encoding="utf-8", ) imported = set(out.splitlines()) assert "threading" not in imported @@ -515,6 +530,7 @@ def test_pth_in_site_vs_python_path(tmp_path): out = subprocess.check_output( [str(session.creator.exe), "-c", r"import sys; print(sys.testpth)"], text=True, + encoding="utf-8", ) assert out == "ok\n" # same with $PYTHONPATH pointing to site_packages @@ -527,6 +543,7 @@ def test_pth_in_site_vs_python_path(tmp_path): [str(session.creator.exe), "-c", r"import sys; print(sys.testpth)"], text=True, env=env, + encoding="utf-8", ) assert out == "ok\n" @@ -540,6 +557,7 @@ def test_getsitepackages_system_site(tmp_path): out = subprocess.check_output( [str(session.creator.exe), "-c", r"import site; print(site.getsitepackages())"], text=True, + encoding="utf-8", ) site_packages = ast.literal_eval(out) @@ -554,6 +572,7 @@ def test_getsitepackages_system_site(tmp_path): out = subprocess.check_output( [str(session.creator.exe), "-c", r"import site; print(site.getsitepackages())"], text=True, + encoding="utf-8", ) site_packages = [str(Path(i).resolve()) for i in ast.literal_eval(out)] @@ -579,6 +598,7 @@ def test_get_site_packages(tmp_path): out = subprocess.check_output( [str(session.creator.exe), "-c", r"import site; print(site.getsitepackages())"], text=True, + encoding="utf-8", ) site_packages = ast.literal_eval(out) @@ -617,7 +637,7 @@ def test_python_path(monkeypatch, tmp_path, python_path_on): if flag: cmd.append(flag) cmd.extend(["-c", "import json; import sys; print(json.dumps(sys.path))"]) - return [i if case_sensitive else i.lower() for i in json.loads(subprocess.check_output(cmd))] + return [i if case_sensitive else i.lower() for i in json.loads(subprocess.check_output(cmd, encoding="utf-8"))] monkeypatch.delenv("PYTHONPATH", raising=False) base = _get_sys_path() diff --git a/tests/unit/discovery/py_info/test_py_info.py b/tests/unit/discovery/py_info/test_py_info.py index 24b129c..f3fdb7e 100644 --- a/tests/unit/discovery/py_info/test_py_info.py +++ b/tests/unit/discovery/py_info/test_py_info.py @@ -289,7 +289,7 @@ def test_discover_exe_on_path_non_spec_name_not_match(mocker): assert CURRENT.satisfies(spec, impl_must_match=True) is False -@pytest.mark.skipif(IS_PYPY, reason="setuptools distutil1s patching does not work") +@pytest.mark.skipif(IS_PYPY, reason="setuptools distutils patching does not work") def test_py_info_setuptools(): from setuptools.dist import Distribution diff --git a/tests/unit/discovery/windows/conftest.py b/tests/unit/discovery/windows/conftest.py index 58da626..94f14da 100644 --- a/tests/unit/discovery/windows/conftest.py +++ b/tests/unit/discovery/windows/conftest.py @@ -9,7 +9,7 @@ def _mock_registry(mocker): from virtualenv.discovery.windows.pep514 import winreg loc, glob = {}, {} - mock_value_str = (Path(__file__).parent / "winreg-mock-values.py").read_text() + mock_value_str = (Path(__file__).parent / "winreg-mock-values.py").read_text(encoding="utf-8") exec(mock_value_str, glob, loc) enum_collect = loc["enum_collect"] value_collect = loc["value_collect"] diff --git a/tests/unit/seed/embed/test_base_embed.py b/tests/unit/seed/embed/test_base_embed.py index 3344c74..ef2f829 100644 --- a/tests/unit/seed/embed/test_base_embed.py +++ b/tests/unit/seed/embed/test_base_embed.py @@ -1,3 +1,5 @@ +import sys + import pytest from virtualenv.run import session_via_cli @@ -10,3 +12,13 @@ from virtualenv.run import session_via_cli def test_download_cli_flag(args, download, tmp_path): session = session_via_cli(args + [str(tmp_path)]) assert session.seeder.download is download + + +def test_embed_wheel_versions(tmp_path): + session = session_via_cli([str(tmp_path)]) + expected = ( + {"pip": "bundle"} + if sys.version_info[:2] >= (3, 12) + else {"pip": "bundle", "setuptools": "bundle", "wheel": "bundle"} + ) + assert session.seeder.distribution_to_versions() == expected diff --git a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py b/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py index 2c8c3e8..015686d 100644 --- a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py +++ b/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py @@ -203,7 +203,7 @@ def test_populated_read_only_cache_and_copied_app_data(tmp_path, current_fastest @pytest.mark.parametrize("pkg", ["pip", "setuptools", "wheel"]) @pytest.mark.usefixtures("session_app_data", "current_fastest", "coverage_env") def test_base_bootstrap_link_via_app_data_no(tmp_path, pkg): - create_cmd = [str(tmp_path), "--seeder", "app-data", f"--no-{pkg}"] + create_cmd = [str(tmp_path), "--seeder", "app-data", f"--no-{pkg}", "--wheel", "bundle", "--setuptools", "bundle"] result = cli_run(create_cmd) assert not (result.creator.purelib / pkg).exists() for key in {"pip", "setuptools", "wheel"} - {pkg}: @@ -231,7 +231,7 @@ def _run_parallel_threads(tmp_path): def _run(name): try: - cli_run(["--seeder", "app-data", str(tmp_path / name), "--no-pip", "--no-setuptools"]) + cli_run(["--seeder", "app-data", str(tmp_path / name), "--no-pip", "--no-setuptools", "--wheel", "bundle"]) except Exception as exception: as_str = str(exception) exceptions.append(as_str) diff --git a/tests/unit/seed/wheels/test_periodic_update.py b/tests/unit/seed/wheels/test_periodic_update.py index e7794f5..c36a983 100644 --- a/tests/unit/seed/wheels/test_periodic_update.py +++ b/tests/unit/seed/wheels/test_periodic_update.py @@ -66,7 +66,7 @@ def test_manual_upgrade(session_app_data, caplog, mocker, for_py_version): @pytest.mark.usefixtures("session_app_data") def test_pick_periodic_update(tmp_path, mocker, for_py_version): - embed, current = get_embed_wheel("setuptools", "3.5"), get_embed_wheel("setuptools", for_py_version) + embed, current = get_embed_wheel("setuptools", "3.6"), get_embed_wheel("setuptools", for_py_version) mocker.patch("virtualenv.seed.wheels.bundle.load_embed_wheel", return_value=embed) completed = datetime.now() - timedelta(days=29) u_log = UpdateLog( @@ -77,7 +77,20 @@ def test_pick_periodic_update(tmp_path, mocker, for_py_version): ) read_dict = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - result = cli_run([str(tmp_path), "--activators", "", "--no-periodic-update", "--no-wheel", "--no-pip"]) + result = cli_run( + [ + str(tmp_path), + "--activators", + "", + "--no-periodic-update", + "--no-wheel", + "--no-pip", + "--setuptools", + "bundle", + "--wheel", + "bundle", + ], + ) assert read_dict.call_count == 1 installed = [i.name for i in result.creator.purelib.iterdir() if i.suffix == ".dist-info"] -- 2.40.0