Skip to content

Commit

Permalink
gh-121735: Fix module-adjacent references in zip files (#123037)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
jaraco authored Sep 12, 2024
1 parent 3bd942f commit ba687d9
Show file tree
Hide file tree
Showing 40 changed files with 223 additions and 261 deletions.
2 changes: 0 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions Lib/importlib/resources/readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ def files(self):

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):
Expand Down
Empty file.
Binary file removed Lib/test/test_importlib/resources/data01/binary.file
Binary file not shown.
Empty file.

This file was deleted.

Binary file removed Lib/test/test_importlib/resources/data01/utf-16.file
Binary file not shown.
1 change: 0 additions & 1 deletion Lib/test/test_importlib/resources/data01/utf-8.file

This file was deleted.

Empty file.
Empty file.
1 change: 0 additions & 1 deletion Lib/test/test_importlib/resources/data02/one/resource1.txt

This file was deleted.

This file was deleted.

Empty file.
1 change: 0 additions & 1 deletion Lib/test/test_importlib/resources/data02/two/resource2.txt

This file was deleted.

Empty file.
Empty file.
Empty file.
Empty file.
Binary file not shown.

This file was deleted.

Binary file not shown.

This file was deleted.

15 changes: 5 additions & 10 deletions Lib/test/test_importlib/resources/test_contents.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import unittest
from importlib import resources

from . import data01
from . import util


Expand All @@ -19,25 +18,21 @@ def test_contents(self):
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',
'subdirectory',
'utf-16.file',
'utf-8.file',
}

def setUp(self):
from . import namespacedata01

self.data = namespacedata01
102 changes: 62 additions & 40 deletions Lib/test/test_importlib/resources/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@

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
Expand Down Expand Up @@ -48,70 +44,96 @@ def test_old_parameter(self):
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()
30 changes: 21 additions & 9 deletions Lib/test/test_importlib/resources/test_functional.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -228,16 +240,16 @@ def test_text_errors(self):


class FunctionalAPITest_StringAnchor(
unittest.TestCase,
FunctionalAPIBase,
StringAnchorMixin,
FunctionalAPIBase,
unittest.TestCase,
):
pass


class FunctionalAPITest_ModuleAnchor(
unittest.TestCase,
FunctionalAPIBase,
ModuleAnchorMixin,
FunctionalAPIBase,
unittest.TestCase,
):
pass
15 changes: 5 additions & 10 deletions Lib/test/test_importlib/resources/test_open.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import unittest

from importlib import resources
from . import data01
from . import util


Expand Down Expand Up @@ -65,24 +64,20 @@ def test_open_text_FileNotFoundError(self):
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):
pass


class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01'
MODULE = 'namespacedata01'


if __name__ == '__main__':
Expand Down
5 changes: 1 addition & 4 deletions Lib/test/test_importlib/resources/test_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import unittest

from importlib import resources
from . import data01
from . import util


Expand All @@ -25,9 +24,7 @@ def test_reading(self):
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
Expand Down
15 changes: 6 additions & 9 deletions Lib/test/test_importlib/resources/test_read.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest

from importlib import import_module, resources
from . import data01

from . import util


Expand Down Expand Up @@ -51,8 +51,8 @@ def test_read_text_with_errors(self):
)


class ReadDiskTests(ReadTests, unittest.TestCase):
data = data01
class ReadDiskTests(ReadTests, util.DiskSetup, unittest.TestCase):
pass


class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
Expand All @@ -68,15 +68,12 @@ def test_read_submodule_resource_by_name(self):
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')
Expand Down
Loading

0 comments on commit ba687d9

Please sign in to comment.