Skip to content

Commit 201053d

Browse files
committed
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.
1 parent a07037d commit 201053d

1 file changed

Lines changed: 91 additions & 7 deletions

File tree

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()

0 commit comments

Comments
 (0)