mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +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 (
|
||||
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
|
||||
# TypeError. That seems terrible.
|
||||
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:
|
||||
return None
|
||||
return reader(spec.name) # type: ignore
|
||||
return reader(spec.name) # type: ignore[union-attr]
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import itertools
|
||||
|
@ -6,6 +8,7 @@ import operator
|
|||
import re
|
||||
import warnings
|
||||
import zipfile
|
||||
from collections.abc import Iterator
|
||||
|
||||
from . import abc
|
||||
|
||||
|
@ -135,27 +138,31 @@ class NamespaceReader(abc.TraversableResources):
|
|||
def __init__(self, namespace_path):
|
||||
if 'NamespacePath' not in str(namespace_path):
|
||||
raise ValueError('Invalid path')
|
||||
self.path = MultiplexedPath(*map(self._resolve, namespace_path))
|
||||
self.path = MultiplexedPath(*filter(bool, map(self._resolve, namespace_path)))
|
||||
|
||||
@classmethod
|
||||
def _resolve(cls, path_str) -> abc.Traversable:
|
||||
def _resolve(cls, path_str) -> abc.Traversable | None:
|
||||
r"""
|
||||
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
|
||||
zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or
|
||||
``/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())
|
||||
return dir
|
||||
dirs = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())
|
||||
return next(dirs, None)
|
||||
|
||||
@classmethod
|
||||
def _candidate_paths(cls, path_str):
|
||||
def _candidate_paths(cls, path_str: str) -> Iterator[abc.Traversable]:
|
||||
yield pathlib.Path(path_str)
|
||||
yield from cls._resolve_zip_path(path_str)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_zip_path(path_str):
|
||||
def _resolve_zip_path(path_str: str):
|
||||
for match in reversed(list(re.finditer(r'[\\/]', path_str))):
|
||||
with contextlib.suppress(
|
||||
FileNotFoundError,
|
||||
|
|
|
@ -77,7 +77,7 @@ class ResourceHandle(Traversable):
|
|||
|
||||
def __init__(self, parent: ResourceContainer, name: str):
|
||||
self.parent = parent
|
||||
self.name = name # type: ignore
|
||||
self.name = name # type: ignore[misc]
|
||||
|
||||
def is_file(self):
|
||||
return True
|
||||
|
|
|
@ -2,15 +2,44 @@ import pathlib
|
|||
import functools
|
||||
|
||||
from typing import Dict, Union
|
||||
from typing import runtime_checkable
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
####
|
||||
# from jaraco.path 3.4.1
|
||||
|
||||
FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore
|
||||
# from jaraco.path 3.7.1
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
@ -25,21 +54,25 @@ def build(spec: FilesSpec, prefix=pathlib.Path()):
|
|||
... "__init__.py": "",
|
||||
... },
|
||||
... "baz.py": "# Some code",
|
||||
... }
|
||||
... "bar.py": Symlink("baz.py"),
|
||||
... },
|
||||
... "bing": Symlink("foo"),
|
||||
... }
|
||||
>>> target = getfixture('tmp_path')
|
||||
>>> build(spec, target)
|
||||
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
|
||||
'# Some code'
|
||||
>>> target.joinpath('bing/bar.py').read_text(encoding='utf-8')
|
||||
'# Some code'
|
||||
"""
|
||||
for name, contents in spec.items():
|
||||
create(contents, pathlib.Path(prefix) / name)
|
||||
create(contents, _ensure_tree_maker(prefix) / name)
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
def create(content: Union[str, bytes, FilesSpec], path):
|
||||
path.mkdir(exist_ok=True)
|
||||
build(content, prefix=path) # type: ignore
|
||||
build(content, prefix=path) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@create.register
|
||||
|
@ -52,5 +85,10 @@ def _(content: str, path):
|
|||
path.write_text(content, encoding='utf-8')
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: Symlink, path):
|
||||
path.symlink_to(content)
|
||||
|
||||
|
||||
# end from jaraco.path
|
||||
####
|
||||
|
|
|
@ -60,6 +60,26 @@ class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
|
|||
class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase):
|
||||
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):
|
||||
ZIP_MODULE = 'namespacedata01'
|
||||
|
@ -86,7 +106,7 @@ class ModulesFiles:
|
|||
"""
|
||||
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')
|
||||
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