|
1 #!/usr/bin/env python |
|
2 # |
|
3 # Copyright 2007 Google Inc. |
|
4 # |
|
5 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 # you may not use this file except in compliance with the License. |
|
7 # You may obtain a copy of the License at |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
11 # Unless required by applicable law or agreed to in writing, software |
|
12 # distributed under the License is distributed on an "AS IS" BASIS, |
|
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
14 # See the License for the specific language governing permissions and |
|
15 # limitations under the License. |
|
16 # |
|
17 |
|
18 """Pure Python zipfile importer. |
|
19 |
|
20 This approximates the standard zipimport module, which isn't supported |
|
21 by Google App Engine. See PEP 302 for more information about the API |
|
22 for import hooks. |
|
23 |
|
24 Usage: |
|
25 import py_zipimport |
|
26 |
|
27 As a side effect of importing, the module overrides sys.path_hooks, |
|
28 and also creates an alias 'zipimport' for itself. When your app is |
|
29 running in Google App Engine production, you don't even need to import |
|
30 it, since this is already done for you. In the Google App Engine SDK |
|
31 this module is not used; instead, the standard zipimport module is |
|
32 used. |
|
33 """ |
|
34 |
|
35 |
|
36 __all__ = ['ZipImportError', 'zipimporter'] |
|
37 |
|
38 |
|
39 import os |
|
40 import sys |
|
41 import types |
|
42 import UserDict |
|
43 import zipfile |
|
44 |
|
45 |
|
46 _SEARCH_ORDER = [ |
|
47 |
|
48 ('.py', False), |
|
49 ('/__init__.py', True), |
|
50 ] |
|
51 |
|
52 |
|
53 _zipfile_cache = {} |
|
54 |
|
55 |
|
56 class ZipImportError(ImportError): |
|
57 """Exception raised by zipimporter objects.""" |
|
58 |
|
59 |
|
60 class zipimporter: |
|
61 """A PEP-302-style importer that can import from a zipfile. |
|
62 |
|
63 Just insert or append this class (not an instance) to sys.path_hooks |
|
64 and you're in business. Instances satisfy both the 'importer' and |
|
65 'loader' APIs specified in PEP 302. |
|
66 """ |
|
67 |
|
68 def __init__(self, path_entry): |
|
69 """Constructor. |
|
70 |
|
71 Args: |
|
72 path_entry: The entry in sys.path. This should be the name of an |
|
73 existing zipfile possibly with a path separator and a prefix |
|
74 path within the archive appended, e.g. /x/django.zip or |
|
75 /x/django.zip/foo/bar. |
|
76 |
|
77 Raises: |
|
78 ZipImportError if the path_entry does not represent a valid |
|
79 zipfile with optional prefix. |
|
80 """ |
|
81 archive = path_entry |
|
82 prefix = '' |
|
83 while not os.path.lexists(archive): |
|
84 head, tail = os.path.split(archive) |
|
85 if head == archive: |
|
86 msg = 'Nothing found for %r' % path_entry |
|
87 raise ZipImportError(msg) |
|
88 archive = head |
|
89 prefix = os.path.join(tail, prefix) |
|
90 if not os.path.isfile(archive): |
|
91 msg = 'Non-file %r found for %r' % (archive, path_entry) |
|
92 raise ZipImportError(msg) |
|
93 self.archive = archive |
|
94 self.prefix = os.path.join(prefix, '') |
|
95 self.zipfile = _zipfile_cache.get(archive) |
|
96 if self.zipfile is None: |
|
97 try: |
|
98 self.zipfile = zipfile.ZipFile(self.archive) |
|
99 except (EnvironmentError, zipfile.BadZipfile), err: |
|
100 msg = 'Can\'t open zipfile %s: %s: %s' % (self.archive, |
|
101 err.__class__.__name__, err) |
|
102 import logging |
|
103 logging.warn(msg) |
|
104 raise ZipImportError(msg) |
|
105 else: |
|
106 _zipfile_cache[archive] = self.zipfile |
|
107 import logging |
|
108 logging.info('zipimporter(%r, %r)', archive, prefix) |
|
109 |
|
110 def __repr__(self): |
|
111 """Return a string representation matching zipimport.c.""" |
|
112 name = self.archive |
|
113 if self.prefix: |
|
114 name = os.path.join(name, self.prefix) |
|
115 return '<zipimporter object "%s">' % name |
|
116 |
|
117 def _get_info(self, fullmodname): |
|
118 """Internal helper for find_module() and load_module(). |
|
119 |
|
120 Args: |
|
121 fullmodname: The dot-separated full module name, e.g. 'django.core.mail'. |
|
122 |
|
123 Returns: |
|
124 A tuple (submodname, is_package, relpath) where: |
|
125 submodname: The final component of the module name, e.g. 'mail'. |
|
126 is_package: A bool indicating whether this is a package. |
|
127 relpath: The path to the module's source code within to the zipfile. |
|
128 |
|
129 Raises: |
|
130 ImportError if the module is not found in the archive. |
|
131 """ |
|
132 parts = fullmodname.split('.') |
|
133 submodname = parts[-1] |
|
134 for suffix, is_package in _SEARCH_ORDER: |
|
135 relpath = os.path.join(self.prefix, |
|
136 submodname + suffix.replace('/', os.sep)) |
|
137 try: |
|
138 self.zipfile.getinfo(relpath.replace(os.sep, '/')) |
|
139 except KeyError: |
|
140 pass |
|
141 else: |
|
142 return submodname, is_package, relpath |
|
143 msg = ('Can\'t find module %s in zipfile %s with prefix %r' % |
|
144 (fullmodname, self.archive, self.prefix)) |
|
145 raise ZipImportError(msg) |
|
146 |
|
147 def _get_source(self, fullmodname): |
|
148 """Internal helper for load_module(). |
|
149 |
|
150 Args: |
|
151 fullmodname: The dot-separated full module name, e.g. 'django.core.mail'. |
|
152 |
|
153 Returns: |
|
154 A tuple (submodname, is_package, fullpath, source) where: |
|
155 submodname: The final component of the module name, e.g. 'mail'. |
|
156 is_package: A bool indicating whether this is a package. |
|
157 fullpath: The path to the module's source code including the |
|
158 zipfile's filename. |
|
159 source: The module's source code. |
|
160 |
|
161 Raises: |
|
162 ImportError if the module is not found in the archive. |
|
163 """ |
|
164 submodname, is_package, relpath = self._get_info(fullmodname) |
|
165 fullpath = '%s%s%s' % (self.archive, os.sep, relpath) |
|
166 source = self.zipfile.read(relpath.replace(os.sep, '/')) |
|
167 source = source.replace('\r\n', '\n') |
|
168 source = source.replace('\r', '\n') |
|
169 return submodname, is_package, fullpath, source |
|
170 |
|
171 def find_module(self, fullmodname, path=None): |
|
172 """PEP-302-compliant find_module() method. |
|
173 |
|
174 Args: |
|
175 fullmodname: The dot-separated full module name, e.g. 'django.core.mail'. |
|
176 path: Optional and ignored; present for API compatibility only. |
|
177 |
|
178 Returns: |
|
179 None if the module isn't found in the archive; self if it is found. |
|
180 """ |
|
181 try: |
|
182 submodname, is_package, relpath = self._get_info(fullmodname) |
|
183 except ImportError: |
|
184 return None |
|
185 else: |
|
186 return self |
|
187 |
|
188 def load_module(self, fullmodname): |
|
189 """PEP-302-compliant load_module() method. |
|
190 |
|
191 Args: |
|
192 fullmodname: The dot-separated full module name, e.g. 'django.core.mail'. |
|
193 |
|
194 Returns: |
|
195 The module object constructed from the source code. |
|
196 |
|
197 Raises: |
|
198 SyntaxError if the module's source code is syntactically incorrect. |
|
199 ImportError if there was a problem accessing the source code. |
|
200 Whatever else can be raised by executing the module's source code. |
|
201 """ |
|
202 submodname, is_package, fullpath, source = self._get_source(fullmodname) |
|
203 code = compile(source, fullpath, 'exec') |
|
204 mod = sys.modules.get(fullmodname) |
|
205 try: |
|
206 if mod is None: |
|
207 mod = sys.modules[fullmodname] = types.ModuleType(fullmodname) |
|
208 mod.__loader__ = self |
|
209 mod.__file__ = fullpath |
|
210 mod.__name__ = fullmodname |
|
211 if is_package: |
|
212 mod.__path__ = [os.path.dirname(mod.__file__)] |
|
213 exec code in mod.__dict__ |
|
214 except: |
|
215 if fullmodname in sys.modules: |
|
216 del sys.modules[fullmodname] |
|
217 raise |
|
218 return mod |
|
219 |
|
220 |
|
221 def get_data(self, fullpath): |
|
222 """Return (binary) content of a data file in the zipfile.""" |
|
223 required_prefix = os.path.join(self.archive, '') |
|
224 if not fullpath.startswith(required_prefix): |
|
225 raise IOError('Path %r doesn\'t start with zipfile name %r' % |
|
226 (fullpath, required_prefix)) |
|
227 relpath = fullpath[len(required_prefix):] |
|
228 try: |
|
229 return self.zipfile.read(relpath) |
|
230 except KeyError: |
|
231 raise IOError('Path %r not found in zipfile %r' % |
|
232 (relpath, self.archive)) |
|
233 |
|
234 def is_package(self, fullmodname): |
|
235 """Return whether a module is a package.""" |
|
236 submodname, is_package, relpath = self._get_info(fullmodname) |
|
237 return is_package |
|
238 |
|
239 def get_code(self, fullmodname): |
|
240 """Return bytecode for a module.""" |
|
241 submodname, is_package, fullpath, source = self._get_source(fullmodname) |
|
242 return compile(source, fullpath, 'exec') |
|
243 |
|
244 def get_source(self, fullmodname): |
|
245 """Return source code for a module.""" |
|
246 submodname, is_package, fullpath, source = self._get_source(fullmodname) |
|
247 return source |
|
248 |
|
249 |
|
250 class ZipFileCache(UserDict.DictMixin): |
|
251 """Helper class to export archive data in _zip_directory_cache. |
|
252 |
|
253 Just take the info from _zipfile_cache and convert it as required. |
|
254 """ |
|
255 |
|
256 def __init__(self, archive): |
|
257 _zipfile_cache[archive] |
|
258 |
|
259 self._archive = archive |
|
260 |
|
261 def keys(self): |
|
262 return _zipfile_cache[self._archive].namelist() |
|
263 |
|
264 def __getitem__(self, filename): |
|
265 info = _zipfile_cache[self._archive].getinfo(filename) |
|
266 dt = info.date_time |
|
267 dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2) |
|
268 dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2] |
|
269 return (os.path.join(self._archive, info.filename), info.compress_type, |
|
270 info.compress_size, info.file_size, info.header_offset, dostime, |
|
271 dosdate, info.CRC) |
|
272 |
|
273 |
|
274 class ZipDirectoryCache(UserDict.DictMixin): |
|
275 """Helper class to export _zip_directory_cache.""" |
|
276 |
|
277 def keys(self): |
|
278 return _zipfile_cache.keys() |
|
279 |
|
280 def __getitem__(self, archive): |
|
281 return ZipFileCache(archive) |
|
282 |
|
283 |
|
284 _zip_directory_cache = ZipDirectoryCache() |
|
285 |
|
286 |
|
287 sys.modules['zipimport'] = sys.modules[__name__] |
|
288 sys.path_hooks[:] = [zipimporter] |