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 311581c..f1fe233 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -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' 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