Skip to content

Commit 0660828

Browse files
committed
gh-97930: Apply changes from importlib_resources 5.12.
1 parent 89413bb commit 0660828

File tree

15 files changed

+241
-127
lines changed

15 files changed

+241
-127
lines changed

Lib/importlib/resources/_adapters.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ def _io_wrapper(file, mode='r', *args, **kwargs):
3434
return TextIOWrapper(file, *args, **kwargs)
3535
elif mode == 'rb':
3636
return file
37-
raise ValueError(
38-
f"Invalid mode value '{mode}', only 'r' and 'rb' are supported"
39-
)
37+
raise ValueError(f"Invalid mode value '{mode}', only 'r' and 'rb' are supported")
4038

4139

4240
class CompatibilityFiles:

Lib/importlib/resources/_itertools.py

+36-33
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,38 @@
1-
from itertools import filterfalse
1+
# from more_itertools 9.0
2+
def only(iterable, default=None, too_long=None):
3+
"""If *iterable* has only one item, return it.
4+
If it has zero items, return *default*.
5+
If it has more than one item, raise the exception given by *too_long*,
6+
which is ``ValueError`` by default.
7+
>>> only([], default='missing')
8+
'missing'
9+
>>> only([1])
10+
1
11+
>>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL
12+
Traceback (most recent call last):
13+
...
14+
ValueError: Expected exactly one item in iterable, but got 1, 2,
15+
and perhaps more.'
16+
>>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL
17+
Traceback (most recent call last):
18+
...
19+
TypeError
20+
Note that :func:`only` attempts to advance *iterable* twice to ensure there
21+
is only one item. See :func:`spy` or :func:`peekable` to check
22+
iterable contents less destructively.
23+
"""
24+
it = iter(iterable)
25+
first_value = next(it, default)
226

3-
from typing import (
4-
Callable,
5-
Iterable,
6-
Iterator,
7-
Optional,
8-
Set,
9-
TypeVar,
10-
Union,
11-
)
12-
13-
# Type and type variable definitions
14-
_T = TypeVar('_T')
15-
_U = TypeVar('_U')
16-
17-
18-
def unique_everseen(
19-
iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = None
20-
) -> Iterator[_T]:
21-
"List unique elements, preserving order. Remember all elements ever seen."
22-
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
23-
# unique_everseen('ABBCcAD', str.lower) --> A B C D
24-
seen: Set[Union[_T, _U]] = set()
25-
seen_add = seen.add
26-
if key is None:
27-
for element in filterfalse(seen.__contains__, iterable):
28-
seen_add(element)
29-
yield element
27+
try:
28+
second_value = next(it)
29+
except StopIteration:
30+
pass
3031
else:
31-
for element in iterable:
32-
k = key(element)
33-
if k not in seen:
34-
seen_add(k)
35-
yield element
32+
msg = (
33+
'Expected exactly one item in iterable, but got {!r}, {!r}, '
34+
'and perhaps more.'.format(first_value, second_value)
35+
)
36+
raise too_long or ValueError(msg)
37+
38+
return first_value

Lib/importlib/resources/readers.py

+30-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import collections
2-
import operator
2+
import itertools
33
import pathlib
4+
import operator
45
import zipfile
56

67
from . import abc
78

8-
from ._itertools import unique_everseen
9+
from ._itertools import only
910

1011

1112
def remove_duplicates(items):
@@ -41,8 +42,10 @@ def open_resource(self, resource):
4142
raise FileNotFoundError(exc.args[0])
4243

4344
def is_resource(self, path):
44-
# workaround for `zipfile.Path.is_file` returning true
45-
# for non-existent paths.
45+
"""
46+
Workaround for `zipfile.Path.is_file` returning true
47+
for non-existent paths.
48+
"""
4649
target = self.files().joinpath(path)
4750
return target.is_file() and target.exists()
4851

@@ -67,8 +70,10 @@ def __init__(self, *paths):
6770
raise NotADirectoryError('MultiplexedPath only supports directories')
6871

6972
def iterdir(self):
70-
files = (file for path in self._paths for file in path.iterdir())
71-
return unique_everseen(files, key=operator.attrgetter('name'))
73+
children = (child for path in self._paths for child in path.iterdir())
74+
by_name = operator.attrgetter('name')
75+
groups = itertools.groupby(sorted(children, key=by_name), key=by_name)
76+
return map(self._follow, (locs for name, locs in groups))
7277

7378
def read_bytes(self):
7479
raise FileNotFoundError(f'{self} is not a file')
@@ -90,6 +95,25 @@ def joinpath(self, *descendants):
9095
# Just return something that will not exist.
9196
return self._paths[0].joinpath(*descendants)
9297

