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 ( from ._common import (
as_file, as_file,
@ -7,7 +14,7 @@ from ._common import (
Anchor, Anchor,
) )
from .functional import ( from ._functional import (
contents, contents,
is_resource, is_resource,
open_binary, 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 # zipimport.zipimporter does not support weak references, resulting in a
# TypeError. That seems terrible. # TypeError. That seems terrible.
spec = package.__spec__ 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: if reader is None:
return None return None
return reader(spec.name) # type: ignore return reader(spec.name) # type: ignore[union-attr]
@functools.singledispatch @functools.singledispatch
@ -93,12 +93,13 @@ def _infer_caller():
""" """
def is_this_file(frame_info): def is_this_file(frame_info):
return frame_info.filename == __file__ return frame_info.filename == stack[0].filename
def is_wrapper(frame_info): def is_wrapper(frame_info):
return frame_info.function == 'wrapper' 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 # also exclude 'wrapper' due to singledispatch in the call stack
callers = itertools.filterfalse(is_wrapper, not_this_file) callers = itertools.filterfalse(is_wrapper, not_this_file)
return next(callers).frame return next(callers).frame
@ -182,7 +183,7 @@ def _(path):
@contextlib.contextmanager @contextlib.contextmanager
def _temp_path(dir: tempfile.TemporaryDirectory): 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: with dir as result:
yield pathlib.Path(result) yield pathlib.Path(result)

View file

@ -5,6 +5,6 @@ __all__ = ['ZipPath']
if sys.version_info >= (3, 10): if sys.version_info >= (3, 10):
from zipfile import Path as ZipPath # type: ignore from zipfile import Path as ZipPath
else: 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 collections
import contextlib import contextlib
import itertools import itertools
@ -5,6 +7,7 @@ import pathlib
import operator import operator
import re import re
import warnings import warnings
from collections.abc import Iterator
from . import abc from . import abc
@ -34,8 +37,10 @@ class FileReader(abc.TraversableResources):
class ZipReader(abc.TraversableResources): class ZipReader(abc.TraversableResources):
def __init__(self, loader, module): def __init__(self, loader, module):
_, _, name = module.rpartition('.') self.prefix = loader.prefix.replace('\\', '/')
self.prefix = loader.prefix.replace('\\', '/') + name + '/' if loader.is_package(module):
_, _, name = module.rpartition('.')
self.prefix += name + '/'
self.archive = loader.archive self.archive = loader.archive
def open_resource(self, resource): def open_resource(self, resource):
@ -133,27 +138,31 @@ 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(*map(self._resolve, namespace_path)) self.path = MultiplexedPath(*filter(bool, map(self._resolve, namespace_path)))
@classmethod @classmethod
def _resolve(cls, path_str) -> abc.Traversable: def _resolve(cls, path_str) -> abc.Traversable | None:
r""" r"""
Given an item from a namespace path, resolve it to a Traversable. 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 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 zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or
``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``. ``/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()) dirs = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())
return dir return next(dirs, None)
@classmethod @classmethod
def _candidate_paths(cls, path_str): def _candidate_paths(cls, path_str: str) -> Iterator[abc.Traversable]:
yield pathlib.Path(path_str) yield pathlib.Path(path_str)
yield from cls._resolve_zip_path(path_str) yield from cls._resolve_zip_path(path_str)
@staticmethod @staticmethod
def _resolve_zip_path(path_str): def _resolve_zip_path(path_str: str):
for match in reversed(list(re.finditer(r'[\\/]', path_str))): for match in reversed(list(re.finditer(r'[\\/]', path_str))):
with contextlib.suppress( with contextlib.suppress(
FileNotFoundError, FileNotFoundError,

View file

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

View file

@ -2,15 +2,44 @@ import pathlib
import functools import functools
from typing import Dict, Union from typing import Dict, Union
from typing import runtime_checkable
from typing import Protocol
#### ####
# from jaraco.path 3.4.1 # from jaraco.path 3.7.1
FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore
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. Build a set of files/directories, as described by the spec.
@ -25,21 +54,25 @@ def build(spec: FilesSpec, prefix=pathlib.Path()):
... "__init__.py": "", ... "__init__.py": "",
... }, ... },
... "baz.py": "# Some code", ... "baz.py": "# Some code",
... } ... "bar.py": Symlink("baz.py"),
... },
... "bing": Symlink("foo"),
... } ... }
>>> target = getfixture('tmp_path') >>> target = getfixture('tmp_path')
>>> build(spec, target) >>> build(spec, target)
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8') >>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
'# Some code' '# Some code'
>>> target.joinpath('bing/bar.py').read_text(encoding='utf-8')
'# Some code'
""" """
for name, contents in spec.items(): for name, contents in spec.items():
create(contents, pathlib.Path(prefix) / name) create(contents, _ensure_tree_maker(prefix) / name)
@functools.singledispatch @functools.singledispatch
def create(content: Union[str, bytes, FilesSpec], path): def create(content: Union[str, bytes, FilesSpec], path):
path.mkdir(exist_ok=True) path.mkdir(exist_ok=True)
build(content, prefix=path) # type: ignore build(content, prefix=path) # type: ignore[arg-type]
@create.register @create.register
@ -52,5 +85,10 @@ def _(content: str, path):
path.write_text(content, encoding='utf-8') path.write_text(content, encoding='utf-8')
@create.register
def _(content: Symlink, path):
path.symlink_to(content)
# end from jaraco.path # 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' 'modules_setup', 'modules_cleanup', 'DirsOnSysPath'
) )
os_helper = try_import('os_helper') or from_test_support('temp_dir') 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 unittest
import importlib_resources as resources import importlib_resources as resources
from . import data01
from . import util from . import util
@ -19,16 +18,17 @@ class ContentsTests:
assert self.expected <= contents assert self.expected <= contents
class ContentsDiskTests(ContentsTests, unittest.TestCase): class ContentsDiskTests(ContentsTests, util.DiskSetup, unittest.TestCase):
def setUp(self): pass
self.data = data01
class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase): class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase):
pass pass
class ContentsNamespaceTests(ContentsTests, unittest.TestCase): class ContentsNamespaceTests(ContentsTests, util.DiskSetup, unittest.TestCase):
MODULE = 'namespacedata01'
expected = { expected = {
# no __init__ because of namespace design # no __init__ because of namespace design
'binary.file', 'binary.file',
@ -36,8 +36,3 @@ class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
'utf-16.file', 'utf-16.file',
'utf-8.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 textwrap
import unittest import unittest
import warnings import warnings
@ -6,11 +10,8 @@ import contextlib
import importlib_resources as resources import importlib_resources as resources
from ..abc import Traversable from ..abc import Traversable
from . import data01
from . import util from . import util
from . import _path from .compat.py39 import os_helper, import_helper
from .compat.py39 import os_helper
from .compat.py312 import import_helper
@contextlib.contextmanager @contextlib.contextmanager
@ -48,70 +49,146 @@ class FilesTests:
resources.files(package=self.data) resources.files(package=self.data)
class OpenDiskTests(FilesTests, unittest.TestCase): class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase):
def setUp(self): pass
self.data = data01
class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
pass pass
class OpenNamespaceTests(FilesTests, unittest.TestCase): class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase):
def setUp(self): MODULE = 'namespacedata01'
from . import 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): class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01' ZIP_MODULE = 'namespacedata01'
class SiteDir: class DirectSpec:
def setUp(self): """
self.fixtures = contextlib.ExitStack() Override behavior of ModuleSetup to write a full spec directly.
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)) MODULE = 'unused'
self.fixtures.enter_context(import_helper.isolated_modules())
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): def test_module_resources(self):
""" """
A module can have resources found adjacent to the module. A module can have resources found adjacent to the module.
""" """
spec = { import mod # type: ignore[import-not-found]
'mod.py': '',
'res.txt': 'resources are the best',
}
_path.build(spec, self.site_dir)
import mod
actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8') 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): class ModuleFilesDiskTests(DirectSpec, util.DiskSetup, ModulesFiles, unittest.TestCase):
def test_implicit_files(self): 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. 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' 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__': if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -1,31 +1,38 @@
import unittest import unittest
import os import os
import contextlib import importlib
try: from .compat.py39 import warnings_helper
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 import importlib_resources as resources
from . import util
# Since the functional API forwards to Traversable, we only test # Since the functional API forwards to Traversable, we only test
# filesystem resources here -- not zip files, namespace packages etc. # filesystem resources here -- not zip files, namespace packages etc.
# We do test for two kinds of Anchor, though. # We do test for two kinds of Anchor, though.
class StringAnchorMixin: class StringAnchorMixin:
anchor01 = 'importlib_resources.tests.data01' anchor01 = 'data01'
anchor02 = 'importlib_resources.tests.data02' anchor02 = 'data02'
class ModuleAnchorMixin: class ModuleAnchorMixin:
from . import data01 as anchor01 @property
from . import data02 as anchor02 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): def _gen_resourcetxt_path_parts(self):
"""Yield various names of a text file in anchor02, each in a subTest""" """Yield various names of a text file in anchor02, each in a subTest"""
for path_parts in ( for path_parts in (
@ -36,6 +43,12 @@ class FunctionalAPIBase:
with self.subTest(path_parts=path_parts): with self.subTest(path_parts=path_parts):
yield 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): def test_read_text(self):
self.assertEqual( self.assertEqual(
resources.read_text(self.anchor01, 'utf-8.file'), resources.read_text(self.anchor01, 'utf-8.file'),
@ -76,13 +89,13 @@ class FunctionalAPIBase:
), ),
'\x00\x01\x02\x03', '\x00\x01\x02\x03',
) )
self.assertEqual( self.assertEndsWith( # ignore the BOM
resources.read_text( resources.read_text(
self.anchor01, self.anchor01,
'utf-16.file', 'utf-16.file',
errors='backslashreplace', errors='backslashreplace',
), ),
'Hello, UTF-16 world!\n'.encode('utf-16').decode( 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
errors='backslashreplace', errors='backslashreplace',
), ),
) )
@ -128,9 +141,9 @@ class FunctionalAPIBase:
'utf-16.file', 'utf-16.file',
errors='backslashreplace', errors='backslashreplace',
) as f: ) as f:
self.assertEqual( self.assertEndsWith( # ignore the BOM
f.read(), f.read(),
'Hello, UTF-16 world!\n'.encode('utf-16').decode( 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
errors='backslashreplace', errors='backslashreplace',
), ),
) )
@ -163,32 +176,32 @@ class FunctionalAPIBase:
self.assertTrue(is_resource(self.anchor02, *path_parts)) self.assertTrue(is_resource(self.anchor02, *path_parts))
def test_contents(self): def test_contents(self):
with check_warnings((".*contents.*", DeprecationWarning)): with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)):
c = resources.contents(self.anchor01) c = resources.contents(self.anchor01)
self.assertGreaterEqual( self.assertGreaterEqual(
set(c), set(c),
{'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}, {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'},
) )
with contextlib.ExitStack() as cm: with self.assertRaises(OSError), warnings_helper.check_warnings((
cm.enter_context(self.assertRaises(OSError)) ".*contents.*",
cm.enter_context(check_warnings((".*contents.*", DeprecationWarning))) DeprecationWarning,
)):
list(resources.contents(self.anchor01, 'utf-8.file')) list(resources.contents(self.anchor01, 'utf-8.file'))
for path_parts in self._gen_resourcetxt_path_parts(): for path_parts in self._gen_resourcetxt_path_parts():
with contextlib.ExitStack() as cm: with self.assertRaises(OSError), warnings_helper.check_warnings((
cm.enter_context(self.assertRaises(OSError)) ".*contents.*",
cm.enter_context(check_warnings((".*contents.*", DeprecationWarning))) DeprecationWarning,
)):
list(resources.contents(self.anchor01, *path_parts)) 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') c = resources.contents(self.anchor01, 'subdirectory')
self.assertGreaterEqual( self.assertGreaterEqual(
set(c), set(c),
{'binary.file'}, {'binary.file'},
) )
@ignore_warnings(category=DeprecationWarning) @warnings_helper.ignore_warnings(category=DeprecationWarning)
def test_common_errors(self): def test_common_errors(self):
for func in ( for func in (
resources.read_text, resources.read_text,
@ -227,16 +240,16 @@ class FunctionalAPIBase:
class FunctionalAPITest_StringAnchor( class FunctionalAPITest_StringAnchor(
unittest.TestCase,
FunctionalAPIBase,
StringAnchorMixin, StringAnchorMixin,
FunctionalAPIBase,
unittest.TestCase,
): ):
pass pass
class FunctionalAPITest_ModuleAnchor( class FunctionalAPITest_ModuleAnchor(
unittest.TestCase,
FunctionalAPIBase,
ModuleAnchorMixin, ModuleAnchorMixin,
FunctionalAPIBase,
unittest.TestCase,
): ):
pass pass

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,10 +6,10 @@ import types
import pathlib import pathlib
import contextlib import contextlib
from . import data01
from ..abc import ResourceReader from ..abc import ResourceReader
from .compat.py39 import import_helper, os_helper from .compat.py39 import import_helper, os_helper
from . import zip as zip_ from . import zip as zip_
from . import _path
from importlib.machinery import ModuleSpec 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. 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. 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): def test_package_object(self):
""" """
Passing in the package itself should succeed. 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): def test_string_path(self):
""" """
Passing in a string for the path should succeed. Passing in a string for the path should succeed.
""" """
path = 'utf-8.file' path = 'utf-8.file'
self.execute(data01, path) self.execute(self.data, path)
def test_pathlib_path(self): def test_pathlib_path(self):
""" """
Passing in a pathlib.PurePath object for the path should succeed. Passing in a pathlib.PurePath object for the path should succeed.
""" """
path = pathlib.PurePath('utf-8.file') path = pathlib.PurePath('utf-8.file')
self.execute(data01, path) self.execute(self.data, path)
def test_importing_module_as_side_effect(self): def test_importing_module_as_side_effect(self):
""" """
The anchor package can already be imported. The anchor package can already be imported.
""" """
del sys.modules[data01.__name__] del sys.modules[self.data.__name__]
self.execute(data01.__name__, 'utf-8.file') self.execute(self.data.__name__, 'utf-8.file')
def test_missing_path(self): def test_missing_path(self):
""" """
@ -141,24 +141,66 @@ class CommonTests(metaclass=abc.ABCMeta):
self.execute(package, 'utf-8.file') self.execute(package, 'utf-8.file')
class ZipSetupBase: fixtures = dict(
ZIP_MODULE = 'data01' 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): def setUp(self):
self.fixtures = contextlib.ExitStack() self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close) self.addCleanup(self.fixtures.close)
self.fixtures.enter_context(import_helper.isolated_modules()) 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()) temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
modules = pathlib.Path(temp_dir) / 'zipped modules.zip' modules = pathlib.Path(temp_dir) / 'zipped modules.zip'
src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE)
self.fixtures.enter_context( 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 pass

View file

@ -2,31 +2,25 @@
Generate zip test data files. Generate zip test data files.
""" """
import contextlib
import os
import pathlib
import zipfile import zipfile
import zipp 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: with zipfile.ZipFile(dst, 'w') as zf:
for src_path, rel in walk(src): for name, contents in walk(tree):
dst_name = src.name / pathlib.PurePosixPath(rel.as_posix()) zf.writestr(name, contents)
zf.write(src_path, dst_name)
zipp.CompleteDirs.inject(zf) zipp.CompleteDirs.inject(zf)
return dst return dst
def walk(datapath): def walk(tree, prefix=''):
for dirpath, dirnames, filenames in os.walk(datapath): for name, contents in tree.items():
with contextlib.suppress(ValueError): if isinstance(contents, dict):
dirnames.remove('__pycache__') yield from walk(contents, prefix=f'{prefix}{name}/')
for filename in filenames: else:
res = pathlib.Path(dirpath) / filename yield f'{prefix}{name}', contents
rel = res.relative_to(datapath)
yield res, rel

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 io
import posixpath
import zipfile
import itertools import itertools
import contextlib
import pathlib import pathlib
import posixpath
import re import re
import stat import stat
import sys import sys
import zipfile
from .compat.py310 import text_encoding from .compat.py310 import text_encoding
from .glob import Translator from .glob import Translator
from ._functools import save_method_args
__all__ = ['Path'] __all__ = ['Path']
@ -37,7 +48,7 @@ def _parents(path):
def _ancestry(path): def _ancestry(path):
""" """
Given a path with elements separated by 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')) >>> list(_ancestry('b/d'))
['b/d', 'b'] ['b/d', 'b']
@ -49,9 +60,14 @@ def _ancestry(path):
['b'] ['b']
>>> list(_ancestry('')) >>> 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) path = path.rstrip(posixpath.sep)
while path and path != posixpath.sep: while path.rstrip(posixpath.sep):
yield path yield path
path, tail = posixpath.split(path) path, tail = posixpath.split(path)
@ -73,82 +89,19 @@ class InitializedState:
Mix-in to save the initialization state for pickling. Mix-in to save the initialization state for pickling.
""" """
@save_method_args
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.__args = args
self.__kwargs = kwargs
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def __getstate__(self): def __getstate__(self):
return self.__args, self.__kwargs return self._saved___init__.args, self._saved___init__.kwargs
def __setstate__(self, state): def __setstate__(self, state):
args, kwargs = state args, kwargs = state
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class SanitizedNames: class CompleteDirs(InitializedState, zipfile.ZipFile):
"""
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):
""" """
A ZipFile subclass that ensures that implied directories A ZipFile subclass that ensures that implied directories
are always included in the namelist. are always included in the namelist.
@ -230,16 +183,18 @@ class FastLookup(CompleteDirs):
""" """
def namelist(self): def namelist(self):
with contextlib.suppress(AttributeError): return self._namelist
return self.__names
self.__names = super().namelist() @functools.cached_property
return self.__names def _namelist(self):
return super().namelist()
def _name_set(self): def _name_set(self):
with contextlib.suppress(AttributeError): return self._name_set_prop
return self.__lookup
self.__lookup = super()._name_set() @functools.cached_property
return self.__lookup def _name_set_prop(self):
return super()._name_set()
def _extract_text_encoding(encoding=None, *args, **kwargs): def _extract_text_encoding(encoding=None, *args, **kwargs):
@ -329,7 +284,7 @@ class Path:
>>> str(path.parent) >>> str(path.parent)
'mem' '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. valid and accessing them will raise an Exception.
>>> zf.filename = None >>> zf.filename = None
@ -388,7 +343,7 @@ class Path:
if self.is_dir(): if self.is_dir():
raise IsADirectoryError(self) raise IsADirectoryError(self)
zip_mode = mode[0] zip_mode = mode[0]
if not self.exists() and zip_mode == 'r': if zip_mode == 'r' and not self.exists():
raise FileNotFoundError(self) raise FileNotFoundError(self)
stream = self.root.open(self.at, zip_mode, pwd=pwd) stream = self.root.open(self.at, zip_mode, pwd=pwd)
if 'b' in mode: if 'b' in mode:
@ -470,8 +425,7 @@ class Path:
prefix = re.escape(self.at) prefix = re.escape(self.at)
tr = Translator(seps='/') tr = Translator(seps='/')
matches = re.compile(prefix + tr.translate(pattern)).fullmatch matches = re.compile(prefix + tr.translate(pattern)).fullmatch
names = (data.filename for data in self.root.filelist) return map(self._next, filter(matches, self.root.namelist()))
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}')

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 io
import sys
def _text_encoding(encoding, stacklevel=2, /): # pragma: no cover def _text_encoding(encoding, stacklevel=2, /): # pragma: no cover
@ -7,5 +7,7 @@ def _text_encoding(encoding, stacklevel=2, /): # pragma: no cover
text_encoding = ( 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 os
import re import re
_default_seps = os.sep + str(os.altsep) * bool(os.altsep) _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. 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): def extend(self, pattern):
r""" r"""
@ -41,6 +40,14 @@ class Translator:
""" """
return rf'(?s:{pattern})\Z' 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): def translate_core(self, pattern):
r""" r"""
Given a glob pattern, produce a regex that matches it. Given a glob pattern, produce a regex that matches it.

View file

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

View file

@ -17,7 +17,7 @@ html5lib==1.1
httpagentparser==1.9.5 httpagentparser==1.9.5
idna==3.7 idna==3.7
importlib-metadata==8.2.0 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 git+https://github.com/Tautulli/ipwhois.git@master#egg=ipwhois
IPy==1.01 IPy==1.01
Mako==1.3.5 Mako==1.3.5