@@ -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+
459460class 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
16441728if __name__ == "__main__" :
16451729 unittest .main ()
0 commit comments