Skip to content

Commit 261088d

Browse files
committed
Defer entry point syntax check to execution time
This way, we can use `pkgutil.resolve_name(..., strict=True)` and not have to duplicate regex patterns or checking logic, nor do we have to resolve the objects at parse time.
1 parent 9a017f2 commit 261088d

2 files changed

Lines changed: 57 additions & 15 deletions

File tree

Lib/site.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,9 @@ def _read_start_file(sitedir, name):
252252
line = line.strip()
253253
if len(line) == 0 or line.startswith("#"):
254254
continue
255-
256-
# Validate mandatory colon-form: pkg.mod:callable.
257-
if ':' not in line:
258-
_trace(f"In {filename!r}, line {n:d}: "
259-
f"skipping invalid entry point: {line}")
260-
continue
261-
255+
# Syntax validation is deferred to entry-point execution time,
256+
# where pkgutil.resolve_name(strict=True) enforces the
257+
# pkg.mod:callable form.
262258
entrypoints.append(line)
263259

264260

@@ -312,15 +308,25 @@ def _execute_start_entrypoints():
312308
313309
Called after all site-packages directories have been processed so that
314310
sys.path is fully populated before any entry point code runs. Uses
315-
pkgutil.resolve_name() for resolution. While that function accepts a
316-
looser constraint on the input string, we enforce the :callable syntax
317-
when the .start file is parsed.
311+
pkgutil.resolve_name(strict=True) which both validates the strict
312+
pkg.mod:callable form and resolves the entry point in one step.
318313
"""
319314
for filename, entrypoints in _pending_entrypoints.items():
320315
for entrypoint in entrypoints:
321316
try:
322317
_trace(f"Executing entry point: {entrypoint} from {filename}")
323-
callable_ = pkgutil.resolve_name(entrypoint)
318+
callable_ = pkgutil.resolve_name(entrypoint, strict=True)
319+
except ValueError as exc:
320+
_print_error(
321+
f"Invalid entry point syntax in {filename}: "
322+
f"{entrypoint!r}", exc)
323+
continue
324+
except Exception as exc:
325+
_print_error(
326+
f"Error resolving entry point {entrypoint} "
327+
f"from {filename}", exc)
328+
continue
329+
try:
324330
callable_()
325331
except Exception as exc:
326332
_print_error(

Lib/test/test_site.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -986,12 +986,28 @@ def test_read_start_file_comments_and_blanks(self):
986986
fullname = os.path.join(self.sitedir, 'foo.start')
987987
self.assertEqual(site._pending_entrypoints[fullname], ['os.path:join'])
988988

989-
def test_read_start_file_missing_colon_skipped(self):
990-
# Entry points without the mandatory colon are skipped.
991-
self._make_start("os.path\nos.path:join\n", name='foo')
989+
def test_read_start_file_accepts_all_non_blank_lines(self):
990+
# Syntax validation is deferred to entry-point execution time
991+
# (where pkgutil.resolve_name(strict=True) enforces the strict
992+
# pkg.mod:callable form), so parsing accepts every non-blank,
993+
# non-comment line, including syntactically invalid ones.
994+
content = (
995+
"os.path\n" # no colon
996+
"pkg.mod:\n" # empty callable
997+
":callable\n" # empty module
998+
"pkg.mod:callable:extra\n" # multiple colons
999+
"os.path:join\n" # valid
1000+
)
1001+
self._make_start(content, name='foo')
9921002
site._read_start_file(self.sitedir, 'foo.start')
9931003
fullname = os.path.join(self.sitedir, 'foo.start')
994-
self.assertEqual(site._pending_entrypoints[fullname], ['os.path:join'])
1004+
self.assertEqual(site._pending_entrypoints[fullname], [
1005+
'os.path',
1006+
'pkg.mod:',
1007+
':callable',
1008+
'pkg.mod:callable:extra',
1009+
'os.path:join',
1010+
])
9951011

9961012
def test_read_start_file_empty(self):
9971013
# PEP 829: an empty .start file is still registered as present
@@ -1118,6 +1134,26 @@ def test_execute_entrypoints_import_error(self):
11181134
self.assertIn('nosuchmodule_xyz', err.getvalue())
11191135
# os.path:join should still have been called (no exception for it)
11201136

1137+
def test_execute_entrypoints_strict_syntax_rejection(self):
1138+
# PEP 829: only the strict pkg.mod:callable form is valid.
1139+
# At entry-point execution, pkgutil.resolve_name(strict=True)
1140+
# raises ValueError for invalid syntax; the invalid entry is
1141+
# reported and execution continues with the next one.
1142+
fullname = os.path.join(self.sitedir, 'bad.start')
1143+
site._pending_entrypoints[fullname] = [
1144+
'os.path', # no colon
1145+
'pkg.mod:', # empty callable
1146+
':callable', # empty module
1147+
'pkg.mod:callable:extra', # multiple colons
1148+
]
1149+
with captured_stderr() as err:
1150+
site._execute_start_entrypoints()
1151+
out = err.getvalue()
1152+
self.assertIn('Invalid entry point syntax', out)
1153+
for bad in ('os.path', 'pkg.mod:', ':callable',
1154+
'pkg.mod:callable:extra'):
1155+
self.assertIn(bad, out)
1156+
11211157
def test_execute_entrypoints_callable_error(self):
11221158
# Callable that raises prints traceback but continues.
11231159
mod_dir = os.path.join(self.sitedir, 'badmod')

0 commit comments

Comments
 (0)