https://github.com/python/cpython/commit/480edc1aae0f54e34d217c72eb2962702c82a666
commit: 480edc1aae0f54e34d217c72eb2962702c82a666
branch: main
author: Jason R. Coombs <[email protected]>
committer: jaraco <[email protected]>
date: 2026-04-12T18:15:01-04:00
summary:

gh-121190: Emit a better error message from `importlib.resources.files()` when 
module spec is `None`" (#148460)

Also merges incidental changes from importlib_resources 7.1.

Co-authored by: Yuichiro Tachibana (Tsuchiya) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-04-12-12-31-45.gh-issue-121190.O6-E5_.rst
M Lib/importlib/resources/_common.py
M Lib/importlib/resources/abc.py
M Lib/importlib/resources/simple.py
M Lib/test/test_importlib/resources/_path.py
M Lib/test/test_importlib/resources/test_compatibilty_files.py
M Lib/test/test_importlib/resources/test_files.py
M Lib/test/test_importlib/resources/test_functional.py
M Lib/test/test_importlib/resources/test_open.py
M Lib/test/test_importlib/resources/test_path.py
M Lib/test/test_importlib/resources/test_read.py
M Lib/test/test_importlib/resources/test_reader.py
M Lib/test/test_importlib/resources/test_resource.py
M Lib/test/test_importlib/resources/util.py

diff --git a/Lib/importlib/resources/_common.py 
b/Lib/importlib/resources/_common.py
index 40eec742aeb70a..6f87d77492f249 100644
--- a/Lib/importlib/resources/_common.py
+++ b/Lib/importlib/resources/_common.py
@@ -7,11 +7,11 @@
 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 is_this_file(frame_info):
         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 is_wrapper(frame_info):
     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.
     *,
diff --git a/Lib/importlib/resources/abc.py b/Lib/importlib/resources/abc.py
index 64a6d843dce98e..0b5fdee80e8796 100644
--- a/Lib/importlib/resources/abc.py
+++ b/Lib/importlib/resources/abc.py
@@ -2,23 +2,21 @@
 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 @@ def open(self, mode: Literal['r'] = 'r', *args: Any, 
**kwargs: Any) -> TextIO: .
     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).
diff --git a/Lib/importlib/resources/simple.py 
b/Lib/importlib/resources/simple.py
index 2e75299b13aabf..5e182d12607c45 100644
--- a/Lib/importlib/resources/simple.py
+++ b/Lib/importlib/resources/simple.py
@@ -5,7 +5,7 @@
 import abc
 import io
 import itertools
-from typing import BinaryIO, List
+from typing import BinaryIO
 
 from .abc import Traversable, TraversableResources
 
@@ -24,14 +24,14 @@ def package(self) -> str:
         """
 
     @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.
         """
diff --git a/Lib/test/test_importlib/resources/_path.py 
b/Lib/test/test_importlib/resources/_path.py
index 0033983dc66286..3720af7c5085d7 100644
--- a/Lib/test/test_importlib/resources/_path.py
+++ b/Lib/test/test_importlib/resources/_path.py
@@ -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 @@ def write_bytes(self, content): ...  # pragma: no cover
     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]
 
diff --git a/Lib/test/test_importlib/resources/test_compatibilty_files.py 
b/Lib/test/test_importlib/resources/test_compatibilty_files.py
index 113f9ae6fdb20d..2fd39bedf258d1 100644
--- a/Lib/test/test_importlib/resources/test_compatibilty_files.py
+++ b/Lib/test/test_importlib/resources/test_compatibilty_files.py
@@ -24,51 +24,46 @@ def files(self):
         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 @@ def test_orphan_path_invalid(self):
 
     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 @@ def files(self):
         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)
diff --git a/Lib/test/test_importlib/resources/test_files.py 
b/Lib/test/test_importlib/resources/test_files.py
index abb5bf36e68c9f..c922d32cedc307 100644
--- a/Lib/test/test_importlib/resources/test_files.py
+++ b/Lib/test/test_importlib/resources/test_files.py
@@ -37,7 +37,7 @@ def test_traversable(self):
     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):
