Skip to content

Commit

Permalink
Added invocation scope
Browse files Browse the repository at this point in the history
  • Loading branch information
niroshaimos committed Oct 5, 2024
1 parent 87569cd commit f014d55
Show file tree
Hide file tree
Showing 4 changed files with 16 additions and 29 deletions.
27 changes: 6 additions & 21 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def get_scope_package(
def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None:
import _pytest.python

if scope is Scope.Function:
if scope is Scope.Function or scope is Scope.Invocation:
# Type ignored because this is actually safe, see:
# https://github.com/python/mypy/issues/4717
return node.getparent(nodes.Item) # type: ignore[type-abstract]
Expand Down Expand Up @@ -185,7 +185,7 @@ def get_parametrized_fixture_argkeys(
) -> Iterator[FixtureArgKey]:
"""Return list of keys for all parametrized arguments which match
the specified scope."""
assert scope is not Scope.Function
assert scope in HIGH_SCOPES

try:
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
Expand Down Expand Up @@ -537,7 +537,7 @@ def getfixturevalue(self, argname: str) -> Any:

if (isinstance(fixturedef, FixtureDef)
and fixturedef is not None
and fixturedef.use_cache is False):
and fixturedef.scope == Scope.Invocation.value):
self._fixture_defs.pop(argname)

return fixturedef.cached_result[0]
Expand Down Expand Up @@ -626,7 +626,7 @@ def _get_active_fixturedef(
finally:
for arg_name in fixturedef.argnames:
arg_fixture = self._fixture_defs.get(arg_name)
if arg_fixture is not None and arg_fixture.use_cache is not True:
if arg_fixture is not None and arg_fixture.scope == Scope.Invocation.value:
self._fixture_defs.pop(arg_name)

return fixturedef
Expand Down Expand Up @@ -769,7 +769,7 @@ def _check_scope(
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
requested_scope: Scope,
) -> None:
if isinstance(requested_fixturedef, PseudoFixtureDef):
if isinstance(requested_fixturedef, PseudoFixtureDef) or requested_scope == Scope.Invocation:
return
if self._scope > requested_scope:
# Try to report something helpful.
Expand Down Expand Up @@ -969,7 +969,6 @@ def __init__(
scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None,
params: Sequence[object] | None,
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
use_cache: bool = True,
*,
_ispytest: bool = False,
) -> None:
Expand Down Expand Up @@ -1017,7 +1016,6 @@ def __init__(
# Can change if the fixture is executed with different parameters.
self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
self._finalizers: Final[list[Callable[[], object]]] = []
self.use_cache = use_cache

@property
def scope(self) -> _ScopeName:
Expand Down Expand Up @@ -1068,7 +1066,7 @@ def execute(self, request: SubRequest) -> FixtureValue:
requested_fixtures_that_should_finalize_us.append(fixturedef)

# Check for (and return) cached value/exception.
if self.cached_result is not None and self.use_cache:
if self.cached_result is not None and self.scope != Scope.Invocation.value:
request_cache_key = self.cache_key(request)
cache_key = self.cached_result[1]
try:
Expand Down Expand Up @@ -1197,7 +1195,6 @@ class FixtureFunctionMarker:
autouse: bool = False
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None
name: str | None = None
cache_result: bool = True

_ispytest: dataclasses.InitVar[bool] = False

Expand Down Expand Up @@ -1240,7 +1237,6 @@ def fixture(
autouse: bool = ...,
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
name: str | None = ...,
cache_result: bool = True,
) -> FixtureFunction: ...


Expand All @@ -1253,7 +1249,6 @@ def fixture(
autouse: bool = ...,
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
name: str | None = None,
cache_result: bool = True,
) -> FixtureFunctionMarker: ...


Expand All @@ -1265,7 +1260,6 @@ def fixture(
autouse: bool = False,
ids: Sequence[object | None] | Callable[[Any], object | None] | None = None,
name: str | None = None,
cache_result: bool = True,
) -> FixtureFunctionMarker | FixtureFunction:
"""Decorator to mark a fixture factory function.
Expand Down Expand Up @@ -1316,11 +1310,6 @@ def fixture(
function arg that requests the fixture; one way to resolve this is to
name the decorated function ``fixture_<fixturename>`` and then use
``@pytest.fixture(name='<fixturename>')``.
:param cache_result:
If True (the default), the fixture result is cached and the fixture
only runs once per scope.
If False, the fixture will run each time it is requested
"""
fixture_marker = FixtureFunctionMarker(
scope=scope,
Expand All @@ -1329,7 +1318,6 @@ def fixture(
ids=None if ids is None else ids if callable(ids) else tuple(ids),
name=name,
_ispytest=True,
cache_result=cache_result
)

# Direct decoration.
Expand Down Expand Up @@ -1660,7 +1648,6 @@ def _register_fixture(
params: Sequence[object] | None = None,
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
autouse: bool = False,
cache_result: bool = True,
) -> None:
"""Register a fixture
Expand Down Expand Up @@ -1691,7 +1678,6 @@ def _register_fixture(
params=params,
ids=ids,
_ispytest=True,
use_cache=cache_result,
)

faclist = self._arg2fixturedefs.setdefault(name, [])
Expand Down Expand Up @@ -1788,7 +1774,6 @@ def parsefactories(
params=marker.params,
ids=marker.ids,
autouse=marker.autouse,
cache_result=marker.cache_result
)

def getfixturedefs(
Expand Down
5 changes: 3 additions & 2 deletions src/_pytest/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from typing import Literal


_ScopeName = Literal["session", "package", "module", "class", "function"]
_ScopeName = Literal["session", "package", "module", "class", "function", "invocation"]


@total_ordering
Expand All @@ -33,6 +33,7 @@ class Scope(Enum):
"""

# Scopes need to be listed from lower to higher.
Invocation: _ScopeName = "invocation"
Function: _ScopeName = "function"
Class: _ScopeName = "class"
Module: _ScopeName = "module"
Expand Down Expand Up @@ -88,4 +89,4 @@ def from_user(


# Ordered list of scopes which can contain many tests (in practice all except Function).
HIGH_SCOPES = [x for x in Scope if x is not Scope.Function]
HIGH_SCOPES = [x for x in Scope if x is not Scope.Function and x is not Scope.Invocation]
8 changes: 4 additions & 4 deletions testing/test_no_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def test_setup_teardown_executed_for_every_fixture_usage_without_caching(pyteste
import pytest
import logging
@pytest.fixture(cache_result=False)
@pytest.fixture(scope="invocation")
def fixt():
logging.info("&&Setting up fixt&&")
yield
Expand Down Expand Up @@ -42,7 +42,7 @@ def test_setup_teardown_executed_for_every_getfixturevalue_usage_without_caching
import pytest
import logging
@pytest.fixture(cache_result=False)
@pytest.fixture(scope="invocation")
def fixt():
logging.info("&&Setting up fixt&&")
yield
Expand All @@ -67,7 +67,7 @@ def test_non_cached_fixture_generates_unique_values_per_usage(pytester: Pytester
"""
import pytest
@pytest.fixture(cache_result=False)
@pytest.fixture(scope="invocation")
def random_num():
import random
return random.randint(-100_000_000_000, 100_000_000_000)
Expand All @@ -94,7 +94,7 @@ def test_non_cached_fixture_generates_unique_values_per_getfixturevalue_usage(py
"""
import pytest
@pytest.fixture(cache_result=False)
@pytest.fixture(scope="invocation")
def random_num():
import random
yield random.randint(-100_000_000_000, 100_000_000_000)
Expand Down
5 changes: 3 additions & 2 deletions testing/test_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ def test_next_lower() -> None:
assert Scope.Package.next_lower() is Scope.Module
assert Scope.Module.next_lower() is Scope.Class
assert Scope.Class.next_lower() is Scope.Function
assert Scope.Function.next_lower() is Scope.Invocation

with pytest.raises(ValueError, match="Function is the lower-most scope"):
Scope.Function.next_lower()
with pytest.raises(ValueError, match="Invocation is the lower-most scope"):
Scope.Invocation.next_lower()


def test_next_higher() -> None:
Expand Down

0 comments on commit f014d55

Please sign in to comment.