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
@@ -65,7 +65,7 @@ class Token:
6565
6666
6767class ParseError (Exception ):
68- """The expression contains invalid syntax.
68+ """The :class:`Expression` contains invalid syntax.
6969
7070 :param column: The column in the line where the error occurred (1-based).
7171 :param message: A description of the error.
@@ -261,13 +261,36 @@ def all_kwargs(s: Scanner) -> list[ast.keyword]:
261261 return ret
262262
263263
264- class MatcherCall (Protocol ):
264+ class ExpressionMatcher (Protocol ):
265+ """A callable which, given an identifier and optional kwargs, should return
266+ whether it matches in an :class:`Expression` evaluation.
267+
268+ Should be prepared to handle arbitrary strings as input.
269+
270+ If no kwargs are provided, the expression of the form `foo`.
271+ If kwargs are provided, the expression is of the form `foo(1, b=True, "s")`.
272+
273+ If the expression is not supported (e.g. don't want to accept the kwargs
274+ syntax variant), should raise :class:`~pytest.UsageError`.
275+
276+ Example::
277+
278+ def matcher(name: str, /, **kwargs: str | int | bool | None) -> bool:
279+ # Match `cat`.
280+ if name == "cat" and not kwargs:
281+ return True
282+ # Match `dog(barks=True)`.
283+ if name == "dog" and kwargs == {"barks": False}:
284+ return True
285+ return False
286+ """
287+
265288 def __call__ (self , name : str , / , ** kwargs : str | int | bool | None ) -> bool : ...
266289
267290
268291@dataclasses .dataclass
269292class MatcherNameAdapter :
270- matcher : MatcherCall
293+ matcher : ExpressionMatcher
271294 name : str
272295
273296 def __bool__ (self ) -> bool :
@@ -280,7 +303,7 @@ def __call__(self, **kwargs: str | int | bool | None) -> bool:
280303class MatcherAdapter (Mapping [str , MatcherNameAdapter ]):
281304 """Adapts a matcher function to a locals mapping as required by eval()."""
282305
283- def __init__ (self , matcher : MatcherCall ) -> None :
306+ def __init__ (self , matcher : ExpressionMatcher ) -> None :
284307 self .matcher = matcher
285308
286309 def __getitem__ (self , key : str ) -> MatcherNameAdapter :
@@ -309,23 +332,28 @@ def compile(cls, input: str) -> Expression:
309332 """Compile a match expression.
310333
311334 :param input: The input expression - one line.
335+
336+ :raises ParseError: If the expression is malformed.
312337 """
313338 astexpr = expression (Scanner (input ))
314- code : types . CodeType = compile (
339+ code = compile (
315340 astexpr ,
316341 filename = "<pytest match expression>" ,
317342 mode = "eval" ,
318343 )
319344 return Expression (code )
320345
321- def evaluate (self , matcher : MatcherCall ) -> bool :
346+ def evaluate (self , matcher : ExpressionMatcher ) -> bool :
322347 """Evaluate the match expression.
323348
324349 :param matcher:
325- Given an identifier, should return whether it matches or not.
326- Should be prepared to handle arbitrary strings as input .
350+ A callback which determines whether an identifier matches or not.
351+ See the :class:`ExpressionMatcher` protocol for details and example .
327352
328353 :returns: Whether the expression matches or not.
354+
355+ :raises UsageError:
356+ If the matcher doesn't support the expression. Cannot happen if the
357+ matcher supports all expressions.
329358 """
330- ret : bool = bool (eval (self .code , {"__builtins__" : {}}, MatcherAdapter (matcher )))
331- return ret
359+ return bool (eval (self .code , {"__builtins__" : {}}, MatcherAdapter (matcher )))
0 commit comments