Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement: Base FixtureArgKeys on param values if possible, not param indices #11271

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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
Expand Down Expand Up @@ -156,13 +157,24 @@


@dataclasses.dataclass(frozen=True)
class FixtureArgKey:
class FixtureArgKeyByIndex:
argname: str
param_index: int
scoped_item_path: Optional[Path]
item_cls: Optional[type]


@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]:
Expand Down Expand Up @@ -196,7 +208,15 @@
assert_never(scope)

param_index = cs.indices[argname]
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
param_value = cs.params[argname]

Check warning on line 211 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L211

Added line #L211 was not covered by tests
if isinstance(param_value, Hashable):
yield FixtureArgKeyByValue(

Check warning on line 213 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L213

Added line #L213 was not covered by tests
argname, param_value, scoped_item_path, item_cls
)
else:
yield FixtureArgKeyByIndex( # type: ignore[unreachable]

Check warning on line 217 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L217

Added line #L217 was not covered by tests
argname, param_index, scoped_item_path, item_cls
)


# Algorithm for sorting on a per-parametrized resource setup basis.
Expand Down
199 changes: 199 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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():
Expand Down Expand Up @@ -4534,3 +4535,201 @@ def test_fixt(custom):
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*",
]
)