From 76c356151a294c2d5bbf789d591cad9ef6e1fb3e Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Wed, 19 Jun 2013 13:13:06 +0100 Subject: [PATCH] Add a `pip check` command. This command ensures that all packages installed have all the requirements they need, and that requirements have compatible versions. This is useful because pip can install incompatible dependencies[1], or a user may have manually (un)installed a package. [1] https://github.com/pypa/pip/issues/775 --- pip/commands/__init__.py | 3 ++ pip/commands/check.py | 37 ++++++++++++++++++++++++ pip/operations/check.py | 52 ++++++++++++++++++++++++++++++++++ tests/functional/test_check.py | 52 ++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 pip/commands/check.py create mode 100644 pip/operations/check.py create mode 100644 tests/functional/test_check.py diff --git a/pip/commands/__init__.py b/pip/commands/__init__.py index 93ab3807cd4..5fc3cfdf89c 100644 --- a/pip/commands/__init__.py +++ b/pip/commands/__init__.py @@ -7,6 +7,7 @@ from pip.commands.freeze import FreezeCommand from pip.commands.help import HelpCommand from pip.commands.list import ListCommand +from pip.commands.check import CheckCommand from pip.commands.search import SearchCommand from pip.commands.show import ShowCommand from pip.commands.install import InstallCommand @@ -27,6 +28,7 @@ UnzipCommand.name: UnzipCommand, ZipCommand.name: ZipCommand, ListCommand.name: ListCommand, + CheckCommand.name: CheckCommand, WheelCommand.name: WheelCommand, } @@ -37,6 +39,7 @@ FreezeCommand, ListCommand, ShowCommand, + CheckCommand, SearchCommand, WheelCommand, ZipCommand, diff --git a/pip/commands/check.py b/pip/commands/check.py new file mode 100644 index 00000000000..54d3b102d12 --- /dev/null +++ b/pip/commands/check.py @@ -0,0 +1,37 @@ +import logging + +from pip.basecommand import Command +from pip.operations.check import ( + check_requirements, get_installed_distributions) + + +logger = logging.getLogger(__name__) + + +class CheckCommand(Command): + """Verify installed packages have compatible dependencies.""" + name = 'check' + usage = """ + %prog [options]""" + summary = 'Verify installed packages have compatible dependencies.' + + def run(self, options, args): + installed = get_installed_distributions(skip=()) + missing_reqs_dict, incompatible_reqs_dict = check_requirements() + + for dist in installed: + key = '%s==%s' % (dist.project_name, dist.version) + + for requirement in missing_reqs_dict.get(key, []): + logger.info( + "%s %s requires %s, which is not installed.", + dist.project_name, dist.version, requirement.project_name) + + for requirement, actual in incompatible_reqs_dict.get(key, []): + logger.info( + "%s %s has requirement %s, but you have %s %s.", + dist.project_name, dist.version, requirement, + actual.project_name, actual.version) + + if missing_reqs_dict or incompatible_reqs_dict: + return 1 diff --git a/pip/operations/check.py b/pip/operations/check.py new file mode 100644 index 00000000000..38712eb5433 --- /dev/null +++ b/pip/operations/check.py @@ -0,0 +1,52 @@ +from pip.utils import get_installed_distributions + + +def check_requirements(): + installed = get_installed_distributions(skip=()) + missing_reqs_dict = {} + incompatible_reqs_dict = {} + + for dist in installed: + key = '%s==%s' % (dist.project_name, dist.version) + + missing_reqs = list(get_missing_reqs(dist, installed)) + if missing_reqs: + missing_reqs_dict[key] = missing_reqs + + incompatible_reqs = list(get_incompatible_reqs(dist, installed)) + if incompatible_reqs: + incompatible_reqs_dict[key] = incompatible_reqs + + return (missing_reqs_dict, incompatible_reqs_dict) + + +def get_missing_reqs(dist, installed_dists): + """Return all of the requirements of `dist` that aren't present in + `installed_dists`. + + """ + installed_names = set(d.project_name.lower() for d in installed_dists) + missing_requirements = set() + + for requirement in dist.requires(): + if requirement.project_name.lower() not in installed_names: + missing_requirements.add(requirement) + yield requirement + + +def get_incompatible_reqs(dist, installed_dists): + """Return all of the requirements of `dist` that are present in + `installed_dists`, but have incompatible versions. + + """ + installed_dists_by_name = {} + for installed_dist in installed_dists: + installed_dists_by_name[installed_dist.project_name] = installed_dist + + incompatible_requirements = set() + for requirement in dist.requires(): + present_dist = installed_dists_by_name.get(requirement.project_name) + + if present_dist and present_dist not in requirement: + incompatible_requirements.add((requirement, present_dist)) + yield (requirement, present_dist) diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py new file mode 100644 index 00000000000..da1b3f7bd7f --- /dev/null +++ b/tests/functional/test_check.py @@ -0,0 +1,52 @@ +def test_check_clean(script): + """On a clean environment, check shouldn't return anything. + + """ + result = script.pip('check') + assert result.stdout == "" + + +def test_check_missing_dependency(script): + # this will also install ipython, a dependency + script.pip('install', 'ipdb==0.7') + + # deliberately remove the dependency + script.pip('uninstall', 'ipython', '--yes') + + result = script.pip('check', expect_error=True) + + assert result.stdout == ("ipdb 0.7 requires ipython, " + "which is not installed.\n") + assert result.returncode == 1 + + +def test_check_missing_dependency_normalize_case(script): + # Install some things + script.pip('install', 'devpi-web==2.2.2') + script.pip('install', 'pyramid==1.5.2') + + # deliberately remove some dependencies + script.pip('uninstall', 'pygments', '--yes') + script.pip('uninstall', 'zope.deprecation', '--yes') + + result = script.pip('check', expect_error=True) + + assert ('devpi-web 2.2.2 requires pygments, ' + 'which is not installed.') in result.stdout + assert ('pyramid 1.5.2 requires zope.deprecation, ' + 'which is not installed.') in result.stdout + assert result.returncode == 1 + + +def test_check_broken_dependency(script): + # this will also install a compatible version of jinja2 + script.pip('install', 'flask==0.10.1') + + # deliberately change dependency to a version that is too old + script.pip('install', 'jinja2==2.3') + + result = script.pip('check', expect_error=True) + + assert result.stdout == ("Flask 0.10.1 has requirement Jinja2>=2.4, " + "but you have Jinja2 2.3.\n") + assert result.returncode == 1