]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-97930: Merge with importlib_resources 5.9 (GH-97929)
authorJason R. Coombs <jaraco@jaraco.com>
Sun, 16 Oct 2022 19:00:39 +0000 (15:00 -0400)
committerGitHub <noreply@github.com>
Sun, 16 Oct 2022 19:00:39 +0000 (15:00 -0400)
* Merge with importlib_resources 5.9

* Update changelog

Lib/importlib/resources/_common.py
Lib/importlib/resources/abc.py
Lib/importlib/resources/readers.py
Lib/importlib/resources/simple.py
Lib/test/test_importlib/resources/test_reader.py
Lib/test/test_importlib/resources/test_resource.py
Misc/NEWS.d/next/Library/2022-10-05-16-10-24.gh-issue-97930.NPSrzE.rst [new file with mode: 0644]

index ca1fa8ab2fe6ff28313134ef37d7c71676280b35..8c0be7ee547e01888535b7b47669d8021e0962ee 100644 (file)
@@ -67,10 +67,14 @@ def from_package(package):
 
 
 @contextlib.contextmanager
-def _tempfile(reader, suffix='',
-              # gh-93353: Keep a reference to call os.remove() in late Python
-              # finalization.
-              *, _os_remove=os.remove):
+def _tempfile(
+    reader,
+    suffix='',
+    # gh-93353: Keep a reference to call os.remove() in late Python
+    # finalization.
+    *,
+    _os_remove=os.remove,
+):
     # Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
     # blocks due to the need to close the temporary file to work on Windows
     # properly.
