Skip to content

Commit bc65cae

Browse files
committed
Use optparse and pip.cmdoptions for parsing requirement-line options
1 parent 054fa6c commit bc65cae

File tree

3 files changed

+54
-44
lines changed

3 files changed

+54
-44
lines changed

pip/req/req_file.py

+46-30
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import os
99
import re
1010
import shlex
11-
import getopt
11+
import optparse
1212

1313
from pip._vendor.six.moves.urllib import parse as urllib_parse
1414
from pip._vendor.six.moves import filterfalse
@@ -17,8 +17,10 @@
1717
from pip.req.req_install import InstallRequirement
1818
from pip.exceptions import RequirementsFileParseError
1919
from pip.utils import normalize_name
20+
from pip import cmdoptions
2021

2122

23+
# ----------------------------------------------------------------------------
2224
# Flags that don't take any options.
2325
parser_flags = set([
2426
'--no-index',
@@ -43,14 +45,27 @@
4345
'--no-allow-insecure', # Remove in 7.0
4446
])
4547

46-
# The following options and flags can be used on requirement lines.
47-
# For example: INITools==0.2 --install-option="--prefix=/opt"
48-
parser_requirement_flags = set()
49-
parser_requirement_options = set([
50-
'--install-option',
51-
'--global-option',
52-
])
48+
# ----------------------------------------------------------------------------
49+
# Requirement lines may take options. For example:
50+
# INITools==0.2 --install-option="--prefix=/opt" --global-option="-v"
51+
# We use optparse to reliably parse these lines.
52+
_req_parser = optparse.OptionParser(add_help_option=False)
53+
_req_parser.add_option(cmdoptions.install_options.make())
54+
_req_parser.add_option(cmdoptions.global_options.make())
55+
_req_parser.disable_interspersed_args()
56+
57+
# By default optparse sys.exits on error occurs. We want to wrap that
58+
# in our own exception.
59+
def parser_exit(self, msg):
60+
raise RequirementsFileParseError(msg)
61+
_req_parser.exit = parser_exit
62+
63+
# ----------------------------------------------------------------------------
64+
# Pre-compiled regex.
65+
_scheme_re = re.compile(r'^(http|https|file):', re.I)
66+
_comment_re = re.compile(r'(^|\s)+#.*$')
5367

68+
# ----------------------------------------------------------------------------
5469
# The types of lines understood by the requirements file parser.
5570
REQUIREMENT = 0
5671
REQUIREMENT_FILE = 1
@@ -59,9 +74,6 @@
5974
OPTION = 4
6075
IGNORE = 5
6176

62-
_scheme_re = re.compile(r'^(http|https|file):', re.I)
63-
_comment_re = re.compile(r'(^|\s)+#.*$')
64-
6577

