Home | History | Annotate | Download | only in webkitpy
      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