1616
1717- Empty expression evaluates to False.
1818- ident evaluates to True or False according to a provided matcher function.
19- - or/and/not evaluate according to the usual boolean semantics.
2019- ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function.
20+ - or/and/not evaluate according to the usual boolean semantics.
2121"""
2222
2323from __future__ import annotations
3131import keyword
3232import re
3333import types
34+ from typing import Final
35+ from typing import final
3436from typing import Literal
3537from typing import NoReturn
3638from typing import overload
@@ -65,7 +67,7 @@ class Token:
6567
6668
6769class ParseError (Exception ):
68- """The expression contains invalid syntax.
70+ """The :class:`Expression` contains invalid syntax.
6971
7072 :param column: The column in the line where the error occurred (1-based).
7173 :param message: A description of the error.
@@ -261,13 +263,36 @@ def all_kwargs(s: Scanner) -> list[ast.keyword]:
261263 return ret
262264
263265
264- class MatcherCall (Protocol ):
266+ class ExpressionMatcher (Protocol ):
267+ """A callable which, given an identifier and optional kwargs, should return
268+ whether it matches in an :class:`Expression` evaluation.
269+
270+ Should be prepared to handle arbitrary strings as input.
271+
272+ If no kwargs are provided, the expression of the form `foo`.
273+ If kwargs are provided, the expression is of the form `foo(1, b=True, "s")`.
274+
275+ If the expression is not supported (e.g. don't want to accept the kwargs
276+ syntax variant), should raise :class:`~pytest.UsageError`.
277+
278+ Example::
279+
280+ def matcher(name: str, /, **kwargs: str | int | bool | None) -> bool:
281+ # Match `cat`.
282+ if name == "cat" and not kwargs:
283+ return True
284+ # Match `dog(barks=True)`.
285+ if name == "dog" and kwargs == {"barks": False}:
286+ return True
287+ return False
288+ """
289+
265290 def __call__ (self , name : str , / , ** kwargs : str | int | bool | None ) -> bool : ...
266291
267292
268293@dataclasses .dataclass
269294class MatcherNameAdapter :
270- matcher : MatcherCall
295+ matcher : ExpressionMatcher
271296 name : str
272297
273298 def __bool__ (self ) -> bool :
@@ -280,7 +305,7 @@ def __call__(self, **kwargs: str | int | bool | None) -> bool:
280305class MatcherAdapter (Mapping [str , MatcherNameAdapter ]):
281306 """Adapts a matcher function to a locals mapping as required by eval()."""
282307
283- def __init__ (self , matcher : MatcherCall ) -> None :
308+ def __init__ (self , matcher : ExpressionMatcher ) -> None :
284309 self .matcher = matcher
285310
286311 def __getitem__ (self , key : str ) -> MatcherNameAdapter :
@@ -293,39 +318,47 @@ def __len__(self) -> int:
293318 raise NotImplementedError ()
294319
295320
321+ @final
296322class Expression :
297323 """A compiled match expression as used by -k and -m.
298324
299325 The expression can be evaluated against different matchers.
300326 """
301327
302- __slots__ = ("code" , )
328+ __slots__ = ("_code" , "input" )
303329
304- def __init__ (self , code : types .CodeType ) -> None :
305- self .code = code
330+ def __init__ (self , input : str , code : types .CodeType ) -> None :
331+ #: The original input line, as a string.
332+ self .input : Final = input
333+ self ._code : Final = code
306334
307335 @classmethod
308336 def compile (cls , input : str ) -> Expression :
309337 """Compile a match expression.
310338
311339 :param input: The input expression - one line.
340+
341+ :raises ParseError: If the expression is malformed.
312342 """
313343 astexpr = expression (Scanner (input ))
314- code : types . CodeType = compile (
344+ code = compile (
315345 astexpr ,
316346 filename = "<pytest match expression>" ,
317347 mode = "eval" ,
318348 )
319- return Expression (code )
349+ return Expression (input , code )
320350
321- def evaluate (self , matcher : MatcherCall ) -> bool :
351+ def evaluate (self , matcher : ExpressionMatcher ) -> bool :
322352 """Evaluate the match expression.
323353
324354 :param matcher:
325- Given an identifier, should return whether it matches or not.
326- Should be prepared to handle arbitrary strings as input .
355+ A callback which determines whether an identifier matches or not.
356+ See the :class:`ExpressionMatcher` protocol for details and example .
327357
328358 :returns: Whether the expression matches or not.
359+
360+ :raises UsageError:
361+ If the matcher doesn't support the expression. Cannot happen if the
362+ matcher supports all expressions.
329363 """
330- ret : bool = bool (eval (self .code , {"__builtins__" : {}}, MatcherAdapter (matcher )))
331- return ret
364+ return bool (eval (self ._code , {"__builtins__" : {}}, MatcherAdapter (matcher )))
0 commit comments