Add `pytest_markeval_namespace` hook.

Add a new hook , `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
This commit is contained in:
Pedro Algarvio 2020-08-27 17:52:16 +01:00 committed by Ran Benita
parent 902739cfc3
commit b16c091253
4 changed files with 209 additions and 0 deletions

View File

@ -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

View File

@ -808,6 +808,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 # error handling and internal debugging hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------

View File

@ -3,6 +3,7 @@ import os
import platform import platform
import sys import sys
import traceback import traceback
from collections.abc import Mapping
from typing import Generator from typing import Generator
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
@ -98,6 +99,16 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
"platform": platform, "platform": platform,
"config": item.config, "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"): if hasattr(item, "obj"):
globals_.update(item.obj.__globals__) # type: ignore[attr-defined] globals_.update(item.obj.__globals__) # type: ignore[attr-defined]
try: try:

View File

@ -1,4 +1,5 @@
import sys import sys
import textwrap
import pytest import pytest
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
@ -155,6 +156,136 @@ class TestEvaluation:
assert skipped assert skipped
assert skipped.reason == "condition: config._hackxyz" 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: class TestXFail:
@pytest.mark.parametrize("strict", [True, False]) @pytest.mark.parametrize("strict", [True, False])
@ -577,6 +708,33 @@ class TestXFail:
result.stdout.fnmatch_lines(["*1 failed*" if strict else "*1 xpassed*"]) result.stdout.fnmatch_lines(["*1 failed*" if strict else "*1 xpassed*"])
assert result.ret == (1 if strict else 0) 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: class TestXFailwithSetupTeardown:
def test_failing_setup_issue9(self, pytester: Pytester) -> None: def test_failing_setup_issue9(self, pytester: Pytester) -> None: