view paint/info.py @ 83:7f442f585580

paint/package.py
author Jeff Hammel <jhammel@mozilla.com>
date Tue, 17 Sep 2013 16:04:05 -0700
parents 00c0f668f332
children
line wrap: on
line source

"""
interfaces to get information from a package
"""


import imp
import pkginfo
import os
import subprocess
import sys

from distutils.dist import Distribution
from subprocess import check_call as call
from StringIO import StringIO

# TODO:
# Reconcile the difference between the keys (and values) between the different
# implementations.  Pick a canon and stick with it.

# SetupOverridePackageInfo:
# {'entry_points': '\n', 'description': 'a dummy package', 'license': '', 'author': 'Jeff Hammel', 'install_requires': [], 'include_package_data': True, 'classifiers': [], 'url': 'http://example.com/', 'author_email': 'jhammel@mozilla.com', 'version': '0.1', 'zip_safe': False, 'packages': ['dummy'], 'long_description': 'dummy\n===========\n\na dummy package\n\n----\n\nJeff Hammel\n\nhttp://example.com/\n\n', 'name': 'dummy'}

# EggInfo:
# {'Name': 'dummy', 'License': 'UNKNOWN', 'Author': 'Jeff Hammel', 'Metadata-Version': '1.0', 'Home-page': 'http://example.com/', 'Summary': 'a dummy package', 'Platform': 'UNKNOWN', 'Version': '0.1', 'Author-email': 'jhammel@mozilla.com', 'Description': 'dummy'}

# see http://www.python.org/dev/peps/pep-0314/ :
# Metadata for Python Software Packages


# TODO: consider using pkginfo

def setup2metadata(**attrs):
    """
    convert setup arguments to standard python metadata:
    http://www.python.org/dev/peps/pep-0314/
    """
    distribution = Distribution(attrs)
    buffer = StringIO()
    distribution.metadata.write_pkg_file(buffer)
    pkginfo_dist = pkginfo.Distribution()
    pkinfo_dist.parse(buffer.getvalue())
    newattrs = dict((i, getattr(pkginfo_dist, i)) for i in pkginfo_dist)
    header_dict = dict((attr_name, header_name)
                       for header_name, attr_name, multiple in pkginfo_dist._getHeaderAttrs())
    info = dict((header_dict[key], value) for key, value in newattrs.items())

    # XXX pkginfo says 'Home-Page' though the spec says Home-page
    info.setdefault('Home-page', info['Home-Page'])

    return info


class PackageInfo(object):
    """abstract base class of package info"""
    def __init__(self, path):
        """
        - path : path to setup.py or its directory
        """
        if os.path.isdir(path):
            path = os.path.join(path, 'setup.py')
        assert os.path.exists(path), "'%s' not found" % path
        self.setup_py = os.path.abspath(path)

    def __call__(self):
        """returns dictionary of package info"""
        raise NotImplementedError("abstract base class")
    def dependencies(self):
        raise NotImplementedError("abstract base class")

class SetupOverridePackageInfo(PackageInfo):
    """
    gather setup.py information by overriding the function
    """
    # TODO: override distutils.core.setup as well
    # http://docs.python.org/2/distutils/index.html#distutils-index

    def __call__(self):
        setuptools = sys.modules.get('setuptools')
        sys.modules['setuptools'] = sys.modules[__name__]
        globals()['setup'] = self._setup
        try:
            module = imp.load_source('setup', self.setup_py)
        finally:
            sys.modules.pop('setuptools')
            if setuptools:
                sys.modules['setuptools'] = setuptools
            globals().pop('setup')
        return self.__dict__.pop('_info')

    def _setup(self, **kwargs):
        self._info = kwargs


class EggInfo(PackageInfo):
    """
    use `python setup.py egg_info` to gather package information
    """

    def __call__(self):

        info = self.read_pkg_info(self._pkg_info())
        # TODO: install_requires
        return info

    @classmethod
    def read_pkg_info(cls, path):
        """reads PKG-INFO and returns a dict"""

        # read the package information
        info_dict = {}
        for line in file(path).readlines():
            if not line or line[0].isspace():
                continue # XXX neglects description
            assert ':' in line
            key, value = [i.strip() for i in line.split(':', 1)]
            info_dict[key] = value

        # return the information
        return info_dict

    def dependencies(self):
        """return the dependencies of the package"""

        # TODO: should probably have a more detailed dict:
        # {'mozinfo': {'version': '>= 0.2',
        #              'url': 'http://something.com/'}}

        # get the egg_info directory
        egg_info = self._egg_info()

        # read the dependencies
        requires = os.path.join(egg_info, 'requires.txt')
        if os.path.exists(requires):
            dependencies = [i.strip() for i in file(requires).readlines() if i.strip()]
        else:
            dependencies = []
        dependencies = dict([(i, None) for i in dependencies])

        # read the dependency links
        dependency_links = os.path.join(egg_info, 'dependency_links.txt')
        if os.path.exists(dependency_links):
            links = [i.strip() for i in file(dependency_links).readlines() if i.strip()]
            for link in links:
                # XXX pretty ghetto
                assert '#egg=' in link
                url, dep = link.split('#egg=', 1)
                if dep in dependencies:
                    dependencies[dep] = link

        return dependencies


    def _egg_info(self):
        """build the egg_info directory"""

        # cached result
        if getattr(self, '_egg_info_path', None):
            return self._egg_info_path

        directory = os.path.dirname(self.setup_py)

        # setup the egg info
        try:
            call([sys.executable, 'setup.py', 'egg_info'], cwd=directory, stdout=subprocess.PIPE)
        except Exception:
            print "Failure to generate egg_info: %s" % self.setup_py
            raise

        # get the .egg-info directory
        egg_info = [i for i in os.listdir(directory)
                    if i.endswith('.egg-info')]
        assert len(egg_info) == 1, 'Expected one .egg-info directory in %s, got: %s' % (directory, egg_info)
        egg_info = os.path.join(directory, egg_info[0])
        assert os.path.isdir(egg_info), "%s is not a directory" % egg_info

        # cache it
        self._egg_info_path = egg_info
        return self._egg_info_path

    def _pkg_info(self):
        """returns path to PKG-INFO file"""

        if getattr(self, '_pkg_info_path', None):
            # return cached value
            return self._pkg_info_path

        try:
            egg_info = self._egg_info()
        except Exception, exception:
            # try to get the package info from a file
            path = os.path.dirname(self.setup_py)
            pkg_info = os.path.join(path, 'PKG-INFO')
            if os.path.exists(pkg_info):
                self._pkg_info_path = pkg_info
                return self._pkg_info_path
            raise Exception("Cannot find or generate PKG-INFO")

        pkg_info = os.path.join(egg_info, 'PKG-INFO')
        assert os.path.exists(pkg_info)
        self._pkg_info_path = pkg_info
        return self._pkg_info_path