gh-121735: Fix module-adjacent references in zip files (#123037)

* gh-116608: Apply style and compatibility changes from importlib_metadata.

* gh-121735: Ensure module-adjacent resources are loadable from a zipfile.

* gh-121735: Allow all modules to be processed by the ZipReader.

* Add blurb

* Remove update-zips script, unneeded.

* Remove unnecessary references to removed static fixtures.

* Remove zipdata fixtures, unused.
This commit is contained in:
Jason R. Coombs 2024-09-11 22:33:07 -04:00 committed by GitHub
parent 3bd942f106
commit ba687d9481
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 225 additions and 263 deletions

2
.gitattributes vendored
View File

@ -27,8 +27,6 @@ Lib/test/cjkencodings/* noeol
Lib/test/tokenizedata/coding20731.py noeol Lib/test/tokenizedata/coding20731.py noeol
Lib/test/decimaltestdata/*.decTest noeol Lib/test/decimaltestdata/*.decTest noeol
Lib/test/test_email/data/*.txt noeol Lib/test/test_email/data/*.txt noeol
Lib/test/test_importlib/resources/data01/* noeol
Lib/test/test_importlib/resources/namespacedata01/* noeol
Lib/test/xmltestdata/* noeol Lib/test/xmltestdata/* noeol
# Shell scripts should have LF even on Windows because of Cygwin # Shell scripts should have LF even on Windows because of Cygwin

View File

@ -34,8 +34,10 @@ class FileReader(abc.TraversableResources):
class ZipReader(abc.TraversableResources): class ZipReader(abc.TraversableResources):
def __init__(self, loader, module): def __init__(self, loader, module):
self.prefix = loader.prefix.replace('\\', '/')
if loader.is_package(module):
_, _, name = module.rpartition('.') _, _, name = module.rpartition('.')
self.prefix = loader.prefix.replace('\\', '/') + name + '/' self.prefix += name + '/'
self.archive = loader.archive self.archive = loader.archive
def open_resource(self, resource): def open_resource(self, resource):

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
from importlib import resources from importlib import 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

@ -6,11 +6,7 @@ import contextlib
from importlib import resources from importlib import resources
from importlib.resources.abc import Traversable from importlib.resources.abc import Traversable
from . import data01
from . import util from . import util
from . import _path
from test.support import os_helper
from test.support import import_helper
@contextlib.contextmanager @contextlib.contextmanager
@ -48,70 +44,96 @@ 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
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:
def test_module_resources(self):
"""
A module can have resources found adjacent to the module.
"""
spec = { spec = {
'mod.py': '', 'mod.py': '',
'res.txt': 'resources are the best', 'res.txt': 'resources are the best',
} }
_path.build(spec, self.site_dir)
def test_module_resources(self):
"""
A module can have resources found adjacent to the module.
"""
import mod 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
"""
Without any parameter, files() will infer the location as the caller.
""" class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.TestCase):
spec = { pass
'somepkg': {
'__init__.py': textwrap.dedent(
class ImplicitContextFiles:
set_val = textwrap.dedent(
""" """
import importlib.resources as res import importlib.resources as res
val = res.files().joinpath('res.txt').read_text(encoding='utf-8') 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', 'res.txt': 'resources are the best',
}, },
} }
_path.build(spec, self.site_dir)
def test_implicit_files_package(self):
"""
Without any parameter, files() will infer the location as the caller.
"""
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'
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,26 +1,38 @@
import unittest import unittest
import os import os
import importlib
from test.support import warnings_helper from test.support import warnings_helper
from importlib import resources from importlib import 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 = 'test.test_importlib.resources.data01' anchor01 = 'data01'
anchor02 = 'test.test_importlib.resources.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 (
@ -228,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
from importlib import resources from importlib import 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
from importlib import resources from importlib import 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
# file-system-backed resources do not get the tempdir # file-system-backed resources do not get the tempdir

View File

@ -1,7 +1,7 @@
import unittest import unittest
from importlib import import_module, resources from importlib import import_module, resources
from . import data01
from . import util from . import util
@ -51,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):
@ -68,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.readers import MultiplexedPath, NamespaceReader from importlib.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,8 +1,5 @@
import sys
import unittest import unittest
import pathlib
from . import data01
from . import util from . import util
from importlib import resources, import_module from importlib import resources, import_module
@ -24,9 +21,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):
@ -37,33 +33,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:
@ -72,7 +74,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()
@ -83,9 +85,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())
@ -116,8 +116,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):
""" """
@ -134,7 +134,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.
""" """
@ -216,24 +216,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

@ -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

@ -6,10 +6,10 @@ import types
import pathlib import pathlib
import contextlib import contextlib
from . import data01
from importlib.resources.abc import ResourceReader from importlib.resources.abc import ResourceReader
from test.support import import_helper, os_helper from test.support 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,29 +2,23 @@
Generate zip test data files. Generate zip test data files.
""" """
import contextlib
import os
import pathlib
import zipfile import zipfile
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)
zipfile._path.CompleteDirs.inject(zf) zipfile._path.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

@ -256,17 +256,9 @@ class zipimporter(_bootstrap_external._LoaderBasics):
def get_resource_reader(self, fullname): def get_resource_reader(self, fullname):
"""Return the ResourceReader for a package in a zip file. """Return the ResourceReader for a module in a zip file."""
If 'fullname' is a package within the zip file, return the
'ResourceReader' object for the package. Otherwise return None.
"""
try:
if not self.is_package(fullname):
return None
except ZipImportError:
return None
from importlib.readers import ZipReader from importlib.readers import ZipReader
return ZipReader(self, fullname) return ZipReader(self, fullname)

View File

@ -2489,21 +2489,6 @@ TESTSUBDIRS= idlelib/idle_test \
test/test_importlib/namespace_pkgs/project3/parent/child \ test/test_importlib/namespace_pkgs/project3/parent/child \
test/test_importlib/partial \ test/test_importlib/partial \
test/test_importlib/resources \ test/test_importlib/resources \
test/test_importlib/resources/data01 \
test/test_importlib/resources/data01/subdirectory \
test/test_importlib/resources/data02 \
test/test_importlib/resources/data02/one \
test/test_importlib/resources/data02/subdirectory \
test/test_importlib/resources/data02/subdirectory/subsubdir \
test/test_importlib/resources/data02/two \
test/test_importlib/resources/data03 \
test/test_importlib/resources/data03/namespace \
test/test_importlib/resources/data03/namespace/portion1 \
test/test_importlib/resources/data03/namespace/portion2 \
test/test_importlib/resources/namespacedata01 \
test/test_importlib/resources/namespacedata01/subdirectory \
test/test_importlib/resources/zipdata01 \
test/test_importlib/resources/zipdata02 \
test/test_importlib/source \ test/test_importlib/source \
test/test_inspect \ test/test_inspect \
test/test_interpreters \ test/test_interpreters \

View File

@ -0,0 +1,3 @@
When working with zip archives, importlib.resources now properly honors
module-adjacent references (e.g. ``files(pkg.mod)`` and not just
``files(pkg)``).