Merge pull request #9154 from bluetech/refactor-callspec2
python: refactor CallSpec2
This commit is contained in:
commit
dced00e60f
|
@ -27,6 +27,8 @@ from typing import Tuple
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
import _pytest
|
import _pytest
|
||||||
from _pytest import fixtures
|
from _pytest import fixtures
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
|
@ -37,6 +39,7 @@ from _pytest._code.code import TerminalRepr
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.compat import ascii_escaped
|
from _pytest.compat import ascii_escaped
|
||||||
|
from _pytest.compat import assert_never
|
||||||
from _pytest.compat import final
|
from _pytest.compat import final
|
||||||
from _pytest.compat import get_default_arg_names
|
from _pytest.compat import get_default_arg_names
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
|
@ -451,11 +454,12 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
module = modulecol.obj
|
module = modulecol.obj
|
||||||
clscol = self.getparent(Class)
|
clscol = self.getparent(Class)
|
||||||
cls = clscol and clscol.obj or None
|
cls = clscol and clscol.obj or None
|
||||||
fm = self.session._fixturemanager
|
|
||||||
|
|
||||||
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
|
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
|
||||||
fixtureinfo = definition._fixtureinfo
|
fixtureinfo = definition._fixtureinfo
|
||||||
|
|
||||||
|
# pytest_generate_tests impls call metafunc.parametrize() which fills
|
||||||
|
# metafunc._calls, the outcome of the hook.
|
||||||
metafunc = Metafunc(
|
metafunc = Metafunc(
|
||||||
definition=definition,
|
definition=definition,
|
||||||
fixtureinfo=fixtureinfo,
|
fixtureinfo=fixtureinfo,
|
||||||
|
@ -469,13 +473,13 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
methods.append(module.pytest_generate_tests)
|
methods.append(module.pytest_generate_tests)
|
||||||
if cls is not None and hasattr(cls, "pytest_generate_tests"):
|
if cls is not None and hasattr(cls, "pytest_generate_tests"):
|
||||||
methods.append(cls().pytest_generate_tests)
|
methods.append(cls().pytest_generate_tests)
|
||||||
|
|
||||||
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
|
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
|
||||||
|
|
||||||
if not metafunc._calls:
|
if not metafunc._calls:
|
||||||
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
|
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
|
||||||
else:
|
else:
|
||||||
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
|
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
|
||||||
|
fm = self.session._fixturemanager
|
||||||
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
||||||
|
|
||||||
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
|
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
|
||||||
|
@ -894,26 +898,65 @@ def hasnew(obj: object) -> bool:
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
@attr.s(frozen=True, slots=True, auto_attribs=True)
|
||||||
class CallSpec2:
|
class CallSpec2:
|
||||||
def __init__(self, metafunc: "Metafunc") -> None:
|
"""A planned parameterized invocation of a test function.
|
||||||
self.metafunc = metafunc
|
|
||||||
self.funcargs: Dict[str, object] = {}
|
|
||||||
self._idlist: List[str] = []
|
|
||||||
self.params: Dict[str, object] = {}
|
|
||||||
# Used for sorting parametrized resources.
|
|
||||||
self._arg2scope: Dict[str, Scope] = {}
|
|
||||||
self.marks: List[Mark] = []
|
|
||||||
self.indices: Dict[str, int] = {}
|
|
||||||
|
|
||||||
def copy(self) -> "CallSpec2":
|
Calculated during collection for a given test function's Metafunc.
|
||||||
cs = CallSpec2(self.metafunc)
|
Once collection is over, each callspec is turned into a single Item
|
||||||
cs.funcargs.update(self.funcargs)
|
and stored in item.callspec.
|
||||||
cs.params.update(self.params)
|
"""
|
||||||
cs.marks.extend(self.marks)
|
|
||||||
cs.indices.update(self.indices)
|
# arg name -> arg value which will be passed to the parametrized test
|
||||||
cs._arg2scope.update(self._arg2scope)
|
# function (direct parameterization).
|
||||||
cs._idlist = list(self._idlist)
|
funcargs: Dict[str, object] = attr.Factory(dict)
|
||||||
return cs
|
# arg name -> arg value which will be passed to a fixture of the same name
|
||||||
|
# (indirect parametrization).
|
||||||
|
params: Dict[str, object] = attr.Factory(dict)
|
||||||
|
# arg name -> arg index.
|
||||||
|
indices: Dict[str, int] = attr.Factory(dict)
|
||||||
|
# Used for sorting parametrized resources.
|
||||||
|
_arg2scope: Dict[str, Scope] = attr.Factory(dict)
|
||||||
|
# Parts which will be added to the item's name in `[..]` separated by "-".
|
||||||
|
_idlist: List[str] = attr.Factory(list)
|
||||||
|
# Marks which will be applied to the item.
|
||||||
|
marks: List[Mark] = attr.Factory(list)
|
||||||
|
|
||||||
|
def setmulti(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
valtypes: Mapping[str, "Literal['params', 'funcargs']"],
|
||||||
|
argnames: Iterable[str],
|
||||||
|
valset: Iterable[object],
|
||||||
|
id: str,
|
||||||
|
marks: Iterable[Union[Mark, MarkDecorator]],
|
||||||
|
scope: Scope,
|
||||||
|
param_index: int,
|
||||||
|
) -> "CallSpec2":
|
||||||
|
funcargs = self.funcargs.copy()
|
||||||
|
params = self.params.copy()
|
||||||
|
indices = self.indices.copy()
|
||||||
|
arg2scope = self._arg2scope.copy()
|
||||||
|
for arg, val in zip(argnames, valset):
|
||||||
|
if arg in params or arg in funcargs:
|
||||||
|
raise ValueError(f"duplicate {arg!r}")
|
||||||
|
valtype_for_arg = valtypes[arg]
|
||||||
|
if valtype_for_arg == "params":
|
||||||
|
params[arg] = val
|
||||||
|
elif valtype_for_arg == "funcargs":
|
||||||
|
funcargs[arg] = val
|
||||||
|
else:
|
||||||
|
assert_never(valtype_for_arg)
|
||||||
|
indices[arg] = param_index
|
||||||
|
arg2scope[arg] = scope
|
||||||
|
return CallSpec2(
|
||||||
|
funcargs=funcargs,
|
||||||
|
params=params,
|
||||||
|
arg2scope=arg2scope,
|
||||||
|
indices=indices,
|
||||||
|
idlist=[*self._idlist, id],
|
||||||
|
marks=[*self.marks, *normalize_mark_list(marks)],
|
||||||
|
)
|
||||||
|
|
||||||
def getparam(self, name: str) -> object:
|
def getparam(self, name: str) -> object:
|
||||||
try:
|
try:
|
||||||
|
@ -923,32 +966,7 @@ class CallSpec2:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> str:
|
def id(self) -> str:
|
||||||
return "-".join(map(str, self._idlist))
|
return "-".join(self._idlist)
|
||||||
|
|
||||||
def setmulti2(
|
|
||||||
self,
|
|
||||||
valtypes: Mapping[str, "Literal['params', 'funcargs']"],
|
|
||||||
argnames: Sequence[str],
|
|
||||||
valset: Iterable[object],
|
|
||||||
id: str,
|
|
||||||
marks: Iterable[Union[Mark, MarkDecorator]],
|
|
||||||
scope: Scope,
|
|
||||||
param_index: int,
|
|
||||||
) -> None:
|
|
||||||
for arg, val in zip(argnames, valset):
|
|
||||||
if arg in self.params or arg in self.funcargs:
|
|
||||||
raise ValueError(f"duplicate {arg!r}")
|
|
||||||
valtype_for_arg = valtypes[arg]
|
|
||||||
if valtype_for_arg == "params":
|
|
||||||
self.params[arg] = val
|
|
||||||
elif valtype_for_arg == "funcargs":
|
|
||||||
self.funcargs[arg] = val
|
|
||||||
else: # pragma: no cover
|
|
||||||
assert False, f"Unhandled valtype for arg: {valtype_for_arg}"
|
|
||||||
self.indices[arg] = param_index
|
|
||||||
self._arg2scope[arg] = scope
|
|
||||||
self._idlist.append(id)
|
|
||||||
self.marks.extend(normalize_mark_list(marks))
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
@ -990,9 +1008,11 @@ class Metafunc:
|
||||||
#: Class object where the test function is defined in or ``None``.
|
#: Class object where the test function is defined in or ``None``.
|
||||||
self.cls = cls
|
self.cls = cls
|
||||||
|
|
||||||
self._calls: List[CallSpec2] = []
|
|
||||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
|
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
|
||||||
|
|
||||||
|
# Result of parametrize().
|
||||||
|
self._calls: List[CallSpec2] = []
|
||||||
|
|
||||||
def parametrize(
|
def parametrize(
|
||||||
self,
|
self,
|
||||||
argnames: Union[str, List[str], Tuple[str, ...]],
|
argnames: Union[str, List[str], Tuple[str, ...]],
|
||||||
|
@ -1009,9 +1029,18 @@ class Metafunc:
|
||||||
_param_mark: Optional[Mark] = None,
|
_param_mark: Optional[Mark] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add new invocations to the underlying test function using the list
|
"""Add new invocations to the underlying test function using the list
|
||||||
of argvalues for the given argnames. Parametrization is performed
|
of argvalues for the given argnames. Parametrization is performed
|
||||||
during the collection phase. If you need to setup expensive resources
|
during the collection phase. If you need to setup expensive resources
|
||||||
see about setting indirect to do it rather at test setup time.
|
see about setting indirect to do it rather than at test setup time.
|
||||||
|
|
||||||
|
Can be called multiple times, in which case each call parametrizes all
|
||||||
|
previous parametrizations, e.g.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
unparametrized: t
|
||||||
|
parametrize ["x", "y"]: t[x], t[y]
|
||||||
|
parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2]
|
||||||
|
|
||||||
:param argnames:
|
:param argnames:
|
||||||
A comma-separated string denoting one or more argument names, or
|
A comma-separated string denoting one or more argument names, or
|
||||||
|
@ -1104,17 +1133,16 @@ class Metafunc:
|
||||||
# more than once) then we accumulate those calls generating the cartesian product
|
# more than once) then we accumulate those calls generating the cartesian product
|
||||||
# of all calls.
|
# of all calls.
|
||||||
newcalls = []
|
newcalls = []
|
||||||
for callspec in self._calls or [CallSpec2(self)]:
|
for callspec in self._calls or [CallSpec2()]:
|
||||||
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
|
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
|
||||||
newcallspec = callspec.copy()
|
newcallspec = callspec.setmulti(
|
||||||
newcallspec.setmulti2(
|
valtypes=arg_values_types,
|
||||||
arg_values_types,
|
argnames=argnames,
|
||||||
argnames,
|
valset=param_set.values,
|
||||||
param_set.values,
|
id=param_id,
|
||||||
param_id,
|
marks=param_set.marks,
|
||||||
param_set.marks,
|
scope=scope_,
|
||||||
scope_,
|
param_index=param_index,
|
||||||
param_index,
|
|
||||||
)
|
)
|
||||||
newcalls.append(newcallspec)
|
newcalls.append(newcallspec)
|
||||||
self._calls = newcalls
|
self._calls = newcalls
|
||||||
|
|
Loading…
Reference in New Issue