]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-121190: Emit a better error message from `importlib.resources.files()` when module...
authorJason R. Coombs <jaraco@jaraco.com>
Sun, 12 Apr 2026 22:15:01 +0000 (18:15 -0400)
committerGitHub <noreply@github.com>
Sun, 12 Apr 2026 22:15:01 +0000 (18:15 -0400)
Also merges incidental changes from importlib_resources 7.1.

Co-authored by: Yuichiro Tachibana (Tsuchiya) <t.yic.yt@gmail.com>

14 files changed:
Lib/importlib/resources/_common.py
Lib/importlib/resources/abc.py
Lib/importlib/resources/simple.py
Lib/test/test_importlib/resources/_path.py
Lib/test/test_importlib/resources/test_compatibilty_files.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/util.py
Misc/NEWS.d/next/Library/2026-04-12-12-31-45.gh-issue-121190.O6-E5_.rst [new file with mode: 0644]

index 40eec742aeb70a5d5d37959bf29d9f81d7367a84..6f87d77492f24956e1670758b82f2ce792b22f42 100644 (file)
@@ -7,11 +7,11 @@ import os
 import pathlib
 import tempfile
 import types
-from typing import cast, Optional, Union
+from typing import Optional, cast
 
 from .abc import ResourceReader, Traversable
 
-Package = Union[types.ModuleType, str]
+Package = types.ModuleType | str
 Anchor = Package
 
 
@@ -32,7 +32,7 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
     # zipimport.zipimporter does not support weak references, resulting in a
     # TypeError.  That seems terrible.
     spec = package.__spec__
-    reader = getattr(spec.loader, "get_resource_reader", None)  # type: ignore[union-attr]
+    reader = getattr(spec.loader, 'get_resource_reader', None)  # type: ignore[union-attr]
     if reader is None:
         return None
     return reader(spec.name)  # type: ignore[union-attr]
@@ -50,7 +50,7 @@ def _(cand: str) -> types.ModuleType:
 
 @resolve.register
 def _(cand: None) -> types.ModuleType:
-    return resolve(_infer_caller().f_globals["__name__"])
+    return resolve(_infer_caller().f_globals['__name__'])
 
 
 def _infer_caller():
@@ -62,7 +62,7 @@ def _infer_caller():
         return frame_info.filename == stack[0].filename
 
     def is_wrapper(frame_info):
-        return frame_info.function == "wrapper"
+        return frame_info.function == 'wrapper'
 
     stack = inspect.stack()
     not_this_file = itertools.filterfalse(is_this_file, stack)
@@ -71,6 +71,19 @@ def _infer_caller():
     return next(callers).frame
 
 
+def _assert_spec(package: types.ModuleType) -> None:
+    """
+    Provide a nicer error message when package is ``__main__``
+    and its ``__spec__`` is ``None``
+    (https://docs.python.org/3/reference/import.html#main-spec).
+    """
+    if package.__spec__ is None:
+        raise TypeError(
+            f"Cannot access resources for '{package.__name__}' "
+            "as it does not appear to correspond to an importable module (its __spec__ is None)."
+        )
+
+
 def from_package(package: types.ModuleType):
     """
     Return a Traversable object for the given package.
@@ -79,6 +92,7 @@ def from_package(package: types.ModuleType):
     # deferred for performance (python/cpython#109829)
     from ._adapters import wrap_spec
 
+    _assert_spec(package)
     spec = wrap_spec(package)
     reader = spec.loader.get_resource_reader(spec.name)
     return reader.files()
@@ -87,7 +101,7 @@ def from_package(package: types.ModuleType):
 @contextlib.contextmanager
 def _tempfile(
     reader,
-    suffix="",
+    suffix='',
     # gh-93353: Keep a reference to call os.remove() in late Python
     # finalization.
     *,
index 64a6d843dce98e51ac6dcb854aab4ba555f3fbed..0b5fdee80e87964dede5913189af4d023d9134f6 100644 (file)
@@ -2,23 +2,21 @@ import abc
 import itertools
 import os
 import pathlib
+from collections.abc import Iterable, Iterator
 from typing import (
     Any,
     BinaryIO,
-    Iterable,
-    Iterator,
     Literal,
     NoReturn,
     Optional,
     Protocol,
     Text,
     TextIO,
-    Union,
     overload,
     runtime_checkable,
 )
 
-StrPath = Union[str, os.PathLike[str]]
+StrPath = str | os.PathLike[str]
 
 __all__ = ["ResourceReader", "Traversable", "TraversableResources"]
 
@@ -151,9 +149,7 @@ class Traversable(Protocol):
     def open(self, mode: Literal['rb'], *args: Any, **kwargs: Any) -> BinaryIO: ...
 
     @abc.abstractmethod
-    def open(
-        self, mode: str = 'r', *args: Any, **kwargs: Any
-    ) -> Union[TextIO, BinaryIO]:
+    def open(self, mode: str = 'r', *args: Any, **kwargs: Any) -> TextIO | BinaryIO:
         """
         mode may be 'r' or 'rb' to open as text or binary. Return a handle
         suitable for reading (same as pathlib.Path.open).
