Bump importlib-resources from 6.4.0 to 6.4.5 (#2394)

* Bump importlib-resources from 6.4.0 to 6.4.5

Bumps [importlib-resources](https://github.com/python/importlib_resources) from 6.4.0 to 6.4.5.
- [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.4.0...v6.4.5)

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

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

* Update importlib-resources==6.4.5

---------

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-11-16 14:48:10 -08:00 committed by GitHub
parent f3a2c02e96
commit 01589cb8b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 484 additions and 316 deletions

View file

@ -1,4 +1,11 @@
"""Read resources contained within a package."""
"""
Read resources contained within a package.
This codebase is shared between importlib.resources in the stdlib
and importlib_resources in PyPI. See
https://github.com/python/importlib_metadata/wiki/Development-Methodology
for more detail.
"""
from ._common import (
as_file,
@ -7,7 +14,7 @@ from ._common import (
Anchor,
)
from .functional import (
from ._functional import (
contents,
is_resource,
open_binary,

View file

@ -66,10 +66,10 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
# zipimport.zipimporter does not support weak references, resulting in a
# TypeError. That seems terrible.
spec = package.__spec__
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore[union-attr]
if reader is None:
return None
return reader(spec.name) # type: ignore
return reader(spec.name) # type: ignore[union-attr]
@functools.singledispatch
@ -93,12 +93,13 @@ def _infer_caller():
"""
def is_this_file(frame_info):
return frame_info.filename == __file__
return frame_info.filename == stack[0].filename
def is_wrapper(frame_info):
return frame_info.function == 'wrapper'
not_this_file = itertools.filterfalse(is_this_file, inspect.stack())
stack = inspect.stack()
not_this_file = itertools.filterfalse(is_this_file, stack)
# also exclude 'wrapper' due to singledispatch in the call stack
callers = itertools.filterfalse(is_wrapper, not_this_file)
return next(callers).frame
@ -182,7 +183,7 @@ def _(path):
@contextlib.contextmanager
def _temp_path(dir: tempfile.TemporaryDirectory):
"""
Wrap tempfile.TemporyDirectory to return a pathlib object.
Wrap tempfile.TemporaryDirectory to return a pathlib object.
"""
with dir as result:
yield pathlib.Path(result)

View file

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

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import collections
import contextlib
import itertools
@ -5,6 +7,7 @@ import pathlib
import operator
import re
import warnings
from collections.abc import Iterator
from . import abc
@ -34,8 +37,10 @@ class FileReader(abc.TraversableResources):
class ZipReader(abc.TraversableResources):
def __init__(self, loader, module):
_, _, name = module.rpartition('.')
self.prefix = loader.prefix.replace('\\', '/') + name + '/'
self.prefix = loader.prefix.replace('\\', '/')
if loader.is_package(module):
_, _, name = module.rpartition('.')
self.prefix += name + '/'
self.archive = loader.archive
def open_resource(self, resource):
@ -133,27 +138,31 @@ class NamespaceReader(abc.TraversableResources):
def __init__(self, namespace_path):
if 'NamespacePath' not in str(namespace_path):
raise ValueError('Invalid path')
self.path = MultiplexedPath(*map(self._resolve, namespace_path))
self.path = MultiplexedPath(*filter(bool, map(self._resolve, namespace_path)))
@classmethod
def _resolve(cls, path_str) -> abc.Traversable:
def _resolve(cls, path_str) -> abc.Traversable | None:
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``.
path_str might also be a sentinel used by editable packages to
trigger other behaviors (see python/importlib_resources#311).
In that case, return None.
"""
(dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())
return dir
dirs = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())
return next(dirs, None)
@classmethod
def _candidate_paths(cls, path_str):
def _candidate_paths(cls, path_str: str) -> Iterator[abc.Traversable]:
yield pathlib.Path(path_str)
yield from cls._resolve_zip_path(path_str)
@staticmethod
def _resolve_zip_path(path_str):
def _resolve_zip_path(path_str: str):
for match in reversed(list(re.finditer(r'[\\/]', path_str))):
with contextlib.suppress(
FileNotFoundError,

View file

@ -77,7 +77,7 @@ class ResourceHandle(Traversable):
def __init__(self, parent: ResourceContainer, name: str):
self.parent = parent
self.name = name # type: ignore
self.name = name # type: ignore[misc]
def is_file(self):
return True

View file

@ -2,15 +2,44 @@ import pathlib
import functools
from typing import Dict, Union
from typing import runtime_checkable
from typing import Protocol
####
# from jaraco.path 3.4.1
FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore
# from jaraco.path 3.7.1
def build(spec: FilesSpec, prefix=pathlib.Path()):
class Symlink(str):
"""
A string indicating the target of a symlink.
"""
FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']]
@runtime_checkable
class TreeMaker(Protocol):
def __truediv__(self, *args, **kwargs): ... # pragma: no cover
def mkdir(self, **kwargs): ... # pragma: no cover
def write_text(self, content, **kwargs): ... # pragma: no cover
def write_bytes(self, content): ... # pragma: no cover
def symlink_to(self, target): ... # pragma: no cover
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value]
def build(
spec: FilesSpec,
prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment]
):
"""
Build a set of files/directories, as described by the spec.
@ -25,21 +54,25 @@ def build(spec: FilesSpec, prefix=pathlib.Path()):
... "__init__.py": "",
... },
... "baz.py": "# Some code",
... }
... "bar.py": Symlink("baz.py"),
... },
... "bing": Symlink("foo"),
... }
>>> target = getfixture('tmp_path')
>>> build(spec, target)
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
'# Some code'
>>> target.joinpath('bing/bar.py').read_text(encoding='utf-8')
'# Some code'
"""
for name, contents in spec.items():
create(contents, pathlib.Path(prefix) / name)
create(contents, _ensure_tree_maker(prefix) / name)
@functools.singledispatch
def create(content: Union[str, bytes, FilesSpec], path):
path.mkdir(exist_ok=True)
build(content, prefix=path) # type: ignore
build(content, prefix=path) # type: ignore[arg-type]
@create.register
@ -52,5 +85,10 @@ def _(content: str, path):
path.write_text(content, encoding='utf-8')
@create.register
def _(content: Symlink, path):
path.symlink_to(content)
# end from jaraco.path
####

View file

@ -8,3 +8,6 @@ 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')
warnings_helper = try_import('warnings_helper') or from_test_support(
'ignore_warnings', 'check_warnings'
)

View file

@ -1 +0,0 @@
Hello, UTF-8 world!

View file

@ -1 +0,0 @@
one resource

View file

@ -1 +0,0 @@
two resource

View file

@ -1 +0,0 @@
Hello, UTF-8 world!

View file

@ -1,7 +1,6 @@
import unittest
import importlib_resources as resources
from . import data01
from . import util
@ -19,16 +18,17 @@ class ContentsTests:
assert self.expected <= contents
class ContentsDiskTests(ContentsTests, unittest.TestCase):
def setUp(self):
self.data = data01
class ContentsDiskTests(ContentsTests, util.DiskSetup, unittest.TestCase):
pass
class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase):
pass
class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
class ContentsNamespaceTests(ContentsTests, util.DiskSetup, unittest.TestCase):
MODULE = 'namespacedata01'
expected = {
# no __init__ because of namespace design
'binary.file',
@ -36,8 +36,3 @@ class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
'utf-16.file',
'utf-8.file',
}
def setUp(self):
from . import namespacedata01
self.data = namespacedata01

View file

@ -1,3 +1,7 @@
import os
import pathlib
import py_compile
import shutil
import textwrap
import unittest
import warnings
@ -6,11 +10,8 @@ import contextlib
import importlib_resources as resources
from ..abc import Traversable
from . import data01
from . import util
from . import _path
from .compat.py39 import os_helper
from .compat.py312 import import_helper
from .compat.py39 import os_helper, import_helper
@contextlib.contextmanager
@ -48,70 +49,146 @@ class FilesTests:
resources.files(package=self.data)
class OpenDiskTests(FilesTests, unittest.TestCase):
def setUp(self):
self.data = data01
class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase):
pass
class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
pass
class OpenNamespaceTests(FilesTests, unittest.TestCase):
def setUp(self):
from . import namespacedata01
class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase):
MODULE = 'namespacedata01'
self.data = namespacedata01
def test_non_paths_in_dunder_path(self):
"""
Non-path items in a namespace package's ``__path__`` are ignored.
As reported in python/importlib_resources#311, some tools
like Setuptools, when creating editable packages, will inject
non-paths into a namespace package's ``__path__``, a
sentinel like
``__editable__.sample_namespace-1.0.finder.__path_hook__``
to cause the ``PathEntryFinder`` to be called when searching
for packages. In that case, resources should still be loadable.
"""
import namespacedata01
namespacedata01.__path__.append(
'__editable__.sample_namespace-1.0.finder.__path_hook__'
)
resources.files(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.isolated_modules())
class DirectSpec:
"""
Override behavior of ModuleSetup to write a full spec directly.
"""
MODULE = 'unused'
def load_fixture(self, name):
self.tree_on_path(self.spec)
class ModulesFilesTests(SiteDir, unittest.TestCase):
class ModulesFiles:
spec = {
'mod.py': '',
'res.txt': 'resources are the best',
}
def test_module_resources(self):
"""
A module can have resources found adjacent to the module.
"""
spec = {
'mod.py': '',
'res.txt': 'resources are the best',
}
_path.build(spec, self.site_dir)
import mod
import mod # type: ignore[import-not-found]
actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8')
assert actual == spec['res.txt']
assert actual == self.spec['res.txt']
class ImplicitContextFilesTests(SiteDir, unittest.TestCase):
def test_implicit_files(self):
class ModuleFilesDiskTests(DirectSpec, util.DiskSetup, ModulesFiles, unittest.TestCase):
pass
class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.TestCase):
pass
class ImplicitContextFiles:
set_val = textwrap.dedent(
f"""
import {resources.__name__} as res
val = res.files().joinpath('res.txt').read_text(encoding='utf-8')
"""
)
spec = {
'somepkg': {
'__init__.py': set_val,
'submod.py': set_val,
'res.txt': 'resources are the best',
},
'frozenpkg': {
'__init__.py': set_val.replace(resources.__name__, 'c_resources'),
'res.txt': 'resources are the best',
},
}
def test_implicit_files_package(self):
"""
Without any parameter, files() will infer the location as the caller.
"""
spec = {
'somepkg': {
'__init__.py': textwrap.dedent(
"""
import importlib_resources as res
val = res.files().joinpath('res.txt').read_text(encoding='utf-8')
"""
),
'res.txt': 'resources are the best',
},
}
_path.build(spec, self.site_dir)
assert importlib.import_module('somepkg').val == 'resources are the best'
def test_implicit_files_submodule(self):
"""
Without any parameter, files() will infer the location as the caller.
"""
assert importlib.import_module('somepkg.submod').val == 'resources are the best'
def _compile_importlib(self):
"""
Make a compiled-only copy of the importlib resources package.
"""
bin_site = self.fixtures.enter_context(os_helper.temp_dir())
c_resources = pathlib.Path(bin_site, 'c_resources')
sources = pathlib.Path(resources.__file__).parent
shutil.copytree(sources, c_resources, ignore=lambda *_: ['__pycache__'])
for dirpath, _, filenames in os.walk(c_resources):
for filename in filenames:
source_path = pathlib.Path(dirpath) / filename
cfile = source_path.with_suffix('.pyc')
py_compile.compile(source_path, cfile)
pathlib.Path.unlink(source_path)
self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site))
def test_implicit_files_with_compiled_importlib(self):
"""
Caller detection works for compiled-only resources module.
python/cpython#123085
"""
self._compile_importlib()
assert importlib.import_module('frozenpkg').val == 'resources are the best'
class ImplicitContextFilesDiskTests(
DirectSpec, util.DiskSetup, ImplicitContextFiles, unittest.TestCase
):
pass
class ImplicitContextFilesZipTests(
DirectSpec, util.ZipSetup, ImplicitContextFiles, unittest.TestCase
):
pass
if __name__ == '__main__':
unittest.main()

