diff --git a/Lib/site.py b/Lib/site.py index 52dd9648734c3e..2142495a887b75 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -390,36 +390,36 @@ def addsitedir(sitedir, known_paths=None, *, defer_processing_start_files=False) if not sitedircase in known_paths: sys.path.append(sitedir) # Add path component known_paths.add(sitedircase) - try: - names = os.listdir(sitedir) - except OSError: - return + try: + names = os.listdir(sitedir) + except OSError: + return - # The following phases are defined by PEP 829. - # Phases 1-3: Read .pth files, accumulating paths and import lines. - pth_names = sorted( - name for name in names - if name.endswith(".pth") and not name.startswith(".") - ) - for name in pth_names: - _read_pth_file(sitedir, name, known_paths) - - # Phases 6-7: Discover .start files and accumulate their entry points. - # Import lines from .pth files with a matching .start file are discarded - # at flush time by _exec_imports(). - start_names = sorted( - name for name in names - if name.endswith(".start") and not name.startswith(".") - ) - for name in start_names: - _read_start_file(sitedir, name) - - # Generally, when addsitedir() is called explicitly, we'll want to process - # all the startup file data immediately. However, when called through - # main(), we'll want to batch up all the startup file processing. main() - # will set this flag to True to defer processing. - if not defer_processing_start_files: - process_startup_files() + # The following phases are defined by PEP 829. + # Phases 1-3: Read .pth files, accumulating paths and import lines. + pth_names = sorted( + name for name in names + if name.endswith(".pth") and not name.startswith(".") + ) + for name in pth_names: + _read_pth_file(sitedir, name, known_paths) + + # Phases 6-7: Discover .start files and accumulate their entry points. + # Import lines from .pth files with a matching .start file are discarded + # at flush time by _exec_imports(). + start_names = sorted( + name for name in names + if name.endswith(".start") and not name.startswith(".") + ) + for name in start_names: + _read_start_file(sitedir, name) + + # Generally, when addsitedir() is called explicitly, we'll want to process + # all the startup file data immediately. However, when called through + # main(), we'll want to batch up all the startup file processing. main() + # will set this flag to True to defer processing. + if not defer_processing_start_files: + process_startup_files() if reset: known_paths = None diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py index ac69e2cbdbbe54..04e7aff209d453 100644 --- a/Lib/test/test_site.py +++ b/Lib/test/test_site.py @@ -118,6 +118,7 @@ def pth_file_tests(self, pth_file): "%s not in sys.modules" % pth_file.imported) self.assertIn(site.makepath(pth_file.good_dir_path)[0], sys.path) self.assertFalse(os.path.exists(pth_file.bad_dir_path)) + self.assertFalse(os.path.exists(pth_file.idempotent_fail_path)) def test_addpackage(self): # Make sure addpackage() imports if the line starts with 'import', @@ -202,6 +203,16 @@ def test_addsitedir_explicit_flush(self): site.process_startup_files() self.pth_file_tests(pth_file) + def test_addsitedir_idempotent(self): + pth_file = PthFile() + pth_file.cleanup(prep=True) + + with pth_file.create(): + dirs = set() + dirs = site.addsitedir(pth_file.base_dir, dirs) + dirs = site.addsitedir(pth_file.base_dir, dirs) + self.pth_file_tests(pth_file) + def test_addsitedir_dotfile(self): pth_file = PthFile('.dotfile') # Ensure we have a clean slate. @@ -411,6 +422,7 @@ def __init__(self, filename_base=TESTFN, imported="time", self.bad_dirname = bad_dirname self.good_dir_path = os.path.join(self.base_dir, self.good_dirname) self.bad_dir_path = os.path.join(self.base_dir, self.bad_dirname) + self.idempotent_fail_path = os.path.join(self.base_dir, 'idempotent') @contextlib.contextmanager def create(self): @@ -427,6 +439,13 @@ def create(self): try: print("#import @bad module name", file=FILE) print("\n", file=FILE) + + PROG = f'''\ +if {self.imported!r} in sys.modules: + open({self.idempotent_fail_path!r}, 'a+').close() +''' + print(f"import sys; exec({PROG!r})", file=FILE) + print("import %s" % self.imported, file=FILE) print(self.good_dirname, file=FILE) print(self.bad_dirname, file=FILE) @@ -455,6 +474,8 @@ def cleanup(self, prep=False): os.rmdir(self.good_dir_path) if os.path.exists(self.bad_dir_path): os.rmdir(self.bad_dir_path) + if os.path.exists(self.idempotent_fail_path): + os.remove(self.idempotent_fail_path) class ImportSideEffectTests(unittest.TestCase): """Test side-effects from importing 'site'.""" diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst new file mode 100644 index 00000000000000..596ca89958c9ed --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst @@ -0,0 +1 @@ +Avoid re-executing ``.pth`` files when :func:`site.addsitedir` is called for a known directory.