index 2e75299b13aabf3dd11aea5a23e6723d67854fc9..5e182d12607c45ebb369f604d227e3aaa62f433b 100644 (file)
@@ -5,7 +5,7 @@ Interface adapters for low-level readers.
 import abc
 import io
 import itertools
-from typing import BinaryIO, List
+from typing import BinaryIO
 
 from .abc import Traversable, TraversableResources
 
@@ -24,14 +24,14 @@ class SimpleReader(abc.ABC):
         """
 
     @abc.abstractmethod
-    def children(self) -> List['SimpleReader']:
+    def children(self) -> list['SimpleReader']:
         """
         Obtain an iterable of SimpleReader for available
         child containers (e.g. directories).
         """
 
     @abc.abstractmethod
-    def resources(self) -> List[str]:
+    def resources(self) -> list[str]:
         """
         Obtain available named resources for this virtual package.
         """
index 0033983dc6628682481023ce3e15b64171423ba8..3720af7c5085d7e1ab1d2054e355b0e2bc404522 100644 (file)
@@ -1,6 +1,6 @@
 import functools
 import pathlib
-from typing import Dict, Protocol, Union, runtime_checkable
+from typing import Protocol, Union, runtime_checkable
 
 ####
 # from jaraco.path 3.7.1
@@ -12,7 +12,7 @@ class Symlink(str):
     """
 
 
-FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']]
+FilesSpec = dict[str, Union[str, bytes, Symlink, 'FilesSpec']]
 
 
 @runtime_checkable
@@ -28,13 +28,13 @@ class TreeMaker(Protocol):
     def symlink_to(self, target): ...  # pragma: no cover
 
 
-def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
+def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker:
     return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj)  # type: ignore[return-value]
 
 
 def build(
     spec: FilesSpec,
-    prefix: Union[str, TreeMaker] = pathlib.Path(),  # type: ignore[assignment]
+    prefix: str | TreeMaker = pathlib.Path(),  # type: ignore[assignment]
 ):
     """
     Build a set of files/directories, as described by the spec.
@@ -66,7 +66,7 @@ def build(
 
 
 @functools.singledispatch
-def create(content: Union[str, bytes, FilesSpec], path):
+def create(content: str | bytes | FilesSpec, path):
     path.mkdir(exist_ok=True)
     build(content, prefix=path)  # type: ignore[arg-type]
 
index 113f9ae6fdb20dad15fb6f75b2c2dc8ee0660e57..2fd39bedf258d1adf5837a64cc1d181d2e75739a 100644 (file)
@@ -24,51 +24,46 @@ class CompatibilityFilesTests(unittest.TestCase):
         return resources.files(self.package)
 
     def test_spec_path_iter(self):
-        self.assertEqual(
-            sorted(path.name for path in self.files.iterdir()),
-            ['a', 'b', 'c'],
-        )
+        assert sorted(path.name for path in self.files.iterdir()) == ['a', 'b', 'c']
 
     def test_child_path_iter(self):
-        self.assertEqual(list((self.files / 'a').iterdir()), [])
+        assert list((self.files / 'a').iterdir()) == []
 
     def test_orphan_path_iter(self):
-        self.assertEqual(list((self.files / 'a' / 'a').iterdir()), [])
-        self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), [])
+        assert list((self.files / 'a' / 'a').iterdir()) == []
+        assert list((self.files / 'a' / 'a' / 'a').iterdir()) == []
 
     def test_spec_path_is(self):
-        self.assertFalse(self.files.is_file())
-        self.assertFalse(self.files.is_dir())
+        assert not self.files.is_file()
+        assert not self.files.is_dir()
 
     def test_child_path_is(self):
-        self.assertTrue((self.files / 'a').is_file())
-        self.assertFalse((self.files / 'a').is_dir())
+        assert (self.files / 'a').is_file()
+        assert not (self.files / 'a').is_dir()
 
     def test_orphan_path_is(self):
-        self.assertFalse((self.files / 'a' / 'a').is_file())
-        self.assertFalse((self.files / 'a' / 'a').is_dir())
-        self.assertFalse((self.files / 'a' / 'a' / 'a').is_file())
-        self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir())
+        assert not (self.files / 'a' / 'a').is_file()
+        assert not (self.files / 'a' / 'a').is_dir()
+        assert not (self.files / 'a' / 'a' / 'a').is_file()
+        assert not (self.files / 'a' / 'a' / 'a').is_dir()
 
     def test_spec_path_name(self):
-        self.assertEqual(self.files.name, 'testingpackage')
+        assert self.files.name == 'testingpackage'
 
     def test_child_path_name(self):
-        self.assertEqual((self.files / 'a').name, 'a')
+        assert (self.files / 'a').name == 'a'
 
     def test_orphan_path_name(self):
-        self.assertEqual((self.files / 'a' / 'b').name, 'b')
-        self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c')
+        assert (self.files / 'a' / 'b').name == 'b'
+        assert (self.files / 'a' / 'b' / 'c').name == 'c'
 
     def test_spec_path_open(self):
-        self.assertEqual(self.files.read_bytes(), b'Hello, world!')
-        self.assertEqual(self.files.read_text(encoding='utf-8'), 'Hello, world!')
+        assert self.files.read_bytes() == b'Hello, world!'
+        assert self.files.read_text(encoding='utf-8') == 'Hello, world!'
 
     def test_child_path_open(self):
-        self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!')
-        self.assertEqual(
-            (self.files / 'a').read_text(encoding='utf-8'), 'Hello, world!'
-        )
+        assert (self.files / 'a').read_bytes() == b'Hello, world!'
+        assert (self.files / 'a').read_text(encoding='utf-8') == 'Hello, world!'
 
     def test_orphan_path_open(self):
         with self.assertRaises(FileNotFoundError):
@@ -86,7 +81,7 @@ class CompatibilityFilesTests(unittest.TestCase):
 
     def test_wrap_spec(self):
         spec = wrap_spec(self.package)
-        self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles)
+        assert isinstance(spec.loader.get_resource_reader(None), CompatibilityFiles)
 
 
 class CompatibilityFilesNoReaderTests(unittest.TestCase):
@@ -99,4 +94,4 @@ class CompatibilityFilesNoReaderTests(unittest.TestCase):
         return resources.files(self.package)
 
     def test_spec_path_joinpath(self):
-        self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath)
+        assert isinstance(self.files / 'a', CompatibilityFiles.OrphanPath)
index abb5bf36e68c9f308529ce7578a76b709891fdb0..c922d32cedc307c593c85166959d31765262cb17 100644 (file)
@@ -37,7 +37,7 @@ class FilesTests:
     def test_joinpath_with_multiple_args(self):
         files = resources.files(self.data)
         binfile = files.joinpath('subdirectory', 'binary.file')
-        self.assertTrue(binfile.is_file())
+        assert binfile.is_file()
 
 
 class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase):
index 9e1a3a0e2767e337b7ab25bd1707f8da35284662..9cec6af9a5d051ab4860a5ea767cc34bc25c650c 100644 (file)
@@ -38,35 +38,40 @@ class FunctionalAPIBase:
             with self.subTest(path_parts=path_parts):
                 yield path_parts
 
-    def assertEndsWith(self, string, suffix):
-        """Assert that `string` ends with `suffix`.
+    @staticmethod
+    def remove_utf16_bom(string):
+        """Remove an architecture-specific UTF-16 BOM prefix when present.
 
-        Used to ignore an architecture-specific UTF-16 byte-order mark."""
-        self.assertEqual(string[-len(suffix) :], suffix)
+        Some platforms surface UTF-16 BOM bytes as escaped text when the
+        fixture is intentionally decoded as UTF-8 with ``errors='backslashreplace'``.
+        Strip that prefix so assertions validate content consistently."""
+        for bom in ('\\xff\\xfe', '\\xfe\\xff', '\ufeff'):
+            if string.startswith(bom):
+                return string[len(bom) :]
+        return string
 
     def test_read_text(self):
-        self.assertEqual(
-            resources.read_text(self.anchor01, 'utf-8.file'),
-            'Hello, UTF-8 world!\n',
+        assert (
+            resources.read_text(self.anchor01, 'utf-8.file') == 'Hello, UTF-8 world!\n'
         )
-        self.assertEqual(
+        assert (
             resources.read_text(
                 self.anchor02,
                 'subdirectory',
                 'subsubdir',
                 'resource.txt',
                 encoding='utf-8',
-            ),
-            'a resource',
+            )
+            == 'a resource'
         )
         for path_parts in self._gen_resourcetxt_path_parts():
-            self.assertEqual(
+            assert (
                 resources.read_text(
                     self.anchor02,
                     *path_parts,
                     encoding='utf-8',
-                ),
-                'a resource',
+                )
+                == 'a resource'
             )
         # Use generic OSError, since e.g. attempting to read a directory can
         # fail with PermissionError rather than IsADirectoryError
@@ -76,46 +81,42 @@ class FunctionalAPIBase:
             resources.read_text(self.anchor01, 'no-such-file')
         with self.assertRaises(UnicodeDecodeError):
             resources.read_text(self.anchor01, 'utf-16.file')
-        self.assertEqual(
+        assert (
             resources.read_text(
                 self.anchor01,
                 'binary.file',
                 encoding='latin1',
-            ),
-            '\x00\x01\x02\x03',
+            )
+            == '\x00\x01\x02\x03'
         )
-        self.assertEndsWith(  # ignore the BOM
+        assert self.remove_utf16_bom(
             resources.read_text(
                 self.anchor01,
                 'utf-16.file',
                 errors='backslashreplace',
             ),
-            'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
-                errors='backslashreplace',
-            ),
+        ) == 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
+            errors='backslashreplace',
         )
 
     def test_read_binary(self):
-        self.assertEqual(
-            resources.read_binary(self.anchor01, 'utf-8.file'),
-            b'Hello, UTF-8 world!\n',
+        assert (
+            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',
-            )
+            assert 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')
+            assert 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')
+                assert 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):
@@ -130,53 +131,49 @@ class FunctionalAPIBase:
             'binary.file',
             encoding='latin1',
         ) as f:
-            self.assertEqual(f.read(), '\x00\x01\x02\x03')
+            assert f.read() == '\x00\x01\x02\x03'
         with resources.open_text(
             self.anchor01,
             'utf-16.file',
             errors='backslashreplace',
         ) as f:
-            self.assertEndsWith(  # ignore the BOM
-                f.read(),
-                'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
-                    errors='backslashreplace',
-                ),
+            assert self.remove_utf16_bom(f.read()) == 'Hello, UTF-16 world!\n'.encode(
+                'utf-16-le'
+            ).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')
+            assert 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')
+                assert 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')
+                assert 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')
+                assert 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'))
+        assert is_resource(self.anchor01, 'utf-8.file')
+        assert not is_resource(self.anchor01, 'no_such_file')
+        assert not is_resource(self.anchor01)
+        assert not is_resource(self.anchor01, 'subdirectory')
         for path_parts in self._gen_resourcetxt_path_parts():
-            self.assertTrue(is_resource(self.anchor02, *path_parts))
+            assert is_resource(self.anchor02, *path_parts)
 
     def test_contents(self):
         with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)):
             c = resources.contents(self.anchor01)
-        self.assertGreaterEqual(
-            set(c),
-            {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'},
-        )
+        assert set(c) >= {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}
         with (
             self.assertRaises(OSError),
             warnings_helper.check_warnings((
@@ -197,10 +194,7 @@ class FunctionalAPIBase:
                 list(resources.contents(self.anchor01, *path_parts))
         with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)):
             c = resources.contents(self.anchor01, 'subdirectory')
-        self.assertGreaterEqual(
-            set(c),
-            {'binary.file'},
-        )
+        assert set(c) >= {'binary.file'}
 
     @warnings_helper.ignore_warnings(category=DeprecationWarning)
     def test_common_errors(self):
index b5a8949d52e532be56e7bd2792b21daff6d5c8db..950f71db05a01865ac56aa361956dedc0625099c 100644 (file)
@@ -23,19 +23,19 @@ class OpenTests:
         target = resources.files(self.data) / 'binary.file'
         with target.open('rb') as fp:
             result = fp.read()
-            self.assertEqual(result, bytes(range(4)))
+            assert result == bytes(range(4))
 
     def test_open_text_default_encoding(self):
         target = resources.files(self.data) / 'utf-8.file'
         with target.open(encoding='utf-8') as fp:
             result = fp.read()
-            self.assertEqual(result, 'Hello, UTF-8 world!\n')
+            assert result == 'Hello, UTF-8 world!\n'
 
     def test_open_text_given_encoding(self):
         target = resources.files(self.data) / 'utf-16.file'
         with target.open(encoding='utf-16', errors='strict') as fp:
             result = fp.read()
-        self.assertEqual(result, 'Hello, UTF-16 world!\n')
+        assert result == 'Hello, UTF-16 world!\n'
 
     def test_open_text_with_errors(self):
         """
