Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions Lib/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ def _init_pathinfo():
_pending_importexecs = {}


def _take_pending(mapping):
"""Return the pending data and clear it before running startup code."""
pending = mapping.copy()
mapping.clear()
return pending


def _read_pthstart_file(sitedir, name, suffix):
"""Parse a .start or .pth file and return (lines, filename).

Expand Down Expand Up @@ -280,11 +287,14 @@ def _read_start_file(sitedir, name):
entrypoints.append(line)


def _extend_syspath():
def _extend_syspath(pending_syspaths=None):
# We've already filtered out duplicates, either in the existing sys.path
# or in all the .pth files we've seen. We've also abspath/normpath'd all
# the entries, so all that's left to do is to ensure that the path exists.
for filename, dirs in _pending_syspaths.items():
if pending_syspaths is None:
pending_syspaths = _pending_syspaths

for filename, dirs in pending_syspaths.items():
for dir_ in dirs:
if os.path.exists(dir_):
_trace(f"Extending sys.path with {dir_} from {filename}")
Expand All @@ -295,16 +305,21 @@ def _extend_syspath():
f"skipping sys.path append")


def _exec_imports():
def _exec_imports(pending_importexecs=None, pending_entrypoints=None):
# For all the `import` lines we've seen in .pth files, exec() them in
# order. However, if they come from a file with a matching .start, then
# we ignore these import lines. For the ones we do process, print a
# warning but only when -v was given.
for filename, imports in _pending_importexecs.items():
if pending_importexecs is None:
pending_importexecs = _pending_importexecs
if pending_entrypoints is None:
pending_entrypoints = _pending_entrypoints

for filename, imports in pending_importexecs.items():
name, dot, pth = filename.rpartition(".")
assert dot == "." and pth == "pth", f"Bad startup filename: {filename}"

if f"{name}.start" in _pending_entrypoints:
if f"{name}.start" in pending_entrypoints:
# Skip import lines in favor of entry points.
continue

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


def _execute_start_entrypoints():
def _execute_start_entrypoints(pending_entrypoints=None):
"""Execute all accumulated .start file entry points.

Called after all site-packages directories have been processed so that
sys.path is fully populated before any entry point code runs. Uses
pkgutil.resolve_name(strict=True) which both validates the strict
pkg.mod:callable form and resolves the entry point in one step.
"""
for filename, entrypoints in _pending_entrypoints.items():
if pending_entrypoints is None:
pending_entrypoints = _pending_entrypoints

for filename, entrypoints in pending_entrypoints.items():
for entrypoint in entrypoints:
try:
_trace(f"Executing entry point: {entrypoint} from {filename}")
Expand All @@ -355,12 +373,15 @@ def _execute_start_entrypoints():

def process_startup_files():
"""Flush all pending sys.path and entry points."""
_extend_syspath()
_exec_imports()
_execute_start_entrypoints()
_pending_syspaths.clear()
_pending_importexecs.clear()
_pending_entrypoints.clear()
# Startup code may call addsitedir(), so remove this batch from the
# globals before executing any import lines or entry points.
pending_syspaths = _take_pending(_pending_syspaths)
pending_importexecs = _take_pending(_pending_importexecs)
pending_entrypoints = _take_pending(_pending_entrypoints)

_extend_syspath(pending_syspaths)
_exec_imports(pending_importexecs, pending_entrypoints)
_execute_start_entrypoints(pending_entrypoints)


def addpackage(sitedir, name, known_paths):
Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,29 @@ def startup():
import epmod
self.assertFalse(epmod.called)

def test_exec_imports_allows_reentrant_addsitedir(self):
nested = os.path.join(self.sitedir, 'nested')
nestedlib = os.path.join(nested, 'nestedlib')
os.mkdir(nested)
os.mkdir(nestedlib)
with open(os.path.join(nested, 'nested.pth'), 'w',
encoding='utf-8') as f:
f.write("nestedlib\n")
with open(os.path.join(nestedlib, 'nestedmod.py'), 'w',
encoding='utf-8') as f:
f.write("value = 42\n")
self.addCleanup(sys.modules.pop, 'nestedmod', None)

outer_pth = os.path.join(self.sitedir, 'outer.pth')
site._pending_importexecs[outer_pth] = [
f"import site; site.addsitedir({nested!r}); import nestedmod"
]

site.process_startup_files()

self.assertIn(nestedlib, sys.path)
self.assertEqual(sys.modules['nestedmod'].value, 42)

# --- _extend_syspath tests ---

def test_extend_syspath_existing_dir(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix reentrant processing of site startup files when a ``.pth`` import line
calls :func:`site.addsitedir`.
Loading