Bump importlib-resources from 6.0.1 to 6.4.0 (#2285)

* Bump importlib-resources from 6.0.1 to 6.4.0

Bumps [importlib-resources](https://github.com/python/importlib_resources) from 6.0.1 to 6.4.0.
- [Release notes](https://github.com/python/importlib_resources/releases)
- [Changelog](https://github.com/python/importlib_resources/blob/main/NEWS.rst)
- [Commits](https://github.com/python/importlib_resources/compare/v6.0.1...v6.4.0)

---
updated-dependencies:
- dependency-name: importlib-resources
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update importlib-resources==6.4.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>

[skip ci]
This commit is contained in:
dependabot[bot] 2024-03-24 15:27:55 -07:00 committed by GitHub
parent 6c6fa34ba4
commit b01b21ae05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 725 additions and 349 deletions

View file

@ -4,6 +4,17 @@ from ._common import (
as_file,
files,
Package,
Anchor,
)
from .functional import (
contents,
is_resource,
open_binary,
open_text,
path,
read_binary,
read_text,
)
from .abc import ResourceReader
@ -11,7 +22,15 @@ from .abc import ResourceReader
__all__ = [
'Package',
'Anchor',
'ResourceReader',
'as_file',
'files',
'contents',
'is_resource',
'open_binary',
'open_text',
'path',
'read_binary',
'read_text',
]

View file

@ -12,8 +12,6 @@ import itertools
from typing import Union, Optional, cast
from .abc import ResourceReader, Traversable
from ._compat import wrap_spec
Package = Union[types.ModuleType, str]
Anchor = Package
@ -27,6 +25,8 @@ def package_to_anchor(func):
>>> files('a', 'b')
Traceback (most recent call last):
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
Remove this compatibility in Python 3.14.
"""
undefined = object()
@ -109,6 +109,9 @@ def from_package(package: types.ModuleType):
Return a Traversable object for the given package.
"""
# deferred for performance (python/cpython#109829)
from .future.adapters import wrap_spec
spec = wrap_spec(package)
reader = spec.loader.get_resource_reader(spec.name)
return reader.files()

View file

@ -1,109 +0,0 @@
# 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
else:
from zipp import Path as ZipPath # type: ignore
try:
from typing import runtime_checkable # type: ignore
except ImportError:
def runtime_checkable(cls): # type: ignore
return cls
try:
from typing import Protocol # type: ignore
except ImportError:
Protocol = abc.ABC # type: ignore
class TraversableResourcesLoader:
"""
Adapt loaders to provide TraversableResources and other
compatibility.
Used primarily for Python 3.9 and earlier where the native
loaders do not yet implement TraversableResources.
"""
def __init__(self, spec):
self.spec = spec
@property
def path(self):
return self.spec.origin
def get_resource_reader(self, name):
from . import readers, _adapters
def _zip_reader(spec):
with suppress(AttributeError):
return readers.ZipReader(spec.loader, spec.name)
def _namespace_reader(spec):
with suppress(AttributeError, ValueError):
return readers.NamespaceReader(spec.submodule_search_locations)
def _available_reader(spec):
with suppress(AttributeError):
return spec.loader.get_resource_reader(spec.name)
def _native_reader(spec):
reader = _available_reader(spec)
return reader if hasattr(reader, 'files') else None
def _file_reader(spec):
try:
path = pathlib.Path(self.path)
except TypeError:
return None
if path.exists():
return readers.FileReader(self)
return (
# local ZipReader if a zip module
_zip_reader(self.spec)
or
# local NamespaceReader if a namespace module
_namespace_reader(self.spec)
or
# local FileReader
_file_reader(self.spec)
or
# native reader if it supplies 'files'
_native_reader(self.spec)
or
# fallback - adapt the spec ResourceReader to TraversableReader
_adapters.CompatibilityFiles(self.spec)
)
def wrap_spec(package):
"""
Construct a package spec with traversable compatibility
on the spec/loader/reader.
Supersedes _adapters.wrap_spec to use TraversableResourcesLoader
from above for older Python compatibility (<3.10).
"""
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]"]

View file

@ -3,8 +3,9 @@ import io
import itertools
import pathlib
from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional
from typing import runtime_checkable, Protocol
from ._compat import runtime_checkable, Protocol, StrPath
from .compat.py38 import StrPath
__all__ = ["ResourceReader", "Traversable", "TraversableResources"]

View file

@ -0,0 +1,11 @@
import os
import sys
from typing import Union
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]"]

View file

@ -0,0 +1,10 @@
import sys
__all__ = ['ZipPath']
if sys.version_info >= (3, 10):
from zipfile import Path as ZipPath # type: ignore
else:
from zipp import Path as ZipPath # type: ignore

View file

@ -0,0 +1,81 @@
"""Simplified function-based API for importlib.resources"""
import warnings
from ._common import files, as_file
_MISSING = object()
def open_binary(anchor, *path_names):
"""Open for binary reading the *resource* within *package*."""
return _get_resource(anchor, path_names).open('rb')
def open_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
"""Open for text reading the *resource* within *package*."""
encoding = _get_encoding_arg(path_names, encoding)
resource = _get_resource(anchor, path_names)
return resource.open('r', encoding=encoding, errors=errors)
def read_binary(anchor, *path_names):
"""Read and return contents of *resource* within *package* as bytes."""
return _get_resource(anchor, path_names).read_bytes()
def read_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
"""Read and return contents of *resource* within *package* as str."""
encoding = _get_encoding_arg(path_names, encoding)
resource = _get_resource(anchor, path_names)
return resource.read_text(encoding=encoding, errors=errors)
def path(anchor, *path_names):
"""Return the path to the *resource* as an actual file system path."""
return as_file(_get_resource(anchor, path_names))
def is_resource(anchor, *path_names):
"""Return ``True`` if there is a resource named *name* in the package,
Otherwise returns ``False``.
"""
return _get_resource(anchor, path_names).is_file()
def contents(anchor, *path_names):
"""Return an iterable over the named resources within the package.
The iterable returns :class:`str` resources (e.g. files).
The iterable does not recurse into subdirectories.
"""
warnings.warn(
"importlib.resources.contents is deprecated. "
"Use files(anchor).iterdir() instead.",
DeprecationWarning,
stacklevel=1,
)
return (resource.name for resource in _get_resource(anchor, path_names).iterdir())
def _get_encoding_arg(path_names, encoding):
# For compatibility with versions where *encoding* was a positional
# argument, it needs to be given explicitly when there are multiple
# *path_names*.
# This limitation can be removed in Python 3.15.
if encoding is _MISSING:
if len(path_names) > 1:
raise TypeError(
"'encoding' argument required with multiple path names",
)
else:
return 'utf-8'
return encoding
def _get_resource(anchor, path_names):
if anchor is None:
raise TypeError("anchor must be module or string, got None")
return files(anchor).joinpath(*path_names)

View file

@ -0,0 +1,95 @@
import functools
import pathlib
from contextlib import suppress
from types import SimpleNamespace
from .. import readers, _adapters
def _block_standard(reader_getter):
"""
Wrap _adapters.TraversableResourcesLoader.get_resource_reader
and intercept any standard library readers.
"""
@functools.wraps(reader_getter)
def wrapper(*args, **kwargs):
"""
If the reader is from the standard library, return None to allow
allow likely newer implementations in this library to take precedence.
"""
try:
reader = reader_getter(*args, **kwargs)
except NotADirectoryError:
# MultiplexedPath may fail on zip subdirectory
return
# Python 3.10+
mod_name = reader.__class__.__module__
if mod_name.startswith('importlib.') and mod_name.endswith('readers'):
return
# Python 3.8, 3.9
if isinstance(reader, _adapters.CompatibilityFiles) and (
reader.spec.loader.__class__.__module__.startswith('zipimport')
or reader.spec.loader.__class__.__module__.startswith(
'_frozen_importlib_external'
)
):
return
return reader
return wrapper
def _skip_degenerate(reader):
"""
Mask any degenerate reader. Ref #298.
"""
is_degenerate = (
isinstance(reader, _adapters.CompatibilityFiles) and not reader._reader
)
return reader if not is_degenerate else None
class TraversableResourcesLoader(_adapters.TraversableResourcesLoader):
"""
Adapt loaders to provide TraversableResources and other
compatibility.
Ensures the readers from importlib_resources are preferred
over stdlib readers.
"""
def get_resource_reader(self, name):
return (
_skip_degenerate(_block_standard(super().get_resource_reader)(name))
or self._standard_reader()
or super().get_resource_reader(name)
)
def _standard_reader(self):
return self._zip_reader() or self._namespace_reader() or self._file_reader()
def _zip_reader(self):
with suppress(AttributeError):
return readers.ZipReader(self.spec.loader, self.spec.name)
def _namespace_reader(self):
with suppress(AttributeError, ValueError):
return readers.NamespaceReader(self.spec.submodule_search_locations)
def _file_reader(self):
try:
path = pathlib.Path(self.spec.origin)
except TypeError:
return None
if path.exists():
return readers.FileReader(SimpleNamespace(path=path))
def wrap_spec(package):
"""
Override _adapters.wrap_spec to use TraversableResourcesLoader
from above. Ensures that future behavior is always available on older
Pythons.
"""
return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)

View file

@ -1,12 +1,15 @@
import collections
import contextlib
import itertools
import pathlib
import operator
import re
import warnings
from . import abc
from ._itertools import only
from ._compat import ZipPath
from .compat.py39 import ZipPath
def remove_duplicates(items):
@ -62,7 +65,7 @@ class MultiplexedPath(abc.Traversable):
"""
def __init__(self, *paths):
self._paths = list(map(pathlib.Path, remove_duplicates(paths)))
self._paths = list(map(_ensure_traversable, remove_duplicates(paths)))
if not self._paths:
message = 'MultiplexedPath must contain at least one path'
raise FileNotFoundError(message)
@ -130,7 +133,36 @@ 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:
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``.
"""
(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(r'[\\/]', path_str))):
with contextlib.suppress(
FileNotFoundError,
IsADirectoryError,
NotADirectoryError,
PermissionError,
):
inner = path_str[match.end() :].replace('\\', '/') + '/'
yield ZipPath(path_str[: match.start()], inner.lstrip('/'))
def resource_path(self, resource):
"""
@ -142,3 +174,21 @@ class NamespaceReader(abc.TraversableResources):
def files(self):
return self.path
def _ensure_traversable(path):
"""
Convert deprecated string arguments to traversables (pathlib.Path).
Remove with Python 3.15.
"""
if not isinstance(path, str):
return path
warnings.warn(
"String arguments are deprecated. Pass a Traversable instead.",
DeprecationWarning,
stacklevel=3,
)
return pathlib.Path(path)

View file

@ -88,7 +88,7 @@ class ResourceHandle(Traversable):
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)
stream = io.TextIOWrapper(stream, *args, **kwargs)
return stream
def joinpath(self, name):

View file

@ -1,32 +0,0 @@
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))

View file

@ -0,0 +1,18 @@
import contextlib
from .py39 import import_helper
@contextlib.contextmanager
def isolated_modules():
"""
Save modules on entry and cleanup on exit.
"""
(saved,) = import_helper.modules_setup()
try:
yield
finally:
import_helper.modules_cleanup(saved)
vars(import_helper).setdefault('isolated_modules', isolated_modules)

View file

@ -0,0 +1,10 @@
"""
Backward-compatability shims to support Python 3.9 and earlier.
"""
from jaraco.test.cpython import from_test_support, try_import
import_helper = try_import('import_helper') or from_test_support(
'modules_setup', 'modules_cleanup', 'DirsOnSysPath'
)
os_helper = try_import('os_helper') or from_test_support('temp_dir')

View file

@ -31,8 +31,8 @@ class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase):
class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
expected = {
# no __init__ because of namespace design
# no subdirectory as incidental difference in fixture
'binary.file',
'subdirectory',
'utf-16.file',
'utf-8.file',
}

View file

@ -3,9 +3,10 @@ import contextlib
import pathlib
import importlib_resources as resources
from .. import abc
from ..abc import TraversableResources, ResourceReader
from . import util
from ._compat import os_helper
from .compat.py39 import os_helper
class SimpleLoader:
@ -38,8 +39,9 @@ class CustomTraversableResourcesTests(unittest.TestCase):
self.addCleanup(self.fixtures.close)
def test_custom_loader(self):
temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
temp_dir = pathlib.Path(self.fixtures.enter_context(os_helper.temp_dir()))
loader = SimpleLoader(MagicResources(temp_dir))
pkg = util.create_package_from_loader(loader)
files = resources.files(pkg)
assert files is temp_dir
assert isinstance(files, abc.Traversable)
assert list(files.iterdir()) == []

View file

@ -1,4 +1,3 @@
import typing
import textwrap
import unittest
import warnings
@ -10,7 +9,8 @@ from ..abc import Traversable
from . import data01
from . import util
from . import _path
from ._compat import os_helper, import_helper
from .compat.py39 import os_helper
from .compat.py312 import import_helper
@contextlib.contextmanager
@ -31,13 +31,14 @@ class FilesTests:
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_joinpath_with_multiple_args(self):
files = resources.files(self.data)
binfile = files.joinpath('subdirectory', 'binary.file')
self.assertTrue(binfile.is_file())
def test_old_parameter(self):
"""
Files used to take a 'package' parameter. Make sure anyone
@ -63,13 +64,17 @@ class OpenNamespaceTests(FilesTests, unittest.TestCase):
self.data = namespacedata01
class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = '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())
self.fixtures.enter_context(import_helper.isolated_modules())
class ModulesFilesTests(SiteDir, unittest.TestCase):

View file

@ -0,0 +1,242 @@
import unittest
import os
import contextlib
try:
from test.support.warnings_helper import ignore_warnings, check_warnings
except ImportError:
# older Python versions
from test.support import ignore_warnings, check_warnings
import importlib_resources as resources
# Since the functional API forwards to Traversable, we only test
# filesystem resources here -- not zip files, namespace packages etc.
# We do test for two kinds of Anchor, though.
class StringAnchorMixin:
anchor01 = 'importlib_resources.tests.data01'
anchor02 = 'importlib_resources.tests.data02'
class ModuleAnchorMixin:
from . import data01 as anchor01
from . import data02 as anchor02
class FunctionalAPIBase:
def _gen_resourcetxt_path_parts(self):
"""Yield various names of a text file in anchor02, each in a subTest"""
for path_parts in (
('subdirectory', 'subsubdir', 'resource.txt'),
('subdirectory/subsubdir/resource.txt',),
('subdirectory/subsubdir', 'resource.txt'),
):
with self.subTest(path_parts=path_parts):
yield path_parts
def test_read_text(self):
self.assertEqual(
resources.read_text(self.anchor01, 'utf-8.file'),
'Hello, UTF-8 world!\n',
)
self.assertEqual(
resources.read_text(
self.anchor02,
'subdirectory',
'subsubdir',
'resource.txt',
encoding='utf-8',
),
'a resource',
)
for path_parts in self._gen_resourcetxt_path_parts():
self.assertEqual(
resources.read_text(
self.anchor02,
*path_parts,
encoding='utf-8',
),
'a resource',
)
# Use generic OSError, since e.g. attempting to read a directory can
# fail with PermissionError rather than IsADirectoryError
with self.assertRaises(OSError):
resources.read_text(self.anchor01)
with self.assertRaises(OSError):
resources.read_text(self.anchor01, 'no-such-file')
with self.assertRaises(UnicodeDecodeError):
resources.read_text(self.anchor01, 'utf-16.file')
self.assertEqual(
resources.read_text(
self.anchor01,
'binary.file',
encoding='latin1',
),
'\x00\x01\x02\x03',
)
self.assertEqual(
resources.read_text(
self.anchor01,
'utf-16.file',
errors='backslashreplace',
),
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
errors='backslashreplace',
),
)
def test_read_binary(self):
self.assertEqual(
resources.read_binary(self.anchor01, 'utf-8.file'),
b'Hello, UTF-8 world!\n',
)
for path_parts in self._gen_resourcetxt_path_parts():
self.assertEqual(
resources.read_binary(self.anchor02, *path_parts),
b'a resource',
)
def test_open_text(self):
with resources.open_text(self.anchor01, 'utf-8.file') as f:
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
for path_parts in self._gen_resourcetxt_path_parts():
with resources.open_text(
self.anchor02,
*path_parts,
encoding='utf-8',
) as f:
self.assertEqual(f.read(), 'a resource')
# Use generic OSError, since e.g. attempting to read a directory can
# fail with PermissionError rather than IsADirectoryError
with self.assertRaises(OSError):
resources.open_text(self.anchor01)
with self.assertRaises(OSError):
resources.open_text(self.anchor01, 'no-such-file')
with resources.open_text(self.anchor01, 'utf-16.file') as f:
with self.assertRaises(UnicodeDecodeError):
f.read()
with resources.open_text(
self.anchor01,
'binary.file',
encoding='latin1',
) as f:
self.assertEqual(f.read(), '\x00\x01\x02\x03')
with resources.open_text(
self.anchor01,
'utf-16.file',
errors='backslashreplace',
) as f:
self.assertEqual(
f.read(),
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
errors='backslashreplace',
),
)
def test_open_binary(self):
with resources.open_binary(self.anchor01, 'utf-8.file') as f:
self.assertEqual(f.read(), b'Hello, UTF-8 world!\n')
for path_parts in self._gen_resourcetxt_path_parts():
with resources.open_binary(
self.anchor02,
*path_parts,
) as f:
self.assertEqual(f.read(), b'a resource')
def test_path(self):
with resources.path(self.anchor01, 'utf-8.file') as path:
with open(str(path), encoding='utf-8') as f:
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
with resources.path(self.anchor01) as path:
with open(os.path.join(path, 'utf-8.file'), encoding='utf-8') as f:
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
def test_is_resource(self):
is_resource = resources.is_resource
self.assertTrue(is_resource(self.anchor01, 'utf-8.file'))
self.assertFalse(is_resource(self.anchor01, 'no_such_file'))
self.assertFalse(is_resource(self.anchor01))
self.assertFalse(is_resource(self.anchor01, 'subdirectory'))
for path_parts in self._gen_resourcetxt_path_parts():
self.assertTrue(is_resource(self.anchor02, *path_parts))
def test_contents(self):
with check_warnings((".*contents.*", DeprecationWarning)):
c = resources.contents(self.anchor01)
self.assertGreaterEqual(
set(c),
{'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'},
)
with contextlib.ExitStack() as cm:
cm.enter_context(self.assertRaises(OSError))
cm.enter_context(check_warnings((".*contents.*", DeprecationWarning)))
list(resources.contents(self.anchor01, 'utf-8.file'))
for path_parts in self._gen_resourcetxt_path_parts():
with contextlib.ExitStack() as cm:
cm.enter_context(self.assertRaises(OSError))
cm.enter_context(check_warnings((".*contents.*", DeprecationWarning)))
list(resources.contents(self.anchor01, *path_parts))
with check_warnings((".*contents.*", DeprecationWarning)):
c = resources.contents(self.anchor01, 'subdirectory')
self.assertGreaterEqual(
set(c),
{'binary.file'},
)
@ignore_warnings(category=DeprecationWarning)
def test_common_errors(self):
for func in (
resources.read_text,
resources.read_binary,
resources.open_text,
resources.open_binary,
resources.path,
resources.is_resource,
resources.contents,
):
with self.subTest(func=func):
# Rejecting None anchor
with self.assertRaises(TypeError):
func(None)
# Rejecting invalid anchor type
with self.assertRaises((TypeError, AttributeError)):
func(1234)
# Unknown module
with self.assertRaises(ModuleNotFoundError):
func('$missing module$')
def test_text_errors(self):
for func in (
resources.read_text,
resources.open_text,
):
with self.subTest(func=func):
# Multiple path arguments need explicit encoding argument.
with self.assertRaises(TypeError):
func(
self.anchor02,
'subdirectory',
'subsubdir',
'resource.txt',
)
class FunctionalAPITest_StringAnchor(
unittest.TestCase,
FunctionalAPIBase,
StringAnchorMixin,
):
pass
class FunctionalAPITest_ModuleAnchor(
unittest.TestCase,
FunctionalAPIBase,
ModuleAnchorMixin,
):
pass

