diff --git a/Lib/site.py b/Lib/site.py index b7f5c7f0246bc1..681476638c7ef0 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -977,6 +977,7 @@ def _venv(state): 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: @@ -989,6 +990,35 @@ def _venv(state): 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( diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 1ff5d7cf0c51dd..30d2344bc8c93c 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -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): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst new file mode 100644 index 00000000000000..474ae434f6885e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst @@ -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``.