]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-127727: Warn when running a virtual environment created for a different minor...
authorSepehr Rasouli <sepehrrasouli06@gmail.com>
Mon, 22 Jun 2026 14:29:15 +0000 (17:59 +0330)
committerGitHub <noreply@github.com>
Mon, 22 Jun 2026 14:29:15 +0000 (15:29 +0100)
Lib/site.py
Lib/test/test_venv.py
Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst [new file with mode: 0644]

index d06549b8df800e3959e4faa07712ef0b65ec8707..873c562d890a3bb3a2e9a2bee42c9b0cee6415f9 100644 (file)
@@ -982,6 +982,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:
@@ -994,6 +995,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(
index 0c6cd1b196ac81273aa9cf7d130ac874d3ed0803..8075a1947918bed3df7df0497316b83154c540f7 100644 (file)
@@ -310,6 +310,226 @@ class BasicTest(BaseTest):
                 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 (file)
index 0000000..474ae43
--- /dev/null
@@ -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``.