From 463331b37e9811d4f231d2b70d17df7cea7a6551 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Sep 2023 09:16:01 -0400 Subject: [PATCH 1/2] When constructing a MultiplexedPath, resolve submodule_search_locations to Traversable objects. Closes python/importlib_resources#287. --- importlib_resources/readers.py | 28 +++++++++++++++++++++- importlib_resources/tests/test_resource.py | 3 --- newsfragments/287.bugfix.rst | 1 + 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 newsfragments/287.bugfix.rst diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index dc1ec94..6a45a23 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -1,7 +1,9 @@ import collections +import contextlib import itertools import pathlib import operator +import re from . import abc @@ -130,7 +132,31 @@ class NamespaceReader(abc.TraversableResources): def __init__(self, namespace_path): if 'NamespacePath' not in str(namespace_path): raise ValueError('Invalid path') - self.path = MultiplexedPath(*list(namespace_path)) + self.path = MultiplexedPath(*map(self._resolve, namespace_path)) + + @classmethod + def _resolve(cls, path_str) -> abc.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 + zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or + ``/foo/baz.zip/inner_dir``. + """ + (dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir()) + return dir + + @classmethod + def _candidate_paths(cls, path_str): + yield pathlib.Path(path_str) + yield from cls._resolve_zip_path(path_str) + + @staticmethod + def _resolve_zip_path(path_str): + for match in reversed(list(re.finditer('/', path_str))): + with contextlib.suppress(FileNotFoundError, IsADirectoryError): + inner = path_str[match.end() :] + yield ZipPath(path_str[: match.start()], inner + '/' * len(inner)) def resource_path(self, resource): """ diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 850d029..de7d734 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -3,8 +3,6 @@ import importlib_resources as resources import pathlib -import pytest - from . import data01 from . import util from importlib import import_module @@ -211,7 +209,6 @@ def tearDownClass(cls): sys.path.remove(cls.site_dir) -@pytest.mark.xfail class ResourceFromNamespaceZipTests( util.ZipSetupBase, ResourceFromNamespaceTests, diff --git a/newsfragments/287.bugfix.rst b/newsfragments/287.bugfix.rst new file mode 100644 index 0000000..9ef3c0b --- /dev/null +++ b/newsfragments/287.bugfix.rst @@ -0,0 +1 @@ +Enabled support for resources in namespace packages in zip files. From a9b0c92b303ba34a1631ce2f60c9ee16ea062d71 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Sep 2023 11:33:00 -0400 Subject: [PATCH 2/2] Honor backslashes in inner paths as found in submodule_search_locations. --- importlib_resources/readers.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 6a45a23..1e2d1ba 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -136,12 +136,12 @@ def __init__(self, namespace_path): @classmethod def _resolve(cls, path_str) -> abc.Traversable: - """ + 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``. + ``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``. """ (dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir()) return dir @@ -153,10 +153,12 @@ def _candidate_paths(cls, path_str): @staticmethod def _resolve_zip_path(path_str): - for match in reversed(list(re.finditer('/', path_str))): - with contextlib.suppress(FileNotFoundError, IsADirectoryError): - inner = path_str[match.end() :] - yield ZipPath(path_str[: match.start()], inner + '/' * len(inner)) + for match in reversed(list(re.finditer(r'[\\/]', path_str))): + with contextlib.suppress( + FileNotFoundError, IsADirectoryError, PermissionError + ): + inner = path_str[match.end() :].replace('\\', '/') + '/' + yield ZipPath(path_str[: match.start()], inner.lstrip('/')) def resource_path(self, resource): """