diff --git a/src/packaging/_parser.py b/src/packaging/_parser.py index c1238c06..aea29323 100644 --- a/src/packaging/_parser.py +++ b/src/packaging/_parser.py @@ -111,7 +111,9 @@ def _parse_requirement_details( return (url, specifier, marker) marker = _parse_requirement_marker( - tokenizer, span_start=url_start, after="URL and whitespace" + tokenizer, + span_start=url_start, + expected="semicolon (after URL and whitespace)", ) else: specifier_start = tokenizer.position @@ -124,10 +126,10 @@ def _parse_requirement_details( marker = _parse_requirement_marker( tokenizer, span_start=specifier_start, - after=( - "version specifier" + expected=( + "comma (within version specifier), semicolon (after version specifier)" if specifier - else "name and no valid version specifier" + else "semicolon (after name with no version specifier)" ), ) @@ -135,7 +137,7 @@ def _parse_requirement_details( def _parse_requirement_marker( - tokenizer: Tokenizer, *, span_start: int, after: str + tokenizer: Tokenizer, *, span_start: int, expected: str ) -> MarkerList: """ requirement_marker = SEMICOLON marker WS? @@ -143,8 +145,9 @@ def _parse_requirement_marker( if not tokenizer.check("SEMICOLON"): tokenizer.raise_syntax_error( - f"Expected end or semicolon (after {after})", + f"Expected {expected} or end", span_start=span_start, + span_end=None, ) tokenizer.read() diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 9907b9c9..23d6995e 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -373,7 +373,7 @@ def test_error_no_space_after_url(self) -> None: # THEN assert ctx.exconly() == ( "packaging.requirements.InvalidRequirement: " - "Expected end or semicolon (after URL and whitespace)\n" + "Expected semicolon (after URL and whitespace) or end\n" " name @ https://example.com/; extra == 'example'\n" " ~~~~~~~~~~~~~~~~~~~~~~^" ) @@ -521,7 +521,8 @@ def test_error_on_legacy_version_outside_triple_equals(self) -> None: # THEN assert ctx.exconly() == ( "packaging.requirements.InvalidRequirement: " - "Expected end or semicolon (after version specifier)\n" + "Expected comma (within version specifier), " + "semicolon (after version specifier) or end\n" " name==1.0.org1\n" " ~~~~~^" ) @@ -537,7 +538,7 @@ def test_error_on_missing_version_after_op(self) -> None: # THEN assert ctx.exconly() == ( "packaging.requirements.InvalidRequirement: " - "Expected end or semicolon (after name and no valid version specifier)\n" + "Expected semicolon (after name with no version specifier) or end\n" " name==\n" " ^" ) @@ -553,7 +554,7 @@ def test_error_on_missing_op_after_name(self) -> None: # THEN assert ctx.exconly() == ( "packaging.requirements.InvalidRequirement: " - "Expected end or semicolon (after name and no valid version specifier)\n" + "Expected semicolon (after name with no version specifier) or end\n" " name 1.0\n" " ^" ) @@ -569,11 +570,29 @@ def test_error_on_random_char_after_specifier(self) -> None: # THEN assert ctx.exconly() == ( "packaging.requirements.InvalidRequirement: " - "Expected end or semicolon (after version specifier)\n" + "Expected comma (within version specifier), " + "semicolon (after version specifier) or end\n" " name >= 1.0 #\n" " ~~~~~~~^" ) + def test_error_on_missing_comma_in_specifier(self) -> None: + # GIVEN + to_parse = "name >= 1.0 <= 2.0" + + # WHEN + with pytest.raises(InvalidRequirement) as ctx: + Requirement(to_parse) + + # THEN + assert ctx.exconly() == ( + "packaging.requirements.InvalidRequirement: " + "Expected comma (within version specifier), " + "semicolon (after version specifier) or end\n" + " name >= 1.0 <= 2.0\n" + " ~~~~~~~^" + ) + class TestRequirementBehaviour: def test_types_with_nothing(self) -> None: