bpo-39791 native hooks for importlib.resources.files (GH-20576) · python/cpython@843c277 · GitHub
Skip to content

Commit 843c277

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>
1 parent 972ab03 commit 843c277

9 files changed

Lines changed: 2118 additions & 2359 deletions

File tree

Lib/importlib/_bootstrap_external.py

Lines changed: 2 additions & 24 deletions

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

0 commit comments

Comments
 (0)