Skip to content

Commit 1b2f494

Browse files
committed
PEP 838: Reject python startup with mismatched python-version
This is a stronger version of #127727 / #149715: Using the standardized `python-version`, we fail at startup with a helpful error message instead of running up until an unclear `ModuleNotFoundError` because the shared library has a tag for a different Python version. This change checks the major version, we shouldn't silently pass when encountering a potential Python 4 that mismatches. This is an optional part of the PEP and may equally be a warning as it currently is with `version`/`version_info`. Stacked on top of #154378.
1 parent 2856a2e commit 1b2f494

3 files changed

Lines changed: 85 additions & 29 deletions

File tree

Lib/site.py

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -982,7 +982,7 @@ def _venv(state):
982982
if candidate_conf:
983983
virtual_conf = candidate_conf
984984
system_site = "true"
985-
version, version_info = None, None
985+
version, version_info, python_version = None, None, None
986986
# Issue 25185: Use UTF-8, as that's what the venv module uses when
987987
# writing the file.
988988
with open(virtual_conf, encoding='utf-8') as f:
@@ -999,31 +999,46 @@ def _venv(state):
999999
version = value
10001000
elif key == 'version_info':
10011001
version_info = value
1002-
1003-
for field_name, field_value in [
1004-
('version',version), ('version_info',version_info)
1002+
elif key == 'python-version':
1003+
python_version = value
1004+
1005+
for field_name, field_value, should_error in [
1006+
# Run the fatal check first.
1007+
('python_version', python_version, True),
1008+
('version',version, False),
1009+
('version_info',version_info, False),
10051010
]:
1006-
if field_value is not None:
1007-
try:
1008-
major, minor = map(int, field_value.split(".")[:2])
1009-
except (ValueError, AttributeError):
1010-
_warn(
1011-
f"Malformed {field_name} string in pyvenv.cfg: {field_value!r}",
1012-
RuntimeWarning,
1013-
)
1014-
else:
1015-
if (
1016-
major == sys.version_info.major
1017-
and minor != sys.version_info.minor
1018-
):
1019-
_warn(
1020-
f"This virtual environment was created for Python {major}.{minor}, "
1021-
f"but the current interpreter is Python "
1022-
f"{sys.version_info.major}.{sys.version_info.minor}. "
1023-
"Consider running `python -m venv --upgrade` to update the environment.",
1024-
RuntimeWarning,
1025-
)
1026-
break
1011+
if field_value is None:
1012+
continue
1013+
try:
1014+
major, minor = map(int, field_value.split(".")[:2])
1015+
except (ValueError, AttributeError):
1016+
_warn(
1017+
f"Malformed {field_name} string in pyvenv.cfg: {field_value!r}",
1018+
RuntimeWarning,
1019+
)
1020+
continue
1021+
if (
1022+
major == sys.version_info.major
1023+
and minor == sys.version_info.minor
1024+
):
1025+
continue
1026+
if should_error:
1027+
raise RuntimeError(
1028+
f"This virtual environment was created for Python {major}.{minor}, "
1029+
f"but the current interpreter is Python "
1030+
f"{sys.version_info.major}.{sys.version_info.minor}. "
1031+
"Consider running `python -m venv --upgrade` to update the environment.",
1032+
)
1033+
else:
1034+
_warn(
1035+
f"This virtual environment was created for Python {major}.{minor}, "
1036+
f"but the current interpreter is Python "
1037+
f"{sys.version_info.major}.{sys.version_info.minor}. "
1038+
"Consider running `python -m venv --upgrade` to update the environment.",
1039+
RuntimeWarning,
1040+
)
1041+
break
10271042

10281043
if sys.prefix != site_prefix:
10291044
_warn(

Lib/test/test_venv.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,45 @@ def test_sysconfig(self):
313313
out, err = check_output(cmd, encoding='utf-8')
314314
self.assertEqual(out.strip(), expected, err)
315315

316+
@requireVenvCreate
317+
def test_python_version_mismatch_error(self):
318+
rmtree(self.env_dir)
319+
self.run_with_capture(venv.create, self.env_dir, with_pip=False)
320+
321+
corrct_version = f"{sys.version_info.major}.{sys.version_info.minor}"
322+
wrong_version = f"{sys.version_info.major}.{sys.version_info.minor + 1}"
323+
324+
cfg_path = self.get_env_file("pyvenv.cfg")
325+
with open(cfg_path, encoding="utf-8") as f:
326+
cfg_content = f.read()
327+
328+
cfg_content = cfg_content.replace(
329+
f"python-version = {corrct_version}",
330+
f"python-version = {wrong_version}",
331+
)
332+
333+
with open(cfg_path, "w", encoding="utf-8") as f:
334+
f.write(cfg_content)
335+
336+
envpy = self.envpy(real_env_dir=True)
337+
338+
proc = subprocess.run(
339+
[envpy, "-c", 'print("done")'],
340+
capture_output=True,
341+
text=True,
342+
env={**os.environ, "PYTHONHOME": ""},
343+
)
344+
345+
self.assertNotEqual(proc.returncode, 0)
346+
self.assertNotIn("done", proc.stdout)
347+
self.assertIn("RuntimeError", proc.stderr)
348+
self.assertIn(f"Python {wrong_version}", proc.stderr)
349+
self.assertIn(
350+
f"Python {sys.version_info.major}.{sys.version_info.minor}",
351+
proc.stderr,
352+
)
353+
self.assertIn("Consider running `python -m venv --upgrade`", proc.stderr)
354+
316355
@requireVenvCreate
317356
def test_version_mismatch_warning(self):
318357
"""
@@ -330,7 +369,7 @@ def test_version_mismatch_warning(self):
330369

331370
new_version = f"{sys.version_info.major}.{wrong_minor}"
332371
if 'version =' in cfg_content:
333-
cfg_content = re.sub(r'version = \d+\.\d+', f'version = {new_version}', cfg_content)
372+
cfg_content = re.sub(r'(?m)^version = \d+\.\d+', f'version = {new_version}', cfg_content)
334373

335374
cfg_content += f'\nversion_info = {new_version}\n'
336375

@@ -422,7 +461,7 @@ def test_malformed_version_warning(self):
422461

423462
malformed_version = "not.a.version"
424463
if 'version =' in cfg_content:
425-
cfg_content = re.sub(r'version = .+', f'version = {malformed_version}', cfg_content)
464+
cfg_content = re.sub(r'(?m)^version = .+', f'version = {malformed_version}', cfg_content)
426465

427466
with open(cfg_path, 'w', encoding='utf-8') as f:
428467
f.write(cfg_content)
@@ -483,7 +522,7 @@ def test_conflicting_version_fields(self):
483522

484523
version_wrong = f"{sys.version_info.major}.{wrong_minor}"
485524
if 'version =' in cfg_content:
486-
cfg_content = re.sub(r'version = \d+\.\d+', f'version = {version_wrong}', cfg_content)
525+
cfg_content = re.sub(r'(?m)^version = \d+\.\d+', f'version = {version_wrong}', cfg_content)
487526

488527
version_info_wrong = f"{sys.version_info.major}.{wrong_minor + 1}"
489528
cfg_content += f'\nversion_info = {version_info_wrong}\n'
@@ -519,7 +558,7 @@ def test_different_major_version_no_warning(self):
519558
new_version = f"{different_major}.{sys.version_info.minor}"
520559

521560
if 'version =' in cfg_content:
522-
cfg_content = re.sub(r'version = \d+\.\d+', f'version = {new_version}', cfg_content)
561+
cfg_content = re.sub(r'(?m)^version = \d+\.\d+', f'version = {new_version}', cfg_content)
523562
with open(cfg_path, 'w', encoding='utf-8') as f:
524563
f.write(cfg_content)
525564

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fail at startup when the ``python-version`` in a virtual
2+
environment's ``pyvenv.cfg`` does not match the running interpreter.

0 commit comments

Comments
 (0)