Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f05e9f6
Prototype of package.site.toml files
warsaw Apr 1, 2026
1f697e6
Merge branch 'main' into pth2toml
warsaw Apr 1, 2026
594f347
Validate [metadata].schema_version
warsaw Apr 1, 2026
ca6e4ed
Added PEP 829 draft
warsaw Apr 1, 2026
508f493
Update and refine PEP 829
warsaw Apr 1, 2026
72b4d9a
Merge branch 'main' into pth2toml
warsaw Apr 14, 2026
390de9f
Checkpointing reference implementation updates
warsaw Apr 15, 2026
8454716
Fix phase ordering
warsaw Apr 15, 2026
3e99f25
Fix docs and tests for PEP 829
warsaw Apr 15, 2026
e1eaffb
Trim the out-of-date site.py docstring
warsaw Apr 15, 2026
caab85c
Fix some tests
warsaw Apr 15, 2026
e393af0
A couple more improvements
warsaw Apr 15, 2026
bc92325
Merge branch 'main' into pth2toml
warsaw Apr 15, 2026
9b0b977
Remove unused import
warsaw Apr 16, 2026
1804f68
Blurb It
warsaw Apr 16, 2026
1bc0ac4
Merge branch 'main' into pth2toml
warsaw Apr 16, 2026
cdcd9cd
Add a migration guide for pth->start files
warsaw Apr 16, 2026
88465c1
Merge branch 'main' into pth2toml
warsaw Apr 16, 2026
d969ae6
Update site.rst documentation
warsaw Apr 17, 2026
d5eb74d
Merge branch 'python:main' into pth2toml
warsaw Apr 23, 2026
981d42b
Merge branch 'pth2toml' into warsaw/829
warsaw Apr 27, 2026
6eb25da
Implement PEP 829 - startup configuration files
warsaw Apr 28, 2026
77e73c2
Add comment for the future
warsaw Apr 28, 2026
c6c4252
Merge branch 'main' into warsaw/829
warsaw Apr 28, 2026
e3be2af
Remove accidental PEP file
warsaw Apr 28, 2026
95f5f68
Fix empty .start file logic bug
warsaw Apr 28, 2026
b36a16a
Fix another test: line numbers are suppressed
warsaw Apr 28, 2026
9a017f2
Add resolve_name(name, *, strict=False)
warsaw Apr 28, 2026
261088d
Defer entry point syntax check to execution time
warsaw Apr 28, 2026
ab8f926
Add optional exc=None to _trace()
warsaw Apr 28, 2026
27cb819
main() should use the flush_pth_start() helper
warsaw Apr 28, 2026
cc61acd
Improve some documentation wording
warsaw Apr 28, 2026
6e34cbc
Add a test to ensure site.addsitedir() works end-to-end
warsaw Apr 28, 2026
133b1a5
Add tests to validate the encoding constraints for .start and .pth files
warsaw Apr 28, 2026
eb6ddef
Add tests to ensure error verbosity with and without -v
warsaw Apr 28, 2026
cb61374
Split the blurb entry into site.py and pkgutil.resolve_name() descrip…
warsaw Apr 28, 2026
9d37c8a
Respond to PR review:
warsaw Apr 28, 2026
3a8db6d
Remove unused cross-reference anchor
warsaw Apr 28, 2026
30f38b0
Two more review fixes
warsaw Apr 28, 2026
618af20
List PEP 829 in whatsnew for 3.15
warsaw Apr 28, 2026
9007fa2
Merge branch 'main' into warsaw/829
warsaw Apr 28, 2026
d32a82d
Inline future deprecation warnings
warsaw Apr 29, 2026
f85e959
Merge branch 'main' into warsaw/829
warsaw Apr 29, 2026
ccb75a4
Merge branch 'main' into warsaw/829
warsaw Apr 29, 2026
760fb81
Use of "i.e." is discouraged
warsaw Apr 29, 2026
b7b22d6
Merge branch 'main' into warsaw/829
warsaw Apr 30, 2026
9292522
Merge branch 'main' into warsaw/829
warsaw Apr 30, 2026
e3ef09b
In version-related markup, use `next` instead of `3.15`
warsaw Apr 30, 2026
35ca4ac
Update Doc/library/site.rst
warsaw May 1, 2026
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
Prev Previous commit
Next Next commit
Add resolve_name(name, *, strict=False)
The PEP describes this as an open issue, but it's the cleanest approach.
  • Loading branch information
warsaw committed Apr 28, 2026
commit 9a017f212dfa2beffcc44cf6e13137d0c451d849
12 changes: 11 additions & 1 deletion Doc/library/pkgutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ support.
The :mod:`importlib.resources` module provides structured access to
module resources.

.. function:: resolve_name(name)
.. function:: resolve_name(name, *, strict=False)

Resolve a name to an object.

Expand All @@ -208,6 +208,7 @@ support.

* ``W(.W)*``
* ``W(.W)*:(W(.W)*)?``
* ``W(.W)*:(W(.W)*)``

The first form is intended for backward compatibility only. It assumes that
some part of the dotted name is a package, and the rest is an object
Expand All @@ -222,6 +223,11 @@ support.
hierarchy within that package. Only one import is needed in this form. If
it ends with the colon, then a module object is returned.

The first two forms are accepted when ``strict=False`` (the default).

The third form requires both the module name and callable, separated by
a colon. Only this form is accepted when ``strict=True``.

The function will return an object (which might be a module), or raise one
of the following exceptions:

