Skip to content

Commit b7f2edb

Browse files
committed
Add public API function pytest.match_markexpr for marker expression evaluation
- Introduced match_markexpr to match marker expressions against markers or pytest Items. - Updated documentation to reflect the new function. - Added comprehensive tests to ensure functionality and error handling. Closes #13737
1 parent 09a9c6a commit b7f2edb

File tree

5 files changed

+108
-0
lines changed

5 files changed

+108
-0
lines changed

changelog/13759.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added public API function `pytest.match_markexpr` to match marker expressions against a set of markers or a pytest Item.
2+
This function is useful for plugins and test code that need to evaluate marker expressions without relying on internal APIs.
3+
Insuring that the public API is properly tested and documented.

doc/en/how-to/mark.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,27 @@ enforce this validation in your project by adding ``--strict-markers`` to ``addo
9191
markers =
9292
slow: marks tests as slow (deselect with '-m "not slow"')
9393
serial
94+
95+
96+
Check if marks match an expression
97+
----------------------------------
98+
99+
You can check if a set of marks or a pytest Item matches a marker expression
100+
using the public API function :func:`pytest.match_markexpr`.
101+
102+
.. code-block:: python
103+
104+
import pytest
105+
106+
107+
def test_example():
108+
item = ... # some pytest Item
109+
assert pytest.match_markexpr("smoke and not slow", item)
110+
111+
112+
def test_example2():
113+
marks = ["smoke", "fast"]
114+
assert pytest.match_markexpr("smoke and not slow", marks)
115+
116+
This function is useful for plugins and test code that need to evaluate marker expressions
117+
without relying on internal APIs.

src/_pytest/mark/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"MarkGenerator",
4242
"ParameterSet",
4343
"get_empty_parameterset_mark",
44+
"match_markexpr",
4445
]
4546

4647

@@ -298,3 +299,28 @@ def pytest_configure(config: Config) -> None:
298299

299300
def pytest_unconfigure(config: Config) -> None:
300301
MARK_GEN._config = config.stash.get(old_mark_config_key, None)
302+
303+
304+
def match_markexpr(expr: str, mark: str | Iterable[str] | object) -> bool:
305+
try:
306+
compiled = Expression.compile(expr)
307+
except ParseError as e:
308+
raise e
309+
310+
# Build an iterable of Mark objects for the matcher
311+
if isinstance(mark, object) and hasattr(mark, "iter_markers"):
312+
markers_iter = mark.iter_markers()
313+
elif isinstance(mark, str):
314+
markers_iter = (Mark(mark, (), {}, _ispytest=True),)
315+
elif isinstance(mark, Iterable) and not isinstance(mark, (str, bytes, bytearray)):
316+
markers_iter = (Mark(name, (), {}, _ispytest=True) for name in mark)
317+
318+
else:
319+
raise UsageError(
320+
"target must be an Item-like object, a marker name string, "
321+
"or an iterable of marker names"
322+
)
323+
324+
# MarkMatcher is defined in this module (_pytest.mark)
325+
matcher = MarkMatcher.from_markers(markers_iter)
326+
return compiled.evaluate(matcher)

src/pytest/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from _pytest.mark import MARK_GEN as mark
3939
from _pytest.mark import MarkDecorator
4040
from _pytest.mark import MarkGenerator
41+
from _pytest.mark import match_markexpr
4142
from _pytest.mark import param
4243
from _pytest.monkeypatch import MonkeyPatch
4344
from _pytest.nodes import Collector
@@ -170,6 +171,7 @@
170171
"importorskip",
171172
"main",
172173
"mark",
174+
"match_markexpr",
173175
"param",
174176
"raises",
175177
"register_assert_rewrite",

testing/test_match_markexpr.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# testing/test_match_markexpr.py
2+
from __future__ import annotations
3+
4+
from _pytest.config.exceptions import UsageError
5+
from _pytest.mark.expression import ParseError
6+
import pytest
7+
8+
9+
def test_public_api_present_match_markexpr() -> None:
10+
assert hasattr(pytest, "match_markexpr")
11+
assert callable(pytest.match_markexpr)
12+
13+
14+
def test_strings_markexpr() -> None:
15+
assert pytest.match_markexpr("smoke or slow", "slow")
16+
assert not pytest.match_markexpr("smoke and slow", "smoke")
17+
18+
19+
def test_iterables_markexpr() -> None:
20+
assert pytest.match_markexpr("smoke and not slow", ["smoke"])
21+
assert not pytest.match_markexpr("smoke and not slow", ["smoke", "slow"])
22+
assert pytest.match_markexpr("a and b", ["a", "b"])
23+
assert not pytest.match_markexpr("a and b", ["a"])
24+
25+
26+
def test_invalid_expression_raises() -> None:
27+
with pytest.raises(
28+
ParseError, match="expected not OR left parenthesis OR identifier; got and"
29+
):
30+
pytest.match_markexpr("smoke and and slow", ["smoke"])
31+
32+
33+
def test_type_error_for_unsupported_target() -> None:
34+
with pytest.raises(
35+
UsageError,
36+
match="target must be an Item-like object, a marker name string, or an iterable of marker names",
37+
):
38+
pytest.match_markexpr("smoke", 123)
39+
40+
41+
def test_item_matching_with_request_node(request: pytest.FixtureRequest) -> None:
42+
request.config.addinivalue_line("markers", "smoke: test marker")
43+
request.config.addinivalue_line("markers", "slow: test marker")
44+
45+
# Use the current test item; add/remove marks dynamically.
46+
request.node.add_marker("smoke")
47+
assert pytest.match_markexpr("smoke and not slow", request.node)
48+
assert not pytest.match_markexpr("slow", request.node)
49+
50+
# Add another mark and re-check
51+
request.node.add_marker("slow")
52+
assert not pytest.match_markexpr("smoke and not slow", request.node)
53+
assert pytest.match_markexpr("smoke and slow", request.node)

0 commit comments

Comments
 (0)