GH-119668: expose importlib.machinery.NamespacePath (#119669)

* GH-119668: expose importlib.NamespacePath

Signed-off-by: Filipe Laíns <lains@riseup.net>

* add news

Signed-off-by: Filipe Laíns <lains@riseup.net>

* add to docs

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Apply suggestions from code review

Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>

* Fix news (importlib.NamespacePath > importlib.machinery.NamespacePath)

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Link to module.__path__ in NamespacePath docs

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Mention the path argument in the documentation

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Simplify docs text

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Highlight argument names in docs text

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Update Lib/importlib/_bootstrap_external.py

Co-authored-by: Brett Cannon <brett@python.org>

* Rewrite NamespacePath's doc

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Specify path_finder's type in the NamespacePath docstring

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Fix doc tests

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Apply suggestions from code review

Co-authored-by: Barry Warsaw <barry@python.org>

* Fix doc lint

Signed-off-by: Filipe Laíns <lains@riseup.net>

* Update Doc/library/importlib.rst

Co-authored-by: Brett Cannon <brett@python.org>

---------

Signed-off-by: Filipe Laíns <lains@riseup.net>
Co-authored-by: Brett Cannon <brett@python.org>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Barry Warsaw <barry@python.org>
This commit is contained in:
Filipe Laíns
2025-11-01 00:39:48 +00:00
committed by GitHub
parent a17c57eee5
commit ede5693be1
4 changed files with 53 additions and 11 deletions

View File

@@ -1013,6 +1013,36 @@ find and load modules.
:exc:`ImportError` is raised.
.. class:: NamespacePath(name, path, path_finder)
Represents a :term:`namespace package`'s path (:attr:`module.__path__`).
When its ``__path__`` value is accessed it will be recomputed if necessary.
This keeps it in-sync with the global state (:attr:`sys.modules`).
The *name* argument is the name of the namespace module.
The *path* argument is the initial path value.
The *path_finder* argument is the callable used to recompute the path value.
The callable has the same signature as :meth:`importlib.abc.MetaPathFinder.find_spec`.
When the parent's :attr:`module.__path__` attribute is updated, the path
value is recomputed.
If the parent module is missing from :data:`sys.modules`, then
:exc:`ModuleNotFoundError` will be raised.
For top-level modules, the parent module's path is :data:`sys.path`.
.. note::
:meth:`PathFinder.invalidate_caches` invalidates :class:`NamespacePath`,
forcing the path value to be recomputed next time it is accessed.
.. versionadded:: next
.. class:: SourceFileLoader(fullname, path)
A concrete implementation of :class:`importlib.abc.SourceLoader` by

View File

@@ -1086,12 +1086,18 @@ class ExtensionFileLoader(FileLoader, _LoaderBasics):
return self.path
class _NamespacePath:
"""Represents a namespace package's path. It uses the module name
to find its parent module, and from there it looks up the parent's
__path__. When this changes, the module's own path is recomputed,
using path_finder. For top-level modules, the parent module's path
is sys.path."""
class NamespacePath:
"""Represents a namespace package's path.
It uses the module *name* to find its parent module, and from there it looks
up the parent's __path__. When this changes, the module's own path is
recomputed, using *path_finder*. The initial value is set to *path*.
For top-level modules, the parent module's path is sys.path.
*path_finder* should be a callable with the same signature as
MetaPathFinder.find_spec((fullname, path, target=None) -> spec).
"""
# When invalidate_caches() is called, this epoch is incremented
# https://bugs.python.org/issue45703
@@ -1153,7 +1159,7 @@ class _NamespacePath:
return len(self._recalculate())
def __repr__(self):
return f'_NamespacePath({self._path!r})'
return f'NamespacePath({self._path!r})'
def __contains__(self, item):
return item in self._recalculate()
@@ -1162,12 +1168,16 @@ class _NamespacePath:
self._path.append(item)
# For backwards-compatibility for anyone desperate enough to get at the class back in the day.
_NamespacePath = NamespacePath
# This class is actually exposed publicly in a namespace package's __loader__
# attribute, so it should be available through a non-private name.
# https://github.com/python/cpython/issues/92054
class NamespaceLoader:
def __init__(self, name, path, path_finder):
self._path = _NamespacePath(name, path, path_finder)
self._path = NamespacePath(name, path, path_finder)
def is_package(self, fullname):
return True
@@ -1222,9 +1232,9 @@ class PathFinder:
del sys.path_importer_cache[name]
elif hasattr(finder, 'invalidate_caches'):
finder.invalidate_caches()
# Also invalidate the caches of _NamespacePaths
# Also invalidate the caches of NamespacePaths
# https://bugs.python.org/issue45703
_NamespacePath._epoch += 1
NamespacePath._epoch += 1
from importlib.metadata import MetadataPathFinder
MetadataPathFinder.invalidate_caches()
@@ -1310,7 +1320,7 @@ class PathFinder:
# We found at least one namespace path. Return a spec which
# can create the namespace package.
spec.origin = None
spec.submodule_search_locations = _NamespacePath(fullname, namespace_path, cls._get_spec)
spec.submodule_search_locations = NamespacePath(fullname, namespace_path, cls._get_spec)
return spec
else:
return None

View File

@@ -16,6 +16,7 @@ from ._bootstrap_external import SourcelessFileLoader
from ._bootstrap_external import ExtensionFileLoader
from ._bootstrap_external import AppleFrameworkLoader
from ._bootstrap_external import NamespaceLoader
from ._bootstrap_external import NamespacePath
def all_suffixes():

View File

@@ -0,0 +1 @@
Publicly expose and document :class:`importlib.machinery.NamespacePath`.