98+
@classmethod
99+
def _follow(cls, children):
100+
"""
101+
Construct a MultiplexedPath if needed.
102+
103+
If children contains a sole element, return it.
104+
Otherwise, return a MultiplexedPath of the items.
105+
Unless one of the items is not a Directory, then return the first.
106+
"""
107+
subdirs, one_dir, one_file = itertools.tee(children, 3)
108+
109+
try:
110+
return only(one_dir)
111+
except ValueError:
112+
try:
113+
return cls(*subdirs)
114+
except NotADirectoryError:
115+
return next(one_file)
116+
93117
def open(self, *args, **kwargs):
94118
raise FileNotFoundError(f'{self} is not a file')
95119

Lib/test/test_importlib/resources/_path.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import pathlib
22
import functools
33

4+
from typing import Dict, Union
5+
46

57
####
6-
# from jaraco.path 3.4
8+
# from jaraco.path 3.4.1
9+
10+
FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore
711

812

9-
def build(spec, prefix=pathlib.Path()):
13+
def build(spec: FilesSpec, prefix=pathlib.Path()):
1014
"""
1115
Build a set of files/directories, as described by the spec.
1216
@@ -23,15 +27,17 @@ def build(spec, prefix=pathlib.Path()):
2327
... "baz.py": "# Some code",
2428
... }
2529
... }
26-
>>> tmpdir = getfixture('tmpdir')
27-
>>> build(spec, tmpdir)
30+
>>> target = getfixture('tmp_path')
31+
>>> build(spec, target)
32+
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
33+
'# Some code'
2834
"""
2935
for name, contents in spec.items():
3036
create(contents, pathlib.Path(prefix) / name)
3137

3238

3339
@functools.singledispatch
34-
def create(content, path):
40+
def create(content: Union[str, bytes, FilesSpec], path):
3541
path.mkdir(exist_ok=True)
3642
build(content, prefix=path) # type: ignore
3743

@@ -43,7 +49,7 @@ def _(content: bytes, path):
4349

4450
@create.register
4551
def _(content: str, path):
46-
path.write_text(content)
52+
path.write_text(content, encoding='utf-8')
4753

4854

4955
# end from jaraco.path
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a resource

Lib/test/test_importlib/resources/test_compatibilty_files.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ def test_orphan_path_name(self):
6464

6565
def test_spec_path_open(self):
6666
self.assertEqual(self.files.read_bytes(), b'Hello, world!')
67-
self.assertEqual(self.files.read_text(), 'Hello, world!')
67+
self.assertEqual(self.files.read_text(encoding='utf-8'), 'Hello, world!')
6868

6969
def test_child_path_open(self):
7070
self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!')
71-
self.assertEqual((self.files / 'a').read_text(), 'Hello, world!')
71+
self.assertEqual(
72+
(self.files / 'a').read_text(encoding='utf-8'), 'Hello, world!'
73+
)
7274

7375
def test_orphan_path_open(self):
7476
with self.assertRaises(FileNotFoundError):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import unittest
2+
import contextlib
3+
import pathlib
4+
5+
from test.support import os_helper
6+
7+
from importlib import resources
8+
from importlib.resources.abc import TraversableResources, ResourceReader
9+
from . import util
10+
11+
12+
class SimpleLoader:
13+
"""
14+
A simple loader that only implements a resource reader.
15+
"""
16+
17+
def __init__(self, reader: ResourceReader):
18+
self.reader = reader
19+
20+
def get_resource_reader(self, package):
21+
return self.reader
22+
23+
24+
class MagicResources(TraversableResources):
25+
"""
26+
Magically returns the resources at path.
27+
"""
28+
29+
def __init__(self, path: pathlib.Path):
30+
self.path = path
31+
32+
def files(self):
33+
return self.path
34+
35+
36+
class CustomTraversableResourcesTests(unittest.TestCase):
37+
def setUp(self):
38+
self.fixtures = contextlib.ExitStack()
39+
self.addCleanup(self.fixtures.close)
40+
41+
def test_custom_loader(self):
42+
temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
43+
loader = SimpleLoader(MagicResources(temp_dir))
44+
pkg = util.create_package_from_loader(loader)
45+
files = resources.files(pkg)
46+
assert files is temp_dir

Lib/test/test_importlib/resources/test_files.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def test_module_resources(self):
8585
_path.build(spec, self.site_dir)
8686
import mod
8787

88-
actual = resources.files(mod).joinpath('res.txt').read_text()
88+
actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8')
8989
assert actual == spec['res.txt']
9090

9191

@@ -99,7 +99,7 @@ def test_implicit_files(self):
9999
'__init__.py': textwrap.dedent(
100100
"""
101101
import importlib.resources as res
102-
val = res.files().joinpath('res.txt').read_text()
102+
val = res.files().joinpath('res.txt').read_text(encoding='utf-8')
103103
"""
104104
),
105105
'res.txt': 'resources are the best',

