Skip to content

Commit aaa83cd

Browse files
authored
bpo-44771: Apply changes from importlib_resources 5.2.1 (GH-27436)
* bpo-44771: Apply changes from importlib_resources@3b24bd6307 * Add blurb * Exclude namespacedata01 from eol conversion.
1 parent 851cca8 commit aaa83cd

19 files changed

+714
-373
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Lib/test/test_email/data/*.txt -text
2828
Lib/test/xmltestdata/* -text
2929
Lib/test/coding20731.py -text
3030
Lib/test/test_importlib/data01/* -text
31+
Lib/test/test_importlib/namespacedata01/* -text
3132

3233
# CRLF files
3334
*.bat text eol=crlf

Lib/importlib/_adapters.py

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from contextlib import suppress
2+
from io import TextIOWrapper
23

34
from . import abc
45

@@ -25,32 +26,119 @@ def __init__(self, spec):
2526
self.spec = spec
2627

2728
def get_resource_reader(self, name):
28-
return DegenerateFiles(self.spec)._native()
29+
return CompatibilityFiles(self.spec)._native()
2930

3031

31-
class DegenerateFiles:
32+
def _io_wrapper(file, mode='r', *args, **kwargs):
33+
if mode == 'r':
34+
return TextIOWrapper(file, *args, **kwargs)
35+
elif mode == 'rb':
36+
return file
37+
raise ValueError(
38+
"Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode)
39+
)
40+
41+
42+
class CompatibilityFiles:
3243
"""
3344
Adapter for an existing or non-existant resource reader
34-
to provide a degenerate .files().
45+
to provide a compability .files().
3546
"""
3647

37-
class Path(abc.Traversable):
48+
class SpecPath(abc.Traversable):
49+
"""
50+
Path tied to a module spec.
51+
Can be read and exposes the resource reader children.
52+
"""
53+
54+
def __init__(self, spec, reader):
55+
self._spec = spec
56+
self._reader = reader
57+
58+
def iterdir(self):
59+
if not self._reader:
60+
return iter(())
61+
return iter(
62+
CompatibilityFiles.ChildPath(self._reader, path)
63+
for path in self._reader.contents()
64+
)
65+
66+
def is_file(self):
67+
return False
68+
69+
is_dir = is_file
70+
71+
def joinpath(self, other):
72+
if not self._reader:
73+
return CompatibilityFiles.OrphanPath(other)
74+
return CompatibilityFiles.ChildPath(self._reader, other)
75+
76+
@property
77+
def name(self):
78+
return self._spec.name
79+
80+
def open(self, mode='r', *args, **kwargs):
81+
return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs)
82+
83+
class ChildPath(abc.Traversable):
84+
"""
85+
Path tied to a resource reader child.
86+
Can be read but doesn't expose any meaningfull children.
87+
"""
88+
89+
def __init__(self, reader, name):
90+
self._reader = reader
91+
self._name = name
92+
3893
def iterdir(self):
3994
return iter(())
4095

96+
def is_file(self):
97+
return self._reader.is_resource(self.name)
98+
4199
def is_dir(self):
100+
return not self.is_file()
101+
102+
def joinpath(self, other):
103+
return CompatibilityFiles.OrphanPath(self.name, other)
104+
105+
@property
106+
def name(self):
107+
return self._name
108+
109+
def open(self, mode='r', *args, **kwargs):
110+
return _io_wrapper(
111+
self._reader.open_resource(self.name), mode, *args, **kwargs
112+
)
113+
114+
class OrphanPath(abc.Traversable):
115+
"""
116+
Orphan path, not tied to a module spec or resource reader.
117+
Can't be read and doesn't expose any meaningful children.
118+
"""
119+
120+
def __init__(self, *path_parts):
121+
if len(path_parts) < 1:
122+
raise ValueError('Need at least one path part to construct a path')
123+
self._path = path_parts
124+
125+
def iterdir(self):
126+
return iter(())
127+
128+
def is_file(self):
42129
return False
43130

44-
is_file = exists = is_dir # type: ignore
131+
is_dir = is_file
45132

46133
def joinpath(self, other):
47-
return DegenerateFiles.Path()
134+
return CompatibilityFiles.OrphanPath(*self._path, other)
48135

136+
@property
49137
def name(self):
50-
return ''
138+
return self._path[-1]
51139

52-
def open(self):
53-
raise ValueError()
140+
def open(self, mode='r', *args, **kwargs):
141+
raise FileNotFoundError("Can't open orphan path")
54142

55143
def __init__(self, spec):
56144
self.spec = spec
@@ -71,7 +159,7 @@ def __getattr__(self, attr):
71159
return getattr(self._reader, attr)
72160

73161
def files(self):
74-
return DegenerateFiles.Path()
162+
return CompatibilityFiles.SpecPath(self.spec, self._reader)
75163

76164

77165
def wrap_spec(package):

Lib/importlib/_common.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ._adapters import wrap_spec
1313

1414
Package = Union[types.ModuleType, str]
15+
Resource = Union[str, os.PathLike]
1516

1617

1718
def files(package):
@@ -93,7 +94,7 @@ def _tempfile(reader, suffix=''):
9394
finally:
9495
try:
9596
os.remove(raw_path)
96-
except FileNotFoundError:
97+
except (FileNotFoundError, PermissionError):
9798
pass
9899

99100

Lib/importlib/_itertools.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from itertools import filterfalse
2+
3+
4+
def unique_everseen(iterable, key=None):
5+
"List unique elements, preserving order. Remember all elements ever seen."
6+
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
7+
# unique_everseen('ABBCcAD', str.lower) --> A B C D
8+
seen = set()
9+
seen_add = seen.add
10+
if key is None:
11+
for element in filterfalse(seen.__contains__, iterable):
12+
seen_add(element)
13+
yield element
14+
else:
15+
for element in iterable:
16+
k = key(element)
17+
if k not in seen:
18+
seen_add(k)
19+
yield element

Lib/importlib/_legacy.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import os
2+
import pathlib
3+
import types
4+
5+
from typing import Union, Iterable, ContextManager, BinaryIO, TextIO
6+
7+
from . import _common
8+
9+
Package = Union[types.ModuleType, str]
10+
Resource = Union[str, os.PathLike]
11+
12+
13+
def open_binary(package: Package, resource: Resource) -> BinaryIO:
14+
"""Return a file-like object opened for binary reading of the resource."""
15+
return (_common.files(package) / _common.normalize_path(resource)).open('rb')
16+
17+
18+
def read_binary(package: Package, resource: Resource) -> bytes:
19+
"""Return the binary contents of the resource."""
20+
return (_common.files(package) / _common.normalize_path(resource)).read_bytes()
21+
22+
23+
def open_text(
24+
package: Package,
25+
resource: Resource,
26+
encoding: str = 'utf-8',
27+
errors: str = 'strict',
28+
) -> TextIO:
29+
"""Return a file-like object opened for text reading of the resource."""
30+
return (_common.files(package) / _common.normalize_path(resource)).open(
31+
'r', encoding=encoding, errors=errors
32+
)
33+
34+
35+
def read_text(
36+
package: Package,
37+
resource: Resource,
38+
encoding: str = 'utf-8',
39+
errors: str = 'strict',
40+
) -> str:
41+
"""Return the decoded string of the resource.
42+
43+
The decoding-related arguments have the same semantics as those of
44+
bytes.decode().
45+
"""
46+
with open_text(package, resource, encoding, errors) as fp:
47+
return fp.read()
48+
49+
50+
def contents(package: Package) -> Iterable[str]:
51+
"""Return an iterable of entries in `package`.
52+
53+
Note that not all entries are resources. Specifically, directories are
54+
not considered resources. Use `is_resource()` on each entry returned here
55+
to check if it is a resource or not.
56+
"""
57+
return [path.name for path in _common.files(package).iterdir()]
58+
59+
60+
def is_resource(package: Package, name: str) -> bool:
61+
"""True if `name` is a resource inside `package`.
62+
63+
Directories are *not* resources.
64+
"""
65+
resource = _common.normalize_path(name)
66+
return any(
67+
traversable.name == resource and traversable.is_file()
68+
for traversable in _common.files(package).iterdir()
69+
)
70+
71+
72+
def path(
73+
package: Package,
74+
resource: Resource,
75+
) -> ContextManager[pathlib.Path]:
76+
"""A context manager providing a file path object to the resource.
77+
78+
If the resource does not already exist on its own on the file system,
79+
a temporary file will be created. If the file was created, the file
80+
will be deleted upon exiting the context manager (no exception is
81+
raised if the file was deleted prior to the context manager
82+
exiting).
83+
"""
84+
return _common.as_file(_common.files(package) / _common.normalize_path(resource))

Lib/importlib/readers.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import collections
2-
import zipfile
2+
import operator
33
import pathlib
4+
import zipfile
5+
46
from . import abc
57

8+
from ._itertools import unique_everseen
9+
610

711
def remove_duplicates(items):
812
return iter(collections.OrderedDict.fromkeys(items))
@@ -63,13 +67,8 @@ def __init__(self, *paths):
6367
raise NotADirectoryError('MultiplexedPath only supports directories')
6468

6569
def iterdir(self):
66-
visited = []
67-
for path in self._paths:
68-
for file in path.iterdir():
69-
if file.name in visited:
70-
continue
71-
visited.append(file.name)
72-
yield file
70+
files = (file for path in self._paths for file in path.iterdir())
71+
return unique_everseen(files, key=operator.attrgetter('name'))
7372

7473
def read_bytes(self):
7574
raise FileNotFoundError(f'{self} is not a file')

0 commit comments

Comments
 (0)