From: Jason R. Coombs Date: Sun, 12 Apr 2026 22:15:01 +0000 (-0400) Subject: gh-121190: Emit a better error message from `importlib.resources.files()` when module... X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=480edc1aae0f54e34d217c72eb2962702c82a666;p=thirdparty%2FPython%2Fcpython.git 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) --- diff --git a/Lib/importlib/resources/_common.py b/Lib/importlib/resources/_common.py index 40eec742aeb7..6f87d77492f2 100644 --- a/Lib/importlib/resources/_common.py +++ b/Lib/importlib/resources/_common.py @@ -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. *, diff --git a/Lib/importlib/resources/abc.py b/Lib/importlib/resources/abc.py index 64a6d843dce9..0b5fdee80e87 100644 --- a/Lib/importlib/resources/abc.py +++ b/Lib/importlib/resources/abc.py @@ -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). diff --git a/Lib/importlib/resources/simple.py b/Lib/importlib/resources/simple.py index 2e75299b13aa..5e182d12607c 100644 --- a/Lib/importlib/resources/simple.py +++ b/Lib/importlib/resources/simple.py @@ -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. """ diff --git a/Lib/test/test_importlib/resources/_path.py b/Lib/test/test_importlib/resources/_path.py index 0033983dc662..3720af7c5085 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 @@ 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] diff --git a/Lib/test/test_importlib/resources/test_compatibilty_files.py b/Lib/test/test_importlib/resources/test_compatibilty_files.py index 113f9ae6fdb2..2fd39bedf258 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 @@ 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) diff --git a/Lib/test/test_importlib/resources/test_files.py b/Lib/test/test_importlib/resources/test_files.py index abb5bf36e68c..c922d32cedc3 100644 --- a/Lib/test/test_importlib/resources/test_files.py +++ b/Lib/test/test_importlib/resources/test_files.py @@ -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): diff --git a/Lib/test/test_importlib/resources/test_functional.py b/Lib/test/test_importlib/resources/test_functional.py index 9e1a3a0e2767..9cec6af9a5d0 100644 --- a/Lib/test/test_importlib/resources/test_functional.py +++ b/Lib/test/test_importlib/resources/test_functional.py @@ -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): diff --git a/Lib/test/test_importlib/resources/test_open.py b/Lib/test/test_importlib/resources/test_open.py index b5a8949d52e5..950f71db05a0 100644 --- a/Lib/test/test_importlib/resources/test_open.py +++ b/Lib/test/test_importlib/resources/test_open.py @@ -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): diff --git a/Lib/test/test_importlib/resources/test_path.py b/Lib/test/test_importlib/resources/test_path.py index 3d158d95b502..162344e5d837 100644 --- a/Lib/test/test_importlib/resources/test_path.py +++ b/Lib/test/test_importlib/resources/test_path.py @@ -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): diff --git a/Lib/test/test_importlib/resources/test_read.py b/Lib/test/test_importlib/resources/test_read.py index cd1cc6dd86ff..4085a64b0eec 100644 --- a/Lib/test/test_importlib/resources/test_read.py +++ b/Lib/test/test_importlib/resources/test_read.py @@ -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__': diff --git a/Lib/test/test_importlib/resources/test_reader.py b/Lib/test/test_importlib/resources/test_reader.py index cf23f38f3aaa..691a78bb060b 100644 --- a/Lib/test/test_importlib/resources/test_reader.py +++ b/Lib/test/test_importlib/resources/test_reader.py @@ -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__': diff --git a/Lib/test/test_importlib/resources/test_resource.py b/Lib/test/test_importlib/resources/test_resource.py index ef69cd049d9b..b114b1f0d80f 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 @@ 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() diff --git a/Lib/test/test_importlib/resources/util.py b/Lib/test/test_importlib/resources/util.py index d6a99289906e..85b5c61518de 100644 --- a/Lib/test/test_importlib/resources/util.py +++ b/Lib/test/test_importlib/resources/util.py @@ -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 index 000000000000..1e18015ed9cd --- /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).