@@ -46,11 +46,10 @@ class OpenTests:
             self.assertRaises(UnicodeError, fp.read)
         with target.open(encoding='utf-8', errors='ignore') as fp:
             result = fp.read()
-        self.assertEqual(
-            result,
+        assert result == (
             'H\x00e\x00l\x00l\x00o\x00,\x00 '
             '\x00U\x00T\x00F\x00-\x001\x006\x00 '
-            '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00',
+            '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00'
         )
 
     def test_open_binary_FileNotFoundError(self):
index 3d158d95b5023a89faffe1657f6ad285ed1bca2f..162344e5d837495a7e12ccfba32b347b49850b5f 100644 (file)
@@ -19,9 +19,9 @@ class PathTests:
         """
         target = resources.files(self.data) / 'utf-8.file'
         with resources.as_file(target) as path:
-            self.assertIsInstance(path, pathlib.Path)
-            self.assertTrue(path.name.endswith("utf-8.file"), repr(path))
-            self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8'))
+            assert isinstance(path, pathlib.Path)
+            assert path.name.endswith("utf-8.file"), repr(path)
+            assert 'Hello, UTF-8 world!\n' == path.read_text(encoding='utf-8')
 
 
 class PathDiskTests(PathTests, util.DiskSetup, unittest.TestCase):
index cd1cc6dd86ff474064e78ba6a91eab17fe54f0ef..4085a64b0eec5756c06c48dd296ebeab4aa932b0 100644 (file)
@@ -18,23 +18,25 @@ class CommonTextTests(util.CommonTests, unittest.TestCase):
 class ReadTests:
     def test_read_bytes(self):
         result = resources.files(self.data).joinpath('binary.file').read_bytes()
-        self.assertEqual(result, bytes(range(4)))
+        assert result == bytes(range(4))
 
     def test_read_text_default_encoding(self):
         result = (
-            resources.files(self.data)
+            resources
+            .files(self.data)
             .joinpath('utf-8.file')
             .read_text(encoding='utf-8')
         )
-        self.assertEqual(result, 'Hello, UTF-8 world!\n')
+        assert result == 'Hello, UTF-8 world!\n'
 
     def test_read_text_given_encoding(self):
         result = (
-            resources.files(self.data)
+            resources
+            .files(self.data)
             .joinpath('utf-16.file')
             .read_text(encoding='utf-16')
         )
-        self.assertEqual(result, 'Hello, UTF-16 world!\n')
+        assert result == 'Hello, UTF-16 world!\n'
 
     def test_read_text_with_errors(self):
         """
@@ -43,11 +45,10 @@ class ReadTests:
         target = resources.files(self.data) / 'utf-16.file'
         self.assertRaises(UnicodeError, target.read_text, encoding='utf-8')
         result = target.read_text(encoding='utf-8', errors='ignore')
-        self.assertEqual(
-            result,
+        assert result == (
             'H\x00e\x00l\x00l\x00o\x00,\x00 '
             '\x00U\x00T\x00F\x00-\x001\x006\x00 '
-            '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00',
+            '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00'
         )
 
 
@@ -59,13 +60,13 @@ class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
     def test_read_submodule_resource(self):
         submodule = import_module('data01.subdirectory')
         result = resources.files(submodule).joinpath('binary.file').read_bytes()
-        self.assertEqual(result, bytes(range(4, 8)))
+        assert result == bytes(range(4, 8))
 
     def test_read_submodule_resource_by_name(self):
         result = (
             resources.files('data01.subdirectory').joinpath('binary.file').read_bytes()
         )
-        self.assertEqual(result, bytes(range(4, 8)))
+        assert result == bytes(range(4, 8))
 
 
 class ReadNamespaceTests(ReadTests, util.DiskSetup, unittest.TestCase):
@@ -78,15 +79,16 @@ class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
     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)))
