Skip to content

Commit dced00e

Browse files
authored
Merge pull request #9154 from bluetech/refactor-callspec2
python: refactor CallSpec2
2 parents c4557c3 + 570b1fa commit dced00e

File tree

1 file changed

+84
-56
lines changed

1 file changed

+84
-56
lines changed

src/_pytest/python.py

Lines changed: 84 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
from typing import TYPE_CHECKING
2828
from typing import Union
2929

30+
import attr
31+
3032
import _pytest
3133
from _pytest import fixtures
3234
from _pytest import nodes
@@ -37,6 +39,7 @@
3739
from _pytest._io import TerminalWriter
3840
from _pytest._io.saferepr import saferepr
3941
from _pytest.compat import ascii_escaped
42+
from _pytest.compat import assert_never
4043
from _pytest.compat import final
4144
from _pytest.compat import get_default_arg_names
4245
from _pytest.compat import get_real_func
@@ -451,11 +454,12 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
451454
module = modulecol.obj
452455
clscol = self.getparent(Class)
453456
cls = clscol and clscol.obj or None
454-
fm = self.session._fixturemanager
455457

456458
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
457459
fixtureinfo = definition._fixtureinfo
458460

461+
# pytest_generate_tests impls call metafunc.parametrize() which fills
462+
# metafunc._calls, the outcome of the hook.
459463
metafunc = Metafunc(
460464
definition=definition,
461465
fixtureinfo=fixtureinfo,
@@ -469,13 +473,13 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
469473
methods.append(module.pytest_generate_tests)
470474
if cls is not None and hasattr(cls, "pytest_generate_tests"):
471475
methods.append(cls().pytest_generate_tests)
472-
473476
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
474477

475478
if not metafunc._calls:
476479
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
477480
else:
478481
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
482+
fm = self.session._fixturemanager
479483
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
480484

481485
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
@@ -894,61 +898,75 @@ def hasnew(obj: object) -> bool:
894898

895899

896900
@final
901+
@attr.s(frozen=True, slots=True, auto_attribs=True)
897902
class CallSpec2:
898-
def __init__(self, metafunc: "Metafunc") -> None:
899-
self.metafunc = metafunc
900-
self.funcargs: Dict[str, object] = {}
901-
self._idlist: List[str] = []
902-
self.params: Dict[str, object] = {}
903-
# Used for sorting parametrized resources.
904-
self._arg2scope: Dict[str, Scope] = {}
905-
self.marks: List[Mark] = []
906-
self.indices: Dict[str, int] = {}
907-
908-
def copy(self) -> "CallSpec2":
909-
cs = CallSpec2(self.metafunc)
910-
cs.funcargs.update(self.funcargs)
911-
cs.params.update(self.params)
912-
cs.marks.extend(self.marks)
913-
cs.indices.update(self.indices)
914-
cs._arg2scope.update(self._arg2scope)
915-
cs._idlist = list(self._idlist)
916-
return cs
917-
918-
def getparam(self, name: str) -> object:
919-
try:
920-
return self.params[name]
921-
except KeyError as e:
922-
raise ValueError(name) from e
903+
"""A planned parameterized invocation of a test function.
923904
924-
@property
925-
def id(self) -> str:
926-
return "-".join(map(str, self._idlist))
905+
Calculated during collection for a given test function's Metafunc.
906+
Once collection is over, each callspec is turned into a single Item
907+
and stored in item.callspec.
908+
"""
927909

928-
def setmulti2(
910+
# arg name -> arg value which will be passed to the parametrized test
911+
# function (direct parameterization).
912+
funcargs: Dict[str, object] = attr.Factory(dict)
913+
# arg name -> arg value which will be passed to a fixture of the same name
914+
# (indirect parametrization).
915+
params: Dict[str, object] = attr.Factory(dict)
916+
# arg name -> arg index.
917+
indices: Dict[str, int] = attr.Factory(dict)
918+
# Used for sorting parametrized resources.
919+
_arg2scope: Dict[str, Scope] = attr.Factory(dict)
920+
# Parts which will be added to the item's name in `[..]` separated by "-".
921+
_idlist: List[str] = attr.Factory(list)
922+
# Marks which will be applied to the item.
923+
marks: List[Mark] = attr.Factory(list)
924+
925+
def setmulti(
929926
self,
927+
*,
930928
valtypes: Mapping[str, "Literal['params', 'funcargs']"],
931-
argnames: Sequence[str],
929+
argnames: Iterable[str],
932930
valset: Iterable[object],
933931
id: str,
934932
marks: Iterable[Union[Mark, MarkDecorator]],
935933
scope: Scope,
936934
param_index: int,
937-
) -> None:
935+
) -> "CallSpec2":
936+
funcargs = self.funcargs.copy()
937+
params = self.params.copy()
938+
indices = self.indices.copy()
939+
arg2scope = self._arg2scope.copy()
938940
for arg, val in zip(argnames, valset):
939-
if arg in self.params or arg in self.funcargs:
941+
if arg in params or arg in funcargs:
940942
raise ValueError(f"duplicate {arg!r}")
941943
valtype_for_arg = valtypes[arg]
942944
if valtype_for_arg == "params":
943-
self.params[arg] = val
945+
params[arg] = val
944946
elif valtype_for_arg == "funcargs":
945-
self.funcargs[arg] = val
946-
else: # pragma: no cover
947-
assert False, f"Unhandled valtype for arg: {valtype_for_arg}"
948-
self.indices[arg] = param_index
949-
self._arg2scope[arg] = scope
950-
self._idlist.append(id)
951-
self.marks.extend(normalize_mark_list(marks))
947+
funcargs[arg] = val
948+
else:
949+
assert_never(valtype_for_arg)
950+
indices[arg] = param_index
951+
arg2scope[arg] = scope
952+
return CallSpec2(
953+
funcargs=funcargs,
954+
params=params,
955+
arg2scope=arg2scope,
956+
indices=indices,
957+
idlist=[*self._idlist, id],
958+
marks=[*self.marks, *normalize_mark_list(marks)],
959+
)
960+
961+
def getparam(self, name: str) -> object:
962+
try:
963+
return self.params[name]
964+
except KeyError as e:
965+
raise ValueError(name) from e
966+
967+
@property
968+
def id(self) -> str:
969+
return "-".join(self._idlist)
952970

953971

954972
@final
@@ -990,9 +1008,11 @@ def __init__(
9901008
#: Class object where the test function is defined in or ``None``.
9911009
self.cls = cls
9921010

993-
self._calls: List[CallSpec2] = []
9941011
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
9951012

1013+
# Result of parametrize().
1014+
self._calls: List[CallSpec2] = []
1015+
9961016
def parametrize(
9971017
self,
9981018
argnames: Union[str, List[str], Tuple[str, ...]],
@@ -1009,9 +1029,18 @@ def parametrize(
10091029
_param_mark: Optional[Mark] = None,
10101030
) -> None:
10111031
"""Add new invocations to the underlying test function using the list
1012-
of argvalues for the given argnames. Parametrization is performed
1013-
during the collection phase. If you need to setup expensive resources
1014-
see about setting indirect to do it rather at test setup time.
1032+
of argvalues for the given argnames. Parametrization is performed
1033+
during the collection phase. If you need to setup expensive resources
1034+
see about setting indirect to do it rather than at test setup time.
1035+
1036+
Can be called multiple times, in which case each call parametrizes all
1037+
previous parametrizations, e.g.
1038+
1039+
::
1040+
1041+
unparametrized: t
1042+
parametrize ["x", "y"]: t[x], t[y]
1043+
parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2]
10151044
10161045
:param argnames:
10171046
A comma-separated string denoting one or more argument names, or
@@ -1104,17 +1133,16 @@ def parametrize(
11041133
# more than once) then we accumulate those calls generating the cartesian product
11051134
# of all calls.
11061135
newcalls = []
1107-
for callspec in self._calls or [CallSpec2(self)]:
1136+
for callspec in self._calls or [CallSpec2()]:
11081137
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
1109-
newcallspec = callspec.copy()
1110-
newcallspec.setmulti2(
1111-
arg_values_types,
1112-
argnames,
1113-
param_set.values,
1114-
param_id,
1115-
param_set.marks,
1116-
scope_,
1117-
param_index,
1138+
newcallspec = callspec.setmulti(
1139+
valtypes=arg_values_types,
1140+
argnames=argnames,
1141+
valset=param_set.values,
1142+
id=param_id,
1143+
marks=param_set.marks,
1144+
scope=scope_,
1145+
param_index=param_index,
11181146
)
11191147
newcalls.append(newcallspec)
11201148
self._calls = newcalls

0 commit comments

Comments
 (0)