Skip to content

Commit 189d15f

Browse files
committed
Apply CPython PR, sans docs and changelogs
1 parent 0db550c commit 189d15f

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed

importlib_resources/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
Anchor,
88
)
99

10+
from .functional import (
11+
contents,
12+
is_resource,
13+
open_binary,
14+
open_text,
15+
path,
16+
read_binary,
17+
read_text,
18+
)
19+
1020
from .abc import ResourceReader
1121

1222

@@ -16,4 +26,11 @@
1626
'ResourceReader',
1727
'as_file',
1828
'files',
29+
'contents',
30+
'is_resource',
31+
'open_binary',
32+
'open_text',
33+
'path',
34+
'read_binary',
35+
'read_text',
1936
]

importlib_resources/functional.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Simplified function-based API for importlib.resources
2+
3+
4+
"""
5+
6+
import os
7+
import warnings
8+
9+
from ._common import files, as_file
10+
11+
12+
_MISSING = object()
13+
14+
def open_binary(anchor, *path_names):
15+
"""Open for binary reading the *resource* within *package*."""
16+
return _get_resource(anchor, path_names).open('rb')
17+
18+
19+
def open_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
20+
"""Open for text reading the *resource* within *package*."""
21+
encoding = _get_encoding_arg(path_names, encoding)
22+
resource = _get_resource(anchor, path_names)
23+
return resource.open('r', encoding=encoding, errors=errors)
24+
25+
26+
def read_binary(anchor, *path_names):
27+
"""Read and return contents of *resource* within *package* as bytes."""
28+
return _get_resource(anchor, path_names).read_bytes()
29+
30+
31+
def read_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
32+
"""Read and return contents of *resource* within *package* as str."""
33+
encoding = _get_encoding_arg(path_names, encoding)
34+
resource = _get_resource(anchor, path_names)
35+
return resource.read_text(encoding=encoding, errors=errors)
36+
37+
38+
def path(anchor, *path_names):
39+
"""Return the path to the *resource* as an actual file system path."""
40+
return as_file(_get_resource(anchor, path_names))
41+
42+
43+
def is_resource(anchor, *path_names):
44+
"""Return ``True`` if there is a resource named *name* in the package,
45+
46+
Otherwise returns ``False``.
47+
"""
48+
return _get_resource(anchor, path_names).is_file()
49+
50+
51+
def contents(anchor, *path_names):
52+
"""Return an iterable over the named resources within the package.
53+
54+
The iterable returns :class:`str` resources (e.g. files).
55+
The iterable does not recurse into subdirectories.
56+
"""
57+
warnings.warn(
58+
"importlib.resources.contents is deprecated. "
59+
"Use files(anchor).iterdir() instead.",
60+
DeprecationWarning,
61+
stacklevel=1,
62+
)
63+
return (
64+
resource.name
65+
for resource
66+
in _get_resource(anchor, path_names).iterdir()
67+
)
68+
69+
70+
def _get_encoding_arg(path_names, encoding):
71+
# For compatibility with versions where *encoding* was a positional
72+
# argument, it needs to be given explicitly when there are multiple
73+
# *path_names*.
74+
# This limitation can be removed in Python 3.15.
75+
if encoding is _MISSING:
76+
if len(path_names) > 1:
77+
raise TypeError(
78+
"'encoding' argument required with multiple path names",
79+
)
80+
else:
81+
return 'utf-8'
82+
return encoding
83+
84+
85+
def _get_resource(anchor, path_names):
86+
if anchor is None:
87+
raise TypeError("anchor must be module or string, got None")
88+
return files(anchor).joinpath(*path_names)
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import unittest
2+
import os
3+
4+
from test.support.warnings_helper import ignore_warnings, check_warnings
5+
6+
import importlib.resources
7+
8+
# Since the functional API forwards to Traversable, we only test
9+
# filesystem resources here -- not zip files, namespace packages etc.
10+
# We do test for two kinds of Anchor, though.
11+
12+
13+
class StringAnchorMixin:
14+
anchor01 = 'test.test_importlib.resources.data01'
15+
anchor02 = 'test.test_importlib.resources.data02'
16+
17+
18+
class ModuleAnchorMixin:
19+
from test.test_importlib.resources import data01 as anchor01
20+
from test.test_importlib.resources import data02 as anchor02
21+
22+
23+
class FunctionalAPIBase():
24+
def _gen_resourcetxt_path_parts(self):
25+
"""Yield various names of a text file in anchor02, each in a subTest
26+
"""
27+
for path_parts in (
28+
('subdirectory', 'subsubdir', 'resource.txt'),
29+
('subdirectory/subsubdir/resource.txt',),
30+
('subdirectory/subsubdir', 'resource.txt'),
31+
):
32+
with self.subTest(path_parts=path_parts):
33+
yield path_parts
34+
35+
def test_read_text(self):
36+
self.assertEqual(
37+
importlib.resources.read_text(self.anchor01, 'utf-8.file'),
38+
'Hello, UTF-8 world!\n',
39+
)
40+
self.assertEqual(
41+
importlib.resources.read_text(
42+
self.anchor02, 'subdirectory', 'subsubdir', 'resource.txt',
43+
encoding='utf-8',
44+
),
45+
'a resource',
46+
)
47+
for path_parts in self._gen_resourcetxt_path_parts():
48+
self.assertEqual(
49+
importlib.resources.read_text(
50+
self.anchor02, *path_parts, encoding='utf-8',
51+
),
52+
'a resource',
53+
)
54+
# Use generic OSError, since e.g. attempting to read a directory can
55+
# fail with PermissionError rather than IsADirectoryError
56+
with self.assertRaises(OSError):
57+
importlib.resources.read_text(self.anchor01)
58+
with self.assertRaises(OSError):
59+
importlib.resources.read_text(self.anchor01, 'no-such-file')
60+
with self.assertRaises(UnicodeDecodeError):
61+
importlib.resources.read_text(self.anchor01, 'utf-16.file')
62+
self.assertEqual(
63+
importlib.resources.read_text(
64+
self.anchor01, 'binary.file', encoding='latin1',
65+
),
66+
'\x00\x01\x02\x03',
67+
)
68+
self.assertEqual(
69+
importlib.resources.read_text(
70+
self.anchor01, 'utf-16.file',
71+
errors='backslashreplace',
72+
),
73+
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
74+
errors='backslashreplace',
75+
)
76+
)
77+
78+
def test_read_binary(self):
79+
self.assertEqual(
80+
importlib.resources.read_binary(self.anchor01, 'utf-8.file'),
81+
b'Hello, UTF-8 world!\n',
82+
)
83+
for path_parts in self._gen_resourcetxt_path_parts():
84+
self.assertEqual(
85+
importlib.resources.read_binary(self.anchor02, *path_parts),
86+
b'a resource',
87+
)
88+
89+
def test_open_text(self):
90+
with importlib.resources.open_text(self.anchor01, 'utf-8.file') as f:
91+
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
92+
for path_parts in self._gen_resourcetxt_path_parts():
93+
with importlib.resources.open_text(
94+
self.anchor02, *path_parts,
95+
encoding='utf-8',
96+
) as f:
97+
self.assertEqual(f.read(), 'a resource')
98+
# Use generic OSError, since e.g. attempting to read a directory can
99+
# fail with PermissionError rather than IsADirectoryError
100+
with self.assertRaises(OSError):
101+
importlib.resources.open_text(self.anchor01)
102+
with self.assertRaises(OSError):
103+
importlib.resources.open_text(self.anchor01, 'no-such-file')
104+
with importlib.resources.open_text(self.anchor01, 'utf-16.file') as f:
105+
with self.assertRaises(UnicodeDecodeError):
106+
f.read()
107+
with importlib.resources.open_text(
108+
self.anchor01, 'binary.file', encoding='latin1',
109+
) as f:
110+
self.assertEqual(f.read(), '\x00\x01\x02\x03')
111+
with importlib.resources.open_text(
112+
self.anchor01, 'utf-16.file',
113+
errors='backslashreplace',
114+
) as f:
115+
self.assertEqual(
116+
f.read(),
117+
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
118+
errors='backslashreplace',
119+
)
120+
)
121+
122+
def test_open_binary(self):
123+
with importlib.resources.open_binary(self.anchor01, 'utf-8.file') as f:
124+
self.assertEqual(f.read(), b'Hello, UTF-8 world!\n')
125+
for path_parts in self._gen_resourcetxt_path_parts():
126+
with importlib.resources.open_binary(
127+
self.anchor02, *path_parts,
128+
) as f:
129+
self.assertEqual(f.read(), b'a resource')
130+
131+
def test_path(self):
132+
with importlib.resources.path(self.anchor01, 'utf-8.file') as path:
133+
with open(str(path)) as f:
134+
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
135+
with importlib.resources.path(self.anchor01) as path:
136+
with open(os.path.join(path, 'utf-8.file')) as f:
137+
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
138+
139+
def test_is_resource(self):
140+
is_resource = importlib.resources.is_resource
141+
self.assertTrue(is_resource(self.anchor01, 'utf-8.file'))
142+
self.assertFalse(is_resource(self.anchor01, 'no_such_file'))
143+
self.assertFalse(is_resource(self.anchor01))
144+
self.assertFalse(is_resource(self.anchor01, 'subdirectory'))
145+
for path_parts in self._gen_resourcetxt_path_parts():
146+
self.assertTrue(is_resource(self.anchor02, *path_parts))
147+
148+
def test_contents(self):
149+
is_resource = importlib.resources.is_resource
150+
with check_warnings((".*contents.*", DeprecationWarning)):
151+
c = importlib.resources.contents(self.anchor01)
152+
self.assertGreaterEqual(
153+
set(c),
154+
{'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'},
155+
)
156+
with (self.assertRaises(OSError),
157+
check_warnings((".*contents.*", DeprecationWarning)),
158+
):
159+
importlib.resources.contents(self.anchor01, 'utf-8.file')
160+
for path_parts in self._gen_resourcetxt_path_parts():
161+
with (self.assertRaises(OSError),
162+
check_warnings((".*contents.*", DeprecationWarning)),
163+
):
164+
importlib.resources.contents(self.anchor01, *path_parts)
165+
with check_warnings((".*contents.*", DeprecationWarning)):
166+
c = importlib.resources.contents(self.anchor01, 'subdirectory')
167+
self.assertGreaterEqual(
168+
set(c),
169+
{'binary.file'},
170+
)
171+
172+
@ignore_warnings(category=DeprecationWarning)
173+
def test_common_errors(self):
174+
for func in (
175+
importlib.resources.read_text,
176+
importlib.resources.read_binary,
177+
importlib.resources.open_text,
178+
importlib.resources.open_binary,
179+
importlib.resources.path,
180+
importlib.resources.is_resource,
181+
importlib.resources.contents,
182+
):
183+
with self.subTest(func=func):
184+
# Rejecting None anchor
185+
with self.assertRaises(TypeError):
186+
func(None)
187+
# Rejecting invalid anchor type
188+
with self.assertRaises((TypeError, AttributeError)):
189+
func(1234)
190+
# Unknown module
191+
with self.assertRaises(ModuleNotFoundError):
192+
func('$missing module$')
193+
194+
def test_text_errors(self):
195+
for func in (
196+
importlib.resources.read_text,
197+
importlib.resources.open_text,
198+
):
199+
with self.subTest(func=func):
200+
# Multiple path arguments need explicit encoding argument.
201+
with self.assertRaises(TypeError):
202+
func(
203+
self.anchor02, 'subdirectory',
204+
'subsubdir', 'resource.txt',
205+
)
206+
207+
208+
class FunctionalAPITest_StringAnchor(
209+
unittest.TestCase, FunctionalAPIBase, StringAnchorMixin,
210+
):
211+
pass
212+
213+
214+
class FunctionalAPITest_ModuleAnchor(
215+
unittest.TestCase, FunctionalAPIBase, ModuleAnchorMixin,
216+
):
217+
pass

0 commit comments

Comments
 (0)