Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/13759.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added public API function `pytest.match_markexpr` to match marker expressions against a set of markers or a pytest Item.
This function is useful for plugins and test code that need to evaluate marker expressions without relying on internal APIs.
Insuring that the public API is properly tested and documented.
25 changes: 25 additions & 0 deletions doc/en/example/markers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -757,3 +757,28 @@ or to select both "event" and "interface" tests:
FAILED test_module.py::test_interface_complex - assert 0
FAILED test_module.py::test_event_simple - assert 0
===================== 3 failed, 1 deselected in 0.12s ======================

.. _match_markexpr:

Checking expression matches against markers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Evaluate a marker expression against a pytest Item or a set of marker names.
.. versionadded:: 8.4

.. code-block:: python

import pytest


def test_example():
item = ... # some pytest Item
assert pytest.match_markexpr("smoke and not slow", item)


def test_example2():
marks = ["smoke", "fast"]
assert pytest.match_markexpr("smoke and not slow", marks)

This function is useful for plugins and test code that need to evaluate marker expressions
without relying on internal APIs.
28 changes: 28 additions & 0 deletions doc/en/how-to/mark.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,31 @@ enforce this validation in your project by adding ``--strict-markers`` to ``addo
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
serial


Check if marks match an expression without test collection
----------------------------------------------------------

You can check if a set of marks or a pytest Item matches a marker expression
using the public API function :func:`pytest.match_markexpr`.

.. code-block:: python

import pytest


def test_example():
item = ... # some pytest Item
assert pytest.match_markexpr("smoke and not slow", item)


def test_example2():
marks = ["smoke", "fast"]
assert pytest.match_markexpr("smoke and not slow", marks)


def test_example3(requests):
assert not pytest.match_markexpr("smoke and not slow", requests.node)

This function is useful for plugins and test code that need to evaluate marker expressions
without relying on internal APIs.
10 changes: 9 additions & 1 deletion doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,19 @@ pytest.warns
:with:

pytest.freeze_includes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~

**Tutorial**: :ref:`freezing-pytest`

.. autofunction:: pytest.freeze_includes

pytest.match_markexpr
~~~~~~~~~~~~~~~~~~~~~

**Tutorial**: :ref:`match_markexpr`

.. autofunction:: pytest.match_markexpr

.. _`marks ref`:

Marks
Expand Down Expand Up @@ -289,6 +296,7 @@ Marks a test function as *expected to fail*.
Defaults to :confval:`xfail_strict`, which is ``False`` by default.



Custom marks
~~~~~~~~~~~~

Expand Down
40 changes: 40 additions & 0 deletions src/_pytest/mark/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"MarkGenerator",
"ParameterSet",
"get_empty_parameterset_mark",
"match_markexpr",
]


Expand Down Expand Up @@ -298,3 +299,42 @@ def pytest_configure(config: Config) -> None:

def pytest_unconfigure(config: Config) -> None:
MARK_GEN._config = config.stash.get(old_mark_config_key, None)


def match_markexpr(expr: str, mark: str | Iterable[str] | object) -> bool:
"""
Match a marker expression against a marker name, an iterable of marker names,
or an Item-like object.

:param expr:
The marker expression to evaluate.
:param mark:
A marker name string, an iterable of marker names, or an Item-like object.
:return:
True if the expression matches, False otherwise.
:raises UsageError:
If the target type is unsupported.

"""
try:
compiled = Expression.compile(expr)
except ParseError as e:
raise e

# Build an iterable of Mark objects for the matcher
if isinstance(mark, object) and hasattr(mark, "iter_markers"):
markers_iter = mark.iter_markers()
elif isinstance(mark, str):
markers_iter = (Mark(mark, (), {}, _ispytest=True),)
elif isinstance(mark, Iterable) and not isinstance(mark, (str, bytes, bytearray)):
markers_iter = (Mark(name, (), {}, _ispytest=True) for name in mark)

else:
raise UsageError(
"target must be an Item-like object, a marker name string, "
"or an iterable of marker names"
)

# MarkMatcher is defined in this module (_pytest.mark)
matcher = MarkMatcher.from_markers(markers_iter)
return compiled.evaluate(matcher)
2 changes: 2 additions & 0 deletions src/pytest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from _pytest.mark import MARK_GEN as mark
from _pytest.mark import MarkDecorator
from _pytest.mark import MarkGenerator
from _pytest.mark import match_markexpr
from _pytest.mark import param
from _pytest.monkeypatch import MonkeyPatch
from _pytest.nodes import Collector
Expand Down Expand Up @@ -170,6 +171,7 @@
"importorskip",
"main",
"mark",
"match_markexpr",
"param",
"raises",
"register_assert_rewrite",
Expand Down
53 changes: 53 additions & 0 deletions testing/test_match_markexpr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# testing/test_match_markexpr.py
from __future__ import annotations

from _pytest.config.exceptions import UsageError
from _pytest.mark.expression import ParseError
import pytest


def test_public_api_present_match_markexpr() -> None:
assert hasattr(pytest, "match_markexpr")
assert callable(pytest.match_markexpr)


def test_strings_markexpr() -> None:
assert pytest.match_markexpr("smoke or slow", "slow")
assert not pytest.match_markexpr("smoke and slow", "smoke")


def test_iterables_markexpr() -> None:
assert pytest.match_markexpr("smoke and not slow", ["smoke"])
assert not pytest.match_markexpr("smoke and not slow", ["smoke", "slow"])
assert pytest.match_markexpr("a and b", ["a", "b"])
assert not pytest.match_markexpr("a and b", ["a"])


def test_invalid_expression_raises() -> None:
with pytest.raises(
ParseError, match="expected not OR left parenthesis OR identifier; got and"
):
pytest.match_markexpr("smoke and and slow", ["smoke"])


def test_type_error_for_unsupported_target() -> None:
with pytest.raises(
UsageError,
match="target must be an Item-like object, a marker name string, or an iterable of marker names",
):
pytest.match_markexpr("smoke", 123)


def test_item_matching_with_request_node(request: pytest.FixtureRequest) -> None:
request.config.addinivalue_line("markers", "smoke: test marker")
request.config.addinivalue_line("markers", "slow: test marker")

# Use the current test item; add/remove marks dynamically.
request.node.add_marker("smoke")
assert pytest.match_markexpr("smoke and not slow", request.node)
assert not pytest.match_markexpr("slow", request.node)

# Add another mark and re-check
request.node.add_marker("slow")
assert not pytest.match_markexpr("smoke and not slow", request.node)
assert pytest.match_markexpr("smoke and slow", request.node)