]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-121735: Fix module-adjacent references in zip files (#123037)
authorJason R. Coombs <jaraco@jaraco.com>
Thu, 12 Sep 2024 02:33:07 +0000 (22:33 -0400)
committerGitHub <noreply@github.com>
Thu, 12 Sep 2024 02:33:07 +0000 (22:33 -0400)
* 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.

40 files changed:
.gitattributes
Lib/importlib/resources/readers.py
Lib/test/test_importlib/resources/data01/__init__.py [deleted file]
Lib/test/test_importlib/resources/data01/binary.file [deleted file]
Lib/test/test_importlib/resources/data01/subdirectory/__init__.py [deleted file]
Lib/test/test_importlib/resources/data01/subdirectory/binary.file [deleted file]
Lib/test/test_importlib/resources/data01/utf-16.file [deleted file]
Lib/test/test_importlib/resources/data01/utf-8.file [deleted file]
Lib/test/test_importlib/resources/data02/__init__.py [deleted file]
Lib/test/test_importlib/resources/data02/one/__init__.py [deleted file]
Lib/test/test_importlib/resources/data02/one/resource1.txt [deleted file]
Lib/test/test_importlib/resources/data02/subdirectory/subsubdir/resource.txt [deleted file]
Lib/test/test_importlib/resources/data02/two/__init__.py [deleted file]
Lib/test/test_importlib/resources/data02/two/resource2.txt [deleted file]
Lib/test/test_importlib/resources/data03/__init__.py [deleted file]
Lib/test/test_importlib/resources/data03/namespace/portion1/__init__.py [deleted file]
Lib/test/test_importlib/resources/data03/namespace/portion2/__init__.py [deleted file]
Lib/test/test_importlib/resources/data03/namespace/resource1.txt [deleted file]
Lib/test/test_importlib/resources/namespacedata01/binary.file [deleted file]
Lib/test/test_importlib/resources/namespacedata01/subdirectory/binary.file [deleted file]
Lib/test/test_importlib/resources/namespacedata01/utf-16.file [deleted file]
Lib/test/test_importlib/resources/namespacedata01/utf-8.file [deleted file]
Lib/test/test_importlib/resources/test_contents.py
Lib/test/test_importlib/resources/test_files.py
Lib/test/test_importlib/resources/test_functional.py
Lib/test/test_importlib/resources/test_open.py
Lib/test/test_importlib/resources/test_path.py
Lib/test/test_importlib/resources/test_read.py
Lib/test/test_importlib/resources/test_reader.py
Lib/test/test_importlib/resources/test_resource.py
Lib/test/test_importlib/resources/update-zips.py [deleted file]
Lib/test/test_importlib/resources/util.py
Lib/test/test_importlib/resources/zip.py
Lib/test/test_importlib/resources/zipdata01/__init__.py [deleted file]
Lib/test/test_importlib/resources/zipdata01/ziptestdata.zip [deleted file]
Lib/test/test_importlib/resources/zipdata02/__init__.py [deleted file]
Lib/test/test_importlib/resources/zipdata02/ziptestdata.zip [deleted file]
Lib/zipimport.py
Makefile.pre.in
Misc/NEWS.d/next/Library/2024-08-15-09-45-34.gh-issue-121735._1q0qf.rst [new file with mode: 0644]

index 5b81d2cb3c90e9ca463303cbd37dcacdea4567c0..2f5a030981fb946623045917651f8d6d02bebba8 100644 (file)
@@ -27,8 +27,6 @@ Lib/test/cjkencodings/*                    noeol
 Lib/test/tokenizedata/coding20731.py       noeol
 Lib/test/decimaltestdata/*.decTest         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
 
 # Shell scripts should have LF even on Windows because of Cygwin
index b86cdeff57c4c2ea14d5e7cc8069126355ad3166..ccc5abbeb4e56ed8d7969105ac449bcd3996a7fa 100644 (file)
@@ -34,8 +34,10 @@ class FileReader(abc.TraversableResources):
 
 class ZipReader(abc.TraversableResources):
     def __init__(self, loader, module):
-        _, _, name = module.rpartition('.')
-        self.prefix = loader.prefix.replace('\\', '/') + name + '/'
+        self.prefix = loader.prefix.replace('\\', '/')
+        if loader.is_package(module):
+            _, _, name = module.rpartition('.')
+            self.prefix += name + '/'
         self.archive = loader.archive
 
     def open_resource(self, resource):
diff --git a/Lib/test/test_importlib/resources/data01/__init__.py b/Lib/test/test_importlib/resources/data01/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/data01/binary.file b/Lib/test/test_importlib/resources/data01/binary.file
deleted file mode 100644 (file)
index eaf36c1..0000000
Binary files a/Lib/test/test_importlib/resources/data01/binary.file and /dev/null differ
diff --git a/Lib/test/test_importlib/resources/data01/subdirectory/__init__.py b/Lib/test/test_importlib/resources/data01/subdirectory/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/data01/subdirectory/binary.file b/Lib/test/test_importlib/resources/data01/subdirectory/binary.file
deleted file mode 100644 (file)
index 5bd8bb8..0000000
+++ /dev/null
@@ -1 +0,0 @@
-\ 4\ 5\ 6\a
\ No newline at end of file
diff --git a/Lib/test/test_importlib/resources/data01/utf-16.file b/Lib/test/test_importlib/resources/data01/utf-16.file
deleted file mode 100644 (file)
index 2cb7722..0000000
Binary files a/Lib/test/test_importlib/resources/data01/utf-16.file and /dev/null differ
diff --git a/Lib/test/test_importlib/resources/data01/utf-8.file b/Lib/test/test_importlib/resources/data01/utf-8.file
deleted file mode 100644 (file)
index 1c0132a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-Hello, UTF-8 world!
diff --git a/Lib/test/test_importlib/resources/data02/__init__.py b/Lib/test/test_importlib/resources/data02/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/data02/one/__init__.py b/Lib/test/test_importlib/resources/data02/one/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/data02/one/resource1.txt b/Lib/test/test_importlib/resources/data02/one/resource1.txt
deleted file mode 100644 (file)
index 61a813e..0000000
+++ /dev/null
@@ -1 +0,0 @@
-one resource
diff --git a/Lib/test/test_importlib/resources/data02/subdirectory/subsubdir/resource.txt b/Lib/test/test_importlib/resources/data02/subdirectory/subsubdir/resource.txt
deleted file mode 100644 (file)
index 48f587a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-a resource
\ No newline at end of file
diff --git a/Lib/test/test_importlib/resources/data02/two/__init__.py b/Lib/test/test_importlib/resources/data02/two/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/data02/two/resource2.txt b/Lib/test/test_importlib/resources/data02/two/resource2.txt
deleted file mode 100644 (file)
index a80ce46..0000000
+++ /dev/null
@@ -1 +0,0 @@
-two resource
diff --git a/Lib/test/test_importlib/resources/data03/__init__.py b/Lib/test/test_importlib/resources/data03/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/data03/namespace/portion1/__init__.py b/Lib/test/test_importlib/resources/data03/namespace/portion1/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/data03/namespace/portion2/__init__.py b/Lib/test/test_importlib/resources/data03/namespace/portion2/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/data03/namespace/resource1.txt b/Lib/test/test_importlib/resources/data03/namespace/resource1.txt
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/namespacedata01/binary.file b/Lib/test/test_importlib/resources/namespacedata01/binary.file
deleted file mode 100644 (file)
index eaf36c1..0000000
Binary files a/Lib/test/test_importlib/resources/namespacedata01/binary.file and /dev/null differ
diff --git a/Lib/test/test_importlib/resources/namespacedata01/subdirectory/binary.file b/Lib/test/test_importlib/resources/namespacedata01/subdirectory/binary.file
deleted file mode 100644 (file)
index 100f506..0000000
+++ /dev/null
@@ -1 +0,0 @@
-\f\r\ e\ f
\ No newline at end of file
diff --git a/Lib/test/test_importlib/resources/namespacedata01/utf-16.file b/Lib/test/test_importlib/resources/namespacedata01/utf-16.file
deleted file mode 100644 (file)
index 2cb7722..0000000
Binary files a/Lib/test/test_importlib/resources/namespacedata01/utf-16.file and /dev/null differ
diff --git a/Lib/test/test_importlib/resources/namespacedata01/utf-8.file b/Lib/test/test_importlib/resources/namespacedata01/utf-8.file
deleted file mode 100644 (file)
index 1c0132a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-Hello, UTF-8 world!
index beab67ccc2168026e3c489054a9918f9cf22c155..4e4e0e9c337f230ee31790d3f980ebaa205f0ee3 100644 (file)
@@ -1,7 +1,6 @@
 import unittest
 from importlib import resources
 
-from . import data01
 from . import util
 
 
@@ -19,16 +18,17 @@ class ContentsTests:
         assert self.expected <= contents
 
 
-class ContentsDiskTests(ContentsTests, unittest.TestCase):
-    def setUp(self):
-        self.data = data01
+class ContentsDiskTests(ContentsTests, util.DiskSetup, unittest.TestCase):
+    pass
 
 
 class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase):
     pass
 
 
-class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
+class ContentsNamespaceTests(ContentsTests, util.DiskSetup, unittest.TestCase):
+    MODULE = 'namespacedata01'
+
     expected = {
         # no __init__ because of namespace design
         'binary.file',
@@ -36,8 +36,3 @@ class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
         'utf-16.file',
         'utf-8.file',
     }
-
-    def setUp(self):
-        from . import namespacedata01
-
-        self.data = namespacedata01
index 7df6d03ead74808a9de89682632d2188240fa7e3..08b840834dfd4b370aa55ab33e7adbb71f32b52f 100644 (file)
@@ -6,11 +6,7 @@ import contextlib
 
 from importlib import resources
 from importlib.resources.abc import Traversable
-from . import data01
 from . import util
-from . import _path
-from test.support import os_helper
-from test.support import import_helper
 
 
 @contextlib.contextmanager
@@ -48,70 +44,96 @@ class FilesTests:
             resources.files(package=self.data)
 
 
-class OpenDiskTests(FilesTests, unittest.TestCase):
-    def setUp(self):
-        self.data = data01
+class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase):
+    pass
 
 
 class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
     pass
 
 
-class OpenNamespaceTests(FilesTests, unittest.TestCase):
-    def setUp(self):
-        from . import namespacedata01
-
-        self.data = namespacedata01
+class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase):
+    MODULE = 'namespacedata01'
 
 
 class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
     ZIP_MODULE = 'namespacedata01'
 
 
-class SiteDir:
-    def setUp(self):
-        self.fixtures = contextlib.ExitStack()
-        self.addCleanup(self.fixtures.close)
-        self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())
-        self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir))
-        self.fixtures.enter_context(import_helper.isolated_modules())
+class DirectSpec:
+    """
+    Override behavior of ModuleSetup to write a full spec directly.
+    """
+
+    MODULE = 'unused'
+
+    def load_fixture(self, name):
+        self.tree_on_path(self.spec)
 
 
-class ModulesFilesTests(SiteDir, unittest.TestCase):
+class ModulesFiles:
+    spec = {
+        'mod.py': '',
+        'res.txt': 'resources are the best',
+    }
+
     def test_module_resources(self):
         """
         A module can have resources found adjacent to the module.
         """
-        spec = {
-            'mod.py': '',
-            'res.txt': 'resources are the best',
-        }
-        _path.build(spec, self.site_dir)
         import mod
 
         actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8')
-        assert actual == spec['res.txt']
+        assert actual == self.spec['res.txt']
+
+
+class ModuleFilesDiskTests(DirectSpec, util.DiskSetup, ModulesFiles, unittest.TestCase):
+    pass
+
+
+class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.TestCase):
+    pass
+
 
+class ImplicitContextFiles:
+    set_val = textwrap.dedent(
+        """
+        import importlib.resources 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',
+        },
+    }
 
-class ImplicitContextFilesTests(SiteDir, unittest.TestCase):
-    def test_implicit_files(self):
+    def test_implicit_files_package(self):
         """
         Without any parameter, files() will infer the location as the caller.
         """
-        spec = {
-            'somepkg': {
-                '__init__.py': textwrap.dedent(
-                    """
-                    import importlib.resources as res
-                    val = res.files().joinpath('res.txt').read_text(encoding='utf-8')
-                    """
-                ),
-                'res.txt': 'resources are the best',
-            },
-        }
-        _path.build(spec, self.site_dir)
         assert importlib.import_module('somepkg').val == 'resources are the best'
 
+    def test_implicit_files_submodule(self):
+        """
+        Without any parameter, files() will infer the location as the caller.
+        """
+        assert importlib.import_module('somepkg.submod').val == 'resources are the best'
+
+
+class ImplicitContextFilesDiskTests(
+    DirectSpec, util.DiskSetup, ImplicitContextFiles, unittest.TestCase
+):
+    pass
+
+
+class ImplicitContextFilesZipTests(
+    DirectSpec, util.ZipSetup, ImplicitContextFiles, unittest.TestCase
+):
+    pass
+
 
 if __name__ == '__main__':
     unittest.main()
index f65ffe646cf0d4d82bd1a23ea51440f87ad88c06..4317abf3162c5290e4de969d0f4b1ea3356a3e72 100644 (file)
@@ -1,26 +1,38 @@
 import unittest
 import os
+import importlib
 
 from test.support import warnings_helper
 
 from importlib import resources
 
+from . import util
+
 # Since the functional API forwards to Traversable, we only test
 # filesystem resources here -- not zip files, namespace packages etc.
 # We do test for two kinds of Anchor, though.
 
 
 class StringAnchorMixin:
-    anchor01 = 'test.test_importlib.resources.data01'
-    anchor02 = 'test.test_importlib.resources.data02'
+    anchor01 = 'data01'
+    anchor02 = 'data02'
 
 
 class ModuleAnchorMixin:
-    from . import data01 as anchor01
-    from . import data02 as anchor02
+    @property
+    def anchor01(self):
+        return importlib.import_module('data01')
+
+    @property
+    def anchor02(self):
+        return importlib.import_module('data02')
+
 
+class FunctionalAPIBase(util.DiskSetup):
+    def setUp(self):
+        super().setUp()
+        self.load_fixture('data02')
 
-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 (
@@ -228,16 +240,16 @@ class FunctionalAPIBase:
 
 
 class FunctionalAPITest_StringAnchor(
-    unittest.TestCase,
-    FunctionalAPIBase,
     StringAnchorMixin,
+    FunctionalAPIBase,
+    unittest.TestCase,
 ):
     pass
 
 
 class FunctionalAPITest_ModuleAnchor(
-    unittest.TestCase,
-    FunctionalAPIBase,
     ModuleAnchorMixin,
+    FunctionalAPIBase,
+    unittest.TestCase,
 ):
     pass
index 3b6b2142ef47b18aa72979bb55788fb95899fafb..8c00378ad3cc9cdaa80008a80a645cf2dec6060b 100644 (file)
@@ -1,7 +1,6 @@
 import unittest
 
 from importlib import resources
-from . import data01
 from . import util
 
 
@@ -65,16 +64,12 @@ class OpenTests:
             target.open(encoding='utf-8')
 
 
-class OpenDiskTests(OpenTests, unittest.TestCase):
-    def setUp(self):
-        self.data = data01
-
+class OpenDiskTests(OpenTests, util.DiskSetup, unittest.TestCase):
+    pass
 
-class OpenDiskNamespaceTests(OpenTests, unittest.TestCase):
-    def setUp(self):
-        from . import namespacedata01
 
-        self.data = namespacedata01
+class OpenDiskNamespaceTests(OpenTests, util.DiskSetup, unittest.TestCase):
+    MODULE = 'namespacedata01'
 
 
 class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
@@ -82,7 +77,7 @@ class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
 
 
 class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
-    ZIP_MODULE = 'namespacedata01'
+    MODULE = 'namespacedata01'
 
 
 if __name__ == '__main__':
index 90b22905ab869235591c77d9239e79bb5d0f1ee9..378dc7a2baeb230566b7153fc8cf666a4bbe614a 100644 (file)
@@ -3,7 +3,6 @@ import pathlib
 import unittest
 
 from importlib import resources
-from . import data01
 from . import util
 
 
@@ -25,9 +24,7 @@ class PathTests:
             self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8'))
 
 
-class PathDiskTests(PathTests, unittest.TestCase):
-    data = data01
-
+class PathDiskTests(PathTests, util.DiskSetup, unittest.TestCase):
     def test_natural_path(self):
         # Guarantee the internal implementation detail that
         # file-system-backed resources do not get the tempdir
index 984feecbb9ed6948d1cc1b80cf516eb346276a29..59c237d964121e55afc2bfff43c0c48fcfa9eb7a 100644 (file)
@@ -1,7 +1,7 @@
 import unittest
 
 from importlib import import_module, resources
-from . import data01
+
 from . import util
 
 
@@ -51,8 +51,8 @@ class ReadTests:
         )
 
 
-class ReadDiskTests(ReadTests, unittest.TestCase):
-    data = data01
+class ReadDiskTests(ReadTests, util.DiskSetup, unittest.TestCase):
+    pass
 
 
 class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
@@ -68,15 +68,12 @@ class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
         self.assertEqual(result, bytes(range(4, 8)))
 
 
-class ReadNamespaceTests(ReadTests, unittest.TestCase):
-    def setUp(self):
-        from . import namespacedata01
-
-        self.data = namespacedata01
+class ReadNamespaceTests(ReadTests, util.DiskSetup, unittest.TestCase):
+    MODULE = 'namespacedata01'
 
 
 class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
-    ZIP_MODULE = 'namespacedata01'
+    MODULE = 'namespacedata01'
 
     def test_read_submodule_resource(self):
         submodule = import_module('namespacedata01.subdirectory')
index dac9c2a892ffd245ba3b8956ef885f04b0888f63..ed5693ab4167988ecb8a07c7d9175a0fdbd6301b 100644 (file)
@@ -1,16 +1,21 @@
 import os.path
-import sys
 import pathlib
 import unittest
 
 from importlib import import_module
 from importlib.readers import MultiplexedPath, NamespaceReader
 
+from . import util
 
-class MultiplexedPathTest(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.folder = pathlib.Path(__file__).parent / 'namespacedata01'
+
+class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
+    MODULE = 'namespacedata01'
+
+    def setUp(self):
+        super().setUp()
+        self.folder = pathlib.Path(self.data.__path__[0])
+        self.data01 = pathlib.Path(self.load_fixture('data01').__file__).parent
+        self.data02 = pathlib.Path(self.load_fixture('data02').__file__).parent
 
     def test_init_no_paths(self):
         with self.assertRaises(FileNotFoundError):
@@ -31,9 +36,8 @@ class MultiplexedPathTest(unittest.TestCase):
         )
 
     def test_iterdir_duplicate(self):
-        data01 = pathlib.Path(__file__).parent.joinpath('data01')
         contents = {
-            path.name for path in MultiplexedPath(self.folder, data01).iterdir()
+            path.name for path in MultiplexedPath(self.folder, self.data01).iterdir()
         }
         for remove in ('__pycache__', '__init__.pyc'):
             try:
@@ -61,9 +65,8 @@ class MultiplexedPathTest(unittest.TestCase):
             path.open()
 
     def test_join_path(self):
-        data01 = pathlib.Path(__file__).parent.joinpath('data01')
-        prefix = str(data01.parent)
-        path = MultiplexedPath(self.folder, data01)
+        prefix = str(self.folder.parent)
+        path = MultiplexedPath(self.folder, self.data01)
         self.assertEqual(
             str(path.joinpath('binary.file'))[len(prefix) + 1 :],
             os.path.join('namespacedata01', 'binary.file'),
@@ -83,10 +86,8 @@ class MultiplexedPathTest(unittest.TestCase):
         assert not path.joinpath('imaginary/foo.py').exists()
 
     def test_join_path_common_subdir(self):
-        data01 = pathlib.Path(__file__).parent.joinpath('data01')
-        data02 = pathlib.Path(__file__).parent.joinpath('data02')
-        prefix = str(data01.parent)
-        path = MultiplexedPath(data01, data02)
+        prefix = str(self.data02.parent)
+        path = MultiplexedPath(self.data01, self.data02)
         self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath)
         self.assertEqual(
             str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :],
@@ -106,16 +107,8 @@ class MultiplexedPathTest(unittest.TestCase):
         )
 
 
-class NamespaceReaderTest(unittest.TestCase):
-    site_dir = str(pathlib.Path(__file__).parent)
-
-    @classmethod
-    def setUpClass(cls):
-        sys.path.append(cls.site_dir)
-
-    @classmethod
-    def tearDownClass(cls):
-        sys.path.remove(cls.site_dir)
+class NamespaceReaderTest(util.DiskSetup, unittest.TestCase):
+    MODULE = 'namespacedata01'
 
     def test_init_error(self):
         with self.assertRaises(ValueError):
@@ -125,7 +118,7 @@ class NamespaceReaderTest(unittest.TestCase):
         namespacedata01 = import_module('namespacedata01')
         reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
 
-        root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))
+        root = self.data.__path__[0]
         self.assertEqual(
             reader.resource_path('binary.file'), os.path.join(root, 'binary.file')
         )
@@ -134,9 +127,8 @@ class NamespaceReaderTest(unittest.TestCase):
         )
 
     def test_files(self):
-        namespacedata01 = import_module('namespacedata01')
-        reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
-        root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))
+        reader = NamespaceReader(self.data.__spec__.submodule_search_locations)
+        root = self.data.__path__[0]
         self.assertIsInstance(reader.files(), MultiplexedPath)
         self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')")
 
index d1d45d9b4617f3534fe92b1c8b23cd4b6365798b..fcede14b891a84fbf4b332919c0fca5b5d2ab971 100644 (file)
@@ -1,8 +1,5 @@
-import sys
 import unittest
-import pathlib
 
-from . import data01
 from . import util
 from importlib import resources, import_module
 
@@ -24,9 +21,8 @@ class ResourceTests:
         self.assertTrue(target.is_dir())
 
 
-class ResourceDiskTests(ResourceTests, unittest.TestCase):
-    def setUp(self):
-        self.data = data01
+class ResourceDiskTests(ResourceTests, util.DiskSetup, unittest.TestCase):
+    pass
 
 
 class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase):
@@ -37,33 +33,39 @@ def names(traversable):
     return {item.name for item in traversable.iterdir()}
 
 
-class ResourceLoaderTests(unittest.TestCase):
+class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
     def test_resource_contents(self):
         package = util.create_package(
-            file=data01, path=data01.__file__, contents=['A', 'B', 'C']
+            file=self.data, path=self.data.__file__, contents=['A', 'B', 'C']
         )
         self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'})
 
     def test_is_file(self):
         package = util.create_package(
-            file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
+            file=self.data,
+            path=self.data.__file__,
+            contents=['A', 'B', 'C', 'D/E', 'D/F'],
         )
         self.assertTrue(resources.files(package).joinpath('B').is_file())
 
     def test_is_dir(self):
         package = util.create_package(
-            file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
+            file=self.data,
+            path=self.data.__file__,
+            contents=['A', 'B', 'C', 'D/E', 'D/F'],
         )
         self.assertTrue(resources.files(package).joinpath('D').is_dir())
 
     def test_resource_missing(self):
         package = util.create_package(
-            file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
+            file=self.data,
+            path=self.data.__file__,
+            contents=['A', 'B', 'C', 'D/E', 'D/F'],
         )
         self.assertFalse(resources.files(package).joinpath('Z').is_file())
 
 
-class ResourceCornerCaseTests(unittest.TestCase):
+class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase):
     def test_package_has_no_reader_fallback(self):
         """
         Test odd ball packages which:
@@ -72,7 +74,7 @@ class ResourceCornerCaseTests(unittest.TestCase):
         # 3. Are not in a zip file
         """
         module = util.create_package(
-            file=data01, path=data01.__file__, contents=['A', 'B', 'C']
+            file=self.data, path=self.data.__file__, contents=['A', 'B', 'C']
         )
         # Give the module a dummy loader.
         module.__loader__ = object()
@@ -83,9 +85,7 @@ class ResourceCornerCaseTests(unittest.TestCase):
         self.assertFalse(resources.files(module).joinpath('A').is_file())
 
 
-class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
-    ZIP_MODULE = 'data01'
-
+class ResourceFromZipsTest01(util.ZipSetup, unittest.TestCase):
     def test_is_submodule_resource(self):
         submodule = import_module('data01.subdirectory')
         self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file())
@@ -116,8 +116,8 @@ class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
         assert not data.parent.exists()
 
 
-class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
-    ZIP_MODULE = 'data02'
+class ResourceFromZipsTest02(util.ZipSetup, unittest.TestCase):
+    MODULE = 'data02'
 
     def test_unrelated_contents(self):
         """
@@ -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
     reference to the zip.
     """
@@ -216,24 +216,20 @@ class ResourceFromNamespaceTests:
         self.assertEqual(contents, {'binary.file'})
 
 
-class ResourceFromNamespaceDiskTests(ResourceFromNamespaceTests, unittest.TestCase):
-    site_dir = str(pathlib.Path(__file__).parent)
-
-    @classmethod
-    def setUpClass(cls):
-        sys.path.append(cls.site_dir)
-
-    @classmethod
-    def tearDownClass(cls):
-        sys.path.remove(cls.site_dir)
+class ResourceFromNamespaceDiskTests(
+    util.DiskSetup,
+    ResourceFromNamespaceTests,
+    unittest.TestCase,
+):
+    MODULE = 'namespacedata01'
 
 
 class ResourceFromNamespaceZipTests(
-    util.ZipSetupBase,
+    util.ZipSetup,
     ResourceFromNamespaceTests,
     unittest.TestCase,
 ):
-    ZIP_MODULE = 'namespacedata01'
+    MODULE = 'namespacedata01'
 
 
 if __name__ == '__main__':
diff --git a/Lib/test/test_importlib/resources/update-zips.py b/Lib/test/test_importlib/resources/update-zips.py
deleted file mode 100755 (executable)
index 231334a..0000000
+++ /dev/null
@@ -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()
index d4bf3e6cc5dfdcc9a62fa8774035a9fb54ba76be..893dda691191924ccdc4b0bbe59b0176fa92f9b9 100644 (file)
@@ -6,10 +6,10 @@ import types
 import pathlib
 import contextlib
 
-from . import data01
 from importlib.resources.abc import ResourceReader
 from test.support import import_helper, os_helper
 from . import zip as zip_
+from . import _path
 
 
 from importlib.machinery import ModuleSpec
@@ -68,7 +68,7 @@ def create_package(file=None, path=None, is_package=True, contents=()):
     )
 
 
-class CommonTests(metaclass=abc.ABCMeta):
+class CommonTestsBase(metaclass=abc.ABCMeta):
     """
     Tests shared by test_open, test_path, and test_read.
     """
@@ -84,34 +84,34 @@ class CommonTests(metaclass=abc.ABCMeta):
         """
         Passing in the package name should succeed.
         """
-        self.execute(data01.__name__, 'utf-8.file')
+        self.execute(self.data.__name__, 'utf-8.file')
 
     def test_package_object(self):
         """
         Passing in the package itself should succeed.
         """
-        self.execute(data01, 'utf-8.file')
+        self.execute(self.data, 'utf-8.file')
 
     def test_string_path(self):
         """
         Passing in a string for the path should succeed.
         """
         path = 'utf-8.file'
-        self.execute(data01, path)
+        self.execute(self.data, path)
 
     def test_pathlib_path(self):
         """
         Passing in a pathlib.PurePath object for the path should succeed.
         """
         path = pathlib.PurePath('utf-8.file')
-        self.execute(data01, path)
+        self.execute(self.data, path)
 
     def test_importing_module_as_side_effect(self):
         """
         The anchor package can already be imported.
         """
-        del sys.modules[data01.__name__]
-        self.execute(data01.__name__, 'utf-8.file')
+        del sys.modules[self.data.__name__]
+        self.execute(self.data.__name__, 'utf-8.file')
 
     def test_missing_path(self):
         """
@@ -141,24 +141,66 @@ class CommonTests(metaclass=abc.ABCMeta):
             self.execute(package, 'utf-8.file')
 
 
-class ZipSetupBase:
-    ZIP_MODULE = 'data01'
-
+fixtures = dict(
+    data01={
+        '__init__.py': '',
+        'binary.file': bytes(range(4)),
+        'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'),
+        'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'),
+        'subdirectory': {
+            '__init__.py': '',
+            'binary.file': bytes(range(4, 8)),
+        },
+    },
+    data02={
+        '__init__.py': '',
+        'one': {'__init__.py': '', 'resource1.txt': 'one resource'},
+        'two': {'__init__.py': '', 'resource2.txt': 'two resource'},
+        'subdirectory': {'subsubdir': {'resource.txt': 'a resource'}},
+    },
+    namespacedata01={
+        'binary.file': bytes(range(4)),
+        'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'),
+        'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'),
+        'subdirectory': {
+            'binary.file': bytes(range(12, 16)),
+        },
+    },
+)
+
+
+class ModuleSetup:
     def setUp(self):
         self.fixtures = contextlib.ExitStack()
         self.addCleanup(self.fixtures.close)
 
         self.fixtures.enter_context(import_helper.isolated_modules())
+        self.data = self.load_fixture(self.MODULE)
+
+    def load_fixture(self, module):
+        self.tree_on_path({module: fixtures[module]})
+        return importlib.import_module(module)
+
+
+class ZipSetup(ModuleSetup):
+    MODULE = 'data01'
 
+    def tree_on_path(self, spec):
         temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
         modules = pathlib.Path(temp_dir) / 'zipped modules.zip'
-        src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE)
         self.fixtures.enter_context(
-            import_helper.DirsOnSysPath(str(zip_.make_zip_file(src_path, modules)))
+            import_helper.DirsOnSysPath(str(zip_.make_zip_file(spec, modules)))
         )
 
-        self.data = importlib.import_module(self.ZIP_MODULE)
+
+class DiskSetup(ModuleSetup):
+    MODULE = 'data01'
+
+    def tree_on_path(self, spec):
+        temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
+        _path.build(spec, pathlib.Path(temp_dir))
+        self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir))
 
 
-class ZipSetup(ZipSetupBase):
+class CommonTests(DiskSetup, CommonTestsBase):
     pass
index 4dcf6facc770cb4cbd3a1bf42671dc950e005960..fc453f02060a31c1eb04f56007b1f65950bdeade 100755 (executable)
@@ -2,29 +2,23 @@
 Generate zip test data files.
 """
 
-import contextlib
-import os
-import pathlib
 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:
-        for src_path, rel in walk(src):
-            dst_name = src.name / pathlib.PurePosixPath(rel.as_posix())
-            zf.write(src_path, dst_name)
+        for name, contents in walk(tree):
+            zf.writestr(name, contents)
         zipfile._path.CompleteDirs.inject(zf)
     return dst
 
 
-def walk(datapath):
-    for dirpath, dirnames, filenames in os.walk(datapath):
-        with contextlib.suppress(ValueError):
-            dirnames.remove('__pycache__')
-        for filename in filenames:
-            res = pathlib.Path(dirpath) / filename
-            rel = res.relative_to(datapath)
-            yield res, rel
+def walk(tree, prefix=''):
+    for name, contents in tree.items():
+        if isinstance(contents, dict):
+            yield from walk(contents, prefix=f'{prefix}{name}/')
+        else:
+            yield f'{prefix}{name}', contents
diff --git a/Lib/test/test_importlib/resources/zipdata01/__init__.py b/Lib/test/test_importlib/resources/zipdata01/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/zipdata01/ziptestdata.zip b/Lib/test/test_importlib/resources/zipdata01/ziptestdata.zip
deleted file mode 100644 (file)
index 9a3bb07..0000000
Binary files a/Lib/test/test_importlib/resources/zipdata01/ziptestdata.zip and /dev/null differ
diff --git a/Lib/test/test_importlib/resources/zipdata02/__init__.py b/Lib/test/test_importlib/resources/zipdata02/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/Lib/test/test_importlib/resources/zipdata02/ziptestdata.zip b/Lib/test/test_importlib/resources/zipdata02/ziptestdata.zip
deleted file mode 100644 (file)
index d63ff51..0000000
Binary files a/Lib/test/test_importlib/resources/zipdata02/ziptestdata.zip and /dev/null differ
index 7ceae2b7387b261cb743649bbbf3f3143440be40..e5192c4d074c4b6cadaab84642f384dfd4e348f6 100644 (file)
@@ -256,17 +256,9 @@ class zipimporter(_bootstrap_external._LoaderBasics):
 
 
     def get_resource_reader(self, fullname):
-        """Return the ResourceReader for a package 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
+        """Return the ResourceReader for a module in a zip file."""
         from importlib.readers import ZipReader
+
         return ZipReader(self, fullname)
 
 
index 77455c0978f71d4ef447622e96c3b51410687cf6..579b3fddabc7c3e86f6730667e253a6c613bc363 100644 (file)
@@ -2489,21 +2489,6 @@ TESTSUBDIRS=     idlelib/idle_test \
                test/test_importlib/namespace_pkgs/project3/parent/child \
                test/test_importlib/partial \
                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_inspect \
                test/test_interpreters \
diff --git a/Misc/NEWS.d/next/Library/2024-08-15-09-45-34.gh-issue-121735._1q0qf.rst b/Misc/NEWS.d/next/Library/2024-08-15-09-45-34.gh-issue-121735._1q0qf.rst
new file mode 100644 (file)
index 0000000..e10b2e7
--- /dev/null
@@ -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)``).