summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changelog/7695.feature.rst19
-rw-r--r--src/_pytest/hookspec.py21
-rw-r--r--src/_pytest/skipping.py11
-rw-r--r--testing/test_skipping.py158
4 files changed, 209 insertions, 0 deletions
diff --git a/changelog/7695.feature.rst b/changelog/7695.feature.rst
new file mode 100644
index 000000000..ec8632fc8
--- /dev/null
+++ b/changelog/7695.feature.rst
@@ -0,0 +1,19 @@
+A new hook was added, `pytest_markeval_namespace` which should return a dictionary.
+This dictionary will be used to augment the "global" variables available to evaluate skipif/xfail/xpass markers.
+
+Pseudo example
+
+``conftest.py``:
+
+.. code-block:: python
+
+ def pytest_markeval_namespace():
+ return {"color": "red"}
+
+``test_func.py``:
+
+.. code-block:: python
+
+ @pytest.mark.skipif("color == 'blue'", reason="Color is not red")
+ def test_func():
+ assert False
diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py
index 33ca782cf..e499b742c 100644
--- a/src/_pytest/hookspec.py
+++ b/src/_pytest/hookspec.py
@@ -809,6 +809,27 @@ def pytest_warning_recorded(
# -------------------------------------------------------------------------
+# Hooks for influencing skipping
+# -------------------------------------------------------------------------
+
+
+def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
+ """Called when constructing the globals dictionary used for
+ evaluating string conditions in xfail/skipif markers.
+
+ This is useful when the condition for a marker requires
+ objects that are expensive or impossible to obtain during
+ collection time, which is required by normal boolean
+ conditions.
+
+ .. versionadded:: 6.2
+
+ :param _pytest.config.Config config: The pytest config object.
+ :returns: A dictionary of additional globals to add.
+ """
+
+
+# -------------------------------------------------------------------------
# error handling and internal debugging hooks
# -------------------------------------------------------------------------
diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py
index afc3610eb..9aacfecee 100644
--- a/src/_pytest/skipping.py
+++ b/src/_pytest/skipping.py
@@ -3,6 +3,7 @@ import os
import platform
import sys
import traceback
+from collections.abc import Mapping
from typing import Generator
from typing import Optional
from typing import Tuple
@@ -98,6 +99,16 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
"platform": platform,
"config": item.config,
}
+ for dictionary in reversed(
+ item.ihook.pytest_markeval_namespace(config=item.config)
+ ):
+ if not isinstance(dictionary, Mapping):
+ raise ValueError(
+ "pytest_markeval_namespace() needs to return a dict, got {!r}".format(
+ dictionary
+ )
+ )
+ globals_.update(dictionary)
if hasattr(item, "obj"):
globals_.update(item.obj.__globals__) # type: ignore[attr-defined]
try:
diff --git a/testing/test_skipping.py b/testing/test_skipping.py
index cfc0cdbca..fc66eb18e 100644
--- a/testing/test_skipping.py
+++ b/testing/test_skipping.py
@@ -1,4 +1,5 @@
import sys
+import textwrap
import pytest
from _pytest.pytester import Pytester
@@ -155,6 +156,136 @@ class TestEvaluation:
assert skipped
assert skipped.reason == "condition: config._hackxyz"
+ def test_skipif_markeval_namespace(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"color": "green"}
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.skipif("color == 'green'")
+ def test_1():
+ assert True
+
+ @pytest.mark.skipif("color == 'red'")
+ def test_2():
+ assert True
+ """
+ )
+ res = pytester.runpytest(p)
+ assert res.ret == 0
+ res.stdout.fnmatch_lines(["*1 skipped*"])
+ res.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_skipif_markeval_namespace_multiple(self, pytester: Pytester) -> None:
+ """Keys defined by ``pytest_markeval_namespace()`` in nested plugins override top-level ones."""
+ root = pytester.mkdir("root")
+ root.joinpath("__init__.py").touch()
+ root.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"arg": "root"}
+ """
+ )
+ )
+ root.joinpath("test_root.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.mark.skipif("arg == 'root'")
+ def test_root():
+ assert False
+ """
+ )
+ )
+ foo = root.joinpath("foo")
+ foo.mkdir()
+ foo.joinpath("__init__.py").touch()
+ foo.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"arg": "foo"}
+ """
+ )
+ )
+ foo.joinpath("test_foo.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.mark.skipif("arg == 'foo'")
+ def test_foo():
+ assert False
+ """
+ )
+ )
+ bar = root.joinpath("bar")
+ bar.mkdir()
+ bar.joinpath("__init__.py").touch()
+ bar.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"arg": "bar"}
+ """
+ )
+ )
+ bar.joinpath("test_bar.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.mark.skipif("arg == 'bar'")
+ def test_bar():
+ assert False
+ """
+ )
+ )
+
+ reprec = pytester.inline_run("-vs", "--capture=no")
+ reprec.assertoutcome(skipped=3)
+
+ def test_skipif_markeval_namespace_ValueError(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_markeval_namespace():
+ return True
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.skipif("color == 'green'")
+ def test_1():
+ assert True
+ """
+ )
+ res = pytester.runpytest(p)
+ assert res.ret == 1
+ res.stdout.fnmatch_lines(
+ [
+ "*ValueError: pytest_markeval_namespace() needs to return a dict, got True*"
+ ]
+ )
+
class TestXFail:
@pytest.mark.parametrize("strict", [True, False])
@@ -577,6 +708,33 @@ class TestXFail:
result.stdout.fnmatch_lines(["*1 failed*" if strict else "*1 xpassed*"])
assert result.ret == (1 if strict else 0)
+ def test_xfail_markeval_namespace(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"color": "green"}
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.xfail("color == 'green'")
+ def test_1():
+ assert False
+
+ @pytest.mark.xfail("color == 'red'")
+ def test_2():
+ assert False
+ """
+ )
+ res = pytester.runpytest(p)
+ assert res.ret == 1
+ res.stdout.fnmatch_lines(["*1 failed*"])
+ res.stdout.fnmatch_lines(["*1 xfailed*"])
+
class TestXFailwithSetupTeardown:
def test_failing_setup_issue9(self, pytester: Pytester) -> None: