Skip to content

Commit 2902f2b

Browse files
committed
GR-26502: add missing _ZipImportResourceReader to the zipimport builtin
1 parent 4603379 commit 2902f2b

File tree

1 file changed

+90
-0
lines changed

1 file changed

+90
-0
lines changed

graalpython/lib-graalpython/zipimport.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,93 @@
4343

4444
zipimporter.create_module = lambda self, spec: None
4545
zipimporter.exec_module = lambda self, module: exec(self.get_code(module.__name__), module.__dict__)
46+
47+
48+
class _ZipImportResourceReader:
49+
"""Private class used to support ZipImport.get_resource_reader().
50+
51+
This class is allowed to reference all the innards and private parts of
52+
the zipimporter.
53+
"""
54+
_registered = False
55+
56+
def __init__(self, zipimporter, fullname):
57+
self.zipimporter = zipimporter
58+
self.fullname = fullname
59+
60+
def open_resource(self, resource):
61+
fullname_as_path = self.fullname.replace('.', '/')
62+
path = f'{fullname_as_path}/{resource}'
63+
from io import BytesIO
64+
try:
65+
return BytesIO(self.zipimporter.get_data(path))
66+
except OSError:
67+
raise FileNotFoundError(path)
68+
69+
def resource_path(self, resource):
70+
# All resources are in the zip file, so there is no path to the file.
71+
# Raising FileNotFoundError tells the higher level API to extract the
72+
# binary data and create a temporary file.
73+
raise FileNotFoundError
74+
75+
def is_resource(self, name):
76+
# Maybe we could do better, but if we can get the data, it's a
77+
# resource. Otherwise it isn't.
78+
fullname_as_path = self.fullname.replace('.', '/')
79+
path = f'{fullname_as_path}/{name}'
80+
try:
81+
self.zipimporter.get_data(path)
82+
except OSError:
83+
return False
84+
return True
85+
86+
def contents(self):
87+
# This is a bit convoluted, because fullname will be a module path,
88+
# but _files is a list of file names relative to the top of the
89+
# archive's namespace. We want to compare file paths to find all the
90+
# names of things inside the module represented by fullname. So we
91+
# turn the module path of fullname into a file path relative to the
92+
# top of the archive, and then we iterate through _files looking for
93+
# names inside that "directory".
94+
from pathlib import Path
95+
fullname_path = Path(self.zipimporter.get_filename(self.fullname))
96+
relative_path = fullname_path.relative_to(self.zipimporter.archive)
97+
# Don't forget that fullname names a package, so its path will include
98+
# __init__.py, which we want to ignore.
99+
assert relative_path.name == '__init__.py'
100+
package_path = relative_path.parent
101+
subdirs_seen = set()
102+
for filename in self.zipimporter._files:
103+
try:
104+
relative = Path(filename).relative_to(package_path)
105+
except ValueError:
106+
continue
107+
# If the path of the file (which is relative to the top of the zip
108+
# namespace), relative to the package given when the resource
109+
# reader was created, has a parent, then it's a name in a
110+
# subdirectory and thus we skip it.
111+
parent_name = relative.parent.name
112+
if len(parent_name) == 0:
113+
yield relative.name
114+
elif parent_name not in subdirs_seen:
115+
subdirs_seen.add(parent_name)
116+
yield parent_name
117+
118+
def _get_resource_reader(self, fullname):
119+
"""Return the ResourceReader for a package in a zip file.
120+
121+
If 'fullname' is a package within the zip file, return the
122+
'ResourceReader' object for the package. Otherwise return None.
123+
"""
124+
try:
125+
if not self.is_package(fullname):
126+
return None
127+
except ZipImportError:
128+
return None
129+
if not _ZipImportResourceReader._registered:
130+
from importlib.abc import ResourceReader
131+
ResourceReader.register(_ZipImportResourceReader)
132+
_ZipImportResourceReader._registered = True
133+
return _ZipImportResourceReader(self, fullname)
134+
135+
zipimporter.get_resource_reader = _get_resource_reader

0 commit comments

Comments
 (0)