+        assert result == bytes(range(12, 16))
 
     def test_read_submodule_resource_by_name(self):
         result = (
-            resources.files('namespacedata01.subdirectory')
+            resources
+            .files('namespacedata01.subdirectory')
             .joinpath('binary.file')
             .read_bytes()
         )
-        self.assertEqual(result, bytes(range(12, 16)))
+        assert result == bytes(range(12, 16))
 
 
 if __name__ == '__main__':
index cf23f38f3aaac585a7a6261e80bc01e8b2561772..691a78bb060b394a5f4d16699f1a53598db8312b 100644 (file)
@@ -30,9 +30,7 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
             contents.remove('__pycache__')
         except (KeyError, ValueError):
             pass
-        self.assertEqual(
-            contents, {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'}
-        )
+        assert contents == {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'}
 
     def test_iterdir_duplicate(self):
         contents = {
@@ -43,16 +41,19 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
                 contents.remove(remove)
             except (KeyError, ValueError):
                 pass
-        self.assertEqual(
-            contents,
-            {'__init__.py', 'binary.file', 'subdirectory', 'utf-16.file', 'utf-8.file'},
-        )
+        assert contents == {
+            '__init__.py',
+            'binary.file',
+            'subdirectory',
+            'utf-16.file',
+            'utf-8.file',
+        }
 
     def test_is_dir(self):
-        self.assertEqual(MultiplexedPath(self.folder).is_dir(), True)
+        assert MultiplexedPath(self.folder).is_dir()
 
     def test_is_file(self):
-        self.assertEqual(MultiplexedPath(self.folder).is_file(), False)
+        assert not MultiplexedPath(self.folder).is_file()
 
     def test_open_file(self):
         path = MultiplexedPath(self.folder)
@@ -66,19 +67,17 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
     def test_join_path(self):
         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'),
+        assert str(path.joinpath('binary.file'))[len(prefix) + 1 :] == os.path.join(
+            'namespacedata01', 'binary.file'
         )
         sub = path.joinpath('subdirectory')
         assert isinstance(sub, MultiplexedPath)
         assert 'namespacedata01' in str(sub)
         assert 'data01' in str(sub)
-        self.assertEqual(
-            str(path.joinpath('imaginary'))[len(prefix) + 1 :],
-            os.path.join('namespacedata01', 'imaginary'),
+        assert str(path.joinpath('imaginary'))[len(prefix) + 1 :] == os.path.join(
+            'namespacedata01', 'imaginary'
         )
-        self.assertEqual(path.joinpath(), path)
+        assert path.joinpath() == path
 
     def test_join_path_compound(self):
         path = MultiplexedPath(self.folder)
@@ -87,23 +86,16 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
     def test_join_path_common_subdir(self):
         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 :],
-            os.path.join('data02', 'subdirectory', 'subsubdir'),
+        assert isinstance(path.joinpath('subdirectory'), MultiplexedPath)
+        assert str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :] == (
+            os.path.join('data02', 'subdirectory', 'subsubdir')
         )
 
     def test_repr(self):
-        self.assertEqual(
-            repr(MultiplexedPath(self.folder)),
-            f"MultiplexedPath('{self.folder}')",
-        )
+        assert repr(MultiplexedPath(self.folder)) == f"MultiplexedPath('{self.folder}')"
 
     def test_name(self):
-        self.assertEqual(
-            MultiplexedPath(self.folder).name,
-            os.path.basename(self.folder),
-        )
+        assert MultiplexedPath(self.folder).name == os.path.basename(self.folder)
 
 
 class NamespaceReaderTest(util.DiskSetup, unittest.TestCase):
@@ -118,18 +110,14 @@ class NamespaceReaderTest(util.DiskSetup, unittest.TestCase):
         reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
 
         root = self.data.__path__[0]
-        self.assertEqual(
-            reader.resource_path('binary.file'), os.path.join(root, 'binary.file')
-        )
-        self.assertEqual(
-            reader.resource_path('imaginary'), os.path.join(root, 'imaginary')
-        )
+        assert reader.resource_path('binary.file') == os.path.join(root, 'binary.file')
+        assert reader.resource_path('imaginary') == os.path.join(root, 'imaginary')
 
     def test_files(self):
         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}')")
+        assert isinstance(reader.files(), MultiplexedPath)
+        assert repr(reader.files()) == f"MultiplexedPath('{root}')"
 
 
 if __name__ == '__main__':
index ef69cd049d9b4c2055f6d236416c9d5a8d3fd9f2..b114b1f0d80fbf345ab0aa3be12ccb617eeede84 100644 (file)
@@ -1,4 +1,5 @@
 import importlib.resources as resources
+import types
 import unittest
 from importlib import import_module
 
@@ -10,16 +11,16 @@ class ResourceTests:
 
     def test_is_file_exists(self):
         target = resources.files(self.data) / 'binary.file'
-        self.assertTrue(target.is_file())
+        assert target.is_file()
 
     def test_is_file_missing(self):
         target = resources.files(self.data) / 'not-a-file'
-        self.assertFalse(target.is_file())
+        assert not target.is_file()
 
     def test_is_dir(self):
         target = resources.files(self.data) / 'subdirectory'
-        self.assertFalse(target.is_file())
-        self.assertTrue(target.is_dir())
+        assert not target.is_file()
+        assert target.is_dir()
 
 
 class ResourceDiskTests(ResourceTests, util.DiskSetup, unittest.TestCase):
@@ -39,7 +40,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
         package = util.create_package(
             file=self.data, path=self.data.__file__, contents=['A', 'B', 'C']
         )
-        self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'})
+        assert names(resources.files(package)) == {'A', 'B', 'C'}
 
     def test_is_file(self):
         package = util.create_package(
@@ -47,7 +48,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
             path=self.data.__file__,
             contents=['A', 'B', 'C', 'D/E', 'D/F'],
         )
-        self.assertTrue(resources.files(package).joinpath('B').is_file())
+        assert resources.files(package).joinpath('B').is_file()
 
     def test_is_dir(self):
         package = util.create_package(
@@ -55,7 +56,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
             path=self.data.__file__,
             contents=['A', 'B', 'C', 'D/E', 'D/F'],
         )
-        self.assertTrue(resources.files(package).joinpath('D').is_dir())
+        assert resources.files(package).joinpath('D').is_dir()
 
     def test_resource_missing(self):
         package = util.create_package(
@@ -63,7 +64,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
             path=self.data.__file__,
             contents=['A', 'B', 'C', 'D/E', 'D/F'],
         )
-        self.assertFalse(resources.files(package).joinpath('Z').is_file())
+        assert not resources.files(package).joinpath('Z').is_file()
 
 
 class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase):
