Skip to content
30 changes: 30 additions & 0 deletions Lib/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@ def venv(known_paths):
if candidate_conf:
virtual_conf = candidate_conf
system_site = "true"
version, version_info = None, None
# Issue 25185: Use UTF-8, as that's what the venv module uses when
# writing the file.
with open(virtual_conf, encoding='utf-8') as f:
Expand All @@ -937,6 +938,35 @@ def venv(known_paths):
system_site = value.lower()
elif key == 'home':
sys._home = value
elif key == 'version':
version = value
elif key == 'version_info':
version_info = value

for field_name, field_value in [
('version',version), ('version_info',version_info)
]:
if field_value is not None:
try:
major, minor = map(int, field_value.split(".")[:2])
except (ValueError, AttributeError):
_warn(
f"Malformed {field_name} string in pyvenv.cfg: {field_value!r}",
RuntimeWarning,
)
else:
if (
major == sys.version_info.major
and minor != sys.version_info.minor
):
_warn(
f"This virtual environment was created for Python {major}.{minor}, "
f"but the current interpreter is Python "
f"{sys.version_info.major}.{sys.version_info.minor}. "
"Consider running `python -m venv --upgrade` to update the environment.",
RuntimeWarning,
)
break

if sys.prefix != site_prefix:
_warn(f'Unexpected value in sys.prefix, expected {site_prefix}, got {sys.prefix}', RuntimeWarning)
Expand Down
220 changes: 220 additions & 0 deletions Lib/test/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,226 @@ def test_sysconfig(self):
out, err = check_output(cmd, encoding='utf-8')
self.assertEqual(out.strip(), expected, err)

@requireVenvCreate
def test_version_mismatch_warning(self):
"""
Test that a warning is emitted when running a venv created for a
different minor Python version.
"""
rmtree(self.env_dir)

wrong_minor = sys.version_info.minor + 1
self.run_with_capture(venv.create, self.env_dir, with_pip=False)

cfg_path = self.get_env_file('pyvenv.cfg')
with open(cfg_path, 'r', encoding='utf-8') as f:
cfg_content = f.read()

new_version = f"{sys.version_info.major}.{wrong_minor}"
if 'version =' in cfg_content:
cfg_content = re.sub(r'version = \d+\.\d+', f'version = {new_version}', cfg_content)

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

with open(cfg_path, 'w', encoding='utf-8') as f:
f.write(cfg_content)

envpy = self.envpy(real_env_dir=True)

proc = subprocess.run(
[envpy, '-c', 'import sys; print("done")'],
capture_output=True,
text=True,
env={**os.environ, "PYTHONHOME": ""}
)

self.assertIn(f"Python {sys.version_info.major}.{wrong_minor}", proc.stderr)
self.assertIn("Consider running `python -m venv --upgrade`", proc.stderr)

@requireVenvCreate
def test_version_info_mismatch_warning(self):
"""
Test that a warning is emitted when version_info (used by virtualenv)
indicates a different minor version.
"""
rmtree(self.env_dir)
wrong_minor = sys.version_info.minor + 1
self.run_with_capture(venv.create, self.env_dir, with_pip=False)

cfg_path = self.get_env_file('pyvenv.cfg')
with open(cfg_path, 'r', encoding='utf-8') as f:
cfg_content = f.read()

# Add only version_info, don't modify version
new_version = f"{sys.version_info.major}.{wrong_minor}"
cfg_content += f'\nversion_info = {new_version}\n'

with open(cfg_path, 'w', encoding='utf-8') as f:
f.write(cfg_content)

envpy = self.envpy(real_env_dir=True)
proc = subprocess.run(
[envpy, '-c', 'import sys; print("done")'],
capture_output=True,
text=True,
env={**os.environ, "PYTHONHOME": ""}
)

self.assertIn(f"Python {sys.version_info.major}.{wrong_minor}", proc.stderr)
self.assertIn("Consider running `python -m venv --upgrade`", proc.stderr)

@requireVenvCreate
def test_version_match_no_warning(self):
"""
Test that no warning is emitted when the venv version matches.
"""
rmtree(self.env_dir)

self.run_with_capture(venv.create, self.env_dir, with_pip=False)
cfg_path = self.get_env_file('pyvenv.cfg')
with open(cfg_path, 'r', encoding='utf-8') as f:
cfg_content = f.read()
expected_version = f"{sys.version_info.major}.{sys.version_info.minor}"

with open(cfg_path, 'w', encoding='utf-8') as f:
f.write(cfg_content)
envpy = self.envpy(real_env_dir=True)
proc = subprocess.run(
[envpy, '-c', 'import sys; print("done")'],
capture_output=True,
text=True,
env={**os.environ, "PYTHONHOME": ""}
)

