@@ -272,8 +272,16 @@ def get_resource_reader(self, fullname):
272
272
If 'fullname' is a package within the zip file, return the
273
273
'ResourceReader' object for the package. Otherwise return None.
274
274
"""
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 )
277
285
278
286
279
287
def __repr__ (self ):
@@ -648,3 +656,74 @@ def _get_module_code(self, fullname):
648
656
return code ,ispackage ,modpath
649
657
else :
650
658
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