From a4e0cc72b6d0ed229b60bb455d7bc758b22132f1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 14:37:12 -0400 Subject: [PATCH 1/7] Add _common._as_tree to demonstrate a directory of resources. --- importlib_resources/_common.py | 35 ++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index b2c1939c..beee62c0 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,7 +6,7 @@ import types import importlib -from typing import Union, Optional +from typing import Union, Optional, ContextManager from .abc import ResourceReader, Traversable from ._compat import wrap_spec @@ -94,7 +94,7 @@ def _tempfile( @functools.singledispatch -def as_file(path): +def as_file(path: Traversable) -> pathlib.Path: """ Given a Traversable object, return that object as a path on the local file system in a context manager. @@ -109,3 +109,34 @@ def _(path): Degenerate behavior for pathlib.Path objects. """ yield path + + +@contextlib.contextmanager +def _temp_path(dir: tempfile.TemporaryDirectory) -> ContextManager[pathlib.Path]: + """ + Wrap tempfile.TemporyDirectory to return a pathlib object. + """ + with dir as result: + yield pathlib.Path(result) + + +@contextlib.contextmanager +def _as_tree(path: Traversable) -> pathlib.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(suffix=path.name)) as temp_dir: + _write_contents(temp_dir, path) + yield temp_dir + + +def _write_contents(target: pathlib.Path, source: Traversable): + for item in source.iterdir(): + child = target.joinpath(item.name) + if item.is_dir(): + child.mkdir() + _write_contents(child, item) + else: + child.open('wb').write(item.read_bytes()) From 1151390b5d57f1da6cf2a3ec42a9c5e1ec4c90bc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 15:06:09 -0400 Subject: [PATCH 2/7] Remove type hints as they're causing trouble. --- importlib_resources/_common.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index beee62c0..ac3e7cbe 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,7 +6,7 @@ import types import importlib -from typing import Union, Optional, ContextManager +from typing import Union, Optional from .abc import ResourceReader, Traversable from ._compat import wrap_spec @@ -94,7 +94,7 @@ def _tempfile( @functools.singledispatch -def as_file(path: Traversable) -> pathlib.Path: +def as_file(path): """ Given a Traversable object, return that object as a path on the local file system in a context manager. @@ -112,7 +112,7 @@ def _(path): @contextlib.contextmanager -def _temp_path(dir: tempfile.TemporaryDirectory) -> ContextManager[pathlib.Path]: +def _temp_path(dir: tempfile.TemporaryDirectory): """ Wrap tempfile.TemporyDirectory to return a pathlib object. """ @@ -121,7 +121,7 @@ def _temp_path(dir: tempfile.TemporaryDirectory) -> ContextManager[pathlib.Path] @contextlib.contextmanager -def _as_tree(path: Traversable) -> pathlib.Path: +def _as_tree(path): """ Given a traversable dir, recursively replicate the whole tree to the file system in a context manager. @@ -132,7 +132,7 @@ def _as_tree(path: Traversable) -> pathlib.Path: yield temp_dir -def _write_contents(target: pathlib.Path, source: Traversable): +def _write_contents(target, source): for item in source.iterdir(): child = target.joinpath(item.name) if item.is_dir(): From 95c77d4150ecf4f630d890ed63f95f5168b0068e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 15:20:20 -0400 Subject: [PATCH 3/7] Allow as_file to return a context manager for a directory or a file as needed. --- importlib_resources/_common.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index ac3e7cbe..8735109d 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -93,13 +93,22 @@ def _tempfile( pass +def _temp_file(path): + return _tempfile(path.read_bytes, suffix=path.name) + + @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) + try: + is_dir = path.is_dir() + except FileNotFoundError: + is_dir = False + + return _temp_dir(path) if is_dir else _temp_file(path) @as_file.register(pathlib.Path) @@ -121,7 +130,7 @@ def _temp_path(dir: tempfile.TemporaryDirectory): @contextlib.contextmanager -def _as_tree(path): +def _temp_dir(path): """ Given a traversable dir, recursively replicate the whole tree to the file system in a context manager. From 9e17e0d1c7adc76b0fe9a722b1102d4279f22429 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 15:29:05 -0400 Subject: [PATCH 4/7] Rewrite _temp_dir so that it produces a directory of the same name as the input path. --- importlib_resources/_common.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 8735109d..5a9f3c86 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -136,16 +136,16 @@ def _temp_dir(path): to the file system in a context manager. """ assert path.is_dir() - with _temp_path(tempfile.TemporaryDirectory(suffix=path.name)) as temp_dir: - _write_contents(temp_dir, path) - yield temp_dir + with _temp_path(tempfile.TemporaryDirectory()) as temp_dir: + yield _write_contents(temp_dir, path) def _write_contents(target, source): - for item in source.iterdir(): - child = target.joinpath(item.name) - if item.is_dir(): - child.mkdir() + 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(item.read_bytes()) + else: + child.open('wb').write(source.read_bytes()) + return child From f15de8cbe24887186d51dbed70634ea055df8441 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 19:04:05 -0400 Subject: [PATCH 5/7] Add test capturing new expectation that subdirectories are supported. --- importlib_resources/tests/test_resource.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 5affd8b0..82390271 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -111,6 +111,14 @@ def test_submodule_contents_by_name(self): {'__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 From 92666d25b5561f4033304efc35c5819f81a52a94 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 19:10:28 -0400 Subject: [PATCH 6/7] Update changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5321b177..f6817bc7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.9.0 +====== + +* #228: ``as_file`` now also supports a ``Traversable`` + representing a directory and (when needed) renders the + full tree to a temporary directory. + v5.8.0 ====== From bac6e8e3e6c681d923e1ec78b6f670cee0883ea3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Jul 2022 12:29:00 -0400 Subject: [PATCH 7/7] Extract function for _is_present_dir. --- importlib_resources/_common.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 5a9f3c86..423b0bb9 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -97,18 +97,26 @@ 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. """ - try: - is_dir = path.is_dir() - except FileNotFoundError: - is_dir = False - - return _temp_dir(path) if is_dir else _temp_file(path) + return _temp_dir(path) if _is_present_dir(path) else _temp_file(path) @as_file.register(pathlib.Path)