Skip to content

Commit 8e4ad95

Browse files
committed
gh-149504: Fix reentrant site startup processing
Copy and clear pending startup file data before executing import lines or .start entry points so recursive site.addsitedir() calls process a separate batch.
1 parent f5c7535 commit 8e4ad95

3 files changed

Lines changed: 59 additions & 13 deletions

File tree

Lib/site.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ def _init_pathinfo():
163163
_pending_importexecs = {}
164164

165165

166+
def _take_pending(mapping):
167+
"""Return the pending data and clear it before running startup code."""
168+
pending = mapping.copy()
169+
mapping.clear()
170+
return pending
171+
172+
166173
def _read_pthstart_file(sitedir, name, suffix):
167174
"""Parse a .start or .pth file and return (lines, filename).
168175
@@ -280,11 +287,14 @@ def _read_start_file(sitedir, name):
280287
entrypoints.append(line)
281288

282289

283-
def _extend_syspath():
290+
def _extend_syspath(pending_syspaths=None):
284291
# We've already filtered out duplicates, either in the existing sys.path
285292
# or in all the .pth files we've seen. We've also abspath/normpath'd all
286293
# the entries, so all that's left to do is to ensure that the path exists.
287-
for filename, dirs in _pending_syspaths.items():
294+
if pending_syspaths is None:
295+
pending_syspaths = _pending_syspaths
296+
297+
for filename, dirs in pending_syspaths.items():
288298
for dir_ in dirs:
289299
if os.path.exists(dir_):
290300
_trace(f"Extending sys.path with {dir_} from {filename}")
@@ -295,16 +305,21 @@ def _extend_syspath():
295305
f"skipping sys.path append")
296306

297307

298-
def _exec_imports():
308+
def _exec_imports(pending_importexecs=None, pending_entrypoints=None):
299309
# For all the `import` lines we've seen in .pth files, exec() them in
300310
# order. However, if they come from a file with a matching .start, then
301311
# we ignore these import lines. For the ones we do process, print a
302312
# warning but only when -v was given.
303-
for filename, imports in _pending_importexecs.items():
313+
if pending_importexecs is None:
314+
pending_importexecs = _pending_importexecs
315+
if pending_entrypoints is None:
316+
pending_entrypoints = _pending_entrypoints
317+
318+
for filename, imports in pending_importexecs.items():
304319
name, dot, pth = filename.rpartition(".")
305320
assert dot == "." and pth == "pth", f"Bad startup filename: {filename}"
306321

307-
if f"{name}.start" in _pending_entrypoints:
322+
if f"{name}.start" in pending_entrypoints:
308323
# Skip import lines in favor of entry points.
309324
continue
310325

@@ -322,15 +337,18 @@ def _exec_imports():
322337
f"Error in import line from {filename}: {line}", exc)
323338

324339

325-
def _execute_start_entrypoints():
340+
def _execute_start_entrypoints(pending_entrypoints=None):
326341
"""Execute all accumulated .start file entry points.
327342
328343
Called after all site-packages directories have been processed so that
329344
sys.path is fully populated before any entry point code runs. Uses
330345
pkgutil.resolve_name(strict=True) which both validates the strict
331346
pkg.mod:callable form and resolves the entry point in one step.
332347
"""
333-
for filename, entrypoints in _pending_entrypoints.items():
348+
if pending_entrypoints is None:
349+
pending_entrypoints = _pending_entrypoints
350+
351+
for filename, entrypoints in pending_entrypoints.items():
334352
for entrypoint in entrypoints:
335353
try:
336354
_trace(f"Executing entry point: {entrypoint} from {filename}")
@@ -355,12 +373,15 @@ def _execute_start_entrypoints():
355373

356374
def process_startup_files():
357375
"""Flush all pending sys.path and entry points."""
358-
_extend_syspath()
359-
_exec_imports()
360-
_execute_start_entrypoints()
361-
_pending_syspaths.clear()
362-
_pending_importexecs.clear()
363-
_pending_entrypoints.clear()
376+
# Startup code may call addsitedir(), so remove this batch from the
377+
# globals before executing any import lines or entry points.
378+
pending_syspaths = _take_pending(_pending_syspaths)
379+
pending_importexecs = _take_pending(_pending_importexecs)
380+
pending_entrypoints = _take_pending(_pending_entrypoints)
381+
382+
_extend_syspath(pending_syspaths)
383+
_exec_imports(pending_importexecs, pending_entrypoints)
384+
_execute_start_entrypoints(pending_entrypoints)
364385

365386

366387
def addpackage(sitedir, name, known_paths):

Lib/test/test_site.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,29 @@ def startup():
12971297
import epmod
12981298
self.assertFalse(epmod.called)
12991299

1300+
def test_exec_imports_allows_reentrant_addsitedir(self):
1301+
nested = os.path.join(self.sitedir, 'nested')
1302+
nestedlib = os.path.join(nested, 'nestedlib')
1303+
os.mkdir(nested)
1304+
os.mkdir(nestedlib)
1305+
with open(os.path.join(nested, 'nested.pth'), 'w',
1306+
encoding='utf-8') as f:
1307+
f.write("nestedlib\n")
1308+
with open(os.path.join(nestedlib, 'nestedmod.py'), 'w',
1309+
encoding='utf-8') as f:
1310+
f.write("value = 42\n")
1311+
self.addCleanup(sys.modules.pop, 'nestedmod', None)
1312+
1313+
outer_pth = os.path.join(self.sitedir, 'outer.pth')
1314+
site._pending_importexecs[outer_pth] = [
1315+
f"import site; site.addsitedir({nested!r}); import nestedmod"
1316+
]
1317+
1318+
site.process_startup_files()
1319+
1320+
self.assertIn(nestedlib, sys.path)
1321+
self.assertEqual(sys.modules['nestedmod'].value, 42)
1322+
13001323
# --- _extend_syspath tests ---
13011324

13021325
def test_extend_syspath_existing_dir(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix reentrant processing of site startup files when a ``.pth`` import line
2+
calls :func:`site.addsitedir`.

0 commit comments

Comments
 (0)