Skip to content

Commit c734452

Browse files
bpo-25711: Move _ZipImportResourceReader from importlib to zipimport.
1 parent 79d1c2e commit c734452

File tree

3 files changed

+1067
-952
lines changed

3 files changed

+1067
-952
lines changed

Lib/importlib/resources.py

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -257,87 +257,3 @@ def contents(package: Package) -> Iterable[str]:
257257
else:
258258
package_directory = Path(package.__spec__.origin).parent
259259
return os.listdir(package_directory)
260-
261-
262-
# Private implementation of ResourceReader and get_resource_reader() called
263-
# from zipimport.c. Don't use these directly! We're implementing these in
264-
# Python because 1) it's easier, 2) zipimport may get rewritten in Python
265-
# itself at some point, so doing this all in C would difficult and a waste of
266-
# effort.
267-
268-
class _ZipImportResourceReader(resources_abc.ResourceReader):
269-
"""Private class used to support ZipImport.get_resource_reader().
270-
271-
This class is allowed to reference all the innards and private parts of
272-
the zipimporter.
273-
"""
274-
275-
def __init__(self, zipimporter, fullname):
276-
self.zipimporter = zipimporter
277-
self.fullname = fullname
278-
279-
def open_resource(self, resource):
280-
fullname_as_path = self.fullname.replace('.', '/')
281-
path = f'{fullname_as_path}/{resource}'
282-
try:
283-
return BytesIO(self.zipimporter.get_data(path))
284-
except OSError:
285-
raise FileNotFoundError(path)
286-
287-
def resource_path(self, resource):
288-
# All resources are in the zip file, so there is no path to the file.
289-
# Raising FileNotFoundError tells the higher level API to extract the
290-
# binary data and create a temporary file.
291-
raise FileNotFoundError
292-
293-
def is_resource(self, name):
294-
# Maybe we could do better, but if we can get the data, it's a
295-
# resource. Otherwise it isn't.
296-
fullname_as_path = self.fullname.replace('.', '/')
297-
path = f'{fullname_as_path}/{name}'
298-
try:
299-
self.zipimporter.get_data(path)
300-
except OSError:
301-
return False
302-
return True
303-
304-
def contents(self):
305-
# This is a bit convoluted, because fullname will be a module path,
306-
# but _files is a list of file names relative to the top of the
307-
# archive's namespace. We want to compare file paths to find all the
308-
# names of things inside the module represented by fullname. So we
309-
# turn the module path of fullname into a file path relative to the
310-
# top of the archive, and then we iterate through _files looking for
311-
# names inside that "directory".
312-
fullname_path = Path(self.zipimporter.get_filename(self.fullname))
313-
relative_path = fullname_path.relative_to(self.zipimporter.archive)
314-
# Don't forget that fullname names a package, so its path will include
315-
# __init__.py, which we want to ignore.
316-
assert relative_path.name == '__init__.py'
317-
package_path = relative_path.parent
318-
subdirs_seen = set()
319-
for filename in self.zipimporter._files:
320-
try:
321-
relative = Path(filename).relative_to(package_path)
322-
except ValueError:
323-
continue
324-
# If the path of the file (which is relative to the top of the zip
325-
# namespace), relative to the package given when the resource
326-
# reader was created, has a parent, then it's a name in a
327-
# subdirectory and thus we skip it.
328-
parent_name = relative.parent.name
329-
if len(parent_name) == 0:
330-
yield relative.name
331-
elif parent_name not in subdirs_seen:
332-
subdirs_seen.add(parent_name)
333-
yield parent_name
334-
335-
336-
# Called from zipimport.c
337-
def _zipimport_get_resource_reader(zipimporter, fullname):
338-
try:
339-
if not zipimporter.is_package(fullname):
340-
return None
341-
except ZipImportError:
342-
return None
343-
return _ZipImportResourceReader(zipimporter, fullname)

Lib/zipimport.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,16 @@ def get_resource_reader(self, fullname):
272272
If 'fullname' is a package within the zip file, return the
273273
'ResourceReader' object for the package. Otherwise return None.
274274
"""
275-
from importlib import resources
276-
return resources._zipimport_get_resource_reader(self, fullname)
275+
try:
276+
if not self.is_package(fullname):
277+
return None
278+
except ZipImportError:
279+
return None
280+
if not _ZipImportResourceReader._registered:
281+
from importlib.abc import ResourceReader
282+
ResourceReader.register(_ZipImportResourceReader)
283+
_ZipImportResourceReader._registered = True
284+
return _ZipImportResourceReader(self, fullname)
277285

278286

279287
def __repr__(self):
@@ -648,3 +656,74 @@ def _get_module_code(self, fullname):
648656
return code, ispackage, modpath
649657
else:
650658
raise ZipImportError(f"can't find module {fullname!r}", name=fullname)
659+
660+
661+
class _ZipImportResourceReader:
662+
"""Private class used to support ZipImport.get_resource_reader().
663+
664+
This class is allowed to reference all the innards and private parts of
665+
the zipimporter.
666+
"""
667+
_registered = False
668+
669+
def __init__(self, zipimporter, fullname):
670+
self.zipimporter = zipimporter
671+
self.fullname = fullname
672+
673+
def open_resource(self, resource):
674+
fullname_as_path = self.fullname.replace('.', '/')
675+
path = f'{fullname_as_path}/{resource}'
676+
from io import BytesIO
677+
try:
678+
return BytesIO(self.zipimporter.get_data(path))
679+
except OSError:
680+
raise FileNotFoundError(path)
681+
682+
def resource_path(self, resource):
683+
# All resources are in the zip file, so there is no path to the file.
684+
# Raising FileNotFoundError tells the higher level API to extract the
685+
# binary data and create a temporary file.
686+
raise FileNotFoundError
687+
688+
def is_resource(self, name):
689+
# Maybe we could do better, but if we can get the data, it's a
690+
# resource. Otherwise it isn't.
691+
fullname_as_path = self.fullname.replace('.', '/')
692+
path = f'{fullname_as_path}/{name}'
693+
try:
694+
self.zipimporter.get_data(path)
695+
except OSError:
696+
return False
697+
return True
698+
699+
def contents(self):
700+
# This is a bit convoluted, because fullname will be a module path,
701+
# but _files is a list of file names relative to the top of the
702+
# archive's namespace. We want to compare file paths to find all the
703+
# names of things inside the module represented by fullname. So we
704+
# turn the module path of fullname into a file path relative to the
705+
# top of the archive, and then we iterate through _files looking for
706+
# names inside that "directory".
707+
from pathlib import Path
708+
fullname_path = Path(self.zipimporter.get_filename(self.fullname))
709+
relative_path = fullname_path.relative_to(self.zipimporter.archive)
710+
# Don't forget that fullname names a package, so its path will include
711+
# __init__.py, which we want to ignore.
712+
assert relative_path.name == '__init__.py'
713+
package_path = relative_path.parent
714+
subdirs_seen = set()
715+
for filename in self.zipimporter._files:
716+
try:
717+
relative = Path(filename).relative_to(package_path)
718+
except ValueError:
719+
continue
720+
# If the path of the file (which is relative to the top of the zip
721+
# namespace), relative to the package given when the resource
722+
# reader was created, has a parent, then it's a name in a
723+
# subdirectory and thus we skip it.
724+
parent_name = relative.parent.name
725+
if len(parent_name) == 0:
726+
yield relative.name
727+
elif parent_name not in subdirs_seen:
728+
subdirs_seen.add(parent_name)
729+
yield parent_name

0 commit comments

Comments
 (0)