diff --git a/news/6527.bugfix b/news/6527.bugfix new file mode 100644 index 00000000000..92d29d9ff58 --- /dev/null +++ b/news/6527.bugfix @@ -0,0 +1,2 @@ +Add the line number and file location to the error message when reading an +invalid requirements file in certain situations. diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 99df626b775..d0b0c7153ee 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -205,11 +205,15 @@ def install_req_from_line( isolated=False, # type: bool options=None, # type: Optional[Dict[str, Any]] wheel_cache=None, # type: Optional[WheelCache] - constraint=False # type: bool + constraint=False, # type: bool + line_source=None, # type: Optional[str] ): # type: (...) -> InstallRequirement """Creates an InstallRequirement from a name, which might be a requirement, directory containing 'setup.py', filename, or URL. + + :param line_source: An optional string describing where the line is from, + for logging purposes in case of an error. """ if is_url(name): marker_sep = '; ' @@ -289,10 +293,17 @@ def install_req_from_line( not any(op in req_as_string for op in operators)): add_msg = "= is not a valid operator. Did you mean == ?" else: - add_msg = "" - raise InstallationError( - "Invalid requirement: '%s'\n%s" % (req_as_string, add_msg) + add_msg = '' + if line_source is None: + source = '' + else: + source = ' (from {})'.format(line_source) + msg = ( + 'Invalid requirement: {!r}{}'.format(req_as_string, source) ) + if add_msg: + msg += '\nHint: {}'.format(add_msg) + raise InstallationError(msg) else: req = None diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index c066b406d56..41fdd362f56 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -138,7 +138,7 @@ def process_line( session=None, # type: Optional[PipSession] wheel_cache=None, # type: Optional[WheelCache] use_pep517=None, # type: Optional[bool] - constraint=False # type: bool + constraint=False, # type: bool ): # type: (...) -> Iterator[InstallRequirement] """Process a single requirements line; This can result in creating/yielding @@ -187,10 +187,16 @@ def process_line( for dest in SUPPORTED_OPTIONS_REQ_DEST: if dest in opts.__dict__ and opts.__dict__[dest]: req_options[dest] = opts.__dict__[dest] + line_source = 'line {} of {}'.format(line_number, filename) yield install_req_from_line( - args_str, line_comes_from, constraint=constraint, + args_str, + comes_from=line_comes_from, use_pep517=use_pep517, - isolated=isolated, options=req_options, wheel_cache=wheel_cache + isolated=isolated, + options=req_options, + wheel_cache=wheel_cache, + constraint=constraint, + line_source=line_source, ) # yield an editable requirement diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 3f8c38ce58e..a7f830475f1 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -530,7 +530,7 @@ def test_unidentifiable_name(self): with pytest.raises(InstallationError) as e: install_req_from_line(test_name) err_msg = e.value.args[0] - assert ("Invalid requirement: '%s'\n" % test_name) == err_msg + assert "Invalid requirement: '{}'".format(test_name) == err_msg def test_requirement_file(self): req_file_path = os.path.join(self.tempdir, 'test.txt') diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 1c73d4f23f1..2cd3e6a471f 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -190,6 +190,26 @@ def test_only_one_req_per_line(self): with pytest.raises(InstallationError): list(process_line("req1 req2", "file", 1)) + def test_error_message(self): + """ + Test the error message if a parsing error occurs (all of path, + line number, and hint). + """ + iterator = process_line( + 'my-package=1.0', + filename='path/requirements.txt', + line_number=3 + ) + with pytest.raises(InstallationError) as exc: + list(iterator) + + expected = ( + "Invalid requirement: 'my-package=1.0' " + '(from line 3 of path/requirements.txt)\n' + 'Hint: = is not a valid operator. Did you mean == ?' + ) + assert str(exc.value) == expected + def test_yield_line_requirement(self): line = 'SomeProject' filename = 'filename'