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

* Bump importlib-resources from 6.0.1 to 6.4.0

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

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

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

* Update importlib-resources==6.4.0

---------

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,11 @@
import os
import sys
from typing import Union
if sys.version_info >= (3, 9):
StrPath = Union[str, os.PathLike[str]]
else:
# PathLike is only subscriptable at runtime in 3.9+
StrPath = Union[str, "os.PathLike[str]"]

View file

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

View file

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

View file

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

View file

@ -1,12 +1,15 @@
import collections import collections
import contextlib
import itertools import itertools
import pathlib import pathlib
import operator import operator
import re
import warnings
from . import abc from . import abc
from ._itertools import only from ._itertools import only
from ._compat import ZipPath from .compat.py39 import ZipPath
def remove_duplicates(items): def remove_duplicates(items):
@ -62,7 +65,7 @@ class MultiplexedPath(abc.Traversable):
""" """
def __init__(self, *paths): def __init__(self, *paths):
self._paths = list(map(pathlib.Path, remove_duplicates(paths))) self._paths = list(map(_ensure_traversable, remove_duplicates(paths)))
if not self._paths: if not self._paths:
message = 'MultiplexedPath must contain at least one path' message = 'MultiplexedPath must contain at least one path'
raise FileNotFoundError(message) raise FileNotFoundError(message)
@ -130,7 +133,36 @@ class NamespaceReader(abc.TraversableResources):
def __init__(self, namespace_path): def __init__(self, namespace_path):
if 'NamespacePath' not in str(namespace_path): if 'NamespacePath' not in str(namespace_path):
raise ValueError('Invalid path') raise ValueError('Invalid path')
self.path = MultiplexedPath(*list(namespace_path)) self.path = MultiplexedPath(*map(self._resolve, namespace_path))
@classmethod
def _resolve(cls, path_str) -> abc.Traversable:
r"""
Given an item from a namespace path, resolve it to a Traversable.
path_str might be a directory on the filesystem or a path to a
zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or
``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``.
"""
(dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())
return dir
@classmethod
def _candidate_paths(cls, path_str):
yield pathlib.Path(path_str)
yield from cls._resolve_zip_path(path_str)
@staticmethod
def _resolve_zip_path(path_str):
for match in reversed(list(re.finditer(r'[\\/]', path_str))):
with contextlib.suppress(
FileNotFoundError,
IsADirectoryError,
NotADirectoryError,
PermissionError,
):
inner = path_str[match.end() :].replace('\\', '/') + '/'
yield ZipPath(path_str[: match.start()], inner.lstrip('/'))
def resource_path(self, resource): def resource_path(self, resource):
""" """
@ -142,3 +174,21 @@ class NamespaceReader(abc.TraversableResources):
def files(self): def files(self):
return self.path return self.path
def _ensure_traversable(path):
"""
Convert deprecated string arguments to traversables (pathlib.Path).
Remove with Python 3.15.
"""
if not isinstance(path, str):
return path
warnings.warn(
"String arguments are deprecated. Pass a Traversable instead.",
DeprecationWarning,
stacklevel=3,
)
return pathlib.Path(path)

View file

@ -88,7 +88,7 @@ class ResourceHandle(Traversable):
def open(self, mode='r', *args, **kwargs): def open(self, mode='r', *args, **kwargs):
stream = self.parent.reader.open_binary(self.name) stream = self.parent.reader.open_binary(self.name)
if 'b' not in mode: if 'b' not in mode:
stream = io.TextIOWrapper(*args, **kwargs) stream = io.TextIOWrapper(stream, *args, **kwargs)
return stream return stream
def joinpath(self, name): def joinpath(self, name):

View file

