Skip to content

Commit 54159a5

Browse files
chryslegaborbernat
authored andcommitted
Don't install setuptools and wheel on Python 3.12+
Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
1 parent 89f80b8 commit 54159a5

File tree

19 files changed

+125
-71
lines changed

19 files changed

+125
-71
lines changed

.github/workflows/check.yml

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
fail-fast: false
2020
matrix:
2121
py:
22+
- "3.12.0-alpha.7"
2223
- "3.11"
2324
- "3.10"
2425
- "3.9"

docs/changelog/2487.feature.rst

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Do not install ``wheel`` and ``setuptools`` seed packages for Python 3.12+. To restore the old behaviour use:
2+
3+
- for ``wheel`` use ``VIRTUALENV_WHEEL=bundle`` environment variable or ``--wheel=bundle`` CLI flag,
4+
- for ``setuptools`` use ``VIRTUALENV_SETUPTOOLS=bundle`` environment variable or ``--setuptools=bundle`` CLI flag.
5+
6+
By :user:`chrysle`.

docs/changelog/2558.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12 support - by :user:`gaborbernat`.

docs/render_cli.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,7 @@ def _get_help_text(row):
180180
content = row.help[: row.help.index("(") - 1]
181181
else:
182182
content = row.help
183-
if name in ("--setuptools", "--pip", "--wheel"):
184-
text = row.help
185-
at = text.index(" bundle ")
186-
help_body = n.paragraph("")
187-
help_body += n.Text(text[: at + 1])
188-
help_body += n.literal(text="bundle")
189-
help_body += n.Text(text[at + 7 :])
190-
else:
191-
help_body = n.paragraph("", "", n.Text(content))
183+
help_body = n.paragraph("", "", n.Text(content))
192184
if row.choices is not None:
193185
help_body += n.Text("; choice of: ")
194186
first = True

docs/user_guide.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ at the moment has two types of virtual environments:
118118
Seeders
119119
-------
120120
These will install for you some seed packages (one or more of: :pypi:`pip`, :pypi:`setuptools`, :pypi:`wheel`) that
121-
enables you to install additional python packages into the created virtual environment (by invoking pip). There are two
122-
main seed mechanism available:
121+
enables you to install additional python packages into the created virtual environment (by invoking pip). Installing
122+
:pypi:`setuptools` and :pypi:`wheel` is disabled by default on Python 3.12+ environments. There are two
123+
main seed mechanisms available:
123124

124125
- ``pip`` - this method uses the bundled pip with virtualenv to install the seed packages (note, a new child process
125126
needs to be created to do this, which can be expensive especially on Windows).

pyproject.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@ optional-dependencies.test = [
5757
"packaging>=23.1",
5858
"pytest>=7.3.1",
5959
"pytest-env>=0.8.1",
60-
"pytest-freezegun>=0.4.2",
6160
"pytest-mock>=3.10",
6261
"pytest-randomly>=3.12",
6362
"pytest-timeout>=2.1",
63+
"setuptools>=67.7.1",
64+
"time-machine>=2.9",
6465
]
6566
urls.Documentation = "https://virtualenv.pypa.io"
6667
urls.Homepage = "https://github.com/pypa/virtualenv"
@@ -116,7 +117,7 @@ ignore = [
116117
[tool.pytest.ini_options]
117118
markers = ["slow"]
118119
timeout = 600
119-
addopts = "--tb=auto -ra --showlocals --no-success-flaky-report"
120+
addopts = "--showlocals --no-success-flaky-report"
120121
env = ["PYTHONIOENCODING=utf-8"]
121122

122123
[tool.coverage]

src/virtualenv/activation/python/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ def templates(self):
1313
def replacements(self, creator, dest_folder):
1414
replacements = super().replacements(creator, dest_folder)
1515
lib_folders = OrderedDict((os.path.relpath(str(i), str(dest_folder)), None) for i in creator.libs)
16+
lib_folders = os.pathsep.join(lib_folders.keys()).replace("\\", "\\\\") # escape Windows path characters
1617
replacements.update(
1718
{
18-
"__LIB_FOLDERS__": os.pathsep.join(lib_folders.keys()),
19+
"__LIB_FOLDERS__": lib_folders,
1920
"__DECODE_PATH__": "",
2021
},
2122
)

src/virtualenv/seed/embed/base_embed.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@ def __init__(self, options):
3030
self.enabled = False
3131

3232
@classmethod
33-
def distributions(cls):
33+
def distributions(cls) -> dict[str, Version]:
3434
return {
3535
"pip": Version.bundle,
3636
"setuptools": Version.bundle,
3737
"wheel": Version.bundle,
3838
}
3939

40-
def distribution_to_versions(self):
40+
def distribution_to_versions(self) -> dict[str, str]:
4141
return {
4242
distribution: getattr(self, f"{distribution}_version")
4343
for distribution in self.distributions()
44-
if getattr(self, f"no_{distribution}") is False
44+
if getattr(self, f"no_{distribution}") is False and getattr(self, f"{distribution}_version") != "none"
4545
}
4646

4747
@classmethod
@@ -71,11 +71,13 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: U100
7171
default=[],
7272
)
7373
for distribution, default in cls.distributions().items():
74+
if interpreter.version_info[:2] >= (3, 12) and distribution in {"wheel", "setuptools"}:
75+
default = "none"
7476
parser.add_argument(
7577
f"--{distribution}",
7678
dest=distribution,
7779
metavar="version",
78-
help=f"version of {distribution} to install as seed: embed, bundle or exact version",
80+
help=f"version of {distribution} to install as seed: embed, bundle, none or exact version",
7981
default=default,
8082
)
8183
for distribution in cls.distributions():
@@ -94,7 +96,7 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: U100
9496
default=not PERIODIC_UPDATE_ON_BY_DEFAULT,
9597
)
9698

