Skip to content

Commit 106a8fc

Browse files
committed
mark/expression: documentation/naming tweaks
Make the module a little nicer ahead of possibly exposing it publicly.
1 parent 8d68586 commit 106a8fc

File tree

2 files changed

+41
-13
lines changed

2 files changed

+41
-13
lines changed

src/_pytest/mark/expression.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
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

2323
from __future__ import annotations
@@ -65,7 +65,7 @@ class Token:
6565

6666

6767
class 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
269292
class 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:
280303
class 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)))

testing/test_mark_expression.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
from _pytest.mark import MarkMatcher
44
from _pytest.mark.expression import Expression
5-
from _pytest.mark.expression import MatcherCall
5+
from _pytest.mark.expression import ExpressionMatcher
66
from _pytest.mark.expression import ParseError
77
import pytest
88

99

10-
def evaluate(input: str, matcher: MatcherCall) -> bool:
10+
def evaluate(input: str, matcher: ExpressionMatcher) -> bool:
1111
return Expression.compile(input).evaluate(matcher)
1212

1313

0 commit comments

Comments
 (0)