Implemented the dynamic scope feature.
This commit is contained in:
parent
404cf0c872
commit
10bf6aac76
1
AUTHORS
1
AUTHORS
|
@ -23,6 +23,7 @@ Andras Tim
|
||||||
Andrea Cimatoribus
|
Andrea Cimatoribus
|
||||||
Andreas Zeidler
|
Andreas Zeidler
|
||||||
Andrey Paramonov
|
Andrey Paramonov
|
||||||
|
Andrzej Klajnert
|
||||||
Andrzej Ostrowski
|
Andrzej Ostrowski
|
||||||
Andy Freeland
|
Andy Freeland
|
||||||
Anthon van der Neut
|
Anthon van der Neut
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them
|
||||||
|
as a keyword argument instead.
|
|
@ -0,0 +1,3 @@
|
||||||
|
The ``scope`` parameter of ``@pytest.fixture`` can now be a callable that receives
|
||||||
|
the fixture name and the ``config`` object as keyword-only parameters.
|
||||||
|
See `the docs <https://docs.pytest.org/en/fixture.html#dynamic-scope>`__ for more information.
|
|
@ -1,7 +1,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture("session")
|
@pytest.fixture(scope="session")
|
||||||
def setup(request):
|
def setup(request):
|
||||||
setup = CostlySetup()
|
setup = CostlySetup()
|
||||||
yield setup
|
yield setup
|
||||||
|
|
|
@ -301,6 +301,32 @@ are finalized when the last test of a *package* finishes.
|
||||||
Use this new feature sparingly and please make sure to report any issues you find.
|
Use this new feature sparingly and please make sure to report any issues you find.
|
||||||
|
|
||||||
|
|
||||||
|
Dynamic scope
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
In some cases, you might want to change the scope of the fixture without changing the code.
|
||||||
|
To do that, pass a callable to ``scope``. The callable must return a string with a valid scope
|
||||||
|
and will be executed only once - during the fixture definition. It will be called with two
|
||||||
|
keyword arguments - ``fixture_name`` as a string and ``config`` with a configuration object.
|
||||||
|
|
||||||
|
This can be especially useful when dealing with fixtures that need time for setup, like spawning
|
||||||
|
a docker container. You can use the command-line argument to control the scope of the spawned
|
||||||
|
containers for different environments. See the example below.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def determine_scope(fixture_name, config):
|
||||||
|
if config.getoption("--keep-containers"):
|
||||||
|
return "session"
|
||||||
|
return "function"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope=determine_scope)
|
||||||
|
def docker_container():
|
||||||
|
yield spawn_container()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Order: Higher-scoped fixtures are instantiated first
|
Order: Higher-scoped fixtures are instantiated first
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -29,3 +29,8 @@ RESULT_LOG = PytestDeprecationWarning(
|
||||||
"--result-log is deprecated and scheduled for removal in pytest 6.0.\n"
|
"--result-log is deprecated and scheduled for removal in pytest 6.0.\n"
|
||||||
"See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information."
|
"See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning(
|
||||||
|
"Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them "
|
||||||
|
"as a keyword argument instead."
|
||||||
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import functools
|
||||||
import inspect
|
import inspect
|
||||||
import itertools
|
import itertools
|
||||||
import sys
|
import sys
|
||||||
|
import warnings
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
@ -27,6 +28,7 @@ from _pytest.compat import getlocation
|
||||||
from _pytest.compat import is_generator
|
from _pytest.compat import is_generator
|
||||||
from _pytest.compat import NOTSET
|
from _pytest.compat import NOTSET
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
|
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
|
|
||||||
|
@ -58,7 +60,6 @@ def pytest_sessionstart(session):
|
||||||
|
|
||||||
scopename2class = {} # type: Dict[str, Type[nodes.Node]]
|
scopename2class = {} # type: Dict[str, Type[nodes.Node]]
|
||||||
|
|
||||||
|
|
||||||
scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]]
|
scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]]
|
||||||
scope2props["package"] = ("fspath",)
|
scope2props["package"] = ("fspath",)
|
||||||
scope2props["module"] = ("fspath", "module")
|
scope2props["module"] = ("fspath", "module")
|
||||||
|
@ -792,6 +793,25 @@ def _teardown_yield_fixture(fixturefunc, it):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _eval_scope_callable(scope_callable, fixture_name, config):
|
||||||
|
try:
|
||||||
|
result = scope_callable(fixture_name=fixture_name, config=config)
|
||||||
|
except Exception:
|
||||||
|
raise TypeError(
|
||||||
|
"Error evaluating {} while defining fixture '{}'.\n"
|
||||||
|
"Expected a function with the signature (*, fixture_name, config)".format(
|
||||||
|
scope_callable, fixture_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not isinstance(result, str):
|
||||||
|
fail(
|
||||||
|
"Expected {} to return a 'str' while defining fixture '{}', but it returned:\n"
|
||||||
|
"{!r}".format(scope_callable, fixture_name, result),
|
||||||
|
pytrace=False,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class FixtureDef:
|
class FixtureDef:
|
||||||
""" A container for a factory definition. """
|
""" A container for a factory definition. """
|
||||||
|
|
||||||
|
@ -811,6 +831,8 @@ class FixtureDef:
|
||||||
self.has_location = baseid is not None
|
self.has_location = baseid is not None
|
||||||
self.func = func
|
self.func = func
|
||||||
self.argname = argname
|
self.argname = argname
|
||||||
|
if callable(scope):
|
||||||
|
scope = _eval_scope_callable(scope, argname, fixturemanager.config)
|
||||||
self.scope = scope
|
self.scope = scope
|
||||||
self.scopenum = scope2index(
|
self.scopenum = scope2index(
|
||||||
scope or "function",
|
scope or "function",
|
||||||
|
@ -986,7 +1008,40 @@ class FixtureFunctionMarker:
|
||||||
return function
|
return function
|
||||||
|
|
||||||
|
|
||||||
def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
FIXTURE_ARGS_ORDER = ("scope", "params", "autouse", "ids", "name")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_fixture_args(callable_or_scope, *args, **kwargs):
|
||||||
|
arguments = dict(scope="function", params=None, autouse=False, ids=None, name=None)
|
||||||
|
|
||||||
|
fixture_function = None
|
||||||
|
if isinstance(callable_or_scope, str):
|
||||||
|
args = list(args)
|
||||||
|
args.insert(0, callable_or_scope)
|
||||||
|
else:
|
||||||
|
fixture_function = callable_or_scope
|
||||||
|
|
||||||
|
positionals = set()
|
||||||
|
for positional, argument_name in zip(args, FIXTURE_ARGS_ORDER):
|
||||||
|
arguments[argument_name] = positional
|
||||||
|
positionals.add(argument_name)
|
||||||
|
|
||||||
|
duplicated_kwargs = {kwarg for kwarg in kwargs.keys() if kwarg in positionals}
|
||||||
|
if duplicated_kwargs:
|
||||||
|
raise TypeError(
|
||||||
|
"The fixture arguments are defined as positional and keyword: {}. "
|
||||||
|
"Use only keyword arguments.".format(", ".join(duplicated_kwargs))
|
||||||
|
)
|
||||||
|
|
||||||
|
if positionals:
|
||||||
|
warnings.warn(FIXTURE_POSITIONAL_ARGUMENTS, stacklevel=2)
|
||||||
|
|
||||||
|
arguments.update(kwargs)
|
||||||
|
|
||||||
|
return fixture_function, arguments
|
||||||
|
|
||||||
|
|
||||||
|
def fixture(callable_or_scope=None, *args, **kwargs):
|
||||||
"""Decorator to mark a fixture factory function.
|
"""Decorator to mark a fixture factory function.
|
||||||
|
|
||||||
This decorator can be used, with or without parameters, to define a
|
This decorator can be used, with or without parameters, to define a
|
||||||
|
@ -1032,21 +1087,33 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
||||||
``fixture_<fixturename>`` and then use
|
``fixture_<fixturename>`` and then use
|
||||||
``@pytest.fixture(name='<fixturename>')``.
|
``@pytest.fixture(name='<fixturename>')``.
|
||||||
"""
|
"""
|
||||||
if callable(scope) and params is None and autouse is False:
|
fixture_function, arguments = _parse_fixture_args(
|
||||||
|
callable_or_scope, *args, **kwargs
|
||||||
|
)
|
||||||
|
scope = arguments.get("scope")
|
||||||
|
params = arguments.get("params")
|
||||||
|
autouse = arguments.get("autouse")
|
||||||
|
ids = arguments.get("ids")
|
||||||
|
name = arguments.get("name")
|
||||||
|
|
||||||
|
if fixture_function and params is None and autouse is False:
|
||||||
# direct decoration
|
# direct decoration
|
||||||
return FixtureFunctionMarker("function", params, autouse, name=name)(scope)
|
return FixtureFunctionMarker(scope, params, autouse, name=name)(
|
||||||
|
fixture_function
|
||||||
|
)
|
||||||
|
|
||||||
if params is not None and not isinstance(params, (list, tuple)):
|
if params is not None and not isinstance(params, (list, tuple)):
|
||||||
params = list(params)
|
params = list(params)
|
||||||
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
|
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
|
||||||
|
|
||||||
|
|
||||||
def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
def yield_fixture(callable_or_scope=None, *args, **kwargs):
|
||||||
""" (return a) decorator to mark a yield-fixture factory function.
|
""" (return a) decorator to mark a yield-fixture factory function.
|
||||||
|
|
||||||
.. deprecated:: 3.0
|
.. deprecated:: 3.0
|
||||||
Use :py:func:`pytest.fixture` directly instead.
|
Use :py:func:`pytest.fixture` directly instead.
|
||||||
"""
|
"""
|
||||||
return fixture(scope=scope, params=params, autouse=autouse, ids=ids, name=name)
|
return fixture(callable_or_scope=callable_or_scope, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
defaultfuncargprefixmarker = fixture()
|
defaultfuncargprefixmarker = fixture()
|
||||||
|
|
|
@ -2216,6 +2216,68 @@ class TestFixtureMarker:
|
||||||
["*ScopeMismatch*You tried*function*session*request*"]
|
["*ScopeMismatch*You tried*function*session*request*"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_dynamic_scope(self, testdir):
|
||||||
|
testdir.makeconftest(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption("--extend-scope", action="store_true", default=False)
|
||||||
|
|
||||||
|
|
||||||
|
def dynamic_scope(fixture_name, config):
|
||||||
|
if config.getoption("--extend-scope"):
|
||||||
|
return "session"
|
||||||
|
return "function"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope=dynamic_scope)
|
||||||
|
def dynamic_fixture(calls=[]):
|
||||||
|
calls.append("call")
|
||||||
|
return len(calls)
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_first(dynamic_fixture):
|
||||||
|
assert dynamic_fixture == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_second(dynamic_fixture):
|
||||||
|
assert dynamic_fixture == 2
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
reprec = testdir.inline_run()
|
||||||
|
reprec.assertoutcome(passed=2)
|
||||||
|
|
||||||
|
reprec = testdir.inline_run("--extend-scope")
|
||||||
|
reprec.assertoutcome(passed=1, failed=1)
|
||||||
|
|
||||||
|
def test_dynamic_scope_bad_return(self, testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def dynamic_scope(**_):
|
||||||
|
return "wrong-scope"
|
||||||
|
|
||||||
|
@pytest.fixture(scope=dynamic_scope)
|
||||||
|
def fixture():
|
||||||
|
pass
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
"Fixture 'fixture' from test_dynamic_scope_bad_return.py "
|
||||||
|
"got an unexpected scope value 'wrong-scope'"
|
||||||
|
)
|
||||||
|
|
||||||
def test_register_only_with_mark(self, testdir):
|
def test_register_only_with_mark(self, testdir):
|
||||||
testdir.makeconftest(
|
testdir.makeconftest(
|
||||||
"""
|
"""
|
||||||
|
@ -4009,3 +4071,54 @@ def test_fixture_named_request(testdir):
|
||||||
" *test_fixture_named_request.py:5",
|
" *test_fixture_named_request.py:5",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_duplicated_arguments(testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
with pytest.raises(TypeError) as excinfo:
|
||||||
|
|
||||||
|
@pytest.fixture("session", scope="session")
|
||||||
|
def arg(arg):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_error():
|
||||||
|
assert (
|
||||||
|
str(excinfo.value)
|
||||||
|
== "The fixture arguments are defined as positional and keyword: scope. "
|
||||||
|
"Use only keyword arguments."
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
reprec = testdir.inline_run()
|
||||||
|
reprec.assertoutcome(passed=1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_with_positionals(testdir):
|
||||||
|
"""Raise warning, but the positionals should still works."""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
|
||||||
|
|
||||||
|
with pytest.warns(pytest.PytestDeprecationWarning) as warnings:
|
||||||
|
@pytest.fixture("function", [0], True)
|
||||||
|
def arg(monkeypatch):
|
||||||
|
monkeypatch.setenv("AUTOUSE_WORKS", "1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_autouse():
|
||||||
|
assert os.environ.get("AUTOUSE_WORKS") == "1"
|
||||||
|
assert str(warnings[0].message) == str(FIXTURE_POSITIONAL_ARGUMENTS)
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
reprec = testdir.inline_run()
|
||||||
|
reprec.assertoutcome(passed=1)
|
||||||
|
|
Loading…
Reference in New Issue