Skip to content
Snippets Groups Projects
pipdeps.py 5.68 KiB
"""
pipdeps
"""
import argparse
import json
import distutils.version
import os
import urllib2
import re
import subprocess
import sys
import packaging.specifiers
import packaging.version

def arg_parse():
    """
    argument parser
    """
    parser = argparse.ArgumentParser(
        description="Pipdeps shows/upgrades outdated packages with respect to existing \
                     dependencies."
    )
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('-l', '--list',
                       action='store_true',
                       help="show upgradable packages and versions")
    group.add_argument('-u', '--upgrade',
                       action='store_true',
                       help="upgrade upgradable packages")
    return parser.parse_args()

def check_strict_version(version):
    """
    Return true if version is strict, otherwise return false
    """
    try:
        distutils.version.StrictVersion(version)
    except ValueError:
        return False
    return True

def upgrade_pip():
    """
    pip install --upgrade pip
    """
    subprocess.check_call(["pip", "install", "--upgrade", "pip"], stderr=subprocess.STDOUT)

def upgrade_package(package, versions):
    """
    pip install --upgrade "<package><versions>"
    """
    subprocess.check_call(
        ["pip", "install", "--upgrade", "%s==%s" % (package, "".join(versions))],
        stderr=subprocess.STDOUT
        )

def get_outdated_packages():
    """
    pip list --outdated
    """
    outdated_packages = subprocess.check_output(
        ["pip", "list", "--outdated"],
        stderr=subprocess.STDOUT
    )
    return [line.split()[0] for line in outdated_packages.strip().split("\n")[2:]]

def get_dependencies_tree():
    """
    pipdeptree --json-tree
    """
    pipdeptree = subprocess.check_output(
        ["pipdeptree", "--json-tree"],
        stderr=subprocess.STDOUT
    )
    return json.loads(pipdeptree.strip())

def json_search(jsonpipdeptree, package, key):
    """
    find package dependencies in json tree
    """
    if isinstance(jsonpipdeptree, dict):
        keys = jsonpipdeptree.keys()
        if 'package_name' in keys and key in keys:
            if re.search(r'^%s$' % package, jsonpipdeptree['package_name'], re.IGNORECASE):
                yield jsonpipdeptree[key]
        for child_val in json_search(jsonpipdeptree['dependencies'], package, key):
            yield child_val
    elif isinstance(jsonpipdeptree, list):
        for item in jsonpipdeptree:
            for item_val in json_search(item, package, key):
                yield item_val

def find_available_versions(package_name):
    """
    Return descending list of available strict version
    """
    url = "https://pypi.python.org/pypi/%s/json" % (package_name,)
    data = json.load(urllib2.urlopen(urllib2.Request(url)))
    versions = data["releases"].keys()
    data = [version for version in versions if check_strict_version(version)]
    return sorted(data, key=distutils.version.StrictVersion, reverse=True)

def find_upgradable_version(available_version, installed_version, required_version):
    """
    Return upgradable version, otherwise return none.
    """
    while True:
        try:
            version = available_version.pop(0)
        except IndexError:
            break
        aver = packaging.version.Version(version)
        iver = packaging.version.Version(installed_version)
        rver = packaging.specifiers.SpecifierSet(",".join(required_version))
        if rver.contains(aver):
            if aver == iver:
                break
            elif aver > iver:
                return version
    return None

def find_upgradable_packages(package_list, jsonpipdeptree, exclude=None):
    """
    Collect data about upgradable packages and return as list of dictionaries
    """
    result = []
    for package in package_list:
        if isinstance(exclude, list):
            if package in exclude:
                continue
        dependencies = [_ for _ in json_search(jsonpipdeptree, package, 'required_version')]
        dependencies = list(set(dependencies))
        if not [dep for dep in dependencies if re.search(r'(^==.*|^\d.*)', dep) is not None]:
            installed_version = "".join(list(set(
                [_ for _ in json_search(jsonpipdeptree, package, 'installed_version')]
            )))
            required_version = [dep for dep in dependencies if 'Any' not in dep]
            available_version = find_available_versions(package)
            upgradable_version = find_upgradable_version(
                available_version, installed_version, required_version)
            rev = {'package': package,
                   'installed_version': installed_version,
                   'required_version': required_version,
                   'available_version': available_version}
            if upgradable_version is not None:
                rev['upgradable_version'] = upgradable_version
                result.append(rev)
    return result

def main():
    """
    main function
    """
    os.environ["PYTHONWARNINGS"] = "ignore:DEPRECATION"
    arguments = arg_parse()
    upgrade_pip()
    outdated_packages = get_outdated_packages()

    while True:
        upgradable_packages = find_upgradable_packages(outdated_packages, get_dependencies_tree())
        if arguments.list:
            if upgradable_packages:
                print upgradable_packages
                sys.exit(1)
            else:
                print "There is nothing to upgrade."
                sys.exit(0)

        try:
            package = upgradable_packages.pop(-1)
        except IndexError:
            break
        upgrade_package(package['package'], package['upgradable_version'])

    print "Done."

if __name__ == "__main__":
    main()