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 (at] 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 # To allow use of the "with" keyword for Python 2.5 users. 145 from __future__ import with_statement 146 147 __version__ = "0.2" 148 __docformat__ = "restructuredtext en" 149 150 import os 151 import new 152 import sys 153 import urllib 154 import logging 155 import tempfile 156 import zipimport 157 158 _logger = logging.getLogger(__name__) 159 160 161 _importer = None 162 163 def _getImporter(): 164 global _importer 165 if _importer is None: 166 _importer = Importer() 167 sys.meta_path.append(_importer) 168 return _importer 169 170 def bind(package_name, url, zip_subpath=None): 171 """bind a top level package name to a URL. 172 173 The package name should be a package name and the url should be a 174 url to something that can be imported using the zipimporter. 175 176 Optional zip_subpath parameter allows searching for modules 177 below the root level of the zip file. 178 """ 179 _getImporter().bind(package_name, url, zip_subpath) 180 181 182 class Cache(object): 183 184 def __init__(self, directory=None): 185 if directory is None: 186 # Default to putting the cache directory in the same directory 187 # as this file. 188 containing_directory = os.path.dirname(__file__) 189 directory = os.path.join(containing_directory, "autoinstall.cache.d"); 190 191 self.directory = directory 192 try: 193 if not os.path.exists(self.directory): 194 self._create_cache_directory() 195 except Exception, err: 196 _logger.exception(err) 197 self.cache_directry = tempfile.mkdtemp() 198 _logger.info("Using cache directory '%s'." % self.directory) 199 200 def _create_cache_directory(self): 201 _logger.debug("Creating cache directory '%s'." % self.directory) 202 os.mkdir(self.directory) 203 readme_path = os.path.join(self.directory, "README") 204 with open(readme_path, "w") as f: 205 f.write("This directory was auto-generated by '%s'.\n" 206 "It is safe to delete.\n" % __file__) 207 208 def get(self, url): 209 _logger.info("Getting '%s' from cache." % url) 210 filename = url.rsplit("/")[-1] 211 212 # so that source url is significant in determining cache hits 213 d = os.path.join(self.directory, "%s" % hash(url)) 214 if not os.path.exists(d): 215 os.mkdir(d) 216 217 filename = os.path.join(d, filename) 218 219 if os.path.exists(filename): 220 _logger.debug("... already cached in file '%s'." % filename) 221 else: 222 _logger.debug("... not in cache. Caching in '%s'." % filename) 223 stream = file(filename, "wb") 224 self.download(url, stream) 225 stream.close() 226 return filename 227 228 def download(self, url, stream): 229 _logger.info("Downloading: %s" % url) 230 try: 231 netstream = urllib.urlopen(url) 232 code = 200 233 if hasattr(netstream, "getcode"): 234 code = netstream.getcode() 235 if not 200 <= code < 300: 236 raise ValueError("HTTP Error code %s" % code) 237 except Exception, err: 238 _logger.exception(err) 239 240 BUFSIZE = 2**13 # 8KB 241 size = 0 242 while True: 243 data = netstream.read(BUFSIZE) 244 if not data: 245 break 246 stream.write(data) 247 size += len(data) 248 netstream.close() 249 _logger.info("Downloaded %d bytes." % size) 250 251 252 class Importer(object): 253 254 def __init__(self): 255 self.packages = {} 256 self.__cache = None 257 258 def __get_store(self): 259 return self.__store 260 store = property(__get_store) 261 262 def _get_cache(self): 263 if self.__cache is None: 264 self.__cache = Cache() 265 return self.__cache 266 def _set_cache(self, cache): 267 self.__cache = cache 268 cache = property(_get_cache, _set_cache) 269 270 def find_module(self, fullname, path=None): 271 """-> self or None. 272 273 Search for a module specified by 'fullname'. 'fullname' must be 274 the fully qualified (dotted) module name. It returns the 275 zipimporter instance itself if the module was found, or None if 276 it wasn't. The optional 'path' argument is ignored -- it's 277 there for compatibility with the importer protocol."); 278 """ 279 _logger.debug("find_module(%s, path=%s)" % (fullname, path)) 280 281 if fullname in self.packages: 282 (url, zip_subpath) = self.packages[fullname] 283 filename = self.cache.get(url) 284 zip_path = "%s/%s" % (filename, zip_subpath) if zip_subpath else filename 285 _logger.debug("fullname: %s url: %s path: %s zip_path: %s" % (fullname, url, path, zip_path)) 286 try: 287 loader = zipimport.zipimporter(zip_path) 288 _logger.debug("returning: %s" % loader) 289 except Exception, e: 290 _logger.exception(e) 291 return None 292 return loader 293 return None 294 295 def bind(self, package_name, url, zip_subpath): 296 _logger.info("binding: %s -> %s subpath: %s" % (package_name, url, zip_subpath)) 297 self.packages[package_name] = (url, zip_subpath) 298 299 300 if __name__=="__main__": 301 import logging 302 #logging.basicConfig() 303 logger = logging.getLogger() 304 305 console = logging.StreamHandler() 306 console.setLevel(logging.DEBUG) 307 # set a format which is simpler for console use 308 formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') 309 # tell the handler to use this format 310 console.setFormatter(formatter) 311 # add the handler to the root logger 312 logger.addHandler(console) 313 logger.setLevel(logging.INFO) 314 315 bind("pymarc", "http://pypi.python.org/packages/2.5/p/pymarc/pymarc-2.1-py2.5.egg") 316 317 import pymarc 318 319 print pymarc.__version__, pymarc.__file__ 320 321 assert pymarc.__version__=="2.1" 322 323 d = _getImporter().cache.directory 324 assert d in pymarc.__file__, "'%s' not found in pymarc.__file__ (%s)" % (d, pymarc.__file__) 325 326 # Can now also bind to non top level packages. The packages 327 # leading up to the package being bound will need to be defined 328 # however. 329 # 330 # bind("rdf.plugins.stores.memory", 331 # "http://pypi.python.org/packages/2.5/r/rdf.plugins.stores.memeory/rdf.plugins.stores.memory-0.9a-py2.5.egg") 332 # 333 # from rdf.plugins.stores.memory import Memory 334 335 336