mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
1) parse with defaults set as they are naturally (vs forcing to None)
and adjust the logic to match; the result is simpler. 2) Due to #1, we can remove some hairy "format_control" hacks 3) Due to #1, we have to relax the parsing and allow: - multiple options per line - any supported option on a line with a requirement (not just --install-option/--global-option, although they are the only options that are passed into a requirement)
This commit is contained in:
parent
0d2cc68da9
commit
7c83f8d3cd
|
@ -22,15 +22,6 @@ class RequirementsFileParseError(InstallationError):
|
|||
"""Raised when a general error occurs parsing a requirements file line."""
|
||||
|
||||
|
||||
class ReqFileOnleOneOptionPerLineError(InstallationError):
|
||||
"""Raised when an option is not allowed in a requirements file."""
|
||||
|
||||
|
||||
class ReqFileOptionNotAllowedWithReqError(InstallationError):
|
||||
"""Raised when an option is not allowed on a requirement line in a requirements
|
||||
file."""
|
||||
|
||||
|
||||
class BestVersionAlreadyInstalled(PipError):
|
||||
"""Raised when the most up-to-date version of a package is already
|
||||
installed."""
|
||||
|
|
|
@ -15,9 +15,7 @@ from pip._vendor.six.moves import filterfalse
|
|||
import pip
|
||||
from pip.download import get_file_content
|
||||
from pip.req.req_install import InstallRequirement
|
||||
from pip.exceptions import (RequirementsFileParseError,
|
||||
ReqFileOnleOneOptionPerLineError,
|
||||
ReqFileOptionNotAllowedWithReqError)
|
||||
from pip.exceptions import (RequirementsFileParseError)
|
||||
from pip.utils import normalize_name
|
||||
from pip import cmdoptions
|
||||
|
||||
|
@ -40,16 +38,16 @@ SUPPORTED_OPTIONS = [
|
|||
cmdoptions.use_wheel,
|
||||
cmdoptions.no_use_wheel,
|
||||
cmdoptions.always_unzip,
|
||||
]
|
||||
|
||||
# options allowed on requirement lines
|
||||
SUPPORTED_OPTIONS_REQ = [
|
||||
cmdoptions.install_options,
|
||||
cmdoptions.global_options,
|
||||
cmdoptions.no_binary,
|
||||
cmdoptions.only_binary,
|
||||
]
|
||||
|
||||
# options to be passed to requirements
|
||||
SUPPORTED_OPTIONS_REQ = [
|
||||
cmdoptions.install_options,
|
||||
cmdoptions.global_options
|
||||
]
|
||||
|
||||
# the 'dest' string values
|
||||
SUPPORTED_OPTIONS_REQ_DEST = [o().dest for o in SUPPORTED_OPTIONS_REQ]
|
||||
|
||||
|
@ -94,46 +92,14 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
|
|||
Process a single requirements line; This can result in creating/yielding
|
||||
requirements, or updating the finder.
|
||||
"""
|
||||
|
||||
parser = build_parser()
|
||||
values = parser.get_default_values()
|
||||
defaults = parser.get_default_values()
|
||||
defaults.index_url = None
|
||||
if finder:
|
||||
values.format_control = finder.format_control
|
||||
else:
|
||||
# Undo the hack that removes defaults so that
|
||||
# this can be parsed correctly.
|
||||
values.format_control = pip.index.FormatControl(set(), set())
|
||||
orig_no_binary = frozenset(values.format_control.no_binary)
|
||||
orig_only_binary = frozenset(values.format_control.only_binary)
|
||||
args = shlex.split(line)
|
||||
opts, args = parser.parse_args(args, values)
|
||||
if opts.use_wheel is False and finder:
|
||||
pip.index.fmt_ctl_no_use_wheel(finder.format_control)
|
||||
setattr(values, 'use_wheel', None)
|
||||
if (orig_no_binary == opts.format_control.no_binary and
|
||||
orig_only_binary == opts.format_control.only_binary):
|
||||
# Make the per-requirement-line check work.
|
||||
setattr(values, 'format_control', None)
|
||||
|
||||
req = None
|
||||
if args:
|
||||
for key, value in opts.__dict__.items():
|
||||
# only certain options can be on req lines
|
||||
if value is not None and key not in SUPPORTED_OPTIONS_REQ_DEST:
|
||||
# get the option string
|
||||
# the option must be supported to get to this point
|
||||
for o in SUPPORTED_OPTIONS:
|
||||
o = o()
|
||||
if o.dest == key:
|
||||
opt_string = o.get_opt_string()
|
||||
msg = ('Option not supported on a'
|
||||
' requirement line: %s' % opt_string)
|
||||
raise ReqFileOptionNotAllowedWithReqError(msg)
|
||||
|
||||
# don't allow multiple/different options (on non-req lines)
|
||||
if not args and len(
|
||||
[v for v in opts.__dict__.values() if v is not None]) > 1:
|
||||
msg = 'Only one option allowed per line.'
|
||||
raise ReqFileOnleOneOptionPerLineError(msg)
|
||||
# `finder.format_control` will be updated during parsing
|
||||
defaults.format_control = finder.format_control
|
||||
opts, args = parser.parse_args(shlex.split(line), defaults)
|
||||
|
||||
# yield a line requirement
|
||||
if args:
|
||||
|
@ -142,12 +108,13 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
|
|||
isolated = options.isolated_mode if options else False
|
||||
if options:
|
||||
cmdoptions.check_install_build_global(options, opts)
|
||||
# trim the None items
|
||||
keys = [opt for opt in opts.__dict__ if getattr(opts, opt) is None]
|
||||
for key in keys:
|
||||
delattr(opts, key)
|
||||
# get the options that apply to requirements
|
||||
req_options = {}
|
||||
for dest in SUPPORTED_OPTIONS_REQ_DEST:
|
||||
if dest in opts.__dict__ and opts.__dict__[dest]:
|
||||
req_options[dest] = opts.__dict__[dest]
|
||||
yield InstallRequirement.from_line(
|
||||
args_line, comes_from, isolated=isolated, options=opts.__dict__,
|
||||
args_line, comes_from, isolated=isolated, options=req_options,
|
||||
wheel_cache=wheel_cache
|
||||
)
|
||||
|
||||
|
@ -181,24 +148,25 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
|
|||
|
||||
# set finder options
|
||||
elif finder:
|
||||
if opts.use_wheel is not None:
|
||||
finder.use_wheel = opts.use_wheel
|
||||
elif opts.no_index is not None:
|
||||
finder.index_urls = []
|
||||
elif opts.allow_all_external is not None:
|
||||
finder.allow_all_external = opts.allow_all_external
|
||||
elif opts.index_url is not None:
|
||||
if opts.index_url:
|
||||
finder.index_urls = [opts.index_url]
|
||||
elif opts.extra_index_urls is not None:
|
||||
if opts.use_wheel is False:
|
||||
finder.use_wheel = False
|
||||
pip.index.fmt_ctl_no_use_wheel(finder.format_control)
|
||||
elif opts.no_index is True:
|
||||
finder.index_urls = []
|
||||
elif opts.allow_all_external:
|
||||
finder.allow_all_external = opts.allow_all_external
|
||||
elif opts.extra_index_urls:
|
||||
finder.index_urls.extend(opts.extra_index_urls)
|
||||
elif opts.allow_external is not None:
|
||||
elif opts.allow_external:
|
||||
finder.allow_external |= set(
|
||||
[normalize_name(v).lower() for v in opts.allow_external])
|
||||
elif opts.allow_unverified is not None:
|
||||
elif opts.allow_unverified:
|
||||
# Remove after 7.0
|
||||
finder.allow_unverified |= set(
|
||||
[normalize_name(v).lower() for v in opts.allow_unverified])
|
||||
elif opts.find_links is not None:
|
||||
elif opts.find_links:
|
||||
# FIXME: it would be nice to keep track of the source
|
||||
# of the find_links: support a find-links local path
|
||||
# relative to a requirements file.
|
||||
|
@ -216,12 +184,9 @@ def build_parser():
|
|||
"""
|
||||
parser = optparse.OptionParser(add_help_option=False)
|
||||
|
||||
options = SUPPORTED_OPTIONS + SUPPORTED_OPTIONS_REQ
|
||||
for option_factory in options:
|
||||
option_factories = SUPPORTED_OPTIONS + SUPPORTED_OPTIONS_REQ
|
||||
for option_factory in option_factories:
|
||||
option = option_factory()
|
||||
# we want no default values; defaults are handled in `pip install`
|
||||
# parsing. just concerned with values that are specifically set.
|
||||
option.default = None
|
||||
parser.add_option(option)
|
||||
|
||||
# By default optparse sys.exits on parsing errors. We want to wrap
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from optparse import Values
|
||||
import os
|
||||
import subprocess
|
||||
from textwrap import dedent
|
||||
|
@ -8,11 +7,9 @@ import pytest
|
|||
from pretend import stub
|
||||
|
||||
import pip
|
||||
from pip.exceptions import (RequirementsFileParseError,
|
||||
ReqFileOnleOneOptionPerLineError,
|
||||
ReqFileOptionNotAllowedWithReqError)
|
||||
from pip.exceptions import (RequirementsFileParseError)
|
||||
from pip.download import PipSession
|
||||
from pip.index import FormatControl, PackageFinder
|
||||
from pip.index import PackageFinder
|
||||
from pip.req.req_install import InstallRequirement
|
||||
from pip.req.req_file import (parse_requirements, process_line, join_lines,
|
||||
ignore_comments)
|
||||
|
@ -28,6 +25,14 @@ def finder(session):
|
|||
return PackageFinder([], [], session=session)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def options(session):
|
||||
return stub(
|
||||
isolated_mode=False, default_vcs=None, index_url='default_url',
|
||||
skip_requirements_regex=False,
|
||||
format_control=pip.index.FormatControl(set(), set()))
|
||||
|
||||
|
||||
class TestIgnoreComments(object):
|
||||
"""tests for `ignore_comment`"""
|
||||
|
||||
|
@ -68,12 +73,6 @@ class TestJoinLines(object):
|
|||
class TestProcessLine(object):
|
||||
"""tests for `process_line`"""
|
||||
|
||||
def setup(self):
|
||||
self.options = stub(
|
||||
isolated_mode=False, default_vcs=None,
|
||||
skip_requirements_regex=False,
|
||||
format_control=pip.index.FormatControl(set(), set()))
|
||||
|
||||
def test_parser_error(self):
|
||||
with pytest.raises(RequirementsFileParseError):
|
||||
list(process_line("--bogus", "file", 1))
|
||||
|
@ -83,14 +82,6 @@ class TestProcessLine(object):
|
|||
with pytest.raises(ValueError):
|
||||
list(process_line("req1 req2", "file", 1))
|
||||
|
||||
def test_only_one_option_per_line(self):
|
||||
with pytest.raises(ReqFileOnleOneOptionPerLineError):
|
||||
list(process_line("--index-url=url --no-use-wheel", "file", 1))
|
||||
|
||||
def test_option_not_allowed_on_req_line(self):
|
||||
with pytest.raises(ReqFileOptionNotAllowedWithReqError):
|
||||
list(process_line("req --index-url=url", "file", 1))
|
||||
|
||||
def test_yield_line_requirement(self):
|
||||
line = 'SomeProject'
|
||||
filename = 'filename'
|
||||
|
@ -136,19 +127,19 @@ class TestProcessLine(object):
|
|||
'global_options': ['yo3', 'yo4'],
|
||||
'install_options': ['yo1', 'yo2']}
|
||||
|
||||
def test_set_isolated(self):
|
||||
def test_set_isolated(self, options):
|
||||
line = 'SomeProject'
|
||||
filename = 'filename'
|
||||
self.options.isolated_mode = True
|
||||
result = process_line(line, filename, 1, options=self.options)
|
||||
options.isolated_mode = True
|
||||
result = process_line(line, filename, 1, options=options)
|
||||
assert list(result)[0].isolated
|
||||
|
||||
def test_set_default_vcs(self):
|
||||
def test_set_default_vcs(self, options):
|
||||
url = 'https://url#egg=SomeProject'
|
||||
line = '-e %s' % url
|
||||
filename = 'filename'
|
||||
self.options.default_vcs = 'git'
|
||||
result = process_line(line, filename, 1, options=self.options)
|
||||
options.default_vcs = 'git'
|
||||
result = process_line(line, filename, 1, options=options)
|
||||
assert list(result)[0].link.url == 'git+' + url
|
||||
|
||||
def test_set_finder_no_index(self, finder):
|
||||
|
@ -237,19 +228,18 @@ class TestParseRequirements(object):
|
|||
'tests/req_just_comment.txt', session=PipSession()):
|
||||
pass
|
||||
|
||||
def test_multiple_appending_options(self, tmpdir, finder):
|
||||
def test_multiple_appending_options(self, tmpdir, finder, options):
|
||||
with open(tmpdir.join("req1.txt"), "w") as fp:
|
||||
fp.write("--extra-index-url url1 \n")
|
||||
fp.write("--extra-index-url url2 ")
|
||||
|
||||
list(parse_requirements(tmpdir.join("req1.txt"), finder=finder,
|
||||
session=PipSession()))
|
||||
session=PipSession(), options=options))
|
||||
|
||||
assert finder.index_urls == ['url1', 'url2']
|
||||
|
||||
def test_skip_regex(self, tmpdir, finder):
|
||||
options = stub(isolated_mode=False, default_vcs=None,
|
||||
skip_requirements_regex='.*Bad.*')
|
||||
def test_skip_regex(self, tmpdir, finder, options):
|
||||
options.skip_requirements_regex = '.*Bad.*'
|
||||
with open(tmpdir.join("req1.txt"), "w") as fp:
|
||||
fp.write("--extra-index-url Bad \n")
|
||||
fp.write("--extra-index-url Good ")
|
||||
|
@ -268,8 +258,7 @@ class TestParseRequirements(object):
|
|||
|
||||
assert finder.index_urls == ['url1', 'url2']
|
||||
|
||||
def test_req_file_parse_no_only_binary(self, data):
|
||||
finder = PackageFinder([], [], session=PipSession())
|
||||
def test_req_file_parse_no_only_binary(self, data, finder):
|
||||
list(parse_requirements(
|
||||
data.reqfiles.join("supported_options2.txt"), finder,
|
||||
session=PipSession()))
|
||||
|
@ -333,7 +322,7 @@ class TestParseRequirements(object):
|
|||
|
||||
parse_requirements(tmpdir.join("req.txt"), session=PipSession())
|
||||
|
||||
def test_install_requirements_with_options(self, tmpdir, finder, session):
|
||||
def test_install_requirements_with_options(self, tmpdir, finder, session, options):
|
||||
global_option = '--dry-run'
|
||||
install_option = '--prefix=/opt'
|
||||
|
||||
|
@ -347,10 +336,6 @@ class TestParseRequirements(object):
|
|||
with open(req_path, 'w') as fh:
|
||||
fh.write(content)
|
||||
|
||||
options = Values()
|
||||
options.format_control = FormatControl(set(), set())
|
||||
options.skip_requirements_regex = None
|
||||
options.isolated_mode = False
|
||||
req = next(parse_requirements(
|
||||
req_path, finder=finder, options=options, session=session))
|
||||
|
||||
|
|
Loading…
Reference in a new issue