97-
def __repr__(self):
99+
def __repr__(self) -> str:
98100
result = self.__class__.__name__
99101
result += "("
100102
if self.extra_search_dir:
@@ -103,7 +105,10 @@ def __repr__(self):
103105
for distribution in self.distributions():
104106
if getattr(self, f"no_{distribution}"):
105107
continue
106-
ver = f"={getattr(self, f'{distribution}_version', None) or 'latest'}"
108+
version = getattr(self, f"{distribution}_version", None)
109+
if version == "none":
110+
continue
111+
ver = f"={version or 'latest'}"
107112
result += f" {distribution}{ver},"
108113
return result[:-1] + ")"
109114

src/virtualenv/util/path/_sync.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import os
55
import shutil
6+
import sys
67
from stat import S_IWUSR
78

89

@@ -58,7 +59,8 @@ def onerror(func, path, exc_info): # noqa: U100
5859
else:
5960
raise
6061

61-
shutil.rmtree(str(dest), ignore_errors=True, onerror=onerror)
62+
kwargs = {"onexc" if sys.version_info >= (3, 12) else "onerror": onerror}
63+
shutil.rmtree(str(dest), ignore_errors=True, **kwargs)
6264

6365

6466
class _Debug:

tests/conftest.py

-16
Original file line numberDiff line numberDiff line change
@@ -144,22 +144,6 @@ def _ignore_global_config(tmp_path_factory):
144144
yield
145145

146146

147-
@pytest.fixture(autouse=True, scope="session")
148-
def _pip_cert(tmp_path_factory):
149-
# workaround for https://github.com/pypa/pip/issues/8984 - if the certificate is explicitly set no error can happen
150-
key = "PIP_CERT"
151-
if key in os.environ:
152-
yield
153-
else:
154-
cert = tmp_path_factory.mktemp("folder") / "cert"
155-
import pkgutil
156-
157-
cert_data = pkgutil.get_data("pip._vendor.certifi", "cacert.pem")
158-
cert.write_bytes(cert_data)
159-
with change_os_environ(key, str(cert)):
160-
yield
161-
162-
163147
@pytest.fixture(autouse=True)
164148
def _check_os_environ_stable():
165149
old = os.environ.copy()

tests/integration/test_run_int.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
4+
35
import pytest
46

57
from virtualenv import cli_run
@@ -8,8 +10,8 @@
810

911

1012
@pytest.mark.skipif(IS_PYPY, reason="setuptools distutils patching does not work")
11-
def test_app_data_pinning(tmp_path):
12-
version = "19.3.1"
13+
def test_app_data_pinning(tmp_path: Path) -> None:
14+
version = "23.0"
1315
result = cli_run([str(tmp_path), "--pip", version, "--activators", "", "--seeder", "app-data"])
1416
code, out, _ = run_cmd([str(result.creator.script("pip")), "list", "--disable-pip-version-check"])
1517
assert not code

tests/unit/config/test___main__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import re
44
import sys
5+
from pathlib import Path
56
from subprocess import PIPE, Popen, check_output
67

78
import pytest
@@ -59,8 +60,8 @@ def test_fail_with_traceback(raise_on_session_done, tmp_path, capsys):
5960

6061

