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

View File

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

View File

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