diff --git a/Lib/site.py b/Lib/site.py index b7f5c7f0246bc1b..fe940cbcd28f72e 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -254,6 +254,7 @@ class StartupState: '_path_entries', '_importexecs', '_entrypoints', + '_sitedir', ) def __init__(self, known_paths=None): @@ -286,6 +287,7 @@ def __init__(self, known_paths=None): # when an entry fails. self._importexecs = {} self._entrypoints = {} + self._sitedir = None def addsitedir(self, sitedir): """Add a site directory and accumulate its .pth and .start startup data. @@ -397,6 +399,8 @@ def _read_pth_file(self, sitedir, name): accepted paths are added to it so that subsequent .pth files in the same batch don't add them more than once. """ + self._sitedir = sitedir + lines, filename = _read_pthstart_file(sitedir, name, ".pth") if lines is None: return @@ -500,6 +504,11 @@ def _extend_syspath(self): ) def _exec_imports(self): + # Inject 'sitedir' local variable in the current frame for + # compatibility with Python 3.14. Especially, "-nspkg.pth" files + # generated by setuptools use: sys._getframe(1).f_locals['sitedir']. + sitedir = self._sitedir + # For each `import` line we've seen in a .pth file, exec() it in # order, unless the .pth has a matching .start file in this same # batch. In that case, PEP 829 says the import lines are diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py index 5fd65ad999210e4..26dd30c7bb6a318 100644 --- a/Lib/test/test_site.py +++ b/Lib/test/test_site.py @@ -233,6 +233,21 @@ def test_addsitedir_hidden_file_attribute(self): self.assertNotIn(site.makepath(pth_file.good_dir_path)[0], sys.path) self.assertIn(pth_file.base_dir, sys.path) + def test_sitedir_variable(self): + # gh-149671: Provide 'sitedir' local variable for compatibility with + # Python 3.14 + code = '; '.join(( + "import sys", + # Code used by "-nspkg.pth" files generated by setuptools + "sitedir = sys._getframe(1).f_locals['sitedir']", + "print(sitedir)", + )) + pth_dir, pth_fn = self.make_pth(code) + with support.captured_stdout() as stdout: + known_paths = site.addpackage(pth_dir, pth_fn, set()) + sitedir = stdout.getvalue().rstrip() + self.assertEqual(sitedir, pth_dir) + # This tests _getuserbase, hence the double underline # to distinguish from a test for getuserbase def test__getuserbase(self): diff --git a/Misc/NEWS.d/next/Library/2026-06-11-11-52-23.gh-issue-149671.6Rpr5r.rst b/Misc/NEWS.d/next/Library/2026-06-11-11-52-23.gh-issue-149671.6Rpr5r.rst new file mode 100644 index 000000000000000..0740fb8fd66095d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-11-11-52-23.gh-issue-149671.6Rpr5r.rst @@ -0,0 +1,3 @@ +Restore compatibility with setuptools ``nspkg.pth`` files in the :mod:`site` +module. Inject ``sitedir`` variable in the frame which executes pth code. +Patch by Victor Stinner.