View file

@ -1,31 +1,38 @@
import unittest
import os
import contextlib
import importlib
try:
from test.support.warnings_helper import ignore_warnings, check_warnings
except ImportError:
# older Python versions
from test.support import ignore_warnings, check_warnings
from .compat.py39 import warnings_helper
import importlib_resources as resources
from . import util
# 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'
anchor01 = 'data01'
anchor02 = 'data02'
class ModuleAnchorMixin:
from . import data01 as anchor01
from . import data02 as anchor02
@property
def anchor01(self):
return importlib.import_module('data01')
@property
def anchor02(self):
return importlib.import_module('data02')
class FunctionalAPIBase:
class FunctionalAPIBase(util.DiskSetup):
def setUp(self):
super().setUp()
self.load_fixture('data02')
def _gen_resourcetxt_path_parts(self):
"""Yield various names of a text file in anchor02, each in a subTest"""
for path_parts in (
@ -36,6 +43,12 @@ class FunctionalAPIBase:
with self.subTest(path_parts=path_parts):
yield path_parts
def assertEndsWith(self, string, suffix):
"""Assert that `string` ends with `suffix`.
Used to ignore an architecture-specific UTF-16 byte-order mark."""
self.assertEqual(string[-len(suffix) :], suffix)
def test_read_text(self):
self.assertEqual(
resources.read_text(self.anchor01, 'utf-8.file'),
@ -76,13 +89,13 @@ class FunctionalAPIBase:
),
'\x00\x01\x02\x03',
)
self.assertEqual(
self.assertEndsWith( # ignore the BOM
resources.read_text(
self.anchor01,
'utf-16.file',
errors='backslashreplace',
),
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
errors='backslashreplace',
),
)
@ -128,9 +141,9 @@ class FunctionalAPIBase:
'utf-16.file',
errors='backslashreplace',
) as f:
self.assertEqual(
self.assertEndsWith( # ignore the BOM
f.read(),
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
errors='backslashreplace',
),
)
@ -163,32 +176,32 @@ class FunctionalAPIBase:
self.assertTrue(is_resource(self.anchor02, *path_parts))
def test_contents(self):
with check_warnings((".*contents.*", DeprecationWarning)):
with warnings_helper.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)))
with self.assertRaises(OSError), warnings_helper.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)))
with self.assertRaises(OSError), warnings_helper.check_warnings((
".*contents.*",
DeprecationWarning,
)):
list(resources.contents(self.anchor01, *path_parts))
with check_warnings((".*contents.*", DeprecationWarning)):
with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)):
c = resources.contents(self.anchor01, 'subdirectory')
self.assertGreaterEqual(
set(c),
{'binary.file'},
)
@ignore_warnings(category=DeprecationWarning)
@warnings_helper.ignore_warnings(category=DeprecationWarning)
def test_common_errors(self):
for func in (
resources.read_text,
@ -227,16 +240,16 @@ class FunctionalAPIBase:
class FunctionalAPITest_StringAnchor(
unittest.TestCase,
FunctionalAPIBase,
StringAnchorMixin,
FunctionalAPIBase,
unittest.TestCase,
):
pass
class FunctionalAPITest_ModuleAnchor(
unittest.TestCase,
FunctionalAPIBase,
ModuleAnchorMixin,
FunctionalAPIBase,
unittest.TestCase,
):
pass

View file

@ -1,7 +1,6 @@
import unittest
import importlib_resources as resources
from . import data01
from . import util
@ -65,16 +64,12 @@ class OpenTests:
target.open(encoding='utf-8')
class OpenDiskTests(OpenTests, unittest.TestCase):
def setUp(self):
self.data = data01
class OpenDiskTests(OpenTests, util.DiskSetup, unittest.TestCase):
pass
class OpenDiskNamespaceTests(OpenTests, unittest.TestCase):
def setUp(self):
from . import namespacedata01
self.data = namespacedata01
class OpenDiskNamespaceTests(OpenTests, util.DiskSetup, unittest.TestCase):
MODULE = 'namespacedata01'
class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
@ -82,7 +77,7 @@ class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01'
MODULE = 'namespacedata01'
if __name__ == '__main__':

View file

@ -3,7 +3,6 @@ import pathlib
import unittest
import importlib_resources as resources
from . import data01
from . import util
@ -25,9 +24,7 @@ class PathTests:
self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8'))
class PathDiskTests(PathTests, unittest.TestCase):
data = data01
class PathDiskTests(PathTests, util.DiskSetup, unittest.TestCase):
def test_natural_path(self):
"""
Guarantee the internal implementation detail that

View file

@ -1,7 +1,6 @@
import unittest
import importlib_resources as resources
from . import data01
from . import util
from importlib import import_module
@ -52,8 +51,8 @@ class ReadTests:
)
class ReadDiskTests(ReadTests, unittest.TestCase):
data = data01
class ReadDiskTests(ReadTests, util.DiskSetup, unittest.TestCase):
pass
class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
@ -69,15 +68,12 @@ class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
self.assertEqual(result, bytes(range(4, 8)))
class ReadNamespaceTests(ReadTests, unittest.TestCase):
def setUp(self):
from . import namespacedata01
self.data = namespacedata01
class ReadNamespaceTests(ReadTests, util.DiskSetup, unittest.TestCase):
MODULE = 'namespacedata01'
class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01'
MODULE = 'namespacedata01'
def test_read_submodule_resource(self):
submodule = import_module('namespacedata01.subdirectory')

View file

@ -1,16 +1,21 @@
import os.path
import sys
import pathlib
import unittest
from importlib import import_module
from importlib_resources.readers import MultiplexedPath, NamespaceReader
from . import util
class MultiplexedPathTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.folder = pathlib.Path(__file__).parent / 'namespacedata01'
class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
MODULE = 'namespacedata01'
def setUp(self):
super().setUp()
self.folder = pathlib.Path(self.data.__path__[0])
self.data01 = pathlib.Path(self.load_fixture('data01').__file__).parent
self.data02 = pathlib.Path(self.load_fixture('data02').__file__).parent
def test_init_no_paths(self):
with self.assertRaises(FileNotFoundError):
@ -31,9 +36,8 @@ class MultiplexedPathTest(unittest.TestCase):
)
def test_iterdir_duplicate(self):
data01 = pathlib.Path(__file__).parent.joinpath('data01')
contents = {
path.name for path in MultiplexedPath(self.folder, data01).iterdir()
path.name for path in MultiplexedPath(self.folder, self.data01).iterdir()
}
for remove in ('__pycache__', '__init__.pyc'):
try:
@ -61,9 +65,8 @@ class MultiplexedPathTest(unittest.TestCase):
path.open()
def test_join_path(self):
data01 = pathlib.Path(__file__).parent.joinpath('data01')
prefix = str(data01.parent)
path = MultiplexedPath(self.folder, data01)
prefix = str(self.folder.parent)
path = MultiplexedPath(self.folder, self.data01)
self.assertEqual(
str(path.joinpath('binary.file'))[len(prefix) + 1 :],
os.path.join('namespacedata01', 'binary.file'),
@ -83,10 +86,8 @@ class MultiplexedPathTest(unittest.TestCase):
assert not path.joinpath('imaginary/foo.py').exists()
def test_join_path_common_subdir(self):
data01 = pathlib.Path(__file__).parent.joinpath('data01')
data02 = pathlib.Path(__file__).parent.joinpath('data02')
prefix = str(data01.parent)
path = MultiplexedPath(data01, data02)
prefix = str(self.data02.parent)
path = MultiplexedPath(self.data01, self.data02)
self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath)
self.assertEqual(
str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :],
@ -106,16 +107,8 @@ class MultiplexedPathTest(unittest.TestCase):
)
class NamespaceReaderTest(unittest.TestCase):
site_dir = str(pathlib.Path(__file__).parent)
@classmethod
def setUpClass(cls):
sys.path.append(cls.site_dir)
@classmethod
def tearDownClass(cls):
sys.path.remove(cls.site_dir)
class NamespaceReaderTest(util.DiskSetup, unittest.TestCase):
MODULE = 'namespacedata01'
def test_init_error(self):
with self.assertRaises(ValueError):
@ -125,7 +118,7 @@ class NamespaceReaderTest(unittest.TestCase):
namespacedata01 = import_module('namespacedata01')
reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))
root = self.data.__path__[0]
self.assertEqual(
reader.resource_path('binary.file'), os.path.join(root, 'binary.file')
)
@ -134,9 +127,8 @@ class NamespaceReaderTest(unittest.TestCase):
)
def test_files(self):
namespacedata01 = import_module('namespacedata01')
reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))
reader = NamespaceReader(self.data.__spec__.submodule_search_locations)
root = self.data.__path__[0]
self.assertIsInstance(reader.files(), MultiplexedPath)
self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')")

View file

@ -1,9 +1,6 @@
import sys
import unittest
import importlib_resources as resources
import pathlib
from . import data01
from . import util
from importlib import import_module
@ -25,9 +22,8 @@ class ResourceTests:
self.assertTrue(target.is_dir())
class ResourceDiskTests(ResourceTests, unittest.TestCase):
def setUp(self):
self.data = data01
class ResourceDiskTests(ResourceTests, util.DiskSetup, unittest.TestCase):
pass
class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase):
@ -38,33 +34,39 @@ def names(traversable):
return {item.name for item in traversable.iterdir()}
class ResourceLoaderTests(unittest.TestCase):
class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
def test_resource_contents(self):
package = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C']
file=self.data, path=self.data.__file__, contents=['A', 'B', 'C']
)
self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'})
def test_is_file(self):
package = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
file=self.data,
path=self.data.__file__,
contents=['A', 'B', 'C', 'D/E', 'D/F'],
)
self.assertTrue(resources.files(package).joinpath('B').is_file())
def test_is_dir(self):
package = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
file=self.data,
path=self.data.__file__,
contents=['A', 'B', 'C', 'D/E', 'D/F'],
)
self.assertTrue(resources.files(package).joinpath('D').is_dir())
def test_resource_missing(self):
package = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
file=self.data,
path=self.data.__file__,
contents=['A', 'B', 'C', 'D/E', 'D/F'],
)
self.assertFalse(resources.files(package).joinpath('Z').is_file())
class ResourceCornerCaseTests(unittest.TestCase):
class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase):
def test_package_has_no_reader_fallback(self):
"""
Test odd ball packages which:
@ -73,7 +75,7 @@ class ResourceCornerCaseTests(unittest.TestCase):
# 3. Are not in a zip file
"""
module = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C']
file=self.data, path=self.data.__file__, contents=['A', 'B', 'C']
)
# Give the module a dummy loader.
module.__loader__ = object()
@ -84,9 +86,7 @@ class ResourceCornerCaseTests(unittest.TestCase):
self.assertFalse(resources.files(module).joinpath('A').is_file())
class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
ZIP_MODULE = 'data01'
class ResourceFromZipsTest01(util.ZipSetup, unittest.TestCase):
def test_is_submodule_resource(self):
submodule = import_module('data01.subdirectory')
self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file())
@ -117,8 +117,8 @@ class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
assert not data.parent.exists()
class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
ZIP_MODULE = 'data02'
class ResourceFromZipsTest02(util.ZipSetup, unittest.TestCase):
MODULE = 'data02'
def test_unrelated_contents(self):
"""
@ -135,7 +135,7 @@ class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
)
class DeletingZipsTest(util.ZipSetupBase, unittest.TestCase):
class DeletingZipsTest(util.ZipSetup, unittest.TestCase):
"""Having accessed resources in a zip file should not keep an open
reference to the zip.
"""
@ -217,24 +217,20 @@ class ResourceFromNamespaceTests:
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,
class ResourceFromNamespaceDiskTests(
util.DiskSetup,
ResourceFromNamespaceTests,
unittest.TestCase,
):
ZIP_MODULE = 'namespacedata01'
MODULE = 'namespacedata01'
class ResourceFromNamespaceZipTests(
util.ZipSetup,
ResourceFromNamespaceTests,
unittest.TestCase,
):
MODULE = 'namespacedata01'
if __name__ == '__main__':

