From 23acf4b16efa58f924e090ba2e691c27ccfb8c53 Mon Sep 17 00:00:00 2001 From: "Aryan Singh K." <70511529+aryansk@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:01:46 +0530 Subject: [PATCH] fix: exclude symlinked stdlib paths --- mypy/pyinfo.py | 8 ++++++-- mypy/test/testpyinfo.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 mypy/test/testpyinfo.py diff --git a/mypy/pyinfo.py b/mypy/pyinfo.py index 0563e16eda718..63f17f1862245 100644 --- a/mypy/pyinfo.py +++ b/mypy/pyinfo.py @@ -47,7 +47,9 @@ def getsyspath() -> list[str]: ) stdlib = sysconfig.get_path("stdlib") stdlib_ext = os.path.join(stdlib, "lib-dynload") - excludes = {stdlib_zip, stdlib, stdlib_ext} + # sysconfig may return a symlinked path while sys.path contains its resolved + # target (for example, with Homebrew Python installations). + excludes = {os.path.realpath(p) for p in (stdlib_zip, stdlib, stdlib_ext)} # Drop the first entry of sys.path # - If pyinfo.py is executed as a script (in a subprocess), this is the directory @@ -63,7 +65,9 @@ def getsyspath() -> list[str]: offset = 0 if sys.version_info >= (3, 11) and sys.flags.safe_path else 1 abs_sys_path = (os.path.abspath(p) for p in sys.path[offset:]) - return [p for p in abs_sys_path if p not in excludes] + # Keep the original absolute spelling in the result; only resolve paths + # for the standard-library exclusion comparison. + return [p for p in abs_sys_path if os.path.realpath(p) not in excludes] def getsearchdirs() -> tuple[list[str], list[str]]: diff --git a/mypy/test/testpyinfo.py b/mypy/test/testpyinfo.py new file mode 100644 index 0000000000000..ad43138d6963a --- /dev/null +++ b/mypy/test/testpyinfo.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from mypy import pyinfo + + +class TestPyInfo(unittest.TestCase): + def test_getsyspath_excludes_symlinked_stdlib(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + real_stdlib = root / "real" / "lib" / "python3.13" + real_stdlib.mkdir(parents=True) + symlinked_stdlib = root / "link" / "lib" / "python3.13" + symlinked_stdlib.parent.parent.symlink_to(root / "real", target_is_directory=True) + site_packages = root / "site-packages" + site_packages.mkdir() + + with ( + patch.object(pyinfo.sys, "base_exec_prefix", str(root)), + patch.object( + pyinfo.sysconfig, "get_path", autospec=True, return_value=str(symlinked_stdlib) + ), + patch.object( + pyinfo.sys, "path", [str(root), str(real_stdlib), str(site_packages)] + ), + ): + search_path = pyinfo.getsyspath() + + assert os.path.abspath(real_stdlib) not in search_path + assert os.path.abspath(site_packages) in search_path + + +if __name__ == "__main__": + unittest.main()