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