From 47d73b1e7787cd66ee57be676f2385d2183f78ac Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 11:49:50 -0400 Subject: [PATCH 1/2] Add test capturing failure when resolving the MultiplexedPath for a namespace package with non-path elements in the path. Ref #311 --- importlib_resources/tests/test_files.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 311581c..6fdcdef 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -8,6 +8,8 @@ import importlib import contextlib +import pytest + import importlib_resources as resources from ..abc import Traversable from . import util @@ -60,6 +62,27 @@ class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase): MODULE = 'namespacedata01' + @pytest.mark.xfail(reason="#311") + 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' From 2c145c5b1ff95290794b2cb63e5c924e1847456d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 12:11:53 -0400 Subject: [PATCH 2/2] Omit sentinel values from a namespace path. When editable installs create sentinels, as they are not a valid directory, they're unsuitable for constructing a `MultiplexedPath`. Filter them out. Fixes #311 --- importlib_resources/readers.py | 12 ++++++++---- importlib_resources/tests/test_files.py | 3 --- newsfragments/311.bugfix.rst | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 newsfragments/311.bugfix.rst diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index dc649cf..4f761c6 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -138,19 +138,23 @@ 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: str) -> Iterator[abc.Traversable]: diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 6fdcdef..f1fe233 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -8,8 +8,6 @@ import importlib import contextlib -import pytest - import importlib_resources as resources from ..abc import Traversable from . import util @@ -62,7 +60,6 @@ class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase): MODULE = 'namespacedata01' - @pytest.mark.xfail(reason="#311") def test_non_paths_in_dunder_path(self): """ Non-path items in a namespace package's ``__path__`` are ignored. diff --git a/newsfragments/311.bugfix.rst b/newsfragments/311.bugfix.rst new file mode 100644 index 0000000..56655f7 --- /dev/null +++ b/newsfragments/311.bugfix.rst @@ -0,0 +1 @@ +Omit sentinel values from a namespace path. \ No newline at end of file