6162
@pytest.mark.usefixtures("session_app_data")
62-
def test_session_report_full(tmp_path, capsys):
63-
run_with_catch([str(tmp_path)])
63+
def test_session_report_full(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
64+
run_with_catch([str(tmp_path), "--setuptools", "bundle", "--wheel", "bundle"])
6465
out, err = capsys.readouterr()
6566
assert err == ""
6667
lines = out.splitlines()

tests/unit/create/test_creator.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,19 @@ def test_create_long_path(tmp_path):
364364
@pytest.mark.parametrize("creator", sorted(set(PythonInfo.current_system().creators().key_to_class) - {"builtin"}))
365365
@pytest.mark.usefixtures("session_app_data")
366366
def test_create_distutils_cfg(creator, tmp_path, monkeypatch):
367-
result = cli_run([str(tmp_path / "venv"), "--activators", "", "--creator", creator])
367+
result = cli_run(
368+
[
369+
str(tmp_path / "venv"),
370+
"--activators",
371+
"",
372+
"--creator",
373+
creator,
374+
"--setuptools",
375+
"bundle",
376+
"--wheel",
377+
"bundle",
378+
],
379+
)
368380

369381
app = Path(__file__).parent / "console_app"
370382
dest = tmp_path / "console_app"
@@ -417,7 +429,9 @@ def list_files(path):
417429

418430
def test_zip_importer_can_import_setuptools(tmp_path):
419431
"""We're patching the loaders so might fail on r/o loaders, such as zipimporter on CPython<3.8"""
420-
result = cli_run([str(tmp_path / "venv"), "--activators", "", "--no-pip", "--no-wheel", "--copies"])
432+
result = cli_run(
433+
[str(tmp_path / "venv"), "--activators", "", "--no-pip", "--no-wheel", "--copies", "--setuptools", "bundle"],
434+
)
421435
zip_path = tmp_path / "site-packages.zip"
422436
with zipfile.ZipFile(str(zip_path), "w", zipfile.ZIP_DEFLATED) as zip_handler:
423437
lib = str(result.creator.purelib)
@@ -451,6 +465,7 @@ def test_no_preimport_threading(tmp_path):
451465
out = subprocess.check_output(
452466
[str(session.creator.exe), "-c", r"import sys; print('\n'.join(sorted(sys.modules)))"],
453467
text=True,
468+
encoding="utf-8",
454469
)
455470
imported = set(out.splitlines())
456471
assert "threading" not in imported
@@ -467,6 +482,7 @@ def test_pth_in_site_vs_python_path(tmp_path):
467482
out = subprocess.check_output(
468483
[str(session.creator.exe), "-c", r"import sys; print(sys.testpth)"],
469484
text=True,
485+
encoding="utf-8",
470486
)
471487
assert out == "ok\n"
472488
# same with $PYTHONPATH pointing to site_packages
@@ -479,6 +495,7 @@ def test_pth_in_site_vs_python_path(tmp_path):
479495
[str(session.creator.exe), "-c", r"import sys; print(sys.testpth)"],
480496
text=True,
481497
env=env,
498+
encoding="utf-8",
482499
)
483500
assert out == "ok\n"
484501

@@ -492,6 +509,7 @@ def test_getsitepackages_system_site(tmp_path):
492509
out = subprocess.check_output(
493510
[str(session.creator.exe), "-c", r"import site; print(site.getsitepackages())"],
494511
text=True,
512+
encoding="utf-8",
495513
)
496514
site_packages = ast.literal_eval(out)
497515

@@ -506,6 +524,7 @@ def test_getsitepackages_system_site(tmp_path):
506524
out = subprocess.check_output(
507525
[str(session.creator.exe), "-c", r"import site; print(site.getsitepackages())"],
508526
text=True,
527+
encoding="utf-8",
509528
)
510529
site_packages = [str(Path(i).resolve()) for i in ast.literal_eval(out)]
511530

@@ -531,6 +550,7 @@ def test_get_site_packages(tmp_path):
531550
out = subprocess.check_output(
532551
[str(session.creator.exe), "-c", r"import site; print(site.getsitepackages())"],
533552
text=True,
553+
encoding="utf-8",
534554
)
535555
site_packages = ast.literal_eval(out)
536556

@@ -569,7 +589,7 @@ def _get_sys_path(flag=None):
569589
if flag:
570590
cmd.append(flag)
571591
cmd.extend(["-c", "import json; import sys; print(json.dumps(sys.path))"])
572-
return [i if case_sensitive else i.lower() for i in json.loads(subprocess.check_output(cmd))]
592+
return [i if case_sensitive else i.lower() for i in json.loads(subprocess.check_output(cmd, encoding="utf-8"))]
573593

