@@ -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