16
16
17
17
- Empty expression evaluates to False.
18
18
- ident evaluates to True or False according to a provided matcher function.
19
- - or/and/not evaluate according to the usual boolean semantics.
20
19
- 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.
21
21
"""
22
22
23
23
from __future__ import annotations
@@ -65,7 +65,7 @@ class Token:
65
65
66
66
67
67
class ParseError (Exception ):
68
- """The expression contains invalid syntax.
68
+ """The :class:`Expression` contains invalid syntax.
69
69
70
70
:param column: The column in the line where the error occurred (1-based).
71
71
:param message: A description of the error.
@@ -261,13 +261,36 @@ def all_kwargs(s: Scanner) -> list[ast.keyword]:
261
261
return ret
262
262
263
263
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
+
265
288
def __call__ (self , name : str , / , ** kwargs : str | int | bool | None ) -> bool : ...
266
289
267
290
268
291
@dataclasses .dataclass
269
292
class MatcherNameAdapter :
270
- matcher : MatcherCall
293
+ matcher : ExpressionMatcher
271
294
name : str
272
295
273
296
def __bool__ (self ) -> bool :
@@ -280,7 +303,7 @@ def __call__(self, **kwargs: str | int | bool | None) -> bool:
280
303
class MatcherAdapter (Mapping [str , MatcherNameAdapter ]):
281
304
"""Adapts a matcher function to a locals mapping as required by eval()."""
282
305
283
- def __init__ (self , matcher : MatcherCall ) -> None :
306
+ def __init__ (self , matcher : ExpressionMatcher ) -> None :
284
307
self .matcher = matcher
285
308
286
309
def __getitem__ (self , key : str ) -> MatcherNameAdapter :
@@ -309,23 +332,28 @@ def compile(cls, input: str) -> Expression:
309
332
"""Compile a match expression.
310
333
311
334
:param input: The input expression - one line.
335
+
336
+ :raises ParseError: If the expression is malformed.
312
337
"""
313
338
astexpr = expression (Scanner (input ))
314
- code : types . CodeType = compile (
339
+ code = compile (
315
340
astexpr ,
316
341
filename = "<pytest match expression>" ,
317
342
mode = "eval" ,
318
343
)
319
344
return Expression (code )
320
345
321
- def evaluate (self , matcher : MatcherCall ) -> bool :
346
+ def evaluate (self , matcher : ExpressionMatcher ) -> bool :
322
347
"""Evaluate the match expression.
323
348
324
349
: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 .
327
352
328
353
: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.
329
358
"""
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