Skip to content

Commit 1507527

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 1507527

File tree

19 files changed

+140
-72
lines changed

19 files changed

+140
-72
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

+4-2
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ optional-dependencies.test = [
5757
"packaging>=23.1",
5858
"pytest>=7.3.1",
5959
"pytest-env>=0.8.1",
60-
"pytest-freezegun>=0.4.2",
60+
'pytest-freezegun>=0.4.2; platform_python_implementation == "pypy"',
6161
"pytest-mock>=3.10",
6262
"pytest-randomly>=3.12",
6363
"pytest-timeout>=2.1",
64+
"setuptools>=67.7.1",
65+
'time-machine>=2.9; platform_python_implementation == "cython"',
6466
]
6567
urls.Documentation = "https://virtualenv.pypa.io"
6668
urls.Homepage = "https://github.com/pypa/virtualenv"
@@ -116,7 +118,7 @@ ignore = [
116118
[tool.pytest.ini_options]
117119
markers = ["slow"]
118120
timeout = 600
119-
addopts = "--tb=auto -ra --showlocals --no-success-flaky-report"
121+
addopts = "--showlocals --no-success-flaky-report"
120122
env = ["PYTHONIOENCODING=utf-8"]
121123

122124
[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

+14-17
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from virtualenv.app_data import AppDataDiskFolder
1414
from virtualenv.discovery.py_info import PythonInfo
15-
from virtualenv.info import IS_WIN, fs_supports_symlink
15+
from virtualenv.info import IS_PYPY, IS_WIN, fs_supports_symlink
1616
from virtualenv.report import LOGGER
1717

1818

@@ -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()
@@ -368,3 +352,16 @@ def _skip_if_test_in_system(session_app_data):
368352
current = PythonInfo.current(session_app_data)
369353
if current.system_executable is not None:
370354
pytest.skip("test not valid if run under system")
355+
356+
357+
if IS_PYPY:
358+
359+
@pytest.fixture()
360+
def time_freeze(freezer):
361+
return freezer.move_to
362+
363+
else:
364+
365+
@pytest.fixture()
366+
def time_freeze(time_machine):
367+
return lambda s: time_machine.move_to(s, tick=False)

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

0 commit comments

Comments
 (0)