diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2a251af5dd019da2949204b2a0aad1fc2076218..001c6eae977c63ce816e0a96ed7ac757c2f86fc6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,7 +15,7 @@ pylint: - export PYTHONIOENCODING=UTF-8 - export LC_CTYPE=en_US.UTF-8 - python setup.py egg_info - - pip install $(paste -d " " -s pipupgradedependencies.egg-info/requires.txt) + - pip install $(paste -d " " -s pipdeps.egg-info/requires.txt) script: - pylint $(find . -type f -name "*.py") @@ -25,7 +25,7 @@ build: artifacts: expire_in: 1 day paths: - - dist/pipupgradedependencies*tar.gz + - dist/pipdeps*tar.gz before_script: - export PYTHONIOENCODING=UTF-8 - export LC_CTYPE=en_US.UTF-8 diff --git a/LICENSE b/LICENSE index c41d8f025814bd818af551d63362b670b274b5f0..4b457196f03b30eb739a4d6afa6a41bad35f4074 100644 --- a/LICENSE +++ b/LICENSE @@ -631,7 +631,7 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - pipupgradedependencies + pipdeps Copyright (C) 2019 IT4Innovations National Supercomputing Center, VSB - Technical University of Ostrava, Czech Republic @@ -653,7 +653,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - pipupgradedependencies Copyright (C) 2019 + pipdeps Copyright (C) 2019 IT4Innovations National Supercomputing Center, VSB - Technical University of Ostrava, Czech Republic This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. diff --git a/README.md b/README.md index 56df80e8ae1ac584a2c55f2661849130a8c03073..f8a56c524a3a10e6f7d14a87e5742b92ebb6eadc 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@ -# pipupgradedependencies +# pipdeps -Pipupgradedependencies upgrades all outdated packages with respect to existing dependencies. +Pipdeps shows/upgrades outdated packages with respect to existing dependencies. Python 2.7 is required. ## Usage ```console -pipupgradedependencies +usage: pipdeps [-h] (-l | -u) + +Pipdeps shows/upgrades outdated packages with respect to existing +dependencies. + +optional arguments: + -h, --help show this help message and exit + -l, --list show upgradable packages and versions + -u, --upgrade upgrade upgradable packages + ``` diff --git a/pipupgradedependencies/__init__.py b/pipdeps/__init__.py similarity index 69% rename from pipupgradedependencies/__init__.py rename to pipdeps/__init__.py index 06e92937270afd1fa2e207d3e14a61293ad041c2..c2da04136d4d43e6cab79b88e7d2e8c882b7972e 100644 --- a/pipupgradedependencies/__init__.py +++ b/pipdeps/__init__.py @@ -1,4 +1,4 @@ """ -pipupgradedependencies init +pipdeps init """ __import__('pkg_resources').declare_namespace(__name__) diff --git a/pipdeps/pipdeps.py b/pipdeps/pipdeps.py new file mode 100644 index 0000000000000000000000000000000000000000..acc17cdc3803e082e09abcd1a7f2f53dc27302d1 --- /dev/null +++ b/pipdeps/pipdeps.py @@ -0,0 +1,178 @@ +""" +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() diff --git a/pipupgradedependencies/pipupgradedependencies.py b/pipupgradedependencies/pipupgradedependencies.py deleted file mode 100644 index 26c3eb62614982a1ba46aab59b49b1d5daf733d4..0000000000000000000000000000000000000000 --- a/pipupgradedependencies/pipupgradedependencies.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -pipupgradedependencies -""" -import argparse -import json -import os -import re -import subprocess -import sys - -def upgrade_pip(): - """ - pip install --upgrade pip - """ - try: - subprocess.check_call(["pip", "install", "--upgrade", "pip"], stderr=subprocess.STDOUT) - except subprocess.CalledProcessError: - sys.exit(1) - -def upgrade_package(package, versions): - """ - pip install --upgrade "<package><versions>" - """ - try: - subprocess.check_call( - ["pip", "install", "--upgrade", - "%s%s" % (package, "".join(versions))], - stderr=subprocess.STDOUT - ) - except subprocess.CalledProcessError: - sys.exit(1) - -def get_outdated_packages(): - """ - pip list --outdated - """ - try: - outdated_packages = subprocess.check_output( - ["pip", "list", "--outdated"], - stderr=subprocess.STDOUT - ) - except subprocess.CalledProcessError: - sys.exit(1) - return [line.split()[0] for line in outdated_packages.strip().split("\n")[2:]] - -def get_dependencies_tree(): - """ - pipdeptree --json-tree - """ - try: - pipdeptree = subprocess.check_output( - ["pipdeptree", "--json-tree"], - stderr=subprocess.STDOUT - ) - except subprocess.CalledProcessError: - sys.exit(1) - return json.loads(pipdeptree.strip()) - -def find_dependencies(jsonpipdeptree, package): - """ - find package dependencies in json tree - """ - if isinstance(jsonpipdeptree, dict): - keys = jsonpipdeptree.keys() - if 'package_name' in keys and 'required_version' in keys: - if re.search(r'^%s$' % package, jsonpipdeptree['package_name'], re.IGNORECASE): - yield jsonpipdeptree['required_version'] - for child_val in find_dependencies(jsonpipdeptree['dependencies'], package): - yield child_val - elif isinstance(jsonpipdeptree, list): - for item in jsonpipdeptree: - for item_val in find_dependencies(item, package): - yield item_val - -def filter_dependencies(package_list, jsonpipdeptree, exclude): - """ - return list of outdated packages that do not have strict dependency version - """ - filtered_dependencies = [] - for package in package_list: - if package in exclude: - continue - dependencies = [_ for _ in find_dependencies(jsonpipdeptree, package)] - dependencies = list(set(dependencies)) - if not [dep for dep in dependencies if re.search(r'(^==.*|^\d.*)', dep) is not None]: - filtered_dependencies.append( - {'package': package, - 'versions': [dep for dep in dependencies if 'Any' not in dep]} - ) - return filtered_dependencies - -def arg_parse(): - """ - argument parser - """ - parser = argparse.ArgumentParser( - description="Pipupgradedependencies upgrades all outdated packages with respect to \ - existing dependencies." - ) - parser.parse_args() - -def main(): - """ - main function - """ - os.environ["PYTHONWARNINGS"] = "ignore:DEPRECATION" - arg_parse() - upgrade_pip() - outdated_packages = get_outdated_packages() - - finished_upgrades = [] - while True: - filtered_outdated_packages = filter_dependencies( - outdated_packages, get_dependencies_tree(), finished_upgrades - ) - - try: - package = filtered_outdated_packages[-1] - except IndexError: - break - finished_upgrades.append(package['package']) - upgrade_package(package['package'], package['versions']) - - if len(filtered_outdated_packages) == len(finished_upgrades): - break - - print "Done." - -if __name__ == "__main__": - main() diff --git a/setup.py b/setup.py index 480bcd525cffb9bc61325b4bb0a67dca051edb53..9df5b4e5db223e8f9a2a55a49d38fc7089473354 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,13 @@ """ -pipupgradedependencies setup +pipdeps setup """ from setuptools import setup, find_packages setup( - name='pipupgradedependencies', + name='pipdeps', - description='Pipupgradedependencies upgrades all outdated packages with respect to existing \ - dependencies.', + description='Pipdeps shows/upgrades outdated packages with respect to existing \ + dependencies.', classifiers=[ 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', @@ -17,11 +17,11 @@ setup( author='IT4Innovations', author_email='support@it4i.cz', - url='https://code.it4i.cz/sccs/pip-upgradedependencies', + url='https://code.it4i.cz/sccs/pip-deps', license='GPLv3+', packages=find_packages(), - namespace_packages=['pipupgradedependencies'], + namespace_packages=['pipdeps'], zip_safe=False, version='0.0.1', #version_format='{tag}', @@ -29,11 +29,12 @@ setup( setup_requires=['setuptools-markdown'], #setup_requires=['setuptools-git-version', 'setuptools-markdown'], install_requires=[ + 'packaging', 'pipdeptree', ], entry_points={ 'console_scripts': [ - 'pipupgradedependencies = pipupgradedependencies.pipupgradedependencies:main', + 'pipdeps = pipdeps.pipdeps:main', ] } )