@ -1,32 +0,0 @@
import os
try:
from test.support import import_helper # type: ignore
except ImportError:
# Python 3.9 and earlier
class import_helper: # type: ignore
from test.support import (
modules_setup,
modules_cleanup,
DirsOnSysPath,
CleanImport,
)
try:
from test.support import os_helper # type: ignore
except ImportError:
# Python 3.9 compat
class os_helper: # type:ignore
from test.support import temp_dir
try:
# Python 3.10
from test.support.os_helper import unlink
except ImportError:
from test.support import unlink as _unlink
def unlink(target):
return _unlink(os.fspath(target))

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import typing
import textwrap import textwrap
import unittest import unittest
import warnings import warnings
@ -10,7 +9,8 @@ from ..abc import Traversable
from . import data01 from . import data01
from . import util from . import util
from . import _path from . import _path
from ._compat import os_helper, import_helper from .compat.py39 import os_helper
from .compat.py312 import import_helper
@contextlib.contextmanager @contextlib.contextmanager
@ -31,13 +31,14 @@ class FilesTests:
actual = files.joinpath('utf-8.file').read_text(encoding='utf-8') actual = files.joinpath('utf-8.file').read_text(encoding='utf-8')
assert actual == 'Hello, UTF-8 world!\n' assert actual == 'Hello, UTF-8 world!\n'
@unittest.skipUnless(
hasattr(typing, 'runtime_checkable'),
"Only suitable when typing supports runtime_checkable",
)
def test_traversable(self): def test_traversable(self):
assert isinstance(resources.files(self.data), Traversable) assert isinstance(resources.files(self.data), Traversable)
def test_joinpath_with_multiple_args(self):
files = resources.files(self.data)
binfile = files.joinpath('subdirectory', 'binary.file')
self.assertTrue(binfile.is_file())
def test_old_parameter(self): def test_old_parameter(self):
""" """
Files used to take a 'package' parameter. Make sure anyone Files used to take a 'package' parameter. Make sure anyone
@ -63,13 +64,17 @@ class OpenNamespaceTests(FilesTests, unittest.TestCase):
self.data = namespacedata01 self.data = namespacedata01
class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01'
class SiteDir: class SiteDir:
def setUp(self): def setUp(self):
self.fixtures = contextlib.ExitStack() self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close) self.addCleanup(self.fixtures.close)
self.site_dir = self.fixtures.enter_context(os_helper.temp_dir()) self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())
self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir)) self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir))
self.fixtures.enter_context(import_helper.CleanImport()) self.fixtures.enter_context(import_helper.isolated_modules())
class ModulesFilesTests(SiteDir, unittest.TestCase): class ModulesFilesTests(SiteDir, unittest.TestCase):

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import io import io
import pathlib
import unittest import unittest
import importlib_resources as resources import importlib_resources as resources
@ -15,18 +16,13 @@ class CommonTests(util.CommonTests, unittest.TestCase):
class PathTests: class PathTests:
def test_reading(self): def test_reading(self):
""" """
Path should be readable. Path should be readable and a pathlib.Path instance.
Test also implicitly verifies the returned object is a pathlib.Path
instance.
""" """
target = resources.files(self.data) / 'utf-8.file' target = resources.files(self.data) / 'utf-8.file'
with resources.as_file(target) as path: with resources.as_file(target) as path:
self.assertIsInstance(path, pathlib.Path)
self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) self.assertTrue(path.name.endswith("utf-8.file"), repr(path))
# pathlib.Path.read_text() was introduced in Python 3.5. self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8'))
with path.open('r', encoding='utf-8') as file:
text = file.read()
self.assertEqual('Hello, UTF-8 world!\n', text)
class PathDiskTests(PathTests, unittest.TestCase): class PathDiskTests(PathTests, unittest.TestCase):

View file

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

View file

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

View file

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

View file

@ -1,53 +0,0 @@
"""
Generate the zip test data files.
Run to build the tests/zipdataNN/ziptestdata.zip files from
files in tests/dataNN.
Replaces the file with the working copy, but does commit anything
to the source repo.
"""
import contextlib
import os
import pathlib
import zipfile
def main():
"""
>>> from unittest import mock
>>> monkeypatch = getfixture('monkeypatch')
>>> monkeypatch.setattr(zipfile, 'ZipFile', mock.MagicMock())
>>> print(); main() # print workaround for bpo-32509
<BLANKLINE>
...data01... -> ziptestdata/...
...
...data02... -> ziptestdata/...
...
"""
suffixes = '01', '02'
tuple(map(generate, suffixes))
def generate(suffix):
root = pathlib.Path(__file__).parent.relative_to(os.getcwd())
zfpath = root / f'zipdata{suffix}/ziptestdata.zip'
with zipfile.ZipFile(zfpath, 'w') as zf:
for src, rel in walk(root / f'data{suffix}'):
dst = 'ziptestdata' / pathlib.PurePosixPath(rel.as_posix())
print(src, '->', dst)
zf.write(src, dst)
def walk(datapath):
for dirpath, dirnames, filenames in os.walk(datapath):
with contextlib.suppress(ValueError):
dirnames.remove('__pycache__')
for filename in filenames:
res = pathlib.Path(dirpath) / filename
rel = res.relative_to(datapath)
yield res, rel
__name__ == '__main__' and main()

View file

@ -4,11 +4,12 @@ import io
import sys import sys
import types import types
import pathlib import pathlib
import contextlib
from . import data01 from . import data01
from . import zipdata01
from ..abc import ResourceReader from ..abc import ResourceReader
from ._compat import import_helper from .compat.py39 import import_helper, os_helper
from . import zip as zip_
from importlib.machinery import ModuleSpec from importlib.machinery import ModuleSpec
@ -141,39 +142,23 @@ class CommonTests(metaclass=abc.ABCMeta):
class ZipSetupBase: class ZipSetupBase:
ZIP_MODULE = None ZIP_MODULE = 'data01'
@classmethod
def setUpClass(cls):
data_path = pathlib.Path(cls.ZIP_MODULE.__file__)
data_dir = data_path.parent
cls._zip_path = str(data_dir / 'ziptestdata.zip')
sys.path.append(cls._zip_path)
cls.data = importlib.import_module('ziptestdata')
@classmethod
def tearDownClass(cls):
try:
sys.path.remove(cls._zip_path)
except ValueError:
pass
try:
del sys.path_importer_cache[cls._zip_path]
del sys.modules[cls.data.__name__]
except KeyError:
pass
try:
del cls.data
del cls._zip_path
except AttributeError:
pass
def setUp(self): def setUp(self):
modules = import_helper.modules_setup() self.fixtures = contextlib.ExitStack()
self.addCleanup(import_helper.modules_cleanup, *modules) self.addCleanup(self.fixtures.close)
self.fixtures.enter_context(import_helper.isolated_modules())
temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
modules = pathlib.Path(temp_dir) / 'zipped modules.zip'
src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE)
self.fixtures.enter_context(
import_helper.DirsOnSysPath(str(zip_.make_zip_file(src_path, modules)))
)
self.data = importlib.import_module(self.ZIP_MODULE)
class ZipSetup(ZipSetupBase): class ZipSetup(ZipSetupBase):
ZIP_MODULE = zipdata01 # type: ignore pass

View file

@ -0,0 +1,32 @@
"""
Generate zip test data files.
"""
import contextlib
import os
import pathlib
import zipfile
import zipp
def make_zip_file(src, dst):
"""
Zip the files in src into a new zipfile at dst.
"""
with zipfile.ZipFile(dst, 'w') as zf:
for src_path, rel in walk(src):
dst_name = src.name / pathlib.PurePosixPath(rel.as_posix())
zf.write(src_path, dst_name)
zipp.CompleteDirs.inject(zf)
return dst
def walk(datapath):
for dirpath, dirnames, filenames in os.walk(datapath):
with contextlib.suppress(ValueError):
dirnames.remove('__pycache__')
for filename in filenames:
res = pathlib.Path(dirpath) / filename
rel = res.relative_to(datapath)
yield res, rel

View file

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

View file

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