mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-22 06:13:25 -07:00
Update importlib-resources==6.4.0
This commit is contained in:
parent
ec321a5a8e
commit
c4587ac50c
36 changed files with 864 additions and 385 deletions
|
@ -4,6 +4,17 @@ from ._common import (
|
||||||
as_file,
|
as_file,
|
||||||
files,
|
files,
|
||||||
Package,
|
Package,
|
||||||
|
Anchor,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .functional import (
|
||||||
|
contents,
|
||||||
|
is_resource,
|
||||||
|
open_binary,
|
||||||
|
open_text,
|
||||||
|
path,
|
||||||
|
read_binary,
|
||||||
|
read_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .abc import ResourceReader
|
from .abc import ResourceReader
|
||||||
|
@ -11,7 +22,15 @@ from .abc import ResourceReader
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Package',
|
'Package',
|
||||||
|
'Anchor',
|
||||||
'ResourceReader',
|
'ResourceReader',
|
||||||
'as_file',
|
'as_file',
|
||||||
'files',
|
'files',
|
||||||
|
'contents',
|
||||||
|
'is_resource',
|
||||||
|
'open_binary',
|
||||||
|
'open_text',
|
||||||
|
'path',
|
||||||
|
'read_binary',
|
||||||
|
'read_text',
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,8 +12,6 @@ import itertools
|
||||||
from typing import Union, Optional, cast
|
from typing import Union, Optional, cast
|
||||||
from .abc import ResourceReader, Traversable
|
from .abc import ResourceReader, Traversable
|
||||||
|
|
||||||
from ._compat import wrap_spec
|
|
||||||
|
|
||||||
Package = Union[types.ModuleType, str]
|
Package = Union[types.ModuleType, str]
|
||||||
Anchor = Package
|
Anchor = Package
|
||||||
|
|
||||||
|
@ -27,6 +25,8 @@ def package_to_anchor(func):
|
||||||
>>> files('a', 'b')
|
>>> files('a', 'b')
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
|
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
|
||||||
|
|
||||||
|
Remove this compatibility in Python 3.14.
|
||||||
"""
|
"""
|
||||||
undefined = object()
|
undefined = object()
|
||||||
|
|
||||||
|
@ -109,6 +109,9 @@ def from_package(package: types.ModuleType):
|
||||||
Return a Traversable object for the given package.
|
Return a Traversable object for the given package.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# deferred for performance (python/cpython#109829)
|
||||||
|
from .future.adapters import wrap_spec
|
||||||
|
|
||||||
spec = wrap_spec(package)
|
spec = wrap_spec(package)
|
||||||
reader = spec.loader.get_resource_reader(spec.name)
|
reader = spec.loader.get_resource_reader(spec.name)
|
||||||
return reader.files()
|
return reader.files()
|
||||||
|
|
|
@ -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]"]
|
|
|
@ -3,8 +3,9 @@ import io
|
||||||
import itertools
|
import itertools
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional
|
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"]
|
__all__ = ["ResourceReader", "Traversable", "TraversableResources"]
|
||||||
|
|
11
lib/importlib_resources/compat/py38.py
Normal file
11
lib/importlib_resources/compat/py38.py
Normal 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]"]
|
10
lib/importlib_resources/compat/py39.py
Normal file
10
lib/importlib_resources/compat/py39.py
Normal 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
|
81
lib/importlib_resources/functional.py
Normal file
81
lib/importlib_resources/functional.py
Normal 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)
|
95
lib/importlib_resources/future/adapters.py
Normal file
95
lib/importlib_resources/future/adapters.py
Normal 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)
|
|
@ -1,12 +1,15 @@
|
||||||
import collections
|
import collections
|
||||||
|
import contextlib
|
||||||
import itertools
|
import itertools
|
||||||
import pathlib
|
import pathlib
|
||||||
import operator
|
import operator
|
||||||
|
import re
|
||||||
|
import warnings
|
||||||
|
|
||||||
from . import abc
|
from . import abc
|
||||||
|
|
||||||
from ._itertools import only
|
from ._itertools import only
|
||||||
from ._compat import ZipPath
|
from .compat.py39 import ZipPath
|
||||||
|
|
||||||
|
|
||||||
def remove_duplicates(items):
|
def remove_duplicates(items):
|
||||||
|
@ -62,7 +65,7 @@ class MultiplexedPath(abc.Traversable):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *paths):
|
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:
|
if not self._paths:
|
||||||
message = 'MultiplexedPath must contain at least one path'
|
message = 'MultiplexedPath must contain at least one path'
|
||||||
raise FileNotFoundError(message)
|
raise FileNotFoundError(message)
|
||||||
|
@ -130,7 +133,36 @@ class NamespaceReader(abc.TraversableResources):
|
||||||
def __init__(self, namespace_path):
|
def __init__(self, namespace_path):
|
||||||
if 'NamespacePath' not in str(namespace_path):
|
if 'NamespacePath' not in str(namespace_path):
|
||||||
raise ValueError('Invalid 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):
|
def resource_path(self, resource):
|
||||||
"""
|
"""
|
||||||
|
@ -142,3 +174,21 @@ class NamespaceReader(abc.TraversableResources):
|
||||||
|
|
||||||
def files(self):
|
def files(self):
|
||||||
return self.path
|
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)
|
||||||
|
|
|
@ -88,7 +88,7 @@ class ResourceHandle(Traversable):
|
||||||
def open(self, mode='r', *args, **kwargs):
|
def open(self, mode='r', *args, **kwargs):
|
||||||
stream = self.parent.reader.open_binary(self.name)
|
stream = self.parent.reader.open_binary(self.name)
|
||||||
if 'b' not in mode:
|
if 'b' not in mode:
|
||||||
stream = io.TextIOWrapper(*args, **kwargs)
|
stream = io.TextIOWrapper(stream, *args, **kwargs)
|
||||||
return stream
|
return stream
|
||||||
|
|
||||||
def joinpath(self, name):
|
def joinpath(self, name):
|
||||||
|
|
|
@ -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))
|
|
0
lib/importlib_resources/tests/compat/__init__.py
Normal file
0
lib/importlib_resources/tests/compat/__init__.py
Normal file
18
lib/importlib_resources/tests/compat/py312.py
Normal file
18
lib/importlib_resources/tests/compat/py312.py
Normal 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)
|
10
lib/importlib_resources/tests/compat/py39.py
Normal file
10
lib/importlib_resources/tests/compat/py39.py
Normal 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')
|
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -31,8 +31,8 @@ class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase):
|
||||||
class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
|
class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
|
||||||
expected = {
|
expected = {
|
||||||
# no __init__ because of namespace design
|
# no __init__ because of namespace design
|
||||||
# no subdirectory as incidental difference in fixture
|
|
||||||
'binary.file',
|
'binary.file',
|
||||||
|
'subdirectory',
|
||||||
'utf-16.file',
|
'utf-16.file',
|
||||||
'utf-8.file',
|
'utf-8.file',
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ import contextlib
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
import importlib_resources as resources
|
import importlib_resources as resources
|
||||||
|
from .. import abc
|
||||||
from ..abc import TraversableResources, ResourceReader
|
from ..abc import TraversableResources, ResourceReader
|
||||||
from . import util
|
from . import util
|
||||||
from ._compat import os_helper
|
from .compat.py39 import os_helper
|
||||||
|
|
||||||
|
|
||||||
class SimpleLoader:
|
class SimpleLoader:
|
||||||
|
@ -38,8 +39,9 @@ class CustomTraversableResourcesTests(unittest.TestCase):
|
||||||
self.addCleanup(self.fixtures.close)
|
self.addCleanup(self.fixtures.close)
|
||||||
|
|
||||||
def test_custom_loader(self):
|
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))
|
loader = SimpleLoader(MagicResources(temp_dir))
|
||||||
pkg = util.create_package_from_loader(loader)
|
pkg = util.create_package_from_loader(loader)
|
||||||
files = resources.files(pkg)
|
files = resources.files(pkg)
|
||||||
assert files is temp_dir
|
assert isinstance(files, abc.Traversable)
|
||||||
|
assert list(files.iterdir()) == []
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import typing
|
|
||||||
import textwrap
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
@ -10,7 +9,8 @@ from ..abc import Traversable
|
||||||
from . import data01
|
from . import data01
|
||||||
from . import util
|
from . import util
|
||||||
from . import _path
|
from . import _path
|
||||||
from ._compat import os_helper, import_helper
|
from .compat.py39 import os_helper
|
||||||
|
from .compat.py312 import import_helper
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
|
@ -31,13 +31,14 @@ class FilesTests:
|
||||||
actual = files.joinpath('utf-8.file').read_text(encoding='utf-8')
|
actual = files.joinpath('utf-8.file').read_text(encoding='utf-8')
|
||||||
assert actual == 'Hello, UTF-8 world!\n'
|
assert actual == 'Hello, UTF-8 world!\n'
|
||||||
|
|
||||||
@unittest.skipUnless(
|
|
||||||
hasattr(typing, 'runtime_checkable'),
|
|
||||||
"Only suitable when typing supports runtime_checkable",
|
|
||||||
)
|
|
||||||
def test_traversable(self):
|
def test_traversable(self):
|
||||||
assert isinstance(resources.files(self.data), Traversable)
|
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):
|
def test_old_parameter(self):
|
||||||
"""
|
"""
|
||||||
Files used to take a 'package' parameter. Make sure anyone
|
Files used to take a 'package' parameter. Make sure anyone
|
||||||
|
@ -63,13 +64,17 @@ class OpenNamespaceTests(FilesTests, unittest.TestCase):
|
||||||
self.data = namespacedata01
|
self.data = namespacedata01
|
||||||
|
|
||||||
|
|
||||||
|
class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
|
||||||
|
ZIP_MODULE = 'namespacedata01'
|
||||||
|
|
||||||
|
|
||||||
class SiteDir:
|
class SiteDir:
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.fixtures = contextlib.ExitStack()
|
self.fixtures = contextlib.ExitStack()
|
||||||
self.addCleanup(self.fixtures.close)
|
self.addCleanup(self.fixtures.close)
|
||||||
self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())
|
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.DirsOnSysPath(self.site_dir))
|
||||||
self.fixtures.enter_context(import_helper.CleanImport())
|
self.fixtures.enter_context(import_helper.isolated_modules())
|
||||||
|
|
||||||
|
|
||||||
class ModulesFilesTests(SiteDir, unittest.TestCase):
|
class ModulesFilesTests(SiteDir, unittest.TestCase):
|
||||||
|
|
242
lib/importlib_resources/tests/test_functional.py
Normal file
242
lib/importlib_resources/tests/test_functional.py
Normal 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
|
|
@ -24,7 +24,7 @@ class OpenTests:
|
||||||
target = resources.files(self.data) / 'binary.file'
|
target = resources.files(self.data) / 'binary.file'
|
||||||
with target.open('rb') as fp:
|
with target.open('rb') as fp:
|
||||||
result = fp.read()
|
result = fp.read()
|
||||||
self.assertEqual(result, b'\x00\x01\x02\x03')
|
self.assertEqual(result, bytes(range(4)))
|
||||||
|
|
||||||
def test_open_text_default_encoding(self):
|
def test_open_text_default_encoding(self):
|
||||||
target = resources.files(self.data) / 'utf-8.file'
|
target = resources.files(self.data) / 'utf-8.file'
|
||||||
|
@ -81,5 +81,9 @@ class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
|
||||||
|
ZIP_MODULE = 'namespacedata01'
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import io
|
import io
|
||||||
|
import pathlib
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import importlib_resources as resources
|
import importlib_resources as resources
|
||||||
|
@ -15,18 +16,13 @@ class CommonTests(util.CommonTests, unittest.TestCase):
|
||||||
class PathTests:
|
class PathTests:
|
||||||
def test_reading(self):
|
def test_reading(self):
|
||||||
"""
|
"""
|
||||||
Path should be readable.
|
Path should be readable and a pathlib.Path instance.
|
||||||
|
|
||||||
Test also implicitly verifies the returned object is a pathlib.Path
|
|
||||||
instance.
|
|
||||||
"""
|
"""
|
||||||
target = resources.files(self.data) / 'utf-8.file'
|
target = resources.files(self.data) / 'utf-8.file'
|
||||||
with resources.as_file(target) as path:
|
with resources.as_file(target) as path:
|
||||||
|
self.assertIsInstance(path, pathlib.Path)
|
||||||
self.assertTrue(path.name.endswith("utf-8.file"), repr(path))
|
self.assertTrue(path.name.endswith("utf-8.file"), repr(path))
|
||||||
# pathlib.Path.read_text() was introduced in Python 3.5.
|
self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8'))
|
||||||
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):
|
class PathDiskTests(PathTests, unittest.TestCase):
|
||||||
|
|
|
@ -19,7 +19,7 @@ class CommonTextTests(util.CommonTests, unittest.TestCase):
|
||||||
class ReadTests:
|
class ReadTests:
|
||||||
def test_read_bytes(self):
|
def test_read_bytes(self):
|
||||||
result = resources.files(self.data).joinpath('binary.file').read_bytes()
|
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):
|
def test_read_text_default_encoding(self):
|
||||||
result = (
|
result = (
|
||||||
|
@ -58,17 +58,15 @@ class ReadDiskTests(ReadTests, unittest.TestCase):
|
||||||
|
|
||||||
class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
|
class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
|
||||||
def test_read_submodule_resource(self):
|
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()
|
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):
|
def test_read_submodule_resource_by_name(self):
|
||||||
result = (
|
result = (
|
||||||
resources.files('ziptestdata.subdirectory')
|
resources.files('data01.subdirectory').joinpath('binary.file').read_bytes()
|
||||||
.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):
|
class ReadNamespaceTests(ReadTests, unittest.TestCase):
|
||||||
|
@ -78,5 +76,22 @@ class ReadNamespaceTests(ReadTests, unittest.TestCase):
|
||||||
self.data = namespacedata01
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -10,8 +10,7 @@ from importlib_resources.readers import MultiplexedPath, NamespaceReader
|
||||||
class MultiplexedPathTest(unittest.TestCase):
|
class MultiplexedPathTest(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
path = pathlib.Path(__file__).parent / 'namespacedata01'
|
cls.folder = pathlib.Path(__file__).parent / 'namespacedata01'
|
||||||
cls.folder = str(path)
|
|
||||||
|
|
||||||
def test_init_no_paths(self):
|
def test_init_no_paths(self):
|
||||||
with self.assertRaises(FileNotFoundError):
|
with self.assertRaises(FileNotFoundError):
|
||||||
|
@ -19,7 +18,7 @@ class MultiplexedPathTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_init_file(self):
|
def test_init_file(self):
|
||||||
with self.assertRaises(NotADirectoryError):
|
with self.assertRaises(NotADirectoryError):
|
||||||
MultiplexedPath(os.path.join(self.folder, 'binary.file'))
|
MultiplexedPath(self.folder / 'binary.file')
|
||||||
|
|
||||||
def test_iterdir(self):
|
def test_iterdir(self):
|
||||||
contents = {path.name for path in MultiplexedPath(self.folder).iterdir()}
|
contents = {path.name for path in MultiplexedPath(self.folder).iterdir()}
|
||||||
|
@ -27,10 +26,12 @@ class MultiplexedPathTest(unittest.TestCase):
|
||||||
contents.remove('__pycache__')
|
contents.remove('__pycache__')
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
pass
|
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):
|
def test_iterdir_duplicate(self):
|
||||||
data01 = os.path.abspath(os.path.join(__file__, '..', 'data01'))
|
data01 = pathlib.Path(__file__).parent.joinpath('data01')
|
||||||
contents = {
|
contents = {
|
||||||
path.name for path in MultiplexedPath(self.folder, data01).iterdir()
|
path.name for path in MultiplexedPath(self.folder, data01).iterdir()
|
||||||
}
|
}
|
||||||
|
@ -60,17 +61,17 @@ class MultiplexedPathTest(unittest.TestCase):
|
||||||
path.open()
|
path.open()
|
||||||
|
|
||||||
def test_join_path(self):
|
def test_join_path(self):
|
||||||
prefix = os.path.abspath(os.path.join(__file__, '..'))
|
data01 = pathlib.Path(__file__).parent.joinpath('data01')
|
||||||
data01 = os.path.join(prefix, 'data01')
|
prefix = str(data01.parent)
|
||||||
path = MultiplexedPath(self.folder, data01)
|
path = MultiplexedPath(self.folder, data01)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str(path.joinpath('binary.file'))[len(prefix) + 1 :],
|
str(path.joinpath('binary.file'))[len(prefix) + 1 :],
|
||||||
os.path.join('namespacedata01', 'binary.file'),
|
os.path.join('namespacedata01', 'binary.file'),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
sub = path.joinpath('subdirectory')
|
||||||
str(path.joinpath('subdirectory'))[len(prefix) + 1 :],
|
assert isinstance(sub, MultiplexedPath)
|
||||||
os.path.join('data01', 'subdirectory'),
|
assert 'namespacedata01' in str(sub)
|
||||||
)
|
assert 'data01' in str(sub)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str(path.joinpath('imaginary'))[len(prefix) + 1 :],
|
str(path.joinpath('imaginary'))[len(prefix) + 1 :],
|
||||||
os.path.join('namespacedata01', 'imaginary'),
|
os.path.join('namespacedata01', 'imaginary'),
|
||||||
|
@ -82,9 +83,9 @@ class MultiplexedPathTest(unittest.TestCase):
|
||||||
assert not path.joinpath('imaginary/foo.py').exists()
|
assert not path.joinpath('imaginary/foo.py').exists()
|
||||||
|
|
||||||
def test_join_path_common_subdir(self):
|
def test_join_path_common_subdir(self):
|
||||||
prefix = os.path.abspath(os.path.join(__file__, '..'))
|
data01 = pathlib.Path(__file__).parent.joinpath('data01')
|
||||||
data01 = os.path.join(prefix, 'data01')
|
data02 = pathlib.Path(__file__).parent.joinpath('data02')
|
||||||
data02 = os.path.join(prefix, 'data02')
|
prefix = str(data01.parent)
|
||||||
path = MultiplexedPath(data01, data02)
|
path = MultiplexedPath(data01, data02)
|
||||||
self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath)
|
self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
import contextlib
|
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
import importlib_resources as resources
|
import importlib_resources as resources
|
||||||
import uuid
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
from . import data01
|
from . import data01
|
||||||
from . import zipdata01, zipdata02
|
|
||||||
from . import util
|
from . import util
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from ._compat import import_helper, os_helper, unlink
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceTests:
|
class ResourceTests:
|
||||||
|
@ -89,34 +85,32 @@ class ResourceCornerCaseTests(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
|
class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
|
||||||
ZIP_MODULE = zipdata01 # type: ignore
|
ZIP_MODULE = 'data01'
|
||||||
|
|
||||||
def test_is_submodule_resource(self):
|
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())
|
self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file())
|
||||||
|
|
||||||
def test_read_submodule_resource_by_name(self):
|
def test_read_submodule_resource_by_name(self):
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
resources.files('ziptestdata.subdirectory')
|
resources.files('data01.subdirectory').joinpath('binary.file').is_file()
|
||||||
.joinpath('binary.file')
|
|
||||||
.is_file()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_submodule_contents(self):
|
def test_submodule_contents(self):
|
||||||
submodule = import_module('ziptestdata.subdirectory')
|
submodule = import_module('data01.subdirectory')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
names(resources.files(submodule)), {'__init__.py', 'binary.file'}
|
names(resources.files(submodule)), {'__init__.py', 'binary.file'}
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_submodule_contents_by_name(self):
|
def test_submodule_contents_by_name(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
names(resources.files('ziptestdata.subdirectory')),
|
names(resources.files('data01.subdirectory')),
|
||||||
{'__init__.py', 'binary.file'},
|
{'__init__.py', 'binary.file'},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_as_file_directory(self):
|
def test_as_file_directory(self):
|
||||||
with resources.as_file(resources.files('ziptestdata')) as data:
|
with resources.as_file(resources.files('data01')) as data:
|
||||||
assert data.name == 'ziptestdata'
|
assert data.name == 'data01'
|
||||||
assert data.is_dir()
|
assert data.is_dir()
|
||||||
assert data.joinpath('subdirectory').is_dir()
|
assert data.joinpath('subdirectory').is_dir()
|
||||||
assert len(list(data.iterdir()))
|
assert len(list(data.iterdir()))
|
||||||
|
@ -124,7 +118,7 @@ class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
|
class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
|
||||||
ZIP_MODULE = zipdata02 # type: ignore
|
ZIP_MODULE = 'data02'
|
||||||
|
|
||||||
def test_unrelated_contents(self):
|
def test_unrelated_contents(self):
|
||||||
"""
|
"""
|
||||||
|
@ -132,93 +126,48 @@ class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
|
||||||
distinct resources. Ref python/importlib_resources#44.
|
distinct resources. Ref python/importlib_resources#44.
|
||||||
"""
|
"""
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
names(resources.files('ziptestdata.one')),
|
names(resources.files('data02.one')),
|
||||||
{'__init__.py', 'resource1.txt'},
|
{'__init__.py', 'resource1.txt'},
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
names(resources.files('ziptestdata.two')),
|
names(resources.files('data02.two')),
|
||||||
{'__init__.py', 'resource2.txt'},
|
{'__init__.py', 'resource2.txt'},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
class DeletingZipsTest(util.ZipSetupBase, unittest.TestCase):
|
||||||
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):
|
|
||||||
"""Having accessed resources in a zip file should not keep an open
|
"""Having accessed resources in a zip file should not keep an open
|
||||||
reference to the zip.
|
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):
|
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):
|
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):
|
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.")
|
@unittest.skip("Desired but not supported.")
|
||||||
def test_as_file_does_not_keep_open(self): # pragma: no cover
|
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):
|
def test_entered_path_does_not_keep_open(self):
|
||||||
"""
|
"""
|
||||||
Mimic what certifi does on import to make its bundle
|
Mimic what certifi does on import to make its bundle
|
||||||
available for the process duration.
|
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):
|
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):
|
def test_read_text_does_not_keep_open(self):
|
||||||
resources.files('ziptestdata').joinpath('utf-8.file').read_text(
|
resources.files('data01').joinpath('utf-8.file').read_text(encoding='utf-8')
|
||||||
encoding='utf-8'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceFromNamespaceTest01(unittest.TestCase):
|
class ResourceFromNamespaceTests:
|
||||||
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):
|
def test_is_submodule_resource(self):
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
resources.files(import_module('namespacedata01'))
|
resources.files(import_module('namespacedata01'))
|
||||||
|
@ -237,7 +186,9 @@ class ResourceFromNamespaceTest01(unittest.TestCase):
|
||||||
contents.remove('__pycache__')
|
contents.remove('__pycache__')
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
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):
|
def test_submodule_contents_by_name(self):
|
||||||
contents = names(resources.files('namespacedata01'))
|
contents = names(resources.files('namespacedata01'))
|
||||||
|
@ -245,7 +196,45 @@ class ResourceFromNamespaceTest01(unittest.TestCase):
|
||||||
contents.remove('__pycache__')
|
contents.remove('__pycache__')
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -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()
|
|
|
@ -4,11 +4,12 @@ import io
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import contextlib
|
||||||
|
|
||||||
from . import data01
|
from . import data01
|
||||||
from . import zipdata01
|
|
||||||
from ..abc import ResourceReader
|
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
|
from importlib.machinery import ModuleSpec
|
||||||
|
@ -141,39 +142,23 @@ class CommonTests(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
|
|
||||||
class ZipSetupBase:
|
class ZipSetupBase:
|
||||||
ZIP_MODULE = None
|
ZIP_MODULE = 'data01'
|
||||||
|
|
||||||
@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):
|
def setUp(self):
|
||||||
modules = import_helper.modules_setup()
|
self.fixtures = contextlib.ExitStack()
|
||||||
self.addCleanup(import_helper.modules_cleanup, *modules)
|
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):
|
class ZipSetup(ZipSetupBase):
|
||||||
ZIP_MODULE = zipdata01 # type: ignore
|
pass
|
||||||
|
|
32
lib/importlib_resources/tests/zip.py
Normal file
32
lib/importlib_resources/tests/zip.py
Normal 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
|
Binary file not shown.
Binary file not shown.
|
@ -5,9 +5,10 @@ import itertools
|
||||||
import contextlib
|
import contextlib
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
from .py310compat import text_encoding
|
from .compat.py310 import text_encoding
|
||||||
from .glob import translate
|
from .glob import Translator
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Path']
|
__all__ = ['Path']
|
||||||
|
@ -148,6 +149,16 @@ class CompleteDirs(InitializedState, zipfile.ZipFile):
|
||||||
source.__class__ = cls
|
source.__class__ = cls
|
||||||
return source
|
return source
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def inject(cls, zf: zipfile.ZipFile) -> zipfile.ZipFile:
|
||||||
|
"""
|
||||||
|
Given a writable zip file zf, inject directory entries for
|
||||||
|
any directories implied by the presence of children.
|
||||||
|
"""
|
||||||
|
for name in cls._implied_dirs(zf.namelist()):
|
||||||
|
zf.writestr(name, b"")
|
||||||
|
return zf
|
||||||
|
|
||||||
|
|
||||||
class FastLookup(CompleteDirs):
|
class FastLookup(CompleteDirs):
|
||||||
"""
|
"""
|
||||||
|
@ -169,8 +180,10 @@ class FastLookup(CompleteDirs):
|
||||||
|
|
||||||
|
|
||||||
def _extract_text_encoding(encoding=None, *args, **kwargs):
|
def _extract_text_encoding(encoding=None, *args, **kwargs):
|
||||||
# stacklevel=3 so that the caller of the caller see any warning.
|
# compute stack level so that the caller of the caller sees any warning.
|
||||||
return text_encoding(encoding, 3), args, kwargs
|
is_pypy = sys.implementation.name == 'pypy'
|
||||||
|
stack_level = 3 + is_pypy
|
||||||
|
return text_encoding(encoding, stack_level), args, kwargs
|
||||||
|
|
||||||
|
|
||||||
class Path:
|
class Path:
|
||||||
|
@ -195,13 +208,13 @@ class Path:
|
||||||
|
|
||||||
Path accepts the zipfile object itself or a filename
|
Path accepts the zipfile object itself or a filename
|
||||||
|
|
||||||
>>> root = Path(zf)
|
>>> path = Path(zf)
|
||||||
|
|
||||||
From there, several path operations are available.
|
From there, several path operations are available.
|
||||||
|
|
||||||
Directory iteration (including the zip file itself):
|
Directory iteration (including the zip file itself):
|
||||||
|
|
||||||
>>> a, b = root.iterdir()
|
>>> a, b = path.iterdir()
|
||||||
>>> a
|
>>> a
|
||||||
Path('mem/abcde.zip', 'a.txt')
|
Path('mem/abcde.zip', 'a.txt')
|
||||||
>>> b
|
>>> b
|
||||||
|
@ -239,16 +252,38 @@ class Path:
|
||||||
'mem/abcde.zip/b/c.txt'
|
'mem/abcde.zip/b/c.txt'
|
||||||
|
|
||||||
At the root, ``name``, ``filename``, and ``parent``
|
At the root, ``name``, ``filename``, and ``parent``
|
||||||
resolve to the zipfile. Note these attributes are not
|
resolve to the zipfile.
|
||||||
valid and will raise a ``ValueError`` if the zipfile
|
|
||||||
has no filename.
|
|
||||||
|
|
||||||
>>> root.name
|
>>> str(path)
|
||||||
|
'mem/abcde.zip/'
|
||||||
|
>>> path.name
|
||||||
'abcde.zip'
|
'abcde.zip'
|
||||||
>>> str(root.filename).replace(os.sep, posixpath.sep)
|
>>> path.filename == pathlib.Path('mem/abcde.zip')
|
||||||
'mem/abcde.zip'
|
True
|
||||||
>>> str(root.parent)
|
>>> str(path.parent)
|
||||||
'mem'
|
'mem'
|
||||||
|
|
||||||
|
If the zipfile has no filename, such attribtues are not
|
||||||
|
valid and accessing them will raise an Exception.
|
||||||
|
|
||||||
|
>>> zf.filename = None
|
||||||
|
>>> path.name
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: ...
|
||||||
|
|
||||||
|
>>> path.filename
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: ...
|
||||||
|
|
||||||
|
>>> path.parent
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: ...
|
||||||
|
|
||||||
|
# workaround python/cpython#106763
|
||||||
|
>>> pass
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
|
__repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
|
||||||
|
@ -365,8 +400,10 @@ class Path:
|
||||||
raise ValueError(f"Unacceptable pattern: {pattern!r}")
|
raise ValueError(f"Unacceptable pattern: {pattern!r}")
|
||||||
|
|
||||||
prefix = re.escape(self.at)
|
prefix = re.escape(self.at)
|
||||||
matches = re.compile(prefix + translate(pattern)).fullmatch
|
tr = Translator(seps='/')
|
||||||
return map(self._next, filter(matches, self.root.namelist()))
|
matches = re.compile(prefix + tr.translate(pattern)).fullmatch
|
||||||
|
names = (data.filename for data in self.root.filelist)
|
||||||
|
return map(self._next, filter(matches, names))
|
||||||
|
|
||||||
def rglob(self, pattern):
|
def rglob(self, pattern):
|
||||||
return self.glob(f'**/{pattern}')
|
return self.glob(f'**/{pattern}')
|
||||||
|
|
0
lib/zipp/compat/__init__.py
Normal file
0
lib/zipp/compat/__init__.py
Normal file
112
lib/zipp/glob.py
112
lib/zipp/glob.py
|
@ -1,18 +1,97 @@
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
def translate(pattern):
|
_default_seps = os.sep + str(os.altsep) * bool(os.altsep)
|
||||||
r"""
|
|
||||||
Given a glob pattern, produce a regex that matches it.
|
|
||||||
|
|
||||||
>>> translate('*.txt')
|
|
||||||
'[^/]*\\.txt'
|
class Translator:
|
||||||
>>> translate('a?txt')
|
|
||||||
'a.txt'
|
|
||||||
>>> translate('**/*')
|
|
||||||
'.*/[^/]*'
|
|
||||||
"""
|
"""
|
||||||
return ''.join(map(replace, separate(pattern)))
|
>>> Translator('xyz')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AssertionError: Invalid separators
|
||||||
|
|
||||||
|
>>> Translator('')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AssertionError: Invalid separators
|
||||||
|
"""
|
||||||
|
|
||||||
|
seps: str
|
||||||
|
|
||||||
|
def __init__(self, seps: str = _default_seps):
|
||||||
|
assert seps and set(seps) <= set(_default_seps), "Invalid separators"
|
||||||
|
self.seps = seps
|
||||||
|
|
||||||
|
def translate(self, pattern):
|
||||||
|
"""
|
||||||
|
Given a glob pattern, produce a regex that matches it.
|
||||||
|
"""
|
||||||
|
return self.extend(self.translate_core(pattern))
|
||||||
|
|
||||||
|
def extend(self, pattern):
|
||||||
|
r"""
|
||||||
|
Extend regex for pattern-wide concerns.
|
||||||
|
|
||||||
|
Apply '(?s:)' to create a non-matching group that
|
||||||
|
matches newlines (valid on Unix).
|
||||||
|
|
||||||
|
Append '\Z' to imply fullmatch even when match is used.
|
||||||
|
"""
|
||||||
|
return rf'(?s:{pattern})\Z'
|
||||||
|
|
||||||
|
def translate_core(self, pattern):
|
||||||
|
r"""
|
||||||
|
Given a glob pattern, produce a regex that matches it.
|
||||||
|
|
||||||
|
>>> t = Translator()
|
||||||
|
>>> t.translate_core('*.txt').replace('\\\\', '')
|
||||||
|
'[^/]*\\.txt'
|
||||||
|
>>> t.translate_core('a?txt')
|
||||||
|
'a[^/]txt'
|
||||||
|
>>> t.translate_core('**/*').replace('\\\\', '')
|
||||||
|
'.*/[^/][^/]*'
|
||||||
|
"""
|
||||||
|
self.restrict_rglob(pattern)
|
||||||
|
return ''.join(map(self.replace, separate(self.star_not_empty(pattern))))
|
||||||
|
|
||||||
|
def replace(self, match):
|
||||||
|
"""
|
||||||
|
Perform the replacements for a match from :func:`separate`.
|
||||||
|
"""
|
||||||
|
return match.group('set') or (
|
||||||
|
re.escape(match.group(0))
|
||||||
|
.replace('\\*\\*', r'.*')
|
||||||
|
.replace('\\*', rf'[^{re.escape(self.seps)}]*')
|
||||||
|
.replace('\\?', r'[^/]')
|
||||||
|
)
|
||||||
|
|
||||||
|
def restrict_rglob(self, pattern):
|
||||||
|
"""
|
||||||
|
Raise ValueError if ** appears in anything but a full path segment.
|
||||||
|
|
||||||
|
>>> Translator().translate('**foo')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: ** must appear alone in a path segment
|
||||||
|
"""
|
||||||
|
seps_pattern = rf'[{re.escape(self.seps)}]+'
|
||||||
|
segments = re.split(seps_pattern, pattern)
|
||||||
|
if any('**' in segment and segment != '**' for segment in segments):
|
||||||
|
raise ValueError("** must appear alone in a path segment")
|
||||||
|
|
||||||
|
def star_not_empty(self, pattern):
|
||||||
|
"""
|
||||||
|
Ensure that * will not match an empty segment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle_segment(match):
|
||||||
|
segment = match.group(0)
|
||||||
|
return '?*' if segment == '*' else segment
|
||||||
|
|
||||||
|
not_seps_pattern = rf'[^{re.escape(self.seps)}]+'
|
||||||
|
return re.sub(not_seps_pattern, handle_segment, pattern)
|
||||||
|
|
||||||
|
|
||||||
def separate(pattern):
|
def separate(pattern):
|
||||||
|
@ -25,16 +104,3 @@ def separate(pattern):
|
||||||
['a', '[?]', 'txt']
|
['a', '[?]', 'txt']
|
||||||
"""
|
"""
|
||||||
return re.finditer(r'([^\[]+)|(?P<set>[\[].*?[\]])|([\[][^\]]*$)', pattern)
|
return re.finditer(r'([^\[]+)|(?P<set>[\[].*?[\]])|([\[][^\]]*$)', pattern)
|
||||||
|
|
||||||
|
|
||||||
def replace(match):
|
|
||||||
"""
|
|
||||||
Perform the replacements for a match from :func:`separate`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return match.group('set') or (
|
|
||||||
re.escape(match.group(0))
|
|
||||||
.replace('\\*\\*', r'.*')
|
|
||||||
.replace('\\*', r'[^/]*')
|
|
||||||
.replace('\\?', r'.')
|
|
||||||
)
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue