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

Improve error shown when there's no space between semicolon and url #592

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions packaging/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,27 @@ def parse_named_requirement(requirement: str) -> Requirement:
url = tokens.read().text[1:].strip()
elif not tokens.match("END"):
specifier = parse_specifier(tokens)
marker = ""
if tokens.try_read("SEMICOLON"):
marker = ""
while not tokens.match("END"):
# we don't validate markers here, it's done later as part of
# packaging/requirements.py
marker += tokens.read().text
else:
marker = ""
tokens.expect(
"END",
error_message="Expected semicolon (followed by markers) or end of string",
)
elif not tokens.match("END"):
if url and url[-1] == ";":
error_msg = (
"Expected space before semicolon (followed by markers) or end of string"
)
# update position to point at the place where the space was expected
tokens.position -= 1
suffix = (
f"Maybe you mean this instead?\n "
f"{tokens.source[:tokens.position]} {tokens.source[tokens.position:]}"
)
else:
error_msg = "Expected semicolon (followed by markers) or end of string"
suffix = ""
tokens.raise_syntax_error(message=error_msg, suffix=suffix)
return Requirement(name, url, extras, specifier, marker)


Expand Down
10 changes: 5 additions & 5 deletions packaging/_tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,16 @@ def try_read(self, *name: str) -> Optional[Token]:
return self.read()
return None

def raise_syntax_error(self, *, message: str) -> NoReturn:
def raise_syntax_error(self, *, message: str, suffix: str = "") -> NoReturn:
"""
Raise SyntaxError at the given position in the marker.
"""
at = f"at position {self.position}:"
marker = " " * self.position + "^"
raise ParseExceptionError(
f"{message}\n{at}\n {self.source}\n {marker}",
self.position,
)
message = f"{message}\n{at}\n {self.source}\n {marker}"
if suffix:
message = f"{message}\n{suffix}"
raise ParseExceptionError(message, self.position)

def _make_token(self, name: str, text: str) -> Token:
"""
Expand Down
8 changes: 8 additions & 0 deletions tests/test_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ def test_marker_with_missing_semicolon(self):
Requirement('name[bar]>=3 python_version == "2.7"')
assert "Expected semicolon (followed by markers) or end of string" in str(e)

def test_url_and_marker_with_missing_space_after_semicolon(self):
with pytest.raises(InvalidRequirement) as e:
Requirement('foobar @ https://foo.com; python_version < "3.11"')
assert (
"Expected space before semicolon (followed by markers) or end of string"
in str(e)
)

def test_types(self):
req = Requirement("foobar[quux]<2,>=3; os_name=='a'")
assert isinstance(req.name, str)
Expand Down