View file

@ -6,10 +6,10 @@ import types
import pathlib
import contextlib
from . import data01
from ..abc import ResourceReader
from .compat.py39 import import_helper, os_helper
from . import zip as zip_
from . import _path
from importlib.machinery import ModuleSpec
@ -68,7 +68,7 @@ def create_package(file=None, path=None, is_package=True, contents=()):
)
class CommonTests(metaclass=abc.ABCMeta):
class CommonTestsBase(metaclass=abc.ABCMeta):
"""
Tests shared by test_open, test_path, and test_read.
"""
@ -84,34 +84,34 @@ class CommonTests(metaclass=abc.ABCMeta):
"""
Passing in the package name should succeed.
"""
self.execute(data01.__name__, 'utf-8.file')
self.execute(self.data.__name__, 'utf-8.file')
def test_package_object(self):
"""
Passing in the package itself should succeed.
"""
self.execute(data01, 'utf-8.file')
self.execute(self.data, 'utf-8.file')
def test_string_path(self):
"""
Passing in a string for the path should succeed.
"""
path = 'utf-8.file'
self.execute(data01, path)
self.execute(self.data, path)
def test_pathlib_path(self):
"""
Passing in a pathlib.PurePath object for the path should succeed.
"""
path = pathlib.PurePath('utf-8.file')
self.execute(data01, path)
self.execute(self.data, path)
def test_importing_module_as_side_effect(self):
"""
The anchor package can already be imported.
"""
del sys.modules[data01.__name__]
self.execute(data01.__name__, 'utf-8.file')
del sys.modules[self.data.__name__]
self.execute(self.data.__name__, 'utf-8.file')
def test_missing_path(self):
"""
@ -141,24 +141,66 @@ class CommonTests(metaclass=abc.ABCMeta):
self.execute(package, 'utf-8.file')
class ZipSetupBase:
ZIP_MODULE = 'data01'
fixtures = dict(
data01={
'__init__.py': '',
'binary.file': bytes(range(4)),
'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'),
'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'),
'subdirectory': {
'__init__.py': '',
'binary.file': bytes(range(4, 8)),
},
},
data02={
'__init__.py': '',
'one': {'__init__.py': '', 'resource1.txt': 'one resource'},
'two': {'__init__.py': '', 'resource2.txt': 'two resource'},
'subdirectory': {'subsubdir': {'resource.txt': 'a resource'}},
},
namespacedata01={
'binary.file': bytes(range(4)),
'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'),
'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'),
'subdirectory': {
'binary.file': bytes(range(12, 16)),
},
},
)
class ModuleSetup:
def setUp(self):
self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close)
self.fixtures.enter_context(import_helper.isolated_modules())
self.data = self.load_fixture(self.MODULE)
def load_fixture(self, module):
self.tree_on_path({module: fixtures[module]})
return importlib.import_module(module)
class ZipSetup(ModuleSetup):
MODULE = 'data01'
def tree_on_path(self, spec):
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)))
import_helper.DirsOnSysPath(str(zip_.make_zip_file(spec, modules)))
)
self.data = importlib.import_module(self.ZIP_MODULE)
class DiskSetup(ModuleSetup):
MODULE = 'data01'
def tree_on_path(self, spec):
temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
_path.build(spec, pathlib.Path(temp_dir))
self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir))
class ZipSetup(ZipSetupBase):
class CommonTests(DiskSetup, CommonTestsBase):
pass

View file

@ -2,31 +2,25 @@
Generate zip test data files.
"""
import contextlib
import os
import pathlib
import zipfile
import zipp
def make_zip_file(src, dst):
def make_zip_file(tree, dst):
"""
Zip the files in src into a new zipfile at dst.
Zip the files in tree 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)
for name, contents in walk(tree):
zf.writestr(name, contents)
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
def walk(tree, prefix=''):
for name, contents in tree.items():
if isinstance(contents, dict):
yield from walk(contents, prefix=f'{prefix}{name}/')
else:
yield f'{prefix}{name}', contents

View file

@ -1,16 +1,27 @@
"""
A Path-like interface for zipfiles.
This codebase is shared between zipfile.Path in the stdlib
and zipp in PyPI. See
https://github.com/python/importlib_metadata/wiki/Development-Methodology
for more detail.
"""
import functools
import io
import posixpath
import zipfile
import itertools
import contextlib
import pathlib
import posixpath
import re
import stat
import sys
import zipfile
from .compat.py310 import text_encoding
from .glob import Translator
from ._functools import save_method_args
__all__ = ['Path']
@ -37,7 +48,7 @@ def _parents(path):
def _ancestry(path):
"""
Given a path with elements separated by
posixpath.sep, generate all elements of that path
posixpath.sep, generate all elements of that path.
>>> list(_ancestry('b/d'))
['b/d', 'b']
@ -49,9 +60,14 @@ def _ancestry(path):
['b']
>>> list(_ancestry(''))
[]
Multiple separators are treated like a single.
>>> list(_ancestry('//b//d///f//'))
['//b//d///f', '//b//d', '//b']
"""
path = path.rstrip(posixpath.sep)
while path and path != posixpath.sep:
while path.rstrip(posixpath.sep):
yield path
path, tail = posixpath.split(path)
@ -73,82 +89,19 @@ class InitializedState:
Mix-in to save the initialization state for pickling.
"""
@save_method_args
def __init__(self, *args, **kwargs):
self.__args = args
self.__kwargs = kwargs
super().__init__(*args, **kwargs)
def __getstate__(self):
return self.__args, self.__kwargs
return self._saved___init__.args, self._saved___init__.kwargs
def __setstate__(self, state):
args, kwargs = state
super().__init__(*args, **kwargs)
class SanitizedNames:
"""
ZipFile mix-in to ensure names are sanitized.
"""
def namelist(self):
return list(map(self._sanitize, super().namelist()))
@staticmethod
def _sanitize(name):
r"""
Ensure a relative path with posix separators and no dot names.
Modeled after
https://github.com/python/cpython/blob/bcc1be39cb1d04ad9fc0bd1b9193d3972835a57c/Lib/zipfile/__init__.py#L1799-L1813
but provides consistent cross-platform behavior.
>>> san = SanitizedNames._sanitize
>>> san('/foo/bar')
'foo/bar'
>>> san('//foo.txt')
'foo.txt'
>>> san('foo/.././bar.txt')
'foo/bar.txt'
>>> san('foo../.bar.txt')
'foo../.bar.txt'
>>> san('\\foo\\bar.txt')
'foo/bar.txt'
>>> san('D:\\foo.txt')
'D/foo.txt'
>>> san('\\\\server\\share\\file.txt')
'server/share/file.txt'
>>> san('\\\\?\\GLOBALROOT\\Volume3')
'?/GLOBALROOT/Volume3'
>>> san('\\\\.\\PhysicalDrive1\\root')
'PhysicalDrive1/root'
Retain any trailing slash.
>>> san('abc/')
'abc/'
Raises a ValueError if the result is empty.
>>> san('../..')
Traceback (most recent call last):
...
ValueError: Empty filename
"""
def allowed(part):
return part and part not in {'..', '.'}
# Remove the drive letter.
# Don't use ntpath.splitdrive, because that also strips UNC paths
bare = re.sub('^([A-Z]):', r'\1', name, flags=re.IGNORECASE)
clean = bare.replace('\\', '/')
parts = clean.split('/')
joined = '/'.join(filter(allowed, parts))
if not joined:
raise ValueError("Empty filename")
return joined + '/' * name.endswith('/')
class CompleteDirs(InitializedState, SanitizedNames, zipfile.ZipFile):
class CompleteDirs(InitializedState, zipfile.ZipFile):
"""
A ZipFile subclass that ensures that implied directories
are always included in the namelist.
@ -230,16 +183,18 @@ class FastLookup(CompleteDirs):
"""
def namelist(self):
with contextlib.suppress(AttributeError):
return self.__names
self.__names = super().namelist()
return self.__names
return self._namelist
@functools.cached_property
def _namelist(self):
return super().namelist()
def _name_set(self):
with contextlib.suppress(AttributeError):
return self.__lookup
self.__lookup = super()._name_set()
return self.__lookup
return self._name_set_prop
@functools.cached_property
def _name_set_prop(self):
return super()._name_set()
def _extract_text_encoding(encoding=None, *args, **kwargs):
@ -329,7 +284,7 @@ class Path:
>>> str(path.parent)
'mem'
If the zipfile has no filename, such attributes are not
If the zipfile has no filename, such attributes are not
valid and accessing them will raise an Exception.
>>> zf.filename = None
@ -388,7 +343,7 @@ class Path:
if self.is_dir():
raise IsADirectoryError(self)
zip_mode = mode[0]
if not self.exists() and zip_mode == 'r':
if zip_mode == 'r' and not self.exists():
raise FileNotFoundError(self)
stream = self.root.open(self.at, zip_mode, pwd=pwd)
if 'b' in mode:
@ -470,8 +425,7 @@ class Path:
prefix = re.escape(self.at)
tr = Translator(seps='/')
matches = re.compile(prefix + tr.translate(pattern)).fullmatch
names = (data.filename for data in self.root.filelist)
return map(self._next, filter(matches, names))
return map(self._next, filter(matches, self.root.namelist()))
def rglob(self, pattern):
return self.glob(f'**/{pattern}')

20
lib/zipp/_functools.py Normal file
View file

@ -0,0 +1,20 @@
import collections
import functools
# from jaraco.functools 4.0.2
def save_method_args(method):
"""
Wrap a method such that when it is called, the args and kwargs are
saved on the method.
"""
args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs')
@functools.wraps(method)
def wrapper(self, /, *args, **kwargs):
attr_name = '_saved_' + method.__name__
attr = args_and_kwargs(args, kwargs)
setattr(self, attr_name, attr)
return method(self, *args, **kwargs)
return wrapper

View file

@ -0,0 +1,37 @@
"""
Expose zipp.Path as .zipfile.Path.
Includes everything else in ``zipfile`` to match future usage. Just
use:
>>> from zipp.compat.overlay import zipfile
in place of ``import zipfile``.
Relative imports are supported too.
>>> from zipp.compat.overlay.zipfile import ZipInfo
The ``zipfile`` object added to ``sys.modules`` needs to be
hashable (#126).
>>> _ = hash(sys.modules['zipp.compat.overlay.zipfile'])
"""
import importlib
import sys
import types
import zipp
class HashableNamespace(types.SimpleNamespace):
def __hash__(self):
return hash(tuple(vars(self)))
zipfile = HashableNamespace(**vars(importlib.import_module('zipfile')))
zipfile.Path = zipp.Path
zipfile._path = zipp
sys.modules[__name__ + '.zipfile'] = zipfile # type: ignore[assignment]

View file

@ -1,5 +1,5 @@
import sys
import io
import sys
def _text_encoding(encoding, stacklevel=2, /): # pragma: no cover
@ -7,5 +7,7 @@ def _text_encoding(encoding, stacklevel=2, /): # pragma: no cover
text_encoding = (
io.text_encoding if sys.version_info > (3, 10) else _text_encoding # type: ignore
io.text_encoding # type: ignore[unused-ignore, attr-defined]
if sys.version_info > (3, 10)
else _text_encoding
)

View file

@ -1,7 +1,6 @@
import os
import re
_default_seps = os.sep + str(os.altsep) * bool(os.altsep)
@ -28,7 +27,7 @@ class Translator:
"""
Given a glob pattern, produce a regex that matches it.
"""
return self.extend(self.translate_core(pattern))
return self.extend(self.match_dirs(self.translate_core(pattern)))
def extend(self, pattern):
r"""
@ -41,6 +40,14 @@ class Translator:
"""
return rf'(?s:{pattern})\Z'
def match_dirs(self, pattern):
"""
Ensure that zipfile.Path directory names are matched.
zipfile.Path directory names always end in a slash.
"""
return rf'{pattern}[/]?'
def translate_core(self, pattern):
r"""
Given a glob pattern, produce a regex that matches it.

View file

@ -1,7 +1,7 @@
apscheduler==3.10.1
cryptography==43.0.0
importlib-metadata==8.2.0
importlib-resources==6.4.0
importlib-resources==6.4.5
pyinstaller==6.8.0
pyopenssl==24.2.1

View file

@ -17,7 +17,7 @@ html5lib==1.1
httpagentparser==1.9.5
idna==3.7
importlib-metadata==8.2.0
importlib-resources==6.4.0
importlib-resources==6.4.5
git+https://github.com/Tautulli/ipwhois.git@master#egg=ipwhois
IPy==1.01
Mako==1.3.5