Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Lib/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,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 @@ -934,6 +935,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