summaryrefslogtreecommitdiffhomepage
path: root/libs/importlib_resources
diff options
context:
space:
mode:
authormorpheus65535 <[email protected]>2022-11-07 13:06:49 -0500
committermorpheus65535 <[email protected]>2022-11-07 13:08:27 -0500
commitbbe2483e21c2c1549ceeed16f021f9581b899f70 (patch)
treebcc2bef2f55789ec6e6c64809c07fb4f4d3d9c86 /libs/importlib_resources
parent708fbfcd8ec0620647975be39a1f6acbbf08f767 (diff)
downloadbazarr-bbe2483e21c2c1549ceeed16f021f9581b899f70.tar.gz
bazarr-bbe2483e21c2c1549ceeed16f021f9581b899f70.zip
Updated vendored dependencies.
Diffstat (limited to 'libs/importlib_resources')
-rw-r--r--libs/importlib_resources/__init__.py2
-rw-r--r--libs/importlib_resources/_common.py147
-rw-r--r--libs/importlib_resources/_compat.py10
-rw-r--r--libs/importlib_resources/_legacy.py3
-rw-r--r--libs/importlib_resources/abc.py63
-rw-r--r--libs/importlib_resources/readers.py16
-rw-r--r--libs/importlib_resources/simple.py70
-rw-r--r--libs/importlib_resources/tests/__init__.py0
-rw-r--r--libs/importlib_resources/tests/_compat.py32
-rw-r--r--libs/importlib_resources/tests/_path.py50
-rw-r--r--libs/importlib_resources/tests/data01/__init__.py0
-rw-r--r--libs/importlib_resources/tests/data01/binary.filebin0 -> 4 bytes
-rw-r--r--libs/importlib_resources/tests/data01/subdirectory/__init__.py0
-rw-r--r--libs/importlib_resources/tests/data01/subdirectory/binary.filebin0 -> 4 bytes
-rw-r--r--libs/importlib_resources/tests/data01/utf-16.filebin0 -> 44 bytes
-rw-r--r--libs/importlib_resources/tests/data01/utf-8.file1
-rw-r--r--libs/importlib_resources/tests/data02/__init__.py0
-rw-r--r--libs/importlib_resources/tests/data02/one/__init__.py0
-rw-r--r--libs/importlib_resources/tests/data02/one/resource1.txt1
-rw-r--r--libs/importlib_resources/tests/data02/two/__init__.py0
-rw-r--r--libs/importlib_resources/tests/data02/two/resource2.txt1
-rw-r--r--libs/importlib_resources/tests/namespacedata01/binary.filebin0 -> 4 bytes
-rw-r--r--libs/importlib_resources/tests/namespacedata01/utf-16.filebin0 -> 44 bytes
-rw-r--r--libs/importlib_resources/tests/namespacedata01/utf-8.file1
-rw-r--r--libs/importlib_resources/tests/test_compatibilty_files.py102
-rw-r--r--libs/importlib_resources/tests/test_contents.py43
-rw-r--r--libs/importlib_resources/tests/test_files.py112
-rw-r--r--libs/importlib_resources/tests/test_open.py81
-rw-r--r--libs/importlib_resources/tests/test_path.py64
-rw-r--r--libs/importlib_resources/tests/test_read.py76
-rw-r--r--libs/importlib_resources/tests/test_reader.py133
-rw-r--r--libs/importlib_resources/tests/test_resource.py260
-rwxr-xr-xlibs/importlib_resources/tests/update-zips.py53
-rw-r--r--libs/importlib_resources/tests/util.py167
-rw-r--r--libs/importlib_resources/tests/zipdata01/__init__.py0
-rw-r--r--libs/importlib_resources/tests/zipdata01/ziptestdata.zipbin0 -> 876 bytes
-rw-r--r--libs/importlib_resources/tests/zipdata02/__init__.py0
-rw-r--r--libs/importlib_resources/tests/zipdata02/ziptestdata.zipbin0 -> 698 bytes
38 files changed, 1399 insertions, 89 deletions
diff --git a/libs/importlib_resources/__init__.py b/libs/importlib_resources/__init__.py
index 15f6b26b9..34e3a9950 100644
--- a/libs/importlib_resources/__init__.py
+++ b/libs/importlib_resources/__init__.py
@@ -17,7 +17,7 @@ from ._legacy import (
Resource,
)
-from importlib_resources.abc import ResourceReader
+from .abc import ResourceReader
__all__ = [
diff --git a/libs/importlib_resources/_common.py b/libs/importlib_resources/_common.py
index a12e2c75d..9f19784d0 100644
--- a/libs/importlib_resources/_common.py
+++ b/libs/importlib_resources/_common.py
@@ -5,25 +5,58 @@ import functools
import contextlib
import types
import importlib
+import inspect
+import warnings
+import itertools
-from typing import Union, Optional
+from typing import Union, Optional, cast
from .abc import ResourceReader, Traversable
from ._compat import wrap_spec
Package = Union[types.ModuleType, str]
+Anchor = Package
-def files(package):
- # type: (Package) -> Traversable
+def package_to_anchor(func):
"""
- Get a Traversable resource from a package
+ Replace 'package' parameter as 'anchor' and warn about the change.
+
+ Other errors should fall through.
+
+ >>> files('a', 'b')
+ Traceback (most recent call last):
+ TypeError: files() takes from 0 to 1 positional arguments but 2 were given
+ """
+ undefined = object()
+
+ @functools.wraps(func)
+ def wrapper(anchor=undefined, package=undefined):
+ if package is not undefined:
+ if anchor is not undefined:
+ return func(anchor, package)
+ warnings.warn(
+ "First parameter to files is renamed to 'anchor'",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return func(package)
+ elif anchor is undefined:
+ return func()
+ return func(anchor)
+
+ return wrapper
+
+
+@package_to_anchor
+def files(anchor: Optional[Anchor] = None) -> Traversable:
+ """
+ Get a Traversable resource for an anchor.
"""
- return from_package(get_package(package))
+ return from_package(resolve(anchor))
-def get_resource_reader(package):
- # type: (types.ModuleType) -> Optional[ResourceReader]
+def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
"""
Return the package's loader if it's a ResourceReader.
"""
@@ -39,24 +72,39 @@ def get_resource_reader(package):
return reader(spec.name) # type: ignore
-def resolve(cand):
- # type: (Package) -> types.ModuleType
- return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand)
+def resolve(cand: Optional[Anchor]) -> types.ModuleType:
+ return cast(types.ModuleType, cand)
+
+
+def _(cand: str) -> types.ModuleType:
+ return importlib.import_module(cand)
+
+def _(cand: None) -> types.ModuleType:
+ return resolve(_infer_caller().f_globals['__name__'])
-def get_package(package):
- # type: (Package) -> types.ModuleType
- """Take a package name or module object and return the module.
- Raise an exception if the resolved module is not a package.
+def _infer_caller():
"""
- resolved = resolve(package)
- if wrap_spec(resolved).submodule_search_locations is None:
- raise TypeError(f'{package!r} is not a package')
- return resolved
+ Walk the stack and find the frame of the first caller not in this module.
+ """
+
+ def is_this_file(frame_info):
+ return frame_info.filename == __file__
+
+ def is_wrapper(frame_info):
+ return frame_info.function == 'wrapper'
+
+ not_this_file = itertools.filterfalse(is_this_file, inspect.stack())
+ # also exclude 'wrapper' due to singledispatch in the call stack
+ callers = itertools.filterfalse(is_wrapper, not_this_file)
+ return next(callers).frame
-def from_package(package):
+def from_package(package: types.ModuleType):
"""
Return a Traversable object for the given package.
@@ -67,7 +115,14 @@ def from_package(package):
@contextlib.contextmanager
-def _tempfile(reader, suffix=''):
+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.
@@ -81,18 +136,35 @@ def _tempfile(reader, suffix=''):
yield pathlib.Path(raw_path)
finally:
try:
- os.remove(raw_path)
+ _os_remove(raw_path)
except FileNotFoundError:
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)
@@ -102,3 +174,34 @@ def _(path):
Degenerate behavior for pathlib.Path objects.
"""
yield path
+
+
+def _temp_path(dir: tempfile.TemporaryDirectory):
+ """
+ Wrap tempfile.TemporyDirectory to return a pathlib object.
+ """
+ with dir as result:
+ yield pathlib.Path(result)
+
+
+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
diff --git a/libs/importlib_resources/_compat.py b/libs/importlib_resources/_compat.py
index 61e48d47d..8d7ade08c 100644
--- a/libs/importlib_resources/_compat.py
+++ b/libs/importlib_resources/_compat.py
@@ -1,9 +1,12 @@
# flake8: noqa
import abc
+import os
import sys
import pathlib
from contextlib import suppress
+from typing import Union
+
if sys.version_info >= (3, 10):
from zipfile import Path as ZipPath # type: ignore
@@ -96,3 +99,10 @@ def wrap_spec(package):
from . import _adapters
return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)
+
+
+if sys.version_info >= (3, 9):
+ StrPath = Union[str, os.PathLike[str]]
+else:
+ # PathLike is only subscriptable at runtime in 3.9+
+ StrPath = Union[str, "os.PathLike[str]"]
diff --git a/libs/importlib_resources/_legacy.py b/libs/importlib_resources/_legacy.py
index 1d5d3f1fb..b1ea8105d 100644
--- a/libs/importlib_resources/_legacy.py
+++ b/libs/importlib_resources/_legacy.py
@@ -27,8 +27,7 @@ def deprecated(func):
return wrapper
-def normalize_path(path):
- # type: (Any) -> str
+def normalize_path(path: Any) -> str:
"""Normalize a path by ensuring it is a string.
If the resulting string contains path separators, an exception is raised.
diff --git a/libs/importlib_resources/abc.py b/libs/importlib_resources/abc.py
index d39dc1adb..23b6aeafe 100644
--- a/libs/importlib_resources/abc.py
+++ b/libs/importlib_resources/abc.py
@@ -1,7 +1,13 @@
import abc
-from typing import BinaryIO, Iterable, Text
+import io
+import itertools
+import pathlib
+from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional
-from ._compat import runtime_checkable, Protocol
+from ._compat import runtime_checkable, Protocol, StrPath
+
+
+__all__ = ["ResourceReader", "Traversable", "TraversableResources"]
class ResourceReader(metaclass=abc.ABCMeta):
@@ -46,27 +52,34 @@ class ResourceReader(metaclass=abc.ABCMeta):
raise FileNotFoundError
+class TraversalError(Exception):
+ pass
+
+
@runtime_checkable
class Traversable(Protocol):
"""
An object with a subset of pathlib.Path methods suitable for
traversing directories and opening files.
+
+ Any exceptions that occur when accessing the backing resource
+ may propagate unaltered.
"""
@abc.abstractmethod
- def iterdir(self):
+ def iterdir(self) -> Iterator["Traversable"]:
"""
Yield Traversable objects in self
"""
- def read_bytes(self):
+ def read_bytes(self) -> bytes:
"""
Read contents of self as bytes
"""
with self.open('rb') as strm:
return strm.read()
- def read_text(self, encoding=None):
+ def read_text(self, encoding: Optional[str] = None) -> str:
"""
Read contents of self as text
"""
@@ -85,13 +98,32 @@ class Traversable(Protocol):
Return True if self is a file
"""
- @abc.abstractmethod
- def joinpath(self, child):
+ def joinpath(self, *descendants: StrPath) -> "Traversable":
"""
- Return Traversable child in self
+ Return Traversable resolved with any descendants applied.
+
+ Each descendant should be a path segment relative to self
+ 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):
+ def __truediv__(self, child: StrPath) -> "Traversable":
"""
Return Traversable child in self
"""
@@ -107,7 +139,8 @@ class Traversable(Protocol):
accepted by io.TextIOWrapper.
"""
- @abc.abstractproperty
+ @property
+ @abc.abstractmethod
def name(self) -> str:
"""
The base name of this object without any parent references.
@@ -121,17 +154,17 @@ class TraversableResources(ResourceReader):
"""
@abc.abstractmethod
- def files(self):
+ def files(self) -> "Traversable":
"""Return a Traversable object for the loaded package."""
- def open_resource(self, resource):
+ def open_resource(self, resource: StrPath) -> io.BufferedReader:
return self.files().joinpath(resource).open('rb')
- def resource_path(self, resource):
+ def resource_path(self, resource: Any) -> NoReturn:
raise FileNotFoundError(resource)
- def is_resource(self, path):
+ def is_resource(self, path: StrPath) -> bool:
return self.files().joinpath(path).is_file()
- def contents(self):
+ def contents(self) -> Iterator[str]:
return (item.name for item in self.files().iterdir())
diff --git a/libs/importlib_resources/readers.py b/libs/importlib_resources/readers.py
index f1190ca45..ab34db740 100644
--- a/libs/importlib_resources/readers.py
+++ b/libs/importlib_resources/readers.py
@@ -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')
diff --git a/libs/importlib_resources/simple.py b/libs/importlib_resources/simple.py
index da073cbdb..7770c922c 100644
--- a/libs/importlib_resources/simple.py
+++ b/libs/importlib_resources/simple.py
@@ -16,31 +16,28 @@ class SimpleReader(abc.ABC):
provider.
"""
- @abc.abstractproperty
- def package(self):
- # type: () -> str
+ @property
+ @abc.abstractmethod
+ def package(self) -> str:
"""
The name of the package for which this reader loads resources.
"""
@abc.abstractmethod
- def children(self):
- # type: () -> List['SimpleReader']
+ def children(self) -> List['SimpleReader']:
"""
Obtain an iterable of SimpleReader for available
child containers (e.g. directories).
"""
@abc.abstractmethod
- def resources(self):
- # type: () -> List[str]
+ def resources(self) -> List[str]:
"""
Obtain available named resources for this virtual package.
"""
@abc.abstractmethod
- def open_binary(self, resource):
- # type: (str) -> BinaryIO
+ def open_binary(self, resource: str) -> BinaryIO:
"""
Obtain a File-like for a named resource.
"""
@@ -50,39 +47,12 @@ class SimpleReader(abc.ABC):
return self.package.split('.')[-1]
-class ResourceHandle(Traversable):
- """
- Handle to a named resource in a ResourceReader.
- """
-
- def __init__(self, parent, name):
- # type: (ResourceContainer, str) -> None
- self.parent = parent
- self.name = name # type: ignore
-
- def is_file(self):
- return True
-
- def is_dir(self):
- return False
-
- def open(self, mode='r', *args, **kwargs):
- stream = self.parent.reader.open_binary(self.name)
- if 'b' not in mode:
- stream = io.TextIOWrapper(*args, **kwargs)
- return stream
-
- def joinpath(self, name):
- raise RuntimeError("Cannot traverse into a resource")
-
-
class ResourceContainer(Traversable):
"""
Traversable container for a package's resources via its reader.
"""
- def __init__(self, reader):
- # type: (SimpleReader) -> None
+ def __init__(self, reader: SimpleReader):
self.reader = reader
def is_dir(self):
@@ -99,10 +69,30 @@ class ResourceContainer(Traversable):
def open(self, *args, **kwargs):
raise IsADirectoryError()
+
+class ResourceHandle(Traversable):
+ """
+ Handle to a named resource in a ResourceReader.
+ """
+
+ def __init__(self, parent: ResourceContainer, name: str):
+ self.parent = parent
+ self.name = name # type: ignore
+
+ def is_file(self):
+ return True
+
+ def is_dir(self):
+ return False
+
+ def open(self, mode='r', *args, **kwargs):
+ stream = self.parent.reader.open_binary(self.name)
+ if 'b' not in mode:
+ stream = io.TextIOWrapper(*args, **kwargs)
+ return stream
+
def joinpath(self, name):
- return next(
- traversable for traversable in self.iterdir() if traversable.name == name
- )
+ raise RuntimeError("Cannot traverse into a resource")
class TraversableReader(TraversableResources, SimpleReader):
diff --git a/libs/importlib_resources/tests/__init__.py b/libs/importlib_resources/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/importlib_resources/tests/__init__.py
diff --git a/libs/importlib_resources/tests/_compat.py b/libs/importlib_resources/tests/_compat.py
new file mode 100644
index 000000000..e7bf06dd4
--- /dev/null
+++ b/libs/importlib_resources/tests/_compat.py
@@ -0,0 +1,32 @@
+import os
+
+
+try:
+ from test.support import import_helper # type: ignore
+except ImportError:
+ # Python 3.9 and earlier
+ class import_helper: # type: ignore
+ from test.support import (
+ modules_setup,
+ modules_cleanup,
+ DirsOnSysPath,
+ CleanImport,
+ )
+
+
+try:
+ from test.support import os_helper # type: ignore
+except ImportError:
+ # Python 3.9 compat
+ class os_helper: # type:ignore
+ from test.support import temp_dir
+
+
+try:
+ # Python 3.10
+ from test.support.os_helper import unlink
+except ImportError:
+ from test.support import unlink as _unlink
+
+ def unlink(target):
+ return _unlink(os.fspath(target))
diff --git a/libs/importlib_resources/tests/_path.py b/libs/importlib_resources/tests/_path.py
new file mode 100644
index 000000000..c630e4d3d
--- /dev/null
+++ b/libs/importlib_resources/tests/_path.py
@@ -0,0 +1,50 @@
+import pathlib
+import functools
+
+
+####
+# from jaraco.path 3.4
+
+
+def build(spec, prefix=pathlib.Path()):
+ """
+ Build a set of files/directories, as described by the spec.
+
+ Each key represents a pathname, and the value represents
+ the content. Content may be a nested directory.
+
+ >>> spec = {
+ ... 'README.txt': "A README file",
+ ... "foo": {
+ ... "__init__.py": "",
+ ... "bar": {
+ ... "__init__.py": "",
+ ... },
+ ... "baz.py": "# Some code",
+ ... }
+ ... }
+ >>> tmpdir = getfixture('tmpdir')
+ >>> build(spec, tmpdir)
+ """
+ for name, contents in spec.items():
+ create(contents, pathlib.Path(prefix) / name)
+
+
+def create(content, path):
+ path.mkdir(exist_ok=True)
+ build(content, prefix=path) # type: ignore
+
+
+def _(content: bytes, path):
+ path.write_bytes(content)
+
+
+def _(content: str, path):
+ path.write_text(content)
+
+
+# end from jaraco.path
+####
diff --git a/libs/importlib_resources/tests/data01/__init__.py b/libs/importlib_resources/tests/data01/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/importlib_resources/tests/data01/__init__.py
diff --git a/libs/importlib_resources/tests/data01/binary.file b/libs/importlib_resources/tests/data01/binary.file
new file mode 100644
index 000000000..eaf36c1da
--- /dev/null
+++ b/libs/importlib_resources/tests/data01/binary.file
Binary files differ
diff --git a/libs/importlib_resources/tests/data01/subdirectory/__init__.py b/libs/importlib_resources/tests/data01/subdirectory/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/importlib_resources/tests/data01/subdirectory/__init__.py
diff --git a/libs/importlib_resources/tests/data01/subdirectory/binary.file b/libs/importlib_resources/tests/data01/subdirectory/binary.file
new file mode 100644
index 000000000..eaf36c1da
--- /dev/null
+++ b/libs/importlib_resources/tests/data01/subdirectory/binary.file
Binary files differ
diff --git a/libs/importlib_resources/tests/data01/utf-16.file b/libs/importlib_resources/tests/data01/utf-16.file
new file mode 100644
index 000000000..2cb772295
--- /dev/null
+++ b/libs/importlib_resources/tests/data01/utf-16.file
Binary files differ
diff --git a/libs/importlib_resources/tests/data01/utf-8.file b/libs/importlib_resources/tests/data01/utf-8.file
new file mode 100644
index 000000000..1c0132ad9
--- /dev/null
+++ b/libs/importlib_resources/tests/data01/utf-8.file
@@ -0,0 +1 @@
+Hello, UTF-8 world!
diff --git a/libs/importlib_resources/tests/data02/__init__.py b/libs/importlib_resources/tests/data02/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/importlib_resources/tests/data02/__init__.py
diff --git a/libs/importlib_resources/tests/data02/one/__init__.py b/libs/importlib_resources/tests/data02/one/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/importlib_resources/tests/data02/one/__init__.py
diff --git a/libs/importlib_resources/tests/data02/one/resource1.txt b/libs/importlib_resources/tests/data02/one/resource1.txt
new file mode 100644
index 000000000..61a813e40
--- /dev/null
+++ b/libs/importlib_resources/tests/data02/one/resource1.txt
@@ -0,0 +1 @@
+one resource
diff --git a/libs/importlib_resources/tests/data02/two/__init__.py b/libs/importlib_resources/tests/data02/two/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/importlib_resources/tests/data02/two/__init__.py
diff --git a/libs/importlib_resources/tests/data02/two/resource2.txt b/libs/importlib_resources/tests/data02/two/resource2.txt
new file mode 100644
index 000000000..a80ce46ea
--- /dev/null
+++ b/libs/importlib_resources/tests/data02/two/resource2.txt
@@ -0,0 +1 @@
+two resource
diff --git a/libs/importlib_resources/tests/namespacedata01/binary.file b/libs/importlib_resources/tests/namespacedata01/binary.file
new file mode 100644
index 000000000..eaf36c1da
--- /dev/null
+++ b/libs/importlib_resources/tests/namespacedata01/binary.file
Binary files differ
diff --git a/libs/importlib_resources/tests/namespacedata01/utf-16.file b/libs/importlib_resources/tests/namespacedata01/utf-16.file
new file mode 100644
index 000000000..2cb772295
--- /dev/null
+++ b/libs/importlib_resources/tests/namespacedata01/utf-16.file
Binary files differ
diff --git a/libs/importlib_resources/tests/namespacedata01/utf-8.file b/libs/importlib_resources/tests/namespacedata01/utf-8.file
new file mode 100644
index 000000000..1c0132ad9
--- /dev/null
+++ b/libs/importlib_resources/tests/namespacedata01/utf-8.file
@@ -0,0 +1 @@
+Hello, UTF-8 world!
diff --git a/libs/importlib_resources/tests/test_compatibilty_files.py b/libs/importlib_resources/tests/test_compatibilty_files.py
new file mode 100644
index 000000000..d92c7c56c
--- /dev/null
+++ b/libs/importlib_resources/tests/test_compatibilty_files.py
@@ -0,0 +1,102 @@
+import io
+import unittest
+
+import importlib_resources as resources
+
+from importlib_resources._adapters import (
+ CompatibilityFiles,
+ wrap_spec,
+)
+
+from . import util
+
+
+class CompatibilityFilesTests(unittest.TestCase):
+ @property
+ def package(self):
+ bytes_data = io.BytesIO(b'Hello, world!')
+ return util.create_package(
+ file=bytes_data,
+ path='some_path',
+ contents=('a', 'b', 'c'),
+ )
+
+ @property
+ def files(self):
+ return resources.files(self.package)
+
+ def test_spec_path_iter(self):
+ self.assertEqual(
+ sorted(path.name for path in self.files.iterdir()),
+ ['a', 'b', 'c'],
+ )
+
+ def test_child_path_iter(self):
+ self.assertEqual(list((self.files / 'a').iterdir()), [])
+
+ def test_orphan_path_iter(self):
+ self.assertEqual(list((self.files / 'a' / 'a').iterdir()), [])
+ self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), [])
+
+ def test_spec_path_is(self):
+ self.assertFalse(self.files.is_file())
+ self.assertFalse(self.files.is_dir())
+
+ def test_child_path_is(self):
+ self.assertTrue((self.files / 'a').is_file())
+ self.assertFalse((self.files / 'a').is_dir())
+
+ def test_orphan_path_is(self):
+ self.assertFalse((self.files / 'a' / 'a').is_file())
+ self.assertFalse((self.files / 'a' / 'a').is_dir())
+ self.assertFalse((self.files / 'a' / 'a' / 'a').is_file())
+ self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir())
+
+ def test_spec_path_name(self):
+ self.assertEqual(self.files.name, 'testingpackage')
+
+ def test_child_path_name(self):
+ self.assertEqual((self.files / 'a').name, 'a')
+
+ def test_orphan_path_name(self):
+ self.assertEqual((self.files / 'a' / 'b').name, 'b')
+ self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c')
+
+ def test_spec_path_open(self):
+ self.assertEqual(self.files.read_bytes(), b'Hello, world!')
+ self.assertEqual(self.files.read_text(), 'Hello, world!')
+
+ def test_child_path_open(self):
+ self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!')
+ self.assertEqual((self.files / 'a').read_text(), 'Hello, world!')
+
+ def test_orphan_path_open(self):
+ with self.assertRaises(FileNotFoundError):
+ (self.files / 'a' / 'b').read_bytes()
+ with self.assertRaises(FileNotFoundError):
+ (self.files / 'a' / 'b' / 'c').read_bytes()
+
+ def test_open_invalid_mode(self):
+ with self.assertRaises(ValueError):
+ self.files.open('0')
+
+ def test_orphan_path_invalid(self):
+ with self.assertRaises(ValueError):
+ CompatibilityFiles.OrphanPath()
+
+ def test_wrap_spec(self):
+ spec = wrap_spec(self.package)
+ self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles)
+
+
+class CompatibilityFilesNoReaderTests(unittest.TestCase):
+ @property
+ def package(self):
+ return util.create_package_from_loader(None)
+
+ @property
+ def files(self):
+ return resources.files(self.package)
+
+ def test_spec_path_joinpath(self):
+ self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath)
diff --git a/libs/importlib_resources/tests/test_contents.py b/libs/importlib_resources/tests/test_contents.py
new file mode 100644
index 000000000..525568e8c
--- /dev/null
+++ b/libs/importlib_resources/tests/test_contents.py
@@ -0,0 +1,43 @@
+import unittest
+import importlib_resources as resources
+
+from . import data01
+from . import util
+
+
+class ContentsTests:
+ expected = {
+ '__init__.py',
+ 'binary.file',
+ 'subdirectory',
+ 'utf-16.file',
+ 'utf-8.file',
+ }
+
+ def test_contents(self):
+ contents = {path.name for path in resources.files(self.data).iterdir()}
+ assert self.expected <= contents
+
+
+class ContentsDiskTests(ContentsTests, unittest.TestCase):
+ def setUp(self):
+ self.data = data01
+
+
+class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase):
+ pass
+
+
+class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
+ expected = {
+ # no __init__ because of namespace design
+ # no subdirectory as incidental difference in fixture
+ 'binary.file',
+ 'utf-16.file',
+ 'utf-8.file',
+ }
+
+ def setUp(self):
+ from . import namespacedata01
+
+ self.data = namespacedata01
diff --git a/libs/importlib_resources/tests/test_files.py b/libs/importlib_resources/tests/test_files.py
new file mode 100644
index 000000000..d258fb5f0
--- /dev/null
+++ b/libs/importlib_resources/tests/test_files.py
@@ -0,0 +1,112 @@
+import typing
+import textwrap
+import unittest
+import warnings
+import importlib
+import contextlib
+
+import importlib_resources as resources
+from ..abc import Traversable
+from . import data01
+from . import util
+from . import _path
+from ._compat import os_helper, import_helper
+
+
+def suppress_known_deprecation():
+ with warnings.catch_warnings(record=True) as ctx:
+ warnings.simplefilter('default', category=DeprecationWarning)
+ yield ctx
+
+
+class FilesTests:
+ def test_read_bytes(self):
+ files = resources.files(self.data)
+ actual = files.joinpath('utf-8.file').read_bytes()
+ assert actual == b'Hello, UTF-8 world!\n'
+
+ def test_read_text(self):
+ files = resources.files(self.data)
+ actual = files.joinpath('utf-8.file').read_text(encoding='utf-8')
+ assert actual == 'Hello, UTF-8 world!\n'
+
+ @unittest.skipUnless(
+ hasattr(typing, 'runtime_checkable'),
+ "Only suitable when typing supports runtime_checkable",
+ )
+ def test_traversable(self):
+ assert isinstance(resources.files(self.data), Traversable)
+
+ def test_old_parameter(self):
+ """
+ Files used to take a 'package' parameter. Make sure anyone
+ passing by name is still supported.
+ """
+ with suppress_known_deprecation():
+ resources.files(package=self.data)
+
+
+class OpenDiskTests(FilesTests, unittest.TestCase):
+ def setUp(self):
+ self.data = data01
+
+
+class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
+ pass
+
+
+class OpenNamespaceTests(FilesTests, unittest.TestCase):
+ def setUp(self):
+ from . import namespacedata01
+
+ self.data = namespacedata01
+
+
+class SiteDir:
+ def setUp(self):
+ self.fixtures = contextlib.ExitStack()
+ self.addCleanup(self.fixtures.close)
+ self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())
+ self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir))
+ self.fixtures.enter_context(import_helper.CleanImport())
+
+
+class ModulesFilesTests(SiteDir, unittest.TestCase):
+ def test_module_resources(self):
+ """
+ A module can have resources found adjacent to the module.
+ """
+ spec = {
+ 'mod.py': '',
+ 'res.txt': 'resources are the best',
+ }
+ _path.build(spec, self.site_dir)
+ import mod
+
+ actual = resources.files(mod).joinpath('res.txt').read_text()
+ assert actual == spec['res.txt']
+
+
+class ImplicitContextFilesTests(SiteDir, unittest.TestCase):
+ def test_implicit_files(self):
+ """
+ Without any parameter, files() will infer the location as the caller.
+ """
+ spec = {
+ 'somepkg': {
+ '__init__.py': textwrap.dedent(
+ """
+ import importlib_resources as res
+ val = res.files().joinpath('res.txt').read_text()
+ """
+ ),
+ 'res.txt': 'resources are the best',
+ },
+ }
+ _path.build(spec, self.site_dir)
+ assert importlib.import_module('somepkg').val == 'resources are the best'
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/libs/importlib_resources/tests/test_open.py b/libs/importlib_resources/tests/test_open.py
new file mode 100644
index 000000000..87b42c3d3
--- /dev/null
+++ b/libs/importlib_resources/tests/test_open.py
@@ -0,0 +1,81 @@
+import unittest
+
+import importlib_resources as resources
+from . import data01
+from . import util
+
+
+class CommonBinaryTests(util.CommonTests, unittest.TestCase):
+ def execute(self, package, path):
+ target = resources.files(package).joinpath(path)
+ with target.open('rb'):
+ pass
+
+
+class CommonTextTests(util.CommonTests, unittest.TestCase):
+ def execute(self, package, path):
+ target = resources.files(package).joinpath(path)
+ with target.open():
+ pass
+
+
+class OpenTests:
+ def test_open_binary(self):
+ target = resources.files(self.data) / 'binary.file'
+ with target.open('rb') as fp:
+ result = fp.read()
+ self.assertEqual(result, b'\x00\x01\x02\x03')
+
+ def test_open_text_default_encoding(self):
+ target = resources.files(self.data) / 'utf-8.file'
+ with target.open() as fp:
+ result = fp.read()
+ self.assertEqual(result, 'Hello, UTF-8 world!\n')
+
+ def test_open_text_given_encoding(self):
+ target = resources.files(self.data) / 'utf-16.file'
+ with target.open(encoding='utf-16', errors='strict') as fp:
+ result = fp.read()
+ self.assertEqual(result, 'Hello, UTF-16 world!\n')
+
+ def test_open_text_with_errors(self):
+ # Raises UnicodeError without the 'errors' argument.
+ target = resources.files(self.data) / 'utf-16.file'
+ with target.open(encoding='utf-8', errors='strict') as fp:
+ self.assertRaises(UnicodeError, fp.read)
+ with target.open(encoding='utf-8', errors='ignore') as fp:
+ result = fp.read()
+ self.assertEqual(
+ result,
+ 'H\x00e\x00l\x00l\x00o\x00,\x00 '
+ '\x00U\x00T\x00F\x00-\x001\x006\x00 '
+ '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00',
+ )
+
+ def test_open_binary_FileNotFoundError(self):
+ target = resources.files(self.data) / 'does-not-exist'
+ self.assertRaises(FileNotFoundError, target.open, 'rb')
+
+ def test_open_text_FileNotFoundError(self):
+ target = resources.files(self.data) / 'does-not-exist'
+ self.assertRaises(FileNotFoundError, target.open)
+
+
+class OpenDiskTests(OpenTests, unittest.TestCase):
+ def setUp(self):
+ self.data = data01
+
+
+class OpenDiskNamespaceTests(OpenTests, unittest.TestCase):
+ def setUp(self):
+ from . import namespacedata01
+
+ self.data = namespacedata01
+
+
+class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
+ pass
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/libs/importlib_resources/tests/test_path.py b/libs/importlib_resources/tests/test_path.py
new file mode 100644
index 000000000..4f4d3943b
--- /dev/null
+++ b/libs/importlib_resources/tests/test_path.py
@@ -0,0 +1,64 @@
+import io
+import unittest
+
+import importlib_resources as resources
+from . import data01
+from . import util
+
+
+class CommonTests(util.CommonTests, unittest.TestCase):
+ def execute(self, package, path):
+ with resources.as_file(resources.files(package).joinpath(path)):
+ pass
+
+
+class PathTests:
+ def test_reading(self):
+ # Path should be readable.
+ # Test also implicitly verifies the returned object is a pathlib.Path
+ # instance.
+ target = resources.files(self.data) / 'utf-8.file'
+ with resources.as_file(target) as path:
+ self.assertTrue(path.name.endswith("utf-8.file"), repr(path))
+ # pathlib.Path.read_text() was introduced in Python 3.5.
+ with path.open('r', encoding='utf-8') as file:
+ text = file.read()
+ self.assertEqual('Hello, UTF-8 world!\n', text)
+
+
+class PathDiskTests(PathTests, unittest.TestCase):
+ data = data01
+
+ def test_natural_path(self):
+ """
+ Guarantee the internal implementation detail that
+ file-system-backed resources do not get the tempdir
+ treatment.
+ """
+ target = resources.files(self.data) / 'utf-8.file'
+ with resources.as_file(target) as path:
+ assert 'data' in str(path)
+
+
+class PathMemoryTests(PathTests, unittest.TestCase):
+ def setUp(self):
+ file = io.BytesIO(b'Hello, UTF-8 world!\n')
+ self.addCleanup(file.close)
+ self.data = util.create_package(
+ file=file, path=FileNotFoundError("package exists only in memory")
+ )
+ self.data.__spec__.origin = None
+ self.data.__spec__.has_location = False
+
+
+class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase):
+ def test_remove_in_context_manager(self):
+ # It is not an error if the file that was temporarily stashed on the
+ # file system is removed inside the `with` stanza.
+ target = resources.files(self.data) / 'utf-8.file'
+ with resources.as_file(target) as path:
+ path.unlink()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/libs/importlib_resources/tests/test_read.py b/libs/importlib_resources/tests/test_read.py
new file mode 100644
index 000000000..41dd6db5f
--- /dev/null
+++ b/libs/importlib_resources/tests/test_read.py
@@ -0,0 +1,76 @@
+import unittest
+import importlib_resources as resources
+
+from . import data01
+from . import util
+from importlib import import_module
+
+
+class CommonBinaryTests(util.CommonTests, unittest.TestCase):
+ def execute(self, package, path):
+ resources.files(package).joinpath(path).read_bytes()
+
+
+class CommonTextTests(util.CommonTests, unittest.TestCase):
+ def execute(self, package, path):
+ resources.files(package).joinpath(path).read_text()
+
+
+class ReadTests:
+ def test_read_bytes(self):
+ result = resources.files(self.data).joinpath('binary.file').read_bytes()
+ self.assertEqual(result, b'\0\1\2\3')
+
+ def test_read_text_default_encoding(self):
+ result = resources.files(self.data).joinpath('utf-8.file').read_text()
+ self.assertEqual(result, 'Hello, UTF-8 world!\n')
+
+ def test_read_text_given_encoding(self):
+ result = (
+ resources.files(self.data)
+ .joinpath('utf-16.file')
+ .read_text(encoding='utf-16')
+ )
+ self.assertEqual(result, 'Hello, UTF-16 world!\n')
+
+ def test_read_text_with_errors(self):
+ # Raises UnicodeError without the 'errors' argument.
+ target = resources.files(self.data) / 'utf-16.file'
+ self.assertRaises(UnicodeError, target.read_text, encoding='utf-8')
+ result = target.read_text(encoding='utf-8', errors='ignore')
+ self.assertEqual(
+ result,
+ 'H\x00e\x00l\x00l\x00o\x00,\x00 '
+ '\x00U\x00T\x00F\x00-\x001\x006\x00 '
+ '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00',
+ )
+
+
+class ReadDiskTests(ReadTests, unittest.TestCase):
+ data = data01
+
+
+class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
+ def test_read_submodule_resource(self):
+ submodule = import_module('ziptestdata.subdirectory')
+ result = resources.files(submodule).joinpath('binary.file').read_bytes()
+ self.assertEqual(result, b'\0\1\2\3')
+
+ def test_read_submodule_resource_by_name(self):
+ result = (
+ resources.files('ziptestdata.subdirectory')
+ .joinpath('binary.file')
+ .read_bytes()
+ )
+ self.assertEqual(result, b'\0\1\2\3')
+
+
+class ReadNamespaceTests(ReadTests, unittest.TestCase):
+ def setUp(self):
+ from . import namespacedata01
+
+ self.data = namespacedata01
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/libs/importlib_resources/tests/test_reader.py b/libs/importlib_resources/tests/test_reader.py
new file mode 100644
index 000000000..1c8ebeeb1
--- /dev/null
+++ b/libs/importlib_resources/tests/test_reader.py
@@ -0,0 +1,133 @@
+import os.path
+import sys
+import pathlib
+import unittest
+
+from importlib import import_module
+from importlib_resources.readers import MultiplexedPath, NamespaceReader
+
+
+class MultiplexedPathTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ path = pathlib.Path(__file__).parent / 'namespacedata01'
+ cls.folder = str(path)
+
+ def test_init_no_paths(self):
+ with self.assertRaises(FileNotFoundError):
+ MultiplexedPath()
+
+ def test_init_file(self):
+ with self.assertRaises(NotADirectoryError):
+ MultiplexedPath(os.path.join(self.folder, 'binary.file'))
+
+ def test_iterdir(self):
+ contents = {path.name for path in MultiplexedPath(self.folder).iterdir()}
+ try:
+ contents.remove('__pycache__')
+ except (KeyError, ValueError):
+ pass
+ self.assertEqual(contents, {'binary.file', 'utf-16.file', 'utf-8.file'})
+
+ def test_iterdir_duplicate(self):
+ data01 = os.path.abspath(os.path.join(__file__, '..', 'data01'))
+ contents = {
+ path.name for path in MultiplexedPath(self.folder, data01).iterdir()
+ }
+ for remove in ('__pycache__', '__init__.pyc'):
+ try:
+ contents.remove(remove)
+ except (KeyError, ValueError):
+ pass
+ self.assertEqual(
+ contents,
+ {'__init__.py', 'binary.file', 'subdirectory', 'utf-16.file', 'utf-8.file'},
+ )
+
+ def test_is_dir(self):
+ self.assertEqual(MultiplexedPath(self.folder).is_dir(), True)
+
+ def test_is_file(self):
+ self.assertEqual(MultiplexedPath(self.folder).is_file(), False)
+
+ def test_open_file(self):
+ path = MultiplexedPath(self.folder)
+ with self.assertRaises(FileNotFoundError):
+ path.read_bytes()
+ with self.assertRaises(FileNotFoundError):
+ path.read_text()
+ with self.assertRaises(FileNotFoundError):
+ path.open()
+
+ def test_join_path(self):
+ prefix = os.path.abspath(os.path.join(__file__, '..'))
+ data01 = os.path.join(prefix, 'data01')
+ path = MultiplexedPath(self.folder, data01)
+ self.assertEqual(
+ str(path.joinpath('binary.file'))[len(prefix) + 1 :],
+ os.path.join('namespacedata01', 'binary.file'),
+ )
+ self.assertEqual(
+ str(path.joinpath('subdirectory'))[len(prefix) + 1 :],
+ os.path.join('data01', 'subdirectory'),
+ )
+ self.assertEqual(
+ 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(
+ repr(MultiplexedPath(self.folder)),
+ f"MultiplexedPath('{self.folder}')",
+ )
+
+ def test_name(self):
+ self.assertEqual(
+ MultiplexedPath(self.folder).name,
+ os.path.basename(self.folder),
+ )
+
+
+class NamespaceReaderTest(unittest.TestCase):
+ site_dir = str(pathlib.Path(__file__).parent)
+
+ @classmethod
+ def setUpClass(cls):
+ sys.path.append(cls.site_dir)
+
+ @classmethod
+ def tearDownClass(cls):
+ sys.path.remove(cls.site_dir)
+
+ def test_init_error(self):
+ with self.assertRaises(ValueError):
+ NamespaceReader(['path1', 'path2'])
+
+ def test_resource_path(self):
+ namespacedata01 = import_module('namespacedata01')
+ reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
+
+ root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))
+ self.assertEqual(
+ reader.resource_path('binary.file'), os.path.join(root, 'binary.file')
+ )
+ self.assertEqual(
+ reader.resource_path('imaginary'), os.path.join(root, 'imaginary')
+ )
+
+ def test_files(self):
+ namespacedata01 = import_module('namespacedata01')
+ reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
+ root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))
+ self.assertIsInstance(reader.files(), MultiplexedPath)
+ self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/libs/importlib_resources/tests/test_resource.py b/libs/importlib_resources/tests/test_resource.py
new file mode 100644
index 000000000..823902716
--- /dev/null
+++ b/libs/importlib_resources/tests/test_resource.py
@@ -0,0 +1,260 @@
+import sys
+import unittest
+import importlib_resources as resources
+import uuid
+import pathlib
+
+from . import data01
+from . import zipdata01, zipdata02
+from . import util
+from importlib import import_module
+from ._compat import import_helper, unlink
+
+
+class ResourceTests:
+ # Subclasses are expected to set the `data` attribute.
+
+ def test_is_file_exists(self):
+ target = resources.files(self.data) / 'binary.file'
+ self.assertTrue(target.is_file())
+
+ def test_is_file_missing(self):
+ target = resources.files(self.data) / 'not-a-file'
+ self.assertFalse(target.is_file())
+
+ def test_is_dir(self):
+ target = resources.files(self.data) / 'subdirectory'
+ self.assertFalse(target.is_file())
+ self.assertTrue(target.is_dir())
+
+
+class ResourceDiskTests(ResourceTests, unittest.TestCase):
+ def setUp(self):
+ self.data = data01
+
+
+class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase):
+ pass
+
+
+def names(traversable):
+ return {item.name for item in traversable.iterdir()}
+
+
+class ResourceLoaderTests(unittest.TestCase):
+ def test_resource_contents(self):
+ package = util.create_package(
+ file=data01, path=data01.__file__, contents=['A', 'B', 'C']
+ )
+ self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'})
+
+ def test_is_file(self):
+ package = util.create_package(
+ file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
+ )
+ self.assertTrue(resources.files(package).joinpath('B').is_file())
+
+ def test_is_dir(self):
+ package = util.create_package(
+ file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
+ )
+ self.assertTrue(resources.files(package).joinpath('D').is_dir())
+
+ def test_resource_missing(self):
+ package = util.create_package(
+ file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
+ )
+ self.assertFalse(resources.files(package).joinpath('Z').is_file())
+
+
+class ResourceCornerCaseTests(unittest.TestCase):
+ def test_package_has_no_reader_fallback(self):
+ # Test odd ball packages which:
+ # 1. Do not have a ResourceReader as a loader
+ # 2. Are not on the file system
+ # 3. Are not in a zip file
+ module = util.create_package(
+ file=data01, path=data01.__file__, contents=['A', 'B', 'C']
+ )
+ # Give the module a dummy loader.
+ module.__loader__ = object()
+ # Give the module a dummy origin.
+ module.__file__ = '/path/which/shall/not/be/named'
+ module.__spec__.loader = module.__loader__
+ module.__spec__.origin = module.__file__
+ self.assertFalse(resources.files(module).joinpath('A').is_file())
+
+
+class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
+ ZIP_MODULE = zipdata01 # type: ignore
+
+ def test_is_submodule_resource(self):
+ submodule = import_module('ziptestdata.subdirectory')
+ self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file())
+
+ def test_read_submodule_resource_by_name(self):
+ self.assertTrue(
+ resources.files('ziptestdata.subdirectory')
+ .joinpath('binary.file')
+ .is_file()
+ )
+
+ def test_submodule_contents(self):
+ submodule = import_module('ziptestdata.subdirectory')
+ self.assertEqual(
+ names(resources.files(submodule)), {'__init__.py', 'binary.file'}
+ )
+
+ def test_submodule_contents_by_name(self):
+ self.assertEqual(
+ names(resources.files('ziptestdata.subdirectory')),
+ {'__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
+
+ def test_unrelated_contents(self):
+ """
+ Test thata zip with two unrelated subpackages return
+ distinct resources. Ref python/importlib_resources#44.
+ """
+ self.assertEqual(
+ names(resources.files('ziptestdata.one')),
+ {'__init__.py', 'resource1.txt'},
+ )
+ self.assertEqual(
+ names(resources.files('ziptestdata.two')),
+ {'__init__.py', 'resource2.txt'},
+ )
+
+
+class DeletingZipsTest(unittest.TestCase):
+ """Having accessed resources in a zip file should not keep an open
+ reference to the zip.
+ """
+
+ ZIP_MODULE = zipdata01
+
+ def setUp(self):
+ modules = import_helper.modules_setup()
+ self.addCleanup(import_helper.modules_cleanup, *modules)
+
+ data_path = pathlib.Path(self.ZIP_MODULE.__file__)
+ data_dir = data_path.parent
+ self.source_zip_path = data_dir / 'ziptestdata.zip'
+ self.zip_path = pathlib.Path(f'{uuid.uuid4()}.zip').absolute()
+ self.zip_path.write_bytes(self.source_zip_path.read_bytes())
+ sys.path.append(str(self.zip_path))
+ self.data = import_module('ziptestdata')
+
+ def tearDown(self):
+ try:
+ sys.path.remove(str(self.zip_path))
+ except ValueError:
+ pass
+
+ try:
+ del sys.path_importer_cache[str(self.zip_path)]
+ del sys.modules[self.data.__name__]
+ except KeyError:
+ pass
+
+ try:
+ unlink(self.zip_path)
+ except OSError:
+ # If the test fails, this will probably fail too
+ pass
+
+ def test_iterdir_does_not_keep_open(self):
+ c = [item.name for item in resources.files('ziptestdata').iterdir()]
+ self.zip_path.unlink()
+ del c
+
+ def test_is_file_does_not_keep_open(self):
+ c = resources.files('ziptestdata').joinpath('binary.file').is_file()
+ self.zip_path.unlink()
+ del c
+
+ def test_is_file_failure_does_not_keep_open(self):
+ c = resources.files('ziptestdata').joinpath('not-present').is_file()
+ self.zip_path.unlink()
+ del c
+
+ @unittest.skip("Desired but not supported.")
+ def test_as_file_does_not_keep_open(self): # pragma: no cover
+ c = resources.as_file(resources.files('ziptestdata') / 'binary.file')
+ self.zip_path.unlink()
+ del c
+
+ def test_entered_path_does_not_keep_open(self):
+ # This is what certifi does on import to make its bundle
+ # available for the process duration.
+ c = resources.as_file(
+ resources.files('ziptestdata') / 'binary.file'
+ ).__enter__()
+ self.zip_path.unlink()
+ del c
+
+ def test_read_binary_does_not_keep_open(self):
+ c = resources.files('ziptestdata').joinpath('binary.file').read_bytes()
+ self.zip_path.unlink()
+ del c
+
+ def test_read_text_does_not_keep_open(self):
+ c = resources.files('ziptestdata').joinpath('utf-8.file').read_text()
+ self.zip_path.unlink()
+ del c
+
+
+class ResourceFromNamespaceTest01(unittest.TestCase):
+ site_dir = str(pathlib.Path(__file__).parent)
+
+ @classmethod
+ def setUpClass(cls):
+ sys.path.append(cls.site_dir)
+
+ @classmethod
+ def tearDownClass(cls):
+ sys.path.remove(cls.site_dir)
+
+ def test_is_submodule_resource(self):
+ self.assertTrue(
+ resources.files(import_module('namespacedata01'))
+ .joinpath('binary.file')
+ .is_file()
+ )
+
+ def test_read_submodule_resource_by_name(self):
+ self.assertTrue(
+ resources.files('namespacedata01').joinpath('binary.file').is_file()
+ )
+
+ def test_submodule_contents(self):
+ contents = names(resources.files(import_module('namespacedata01')))
+ try:
+ contents.remove('__pycache__')
+ except KeyError:
+ pass
+ self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'})
+
+ def test_submodule_contents_by_name(self):
+ contents = names(resources.files('namespacedata01'))
+ try:
+ contents.remove('__pycache__')
+ except KeyError:
+ pass
+ self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'})
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/libs/importlib_resources/tests/update-zips.py b/libs/importlib_resources/tests/update-zips.py
new file mode 100755
index 000000000..231334aa7
--- /dev/null
+++ b/libs/importlib_resources/tests/update-zips.py
@@ -0,0 +1,53 @@
+"""
+Generate the zip test data files.
+
+Run to build the tests/zipdataNN/ziptestdata.zip files from
+files in tests/dataNN.
+
+Replaces the file with the working copy, but does commit anything
+to the source repo.
+"""
+
+import contextlib
+import os
+import pathlib
+import zipfile
+
+
+def main():
+ """
+ >>> from unittest import mock
+ >>> monkeypatch = getfixture('monkeypatch')
+ >>> monkeypatch.setattr(zipfile, 'ZipFile', mock.MagicMock())
+ >>> print(); main() # print workaround for bpo-32509
+ <BLANKLINE>
+ ...data01... -> ziptestdata/...
+ ...
+ ...data02... -> ziptestdata/...
+ ...
+ """
+ suffixes = '01', '02'
+ tuple(map(generate, suffixes))
+
+
+def generate(suffix):
+ root = pathlib.Path(__file__).parent.relative_to(os.getcwd())
+ zfpath = root / f'zipdata{suffix}/ziptestdata.zip'
+ with zipfile.ZipFile(zfpath, 'w') as zf:
+ for src, rel in walk(root / f'data{suffix}'):
+ dst = 'ziptestdata' / pathlib.PurePosixPath(rel.as_posix())
+ print(src, '->', dst)
+ zf.write(src, dst)
+
+
+def walk(datapath):
+ for dirpath, dirnames, filenames in os.walk(datapath):
+ with contextlib.suppress(ValueError):
+ dirnames.remove('__pycache__')
+ for filename in filenames:
+ res = pathlib.Path(dirpath) / filename
+ rel = res.relative_to(datapath)
+ yield res, rel
+
+
+__name__ == '__main__' and main()
diff --git a/libs/importlib_resources/tests/util.py b/libs/importlib_resources/tests/util.py
new file mode 100644
index 000000000..b596c0ce4
--- /dev/null
+++ b/libs/importlib_resources/tests/util.py
@@ -0,0 +1,167 @@
+import abc
+import importlib
+import io
+import sys
+import types
+import pathlib
+
+from . import data01
+from . import zipdata01
+from ..abc import ResourceReader
+from ._compat import import_helper
+
+
+from importlib.machinery import ModuleSpec
+
+
+class Reader(ResourceReader):
+ def __init__(self, **kwargs):
+ vars(self).update(kwargs)
+
+ def get_resource_reader(self, package):
+ return self
+
+ def open_resource(self, path):
+ self._path = path
+ if isinstance(self.file, Exception):
+ raise self.file
+ return self.file
+
+ def resource_path(self, path_):
+ self._path = path_
+ if isinstance(self.path, Exception):
+ raise self.path
+ return self.path
+
+ def is_resource(self, path_):
+ self._path = path_
+ if isinstance(self.path, Exception):
+ raise self.path
+
+ def part(entry):
+ return entry.split('/')
+
+ return any(
+ len(parts) == 1 and parts[0] == path_ for parts in map(part, self._contents)
+ )
+
+ def contents(self):
+ if isinstance(self.path, Exception):
+ raise self.path
+ yield from self._contents
+
+
+def create_package_from_loader(loader, is_package=True):
+ name = 'testingpackage'
+ module = types.ModuleType(name)
+ spec = ModuleSpec(name, loader, origin='does-not-exist', is_package=is_package)
+ module.__spec__ = spec
+ module.__loader__ = loader
+ return module
+
+
+def create_package(file=None, path=None, is_package=True, contents=()):
+ return create_package_from_loader(
+ Reader(file=file, path=path, _contents=contents),
+ is_package,
+ )
+
+
+class CommonTests(metaclass=abc.ABCMeta):
+ """
+ Tests shared by test_open, test_path, and test_read.
+ """
+
+ @abc.abstractmethod
+ def execute(self, package, path):
+ """
+ Call the pertinent legacy API function (e.g. open_text, path)
+ on package and path.
+ """
+
+ def test_package_name(self):
+ # Passing in the package name should succeed.
+ self.execute(data01.__name__, 'utf-8.file')
+
+ def test_package_object(self):
+ # Passing in the package itself should succeed.
+ self.execute(data01, 'utf-8.file')
+
+ def test_string_path(self):
+ # Passing in a string for the path should succeed.
+ path = 'utf-8.file'
+ self.execute(data01, path)
+
+ def test_pathlib_path(self):
+ # Passing in a pathlib.PurePath object for the path should succeed.
+ path = pathlib.PurePath('utf-8.file')
+ self.execute(data01, path)
+
+ def test_importing_module_as_side_effect(self):
+ # The anchor package can already be imported.
+ del sys.modules[data01.__name__]
+ self.execute(data01.__name__, 'utf-8.file')
+
+ def test_missing_path(self):
+ # Attempting to open or read or request the path for a
+ # non-existent path should succeed if open_resource
+ # can return a viable data stream.
+ bytes_data = io.BytesIO(b'Hello, world!')
+ package = create_package(file=bytes_data, path=FileNotFoundError())
+ self.execute(package, 'utf-8.file')
+ self.assertEqual(package.__loader__._path, 'utf-8.file')
+
+ def test_extant_path(self):
+ # Attempting to open or read or request the path when the
+ # path does exist should still succeed. Does not assert
+ # anything about the result.
+ bytes_data = io.BytesIO(b'Hello, world!')
+ # any path that exists
+ path = __file__
+ package = create_package(file=bytes_data, path=path)
+ self.execute(package, 'utf-8.file')
+ self.assertEqual(package.__loader__._path, 'utf-8.file')
+
+ def test_useless_loader(self):
+ package = create_package(file=FileNotFoundError(), path=FileNotFoundError())
+ with self.assertRaises(FileNotFoundError):
+ self.execute(package, 'utf-8.file')
+
+
+class ZipSetupBase:
+ ZIP_MODULE = None
+
+ @classmethod
+ def setUpClass(cls):
+ data_path = pathlib.Path(cls.ZIP_MODULE.__file__)
+ data_dir = data_path.parent
+ cls._zip_path = str(data_dir / 'ziptestdata.zip')
+ sys.path.append(cls._zip_path)
+ cls.data = importlib.import_module('ziptestdata')
+
+ @classmethod
+ def tearDownClass(cls):
+ try:
+ sys.path.remove(cls._zip_path)
+ except ValueError:
+ pass
+
+ try:
+ del sys.path_importer_cache[cls._zip_path]
+ del sys.modules[cls.data.__name__]
+ except KeyError:
+ pass
+
+ try:
+ del cls.data
+ del cls._zip_path
+ except AttributeError:
+ pass
+
+ def setUp(self):
+ modules = import_helper.modules_setup()
+ self.addCleanup(import_helper.modules_cleanup, *modules)
+
+
+class ZipSetup(ZipSetupBase):
+ ZIP_MODULE = zipdata01 # type: ignore
diff --git a/libs/importlib_resources/tests/zipdata01/__init__.py b/libs/importlib_resources/tests/zipdata01/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/importlib_resources/tests/zipdata01/__init__.py
diff --git a/libs/importlib_resources/tests/zipdata01/ziptestdata.zip b/libs/importlib_resources/tests/zipdata01/ziptestdata.zip
new file mode 100644
index 000000000..9a3bb0739
--- /dev/null
+++ b/libs/importlib_resources/tests/zipdata01/ziptestdata.zip
Binary files differ
diff --git a/libs/importlib_resources/tests/zipdata02/__init__.py b/libs/importlib_resources/tests/zipdata02/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/importlib_resources/tests/zipdata02/__init__.py
diff --git a/libs/importlib_resources/tests/zipdata02/ziptestdata.zip b/libs/importlib_resources/tests/zipdata02/ziptestdata.zip
new file mode 100644
index 000000000..d63ff512d
--- /dev/null
+++ b/libs/importlib_resources/tests/zipdata02/ziptestdata.zip
Binary files differ