initial split out of metafunc
This commit is contained in:
parent
c58281b9ae
commit
2efd001530
|
@ -71,9 +71,7 @@ if TYPE_CHECKING:
|
|||
|
||||
from _pytest.scope import _ScopeName
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import CallSpec2
|
||||
from _pytest.python import Metafunc
|
||||
|
||||
from _pytest.python.metafunc import CallSpec2, Metafunc
|
||||
|
||||
# The value of the fixture -- return/yield of the fixture function (type variable).
|
||||
FixtureValue = TypeVar("FixtureValue")
|
||||
|
|
|
@ -34,8 +34,8 @@ if TYPE_CHECKING:
|
|||
from _pytest.nodes import Item
|
||||
from _pytest.outcomes import Exit
|
||||
from _pytest.python import Class
|
||||
from _pytest.python.metafunc import Metafunc
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Metafunc
|
||||
from _pytest.python import Module
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
"""Python test discovery, setup and run of test functions."""
|
||||
import dataclasses
|
||||
import enum
|
||||
import fnmatch
|
||||
import inspect
|
||||
import itertools
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
import warnings
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import Iterable
|
||||
|
@ -21,7 +15,6 @@ from typing import Iterator
|
|||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Pattern
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
@ -35,11 +28,6 @@ from _pytest._code import filter_traceback
|
|||
from _pytest._code import getfslineno
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ascii_escaped
|
||||
from _pytest.compat import assert_never
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import get_default_arg_names
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import getimfunc
|
||||
from _pytest.compat import is_async_function
|
||||
|
@ -48,39 +36,31 @@ from _pytest.compat import LEGACY_PATH
|
|||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import safe_isclass
|
||||
from _pytest.compat import STRING_TYPES
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
|
||||
from _pytest.deprecated import INSTANCE_COLLECTOR
|
||||
from _pytest.deprecated import NOSE_SUPPORT_METHOD
|
||||
from _pytest.fixtures import FuncFixtureInfo
|
||||
from _pytest.main import Session
|
||||
from _pytest.mark import MARK_GEN
|
||||
from _pytest.mark import ParameterSet
|
||||
from _pytest.mark.structures import get_unpacked_marks
|
||||
from _pytest.mark.structures import Mark
|
||||
from _pytest.mark.structures import MarkDecorator
|
||||
from _pytest.mark.structures import normalize_mark_list
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportPathMismatchError
|
||||
from _pytest.pathlib import parts
|
||||
from _pytest.pathlib import visit
|
||||
from _pytest.scope import Scope
|
||||
from _pytest.python.metafunc import CallSpec2
|
||||
from _pytest.python.metafunc import Metafunc
|
||||
from _pytest.warning_types import PytestCollectionWarning
|
||||
from _pytest.warning_types import PytestReturnNotNoneWarning
|
||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
from _pytest.scope import _ScopeName
|
||||
pass
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
|
@ -951,602 +931,6 @@ def hasnew(obj: object) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
@final
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class IdMaker:
|
||||
"""Make IDs for a parametrization."""
|
||||
|
||||
__slots__ = (
|
||||
"argnames",
|
||||
"parametersets",
|
||||
"idfn",
|
||||
"ids",
|
||||
"config",
|
||||
"nodeid",
|
||||
"func_name",
|
||||
)
|
||||
|
||||
# The argnames of the parametrization.
|
||||
argnames: Sequence[str]
|
||||
# The ParameterSets of the parametrization.
|
||||
parametersets: Sequence[ParameterSet]
|
||||
# Optionally, a user-provided callable to make IDs for parameters in a
|
||||
# ParameterSet.
|
||||
idfn: Optional[Callable[[Any], Optional[object]]]
|
||||
# Optionally, explicit IDs for ParameterSets by index.
|
||||
ids: Optional[Sequence[Optional[object]]]
|
||||
# Optionally, the pytest config.
|
||||
# Used for controlling ASCII escaping, and for calling the
|
||||
# :hook:`pytest_make_parametrize_id` hook.
|
||||
config: Optional[Config]
|
||||
# Optionally, the ID of the node being parametrized.
|
||||
# Used only for clearer error messages.
|
||||
nodeid: Optional[str]
|
||||
# Optionally, the ID of the function being parametrized.
|
||||
# Used only for clearer error messages.
|
||||
func_name: Optional[str]
|
||||
|
||||
def make_unique_parameterset_ids(self) -> List[str]:
|
||||
"""Make a unique identifier for each ParameterSet, that may be used to
|
||||
identify the parametrization in a node ID.
|
||||
|
||||
Format is <prm_1_token>-...-<prm_n_token>[counter], where prm_x_token is
|
||||
- user-provided id, if given
|
||||
- else an id derived from the value, applicable for certain types
|
||||
- else <argname><parameterset index>
|
||||
The counter suffix is appended only in case a string wouldn't be unique
|
||||
otherwise.
|
||||
"""
|
||||
resolved_ids = list(self._resolve_ids())
|
||||
# All IDs must be unique!
|
||||
if len(resolved_ids) != len(set(resolved_ids)):
|
||||
# Record the number of occurrences of each ID.
|
||||
id_counts = Counter(resolved_ids)
|
||||
# Map the ID to its next suffix.
|
||||
id_suffixes: Dict[str, int] = defaultdict(int)
|
||||
# Suffix non-unique IDs to make them unique.
|
||||
for index, id in enumerate(resolved_ids):
|
||||
if id_counts[id] > 1:
|
||||
resolved_ids[index] = f"{id}{id_suffixes[id]}"
|
||||
id_suffixes[id] += 1
|
||||
return resolved_ids
|
||||
|
||||
def _resolve_ids(self) -> Iterable[str]:
|
||||
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
|
||||
for idx, parameterset in enumerate(self.parametersets):
|
||||
if parameterset.id is not None:
|
||||
# ID provided directly - pytest.param(..., id="...")
|
||||
yield parameterset.id
|
||||
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
|
||||
# ID provided in the IDs list - parametrize(..., ids=[...]).
|
||||
yield self._idval_from_value_required(self.ids[idx], idx)
|
||||
else:
|
||||
# ID not provided - generate it.
|
||||
yield "-".join(
|
||||
self._idval(val, argname, idx)
|
||||
for val, argname in zip(parameterset.values, self.argnames)
|
||||
)
|
||||
|
||||
def _idval(self, val: object, argname: str, idx: int) -> str:
|
||||
"""Make an ID for a parameter in a ParameterSet."""
|
||||
idval = self._idval_from_function(val, argname, idx)
|
||||
if idval is not None:
|
||||
return idval
|
||||
idval = self._idval_from_hook(val, argname)
|
||||
if idval is not None:
|
||||
return idval
|
||||
idval = self._idval_from_value(val)
|
||||
if idval is not None:
|
||||
return idval
|
||||
return self._idval_from_argname(argname, idx)
|
||||
|
||||
def _idval_from_function(
|
||||
self, val: object, argname: str, idx: int
|
||||
) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet using the
|
||||
user-provided id callable, if given."""
|
||||
if self.idfn is None:
|
||||
return None
|
||||
try:
|
||||
id = self.idfn(val)
|
||||
except Exception as e:
|
||||
prefix = f"{self.nodeid}: " if self.nodeid is not None else ""
|
||||
msg = "error raised while trying to determine id of parameter '{}' at position {}"
|
||||
msg = prefix + msg.format(argname, idx)
|
||||
raise ValueError(msg) from e
|
||||
if id is None:
|
||||
return None
|
||||
return self._idval_from_value(id)
|
||||
|
||||
def _idval_from_hook(self, val: object, argname: str) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet by calling the
|
||||
:hook:`pytest_make_parametrize_id` hook."""
|
||||
if self.config:
|
||||
id: Optional[str] = self.config.hook.pytest_make_parametrize_id(
|
||||
config=self.config, val=val, argname=argname
|
||||
)
|
||||
return id
|
||||
return None
|
||||
|
||||
def _idval_from_value(self, val: object) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet from its value,
|
||||
if the value type is supported."""
|
||||
if isinstance(val, STRING_TYPES):
|
||||
return _ascii_escaped_by_config(val, self.config)
|
||||
elif val is None or isinstance(val, (float, int, bool, complex)):
|
||||
return str(val)
|
||||
elif isinstance(val, Pattern):
|
||||
return ascii_escaped(val.pattern)
|
||||
elif val is NOTSET:
|
||||
# Fallback to default. Note that NOTSET is an enum.Enum.
|
||||
pass
|
||||
elif isinstance(val, enum.Enum):
|
||||
return str(val)
|
||||
elif isinstance(getattr(val, "__name__", None), str):
|
||||
# Name of a class, function, module, etc.
|
||||
name: str = getattr(val, "__name__")
|
||||
return name
|
||||
return None
|
||||
|
||||
def _idval_from_value_required(self, val: object, idx: int) -> str:
|
||||
"""Like _idval_from_value(), but fails if the type is not supported."""
|
||||
id = self._idval_from_value(val)
|
||||
if id is not None:
|
||||
return id
|
||||
|
||||
# Fail.
|
||||
if self.func_name is not None:
|
||||
prefix = f"In {self.func_name}: "
|
||||
elif self.nodeid is not None:
|
||||
prefix = f"In {self.nodeid}: "
|
||||
else:
|
||||
prefix = ""
|
||||
msg = (
|
||||
f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
|
||||
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
@staticmethod
|
||||
def _idval_from_argname(argname: str, idx: int) -> str:
|
||||
"""Make an ID for a parameter in a ParameterSet from the argument name
|
||||
and the index of the ParameterSet."""
|
||||
return str(argname) + str(idx)
|
||||
|
||||
|
||||
@final
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CallSpec2:
|
||||
"""A planned parameterized invocation of a test function.
|
||||
|
||||
Calculated during collection for a given test function's Metafunc.
|
||||
Once collection is over, each callspec is turned into a single Item
|
||||
and stored in item.callspec.
|
||||
"""
|
||||
|
||||
# arg name -> arg value which will be passed to the parametrized test
|
||||
# function (direct parameterization).
|
||||
funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
|
||||
# arg name -> arg value which will be passed to a fixture of the same name
|
||||
# (indirect parametrization).
|
||||
params: Dict[str, object] = dataclasses.field(default_factory=dict)
|
||||
# arg name -> arg index.
|
||||
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
|
||||
# Used for sorting parametrized resources.
|
||||
_arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict)
|
||||
# Parts which will be added to the item's name in `[..]` separated by "-".
|
||||
_idlist: List[str] = dataclasses.field(default_factory=list)
|
||||
# Marks which will be applied to the item.
|
||||
marks: List[Mark] = dataclasses.field(default_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,
|
||||
indices=indices,
|
||||
_arg2scope=arg2scope,
|
||||
_idlist=[*self._idlist, id],
|
||||
marks=[*self.marks, *normalize_mark_list(marks)],
|
||||
)
|
||||
|
||||
def getparam(self, name: str) -> object:
|
||||
try:
|
||||
return self.params[name]
|
||||
except KeyError as e:
|
||||
raise ValueError(name) from e
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return "-".join(self._idlist)
|
||||
|
||||
|
||||
@final
|
||||
class Metafunc:
|
||||
"""Objects passed to the :hook:`pytest_generate_tests` hook.
|
||||
|
||||
They help to inspect a test function and to generate tests according to
|
||||
test configuration or values specified in the class or module where a
|
||||
test function is defined.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
definition: "FunctionDefinition",
|
||||
fixtureinfo: fixtures.FuncFixtureInfo,
|
||||
config: Config,
|
||||
cls=None,
|
||||
module=None,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
|
||||
#: Access to the underlying :class:`_pytest.python.FunctionDefinition`.
|
||||
self.definition = definition
|
||||
|
||||
#: Access to the :class:`pytest.Config` object for the test session.
|
||||
self.config = config
|
||||
|
||||
#: The module object where the test function is defined in.
|
||||
self.module = module
|
||||
|
||||
#: Underlying Python test function.
|
||||
self.function = definition.obj
|
||||
|
||||
#: Set of fixture names required by the test function.
|
||||
self.fixturenames = fixtureinfo.names_closure
|
||||
|
||||
#: Class object where the test function is defined in or ``None``.
|
||||
self.cls = cls
|
||||
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
|
||||
|
||||
# Result of parametrize().
|
||||
self._calls: List[CallSpec2] = []
|
||||
|
||||
def parametrize(
|
||||
self,
|
||||
argnames: Union[str, Sequence[str]],
|
||||
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
|
||||
indirect: Union[bool, Sequence[str]] = False,
|
||||
ids: Optional[
|
||||
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
] = None,
|
||||
scope: "Optional[_ScopeName]" = None,
|
||||
*,
|
||||
_param_mark: Optional[Mark] = None,
|
||||
) -> None:
|
||||
"""Add new invocations to the underlying test function using the list
|
||||
of argvalues for the given argnames. Parametrization is performed
|
||||
during the collection phase. If you need to setup expensive resources
|
||||
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:
|
||||
A comma-separated string denoting one or more argument names, or
|
||||
a list/tuple of argument strings.
|
||||
|
||||
:param argvalues:
|
||||
The list of argvalues determines how often a test is invoked with
|
||||
different argument values.
|
||||
|
||||
If only one argname was specified argvalues is a list of values.
|
||||
If N argnames were specified, argvalues must be a list of
|
||||
N-tuples, where each tuple-element specifies a value for its
|
||||
respective argname.
|
||||
|
||||
:param indirect:
|
||||
A list of arguments' names (subset of argnames) or a boolean.
|
||||
If True the list contains all names from the argnames. Each
|
||||
argvalue corresponding to an argname in this list will
|
||||
be passed as request.param to its respective argname fixture
|
||||
function so that it can perform more expensive setups during the
|
||||
setup phase of a test rather than at collection time.
|
||||
|
||||
:param ids:
|
||||
Sequence of (or generator for) ids for ``argvalues``,
|
||||
or a callable to return part of the id for each argvalue.
|
||||
|
||||
With sequences (and generators like ``itertools.count()``) the
|
||||
returned ids should be of type ``string``, ``int``, ``float``,
|
||||
``bool``, or ``None``.
|
||||
They are mapped to the corresponding index in ``argvalues``.
|
||||
``None`` means to use the auto-generated id.
|
||||
|
||||
If it is a callable it will be called for each entry in
|
||||
``argvalues``, and the return value is used as part of the
|
||||
auto-generated id for the whole set (where parts are joined with
|
||||
dashes ("-")).
|
||||
This is useful to provide more specific ids for certain items, e.g.
|
||||
dates. Returning ``None`` will use an auto-generated id.
|
||||
|
||||
If no ids are provided they will be generated automatically from
|
||||
the argvalues.
|
||||
|
||||
:param scope:
|
||||
If specified it denotes the scope of the parameters.
|
||||
The scope is used for grouping tests by parameter instances.
|
||||
It will also override any fixture-function defined scope, allowing
|
||||
to set a dynamic scope using test context or configuration.
|
||||
"""
|
||||
argnames, parametersets = ParameterSet._for_parametrize(
|
||||
argnames,
|
||||
argvalues,
|
||||
self.function,
|
||||
self.config,
|
||||
nodeid=self.definition.nodeid,
|
||||
)
|
||||
del argvalues
|
||||
|
||||
if "request" in argnames:
|
||||
fail(
|
||||
"'request' is a reserved name and cannot be used in @pytest.mark.parametrize",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
if scope is not None:
|
||||
scope_ = Scope.from_user(
|
||||
scope, descr=f"parametrize() call in {self.function.__name__}"
|
||||
)
|
||||
else:
|
||||
scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
|
||||
|
||||
self._validate_if_using_arg_names(argnames, indirect)
|
||||
|
||||
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
|
||||
|
||||
# Use any already (possibly) generated ids with parametrize Marks.
|
||||
if _param_mark and _param_mark._param_ids_from:
|
||||
generated_ids = _param_mark._param_ids_from._param_ids_generated
|
||||
if generated_ids is not None:
|
||||
ids = generated_ids
|
||||
|
||||
ids = self._resolve_parameter_set_ids(
|
||||
argnames, ids, parametersets, nodeid=self.definition.nodeid
|
||||
)
|
||||
|
||||
# Store used (possibly generated) ids with parametrize Marks.
|
||||
if _param_mark and _param_mark._param_ids_from and generated_ids is None:
|
||||
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
|
||||
|
||||
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
|
||||
# more than once) then we accumulate those calls generating the cartesian product
|
||||
# of all calls.
|
||||
newcalls = []
|
||||
for callspec in self._calls or [CallSpec2()]:
|
||||
for param_index, (param_id, param_set) in enumerate(
|
||||
zip(ids, parametersets)
|
||||
):
|
||||
newcallspec = callspec.setmulti(
|
||||
valtypes=arg_values_types,
|
||||
argnames=argnames,
|
||||
valset=param_set.values,
|
||||
id=param_id,
|
||||
marks=param_set.marks,
|
||||
scope=scope_,
|
||||
param_index=param_index,
|
||||
)
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
|
||||
def _resolve_parameter_set_ids(
|
||||
self,
|
||||
argnames: Sequence[str],
|
||||
ids: Optional[
|
||||
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
],
|
||||
parametersets: Sequence[ParameterSet],
|
||||
nodeid: str,
|
||||
) -> List[str]:
|
||||
"""Resolve the actual ids for the given parameter sets.
|
||||
|
||||
:param argnames:
|
||||
Argument names passed to ``parametrize()``.
|
||||
:param ids:
|
||||
The `ids` parameter of the ``parametrize()`` call (see docs).
|
||||
:param parametersets:
|
||||
The parameter sets, each containing a set of values corresponding
|
||||
to ``argnames``.
|
||||
:param nodeid str:
|
||||
The nodeid of the definition item that generated this
|
||||
parametrization.
|
||||
:returns:
|
||||
List with ids for each parameter set given.
|
||||
"""
|
||||
if ids is None:
|
||||
idfn = None
|
||||
ids_ = None
|
||||
elif callable(ids):
|
||||
idfn = ids
|
||||
ids_ = None
|
||||
else:
|
||||
idfn = None
|
||||
ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
|
||||
id_maker = IdMaker(
|
||||
argnames,
|
||||
parametersets,
|
||||
idfn,
|
||||
ids_,
|
||||
self.config,
|
||||
nodeid=nodeid,
|
||||
func_name=self.function.__name__,
|
||||
)
|
||||
return id_maker.make_unique_parameterset_ids()
|
||||
|
||||
def _validate_ids(
|
||||
self,
|
||||
ids: Iterable[Optional[object]],
|
||||
parametersets: Sequence[ParameterSet],
|
||||
func_name: str,
|
||||
) -> List[Optional[object]]:
|
||||
try:
|
||||
num_ids = len(ids) # type: ignore[arg-type]
|
||||
except TypeError:
|
||||
try:
|
||||
iter(ids)
|
||||
except TypeError as e:
|
||||
raise TypeError("ids must be a callable or an iterable") from e
|
||||
num_ids = len(parametersets)
|
||||
|
||||
# num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
|
||||
if num_ids != len(parametersets) and num_ids != 0:
|
||||
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
|
||||
fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
|
||||
|
||||
return list(itertools.islice(ids, num_ids))
|
||||
|
||||
def _resolve_arg_value_types(
|
||||
self,
|
||||
argnames: Sequence[str],
|
||||
indirect: Union[bool, Sequence[str]],
|
||||
) -> Dict[str, "Literal['params', 'funcargs']"]:
|
||||
"""Resolve if each parametrized argument must be considered a
|
||||
parameter to a fixture or a "funcarg" to the function, based on the
|
||||
``indirect`` parameter of the parametrized() call.
|
||||
|
||||
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||
:param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
|
||||
:rtype: Dict[str, str]
|
||||
A dict mapping each arg name to either:
|
||||
* "params" if the argname should be the parameter of a fixture of the same name.
|
||||
* "funcargs" if the argname should be a parameter to the parametrized test function.
|
||||
"""
|
||||
if isinstance(indirect, bool):
|
||||
valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys(
|
||||
argnames, "params" if indirect else "funcargs"
|
||||
)
|
||||
elif isinstance(indirect, Sequence):
|
||||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
for arg in indirect:
|
||||
if arg not in argnames:
|
||||
fail(
|
||||
"In {}: indirect fixture '{}' doesn't exist".format(
|
||||
self.function.__name__, arg
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
valtypes[arg] = "params"
|
||||
else:
|
||||
fail(
|
||||
"In {func}: expected Sequence or boolean for indirect, got {type}".format(
|
||||
type=type(indirect).__name__, func=self.function.__name__
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
return valtypes
|
||||
|
||||
def _validate_if_using_arg_names(
|
||||
self,
|
||||
argnames: Sequence[str],
|
||||
indirect: Union[bool, Sequence[str]],
|
||||
) -> None:
|
||||
"""Check if all argnames are being used, by default values, or directly/indirectly.
|
||||
|
||||
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||
:param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
|
||||
:raises ValueError: If validation fails.
|
||||
"""
|
||||
default_arg_names = set(get_default_arg_names(self.function))
|
||||
func_name = self.function.__name__
|
||||
for arg in argnames:
|
||||
if arg not in self.fixturenames:
|
||||
if arg in default_arg_names:
|
||||
fail(
|
||||
"In {}: function already takes an argument '{}' with a default value".format(
|
||||
func_name, arg
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
else:
|
||||
if isinstance(indirect, Sequence):
|
||||
name = "fixture" if arg in indirect else "argument"
|
||||
else:
|
||||
name = "fixture" if indirect else "argument"
|
||||
fail(
|
||||
f"In {func_name}: function uses no {name} '{arg}'",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
|
||||
def _find_parametrized_scope(
|
||||
argnames: Sequence[str],
|
||||
arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]],
|
||||
indirect: Union[bool, Sequence[str]],
|
||||
) -> Scope:
|
||||
"""Find the most appropriate scope for a parametrized call based on its arguments.
|
||||
|
||||
When there's at least one direct argument, always use "function" scope.
|
||||
|
||||
When a test function is parametrized and all its arguments are indirect
|
||||
(e.g. fixtures), return the most narrow scope based on the fixtures used.
|
||||
|
||||
Related to issue #1832, based on code posted by @Kingdread.
|
||||
"""
|
||||
if isinstance(indirect, Sequence):
|
||||
all_arguments_are_fixtures = len(indirect) == len(argnames)
|
||||
else:
|
||||
all_arguments_are_fixtures = bool(indirect)
|
||||
|
||||
if all_arguments_are_fixtures:
|
||||
fixturedefs = arg2fixturedefs or {}
|
||||
used_scopes = [
|
||||
fixturedef[0]._scope
|
||||
for name, fixturedef in fixturedefs.items()
|
||||
if name in argnames
|
||||
]
|
||||
# Takes the most narrow scope from used fixtures.
|
||||
return min(used_scopes, default=Scope.Function)
|
||||
|
||||
return Scope.Function
|
||||
|
||||
|
||||
def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str:
|
||||
if config is None:
|
||||
escape_option = False
|
||||
else:
|
||||
escape_option = config.getini(
|
||||
"disable_test_id_escaping_and_forfeit_all_rights_to_community_support"
|
||||
)
|
||||
# TODO: If escaping is turned off and the user passes bytes,
|
||||
# will return a bytes. For now we ignore this but the
|
||||
# code *probably* doesn't handle this case.
|
||||
return val if escape_option else ascii_escaped(val) # type: ignore
|
||||
|
||||
|
||||
class Function(PyobjMixin, nodes.Item):
|
||||
"""An Item responsible for setting up and executing a Python test function.
|
||||
|
||||
|
|
|
@ -0,0 +1,635 @@
|
|||
import dataclasses
|
||||
import enum
|
||||
import itertools
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Pattern
|
||||
from typing import Sequence
|
||||
from typing import Union
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
from _pytest import fixtures
|
||||
from _pytest import python
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ascii_escaped
|
||||
from _pytest.compat import assert_never
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import get_default_arg_names
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import STRING_TYPES
|
||||
from _pytest.config import Config
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.mark import Mark
|
||||
from _pytest.mark import MarkDecorator
|
||||
from _pytest.mark import ParameterSet
|
||||
from _pytest.mark.structures import normalize_mark_list
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.scope import _ScopeName
|
||||
from _pytest.scope import Scope
|
||||
|
||||
|
||||
@final
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class IdMaker:
|
||||
"""Make IDs for a parametrization."""
|
||||
|
||||
__slots__ = (
|
||||
"argnames",
|
||||
"parametersets",
|
||||
"idfn",
|
||||
"ids",
|
||||
"config",
|
||||
"nodeid",
|
||||
"func_name",
|
||||
)
|
||||
|
||||
# The argnames of the parametrization.
|
||||
argnames: Sequence[str]
|
||||
# The ParameterSets of the parametrization.
|
||||
parametersets: Sequence[ParameterSet]
|
||||
# Optionally, a user-provided callable to make IDs for parameters in a
|
||||
# ParameterSet.
|
||||
idfn: Optional[Callable[[Any], Optional[object]]]
|
||||
# Optionally, explicit IDs for ParameterSets by index.
|
||||
ids: Optional[Sequence[Optional[object]]]
|
||||
# Optionally, the pytest config.
|
||||
# Used for controlling ASCII escaping, and for calling the
|
||||
# :hook:`pytest_make_parametrize_id` hook.
|
||||
config: Optional[Config]
|
||||
# Optionally, the ID of the node being parametrized.
|
||||
# Used only for clearer error messages.
|
||||
nodeid: Optional[str]
|
||||
# Optionally, the ID of the function being parametrized.
|
||||
# Used only for clearer error messages.
|
||||
func_name: Optional[str]
|
||||
|
||||
def make_unique_parameterset_ids(self) -> List[str]:
|
||||
"""Make a unique identifier for each ParameterSet, that may be used to
|
||||
identify the parametrization in a node ID.
|
||||
|
||||
Format is <prm_1_token>-...-<prm_n_token>[counter], where prm_x_token is
|
||||
- user-provided id, if given
|
||||
- else an id derived from the value, applicable for certain types
|
||||
- else <argname><parameterset index>
|
||||
The counter suffix is appended only in case a string wouldn't be unique
|
||||
otherwise.
|
||||
"""
|
||||
resolved_ids = list(self._resolve_ids())
|
||||
# All IDs must be unique!
|
||||
if len(resolved_ids) != len(set(resolved_ids)):
|
||||
# Record the number of occurrences of each ID.
|
||||
id_counts = Counter(resolved_ids)
|
||||
# Map the ID to its next suffix.
|
||||
id_suffixes: Dict[str, int] = defaultdict(int)
|
||||
# Suffix non-unique IDs to make them unique.
|
||||
for index, id in enumerate(resolved_ids):
|
||||
if id_counts[id] > 1:
|
||||
resolved_ids[index] = f"{id}{id_suffixes[id]}"
|
||||
id_suffixes[id] += 1
|
||||
return resolved_ids
|
||||
|
||||
def _resolve_ids(self) -> Iterable[str]:
|
||||
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
|
||||
for idx, parameterset in enumerate(self.parametersets):
|
||||
if parameterset.id is not None:
|
||||
# ID provided directly - pytest.param(..., id="...")
|
||||
yield parameterset.id
|
||||
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
|
||||
# ID provided in the IDs list - parametrize(..., ids=[...]).
|
||||
yield self._idval_from_value_required(self.ids[idx], idx)
|
||||
else:
|
||||
# ID not provided - generate it.
|
||||
yield "-".join(
|
||||
self._idval(val, argname, idx)
|
||||
for val, argname in zip(parameterset.values, self.argnames)
|
||||
)
|
||||
|
||||
def _idval(self, val: object, argname: str, idx: int) -> str:
|
||||
"""Make an ID for a parameter in a ParameterSet."""
|
||||
idval = self._idval_from_function(val, argname, idx)
|
||||
if idval is not None:
|
||||
return idval
|
||||
idval = self._idval_from_hook(val, argname)
|
||||
if idval is not None:
|
||||
return idval
|
||||
idval = self._idval_from_value(val)
|
||||
if idval is not None:
|
||||
return idval
|
||||
return self._idval_from_argname(argname, idx)
|
||||
|
||||
def _idval_from_function(
|
||||
self, val: object, argname: str, idx: int
|
||||
) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet using the
|
||||
user-provided id callable, if given."""
|
||||
if self.idfn is None:
|
||||
return None
|
||||
try:
|
||||
id = self.idfn(val)
|
||||
except Exception as e:
|
||||
prefix = f"{self.nodeid}: " if self.nodeid is not None else ""
|
||||
msg = "error raised while trying to determine id of parameter '{}' at position {}"
|
||||
msg = prefix + msg.format(argname, idx)
|
||||
raise ValueError(msg) from e
|
||||
if id is None:
|
||||
return None
|
||||
return self._idval_from_value(id)
|
||||
|
||||
def _idval_from_hook(self, val: object, argname: str) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet by calling the
|
||||
:hook:`pytest_make_parametrize_id` hook."""
|
||||
if self.config:
|
||||
id: Optional[str] = self.config.hook.pytest_make_parametrize_id(
|
||||
config=self.config, val=val, argname=argname
|
||||
)
|
||||
return id
|
||||
return None
|
||||
|
||||
def _idval_from_value(self, val: object) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet from its value,
|
||||
if the value type is supported."""
|
||||
if isinstance(val, STRING_TYPES):
|
||||
return _ascii_escaped_by_config(val, self.config)
|
||||
elif val is None or isinstance(val, (float, int, bool, complex)):
|
||||
return str(val)
|
||||
elif isinstance(val, Pattern):
|
||||
return ascii_escaped(val.pattern)
|
||||
elif val is NOTSET:
|
||||
# Fallback to default. Note that NOTSET is an enum.Enum.
|
||||
pass
|
||||
elif isinstance(val, enum.Enum):
|
||||
return str(val)
|
||||
elif isinstance(getattr(val, "__name__", None), str):
|
||||
# Name of a class, function, module, etc.
|
||||
name: str = getattr(val, "__name__")
|
||||
return name
|
||||
return None
|
||||
|
||||
def _idval_from_value_required(self, val: object, idx: int) -> str:
|
||||
"""Like _idval_from_value(), but fails if the type is not supported."""
|
||||
id = self._idval_from_value(val)
|
||||
if id is not None:
|
||||
return id
|
||||
|
||||
# Fail.
|
||||
if self.func_name is not None:
|
||||
prefix = f"In {self.func_name}: "
|
||||
elif self.nodeid is not None:
|
||||
prefix = f"In {self.nodeid}: "
|
||||
else:
|
||||
prefix = ""
|
||||
msg = (
|
||||
f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
|
||||
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
@staticmethod
|
||||
def _idval_from_argname(argname: str, idx: int) -> str:
|
||||
"""Make an ID for a parameter in a ParameterSet from the argument name
|
||||
and the index of the ParameterSet."""
|
||||
return str(argname) + str(idx)
|
||||
|
||||
|
||||
def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str:
|
||||
if config is None:
|
||||
escape_option = False
|
||||
else:
|
||||
escape_option = config.getini(
|
||||
"disable_test_id_escaping_and_forfeit_all_rights_to_community_support"
|
||||
)
|
||||
# TODO: If escaping is turned off and the user passes bytes,
|
||||
# will return a bytes. For now we ignore this but the
|
||||
# code *probably* doesn't handle this case.
|
||||
return val if escape_option else ascii_escaped(val) # type: ignore
|
||||
|
||||
|
||||
@final
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CallSpec2:
|
||||
"""A planned parameterized invocation of a test function.
|
||||
|
||||
Calculated during collection for a given test function's Metafunc.
|
||||
Once collection is over, each callspec is turned into a single Item
|
||||
and stored in item.callspec.
|
||||
"""
|
||||
|
||||
# arg name -> arg value which will be passed to the parametrized test
|
||||
# function (direct parameterization).
|
||||
funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
|
||||
# arg name -> arg value which will be passed to a fixture of the same name
|
||||
# (indirect parametrization).
|
||||
params: Dict[str, object] = dataclasses.field(default_factory=dict)
|
||||
# arg name -> arg index.
|
||||
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
|
||||
# Used for sorting parametrized resources.
|
||||
_arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict)
|
||||
# Parts which will be added to the item's name in `[..]` separated by "-".
|
||||
_idlist: List[str] = dataclasses.field(default_factory=list)
|
||||
# Marks which will be applied to the item.
|
||||
marks: List[Mark] = dataclasses.field(default_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,
|
||||
indices=indices,
|
||||
_arg2scope=arg2scope,
|
||||
_idlist=[*self._idlist, id],
|
||||
marks=[*self.marks, *normalize_mark_list(marks)],
|
||||
)
|
||||
|
||||
def getparam(self, name: str) -> object:
|
||||
try:
|
||||
return self.params[name]
|
||||
except KeyError as e:
|
||||
raise ValueError(name) from e
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return "-".join(self._idlist)
|
||||
|
||||
|
||||
@final
|
||||
class Metafunc:
|
||||
"""Objects passed to the :hook:`pytest_generate_tests` hook.
|
||||
|
||||
They help to inspect a test function and to generate tests according to
|
||||
test configuration or values specified in the class or module where a
|
||||
test function is defined.
|
||||
"""
|
||||
|
||||
fixturenames: List[str]
|
||||
_arg2fixturedefs: Dict[str, Sequence["fixtures.FixtureDef[Any]"]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
definition: "python.FunctionDefinition",
|
||||
fixtureinfo: fixtures.FuncFixtureInfo,
|
||||
config: Config,
|
||||
cls=None,
|
||||
module=None,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
|
||||
#: Access to the underlying :class:`_pytest.python.FunctionDefinition`.
|
||||
self.definition = definition
|
||||
|
||||
#: Access to the :class:`pytest.Config` object for the test session.
|
||||
self.config = config
|
||||
|
||||
#: The module object where the test function is defined in.
|
||||
self.module = module
|
||||
|
||||
#: Underlying Python test function.
|
||||
self.function = definition.obj
|
||||
|
||||
#: Set of fixture names required by the test function.
|
||||
self.fixturenames = fixtureinfo.names_closure
|
||||
|
||||
#: Class object where the test function is defined in or ``None``.
|
||||
self.cls = cls
|
||||
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
|
||||
|
||||
# Result of parametrize().
|
||||
self._calls: List[CallSpec2] = []
|
||||
|
||||
def parametrize(
|
||||
self,
|
||||
argnames: Union[str, Sequence[str]],
|
||||
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
|
||||
indirect: Union[bool, Sequence[str]] = False,
|
||||
ids: Optional[
|
||||
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
] = None,
|
||||
scope: "Optional[_ScopeName]" = None,
|
||||
*,
|
||||
_param_mark: Optional[Mark] = None,
|
||||
) -> None:
|
||||
"""Add new invocations to the underlying test function using the list
|
||||
of argvalues for the given argnames. Parametrization is performed
|
||||
during the collection phase. If you need to setup expensive resources
|
||||
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:
|
||||
A comma-separated string denoting one or more argument names, or
|
||||
a list/tuple of argument strings.
|
||||
|
||||
:param argvalues:
|
||||
The list of argvalues determines how often a test is invoked with
|
||||
different argument values.
|
||||
|
||||
If only one argname was specified argvalues is a list of values.
|
||||
If N argnames were specified, argvalues must be a list of
|
||||
N-tuples, where each tuple-element specifies a value for its
|
||||
respective argname.
|
||||
|
||||
:param indirect:
|
||||
A list of arguments' names (subset of argnames) or a boolean.
|
||||
If True the list contains all names from the argnames. Each
|
||||
argvalue corresponding to an argname in this list will
|
||||
be passed as request.param to its respective argname fixture
|
||||
function so that it can perform more expensive setups during the
|
||||
setup phase of a test rather than at collection time.
|
||||
|
||||
:param ids:
|
||||
Sequence of (or generator for) ids for ``argvalues``,
|
||||
or a callable to return part of the id for each argvalue.
|
||||
|
||||
With sequences (and generators like ``itertools.count()``) the
|
||||
returned ids should be of type ``string``, ``int``, ``float``,
|
||||
``bool``, or ``None``.
|
||||
They are mapped to the corresponding index in ``argvalues``.
|
||||
``None`` means to use the auto-generated id.
|
||||
|
||||
If it is a callable it will be called for each entry in
|
||||
``argvalues``, and the return value is used as part of the
|
||||
auto-generated id for the whole set (where parts are joined with
|
||||
dashes ("-")).
|
||||
This is useful to provide more specific ids for certain items, e.g.
|
||||
dates. Returning ``None`` will use an auto-generated id.
|
||||
|
||||
If no ids are provided they will be generated automatically from
|
||||
the argvalues.
|
||||
|
||||
:param scope:
|
||||
If specified it denotes the scope of the parameters.
|
||||
The scope is used for grouping tests by parameter instances.
|
||||
It will also override any fixture-function defined scope, allowing
|
||||
to set a dynamic scope using test context or configuration.
|
||||
"""
|
||||
argnames, parametersets = ParameterSet._for_parametrize(
|
||||
argnames,
|
||||
argvalues,
|
||||
self.function,
|
||||
self.config,
|
||||
nodeid=self.definition.nodeid,
|
||||
)
|
||||
del argvalues
|
||||
|
||||
if "request" in argnames:
|
||||
fail(
|
||||
"'request' is a reserved name and cannot be used in @pytest.mark.parametrize",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
if scope is not None:
|
||||
scope_ = Scope.from_user(
|
||||
scope, descr=f"parametrize() call in {self.function.__name__}"
|
||||
)
|
||||
else:
|
||||
scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
|
||||
|
||||
self._validate_if_using_arg_names(argnames, indirect)
|
||||
|
||||
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
|
||||
|
||||
# Use any already (possibly) generated ids with parametrize Marks.
|
||||
if _param_mark and _param_mark._param_ids_from:
|
||||
generated_ids = _param_mark._param_ids_from._param_ids_generated
|
||||
if generated_ids is not None:
|
||||
ids = generated_ids
|
||||
|
||||
ids = self._resolve_parameter_set_ids(
|
||||
argnames, ids, parametersets, nodeid=self.definition.nodeid
|
||||
)
|
||||
|
||||
# Store used (possibly generated) ids with parametrize Marks.
|
||||
if _param_mark and _param_mark._param_ids_from and generated_ids is None:
|
||||
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
|
||||
|
||||
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
|
||||
# more than once) then we accumulate those calls generating the cartesian product
|
||||
# of all calls.
|
||||
newcalls = []
|
||||
for callspec in self._calls or [CallSpec2()]:
|
||||
for param_index, (param_id, param_set) in enumerate(
|
||||
zip(ids, parametersets)
|
||||
):
|
||||
newcallspec = callspec.setmulti(
|
||||
valtypes=arg_values_types,
|
||||
argnames=argnames,
|
||||
valset=param_set.values,
|
||||
id=param_id,
|
||||
marks=param_set.marks,
|
||||
scope=scope_,
|
||||
param_index=param_index,
|
||||
)
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
|
||||
def _resolve_parameter_set_ids(
|
||||
self,
|
||||
argnames: Sequence[str],
|
||||
ids: Optional[
|
||||
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
],
|
||||
parametersets: Sequence[ParameterSet],
|
||||
nodeid: str,
|
||||
) -> List[str]:
|
||||
"""Resolve the actual ids for the given parameter sets.
|
||||
|
||||
:param argnames:
|
||||
Argument names passed to ``parametrize()``.
|
||||
:param ids:
|
||||
The `ids` parameter of the ``parametrize()`` call (see docs).
|
||||
:param parametersets:
|
||||
The parameter sets, each containing a set of values corresponding
|
||||
to ``argnames``.
|
||||
:param nodeid str:
|
||||
The nodeid of the definition item that generated this
|
||||
parametrization.
|
||||
:returns:
|
||||
List with ids for each parameter set given.
|
||||
"""
|
||||
if ids is None:
|
||||
idfn = None
|
||||
ids_ = None
|
||||
elif callable(ids):
|
||||
idfn = ids
|
||||
ids_ = None
|
||||
else:
|
||||
idfn = None
|
||||
ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
|
||||
id_maker = IdMaker(
|
||||
argnames,
|
||||
parametersets,
|
||||
idfn,
|
||||
ids_,
|
||||
self.config,
|
||||
nodeid=nodeid,
|
||||
func_name=self.function.__name__,
|
||||
)
|
||||
return id_maker.make_unique_parameterset_ids()
|
||||
|
||||
def _validate_ids(
|
||||
self,
|
||||
ids: Iterable[Optional[object]],
|
||||
parametersets: Sequence[ParameterSet],
|
||||
func_name: str,
|
||||
) -> List[Optional[object]]:
|
||||
try:
|
||||
num_ids = len(ids) # type: ignore[arg-type]
|
||||
except TypeError:
|
||||
try:
|
||||
iter(ids)
|
||||
except TypeError as e:
|
||||
raise TypeError("ids must be a callable or an iterable") from e
|
||||
num_ids = len(parametersets)
|
||||
|
||||
# num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
|
||||
if num_ids != len(parametersets) and num_ids != 0:
|
||||
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
|
||||
fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
|
||||
|
||||
return list(itertools.islice(ids, num_ids))
|
||||
|
||||
def _resolve_arg_value_types(
|
||||
self,
|
||||
argnames: Sequence[str],
|
||||
indirect: Union[bool, Sequence[str]],
|
||||
) -> Dict[str, "Literal['params', 'funcargs']"]:
|
||||
"""Resolve if each parametrized argument must be considered a
|
||||
parameter to a fixture or a "funcarg" to the function, based on the
|
||||
``indirect`` parameter of the parametrized() call.
|
||||
|
||||
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||
:param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
|
||||
:rtype: Dict[str, str]
|
||||
A dict mapping each arg name to either:
|
||||
* "params" if the argname should be the parameter of a fixture of the same name.
|
||||
* "funcargs" if the argname should be a parameter to the parametrized test function.
|
||||
"""
|
||||
if isinstance(indirect, bool):
|
||||
valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys(
|
||||
argnames, "params" if indirect else "funcargs"
|
||||
)
|
||||
elif isinstance(indirect, Sequence):
|
||||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
for arg in indirect:
|
||||
if arg not in argnames:
|
||||
fail(
|
||||
"In {}: indirect fixture '{}' doesn't exist".format(
|
||||
self.function.__name__, arg
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
valtypes[arg] = "params"
|
||||
else:
|
||||
fail(
|
||||
"In {func}: expected Sequence or boolean for indirect, got {type}".format(
|
||||
type=type(indirect).__name__, func=self.function.__name__
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
return valtypes
|
||||
|
||||
def _validate_if_using_arg_names(
|
||||
self,
|
||||
argnames: Sequence[str],
|
||||
indirect: Union[bool, Sequence[str]],
|
||||
) -> None:
|
||||
"""Check if all argnames are being used, by default values, or directly/indirectly.
|
||||
|
||||
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||
:param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
|
||||
:raises ValueError: If validation fails.
|
||||
"""
|
||||
default_arg_names = set(get_default_arg_names(self.function))
|
||||
func_name = self.function.__name__
|
||||
for arg in argnames:
|
||||
if arg not in self.fixturenames:
|
||||
if arg in default_arg_names:
|
||||
fail(
|
||||
"In {}: function already takes an argument '{}' with a default value".format(
|
||||
func_name, arg
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
else:
|
||||
if isinstance(indirect, Sequence):
|
||||
name = "fixture" if arg in indirect else "argument"
|
||||
else:
|
||||
name = "fixture" if indirect else "argument"
|
||||
fail(
|
||||
f"In {func_name}: function uses no {name} '{arg}'",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
|
||||
def _find_parametrized_scope(
|
||||
argnames: Sequence[str],
|
||||
arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]],
|
||||
indirect: Union[bool, Sequence[str]],
|
||||
) -> Scope:
|
||||
"""Find the most appropriate scope for a parametrized call based on its arguments.
|
||||
|
||||
When there's at least one direct argument, always use "function" scope.
|
||||
|
||||
When a test function is parametrized and all its arguments are indirect
|
||||
(e.g. fixtures), return the most narrow scope based on the fixtures used.
|
||||
|
||||
Related to issue #1832, based on code posted by @Kingdread.
|
||||
"""
|
||||
if isinstance(indirect, Sequence):
|
||||
all_arguments_are_fixtures = len(indirect) == len(argnames)
|
||||
else:
|
||||
all_arguments_are_fixtures = bool(indirect)
|
||||
|
||||
if all_arguments_are_fixtures:
|
||||
fixturedefs = arg2fixturedefs or {}
|
||||
used_scopes = [
|
||||
fixturedef[0]._scope
|
||||
for name, fixturedef in fixturedefs.items()
|
||||
if name in argnames
|
||||
]
|
||||
# Takes the most narrow scope from used fixtures.
|
||||
return min(used_scopes, default=Scope.Function)
|
||||
|
||||
return Scope.Function
|
|
@ -49,7 +49,6 @@ from _pytest.pytester import RecordedHookCall
|
|||
from _pytest.pytester import RunResult
|
||||
from _pytest.python import Class
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Metafunc
|
||||
from _pytest.python import Module
|
||||
from _pytest.python import Package
|
||||
from _pytest.python_api import approx
|
||||
|
@ -116,7 +115,6 @@ __all__ = [
|
|||
"Mark",
|
||||
"MarkDecorator",
|
||||
"MarkGenerator",
|
||||
"Metafunc",
|
||||
"Module",
|
||||
"MonkeyPatch",
|
||||
"OptionGroup",
|
||||
|
|
|
@ -16,6 +16,7 @@ from typing import Union
|
|||
import hypothesis
|
||||
from hypothesis import strategies
|
||||
|
||||
import _pytest.python.metafunc
|
||||
import pytest
|
||||
from _pytest import fixtures
|
||||
from _pytest import python
|
||||
|
@ -24,12 +25,12 @@ from _pytest.compat import getfuncargnames
|
|||
from _pytest.compat import NOTSET
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.python import IdMaker
|
||||
from _pytest.python.metafunc import IdMaker
|
||||
from _pytest.scope import Scope
|
||||
|
||||
|
||||
class TestMetafunc:
|
||||
def Metafunc(self, func, config=None) -> python.Metafunc:
|
||||
def Metafunc(self, func, config=None) -> _pytest.python.metafunc.Metafunc:
|
||||
# The unit tests of this class check if things work correctly
|
||||
# on the funcarg level, so we don't need a full blown
|
||||
# initialization.
|
||||
|
@ -47,7 +48,9 @@ class TestMetafunc:
|
|||
names = getfuncargnames(func)
|
||||
fixtureinfo: Any = FuncFixtureInfoMock(names)
|
||||
definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid")
|
||||
return python.Metafunc(definition, fixtureinfo, config, _ispytest=True)
|
||||
return _pytest.python.metafunc.Metafunc(
|
||||
definition, fixtureinfo, config, _ispytest=True
|
||||
)
|
||||
|
||||
def test_no_funcargs(self) -> None:
|
||||
def function():
|
||||
|
@ -138,7 +141,7 @@ class TestMetafunc:
|
|||
|
||||
def test_find_parametrized_scope(self) -> None:
|
||||
"""Unit test for _find_parametrized_scope (#3941)."""
|
||||
from _pytest.python import _find_parametrized_scope
|
||||
from _pytest.python.metafunc import _find_parametrized_scope
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DummyFixtureDef:
|
||||
|
|
Loading…
Reference in New Issue