Implement the feature

This commit is contained in:
Sadra Barikbin 2023-08-02 01:51:01 +03:30
parent 485c555812
commit cab7a0e9d4
3 changed files with 249 additions and 20 deletions

View File

@ -15,6 +15,7 @@ from typing import Final
from typing import final
from typing import Generator
from typing import Generic
from typing import Hashable
from typing import Iterable
from typing import Iterator
from typing import List
@ -239,11 +240,28 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
)
# Parametrized fixture key, helper alias for code below.
_Key = Tuple[object, ...]
@dataclasses.dataclass(frozen=True)
class FixtureArgKeyByIndex:
argname: str
param_index: int
scoped_item_path: Optional[Path]
item_cls: Optional[type]
def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_Key]:
@dataclasses.dataclass(frozen=True)
class FixtureArgKeyByValue:
argname: str
param_value: Hashable
scoped_item_path: Optional[Path]
item_cls: Optional[type]
FixtureArgKey = Union[FixtureArgKeyByIndex, FixtureArgKeyByValue]
def get_parametrized_fixture_keys(
item: nodes.Item, scope: Scope
) -> Iterator[FixtureArgKey]:
"""Return list of keys for all parametrized arguments which match
the specified scope."""
assert scope is not Scope.Function
@ -256,21 +274,33 @@ def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_K
# cs.indices.items() is random order of argnames. Need to
# sort this so that different calls to
# get_parametrized_fixture_keys will be deterministic.
for argname, param_index in sorted(cs.indices.items()):
for argname in sorted(cs.indices):
if cs._arg2scope[argname] != scope:
continue
item_cls = None
if scope is Scope.Session:
key: _Key = (argname, param_index)
scoped_item_path = None
elif scope is Scope.Package:
key = (argname, param_index, item.path)
scoped_item_path = item.path.parent
elif scope is Scope.Module:
key = (argname, param_index, item.path)
scoped_item_path = item.path
elif scope is Scope.Class:
scoped_item_path = item.path
item_cls = item.cls # type: ignore[attr-defined]
key = (argname, param_index, item.path, item_cls)
else:
assert_never(scope)
yield key
param_index = cs.indices[argname]
param_value = cs.params[argname]
if isinstance(param_value, Hashable):
yield FixtureArgKeyByValue(
argname, param_value, scoped_item_path, item_cls
)
else:
yield FixtureArgKeyByIndex( # type: ignore[unreachable]
argname, param_index, scoped_item_path, item_cls
)
# Algorithm for sorting on a per-parametrized resource setup basis.
@ -280,12 +310,12 @@ def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_K
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]] = {}
items_by_argkey: Dict[Scope, Dict[_Key, Deque[nodes.Item]]] = {}
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
for scope in HIGH_SCOPES:
d: Dict[nodes.Item, Dict[_Key, None]] = {}
d: Dict[nodes.Item, Dict[FixtureArgKey, None]] = {}
argkeys_cache[scope] = d
item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque)
item_d: Dict[FixtureArgKey, Deque[nodes.Item]] = defaultdict(deque)
items_by_argkey[scope] = item_d
for item in items:
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
@ -301,8 +331,8 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
def fix_cache_order(
item: nodes.Item,
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
) -> None:
for scope in HIGH_SCOPES:
for key in argkeys_cache[scope].get(item, []):
@ -311,13 +341,13 @@ def fix_cache_order(
def reorder_items_atscope(
items: Dict[nodes.Item, None],
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
scope: Scope,
) -> Dict[nodes.Item, None]:
if scope is Scope.Function or len(items) < 3:
return items
ignore: Set[Optional[_Key]] = set()
ignore: Set[Optional[FixtureArgKey]] = set()
items_deque = deque(items)
items_done: Dict[nodes.Item, None] = {}
scoped_items_by_argkey = items_by_argkey[scope]

View File

@ -22,13 +22,13 @@ def checked_order():
assert order == [
("issue_519.py", "fix1", "arg1v1"),
("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"),
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"),
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"),
("issue_519.py", "fix1", "arg1v2"),
("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"),
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"),
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
("test_two[arg1v2-arg2v2]", "fix2", "arg2v2"),
]

View File

@ -12,6 +12,7 @@ from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import get_public_names
from _pytest.pytester import Pytester
from _pytest.python import Function
from _pytest.scope import Scope
def test_getfuncargnames_functions():
@ -4536,3 +4537,201 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None:
result.assert_outcomes(errors=1)
result.stdout.fnmatch_lines([expected])
assert result.ret == ExitCode.TESTS_FAILED
@pytest.mark.parametrize("scope", ["module", "package"])
def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices(
scope,
pytester: Pytester,
):
package = pytester.mkdir("package")
package.joinpath("__init__.py").write_text("", encoding="utf-8")
package.joinpath("test_a.py").write_text(
textwrap.dedent(
f"""\
import pytest
@pytest.fixture(scope='{scope}')
def fixture1(request):
pass
@pytest.mark.parametrize("fixture1", [1, 0], indirect=True)
def test_0(fixture1):
pass
@pytest.mark.parametrize("fixture1", [2, 1], indirect=True)
def test_1(fixture1):
pass
def test_2():
pass
@pytest.mark.parametrize("param", [0, 1, 2], scope='{scope}')
def test_3(param):
pass
@pytest.mark.parametrize("param", [2, 1, 0], scope='{scope}')
def test_4(param):
pass
"""
),
encoding="utf-8",
)
result = pytester.runpytest("--collect-only")
result.stdout.re_match_lines(
[
r" <Function test_0\[1\]>",
r" <Function test_1\[1\]>",
r" <Function test_0\[0\]>",
r" <Function test_1\[2\]>",
r" <Function test_2>",
r" <Function test_3\[0\]>",
r" <Function test_4\[0\]>",
r" <Function test_3\[1\]>",
r" <Function test_4\[1\]>",
r" <Function test_3\[2\]>",
r" <Function test_4\[2\]>",
]
)
def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices_2(
pytester: Pytester,
):
pytester.makepyfile(
"""
import pytest
@pytest.fixture(scope='module')
def fixture1(request):
pass
@pytest.fixture(scope='module')
def fixture2(request):
pass
@pytest.mark.parametrize("fixture1, fixture2", [("a", 0), ("b", 1), ("a", 2)], indirect=True)
def test_1(fixture1, fixture2):
pass
@pytest.mark.parametrize("fixture1, fixture2", [("c", 4), ("a", 3)], indirect=True)
def test_2(fixture1, fixture2):
pass
def test_3():
pass
@pytest.mark.parametrize("param1, param2", [("a", 0), ("b", 1), ("a", 2)], scope='module')
def test_4(param1, param2):
pass
@pytest.mark.parametrize("param1, param2", [("c", 4), ("a", 3)], scope='module')
def test_5(param1, param2):
pass
"""
)
result = pytester.runpytest("--collect-only")
result.stdout.re_match_lines(
[
r" <Function test_1\[a-0\]>",
r" <Function test_1\[a-2\]>",
r" <Function test_2\[a-3\]>",
r" <Function test_1\[b-1\]>",
r" <Function test_2\[c-4\]>",
r" <Function test_3>",
r" <Function test_4\[a-0\]>",
r" <Function test_4\[a-2\]>",
r" <Function test_5\[a-3\]>",
r" <Function test_4\[b-1\]>",
r" <Function test_5\[c-4\]>",
]
)
@pytest.mark.xfail(
reason="It isn't differentiated between direct `fixture` param and fixture `fixture`. Will be"
"solved by adding `baseid` to `FixtureArgKey`."
)
def test_reorder_with_high_scoped_direct_and_fixture_parametrization(
pytester: Pytester,
):
pytester.makepyfile(
"""
import pytest
@pytest.fixture(params=[0, 1], scope='module')
def fixture(request):
pass
def test_1(fixture):
pass
def test_2():
pass
@pytest.mark.parametrize("fixture", [1, 2], scope='module')
def test_3(fixture):
pass
"""
)
result = pytester.runpytest("--collect-only")
result.stdout.re_match_lines(
[
r" <Function test_1\[0\]>",
r" <Function test_1\[1\]>",
r" <Function test_2>",
r" <Function test_3\[1\]>",
r" <Function test_3\[2\]>",
]
)
def test_get_parametrized_fixture_keys_with_unhashable_params(
pytester: Pytester,
) -> None:
module = pytester.makepyfile(
"""
import pytest
@pytest.mark.parametrize("arg", [[1], [2]], scope='module')
def test(arg):
pass
"""
)
test_0, test_1 = pytester.genitems((pytester.getmodulecol(module),))
test_0_keys = list(fixtures.get_parametrized_fixture_keys(test_0, Scope.Module))
test_1_keys = list(fixtures.get_parametrized_fixture_keys(test_1, Scope.Module))
assert len(test_0_keys) == len(test_1_keys) == 1
assert isinstance(test_0_keys[0], fixtures.FixtureArgKeyByIndex)
assert test_0_keys[0].param_index == 0
assert isinstance(test_1_keys[0], fixtures.FixtureArgKeyByIndex)
assert test_1_keys[0].param_index == 1
def test_reordering_with_unhashable_parametrize_args(pytester: Pytester) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.mark.parametrize("arg", [[1], [2]], scope='module')
def test_1(arg):
print(arg)
def test_2():
print("test_2")
@pytest.mark.parametrize("arg", [[3], [4]], scope='module')
def test_3(arg):
print(arg)
"""
)
result = pytester.runpytest("-s")
result.stdout.fnmatch_lines(
[
r"*1*",
r"*3*",
r"*2*",
r"*4*",
r"*test_2*",
]
)