diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 159ff2cd1..babcd9e2f 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -80,7 +80,7 @@ keyword arguments, e.g. to run only tests marked with ``device`` and the specifi .. code-block:: pytest - $ pytest -v -m 'device(serial="123")' + $ pytest -v -m "device(serial='123')" =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache diff --git a/doc/en/how-to/usage.rst b/doc/en/how-to/usage.rst index 05ee04600..0e0a0310f 100644 --- a/doc/en/how-to/usage.rst +++ b/doc/en/how-to/usage.rst @@ -88,7 +88,7 @@ with the ``phase`` keyword argument set to ``1``: .. code-block:: bash - pytest -m slow(phase=1) + pytest -m "slow(phase=1)" For more information see :ref:`marks `. diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 3f4071dce..89cc0e94d 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -5,15 +5,19 @@ The grammar is: expression: expr? EOF expr: and_expr ('or' and_expr)* and_expr: not_expr ('and' not_expr)* -not_expr: 'not' not_expr | '(' expr ')' | ident ( '(' name '=' value ( ', ' name '=' value )* ')')* +not_expr: 'not' not_expr | '(' expr ')' | ident kwargs? ident: (\w|:|\+|-|\.|\[|\]|\\|/)+ +kwargs: ('(' name '=' value ( ', ' name '=' value )* ')') +name: a valid ident, but not a reserved keyword +value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None' The semantics are: - Empty expression evaluates to False. -- ident evaluates to True of False according to a provided matcher function. +- ident evaluates to True or False according to a provided matcher function. - or/and/not evaluate according to the usual boolean semantics. +- ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function. """ from __future__ import annotations @@ -48,7 +52,7 @@ class TokenType(enum.Enum): IDENT = "identifier" EOF = "end of input" EQUAL = "=" - STRING = "str" + STRING = "string literal" COMMA = "," diff --git a/testing/test_mark.py b/testing/test_mark.py index 6a94cc9f7..89eef7920 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -235,7 +235,7 @@ def test_mark_option( @pytest.mark.parametrize( ("expr", "expected_passed"), - [ # TODO: improve/sort out + [ ("car(color='red')", ["test_one"]), ("car(color='red') or car(color='blue')", ["test_one", "test_two"]), ("car and not car(temp=5)", ["test_one", "test_three"]), diff --git a/testing/test_mark_expression.py b/testing/test_mark_expression.py index c31ab4470..f8f5f9221 100644 --- a/testing/test_mark_expression.py +++ b/testing/test_mark_expression.py @@ -228,9 +228,10 @@ def test_invalid_idents(ident: str) -> None: r'escaping with "\\" not supported in marker expression', ), ("mark(empty_list=[])", r'unexpected character/s "\[\]"'), + ("'str'", "expected not OR left parenthesis OR identifier; got string literal"), ), ) -def test_invalid_kwarg_name_or_value( # TODO: move to `test_syntax_errors` ? +def test_invalid_kwarg_name_or_value( expr: str, expected_error_msg: str, mark_matcher: MarkMatcher ) -> None: with pytest.raises(ParseError, match=expected_error_msg): @@ -289,7 +290,7 @@ def test_keyword_expressions_with_numbers( ("builtin_matchers_mark(z=1)", False), ), ) -def test_builtin_matchers_keyword_expressions( # TODO: naming when decided +def test_builtin_matchers_keyword_expressions( expr: str, expected: bool, mark_matcher: MarkMatcher ) -> None: assert evaluate(expr, mark_matcher) is expected