diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 39d262c80..c1770c197 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -99,18 +99,18 @@ class Scanner: elif input[pos] == ",": yield Token(TokenType.COMMA, ",", pos) pos += 1 - elif (quote_char := input[pos]) == "'" or input[pos] == '"': - quote_position = input[pos + 1 :].find(quote_char) - if quote_position == -1: + elif (quote_char := input[pos]) in ("'", '"'): + end_quote_pos = input.find(quote_char, pos + 1) + if end_quote_pos == -1: raise ParseError( pos + 1, f'closing quote "{quote_char}" is missing', ) - value = input[pos : pos + 2 + quote_position] - if "\\" in value: + value = input[pos : end_quote_pos + 1] + if (backslash_pos := input.find("\\")) != -1: raise ParseError( - pos + 1, - "escaping not supported in marker expression", + backslash_pos + 1, + r'escaping with "\" not supported in marker expression', ) yield Token(TokenType.STRING, value, pos) pos += len(value) @@ -218,10 +218,15 @@ BUILTIN_MATCHERS = {"True": True, "False": False, "None": None} def single_kwarg(s: Scanner) -> ast.keyword: keyword_name = s.accept(TokenType.IDENT, reject=True) - if not keyword_name.value.isidentifier() or keyword.iskeyword(keyword_name.value): + if not keyword_name.value.isidentifier(): raise ParseError( keyword_name.pos + 1, - f'unexpected character/s "{keyword_name.value}"', + f"not a valid python identifier {keyword_name.value}", + ) + if keyword.iskeyword(keyword_name.value): + raise ParseError( + keyword_name.pos + 1, + f"unexpected reserved python keyword `{keyword_name.value}`", ) s.accept(TokenType.EQUAL, reject=True) diff --git a/testing/test_mark_expression.py b/testing/test_mark_expression.py index e7ecaa7db..3c42cc967 100644 --- a/testing/test_mark_expression.py +++ b/testing/test_mark_expression.py @@ -207,25 +207,29 @@ def test_invalid_idents(ident: str) -> None: @pytest.mark.parametrize( "expr, expected_error_msg", ( - ("mark(1=2)", 'unexpected character/s "1"'), - ("mark(/=2)", 'unexpected character/s "/"'), - ("mark(True=False)", 'unexpected character/s "True"'), - ("mark(def=False)", 'unexpected character/s "def"'), - ("mark(class=False)", 'unexpected character/s "class"'), - ("mark(if=False)", 'unexpected character/s "if"'), - ("mark(else=False)", 'unexpected character/s "else"'), - ("mark(1)", 'unexpected character/s "1"'), - ("mark(var:=False", 'unexpected character/s "var:"'), - ("mark(valid=False, def=1)", 'unexpected character/s "def"'), + ("mark(True=False)", "unexpected reserved python keyword `True`"), + ("mark(def=False)", "unexpected reserved python keyword `def`"), + ("mark(class=False)", "unexpected reserved python keyword `class`"), + ("mark(if=False)", "unexpected reserved python keyword `if`"), + ("mark(else=False)", "unexpected reserved python keyword `else`"), + ("mark(valid=False, def=1)", "unexpected reserved python keyword `def`"), + ("mark(1)", "not a valid python identifier 1"), + ("mark(var:=False", "not a valid python identifier var:"), + ("mark(1=2)", "not a valid python identifier 1"), + ("mark(/=2)", "not a valid python identifier /"), ("mark(var==", "expected identifier; got ="), + ("mark(var)", "expected =; got right parenthesis"), ("mark(var=none)", 'unexpected character/s "none"'), ("mark(var=1.1)", 'unexpected character/s "1.1"'), - ("mark(var)", "expected =; got right parenthesis"), ("mark(var=')", """closing quote "'" is missing"""), ('mark(var=")', 'closing quote """ is missing'), ("""mark(var="')""", 'closing quote """ is missing'), ("""mark(var='")""", """closing quote "'" is missing"""), - (r"mark(var='\hugo')", "escaping not supported in marker expression"), + ( + r"mark(var='\hugo')", + r'escaping with "\\" not supported in marker expression', + ), + ("mark(empty_list=[])", r'unexpected character/s "\[\]"'), ), ) def test_invalid_kwarg_name_or_value( # TODO: move to `test_syntax_errors` ?