Expand All @@ -233,3 +239,7 @@ support.
hierarchy within the imported package to get to the desired object.

.. versionadded:: 3.9

.. versionchanged:: 3.15

The optional keyword-only ``strict`` flag was added.
44 changes: 31 additions & 13 deletions Lib/pkgutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import os.path
import sys

lazy import re


__all__ = [
'get_importer', 'iter_importers',
'walk_packages', 'iter_modules', 'get_data',
Expand Down Expand Up @@ -398,9 +401,10 @@ def get_data(package, resource):
return loader.get_data(resource_name)


_NAME_PATTERN = None
_LENIENT_PATTERN = None
_STRICT_PATTERN = None

def resolve_name(name):
def resolve_name(name, *, strict=False):
"""
Resolve a name to an object.

Expand All @@ -410,6 +414,7 @@ def resolve_name(name):

W(.W)*
W(.W)*:(W(.W)*)?
W(.W)*:(W(.W)*)

The first form is intended for backward compatibility only. It assumes that
some part of the dotted name is a package, and the rest is an object
Expand All @@ -424,6 +429,11 @@ def resolve_name(name):
hierarchy within that package. Only one import is needed in this form. If
it ends with the colon, then a module object is returned.

The first two forms are accepted when `strict=False` (the default).

The third form requires both the module name and callable, separated by
a colon. Only this form is accepted when `strict=True`.

The function will return an object (which might be a module), or raise one
of the following exceptions:

Expand All @@ -432,18 +442,26 @@ def resolve_name(name):
AttributeError - if a failure occurred when traversing the object hierarchy
within the imported package to get to the desired object.
"""
global _NAME_PATTERN
if _NAME_PATTERN is None:
# Lazy import to speedup Python startup time
import re
dotted_words = r'(?!\d)(\w+)(\.(?!\d)(\w+))*'
_NAME_PATTERN = re.compile(f'^(?P<pkg>{dotted_words})'
f'(?P<cln>:(?P<obj>{dotted_words})?)?$',
re.UNICODE)

m = _NAME_PATTERN.match(name)
if not m:
global _LENIENT_PATTERN, _STRICT_PATTERN
dotted_words = r'(?!\d)(\w+)(\.(?!\d)(\w+))*'
if strict:
if _STRICT_PATTERN is None:
_STRICT_PATTERN = re.compile(
f'^(?P<pkg>{dotted_words})'
f'(?P<cln>:(?P<obj>{dotted_words}))$',
re.UNICODE)
pattern = _STRICT_PATTERN
else:
if _LENIENT_PATTERN is None:
_LENIENT_PATTERN = re.compile(
f'^(?P<pkg>{dotted_words})'
f'(?P<cln>:(?P<obj>{dotted_words})?)?$',
re.UNICODE)
pattern = _LENIENT_PATTERN

if (m := pattern.match(name)) is None:
raise ValueError(f'invalid format: {name!r}')

gd = m.groupdict()
if gd.get('cln'):
# there is a colon - a one-step import is all that's needed
Expand Down
50 changes: 50 additions & 0 deletions Lib/test/test_pkgutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,56 @@ def test_name_resolution(self):
with self.assertRaises(exc):
pkgutil.resolve_name(s)

def test_name_resolution_strict(self):
# PEP 829: strict=True accepts only the pkg.mod:callable form
# (W(.W)*:W(.W)*) -- both the colon and the callable are required.
import logging
import logging.handlers
Comment on lines +328 to +329
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason these aren't lazy imports instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably just turn all instances of this in the file into module scope imports. I don't think they need to be lazy since it's just a test.


success_cases = (
('os.path:pathsep', os.path.pathsep),
('logging.handlers:SysLogHandler',
logging.handlers.SysLogHandler),
('logging.handlers:SysLogHandler.LOG_ALERT',
logging.handlers.SysLogHandler.LOG_ALERT),
('builtins:int', int),
('builtins:int.from_bytes', int.from_bytes),
('os:path', os.path),
)

# All of these are accepted under strict=False but must be
# rejected under strict=True.
failure_cases = (
'os', # no colon (non-strict form)
'os.path', # no colon
'logging:', # colon, empty callable
'os.foo:', # colon, empty callable
':int', # empty package
'os.path:join:extra', # extra colon
'os.path.9abc:join', # invalid identifier in package
'os.path:9abc', # invalid identifier in callable
'', # empty
'?abc:foo', # invalid character
)

for s, expected in success_cases:
with self.subTest(s=s):
self.assertEqual(
pkgutil.resolve_name(s, strict=True), expected)

for s in failure_cases:
with self.subTest(s=s):
with self.assertRaises(ValueError):
pkgutil.resolve_name(s, strict=True)

# Cache independence: a strict=True call must not poison
# strict=False (and vice versa). Exercise both orderings.
self.assertEqual(
pkgutil.resolve_name('os:path', strict=True), os.path)
self.assertEqual(pkgutil.resolve_name('os.path'), os.path)
self.assertEqual(
pkgutil.resolve_name('os:path', strict=True), os.path)

def test_name_resolution_import_rebinding(self):
# The same data is also used for testing import in test_import and
# mock.patch in test_unittest.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
:pep:`829` (package startup configuration files) implements a new format
``<name>.start`` parallel to ``<name>.pth`` files, to replace ``import``
lines in the latter.

:func:`pkgutil.resolve_name` gets a new optional, keyword-only argument called
``strict``. The default is ``False`` for backward compatibility.