574594
monkeypatch.delenv("PYTHONPATH", raising=False)
575595
base = _get_sys_path()

tests/unit/discovery/py_info/test_py_info.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def test_discover_exe_on_path_non_spec_name_not_match(mocker):
291291
assert CURRENT.satisfies(spec, impl_must_match=True) is False
292292

293293

294-
@pytest.mark.skipif(IS_PYPY, reason="setuptools distutil1s patching does not work")
294+
@pytest.mark.skipif(IS_PYPY, reason="setuptools distutils patching does not work")
295295
def test_py_info_setuptools():
296296
from setuptools.dist import Distribution
297297

tests/unit/discovery/windows/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def _mock_registry(mocker):
1111
from virtualenv.discovery.windows.pep514 import winreg
1212

1313
loc, glob = {}, {}
14-
mock_value_str = (Path(__file__).parent / "winreg-mock-values.py").read_text()
14+
mock_value_str = (Path(__file__).parent / "winreg-mock-values.py").read_text(encoding="utf-8")
1515
exec(mock_value_str, glob, loc)
1616
enum_collect = loc["enum_collect"]
1717
value_collect = loc["value_collect"]

tests/unit/seed/embed/test_base_embed.py

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import annotations
22

3+
import sys
4+
from pathlib import Path
5+
36
import pytest
47

58
from virtualenv.run import session_via_cli
@@ -12,3 +15,13 @@
1215
def test_download_cli_flag(args, download, tmp_path):
1316
session = session_via_cli(args + [str(tmp_path)])
1417
assert session.seeder.download is download
18+
19+
20+
def test_embed_wheel_versions(tmp_path: Path) -> None:
21+
session = session_via_cli([str(tmp_path)])
22+
expected = (
23+
{"pip": "bundle"}
24+
if sys.version_info[:2] >= (3, 12)
25+
else {"pip": "bundle", "setuptools": "bundle", "wheel": "bundle"}
26+
)
27+
assert session.seeder.distribution_to_versions() == expected

tests/unit/seed/embed/test_bootstrap_link_via_app_data.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import contextlib
44
import os
55
import sys
6+
from pathlib import Path
67
from stat import S_IWGRP, S_IWOTH, S_IWUSR
78
from subprocess import Popen, check_call
89
from threading import Thread
910

1011
import pytest
12+
from pytest_mock import MockerFixture
1113

1214
from virtualenv.discovery import cached_py_info
1315
from virtualenv.discovery.py_info import PythonInfo
@@ -202,7 +204,7 @@ def test_populated_read_only_cache_and_copied_app_data(tmp_path, current_fastest
202204
@pytest.mark.parametrize("pkg", ["pip", "setuptools", "wheel"])
203205
@pytest.mark.usefixtures("session_app_data", "current_fastest", "coverage_env")
204206
def test_base_bootstrap_link_via_app_data_no(tmp_path, pkg):
205-
create_cmd = [str(tmp_path), "--seeder", "app-data", f"--no-{pkg}"]
207+
create_cmd = [str(tmp_path), "--seeder", "app-data", f"--no-{pkg}", "--wheel", "bundle", "--setuptools", "bundle"]
206208
result = cli_run(create_cmd)
207209
assert not (result.creator.purelib / pkg).exists()
208210
for key in {"pip", "setuptools", "wheel"} - {pkg}:
@@ -216,7 +218,7 @@ def test_app_data_parallel_ok(tmp_path):
216218

217219

218220
@pytest.mark.usefixtures("temp_app_data")
219-
def test_app_data_parallel_fail(tmp_path, mocker):
221+
def test_app_data_parallel_fail(tmp_path: Path, mocker: MockerFixture) -> None:
220222
mocker.patch("virtualenv.seed.embed.via_app_data.pip_install.base.PipInstall.build_image", side_effect=RuntimeError)
221223
exceptions = _run_parallel_threads(tmp_path)
222224
assert len(exceptions) == 2
@@ -230,7 +232,7 @@ def _run_parallel_threads(tmp_path):
230232

231233
def _run(name):
232234
try:
233-
cli_run(["--seeder", "app-data", str(tmp_path / name), "--no-pip", "--no-setuptools"])
235+
cli_run(["--seeder", "app-data", str(tmp_path / name), "--no-pip", "--no-setuptools", "--wheel", "bundle"])
234236
except Exception as exception:
235237
as_str = str(exception)
236238
exceptions.append(as_str)

0 commit comments

Comments
 (0)