diff --git a/pip/commands/__init__.py b/pip/commands/__init__.py index e0702d2700b..49f3a7fe496 100644 --- a/pip/commands/__init__.py +++ b/pip/commands/__init__.py @@ -8,6 +8,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 @@ -29,6 +30,7 @@ UnzipCommand.name: UnzipCommand, ZipCommand.name: ZipCommand, ListCommand.name: ListCommand, + CheckCommand.name: CheckCommand, WheelCommand.name: WheelCommand, } @@ -39,6 +41,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..bfa64602317 --- /dev/null +++ b/pip/commands/check.py @@ -0,0 +1,67 @@ +from pip.log import logger +from pip.basecommand import Command +from pip.util import get_installed_distributions + + +class CheckCommand(Command): + """Verify installed packages have compatible dependencies.""" + name = 'check' + usage = """ + %prog [options]""" + summary = 'Verify installed packages have compatible dependencies.' + + def setup_logging(self): + logger.move_stdout_to_stderr() + + def run(self, options, args): + all_requirements_met = True + + installed = get_installed_distributions() + for dist in installed: + + missing_requirements = self.get_missing_requirements(dist, installed) + for requirement in missing_requirements: + logger.notify("%s %s requires %s, which is not installed." % + (dist.project_name, dist.version, requirement.project_name)) + + incompatible_requirements = self.get_incompatible_requirements(dist, installed) + for requirement, actual in incompatible_requirements: + logger.notify("%s %s has requirement %s, but you have %s %s." % + (dist.project_name, dist.version, requirement, + actual.project_name, actual.version)) + + if missing_requirements or incompatible_requirements: + all_requirements_met = False + + if not all_requirements_met: + return 1 + + def get_missing_requirements(self, dist, installed_dists): + """Return all of the requirements of `dist` that aren't present in + `installed_dists`. + + """ + installed_names = set(d.project_name for d in installed_dists) + + missing_requirements = set() + for requirement in dist.requires(): + if requirement.project_name not in installed_names: + missing_requirements.add(requirement) + yield requirement + + def get_incompatible_requirements(self, 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..747121f23bb --- /dev/null +++ b/tests/functional/test_check.py @@ -0,0 +1,32 @@ +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, expect_stderr=True) + + assert result.stderr == "ipdb 0.7 requires ipython, which is not installed.\n" + 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, expect_stderr=True) + + assert result.stderr == "Flask 0.10.1 has requirement Jinja2>=2.4, but you have Jinja2 2.3.\n" + assert result.returncode == 1