diff --git a/Lib/site.py b/Lib/site.py index 294a4d6f641c39..36f81d69c8faa7 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -1031,11 +1031,14 @@ def _venv(state): ) break - if sys.prefix != site_prefix: + # site_prefix is already normalised by the abspath() above, but sys.prefix + # isn't. Normalise before comparing, or two spellings of the same directory + # look like a mismatch. + if os.path.normpath(sys.prefix) != site_prefix: _warn( f'Unexpected value in sys.prefix, expected {site_prefix}, got {sys.prefix}', RuntimeWarning) - if sys.exec_prefix != site_prefix: + if os.path.normpath(sys.exec_prefix) != site_prefix: _warn( f'Unexpected value in sys.exec_prefix, expected {site_prefix}, got {sys.exec_prefix}', RuntimeWarning) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index b4ad1bf3f41294..7d5220708fe226 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -23,8 +23,8 @@ is_wasm32, requires_venv_with_pip, TEST_HOME_DIR, requires_resource, copy_python_src_ignore) -from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree, - TESTFN, FakePath) +from test.support.os_helper import (can_symlink, change_cwd, EnvironmentVarGuard, + rmtree, TESTFN, FakePath) import unittest import venv from unittest.mock import patch, Mock @@ -302,6 +302,58 @@ def test_prefixes(self): self.assertEqual(pathlib.Path(out.strip().decode()), pathlib.Path(expected), prefix) + @requireVenvCreate + def test_prefixes_with_non_normalised_executable(self): + """ + Test that a non-normalised executable path isn't counted as a mismatch. + """ + # gh-156495: sys.prefix isn't normalised, but the prefix derived from + # sys.executable is, so a string comparison flagged two spellings of the + # same directory. + rmtree(self.env_dir) + self.run_with_capture(venv.create, self.env_dir) + # Run from a directory next to the env so argv[0] starts with '..', + # which normpath() can't collapse. + subdir = tempfile.mkdtemp(dir=os.path.dirname(self.env_dir)) + self.addCleanup(rmtree, subdir) + relative_exe = os.path.join( + os.pardir, os.path.basename(self.env_dir), self.bindir, self.exe) + # Windows does not resolve a relative executable against subprocess's + # cwd argument, so change directory in this process instead. + with change_cwd(subdir): + p = subprocess.run( + [relative_exe, '-c', + 'import sys; print(sys.prefix); print(sys.exec_prefix)'], + capture_output=True, encoding='utf-8', + env={**os.environ, 'PYTHONHOME': ''}) + self.assertEqual(p.returncode, 0, p.stderr) + self.assertNotIn('Unexpected value in sys.prefix', p.stderr) + self.assertNotIn('Unexpected value in sys.exec_prefix', p.stderr) + prefix, exec_prefix = p.stdout.splitlines() + for name, value in (('prefix', prefix), ('exec_prefix', exec_prefix)): + self.assertEqual(os.path.realpath(value), + os.path.realpath(self.env_dir), name) + + @requireVenvCreate + def test_prefixes_mismatch_is_still_reported(self): + """ + Test that a genuine prefix mismatch is still reported. + """ + rmtree(self.env_dir) + self.run_with_capture(venv.create, self.env_dir) + # Move pyvenv.cfg next to the interpreter instead of leaving it in the + # prefix, so that sys.prefix and the prefix derived from sys.executable + # differ for real and not only in spelling. + os.rename(os.path.join(self.env_dir, 'pyvenv.cfg'), + os.path.join(self.env_dir, self.bindir, 'pyvenv.cfg')) + p = subprocess.run( + [self.envpy(), '-c', 'import sys; print(sys.prefix)'], + capture_output=True, encoding='utf-8', + env={**os.environ, 'PYTHONHOME': ''}) + self.assertEqual(p.returncode, 0, p.stderr) + self.assertIn('Unexpected value in sys.prefix', p.stderr) + self.assertIn('Unexpected value in sys.exec_prefix', p.stderr) + @requireVenvCreate def test_sysconfig(self): """ diff --git a/Misc/NEWS.d/next/Library/2026-08-27-14-00-00.gh-issue-156495.Qk3vTz.rst b/Misc/NEWS.d/next/Library/2026-08-27-14-00-00.gh-issue-156495.Qk3vTz.rst new file mode 100644 index 00000000000000..8ff3d3f494537c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-27-14-00-00.gh-issue-156495.Qk3vTz.rst @@ -0,0 +1,4 @@ +Fix a false-positive :exc:`RuntimeWarning` from :mod:`site` when a virtual +environment interpreter is started through a non-normalised path such as +``../.venv/bin/python``. ``sys.prefix`` and the prefix derived from +:data:`sys.executable` are now compared in normalised form.