Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added deps command to resolve package dependencies without installing #826

Closed
wants to merge 8 commits into from
3 changes: 3 additions & 0 deletions pip/commands/__init__.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pip.commands.bundle import BundleCommand from pip.commands.bundle import BundleCommand
from pip.commands.completion import CompletionCommand from pip.commands.completion import CompletionCommand
from pip.commands.freeze import FreezeCommand from pip.commands.freeze import FreezeCommand
from pip.commands.deps import DependenciesCommand
from pip.commands.help import HelpCommand from pip.commands.help import HelpCommand
from pip.commands.list import ListCommand from pip.commands.list import ListCommand
from pip.commands.search import SearchCommand from pip.commands.search import SearchCommand
Expand All @@ -20,6 +21,7 @@
BundleCommand.name: BundleCommand, BundleCommand.name: BundleCommand,
CompletionCommand.name: CompletionCommand, CompletionCommand.name: CompletionCommand,
FreezeCommand.name: FreezeCommand, FreezeCommand.name: FreezeCommand,
DependenciesCommand.name: DependenciesCommand,
HelpCommand.name: HelpCommand, HelpCommand.name: HelpCommand,
SearchCommand.name: SearchCommand, SearchCommand.name: SearchCommand,
ShowCommand.name: ShowCommand, ShowCommand.name: ShowCommand,
Expand All @@ -35,6 +37,7 @@
InstallCommand, InstallCommand,
UninstallCommand, UninstallCommand,
FreezeCommand, FreezeCommand,
DependenciesCommand,
ListCommand, ListCommand,
ShowCommand, ShowCommand,
SearchCommand, SearchCommand,
Expand Down
149 changes: 149 additions & 0 deletions pip/commands/deps.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,149 @@
import os
import sys
from pip.req import InstallRequirement, RequirementSet, parse_requirements
from pip.log import logger
from pip.locations import build_prefix, src_prefix
from pip.basecommand import Command, ERROR, SUCCESS
from pip.index import PackageFinder
from pip.cmdoptions import make_option_group, index_group


class DependenciesCommand(Command):
"""
Resolve package dependencies from:

- PyPI (and other indexes) using requirement specifiers.
- VCS project urls.
- Local project directories.
- Local or remote source archives.

pip also supports resolving from "requirements files", which provide
an easy way to specify a whole environment to be installed.

See http://www.pip-installer.org for details on VCS url formats and
requirements files.
"""
name = 'deps'

usage = """
%prog [options] <requirement specifier> ...
%prog [options] -r <requirements file> ...
%prog [options] [-e] <vcs project url> ...
%prog [options] [-e] <local project path> ...
%prog [options] <archive url/path> ..."""

summary = 'Resolve dependencies for packages.'
bundle = False

def __init__(self, *args, **kw):
super(DependenciesCommand, self).__init__(*args, **kw)

cmd_opts = self.cmd_opts

cmd_opts.add_option(
'-e', '--editable',
dest='editables',
action='append',
default=[],
metavar='path/url',
help='Install a project in editable mode (i.e. setuptools "develop mode") from a local project path or a VCS url.')

cmd_opts.add_option(
'-r', '--requirement',
dest='requirements',
action='append',
default=[],
metavar='file',
help='Install from the given requirements file. '
'This option can be used multiple times.')

index_opts = make_option_group(index_group, self.parser)

self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, cmd_opts)

def _build_package_finder(self, options, index_urls):
"""
Create a package finder appropriate to this install command.
This method is meant to be overridden by subclasses, not
called directly.
"""
return PackageFinder(find_links=options.find_links,
index_urls=index_urls,
use_mirrors=options.use_mirrors,
mirrors=options.mirrors)

def _requirement_line(self, req):
line = ''

if req.dependency_links:
line += '\n'.join(
['--find-links {}'.format(l) for l in req.dependency_links])
line += '\n'

if req.editable:
line += '-e '

if req.url:
line += req.url
else:
line += '%s==%s' % (req.name, req.installed_version)

return line

def run(self, options, args):
options.no_install = True

options.build_dir = os.path.abspath(build_prefix)
options.src_dir = os.path.abspath(src_prefix)
index_urls = [options.index_url] + options.extra_index_urls
if options.no_index:
logger.notify('Ignoring indexes: %s' % ','.join(index_urls))
index_urls = []

