Skip to content

Commit a0fd8c2

Browse files
authored
Merge pull request #7261 from chrahunt/refactor/simplify-req-file-parsing-3
Simplify requirement file parsing
2 parents c9dcb0d + 85918af commit a0fd8c2

File tree

3 files changed

+377
-237
lines changed

3 files changed

+377
-237
lines changed

src/pip/_internal/req/req_file.py

Lines changed: 156 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@
8484
SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ]
8585

8686

87+
class ParsedLine(object):
88+
def __init__(
89+
self,
90+
filename, # type: str
91+
lineno, # type: int
92+
comes_from, # type: str
93+
args, # type: str
94+
opts, # type: Values
95+
constraint, # type: bool
96+
):
97+
# type: (...) -> None
98+
self.filename = filename
99+
self.lineno = lineno
100+
self.comes_from = comes_from
101+
self.args = args
102+
self.opts = opts
103+
self.constraint = constraint
104+
105+
87106
def parse_requirements(
88107
filename, # type: str
89108
finder=None, # type: Optional[PackageFinder]
@@ -113,20 +132,19 @@ def parse_requirements(
113132
"'session'"
114133
)
115134

116-
_, content = get_file_content(
117-
filename, comes_from=comes_from, session=session
118-
)
119-
120135
skip_requirements_regex = (
121136
options.skip_requirements_regex if options else None
122137
)
123-
lines_enum = preprocess(content, skip_requirements_regex)
138+
line_parser = get_line_parser(finder)
139+
parser = RequirementsFileParser(
140+
session, line_parser, comes_from, skip_requirements_regex
141+
)
124142

125-
for line_number, line in lines_enum:
126-
req_iter = process_line(line, filename, line_number, finder,
127-
comes_from, options, session, wheel_cache,
128-
use_pep517=use_pep517, constraint=constraint)
129-
for req in req_iter:
143+
for parsed_line in parser.parse(filename, constraint):
144+
req = handle_line(
145+
parsed_line, finder, options, session, wheel_cache, use_pep517
146+
)
147+
if req is not None:
130148
yield req
131149

132150

@@ -146,21 +164,17 @@ def preprocess(content, skip_requirements_regex):
146164
return lines_enum
147165

148166

149-
def process_line(
150-
line, # type: Text
151-
filename, # type: str
152-
line_number, # type: int
167+
def handle_line(
168+
line, # type: ParsedLine
153169
finder=None, # type: Optional[PackageFinder]
154-
comes_from=None, # type: Optional[str]
155170
options=None, # type: Optional[optparse.Values]
156171
session=None, # type: Optional[PipSession]
157172
wheel_cache=None, # type: Optional[WheelCache]
158173
use_pep517=None, # type: Optional[bool]
159-
constraint=False, # type: bool
160174
):
161-
# type: (...) -> Iterator[InstallRequirement]
162-
"""Process a single requirements line; This can result in creating/yielding
163-
requirements, or updating the finder.
175+
# type: (...) -> Optional[InstallRequirement]
176+
"""Handle a single parsed requirements line; This can result in
177+
creating/yielding requirements, or updating the finder.
164178
165179
For lines that contain requirements, the only options that have an effect
166180
are from SUPPORTED_OPTIONS_REQ, and they are scoped to the
@@ -172,102 +186,65 @@ def process_line(
172186
be present, but are ignored. These lines may contain multiple options
173187
(although our docs imply only one is supported), and all our parsed and
174188
affect the finder.
175-
176-
:param constraint: If True, parsing a constraints file.
177-
:param options: OptionParser options that we may update
178189
"""
179-
line_parser = get_line_parser(finder)
180-
try:
181-
args_str, opts = line_parser(line)
182-
except OptionParsingError as e:
183-
# add offending line
184-
msg = 'Invalid requirement: %s\n%s' % (line, e.msg)
185-
raise RequirementsFileParseError(msg)
186-
187-
# parse a nested requirements file
188-
if (
189-
not args_str and
190-
not opts.editables and
191-
(opts.requirements or opts.constraints)
192-
):
193-
if opts.requirements:
194-
req_path = opts.requirements[0]
195-
nested_constraint = False
196-
else:
197-
req_path = opts.constraints[0]
198-
nested_constraint = True
199-
# original file is over http
200-
if SCHEME_RE.search(filename):
201-
# do a url join so relative paths work
202-
req_path = urllib_parse.urljoin(filename, req_path)
203-
# original file and nested file are paths
204-
elif not SCHEME_RE.search(req_path):
205-
# do a join so relative paths work
206-
req_path = os.path.join(os.path.dirname(filename), req_path)
207-
parsed_reqs = parse_requirements(
208-
req_path, finder, comes_from, options, session,
209-
constraint=nested_constraint, wheel_cache=wheel_cache
210-
)
211-
for req in parsed_reqs:
212-
yield req
213-
return
214190

215191
# preserve for the nested code path
216192
line_comes_from = '%s %s (line %s)' % (
217-
'-c' if constraint else '-r', filename, line_number,
193+
'-c' if line.constraint else '-r', line.filename, line.lineno,
218194
)
219195

220-
# yield a line requirement
221-
if args_str:
196+
# return a line requirement
197+
if line.args:
222198
isolated = options.isolated_mode if options else False
223199
if options:
224-
cmdoptions.check_install_build_global(options, opts)
200+
cmdoptions.check_install_build_global(options, line.opts)
225201
# get the options that apply to requirements
226202
req_options = {}
227203
for dest in SUPPORTED_OPTIONS_REQ_DEST:
228-
if dest in opts.__dict__ and opts.__dict__[dest]:
229-
req_options[dest] = opts.__dict__[dest]
230-
line_source = 'line {} of {}'.format(line_number, filename)
231-
yield install_req_from_line(
232-
args_str,
204+
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
205+
req_options[dest] = line.opts.__dict__[dest]
206+
line_source = 'line {} of {}'.format(line.lineno, line.filename)
207+
return install_req_from_line(
208+
line.args,
233209
comes_from=line_comes_from,
234210
use_pep517=use_pep517,
235211
isolated=isolated,
236212
options=req_options,
237213
wheel_cache=wheel_cache,
238-
constraint=constraint,
214+
constraint=line.constraint,
239215
line_source=line_source,
240216
)
241217

242-
# yield an editable requirement
243-
elif opts.editables:
218+
# return an editable requirement
219+
elif line.opts.editables:
244220
isolated = options.isolated_mode if options else False
245-
yield install_req_from_editable(
246-
opts.editables[0], comes_from=line_comes_from,
221+
return install_req_from_editable(
222+
line.opts.editables[0], comes_from=line_comes_from,
247223
use_pep517=use_pep517,
248-
constraint=constraint, isolated=isolated, wheel_cache=wheel_cache
224+
constraint=line.constraint, isolated=isolated,
225+
wheel_cache=wheel_cache
249226
)
250227

251228
# percolate hash-checking option upward
252-
elif opts.require_hashes:
253-
options.require_hashes = opts.require_hashes
229+
elif line.opts.require_hashes:
230+
options.require_hashes = line.opts.require_hashes
254231

255232
# set finder options
256233
elif finder:
257234
find_links = finder.find_links
258235
index_urls = finder.index_urls
259-
if opts.index_url:
260-
index_urls = [opts.index_url]
261-
if opts.no_index is True:
236+
if line.opts.index_url:
237+
index_urls = [line.opts.index_url]
238+
if line.opts.no_index is True:
262239
index_urls = []
263-
if opts.extra_index_urls:
264-
index_urls.extend(opts.extra_index_urls)
265-
if opts.find_links:
240+
if line.opts.extra_index_urls:
241+
index_urls.extend(line.opts.extra_index_urls)
242+
if line.opts.find_links:
266243
# FIXME: it would be nice to keep track of the source
267244
# of the find_links: support a find-links local path
268245
# relative to a requirements file.
269-
value = opts.find_links[0]
270-
req_dir = os.path.dirname(os.path.abspath(filename))
246+
value = line.opts.find_links[0]
247+
req_dir = os.path.dirname(os.path.abspath(line.filename))
271248
relative_to_reqs_file = os.path.join(req_dir, value)
272249
if os.path.exists(relative_to_reqs_file):
273250
value = relative_to_reqs_file
@@ -279,23 +256,110 @@ def process_line(
279256
)
280257
finder.search_scope = search_scope
281258

282-
if opts.pre:
259+
if line.opts.pre:
283260
finder.set_allow_all_prereleases()
284-
for host in opts.trusted_hosts or []:
285-
source = 'line {} of {}'.format(line_number, filename)
286-
session.add_trusted_host(host, source=source)
261+
262+
if session:
263+
for host in line.opts.trusted_hosts or []:
264+
source = 'line {} of {}'.format(line.lineno, line.filename)
265+
session.add_trusted_host(host, source=source)
266+
267+
return None
268+
269+
270+
class RequirementsFileParser(object):
271+
def __init__(
272+
self,
273+
session, # type: PipSession
274+
line_parser, # type: LineParser
275+
comes_from, # type: str
276+
skip_requirements_regex, # type: Optional[str]
277+
):
278+
# type: (...) -> None
279+
self._session = session
280+
self._line_parser = line_parser
281+
self._comes_from = comes_from
282+
self._skip_requirements_regex = skip_requirements_regex
283+
284+
def parse(self, filename, constraint):
285+
# type: (str, bool) -> Iterator[ParsedLine]
286+
"""Parse a given file, yielding parsed lines.
287+
"""
288+
for line in self._parse_and_recurse(filename, constraint):
289+
yield line
290+
291+
def _parse_and_recurse(self, filename, constraint):
292+
# type: (str, bool) -> Iterator[ParsedLine]
293+
for line in self._parse_file(filename, constraint):
294+
if (
295+
not line.args and
296+
not line.opts.editables and
297+
(line.opts.requirements or line.opts.constraints)
298+
):
299+
# parse a nested requirements file
300+
if line.opts.requirements:
301+
req_path = line.opts.requirements[0]
302+
nested_constraint = False
303+
else:
304+
req_path = line.opts.constraints[0]
305+
nested_constraint = True
306+
307+
# original file is over http
308+
if SCHEME_RE.search(filename):
309+
# do a url join so relative paths work
310+
req_path = urllib_parse.urljoin(filename, req_path)
311+
# original file and nested file are paths
312+
elif not SCHEME_RE.search(req_path):
313+
# do a join so relative paths work
314+
req_path = os.path.join(
315+
os.path.dirname(filename), req_path,
316+
)
317+
318+
for inner_line in self._parse_and_recurse(
319+
req_path, nested_constraint,
320+
):
321+
yield inner_line
322+
else:
323+
yield line
324+
325+
def _parse_file(self, filename, constraint):
326+
# type: (str, bool) -> Iterator[ParsedLine]
327+
_, content = get_file_content(
328+
filename, comes_from=self._comes_from, session=self._session
329+
)
330+
331+
lines_enum = preprocess(content, self._skip_requirements_regex)
332+
333+
for line_number, line in lines_enum:
334+
try:
335+
args_str, opts = self._line_parser(line)
336+
except OptionParsingError as e:
337+
# add offending line
338+
msg = 'Invalid requirement: %s\n%s' % (line, e.msg)
339+
raise RequirementsFileParseError(msg)
340+
341+
yield ParsedLine(
342+
filename,
343+
line_number,
344+
self._comes_from,
345+
args_str,
346+
opts,
347+
constraint,
348+
)
287349

288350

289351
def get_line_parser(finder):
290352
# type: (Optional[PackageFinder]) -> LineParser
291-
parser = build_parser()
292-
defaults = parser.get_default_values()
293-
defaults.index_url = None
294-
if finder:
295-
defaults.format_control = finder.format_control
296-
297353
def parse_line(line):
298354
# type: (Text) -> Tuple[str, Values]
355+
# Build new parser for each line since it accumulates appendable
356+
# options.
357+
parser = build_parser()
358+
defaults = parser.get_default_values()
359+
defaults.index_url = None
360+
if finder:
361+
defaults.format_control = finder.format_control
362+
299363
args_str, options_str = break_args_options(line)
300364
# Prior to 2.7.3, shlex cannot deal with unicode entries
301365
if sys.version_info < (2, 7, 3):

tests/unit/test_req.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
install_req_from_req_string,
3030
parse_editable,
3131
)
32-
from pip._internal.req.req_file import process_line
32+
from pip._internal.req.req_file import ParsedLine, get_line_parser, handle_line
3333
from pip._internal.req.req_tracker import RequirementTracker
3434
from pip._internal.utils.urls import path_to_url
3535
from tests.lib import (
@@ -41,7 +41,18 @@
4141

4242

4343
def get_processed_req_from_line(line, fname='file', lineno=1):
44-
req = list(process_line(line, fname, lineno))[0]
44+
line_parser = get_line_parser(None)
45+
args_str, opts = line_parser(line)
46+
parsed_line = ParsedLine(
47+
fname,
48+
lineno,
49+
fname,
50+
args_str,
51+
opts,
52+
False,
53+
)
54+
req = handle_line(parsed_line)
55+
assert req is not None
4556
req.is_direct = True
4657
return req
4758

0 commit comments

Comments
 (0)