2010-01-04 Eric Seidel <eric@webkit.org>
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / autoinstall.py
1 # Copyright (c) 2009, Daniel Krech All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
5 # met:
6
7 #  * Redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer.
9
10 #  * Redistributions in binary form must reproduce the above copyright
11 # notice, this list of conditions and the following disclaimer in the
12 # documentation and/or other materials provided with the distribution.
13
14 #  * Neither the name of the Daniel Krech nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 """\
31 package loader for auto installing Python packages.
32
33 A package loader in the spirit of Zero Install that can be used to
34 inject dependencies into the import process. 
35
36
37 To install::
38
39     easy_install -U autoinstall
40
41       or 
42
43     download, unpack, python setup.py install
44
45       or 
46
47     try the bootstrap loader. See below.
48
49
50 To use::
51
52     # You can bind any package name to a URL pointing to something
53     # that can be imported using the zipimporter.
54
55     autoinstall.bind("pymarc", "http://pypi.python.org/packages/2.5/p/pymarc/pymarc-2.1-py2.5.egg")
56
57     import pymarc
58
59     print pymarc.__version__, pymarc.__file__
60
61     
62 Changelog::
63
64 - added support for non top level packages.
65 - cache files now use filename part from URL.
66 - applied patch from Eric Seidel <eseidel@google.com> to add support
67 for loading modules where the module is not at the root of the .zip
68 file.
69
70
71 TODO::
72
73 - a description of the intended use case
74 - address other issues pointed out in:
75
76     http://mail.python.org/pipermail/python-dev/2008-March/077926.html
77
78 Scribbles::
79
80 pull vs. push
81 user vs. system
82 web vs. filesystem
83 auto vs. manual
84
85 manage development sandboxes
86
87 optional interfaces...
88
89     def get_data(pathname) -> string with file data.
90
91     Return the data associated with 'pathname'. Raise IOError if
92     the file wasn't found.");
93
94     def is_package,
95     "is_package(fullname) -> bool.
96
97     Return True if the module specified by fullname is a package.
98     Raise ZipImportError is the module couldn't be found.");
99
100     def get_code,
101     "get_code(fullname) -> code object.
102
103     Return the code object for the specified module. Raise ZipImportError
104     is the module couldn't be found.");
105
106     def get_source,
107     "get_source(fullname) -> source string.
108
109     Return the source code for the specified module. Raise ZipImportError
110     is the module couldn't be found, return None if the archive does
111     contain the module, but has no source for it.");
112
113
114 Autoinstall can also be bootstraped with the nascent package loader
115 bootstrap module. For example::
116
117     #  or via the bootstrap
118     # loader.
119
120     try:
121         _version = "0.2"
122         import autoinstall
123         if autoinstall.__version__ != _version:
124             raise ImportError("A different version than expected found.")
125     except ImportError, e:
126         # http://svn.python.org/projects/sandbox/trunk/bootstrap/bootstrap.py
127         import bootstrap 
128         pypi = "http://pypi.python.org"
129         dir = "packages/source/a/autoinstall"
130         url = "%s/%s/autoinstall-%s.tar.gz" % (pypi, dir, _version)
131         bootstrap.main((url,))
132         import autoinstall
133
134 References::
135
136   http://0install.net/
137   http://www.python.org/dev/peps/pep-0302/
138   http://svn.python.org/projects/sandbox/trunk/import_in_py
139   http://0install.net/injector-find.html
140   http://roscidus.com/desktop/node/903
141
142 """
143
144 __version__ = "0.2"
145 __docformat__ = "restructuredtext en"
146
147 import os
148 import new
149 import sys
150 import urllib
151 import logging
152 import tempfile
153 import zipimport
154
155 _logger = logging.getLogger(__name__)
156
157
158 _importer = None
159
160 def _getImporter():
161     global _importer
162     if _importer is None:
163         _importer = Importer()
164         sys.meta_path.append(_importer)
165     return _importer
166
167 def bind(package_name, url, zip_subpath=None):
168     """bind a top level package name to a URL.
169
170     The package name should be a package name and the url should be a
171     url to something that can be imported using the zipimporter.
172
173     Optional zip_subpath parameter allows searching for modules
174     below the root level of the zip file.
175     """
176     _getImporter().bind(package_name, url, zip_subpath)
177
178
179 class Cache(object):
180
181     def __init__(self, directory=None):
182         self.directory = directory or "./autoinstall.cache.d/"
183         try:
184             if not os.path.exists(self.directory):
185                 _logger.debug("Creating cache directory '%s'." % self.directory)
186                 os.mkdir(self.directory)
187         except Exception, err:
188             _logger.exception(err)
189             self.cache_directry = tempfile.mkdtemp()
190         _logger.info("Using cache directory '%s'." % self.directory)
191     
192     def get(self, url):
193         _logger.info("Getting '%s' from cache." % url)
194         filename = url.rsplit("/")[-1]
195
196         # so that source url is significant in determining cache hits
197         d = os.path.join(self.directory, "%s" % hash(url))
198         if not os.path.exists(d):
199             os.mkdir(d)
200
201         filename = os.path.join(d, filename) 
202
203         if os.path.exists(filename):
204             _logger.debug("... already cached in file '%s'." % filename)
205         else:
206             _logger.debug("... not in cache. Caching in '%s'." % filename)
207             stream = file(filename, "wb")
208             self.download(url, stream)
209             stream.close()
210         return filename
211
212     def download(self, url, stream):
213         _logger.info("Downloading: %s" % url)
214         try:
215             netstream = urllib.urlopen(url)
216             code = 200
217             if hasattr(netstream, "getcode"):
218                 code = netstream.getcode()
219             if not 200 <= code < 300:
220                 raise ValueError("HTTP Error code %s" % code)
221         except Exception, err:
222             _logger.exception(err)
223
224         BUFSIZE = 2**13  # 8KB
225         size = 0
226         while True:
227             data = netstream.read(BUFSIZE)
228             if not data:
229                 break
230             stream.write(data)
231             size += len(data)
232         netstream.close()
233         _logger.info("Downloaded %d bytes." % size)
234
235
236 class Importer(object):
237
238     def __init__(self):
239         self.packages = {}
240         self.__cache = None
241
242     def __get_store(self):
243         return self.__store
244     store = property(__get_store)
245
246     def _get_cache(self):
247         if self.__cache is None:
248             self.__cache = Cache()
249         return self.__cache
250     def _set_cache(self, cache):
251         self.__cache = cache
252     cache = property(_get_cache, _set_cache)
253
254     def find_module(self, fullname, path=None):
255         """-> self or None.
256
257         Search for a module specified by 'fullname'. 'fullname' must be
258         the fully qualified (dotted) module name. It returns the
259         zipimporter instance itself if the module was found, or None if
260         it wasn't. The optional 'path' argument is ignored -- it's
261         there for compatibility with the importer protocol.");
262         """
263         _logger.debug("find_module(%s, path=%s)" % (fullname, path))
264
265         if fullname in self.packages:
266             (url, zip_subpath) = self.packages[fullname]
267             filename = self.cache.get(url)
268             zip_path = "%s/%s" % (filename, zip_subpath) if zip_subpath else filename
269             _logger.debug("fullname: %s url: %s path: %s zip_path: %s" % (fullname, url, path, zip_path))
270             try:
271                 loader = zipimport.zipimporter(zip_path)
272                 _logger.debug("returning: %s" % loader)
273             except Exception, e:
274                 _logger.exception(e)
275                 return None
276             return loader
277         return None
278
279     def bind(self, package_name, url, zip_subpath):
280         _logger.info("binding: %s -> %s subpath: %s" % (package_name, url, zip_subpath))
281         self.packages[package_name] = (url, zip_subpath)
282
283
284 if __name__=="__main__":
285     import logging
286     #logging.basicConfig()
287     logger = logging.getLogger()
288
289     console = logging.StreamHandler()
290     console.setLevel(logging.DEBUG)
291     # set a format which is simpler for console use
292     formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
293     # tell the handler to use this format
294     console.setFormatter(formatter)
295     # add the handler to the root logger
296     logger.addHandler(console)
297     logger.setLevel(logging.INFO)
298
299     bind("pymarc", "http://pypi.python.org/packages/2.5/p/pymarc/pymarc-2.1-py2.5.egg")
300
301     import pymarc
302
303     print pymarc.__version__, pymarc.__file__
304
305     assert pymarc.__version__=="2.1"
306
307     d = _getImporter().cache.directory
308     assert d in pymarc.__file__, "'%s' not found in pymarc.__file__ (%s)" % (d, pymarc.__file__)
309
310     # Can now also bind to non top level packages. The packages
311     # leading up to the package being bound will need to be defined
312     # however.
313     #
314     # bind("rdf.plugins.stores.memory", 
315     #      "http://pypi.python.org/packages/2.5/r/rdf.plugins.stores.memeory/rdf.plugins.stores.memory-0.9a-py2.5.egg")
316     #
317     # from rdf.plugins.stores.memory import Memory
318
319