View file

@ -24,7 +24,7 @@ class OpenTests:
target = resources.files(self.data) / 'binary.file'
with target.open('rb') as fp:
result = fp.read()
self.assertEqual(result, b'\x00\x01\x02\x03')
self.assertEqual(result, bytes(range(4)))
def test_open_text_default_encoding(self):
target = resources.files(self.data) / 'utf-8.file'
@ -81,5 +81,9 @@ class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
pass
class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01'
if __name__ == '__main__':
unittest.main()

View file

@ -1,4 +1,5 @@
import io
import pathlib
import unittest
import importlib_resources as resources
@ -15,18 +16,13 @@ class CommonTests(util.CommonTests, unittest.TestCase):
class PathTests:
def test_reading(self):
"""
Path should be readable.
Test also implicitly verifies the returned object is a pathlib.Path
instance.
Path should be readable and a pathlib.Path instance.
"""
target = resources.files(self.data) / 'utf-8.file'
with resources.as_file(target) as path:
self.assertIsInstance(path, pathlib.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)
self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8'))
class PathDiskTests(PathTests, unittest.TestCase):

View file

@ -19,7 +19,7 @@ class CommonTextTests(util.CommonTests, unittest.TestCase):
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')
self.assertEqual(result, bytes(range(4)))
def test_read_text_default_encoding(self):
result = (
@ -58,17 +58,15 @@ class ReadDiskTests(ReadTests, unittest.TestCase):
class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
def test_read_submodule_resource(self):
submodule = import_module('ziptestdata.subdirectory')
submodule = import_module('data01.subdirectory')
result = resources.files(submodule).joinpath('binary.file').read_bytes()
self.assertEqual(result, b'\0\1\2\3')
self.assertEqual(result, bytes(range(4, 8)))
def test_read_submodule_resource_by_name(self):
result = (
resources.files('ziptestdata.subdirectory')
.joinpath('binary.file')
.read_bytes()
resources.files('data01.subdirectory').joinpath('binary.file').read_bytes()
)
self.assertEqual(result, b'\0\1\2\3')
self.assertEqual(result, bytes(range(4, 8)))
class ReadNamespaceTests(ReadTests, unittest.TestCase):
@ -78,5 +76,22 @@ class ReadNamespaceTests(ReadTests, unittest.TestCase):
self.data = namespacedata01
class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01'
def test_read_submodule_resource(self):
submodule = import_module('namespacedata01.subdirectory')
result = resources.files(submodule).joinpath('binary.file').read_bytes()
self.assertEqual(result, bytes(range(12, 16)))
def test_read_submodule_resource_by_name(self):
result = (
resources.files('namespacedata01.subdirectory')
.joinpath('binary.file')
.read_bytes()
)
self.assertEqual(result, bytes(range(12, 16)))
if __name__ == '__main__':
unittest.main()

View file

@ -10,8 +10,7 @@ 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)
cls.folder = pathlib.Path(__file__).parent / 'namespacedata01'
def test_init_no_paths(self):
with self.assertRaises(FileNotFoundError):
@ -19,7 +18,7 @@ class MultiplexedPathTest(unittest.TestCase):
def test_init_file(self):
with self.assertRaises(NotADirectoryError):
MultiplexedPath(os.path.join(self.folder, 'binary.file'))
MultiplexedPath(self.folder / 'binary.file')
def test_iterdir(self):
contents = {path.name for path in MultiplexedPath(self.folder).iterdir()}
@ -27,10 +26,12 @@ class MultiplexedPathTest(unittest.TestCase):
contents.remove('__pycache__')
except (KeyError, ValueError):
pass
self.assertEqual(contents, {'binary.file', 'utf-16.file', 'utf-8.file'})
self.assertEqual(
contents, {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'}
)
def test_iterdir_duplicate(self):
data01 = os.path.abspath(os.path.join(__file__, '..', 'data01'))
data01 = pathlib.Path(__file__).parent.joinpath('data01')
contents = {
path.name for path in MultiplexedPath(self.folder, data01).iterdir()
}
@ -60,17 +61,17 @@ class MultiplexedPathTest(unittest.TestCase):
path.open()
def test_join_path(self):
prefix = os.path.abspath(os.path.join(__file__, '..'))
data01 = os.path.join(prefix, 'data01')
data01 = pathlib.Path(__file__).parent.joinpath('data01')
prefix = str(data01.parent)
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'),
)
sub = path.joinpath('subdirectory')
assert isinstance(sub, MultiplexedPath)
assert 'namespacedata01' in str(sub)
assert 'data01' in str(sub)
self.assertEqual(
str(path.joinpath('imaginary'))[len(prefix) + 1 :],
os.path.join('namespacedata01', 'imaginary'),
@ -82,9 +83,9 @@ class MultiplexedPathTest(unittest.TestCase):
assert not path.joinpath('imaginary/foo.py').exists()
def test_join_path_common_subdir(self):
prefix = os.path.abspath(os.path.join(__file__, '..'))
data01 = os.path.join(prefix, 'data01')
data02 = os.path.join(prefix, 'data02')
data01 = pathlib.Path(__file__).parent.joinpath('data01')
data02 = pathlib.Path(__file__).parent.joinpath('data02')
prefix = str(data01.parent)
path = MultiplexedPath(data01, data02)
self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath)
self.assertEqual(

View file

@ -1,15 +1,11 @@
import contextlib
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, os_helper, unlink
class ResourceTests:
@ -89,34 +85,32 @@ class ResourceCornerCaseTests(unittest.TestCase):
class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
ZIP_MODULE = zipdata01 # type: ignore
ZIP_MODULE = 'data01'
def test_is_submodule_resource(self):
submodule = import_module('ziptestdata.subdirectory')
submodule = import_module('data01.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()
resources.files('data01.subdirectory').joinpath('binary.file').is_file()
)
def test_submodule_contents(self):
submodule = import_module('ziptestdata.subdirectory')
submodule = import_module('data01.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')),
names(resources.files('data01.subdirectory')),
{'__init__.py', 'binary.file'},
)
def test_as_file_directory(self):
with resources.as_file(resources.files('ziptestdata')) as data:
assert data.name == 'ziptestdata'
with resources.as_file(resources.files('data01')) as data:
assert data.name == 'data01'
assert data.is_dir()
assert data.joinpath('subdirectory').is_dir()
assert len(list(data.iterdir()))
@ -124,7 +118,7 @@ class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
ZIP_MODULE = zipdata02 # type: ignore
ZIP_MODULE = 'data02'
def test_unrelated_contents(self):
"""
@ -132,93 +126,48 @@ class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
distinct resources. Ref python/importlib_resources#44.
"""
self.assertEqual(
names(resources.files('ziptestdata.one')),
names(resources.files('data02.one')),
{'__init__.py', 'resource1.txt'},
)
self.assertEqual(
names(resources.files('ziptestdata.two')),
names(resources.files('data02.two')),
{'__init__.py', 'resource2.txt'},
)
@contextlib.contextmanager
def zip_on_path(dir):
data_path = pathlib.Path(zipdata01.__file__)
source_zip_path = data_path.parent.joinpath('ziptestdata.zip')
zip_path = pathlib.Path(dir) / f'{uuid.uuid4()}.zip'
zip_path.write_bytes(source_zip_path.read_bytes())
sys.path.append(str(zip_path))
import_module('ziptestdata')
try:
yield
finally:
with contextlib.suppress(ValueError):
sys.path.remove(str(zip_path))
with contextlib.suppress(KeyError):
del sys.path_importer_cache[str(zip_path)]
del sys.modules['ziptestdata']
with contextlib.suppress(OSError):
unlink(zip_path)
class DeletingZipsTest(unittest.TestCase):
class DeletingZipsTest(util.ZipSetupBase, unittest.TestCase):
"""Having accessed resources in a zip file should not keep an open
reference to the zip.
"""
def setUp(self):
self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close)
modules = import_helper.modules_setup()
self.addCleanup(import_helper.modules_cleanup, *modules)
temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
self.fixtures.enter_context(zip_on_path(temp_dir))
def test_iterdir_does_not_keep_open(self):
[item.name for item in resources.files('ziptestdata').iterdir()]
[item.name for item in resources.files('data01').iterdir()]
def test_is_file_does_not_keep_open(self):
resources.files('ziptestdata').joinpath('binary.file').is_file()
resources.files('data01').joinpath('binary.file').is_file()
def test_is_file_failure_does_not_keep_open(self):
resources.files('ziptestdata').joinpath('not-present').is_file()
resources.files('data01').joinpath('not-present').is_file()
@unittest.skip("Desired but not supported.")
def test_as_file_does_not_keep_open(self): # pragma: no cover
resources.as_file(resources.files('ziptestdata') / 'binary.file')
resources.as_file(resources.files('data01') / 'binary.file')
def test_entered_path_does_not_keep_open(self):
"""
Mimic what certifi does on import to make its bundle
available for the process duration.
"""
resources.as_file(resources.files('ziptestdata') / 'binary.file').__enter__()
resources.as_file(resources.files('data01') / 'binary.file').__enter__()
def test_read_binary_does_not_keep_open(self):
resources.files('ziptestdata').joinpath('binary.file').read_bytes()
resources.files('data01').joinpath('binary.file').read_bytes()
def test_read_text_does_not_keep_open(self):
resources.files('ziptestdata').joinpath('utf-8.file').read_text(
encoding='utf-8'
)
resources.files('data01').joinpath('utf-8.file').read_text(encoding='utf-8')
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)
class ResourceFromNamespaceTests:
def test_is_submodule_resource(self):
self.assertTrue(
resources.files(import_module('namespacedata01'))
@ -237,7 +186,9 @@ class ResourceFromNamespaceTest01(unittest.TestCase):
contents.remove('__pycache__')
except KeyError:
pass
self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'})
self.assertEqual(
contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
)
def test_submodule_contents_by_name(self):
contents = names(resources.files('namespacedata01'))
@ -245,7 +196,45 @@ class ResourceFromNamespaceTest01(unittest.TestCase):
contents.remove('__pycache__')
except KeyError:
pass
self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'})
self.assertEqual(
contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
)
def test_submodule_sub_contents(self):
contents = names(resources.files(import_module('namespacedata01.subdirectory')))
try:
contents.remove('__pycache__')
except KeyError:
pass
self.assertEqual(contents, {'binary.file'})
def test_submodule_sub_contents_by_name(self):
contents = names(resources.files('namespacedata01.subdirectory'))
try:
contents.remove('__pycache__')
except KeyError:
pass
self.assertEqual(contents, {'binary.file'})
class ResourceFromNamespaceDiskTests(ResourceFromNamespaceTests, 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)
class ResourceFromNamespaceZipTests(
util.ZipSetupBase,
ResourceFromNamespaceTests,
unittest.TestCase,
):
ZIP_MODULE = 'namespacedata01'
if __name__ == '__main__':

View file

@ -1,53 +0,0 @@
"""
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()

View file

@ -4,11 +4,12 @@ import io
import sys
import types
import pathlib
import contextlib
from . import data01
from . import zipdata01
from ..abc import ResourceReader
from ._compat import import_helper
from .compat.py39 import import_helper, os_helper
from . import zip as zip_
from importlib.machinery import ModuleSpec
@ -141,39 +142,23 @@ class CommonTests(metaclass=abc.ABCMeta):
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
ZIP_MODULE = 'data01'
def setUp(self):
modules = import_helper.modules_setup()
self.addCleanup(import_helper.modules_cleanup, *modules)
self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close)
self.fixtures.enter_context(import_helper.isolated_modules())
temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
modules = pathlib.Path(temp_dir) / 'zipped modules.zip'
src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE)
self.fixtures.enter_context(
import_helper.DirsOnSysPath(str(zip_.make_zip_file(src_path, modules)))
)
self.data = importlib.import_module(self.ZIP_MODULE)
class ZipSetup(ZipSetupBase):
ZIP_MODULE = zipdata01 # type: ignore
pass

View file

@ -0,0 +1,32 @@
"""
Generate zip test data files.
"""
import contextlib
import os
import pathlib
import zipfile
import zipp
def make_zip_file(src, dst):
"""
Zip the files in src into a new zipfile at dst.
"""
with zipfile.ZipFile(dst, 'w') as zf:
for src_path, rel in walk(src):
dst_name = src.name / pathlib.PurePosixPath(rel.as_posix())
zf.write(src_path, dst_name)
zipp.CompleteDirs.inject(zf)
return 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

View file

@ -1,6 +1,6 @@
apscheduler==3.10.1
importlib-metadata==6.8.0
importlib-resources==6.0.1
importlib-resources==6.4.0
pyinstaller==6.4.0
pyopenssl==24.0.0
pycryptodomex==3.20.0

View file

@ -19,7 +19,7 @@ html5lib==1.1
httpagentparser==1.9.5
idna==3.4
importlib-metadata==6.8.0
importlib-resources==6.0.1
importlib-resources==6.4.0
git+https://github.com/Tautulli/ipwhois.git@master#egg=ipwhois
IPy==1.01
Mako==1.3.2