@@ -89,13 +93,30 @@ def _tempfile(reader, suffix='',
             pass
 
 
+def _temp_file(path):
+    return _tempfile(path.read_bytes, suffix=path.name)
+
+
+def _is_present_dir(path: Traversable) -> bool:
+    """
+    Some Traversables implement ``is_dir()`` to raise an
+    exception (i.e. ``FileNotFoundError``) when the
+    directory doesn't exist. This function wraps that call
+    to always return a boolean and only return True
+    if there's a dir and it exists.
+    """
+    with contextlib.suppress(FileNotFoundError):
+        return path.is_dir()
+    return False
+
+
 @functools.singledispatch
 def as_file(path):
     """
     Given a Traversable object, return that object as a
     path on the local file system in a context manager.
     """
-    return _tempfile(path.read_bytes, suffix=path.name)
+    return _temp_dir(path) if _is_present_dir(path) else _temp_file(path)
 
 
 @as_file.register(pathlib.Path)
@@ -105,3 +126,34 @@ def _(path):
     Degenerate behavior for pathlib.Path objects.
     """
     yield path
+
+
+@contextlib.contextmanager
+def _temp_path(dir: tempfile.TemporaryDirectory):
+    """
+    Wrap tempfile.TemporyDirectory to return a pathlib object.
+    """
+    with dir as result:
+        yield pathlib.Path(result)
+
+
+@contextlib.contextmanager
+def _temp_dir(path):
+    """
+    Given a traversable dir, recursively replicate the whole tree
+    to the file system in a context manager.
+    """
+    assert path.is_dir()
+    with _temp_path(tempfile.TemporaryDirectory()) as temp_dir:
+        yield _write_contents(temp_dir, path)
+
+
+def _write_contents(target, source):
+    child = target.joinpath(source.name)
+    if source.is_dir():
+        child.mkdir()
+        for item in source.iterdir():
+            _write_contents(child, item)
+    else:
+        child.open('wb').write(source.read_bytes())
+    return child
index 0b7bfdc415829e15a77ac94420a0e5258e359d79..67c78c078567056f2499a428ef1335102e9e9f72 100644 (file)
@@ -1,6 +1,8 @@
 import abc
 import io
+import itertools
 import os
+import pathlib
 from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional
 from typing import runtime_checkable, Protocol
 from typing import Union
@@ -53,6 +55,10 @@ class ResourceReader(metaclass=abc.ABCMeta):
         raise FileNotFoundError
 
 
+class TraversalError(Exception):
+    pass
+
+
 @runtime_checkable
 class Traversable(Protocol):
     """
@@ -95,7 +101,6 @@ class Traversable(Protocol):
         Return True if self is a file
         """
 
-    @abc.abstractmethod
     def joinpath(self, *descendants: StrPath) -> "Traversable":
         """
         Return Traversable resolved with any descendants applied.
@@ -104,6 +109,22 @@ class Traversable(Protocol):
         and each may contain multiple levels separated by
         ``posixpath.sep`` (``/``).
         """
+        if not descendants:
+            return self
+        names = itertools.chain.from_iterable(
+            path.parts for path in map(pathlib.PurePosixPath, descendants)
+        )
+        target = next(names)
+        matches = (
+            traversable for traversable in self.iterdir() if traversable.name == target
+        )
+        try:
+            match = next(matches)
+        except StopIteration:
+            raise TraversalError(
+                "Target not found during traversal.", target, list(names)
+            )
+        return match.joinpath(*names)
 
     def __truediv__(self, child: StrPath) -> "Traversable":
         """
index b470a2062b2b3acad0a29528a78ba898b296abd9..80cb320dd8bda0cb5fed505694c2332ce7bc4d49 100644 (file)
@@ -82,15 +82,13 @@ class MultiplexedPath(abc.Traversable):
     def is_file(self):
         return False
 
-    def joinpath(self, child):
-        # first try to find child in current paths
-        for file in self.iterdir():
-            if file.name == child:
-                return file
-        # if it does not exist, construct it with the first path
-        return self._paths[0] / child
-
-    __truediv__ = joinpath
+    def joinpath(self, *descendants):
+        try:
+            return super().joinpath(*descendants)
+        except abc.TraversalError:
+            # One of the paths did not resolve (a directory does not exist).
+            # Just return something that will not exist.
+            return self._paths[0].joinpath(*descendants)
 
     def open(self, *args, **kwargs):
         raise FileNotFoundError(f'{self} is not a file')
index d0fbf237762c2febfb53158f9b5ee783808efce4..b85e4694acedff5729e2dcdda7d39cc0b969ae78 100644 (file)
@@ -99,20 +99,6 @@ class ResourceContainer(Traversable):
     def open(self, *args, **kwargs):
         raise IsADirectoryError()
 
-    @staticmethod
-    def _flatten(compound_names):
-        for name in compound_names:
-            yield from name.split('/')
-
-    def joinpath(self, *descendants):
-        if not descendants:
-            return self
-        names = self._flatten(descendants)
-        target = next(names)
-        return next(
-            traversable for traversable in self.iterdir() if traversable.name == target
-        ).joinpath(*names)
-
 
 class TraversableReader(TraversableResources, SimpleReader):
     """
index 9d20c976b825052f094e23747d2984309b0b0042..4fd9e6bbe4281c19b7446524a38aaaed3e7fab3c 100644 (file)
@@ -75,6 +75,11 @@ class MultiplexedPathTest(unittest.TestCase):
             str(path.joinpath('imaginary'))[len(prefix) + 1 :],
             os.path.join('namespacedata01', 'imaginary'),
         )
+        self.assertEqual(path.joinpath(), path)
+
+    def test_join_path_compound(self):
+        path = MultiplexedPath(self.folder)
+        assert not path.joinpath('imaginary/foo.py').exists()
 
     def test_repr(self):
         self.assertEqual(
index 1d6df0cc8431730fc453ceff5f7b34549591a176..f7e3abbdc805a75e4998e6fadf9049e64cb1b512 100644 (file)
@@ -111,6 +111,14 @@ class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
             {'__init__.py', 'binary.file'},
         )
 
+    def test_as_file_directory(self):
+        with resources.as_file(resources.files('ziptestdata')) as data:
+            assert data.name == 'ziptestdata'
+            assert data.is_dir()
+            assert data.joinpath('subdirectory').is_dir()
+            assert len(list(data.iterdir()))
+        assert not data.parent.exists()
+
 
 class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
     ZIP_MODULE = zipdata02  # type: ignore
diff --git a/Misc/NEWS.d/next/Library/2022-10-05-16-10-24.gh-issue-97930.NPSrzE.rst b/Misc/NEWS.d/next/Library/2022-10-05-16-10-24.gh-issue-97930.NPSrzE.rst
new file mode 100644 (file)
index 0000000..860f6ad
--- /dev/null
@@ -0,0 +1,3 @@
+Apply changes from importlib_resources 5.8 and 5.9: ``Traversable.joinpath``
+provides a concrete implementation. ``as_file`` now supports directories of
+resources.