6678
def parse_requirements(filename, finder=None, comes_from=None, options=None, session=None):
6779
"""
@@ -104,12 +116,6 @@ def parse_content(filename, content, finder=None, comes_from=None, options=None,
104116
# ---------------------------------------------------------------------
105117
if linetype == REQUIREMENT:
106118
req, opts = value
107-
108-
# InstallRequirement.install() expects these options to be lists.
109-
if opts:
110-
for opt in '--global-option', '--install-option':
111-
opts[opt] = shlex.split(opts[opt]) if opt in opts else []
112-
113119
comes_from = '-r %s (line %s)' % (filename, line_number)
114120
isolated = options.isolated_mode if options else False
115121
yield InstallRequirement.from_line(
@@ -182,13 +188,10 @@ def parse_content(filename, content, finder=None, comes_from=None, options=None,
182188

183189
def parse_line(line):
184190
if not line.startswith('-'):
191+
# Split the requirements from the options.
185192
if ' --' in line:
186193
req, opts = line.split(' --', 1)
187-
opts = parse_requirement_options(
188-
'--%s' % opts,
189-
parser_requirement_flags,
190-
parser_requirement_options
191-
)
194+
opts = parse_requirement_options('--%s' % opts)
192195
else:
193196
req = line
194197
opts = {}
@@ -223,15 +226,20 @@ def parse_line(line):
223226
return IGNORE, line
224227

225228

226-
def parse_requirement_options(req_line, flags=None, options=None):
227-
long_opts = []
228-
if options:
229-
long_opts += ['%s=' % i.lstrip('-') for i in options]
230-
if flags:
231-
long_opts += [i.lstrip('-') for i in flags]
229+
def parse_requirement_options(args):
230+
args = shlex.split(args)
231+
opts, _ = _req_parser.parse_args(args)
232+
233+
if opts.install_options:
234+
opts.install_options = flat_shlex_split(opts.install_options)
235+
if opts.global_options:
236+
opts.global_options = flat_shlex_split(opts.global_options)
232237

233-
opts, _ = getopt.getopt(shlex.split(req_line), '', long_opts)
234-
return dict(opts)
238+
for opt, value in opts.__dict__.items():
239+
if value is None:
240+
delattr(opts, opt)
241+
242+
return opts.__dict__
235243

236244

237245
# -----------------------------------------------------------------------------
@@ -281,4 +289,12 @@ def partition_line(line):
281289
return firstword, rest
282290

283291

292+
def flat_shlex_split(x):
293+
'''
294+
>>> flat_shlex_split(['--one --two', '--three "4" --five'])
295+
['--one', '--two', '--three', '4', '--five']
296+
'''
297+
return [j for i in x for j in shlex.split(i)]
298+
299+
284300
__all__ = 'parse_requirements'

pip/req/req_install.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -784,8 +784,8 @@ def install(self, install_options, global_options=[], root=None):
784784
# Options specified in requirements file override those
785785
# specified on the command line, since the last option given
786786
# to setup.py is the one that is used.
787-
global_options += self.options.get('--global-option', [])
788-
install_options += self.options.get('--install-option', [])
787+
global_options += self.options.get('global_options', [])
788+
install_options += self.options.get('install_options', [])
789789

790790
if self.isolated:
791791
global_options = list(global_options) + ["--no-user-cfg"]

tests/unit/test_req.py

+6-12
Original file line numberDiff line numberDiff line change
@@ -461,30 +461,24 @@ def test_parse_flags_from_requirements(finder):
461461

462462

463463
def test_get_requirement_options():
464+
pl = parse_line
464465
pro = parse_requirement_options
465466

466-
res = pro('--aflag --bflag', ['--aflag', '--bflag'])
467-
assert res == {'--aflag': '', '--bflag': ''}
468-
469-
res = pro('--install-option="--abc --zxc"', [], ['--install-option'])
470-
assert res == {'--install-option': '--abc --zxc'}
471-
472-
res = pro('--aflag --global-option="--abc" --install-option="--aflag"',
473-
['--aflag'], ['--install-option', '--global-option'])
474-
assert res == {'--aflag': '', '--global-option': '--abc', '--install-option': '--aflag'}
467+
res = pro('--install-option="--abc --zxc"')
468+
assert res == {'install_options': ['--abc', '--zxc']}
475469

476470
line = 'INITools==2.0 --global-option="--one --two -3" --install-option="--prefix=/opt"'
477471
assert parse_line(line) == (REQUIREMENT, (
478472
'INITools==2.0', {
479-
'--global-option': '--one --two -3',
480-
'--install-option': '--prefix=/opt'
473+
'global_options': ['--one', '--two', '-3'],
474+
'install_options': ['--prefix=/opt'],
481475
}))
482476

483477

484478
def test_install_requirements_with_options(tmpdir, finder, session):
485479
content = '''
486480
INITools == 2.0 --global-option="--one --two -3" \
487-
--install-option"--prefix=/opt"
481+
--install-option "--prefix=/opt"
488482
'''
489483

490484
req_path = tmpdir.join('requirements.txt')

0 commit comments

Comments
 (0)