43
43
44
44
zipimporter .create_module = lambda self , spec : None
45
45
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