Skip to content
Merged
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
30 changes: 30 additions & 0 deletions Doc/library/importlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 21 additions & 11 deletions Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,12 +1086,18 @@ def get_filename(self, fullname):
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
Expand Down Expand Up @@ -1153,7 +1159,7 @@ def __len__(self):
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()
Expand All @@ -1162,12 +1168,16 @@ def append(self, item):
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
Expand Down Expand Up @@ -1222,9 +1232,9 @@ def invalidate_caches():
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()
Expand Down Expand Up @@ -1310,7 +1320,7 @@ def find_spec(cls, fullname, path=None, target=None):
# 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
Expand Down
1 change: 1 addition & 0 deletions Lib/importlib/machinery.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ._bootstrap_external import ExtensionFileLoader
from ._bootstrap_external import AppleFrameworkLoader
from ._bootstrap_external import NamespaceLoader
from ._bootstrap_external import NamespacePath


def all_suffixes():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Publicly expose and document :class:`importlib.machinery.NamespacePath`.
Loading