Skip to content

Commit 3c298e2

Browse files
warsawBugBounty Mindscoder
authored
gh-149819: fix .pth and .start file processing in subprocess when inheriting PYTHONPATH (#150177)
* gh-149819: Fix .pth files not loaded in Python subprocesses After PR gh-149583 (Fix double evaluation of .pth and .site files in venvs), .pth files are no longer loaded in subprocesses started with subprocess.run([sys.executable, ...]). The root cause: main() seeds known_paths from removeduppaths() with all sys.path entries inherited from the parent process. addsitedir() then skips .pth processing for every directory already in known_paths. Fix: - main(): call removeduppaths() for dedup but start known_paths as a fresh empty set, so that addsitedir() processes .pth files in every site-packages directory regardless of inherited sys.path. - addsitedir(): move known_paths.add() before the sys.path.append and guard the append with 'sitedir not in sys.path' to avoid creating duplicate entries when called with a fresh known_paths. This preserves the gh-75723 dedup guarantee while allowing subprocesses to load .pth files. * Fill out the tests for GH#149888 * Extend _make_start() and _make_pth() to take an optional `basedir` which is used instead of `site.tmpdir` if given. * Add test_pth_processed_when_sitedir_already_on_path() to test the core GH#149819 bug: .pth files in subprocesses aren't handled if PYTHONPATH pointing to the .pth directory is inherited. * Similarly add test_start_processed_when_sitedir_already_on_path() to verify that .start files in the same circumstances are also now processed. * Update Lib/site.py Co-authored-by: scoder <stefan_ml@behnel.de> * Oops! Remove redundant code --------- Co-authored-by: BugBounty Mind <bugbounty-mind@deepseek.tui> Co-authored-by: scoder <stefan_ml@behnel.de>
1 parent 65f9932 commit 3c298e2

3 files changed

Lines changed: 105 additions & 14 deletions

File tree

Lib/site.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -490,13 +490,16 @@ def addsitedir(sitedir, known_paths=None, *, defer_processing_start_files=False)
490490
reset = False
491491
sitedir, sitedircase = makepath(sitedir)
492492

493-
# If the normcase'd new sitedir isn't already known, append it to
494-
# sys.path, keep a record of it, and process all .pth and .start files
495-
# found in that directory. If the new sitedir is known, be sure not
496-
# to process all of those more than once! gh-75723
493+
# If the normcase'd new sitedir isn't already known, record it to
494+
# prevent re-processing, append it to sys.path (only if not already
495+
# present), and process all .pth and .start files found in that
496+
# directory. Use a direct sys.path membership check for the append
497+
# guard so that callers (like main()) can pass a fresh known_paths
498+
# set while avoiding duplicate sys.path entries (gh-149819).
497499
if sitedircase not in known_paths:
498-
sys.path.append(sitedir)
499500
known_paths.add(sitedircase)
501+
if sitedir not in sys.path:
502+
sys.path.append(sitedir)
500503

501504
try:
502505
names = os.listdir(sitedir)
@@ -1000,13 +1003,13 @@ def main():
10001003
global ENABLE_USER_SITE
10011004

10021005
orig_path = sys.path[:]
1003-
known_paths = removeduppaths()
1006+
removeduppaths()
10041007
if orig_path != sys.path:
10051008
# removeduppaths() might make sys.path absolute.
10061009
# Fix __file__ of already imported modules too.
10071010
abs_paths()
10081011

1009-
known_paths = venv(known_paths)
1012+
known_paths = venv(known_paths=set())
10101013
if ENABLE_USER_SITE is None:
10111014
ENABLE_USER_SITE = check_enableusersite()
10121015
known_paths = addusersitepackages(known_paths, defer_processing_start_files=True)

Lib/test/test_site.py

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ def cleanup(self, prep=False):
456456
if os.path.exists(self.bad_dir_path):
457457
os.rmdir(self.bad_dir_path)
458458

459+
459460
class ImportSideEffectTests(unittest.TestCase):
460461
"""Test side-effects from importing 'site'."""
461462

@@ -545,7 +546,6 @@ def test_customization_modules_on_startup(self):
545546
output = subprocess.check_output([sys.executable, '-s', '-c', '""'])
546547
self.assertNotIn(eyecatcher, output.decode('utf-8'))
547548

548-
549549
@unittest.skipUnless(hasattr(urllib.request, "HTTPSHandler"),
550550
'need SSL support to download license')
551551
@test.support.requires_resource('network')
@@ -926,18 +926,28 @@ def setUp(self):
926926
def _reset_startup_state(self):
927927
site._startup_state = None
928928

929-
def _make_start(self, content, name='testpkg'):
930-
"""Write a <name>.start file and return its basename."""
929+
def _make_start(self, content, name='testpkg', basedir=None):
930+
"""Write a <name>.start file and return its basename.
931+
932+
``basedir`` defaults to ``self.tmpdir``. Pass an explicit directory
933+
when the .start file needs to live somewhere other than the test's
934+
primary tmpdir (e.g. a nested user-site).
935+
"""
931936
basename = f"{name}.start"
932-
filepath = os.path.join(self.tmpdir, basename)
937+
filepath = os.path.join(self.tmpdir if basedir is None else basedir, basename)
933938
with open(filepath, 'w', encoding='utf-8') as f:
934939
f.write(content)
935940
return basename
936941

937-
def _make_pth(self, content, name='testpkg'):
938-
"""Write a <name>.pth file and return its basename."""
942+
def _make_pth(self, content, name='testpkg', basedir=None):
943+
"""Write a <name>.pth file and return its basename.
944+
945+
``basedir`` defaults to ``self.tmpdir``. Pass an explicit directory
946+
when the .pth file needs to live somewhere other than the test's
947+
primary tmpdir (e.g. a nested user-site).
948+
"""
939949
basename = f"{name}.pth"
940-
filepath = os.path.join(self.tmpdir, basename)
950+
filepath = os.path.join(self.tmpdir if basedir is None else basedir, basename)
941951
with open(filepath, 'w', encoding='utf-8') as f:
942952
f.write(content)
943953
return basename
@@ -1640,6 +1650,80 @@ def bootstrap():
16401650
self.assertIn(overlay, sys.path)
16411651
self.assertIn(pkgdir, sys.path)
16421652

1653+
# gh-149819
1654+
@unittest.skipUnless(site.ENABLE_USER_SITE, "requires user-site")
1655+
@support.requires_subprocess()
1656+
def test_pth_processed_when_sitedir_already_on_path(self):
1657+
# A .pth file in a site-packages directory must still be processed by
1658+
# site.main() when that directory is already on sys.path at
1659+
# interpreter start up, for example in a subprocess that inherits
1660+
# PYTHONPATH from its parent. Before the fix, main() seeded
1661+
# known_paths with all entries derived from removeduppaths(), and
1662+
# addsitedir() then skipped .pth processing for any directory already
1663+
# in known_paths.
1664+
user_base = self.tmpdir
1665+
user_site = site._get_path(user_base)
1666+
os.makedirs(user_site)
1667+
sentinel = "GH149819_PTH_RAN"
1668+
# Writing some text to stderr is the simplest observable side effect.
1669+
self._make_pth(f"""\
1670+
import sys; sys.stderr.write({sentinel!r}); sys.stderr.flush()
1671+
""",
1672+
name='gh149819',
1673+
basedir=user_site)
1674+
with EnvironmentVarGuard() as env:
1675+
# PYTHONUSERBASE points USER_SITE at our temp directory so
1676+
# site.main() will call addsitedir() on it, rather than on the
1677+
# host interpreter's real user-site.
1678+
env['PYTHONUSERBASE'] = user_base
1679+
# PYTHONPATH puts that same directory on sys.path before
1680+
# site.main() runs in the subprocess. This is what triggers the
1681+
# bug: removeduppaths() records it in known_paths, and the unfixed
1682+
# addsitedir() then skips .pth processing.
1683+
env['PYTHONPATH'] = user_site
1684+
result = subprocess.run(
1685+
[sys.executable, '-c', ''],
1686+
capture_output=True,
1687+
check=True,
1688+
)
1689+
self.assertIn(sentinel.encode(), result.stderr)
1690+
1691+
@unittest.skipUnless(site.ENABLE_USER_SITE, "requires user-site")
1692+
@support.requires_subprocess()
1693+
def test_start_processed_when_sitedir_already_on_path(self):
1694+
# Companion to test_pth_processed_when_sitedir_already_on_path:
1695+
# the same dedup-guard skip in addsitedir() suppressed both .pth
1696+
# and .start file processing, so verify .start entry points also
1697+
# run for a site-packages directory inherited via PYTHONPATH.
1698+
user_base = self.tmpdir
1699+
user_site = site._get_path(user_base)
1700+
os.makedirs(user_site)
1701+
sentinel = "GH149819_START_RAN"
1702+
# The .start entry point resolves to a callable, so we write a
1703+
# tiny importable module that outputs the sentinel text. It lands in
1704+
# <self.sitedir>/extdir. That path is added to PYTHONPATH below so
1705+
# the subprocess can import it.
1706+
extdir = self._make_mod(f"""\
1707+
import sys
1708+
def run():
1709+
sys.stderr.write({sentinel!r})
1710+
sys.stderr.flush()
1711+
""", name='gh149819mod')
1712+
self._make_start(
1713+
'gh149819mod:run\n', name='gh149819', basedir=user_site
1714+
)
1715+
with EnvironmentVarGuard() as env:
1716+
# See above for details.
1717+
env['PYTHONUSERBASE'] = user_base
1718+
env['PYTHONPATH'] = os.pathsep.join([user_site, extdir])
1719+
result = subprocess.run(
1720+
[sys.executable, '-c', ''],
1721+
capture_output=True,
1722+
check=True,
1723+
)
1724+
self.assertIn(sentinel.encode(), result.stderr)
1725+
1726+
16431727

16441728
if __name__ == "__main__":
16451729
unittest.main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix regression in :func:`site.addsitedir` where ``.pth`` files were no
2+
longer processed in Python subprocesses. This happened because
3+
:func:`site.main` seeded ``known_paths`` with entries inherited from
4+
the parent process, causing ``addsitedir`` to skip ``.pth`` processing.

0 commit comments

Comments
 (0)