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
31
31
import keyword
32
32
import re
33
33
import types
34
+ from typing import Final
35
+ from typing import final
34
36
from typing import Literal
35
37
from typing import NoReturn
36
38
from typing import overload
@@ -65,7 +67,7 @@ class Token:
65
67
66
68
67
69
class ParseError (Exception ):
68
- """The expression contains invalid syntax.
70
+ """The :class:`Expression` contains invalid syntax.
69
71
70
72
:param column: The column in the line where the error occurred (1-based).
71
73
:param message: A description of the error.
@@ -261,13 +263,36 @@ def all_kwargs(s: Scanner) -> list[ast.keyword]:
261
263
return ret
262
264
263
265
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
+
265
290
def __call__ (self , name : str , / , ** kwargs : str | int | bool | None ) -> bool : ...
266
291
267
292
268
293
@dataclasses .dataclass
269
294
class MatcherNameAdapter :
270
- matcher : MatcherCall
295
+ matcher : ExpressionMatcher
271
296
name : str
272
297
273
298
def __bool__ (self ) -> bool :
@@ -280,7 +305,7 @@ def __call__(self, **kwargs: str | int | bool | None) -> bool:
280
305
class MatcherAdapter (Mapping [str , MatcherNameAdapter ]):
281
306
"""Adapts a matcher function to a locals mapping as required by eval()."""
282
307
283
- def __init__ (self , matcher : MatcherCall ) -> None :
308
+ def __init__ (self , matcher : ExpressionMatcher ) -> None :
284
309
self .matcher = matcher
285
310
286
311
def __getitem__ (self , key : str ) -> MatcherNameAdapter :
@@ -293,39 +318,47 @@ def __len__(self) -> int:
293
318
raise NotImplementedError ()
294
319
295
320
321
+ @final
296
322
class Expression :
297
323
"""A compiled match expression as used by -k and -m.
298
324
299
325
The expression can be evaluated against different matchers.
300
326
"""
301
327
302
- __slots__ = ("code" , )
328
+ __slots__ = ("_code" , "input" )
303
329
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
306
334
307
335
@classmethod
308
336
def compile (cls , input : str ) -> Expression :
309
337
"""Compile a match expression.
310
338
311
339
:param input: The input expression - one line.
340
+
341
+ :raises ParseError: If the expression is malformed.
312
342
"""
313
343
astexpr = expression (Scanner (input ))
314
- code : types . CodeType = compile (
344
+ code = compile (
315
345
astexpr ,
316
346
filename = "<pytest match expression>" ,
317
347
mode = "eval" ,
318
348
)
319
- return Expression (code )
349
+ return Expression (input , code )
320
350
321
- def evaluate (self , matcher : MatcherCall ) -> bool :
351
+ def evaluate (self , matcher : ExpressionMatcher ) -> bool :
322
352
"""Evaluate the match expression.
323
353
324
354
: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 .
327
357
328
358
: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.
329
363
"""
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