Skip to content

Commit 9cf1be4

Browse files
bpo-39791 native hooks for importlib.resources.files (GH-20576)
* Provide native .files support on SourceFileLoader. * Add native importlib.resources.files() support to zipimporter. Remove fallback support. * make regen-all * 📜🤖 Added by blurb_it. * Move 'files' into the ResourceReader so it can carry the relevant module name context. * Create 'importlib.readers' module and add FileReader to it. * Add zip reader and rely on it for a TraversableResources object on zipimporter. * Remove TraversableAdapter, no longer needed. * Update blurb. * Replace backslashes with forward slashes. * Incorporate changes from importlib_metadata 2.0, finalizing the interface for extension via get_resource_reader. Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> (cherry picked from commit 843c277) Co-authored-by: Jason R. Coombs <[email protected]>
1 parent 6440911 commit 9cf1be4

File tree

9 files changed

+2118
-2359
lines changed

9 files changed

+2118
-2359
lines changed

‎Lib/importlib/_bootstrap_external.py‎

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -982,32 +982,10 @@ def get_data(self, path):
982982
with _io.FileIO(path, 'r') as file:
983983
return file.read()
984984

985-
# ResourceReader ABC API.
986-
987985
@_check_name
988986
def get_resource_reader(self, module):
989-
if self.is_package(module):
990-
return self
991-
return None
992-
993-
def open_resource(self, resource):
994-
path = _path_join(_path_split(self.path)[0], resource)
995-
return _io.FileIO(path, 'r')
996-
997-
def resource_path(self, resource):
998-
if not self.is_resource(resource):
999-
raise FileNotFoundError
1000-
path = _path_join(_path_split(self.path)[0], resource)
1001-
return path
1002-
1003-
def is_resource(self, name):
1004-
if path_sep in name:
1005-
return False
1006-
path = _path_join(_path_split(self.path)[0], name)
1007-
return _path_isfile(path)
1008-
1009-
def contents(self):
1010-
return iter(_os.listdir(_path_split(self.path)[0]))
987+
from importlib.readers import FileReader
988+
return FileReader(self)
1011989

1012990

1013991
class SourceFileLoader(FileLoader, SourceLoader):

‎Lib/importlib/_common.py‎

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,82 @@
11
import os
22
import pathlib
3-
import zipfile
43
import tempfile
54
import functools
65
import contextlib
6+
import types
7+
import importlib
78

9+
from typing import Union, Any, Optional
10+
from .abc import ResourceReader
811

9-
def from_package(package):
12+
Package = Union[types.ModuleType, str]
13+
14+
15+
def files(package):
1016
"""
11-
Return a Traversable object for the given package.
17+
Get a Traversable resource from a package
18+
"""
19+
return from_package(get_package(package))
20+
1221

22+
def normalize_path(path):
23+
# type: (Any) -> str
24+
"""Normalize a path by ensuring it is a string.
25+
26+
If the resulting string contains path separators, an exception is raised.
1327
"""
14-
spec = package.__spec__
15-
return from_traversable_resources(spec) or fallback_resources(spec)
28+
str_path = str(path)
29+
parent, file_name = os.path.split(str_path)
30+
if parent:
31+
raise ValueError('{!r} must be only a file name'.format(path))
32+
return file_name
1633

1734

18-
def from_traversable_resources(spec):
35+
def get_resource_reader(package):
36+
# type: (types.ModuleType) -> Optional[ResourceReader]
1937
"""
20-
If the spec.loader implements TraversableResources,
21-
directly or implicitly, it will have a ``files()`` method.
38+
Return the package's loader if it's a ResourceReader.
2239
"""
23-
with contextlib.suppress(AttributeError):
24-
return spec.loader.files()
40+
# We can't use
41+
# a issubclass() check here because apparently abc.'s __subclasscheck__()
42+
# hook wants to create a weak reference to the object, but
43+
# zipimport.zipimporter does not support weak references, resulting in a
44+
# TypeError. That seems terrible.
45+
spec = package.__spec__
46+
reader = getattr(spec.loader, 'get_resource_reader', None)
47+
if reader is None:
48+
return None
49+
return reader(spec.name)
2550

2651

27-
def fallback_resources(spec):
28-
package_directory = pathlib.Path(spec.origin).parent
29-
try:
30-
archive_path = spec.loader.archive
31-
rel_path = package_directory.relative_to(archive_path)
32-
return zipfile.Path(archive_path, str(rel_path) + '/')
33-
except Exception:
34-
pass
35-
return package_directory
52+
def resolve(cand):
53+
# type: (Package) -> types.ModuleType
54+
return (
55+
cand if isinstance(cand, types.ModuleType)
56+
else importlib.import_module(cand)
57+
)
58+
59+
60+
def get_package(package):
61+
# type: (Package) -> types.ModuleType
62+
"""Take a package name or module object and return the module.
63+
64+
Raise an exception if the resolved module is not a package.
65+
"""
66+
resolved = resolve(package)
67+
if resolved.__spec__.submodule_search_locations is None:
68+
raise TypeError('{!r} is not a package'.format(package))
69+
return resolved
70+
71+
72+
def from_package(package):
73+
"""
74+
Return a Traversable object for the given package.
75+
76+
"""
77+
spec = package.__spec__
78+
reader = spec.loader.get_resource_reader(spec.name)
79+
return reader.files()
3680

3781

3882
@contextlib.contextmanager

‎Lib/importlib/abc.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ def resource_path(self, resource):
468468
raise FileNotFoundError(resource)
469469

470470
def is_resource(self, path):
471-
return self.files().joinpath(path).isfile()
471+
return self.files().joinpath(path).is_file()
472472

473473
def contents(self):
474474
return (item.name for item in self.files().iterdir())

‎Lib/importlib/readers.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import zipfile
2+
import pathlib
3+
from . import abc
4+
5+
6+
class FileReader(abc.TraversableResources):
7+
def __init__(self, loader):
8+
self.path = pathlib.Path(loader.path).parent
9+
10+
def files(self):
11+
return self.path
12+
13+
14+
class ZipReader(FileReader):
15+
def __init__(self, loader, module):
16+
_, _, name = module.rpartition('.')
17+
prefix = loader.prefix.replace('\\', '/') + name + '/'
18+
self.path = zipfile.Path(loader.archive, prefix)
19+
20+
def open_resource(self, resource):
21+
try:
22+
return super().open_resource(resource)
23+
except KeyError as exc:
24+
raise FileNotFoundError(exc.args[0])
25+
26+
def is_resource(self, path):
27+
# workaround for `zipfile.Path.is_file` returning true
28+
# for non-existent paths.
29+
target = self.files().joinpath(path)
30+
return target.is_file() and target.exists()

‎Lib/importlib/resources.py‎

Lines changed: 14 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import os
22

3-
from . import abc as resources_abc
43
from . import _common
5-
from ._common import as_file
4+
from ._common import as_file, files
65
from contextlib import contextmanager, suppress
7-
from importlib import import_module
86
from importlib.abc import ResourceLoader
97
from io import BytesIO, TextIOWrapper
108
from pathlib import Path
119
from types import ModuleType
12-
from typing import ContextManager, Iterable, Optional, Union
10+
from typing import ContextManager, Iterable, Union
1311
from typing import cast
1412
from typing.io import BinaryIO, TextIO
1513

@@ -33,60 +31,11 @@
3331
Resource = Union[str, os.PathLike]
3432

3533

36-
def _resolve(name) -> ModuleType:
37-
"""If name is a string, resolve to a module."""
38-
if hasattr(name, '__spec__'):
39-
return name
40-
return import_module(name)
41-
42-
43-
def _get_package(package) -> ModuleType:
44-
"""Take a package name or module object and return the module.
45-
46-
If a name, the module is imported. If the resolved module
47-
object is not a package, raise an exception.
48-
"""
49-
module = _resolve(package)
50-
if module.__spec__.submodule_search_locations is None:
51-
raise TypeError('{!r} is not a package'.format(package))
52-
return module
53-
54-
55-
def _normalize_path(path) -> str:
56-
"""Normalize a path by ensuring it is a string.
57-
58-
If the resulting string contains path separators, an exception is raised.
59-
"""
60-
parent, file_name = os.path.split(path)
61-
if parent:
62-
raise ValueError('{!r} must be only a file name'.format(path))
63-
return file_name
64-
65-
66-
def _get_resource_reader(
67-
package: ModuleType) -> Optional[resources_abc.ResourceReader]:
68-
# Return the package's loader if it's a ResourceReader. We can't use
69-
# a issubclass() check here because apparently abc.'s __subclasscheck__()
70-
# hook wants to create a weak reference to the object, but
71-
# zipimport.zipimporter does not support weak references, resulting in a
72-
# TypeError. That seems terrible.
73-
spec = package.__spec__
74-
if hasattr(spec.loader, 'get_resource_reader'):
75-
return cast(resources_abc.ResourceReader,
76-
spec.loader.get_resource_reader(spec.name))
77-
return None
78-
79-
80-
def _check_location(package):
81-
if package.__spec__.origin is None or not package.__spec__.has_location:
82-
raise FileNotFoundError(f'Package has no location {package!r}')
83-
84-
8534
def open_binary(package: Package, resource: Resource) -> BinaryIO:
8635
"""Return a file-like object opened for binary reading of the resource."""
87-
resource = _normalize_path(resource)
88-
package = _get_package(package)
89-
reader = _get_resource_reader(package)
36+
resource = _common.normalize_path(resource)
37+
package = _common.get_package(package)
38+
reader = _common.get_resource_reader(package)
9039
if reader is not None:
9140
return reader.open_resource(resource)
9241
absolute_package_path = os.path.abspath(
@@ -140,13 +89,6 @@ def read_text(package: Package,
14089
return fp.read()
14190

14291

143-
def files(package: Package) -> resources_abc.Traversable:
144-
"""
145-
Get a Traversable resource from a package
146-
"""
147-
return _common.from_package(_get_package(package))
148-
149-
15092
def path(
15193
package: Package, resource: Resource,
15294
) -> 'ContextManager[Path]':
@@ -158,17 +100,18 @@ def path(
158100
raised if the file was deleted prior to the context manager
159101
exiting).
160102
"""
161-
reader = _get_resource_reader(_get_package(package))
103+
reader = _common.get_resource_reader(_common.get_package(package))
162104
return (
163105
_path_from_reader(reader, resource)
164106
if reader else
165-
_common.as_file(files(package).joinpath(_normalize_path(resource)))
107+
_common.as_file(
108+
_common.files(package).joinpath(_common.normalize_path(resource)))
166109
)
167110

168111

169112
@contextmanager
170113
def _path_from_reader(reader, resource):
171-
norm_resource = _normalize_path(resource)
114+
norm_resource = _common.normalize_path(resource)
172115
with suppress(FileNotFoundError):
173116
yield Path(reader.resource_path(norm_resource))
174117
return
@@ -182,9 +125,9 @@ def is_resource(package: Package, name: str) -> bool:
182125
183126
Directories are *not* resources.
184127
"""
185-
package = _get_package(package)
186-
_normalize_path(name)
187-
reader = _get_resource_reader(package)
128+
package = _common.get_package(package)
129+
_common.normalize_path(name)
130+
reader = _common.get_resource_reader(package)
188131
if reader is not None:
189132
return reader.is_resource(name)
190133
package_contents = set(contents(package))
@@ -200,8 +143,8 @@ def contents(package: Package) -> Iterable[str]:
200143
not considered resources. Use `is_resource()` on each entry returned here
201144
to check if it is a resource or not.
202145
"""
203-
package = _get_package(package)
204-
reader = _get_resource_reader(package)
146+
package = _common.get_package(package)
147+
reader = _common.get_resource_reader(package)
205148
if reader is not None:
206149
return reader.contents()
207150
# Is the package a namespace package? By definition, namespace packages

0 commit comments

Comments
 (0)