diff --git a/Lib/test/test_importlib/resources/test_functional.py 
b/Lib/test/test_importlib/resources/test_functional.py
index 9e1a3a0e2767e3..9cec6af9a5d051 100644
--- a/Lib/test/test_importlib/resources/test_functional.py
+++ b/Lib/test/test_importlib/resources/test_functional.py
@@ -38,35 +38,40 @@ def _gen_resourcetxt_path_parts(self):
             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 @@ def test_read_text(self):
             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 @@ def test_open_text(self):
             '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 @@ def test_contents(self):
                 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):
diff --git a/Lib/test/test_importlib/resources/test_open.py 
b/Lib/test/test_importlib/resources/test_open.py
index b5a8949d52e532..950f71db05a018 100644
--- a/Lib/test/test_importlib/resources/test_open.py
+++ b/Lib/test/test_importlib/resources/test_open.py
@@ -23,19 +23,19 @@ def test_open_binary(self):
         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 @@ def test_open_text_with_errors(self):
             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):
diff --git a/Lib/test/test_importlib/resources/test_path.py 
b/Lib/test/test_importlib/resources/test_path.py
index 3d158d95b5023a..162344e5d83749 100644
--- a/Lib/test/test_importlib/resources/test_path.py
+++ b/Lib/test/test_importlib/resources/test_path.py
@@ -19,9 +19,9 @@ def test_reading(self):
         """
         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):
diff --git a/Lib/test/test_importlib/resources/test_read.py 
b/Lib/test/test_importlib/resources/test_read.py
index cd1cc6dd86ff47..4085a64b0eec57 100644
--- a/Lib/test/test_importlib/resources/test_read.py
+++ b/Lib/test/test_importlib/resources/test_read.py
@@ -18,23 +18,25 @@ def execute(self, package, path):
 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 @@ def test_read_text_with_errors(self):
         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__':
diff --git a/Lib/test/test_importlib/resources/test_reader.py 
b/Lib/test/test_importlib/resources/test_reader.py
index cf23f38f3aaac5..691a78bb060b39 100644
--- a/Lib/test/test_importlib/resources/test_reader.py
+++ b/Lib/test/test_importlib/resources/test_reader.py
@@ -30,9 +30,7 @@ def test_iterdir(self):
             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 @@ def test_iterdir_duplicate(self):
                 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 @@ def test_open_file(self):
     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 @@ def test_join_path_compound(self):
     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 @@ def test_resource_path(self):
         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__':
diff --git a/Lib/test/test_importlib/resources/test_resource.py 
b/Lib/test/test_importlib/resources/test_resource.py
index ef69cd049d9b4c..b114b1f0d80fbf 100644
--- a/Lib/test/test_importlib/resources/test_resource.py
+++ b/Lib/test/test_importlib/resources/test_resource.py
@@ -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 @@ def test_resource_contents(self):
         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 @@ def test_is_file(self):
             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 @@ def test_is_dir(self):
             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 @@ def test_resource_missing(self):
             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 @@ def test_package_has_no_reader_fallback(self):
         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 @@ def test_unrelated_contents(self):
         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 @@ def test_read_text_does_not_keep_open(self):
 
 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 @@ def test_submodule_contents(self):
             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 @@ def test_submodule_contents_by_name(self):
             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 @@ def test_submodule_sub_contents(self):
             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 @@ def test_submodule_sub_contents_by_name(self):
             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()
diff --git a/Lib/test/test_importlib/resources/util.py 
b/Lib/test/test_importlib/resources/util.py
index d6a99289906e35..85b5c61518de44 100644
--- a/Lib/test/test_importlib/resources/util.py
+++ b/Lib/test/test_importlib/resources/util.py
@@ -122,7 +122,7 @@ def test_missing_path(self):
         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 @@ def test_extant_path(self):
         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
index 00000000000000..1e18015ed9cd43
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-04-12-12-31-45.gh-issue-121190.O6-E5_.rst
@@ -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).

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to