Lib/test/test_importlib/resources/test_open.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def execute(self, package, path):
1515
class CommonTextTests(util.CommonTests, unittest.TestCase):
1616
def execute(self, package, path):
1717
target = resources.files(package).joinpath(path)
18-
with target.open():
18+
with target.open(encoding='utf-8'):
1919
pass
2020

2121

@@ -28,7 +28,7 @@ def test_open_binary(self):
2828

2929
def test_open_text_default_encoding(self):
3030
target = resources.files(self.data) / 'utf-8.file'
31-
with target.open() as fp:
31+
with target.open(encoding='utf-8') as fp:
3232
result = fp.read()
3333
self.assertEqual(result, 'Hello, UTF-8 world!\n')
3434

@@ -39,7 +39,9 @@ def test_open_text_given_encoding(self):
3939
self.assertEqual(result, 'Hello, UTF-16 world!\n')
4040

4141
def test_open_text_with_errors(self):
42-
# Raises UnicodeError without the 'errors' argument.
42+
"""
43+
Raises UnicodeError without the 'errors' argument.
44+
"""
4345
target = resources.files(self.data) / 'utf-16.file'
4446
with target.open(encoding='utf-8', errors='strict') as fp:
4547
self.assertRaises(UnicodeError, fp.read)
@@ -54,11 +56,13 @@ def test_open_text_with_errors(self):
5456

5557
def test_open_binary_FileNotFoundError(self):
5658
target = resources.files(self.data) / 'does-not-exist'
57-
self.assertRaises(FileNotFoundError, target.open, 'rb')
59+
with self.assertRaises(FileNotFoundError):
60+
target.open('rb')
5861

5962
def test_open_text_FileNotFoundError(self):
6063
target = resources.files(self.data) / 'does-not-exist'
61-
self.assertRaises(FileNotFoundError, target.open)
64+
with self.assertRaises(FileNotFoundError):
65+
target.open(encoding='utf-8')
6266

6367

6468
class OpenDiskTests(OpenTests, unittest.TestCase):

Lib/test/test_importlib/resources/test_path.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ def execute(self, package, path):
1414

1515
class PathTests:
1616
def test_reading(self):
17-
# Path should be readable.
18-
# Test also implicitly verifies the returned object is a pathlib.Path
19-
# instance.
17+
"""
18+
Path should be readable.
19+
20+
Test also implicitly verifies the returned object is a pathlib.Path
21+
instance.
22+
"""
2023
target = resources.files(self.data) / 'utf-8.file'
2124
with resources.as_file(target) as path:
2225
self.assertTrue(path.name.endswith("utf-8.file"), repr(path))
@@ -51,8 +54,10 @@ def setUp(self):
5154

5255
class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase):
5356
def test_remove_in_context_manager(self):
54-
# It is not an error if the file that was temporarily stashed on the
55-
# file system is removed inside the `with` stanza.
57+
"""
58+
It is not an error if the file that was temporarily stashed on the
59+
file system is removed inside the `with` stanza.
60+
"""
5661
target = resources.files(self.data) / 'utf-8.file'
5762
with resources.as_file(target) as path:
5863
path.unlink()

Lib/test/test_importlib/resources/test_read.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def execute(self, package, path):
1212

1313
class CommonTextTests(util.CommonTests, unittest.TestCase):
1414
def execute(self, package, path):
15-
resources.files(package).joinpath(path).read_text()
15+
resources.files(package).joinpath(path).read_text(encoding='utf-8')
1616

1717

1818
class ReadTests:
@@ -21,7 +21,11 @@ def test_read_bytes(self):
2121
self.assertEqual(result, b'\0\1\2\3')
2222

2323
def test_read_text_default_encoding(self):
24-
result = resources.files(self.data).joinpath('utf-8.file').read_text()
24+
result = (
25+
resources.files(self.data)
26+
.joinpath('utf-8.file')
27+
.read_text(encoding='utf-8')
28+
)
2529
self.assertEqual(result, 'Hello, UTF-8 world!\n')
2630

2731
def test_read_text_given_encoding(self):
@@ -33,7 +37,9 @@ def test_read_text_given_encoding(self):
3337
self.assertEqual(result, 'Hello, UTF-16 world!\n')
3438

3539
def test_read_text_with_errors(self):
36-
# Raises UnicodeError without the 'errors' argument.
40+
"""
41+
Raises UnicodeError without the 'errors' argument.
42+
"""
3743
target = resources.files(self.data) / 'utf-16.file'
3844
self.assertRaises(UnicodeError, target.read_text, encoding='utf-8')
3945
result = target.read_text(encoding='utf-8', errors='ignore')

0 commit comments

Comments
 (0)