Skip to content

Commit 480edc1

Browse files
authored
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) <t.yic.yt@gmail.com>
1 parent b29afe6 commit 480edc1

File tree

14 files changed

+197
-202
lines changed

14 files changed

+197
-202
lines changed

Lib/importlib/resources/_common.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
import pathlib
88
import tempfile
99
import types
10-
from typing import cast, Optional, Union
10+
from typing import Optional, cast
1111

1212
from .abc import ResourceReader, Traversable
1313

14-
Package = Union[types.ModuleType, str]
14+
Package = types.ModuleType | str
1515
Anchor = Package
1616

1717

@@ -32,7 +32,7 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
3232
# zipimport.zipimporter does not support weak references, resulting in a
3333
# TypeError. That seems terrible.
3434
spec = package.__spec__
35-
reader = getattr(spec.loader, "get_resource_reader", None) # type: ignore[union-attr]
35+
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore[union-attr]
3636
if reader is None:
3737
return None
3838
return reader(spec.name) # type: ignore[union-attr]
@@ -50,7 +50,7 @@ def _(cand: str) -> types.ModuleType:
5050

5151
@resolve.register
5252
def _(cand: None) -> types.ModuleType:
53-
return resolve(_infer_caller().f_globals["__name__"])
53+
return resolve(_infer_caller().f_globals['__name__'])
5454

5555

5656
def _infer_caller():
@@ -62,7 +62,7 @@ def is_this_file(frame_info):
6262
return frame_info.filename == stack[0].filename
6363

6464
def is_wrapper(frame_info):
65-
return frame_info.function == "wrapper"
65+
return frame_info.function == 'wrapper'
6666

6767
stack = inspect.stack()
6868
not_this_file = itertools.filterfalse(is_this_file, stack)
@@ -71,6 +71,19 @@ def is_wrapper(frame_info):
7171
return next(callers).frame
7272

7373

74+
def _assert_spec(package: types.ModuleType) -> None:
75+
"""
76+
Provide a nicer error message when package is ``__main__``
77+
and its ``__spec__`` is ``None``
78+
(https://docs.python.org/3/reference/import.html#main-spec).
79+
"""
80+
if package.__spec__ is None:
81+
raise TypeError(
82+
f"Cannot access resources for '{package.__name__}' "
83+
"as it does not appear to correspond to an importable module (its __spec__ is None)."
84+
)
85+
86+
7487
def from_package(package: types.ModuleType):
7588
"""
7689
Return a Traversable object for the given package.
@@ -79,6 +92,7 @@ def from_package(package: types.ModuleType):
7992
# deferred for performance (python/cpython#109829)
8093
from ._adapters import wrap_spec
8194

95+
_assert_spec(package)
8296
spec = wrap_spec(package)
8397
reader = spec.loader.get_resource_reader(spec.name)
8498
return reader.files()
@@ -87,7 +101,7 @@ def from_package(package: types.ModuleType):
87101
@contextlib.contextmanager
88102
def _tempfile(
89103
reader,
90-
suffix="",
104+
suffix='',
91105
# gh-93353: Keep a reference to call os.remove() in late Python
92106
# finalization.
93107
*,

Lib/importlib/resources/abc.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@
22
import itertools
33
import os
44
import pathlib
5+
from collections.abc import Iterable, Iterator
56
from typing import (
67
Any,
78
BinaryIO,
8-
Iterable,
9-
Iterator,
109
Literal,
1110
NoReturn,
1211
Optional,
1312
Protocol,
1413
Text,
1514
TextIO,
16-
Union,
1715
overload,
1816
runtime_checkable,
1917
)
2018

21-
StrPath = Union[str, os.PathLike[str]]
19+
StrPath = str | os.PathLike[str]
2220

2321
__all__ = ["ResourceReader", "Traversable", "TraversableResources"]
2422

@@ -151,9 +149,7 @@ def open(self, mode: Literal['r'] = 'r', *args: Any, **kwargs: Any) -> TextIO: .
151149
def open(self, mode: Literal['rb'], *args: Any, **kwargs: Any) -> BinaryIO: ...
152150

153151
@abc.abstractmethod
154-
def open(
155-
self, mode: str = 'r', *args: Any, **kwargs: Any
156-
) -> Union[TextIO, BinaryIO]:
152+
def open(self, mode: str = 'r', *args: Any, **kwargs: Any) -> TextIO | BinaryIO:
157153
"""
158154
mode may be 'r' or 'rb' to open as text or binary. Return a handle
159155
suitable for reading (same as pathlib.Path.open).

Lib/importlib/resources/simple.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import abc
66
import io
77
import itertools
8-
from typing import BinaryIO, List
8+
from typing import BinaryIO
99

1010
from .abc import Traversable, TraversableResources
1111

@@ -24,14 +24,14 @@ def package(self) -> str:
2424
"""
2525

2626
@abc.abstractmethod
27-
def children(self) -> List['SimpleReader']:
27+
def children(self) -> list['SimpleReader']:
2828
"""
2929
Obtain an iterable of SimpleReader for available
3030
child containers (e.g. directories).
3131
"""
3232

3333
@abc.abstractmethod
34-
def resources(self) -> List[str]:
34+
def resources(self) -> list[str]:
3535
"""
3636
Obtain available named resources for this virtual package.
3737
"""

Lib/test/test_importlib/resources/_path.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import functools
22
import pathlib
3-
from typing import Dict, Protocol, Union, runtime_checkable
3+
from typing import Protocol, Union, runtime_checkable
44

55
####
66
# from jaraco.path 3.7.1
@@ -12,7 +12,7 @@ class Symlink(str):
1212
"""
1313

1414

15-
FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']]
15+
FilesSpec = dict[str, Union[str, bytes, Symlink, 'FilesSpec']]
1616

1717

1818
@runtime_checkable
@@ -28,13 +28,13 @@ def write_bytes(self, content): ... # pragma: no cover
2828
def symlink_to(self, target): ... # pragma: no cover
2929

3030

31-
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
31+
def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker:
3232
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value]
3333

3434

3535
def build(
3636
spec: FilesSpec,
37-
prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment]
37+
prefix: str | TreeMaker = pathlib.Path(), # type: ignore[assignment]
3838
):
3939
"""
4040
Build a set of files/directories, as described by the spec.
@@ -66,7 +66,7 @@ def build(
6666

6767

6868
@functools.singledispatch
69-
def create(content: Union[str, bytes, FilesSpec], path):
69+
def create(content: str | bytes | FilesSpec, path):
7070
path.mkdir(exist_ok=True)
7171
build(content, prefix=path) # type: ignore[arg-type]
7272

Lib/test/test_importlib/resources/test_compatibilty_files.py

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,51 +24,46 @@ def files(self):
2424
return resources.files(self.package)
2525

2626
def test_spec_path_iter(self):
27-
self.assertEqual(
28-
sorted(path.name for path in self.files.iterdir()),
29-
['a', 'b', 'c'],
30-
)
27+
assert sorted(path.name for path in self.files.iterdir()) == ['a', 'b', 'c']
3128

3229
def test_child_path_iter(self):
33-
self.assertEqual(list((self.files / 'a').iterdir()), [])
30+
assert list((self.files / 'a').iterdir()) == []
3431

3532
def test_orphan_path_iter(self):
36-
self.assertEqual(list((self.files / 'a' / 'a').iterdir()), [])
37-
self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), [])
33+
assert list((self.files / 'a' / 'a').iterdir()) == []
34+
assert list((self.files / 'a' / 'a' / 'a').iterdir()) == []
3835

3936
def test_spec_path_is(self):
40-
self.assertFalse(self.files.is_file())
41-
self.assertFalse(self.files.is_dir())
37+
assert not self.files.is_file()
38+
assert not self.files.is_dir()
4239

4340
def test_child_path_is(self):
44-
self.assertTrue((self.files / 'a').is_file())
45-
self.assertFalse((self.files / 'a').is_dir())
41+
assert (self.files / 'a').is_file()
42+
assert not (self.files / 'a').is_dir()
4643

4744
def test_orphan_path_is(self):
48-
self.assertFalse((self.files / 'a' / 'a').is_file())
49-
self.assertFalse((self.files / 'a' / 'a').is_dir())
50-
self.assertFalse((self.files / 'a' / 'a' / 'a').is_file())
51-
self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir())
45+
assert not (self.files / 'a' / 'a').is_file()
46+
assert not (self.files / 'a' / 'a').is_dir()
47+
assert not (self.files / 'a' / 'a' / 'a').is_file()
48+
assert not (self.files / 'a' / 'a' / 'a').is_dir()
5249

5350
def test_spec_path_name(self):
54-
self.assertEqual(self.files.name, 'testingpackage')
51+
assert self.files.name == 'testingpackage'
5552

5653
def test_child_path_name(self):
57-
self.assertEqual((self.files / 'a').name, 'a')
54+
assert (self.files / 'a').name == 'a'
5855

5956
def test_orphan_path_name(self):
60-
self.assertEqual((self.files / 'a' / 'b').name, 'b')
61-
self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c')
57+
assert (self.files / 'a' / 'b').name == 'b'
58+
assert (self.files / 'a' / 'b' / 'c').name == 'c'
6259

6360
def test_spec_path_open(self):
64-
self.assertEqual(self.files.read_bytes(), b'Hello, world!')
65-
self.assertEqual(self.files.read_text(encoding='utf-8'), 'Hello, world!')
61+
assert self.files.read_bytes() == b'Hello, world!'
62+
assert self.files.read_text(encoding='utf-8') == 'Hello, world!'
6663

6764
def test_child_path_open(self):
68-
self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!')
69-
self.assertEqual(
70-
(self.files / 'a').read_text(encoding='utf-8'), 'Hello, world!'
71-
)
65+
assert (self.files / 'a').read_bytes() == b'Hello, world!'
66+
assert (self.files / 'a').read_text(encoding='utf-8') == 'Hello, world!'
7267

7368
def test_orphan_path_open(self):
7469
with self.assertRaises(FileNotFoundError):
@@ -86,7 +81,7 @@ def test_orphan_path_invalid(self):
8681

8782
def test_wrap_spec(self):
8883
spec = wrap_spec(self.package)
89-
self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles)
84+
assert isinstance(spec.loader.get_resource_reader(None), CompatibilityFiles)
9085

9186

9287
class CompatibilityFilesNoReaderTests(unittest.TestCase):
@@ -99,4 +94,4 @@ def files(self):
9994
return resources.files(self.package)
10095

10196
def test_spec_path_joinpath(self):
102-
self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath)
97+
assert isinstance(self.files / 'a', CompatibilityFiles.OrphanPath)

Lib/test/test_importlib/resources/test_files.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_traversable(self):
3737
def test_joinpath_with_multiple_args(self):
3838
files = resources.files(self.data)
3939
binfile = files.joinpath('subdirectory', 'binary.file')
40-
self.assertTrue(binfile.is_file())
40+
assert binfile.is_file()
4141

4242

4343
class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase):

0 commit comments

Comments
 (0)