finder = self._build_package_finder(options, index_urls)

requirement_set = RequirementSet(
build_dir=options.build_dir,
src_dir=options.src_dir,
download_dir=None,
ignore_installed=True,
force_reinstall=True)

for name in args:
requirement_set.add_requirement(
InstallRequirement.from_line(name, None))
for name in options.editables:
requirement_set.add_requirement(
InstallRequirement.from_editable(name, default_vcs=options.default_vcs))
for filename in options.requirements:
for req in parse_requirements(filename, finder=finder, options=options):
requirement_set.add_requirement(req)

if not requirement_set.has_requirements:
opts = {'name': self.name}
if options.find_links:
msg = ('You must give at least one requirement to %(name)s '
'(maybe you meant "pip %(name)s %(links)s"?)' %
dict(opts, links=' '.join(options.find_links)))
else:
msg = ('You must give at least one requirement '
'to %(name)s (see "pip help %(name)s")' % opts)
logger.warn(msg)
return ERROR

requirement_set.prepare_files(finder)

requirements = '\n'.join(
[self._requirement_line(req) for req in
requirement_set.successfully_downloaded])

if requirements:
requirement_set.cleanup_files()
sys.stdout.write(requirements + '\n')

return SUCCESS

def setup_logging(self):
# Avoid download progress on stdout
logger.move_stdout_to_stderr()
Binary file added tests/packages/deps/dependant-1.0.tar.gz
Binary file not shown.
Binary file added tests/packages/deps/dependency-1.0.tar.gz
Binary file not shown.
Binary file added tests/packages/deps/dependency-links-1.0.tar.gz
Binary file not shown.
92 changes: 92 additions & 0 deletions tests/test_deps.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-

import os
from unittest import TestCase
from tests.test_pip import reset_env, run_pip, here
from pip.basecommand import ERROR, SUCCESS


def _run_local_deps(*args, **kwargs):
find_links = 'file://' + os.path.join(here, 'packages/deps')
return _run_deps('-f', find_links, *args, **kwargs)

def _run_deps(*args, **kwargs):
kwargs['expect_stderr'] = True
return run_pip('deps', '--no-index', *args, **kwargs)


class TestDepsCommandWithASinglePackage(TestCase):

@classmethod
def setupClass(cls):
reset_env()
cls.result = _run_local_deps('dependency')

def test_exits_with_success(self):
self.assertEqual(self.result.returncode, SUCCESS)

def test_returns_version_info(self):
self.assertTrue('dependency==1.0' in self.result.stdout)

def test_redirects_downloading_messages_to_stderr(self):
self.assertTrue('Downloading/unpacking' in self.result.stderr)

def test_removes_downloaded_files(self):
self.assertFalse(self.result.files_created)


class TestDepsCommandWithDependencies(TestCase):

@classmethod
def setupClass(cls):
reset_env()
cls.result = _run_local_deps('dependant')

def test_returns_version_info(self):
assert 'dependant==1.0' in self.result.stdout
assert 'dependency==1.0' in self.result.stdout

def test_exits_with_success(self):
self.assertEqual(self.result.returncode, SUCCESS)


class TestDepsCommandWithNonExistentPackage(TestCase):

@classmethod
def setupClass(cls):
reset_env()
cls.result = _run_local_deps('non-existent', expect_error=True)

def test_exits_with_error(self):
self.assertEqual(self.result.returncode, ERROR)


class TestDepsWithURL(TestCase):

@classmethod
def setupClass(cls):
reset_env()
path = os.path.join(here, 'packages/deps/dependency-1.0.tar.gz')
cls.url = 'file://' + path
cls.result = _run_deps(cls.url)

def test_returns_dependency_url(self):
assert self.url in self.result.stdout

def test_exits_with_success(self):
self.assertEqual(self.result.returncode, SUCCESS)


class TestDepsWithDependencyLinks(TestCase):

@classmethod
def setupClass(cls):
reset_env()
cls.result = _run_local_deps('dependency-links')

def test_returns_dependency_links(self):
assert 'dependency-links==1.0' in self.result.stdout
assert '--find-links http://pypi.python.org/simple' in self.result.stdout

def test_exits_with_success(self):
self.assertEqual(self.result.returncode, SUCCESS)