self.assertNotIn("Consider running `python -m venv --upgrade`", proc.stderr)

@requireVenvCreate
def test_malformed_version_warning(self):
"""
Test that a warning is emitted on malformed version string
in pyenv.cfg
"""
rmtree(self.env_dir)

self.run_with_capture(venv.create, self.env_dir, with_pip=False)

cfg_path = self.get_env_file('pyvenv.cfg')
with open(cfg_path, 'r', encoding='utf-8') as f:
cfg_content = f.read()

malformed_version = "not.a.version"
if 'version =' in cfg_content:
cfg_content = re.sub(r'version = .+', f'version = {malformed_version}', cfg_content)

with open(cfg_path, 'w', encoding='utf-8') as f:
f.write(cfg_content)

envpy = self.envpy(real_env_dir=True)
proc = subprocess.run(
[envpy, '-c', 'import sys; print("done")'],
capture_output=True,
text=True,
env={**os.environ, "PYTHONHOME": ""}
)
self.assertIn("Malformed version string", proc.stderr)
self.assertIn(malformed_version, proc.stderr)

@requireVenvCreate
def test_malformed_version_info_warning(self):
"""
Test that a warning is emitted on malformed version_info string
in pyenv.cfg
"""
rmtree(self.env_dir)
self.run_with_capture(venv.create, self.env_dir, with_pip=False)

cfg_path = self.get_env_file('pyvenv.cfg')
with open(cfg_path, 'r', encoding='utf-8') as f:
cfg_content = f.read()

malformed_version = "invalid.version"
cfg_content += f'\nversion_info = {malformed_version}\n'

with open(cfg_path, 'w', encoding='utf-8') as f:
f.write(cfg_content)

envpy = self.envpy(real_env_dir=True)
proc = subprocess.run(
[envpy, '-c', 'import sys; print("done")'],
capture_output=True,
text=True,
env={**os.environ, "PYTHONHOME": ""}
)

self.assertIn("Malformed version_info string", proc.stderr)
self.assertIn(malformed_version, proc.stderr)

@requireVenvCreate
def test_conflicting_version_fields(self):
"""
Test behavior when both version and version_info are present
but contain different values. Should warn based on first mismatch found.
"""
rmtree(self.env_dir)
wrong_minor = sys.version_info.minor + 1
self.run_with_capture(venv.create, self.env_dir, with_pip=False)

cfg_path = self.get_env_file('pyvenv.cfg')
with open(cfg_path, 'r', encoding='utf-8') as f:
cfg_content = f.read()

version_wrong = f"{sys.version_info.major}.{wrong_minor}"
if 'version =' in cfg_content:
cfg_content = re.sub(r'version = \d+\.\d+', f'version = {version_wrong}', cfg_content)

version_info_wrong = f"{sys.version_info.major}.{wrong_minor + 1}"
cfg_content += f'\nversion_info = {version_info_wrong}\n'

with open(cfg_path, 'w', encoding='utf-8') as f:
f.write(cfg_content)

envpy = self.envpy(real_env_dir=True)
proc = subprocess.run(
[envpy, '-c', 'import sys; print("done")'],
capture_output=True,
text=True,
env={**os.environ, "PYTHONHOME": ""}
)

self.assertIn("Consider running `python -m venv --upgrade`", proc.stderr)
self.assertEqual(proc.stderr.count("Consider running `python -m venv --upgrade`"), 1)

@requireVenvCreate
def test_different_major_version_no_warning(self):
"""
Test that no warning is emitted when major version differs.
The warning should only trigger for same major, different minor.
"""
rmtree(self.env_dir)
self.run_with_capture(venv.create, self.env_dir, with_pip=False)

cfg_path = self.get_env_file('pyvenv.cfg')
with open(cfg_path, 'r', encoding='utf-8') as f:
cfg_content = f.read()

different_major = sys.version_info.major + 1
new_version = f"{different_major}.{sys.version_info.minor}"

if 'version =' in cfg_content:
cfg_content = re.sub(r'version = \d+\.\d+', f'version = {new_version}', cfg_content)
with open(cfg_path, 'w', encoding='utf-8') as f:
f.write(cfg_content)

envpy = self.envpy(real_env_dir=True)
proc = subprocess.run(
[envpy, '-c', 'import sys; print("done")'],
capture_output=True,
text=True,
env={**os.environ, "PYTHONHOME": ""}
)

self.assertNotIn("Consider running `python -m venv --upgrade`", proc.stderr)

@requireVenvCreate
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
def test_sysconfig_symlinks(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Warn when running a virtual environment created for a different minor Python
version than the current interpreter, and suggest using ``python -m venv
--upgrade``.
Loading