@@ -83,30 +84,26 @@ class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase):
         module.__file__ = '/path/which/shall/not/be/named'
         module.__spec__.loader = module.__loader__
         module.__spec__.origin = module.__file__
-        self.assertFalse(resources.files(module).joinpath('A').is_file())
+        assert not resources.files(module).joinpath('A').is_file()
 
 
 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())
+        assert resources.files(submodule).joinpath('binary.file').is_file()
 
     def test_read_submodule_resource_by_name(self):
-        self.assertTrue(
-            resources.files('data01.subdirectory').joinpath('binary.file').is_file()
-        )
+        assert resources.files('data01.subdirectory').joinpath('binary.file').is_file()
 
     def test_submodule_contents(self):
         submodule = import_module('data01.subdirectory')
-        self.assertEqual(
-            names(resources.files(submodule)), {'__init__.py', 'binary.file'}
-        )
+        assert names(resources.files(submodule)) == {'__init__.py', 'binary.file'}
 
     def test_submodule_contents_by_name(self):
-        self.assertEqual(
-            names(resources.files('data01.subdirectory')),
-            {'__init__.py', 'binary.file'},
-        )
+        assert names(resources.files('data01.subdirectory')) == {
+            '__init__.py',
+            'binary.file',
+        }
 
     def test_as_file_directory(self):
         with resources.as_file(resources.files('data01')) as data:
@@ -125,14 +122,8 @@ class ResourceFromZipsTest02(util.ZipSetup, unittest.TestCase):
         Test thata zip with two unrelated subpackages return
         distinct resources. Ref python/importlib_resources#44.
         """
-        self.assertEqual(
-            names(resources.files('data02.one')),
-            {'__init__.py', 'resource1.txt'},
-        )
-        self.assertEqual(
-            names(resources.files('data02.two')),
-            {'__init__.py', 'resource2.txt'},
-        )
+        assert names(resources.files('data02.one')) == {'__init__.py', 'resource1.txt'}
+        assert names(resources.files('data02.two')) == {'__init__.py', 'resource2.txt'}
 
 
 class DeletingZipsTest(util.ZipSetup, unittest.TestCase):
@@ -169,16 +160,15 @@ class DeletingZipsTest(util.ZipSetup, unittest.TestCase):
 
 class ResourceFromNamespaceTests:
     def test_is_submodule_resource(self):
-        self.assertTrue(
-            resources.files(import_module('namespacedata01'))
+        assert (
+            resources
+            .files(import_module('namespacedata01'))
             .joinpath('binary.file')
             .is_file()
         )
 
     def test_read_submodule_resource_by_name(self):
-        self.assertTrue(
-            resources.files('namespacedata01').joinpath('binary.file').is_file()
-        )
+        assert resources.files('namespacedata01').joinpath('binary.file').is_file()
 
     def test_submodule_contents(self):
         contents = names(resources.files(import_module('namespacedata01')))
@@ -186,9 +176,7 @@ class ResourceFromNamespaceTests:
             contents.remove('__pycache__')
         except KeyError:
             pass
-        self.assertEqual(
-            contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
-        )
+        assert contents == {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
 
     def test_submodule_contents_by_name(self):
         contents = names(resources.files('namespacedata01'))
@@ -196,9 +184,7 @@ class ResourceFromNamespaceTests:
             contents.remove('__pycache__')
         except KeyError:
             pass
-        self.assertEqual(
-            contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
-        )
+        assert contents == {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
 
     def test_submodule_sub_contents(self):
         contents = names(resources.files(import_module('namespacedata01.subdirectory')))
@@ -206,7 +192,7 @@ class ResourceFromNamespaceTests:
             contents.remove('__pycache__')
         except KeyError:
             pass
-        self.assertEqual(contents, {'binary.file'})
+        assert contents == {'binary.file'}
 
     def test_submodule_sub_contents_by_name(self):
         contents = names(resources.files('namespacedata01.subdirectory'))
@@ -214,7 +200,7 @@ class ResourceFromNamespaceTests:
             contents.remove('__pycache__')
         except KeyError:
             pass
-        self.assertEqual(contents, {'binary.file'})
+        assert contents == {'binary.file'}
 
 
 class ResourceFromNamespaceDiskTests(
@@ -233,5 +219,24 @@ class ResourceFromNamespaceZipTests(
     MODULE = 'namespacedata01'
 
 
+class MainModuleTests(unittest.TestCase):
+    def test_main_module_with_none_spec(self):
+        """
+        __main__ module with no spec should raise TypeError (for clarity).
+
+        See python/cpython#138531 for details.
+        """
+        # construct a __main__ module with no __spec__.
+        mainmodule = types.ModuleType("__main__")
+
+        assert mainmodule.__spec__ is None
+
+        with self.assertRaises(
+            TypeError,
+            msg="Cannot access resources for '__main__' as it does not appear to correspond to an importable module (its __spec__ is None).",
+        ):
+            resources.files(mainmodule)
+
+
 if __name__ == '__main__':
     unittest.main()
index d6a99289906e35104b43e003679559026f119b31..85b5c61518de444754b159c8119cfc8e29bfb6bc 100644 (file)
@@ -122,7 +122,7 @@ class CommonTestsBase(metaclass=abc.ABCMeta):
         bytes_data = io.BytesIO(b'Hello, world!')
         package = create_package(file=bytes_data, path=FileNotFoundError())
         self.execute(package, 'utf-8.file')
-        self.assertEqual(package.__loader__._path, 'utf-8.file')
+        assert package.__loader__._path == 'utf-8.file'
 
     def test_extant_path(self):
         # Attempting to open or read or request the path when the
@@ -133,7 +133,7 @@ class CommonTestsBase(metaclass=abc.ABCMeta):
         path = __file__
         package = create_package(file=bytes_data, path=path)
         self.execute(package, 'utf-8.file')
-        self.assertEqual(package.__loader__._path, 'utf-8.file')
+        assert package.__loader__._path == 'utf-8.file'
 
     def test_useless_loader(self):
         package = create_package(file=FileNotFoundError(), path=FileNotFoundError())
diff --git a/Misc/NEWS.d/next/Library/2026-04-12-12-31-45.gh-issue-121190.O6-E5_.rst b/Misc/NEWS.d/next/Library/2026-04-12-12-31-45.gh-issue-121190.O6-E5_.rst
new file mode 100644 (file)
index 0000000..1e18015
--- /dev/null
@@ -0,0 +1,2 @@
+``importlib.resources.files()`` now emits a more meaningful error message
+when module spec is None (as found in some ``__main__`` modules).