mirror of
https://github.com/python/cpython.git
synced 2025-08-04 08:59:19 +00:00
gh-123987: Fix NotADirectoryError in NamespaceReader when sentinel present (#124018)
This commit is contained in:
parent
fccbfc40b5
commit
b543b32eff
7 changed files with 93 additions and 18 deletions
|
@ -1,4 +1,11 @@
|
||||||
"""Read resources contained within a package."""
|
"""
|
||||||
|
Read resources contained within a package.
|
||||||
|
|
||||||
|
This codebase is shared between importlib.resources in the stdlib
|
||||||
|
and importlib_resources in PyPI. See
|
||||||
|
https://github.com/python/importlib_metadata/wiki/Development-Methodology
|
||||||
|
for more detail.
|
||||||
|
"""
|
||||||
|
|
||||||
from ._common import (
|
from ._common import (
|
||||||
as_file,
|
as_file,
|
||||||
|
|
|
@ -66,10 +66,10 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
|
||||||
# zipimport.zipimporter does not support weak references, resulting in a
|
# zipimport.zipimporter does not support weak references, resulting in a
|
||||||
# TypeError. That seems terrible.
|
# TypeError. That seems terrible.
|
||||||
spec = package.__spec__
|
spec = package.__spec__
|
||||||
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore
|
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore[union-attr]
|
||||||
if reader is None:
|
if reader is None:
|
||||||
return None
|
return None
|
||||||
return reader(spec.name) # type: ignore
|
return reader(spec.name) # type: ignore[union-attr]
|
||||||
|
|
||||||
|
|
||||||
@functools.singledispatch
|
@functools.singledispatch
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import itertools
|
import itertools
|
||||||
|
@ -6,6 +8,7 @@ import operator
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
from . import abc
|
from . import abc
|
||||||
|
|
||||||
|
@ -135,27 +138,31 @@ class NamespaceReader(abc.TraversableResources):
|
||||||
def __init__(self, namespace_path):
|
def __init__(self, namespace_path):
|
||||||
if 'NamespacePath' not in str(namespace_path):
|
if 'NamespacePath' not in str(namespace_path):
|
||||||
raise ValueError('Invalid path')
|
raise ValueError('Invalid path')
|
||||||
self.path = MultiplexedPath(*map(self._resolve, namespace_path))
|
self.path = MultiplexedPath(*filter(bool, map(self._resolve, namespace_path)))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _resolve(cls, path_str) -> abc.Traversable:
|
def _resolve(cls, path_str) -> abc.Traversable | None:
|
||||||
r"""
|
r"""
|
||||||
Given an item from a namespace path, resolve it to a Traversable.
|
Given an item from a namespace path, resolve it to a Traversable.
|
||||||
|
|
||||||
path_str might be a directory on the filesystem or a path to a
|
path_str might be a directory on the filesystem or a path to a
|
||||||
zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or
|
zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or
|
||||||
``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``.
|
``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``.
|
||||||
|
|
||||||
|
path_str might also be a sentinel used by editable packages to
|
||||||
|
trigger other behaviors (see python/importlib_resources#311).
|
||||||
|
In that case, return None.
|
||||||
"""
|
"""
|
||||||
(dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())
|
dirs = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())
|
||||||
return dir
|
return next(dirs, None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _candidate_paths(cls, path_str):
|
def _candidate_paths(cls, path_str: str) -> Iterator[abc.Traversable]:
|
||||||
yield pathlib.Path(path_str)
|
yield pathlib.Path(path_str)
|
||||||
yield from cls._resolve_zip_path(path_str)
|
yield from cls._resolve_zip_path(path_str)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_zip_path(path_str):
|
def _resolve_zip_path(path_str: str):
|
||||||
for match in reversed(list(re.finditer(r'[\\/]', path_str))):
|
for match in reversed(list(re.finditer(r'[\\/]', path_str))):
|
||||||
with contextlib.suppress(
|
with contextlib.suppress(
|
||||||
FileNotFoundError,
|
FileNotFoundError,
|
||||||
|
|
|
@ -77,7 +77,7 @@ class ResourceHandle(Traversable):
|
||||||
|
|
||||||
def __init__(self, parent: ResourceContainer, name: str):
|
def __init__(self, parent: ResourceContainer, name: str):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.name = name # type: ignore
|
self.name = name # type: ignore[misc]
|
||||||
|
|
||||||
def is_file(self):
|
def is_file(self):
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -2,15 +2,44 @@ import pathlib
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union
|
||||||
|
from typing import runtime_checkable
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
|
||||||
####
|
####
|
||||||
# from jaraco.path 3.4.1
|
# from jaraco.path 3.7.1
|
||||||
|
|
||||||
FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def build(spec: FilesSpec, prefix=pathlib.Path()):
|
class Symlink(str):
|
||||||
|
"""
|
||||||
|
A string indicating the target of a symlink.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']]
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class TreeMaker(Protocol):
|
||||||
|
def __truediv__(self, *args, **kwargs): ... # pragma: no cover
|
||||||
|
|
||||||
|
def mkdir(self, **kwargs): ... # pragma: no cover
|
||||||
|
|
||||||
|
def write_text(self, content, **kwargs): ... # pragma: no cover
|
||||||
|
|
||||||
|
def write_bytes(self, content): ... # pragma: no cover
|
||||||
|
|
||||||
|
def symlink_to(self, target): ... # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
|
||||||
|
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
|
def build(
|
||||||
|
spec: FilesSpec,
|
||||||
|
prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment]
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Build a set of files/directories, as described by the spec.
|
Build a set of files/directories, as described by the spec.
|
||||||
|
|
||||||
|
@ -25,21 +54,25 @@ def build(spec: FilesSpec, prefix=pathlib.Path()):
|
||||||
... "__init__.py": "",
|
... "__init__.py": "",
|
||||||
... },
|
... },
|
||||||
... "baz.py": "# Some code",
|
... "baz.py": "# Some code",
|
||||||
... }
|
... "bar.py": Symlink("baz.py"),
|
||||||
|
... },
|
||||||
|
... "bing": Symlink("foo"),
|
||||||
... }
|
... }
|
||||||
>>> target = getfixture('tmp_path')
|
>>> target = getfixture('tmp_path')
|
||||||
>>> build(spec, target)
|
>>> build(spec, target)
|
||||||
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
|
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
|
||||||
'# Some code'
|
'# Some code'
|
||||||
|
>>> target.joinpath('bing/bar.py').read_text(encoding='utf-8')
|
||||||
|
'# Some code'
|
||||||
"""
|
"""
|
||||||
for name, contents in spec.items():
|
for name, contents in spec.items():
|
||||||
create(contents, pathlib.Path(prefix) / name)
|
create(contents, _ensure_tree_maker(prefix) / name)
|
||||||
|
|
||||||
|
|
||||||
@functools.singledispatch
|
@functools.singledispatch
|
||||||
def create(content: Union[str, bytes, FilesSpec], path):
|
def create(content: Union[str, bytes, FilesSpec], path):
|
||||||
path.mkdir(exist_ok=True)
|
path.mkdir(exist_ok=True)
|
||||||
build(content, prefix=path) # type: ignore
|
build(content, prefix=path) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
@create.register
|
@create.register
|
||||||
|
@ -52,5 +85,10 @@ def _(content: str, path):
|
||||||
path.write_text(content, encoding='utf-8')
|
path.write_text(content, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@create.register
|
||||||
|
def _(content: Symlink, path):
|
||||||
|
path.symlink_to(content)
|
||||||
|
|
||||||
|
|
||||||
# end from jaraco.path
|
# end from jaraco.path
|
||||||
####
|
####
|
||||||
|
|
|
@ -60,6 +60,26 @@ class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
|
||||||
class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase):
|
class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase):
|
||||||
MODULE = 'namespacedata01'
|
MODULE = 'namespacedata01'
|
||||||
|
|
||||||
|
def test_non_paths_in_dunder_path(self):
|
||||||
|
"""
|
||||||
|
Non-path items in a namespace package's ``__path__`` are ignored.
|
||||||
|
|
||||||
|
As reported in python/importlib_resources#311, some tools
|
||||||
|
like Setuptools, when creating editable packages, will inject
|
||||||
|
non-paths into a namespace package's ``__path__``, a
|
||||||
|
sentinel like
|
||||||
|
``__editable__.sample_namespace-1.0.finder.__path_hook__``
|
||||||
|
to cause the ``PathEntryFinder`` to be called when searching
|
||||||
|
for packages. In that case, resources should still be loadable.
|
||||||
|
"""
|
||||||
|
import namespacedata01
|
||||||
|
|
||||||
|
namespacedata01.__path__.append(
|
||||||
|
'__editable__.sample_namespace-1.0.finder.__path_hook__'
|
||||||
|
)
|
||||||
|
|
||||||
|
resources.files(namespacedata01)
|
||||||
|
|
||||||
|
|
||||||
class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
|
class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
|
||||||
ZIP_MODULE = 'namespacedata01'
|
ZIP_MODULE = 'namespacedata01'
|
||||||
|
@ -86,7 +106,7 @@ class ModulesFiles:
|
||||||
"""
|
"""
|
||||||
A module can have resources found adjacent to the module.
|
A module can have resources found adjacent to the module.
|
||||||
"""
|
"""
|
||||||
import mod
|
import mod # type: ignore[import-not-found]
|
||||||
|
|
||||||
actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8')
|
actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8')
|
||||||
assert actual == self.spec['res.txt']
|
assert actual == self.spec['res.txt']
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Fixed issue in NamespaceReader where a non-path item in a namespace path,
|
||||||
|
such as a sentinel added by an editable installer, would break resource
|
||||||
|
loading.
|
Loading…
Add table
Add a link
Reference in a new issue