Implement the feature
This commit is contained in:
parent
485c555812
commit
cab7a0e9d4
|
@ -15,6 +15,7 @@ from typing import Final
|
||||||
from typing import final
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
|
from typing import Hashable
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -239,11 +240,28 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Parametrized fixture key, helper alias for code below.
|
@dataclasses.dataclass(frozen=True)
|
||||||
_Key = Tuple[object, ...]
|
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
|
"""Return list of keys for all parametrized arguments which match
|
||||||
the specified scope."""
|
the specified scope."""
|
||||||
assert scope is not Scope.Function
|
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
|
# cs.indices.items() is random order of argnames. Need to
|
||||||
# sort this so that different calls to
|
# sort this so that different calls to
|
||||||
# get_parametrized_fixture_keys will be deterministic.
|
# 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:
|
if cs._arg2scope[argname] != scope:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
item_cls = None
|
||||||
if scope is Scope.Session:
|
if scope is Scope.Session:
|
||||||
key: _Key = (argname, param_index)
|
scoped_item_path = None
|
||||||
elif scope is Scope.Package:
|
elif scope is Scope.Package:
|
||||||
key = (argname, param_index, item.path)
|
scoped_item_path = item.path.parent
|
||||||
elif scope is Scope.Module:
|
elif scope is Scope.Module:
|
||||||
key = (argname, param_index, item.path)
|
scoped_item_path = item.path
|
||||||
elif scope is Scope.Class:
|
elif scope is Scope.Class:
|
||||||
|
scoped_item_path = item.path
|
||||||
item_cls = item.cls # type: ignore[attr-defined]
|
item_cls = item.cls # type: ignore[attr-defined]
|
||||||
key = (argname, param_index, item.path, item_cls)
|
|
||||||
else:
|
else:
|
||||||
assert_never(scope)
|
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.
|
# 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]:
|
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]] = {}
|
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
|
||||||
items_by_argkey: Dict[Scope, Dict[_Key, Deque[nodes.Item]]] = {}
|
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
|
||||||
for scope in HIGH_SCOPES:
|
for scope in HIGH_SCOPES:
|
||||||
d: Dict[nodes.Item, Dict[_Key, None]] = {}
|
d: Dict[nodes.Item, Dict[FixtureArgKey, None]] = {}
|
||||||
argkeys_cache[scope] = d
|
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
|
items_by_argkey[scope] = item_d
|
||||||
for item in items:
|
for item in items:
|
||||||
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
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(
|
def fix_cache_order(
|
||||||
item: nodes.Item,
|
item: nodes.Item,
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
|
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
||||||
items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
|
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
|
||||||
) -> None:
|
) -> None:
|
||||||
for scope in HIGH_SCOPES:
|
for scope in HIGH_SCOPES:
|
||||||
for key in argkeys_cache[scope].get(item, []):
|
for key in argkeys_cache[scope].get(item, []):
|
||||||
|
@ -311,13 +341,13 @@ def fix_cache_order(
|
||||||
|
|
||||||
def reorder_items_atscope(
|
def reorder_items_atscope(
|
||||||
items: Dict[nodes.Item, None],
|
items: Dict[nodes.Item, None],
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
|
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
||||||
items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
|
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
) -> Dict[nodes.Item, None]:
|
) -> Dict[nodes.Item, None]:
|
||||||
if scope is Scope.Function or len(items) < 3:
|
if scope is Scope.Function or len(items) < 3:
|
||||||
return items
|
return items
|
||||||
ignore: Set[Optional[_Key]] = set()
|
ignore: Set[Optional[FixtureArgKey]] = set()
|
||||||
items_deque = deque(items)
|
items_deque = deque(items)
|
||||||
items_done: Dict[nodes.Item, None] = {}
|
items_done: Dict[nodes.Item, None] = {}
|
||||||
scoped_items_by_argkey = items_by_argkey[scope]
|
scoped_items_by_argkey = items_by_argkey[scope]
|
||||||
|
|
|
@ -22,13 +22,13 @@ def checked_order():
|
||||||
assert order == [
|
assert order == [
|
||||||
("issue_519.py", "fix1", "arg1v1"),
|
("issue_519.py", "fix1", "arg1v1"),
|
||||||
("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"),
|
("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"),
|
||||||
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
|
|
||||||
("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"),
|
("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"),
|
||||||
|
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
|
||||||
("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"),
|
("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"),
|
||||||
("issue_519.py", "fix1", "arg1v2"),
|
("issue_519.py", "fix1", "arg1v2"),
|
||||||
("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"),
|
("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"),
|
||||||
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
|
|
||||||
("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"),
|
("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"),
|
||||||
|
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
|
||||||
("test_two[arg1v2-arg2v2]", "fix2", "arg2v2"),
|
("test_two[arg1v2-arg2v2]", "fix2", "arg2v2"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.pytester import get_public_names
|
from _pytest.pytester import get_public_names
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
from _pytest.python import Function
|
from _pytest.python import Function
|
||||||
|
from _pytest.scope import Scope
|
||||||
|
|
||||||
|
|
||||||
def test_getfuncargnames_functions():
|
def test_getfuncargnames_functions():
|
||||||
|
@ -4536,3 +4537,201 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None:
|
||||||
result.assert_outcomes(errors=1)
|
result.assert_outcomes(errors=1)
|
||||||
result.stdout.fnmatch_lines([expected])
|
result.stdout.fnmatch_lines([expected])
|
||||||
assert result.ret == ExitCode.TESTS_FAILED
|
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*",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue