|
|
|
|
|
from __future__ import annotations |
|
|
|
|
|
import collections.abc |
|
|
import dataclasses |
|
|
import inspect |
|
|
from typing import Any |
|
|
from typing import Callable |
|
|
from typing import Collection |
|
|
from typing import final |
|
|
from typing import Iterable |
|
|
from typing import Iterator |
|
|
from typing import Mapping |
|
|
from typing import MutableMapping |
|
|
from typing import NamedTuple |
|
|
from typing import overload |
|
|
from typing import Sequence |
|
|
from typing import TYPE_CHECKING |
|
|
from typing import TypeVar |
|
|
from typing import Union |
|
|
import warnings |
|
|
|
|
|
from .._code import getfslineno |
|
|
from ..compat import ascii_escaped |
|
|
from ..compat import NOTSET |
|
|
from ..compat import NotSetType |
|
|
from _pytest.config import Config |
|
|
from _pytest.deprecated import check_ispytest |
|
|
from _pytest.deprecated import MARKED_FIXTURE |
|
|
from _pytest.outcomes import fail |
|
|
from _pytest.scope import _ScopeName |
|
|
from _pytest.warning_types import PytestUnknownMarkWarning |
|
|
|
|
|
|
|
|
if TYPE_CHECKING: |
|
|
from ..nodes import Node |
|
|
|
|
|
|
|
|
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" |
|
|
|
|
|
|
|
|
def istestfunc(func) -> bool: |
|
|
return callable(func) and getattr(func, "__name__", "<lambda>") != "<lambda>" |
|
|
|
|
|
|
|
|
def get_empty_parameterset_mark( |
|
|
config: Config, argnames: Sequence[str], func |
|
|
) -> MarkDecorator: |
|
|
from ..nodes import Collector |
|
|
|
|
|
fs, lineno = getfslineno(func) |
|
|
reason = "got empty parameter set %r, function %s at %s:%d" % ( |
|
|
argnames, |
|
|
func.__name__, |
|
|
fs, |
|
|
lineno, |
|
|
) |
|
|
|
|
|
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) |
|
|
if requested_mark in ("", None, "skip"): |
|
|
mark = MARK_GEN.skip(reason=reason) |
|
|
elif requested_mark == "xfail": |
|
|
mark = MARK_GEN.xfail(reason=reason, run=False) |
|
|
elif requested_mark == "fail_at_collect": |
|
|
f_name = func.__name__ |
|
|
_, lineno = getfslineno(func) |
|
|
raise Collector.CollectError( |
|
|
"Empty parameter set in '%s' at line %d" % (f_name, lineno + 1) |
|
|
) |
|
|
else: |
|
|
raise LookupError(requested_mark) |
|
|
return mark |
|
|
|
|
|
|
|
|
class ParameterSet(NamedTuple): |
|
|
values: Sequence[object | NotSetType] |
|
|
marks: Collection[MarkDecorator | Mark] |
|
|
id: str | None |
|
|
|
|
|
@classmethod |
|
|
def param( |
|
|
cls, |
|
|
*values: object, |
|
|
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (), |
|
|
id: str | None = None, |
|
|
) -> ParameterSet: |
|
|
if isinstance(marks, MarkDecorator): |
|
|
marks = (marks,) |
|
|
else: |
|
|
assert isinstance(marks, collections.abc.Collection) |
|
|
|
|
|
if id is not None: |
|
|
if not isinstance(id, str): |
|
|
raise TypeError(f"Expected id to be a string, got {type(id)}: {id!r}") |
|
|
id = ascii_escaped(id) |
|
|
return cls(values, marks, id) |
|
|
|
|
|
@classmethod |
|
|
def extract_from( |
|
|
cls, |
|
|
parameterset: ParameterSet | Sequence[object] | object, |
|
|
force_tuple: bool = False, |
|
|
) -> ParameterSet: |
|
|
"""Extract from an object or objects. |
|
|
|
|
|
:param parameterset: |
|
|
A legacy style parameterset that may or may not be a tuple, |
|
|
and may or may not be wrapped into a mess of mark objects. |
|
|
|
|
|
:param force_tuple: |
|
|
Enforce tuple wrapping so single argument tuple values |
|
|
don't get decomposed and break tests. |
|
|
""" |
|
|
if isinstance(parameterset, cls): |
|
|
return parameterset |
|
|
if force_tuple: |
|
|
return cls.param(parameterset) |
|
|
else: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return cls(parameterset, marks=[], id=None) |
|
|
|
|
|
@staticmethod |
|
|
def _parse_parametrize_args( |
|
|
argnames: str | Sequence[str], |
|
|
argvalues: Iterable[ParameterSet | Sequence[object] | object], |
|
|
*args, |
|
|
**kwargs, |
|
|
) -> tuple[Sequence[str], bool]: |
|
|
if isinstance(argnames, str): |
|
|
argnames = [x.strip() for x in argnames.split(",") if x.strip()] |
|
|
force_tuple = len(argnames) == 1 |
|
|
else: |
|
|
force_tuple = False |
|
|
return argnames, force_tuple |
|
|
|
|
|
@staticmethod |
|
|
def _parse_parametrize_parameters( |
|
|
argvalues: Iterable[ParameterSet | Sequence[object] | object], |
|
|
force_tuple: bool, |
|
|
) -> list[ParameterSet]: |
|
|
return [ |
|
|
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues |
|
|
] |
|
|
|
|
|
@classmethod |
|
|
def _for_parametrize( |
|
|
cls, |
|
|
argnames: str | Sequence[str], |
|
|
argvalues: Iterable[ParameterSet | Sequence[object] | object], |
|
|
func, |
|
|
config: Config, |
|
|
nodeid: str, |
|
|
) -> tuple[Sequence[str], list[ParameterSet]]: |
|
|
argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) |
|
|
parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) |
|
|
del argvalues |
|
|
|
|
|
if parameters: |
|
|
|
|
|
for param in parameters: |
|
|
if len(param.values) != len(argnames): |
|
|
msg = ( |
|
|
'{nodeid}: in "parametrize" the number of names ({names_len}):\n' |
|
|
" {names}\n" |
|
|
"must be equal to the number of values ({values_len}):\n" |
|
|
" {values}" |
|
|
) |
|
|
fail( |
|
|
msg.format( |
|
|
nodeid=nodeid, |
|
|
values=param.values, |
|
|
names=argnames, |
|
|
names_len=len(argnames), |
|
|
values_len=len(param.values), |
|
|
), |
|
|
pytrace=False, |
|
|
) |
|
|
else: |
|
|
|
|
|
|
|
|
mark = get_empty_parameterset_mark(config, argnames, func) |
|
|
parameters.append( |
|
|
ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None) |
|
|
) |
|
|
return argnames, parameters |
|
|
|
|
|
|
|
|
@final |
|
|
@dataclasses.dataclass(frozen=True) |
|
|
class Mark: |
|
|
"""A pytest mark.""" |
|
|
|
|
|
|
|
|
name: str |
|
|
|
|
|
args: tuple[Any, ...] |
|
|
|
|
|
kwargs: Mapping[str, Any] |
|
|
|
|
|
|
|
|
_param_ids_from: Mark | None = dataclasses.field(default=None, repr=False) |
|
|
|
|
|
_param_ids_generated: Sequence[str] | None = dataclasses.field( |
|
|
default=None, repr=False |
|
|
) |
|
|
|
|
|
def __init__( |
|
|
self, |
|
|
name: str, |
|
|
args: tuple[Any, ...], |
|
|
kwargs: Mapping[str, Any], |
|
|
param_ids_from: Mark | None = None, |
|
|
param_ids_generated: Sequence[str] | None = None, |
|
|
*, |
|
|
_ispytest: bool = False, |
|
|
) -> None: |
|
|
""":meta private:""" |
|
|
check_ispytest(_ispytest) |
|
|
|
|
|
object.__setattr__(self, "name", name) |
|
|
object.__setattr__(self, "args", args) |
|
|
object.__setattr__(self, "kwargs", kwargs) |
|
|
object.__setattr__(self, "_param_ids_from", param_ids_from) |
|
|
object.__setattr__(self, "_param_ids_generated", param_ids_generated) |
|
|
|
|
|
def _has_param_ids(self) -> bool: |
|
|
return "ids" in self.kwargs or len(self.args) >= 4 |
|
|
|
|
|
def combined_with(self, other: Mark) -> Mark: |
|
|
"""Return a new Mark which is a combination of this |
|
|
Mark and another Mark. |
|
|
|
|
|
Combines by appending args and merging kwargs. |
|
|
|
|
|
:param Mark other: The mark to combine with. |
|
|
:rtype: Mark |
|
|
""" |
|
|
assert self.name == other.name |
|
|
|
|
|
|
|
|
param_ids_from: Mark | None = None |
|
|
if self.name == "parametrize": |
|
|
if other._has_param_ids(): |
|
|
param_ids_from = other |
|
|
elif self._has_param_ids(): |
|
|
param_ids_from = self |
|
|
|
|
|
return Mark( |
|
|
self.name, |
|
|
self.args + other.args, |
|
|
dict(self.kwargs, **other.kwargs), |
|
|
param_ids_from=param_ids_from, |
|
|
_ispytest=True, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type]) |
|
|
|
|
|
|
|
|
@dataclasses.dataclass |
|
|
class MarkDecorator: |
|
|
"""A decorator for applying a mark on test functions and classes. |
|
|
|
|
|
``MarkDecorators`` are created with ``pytest.mark``:: |
|
|
|
|
|
mark1 = pytest.mark.NAME # Simple MarkDecorator |
|
|
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator |
|
|
|
|
|
and can then be applied as decorators to test functions:: |
|
|
|
|
|
@mark2 |
|
|
def test_function(): |
|
|
pass |
|
|
|
|
|
When a ``MarkDecorator`` is called, it does the following: |
|
|
|
|
|
1. If called with a single class as its only positional argument and no |
|
|
additional keyword arguments, it attaches the mark to the class so it |
|
|
gets applied automatically to all test cases found in that class. |
|
|
|
|
|
2. If called with a single function as its only positional argument and |
|
|
no additional keyword arguments, it attaches the mark to the function, |
|
|
containing all the arguments already stored internally in the |
|
|
``MarkDecorator``. |
|
|
|
|
|
3. When called in any other case, it returns a new ``MarkDecorator`` |
|
|
instance with the original ``MarkDecorator``'s content updated with |
|
|
the arguments passed to this call. |
|
|
|
|
|
Note: The rules above prevent a ``MarkDecorator`` from storing only a |
|
|
single function or class reference as its positional argument with no |
|
|
additional keyword or positional arguments. You can work around this by |
|
|
using `with_args()`. |
|
|
""" |
|
|
|
|
|
mark: Mark |
|
|
|
|
|
def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None: |
|
|
""":meta private:""" |
|
|
check_ispytest(_ispytest) |
|
|
self.mark = mark |
|
|
|
|
|
@property |
|
|
def name(self) -> str: |
|
|
"""Alias for mark.name.""" |
|
|
return self.mark.name |
|
|
|
|
|
@property |
|
|
def args(self) -> tuple[Any, ...]: |
|
|
"""Alias for mark.args.""" |
|
|
return self.mark.args |
|
|
|
|
|
@property |
|
|
def kwargs(self) -> Mapping[str, Any]: |
|
|
"""Alias for mark.kwargs.""" |
|
|
return self.mark.kwargs |
|
|
|
|
|
@property |
|
|
def markname(self) -> str: |
|
|
""":meta private:""" |
|
|
return self.name |
|
|
|
|
|
def with_args(self, *args: object, **kwargs: object) -> MarkDecorator: |
|
|
"""Return a MarkDecorator with extra arguments added. |
|
|
|
|
|
Unlike calling the MarkDecorator, with_args() can be used even |
|
|
if the sole argument is a callable/class. |
|
|
""" |
|
|
mark = Mark(self.name, args, kwargs, _ispytest=True) |
|
|
return MarkDecorator(self.mark.combined_with(mark), _ispytest=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overload |
|
|
def __call__(self, arg: Markable) -> Markable: |
|
|
pass |
|
|
|
|
|
@overload |
|
|
def __call__(self, *args: object, **kwargs: object) -> MarkDecorator: |
|
|
pass |
|
|
|
|
|
def __call__(self, *args: object, **kwargs: object): |
|
|
"""Call the MarkDecorator.""" |
|
|
if args and not kwargs: |
|
|
func = args[0] |
|
|
is_class = inspect.isclass(func) |
|
|
if len(args) == 1 and (istestfunc(func) or is_class): |
|
|
store_mark(func, self.mark, stacklevel=3) |
|
|
return func |
|
|
return self.with_args(*args, **kwargs) |
|
|
|
|
|
|
|
|
def get_unpacked_marks( |
|
|
obj: object | type, |
|
|
*, |
|
|
consider_mro: bool = True, |
|
|
) -> list[Mark]: |
|
|
"""Obtain the unpacked marks that are stored on an object. |
|
|
|
|
|
If obj is a class and consider_mro is true, return marks applied to |
|
|
this class and all of its super-classes in MRO order. If consider_mro |
|
|
is false, only return marks applied directly to this class. |
|
|
""" |
|
|
if isinstance(obj, type): |
|
|
if not consider_mro: |
|
|
mark_lists = [obj.__dict__.get("pytestmark", [])] |
|
|
else: |
|
|
mark_lists = [ |
|
|
x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__) |
|
|
] |
|
|
mark_list = [] |
|
|
for item in mark_lists: |
|
|
if isinstance(item, list): |
|
|
mark_list.extend(item) |
|
|
else: |
|
|
mark_list.append(item) |
|
|
else: |
|
|
mark_attribute = getattr(obj, "pytestmark", []) |
|
|
if isinstance(mark_attribute, list): |
|
|
mark_list = mark_attribute |
|
|
else: |
|
|
mark_list = [mark_attribute] |
|
|
return list(normalize_mark_list(mark_list)) |
|
|
|
|
|
|
|
|
def normalize_mark_list( |
|
|
mark_list: Iterable[Mark | MarkDecorator], |
|
|
) -> Iterable[Mark]: |
|
|
""" |
|
|
Normalize an iterable of Mark or MarkDecorator objects into a list of marks |
|
|
by retrieving the `mark` attribute on MarkDecorator instances. |
|
|
|
|
|
:param mark_list: marks to normalize |
|
|
:returns: A new list of the extracted Mark objects |
|
|
""" |
|
|
for mark in mark_list: |
|
|
mark_obj = getattr(mark, "mark", mark) |
|
|
if not isinstance(mark_obj, Mark): |
|
|
raise TypeError(f"got {mark_obj!r} instead of Mark") |
|
|
yield mark_obj |
|
|
|
|
|
|
|
|
def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None: |
|
|
"""Store a Mark on an object. |
|
|
|
|
|
This is used to implement the Mark declarations/decorators correctly. |
|
|
""" |
|
|
assert isinstance(mark, Mark), mark |
|
|
|
|
|
from ..fixtures import getfixturemarker |
|
|
|
|
|
if getfixturemarker(obj) is not None: |
|
|
warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel) |
|
|
|
|
|
|
|
|
|
|
|
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING: |
|
|
|
|
|
class _SkipMarkDecorator(MarkDecorator): |
|
|
@overload |
|
|
def __call__(self, arg: Markable) -> Markable: ... |
|
|
|
|
|
@overload |
|
|
def __call__(self, reason: str = ...) -> MarkDecorator: ... |
|
|
|
|
|
class _SkipifMarkDecorator(MarkDecorator): |
|
|
def __call__( |
|
|
self, |
|
|
condition: str | bool = ..., |
|
|
*conditions: str | bool, |
|
|
reason: str = ..., |
|
|
) -> MarkDecorator: ... |
|
|
|
|
|
class _XfailMarkDecorator(MarkDecorator): |
|
|
@overload |
|
|
def __call__(self, arg: Markable) -> Markable: ... |
|
|
|
|
|
@overload |
|
|
def __call__( |
|
|
self, |
|
|
condition: str | bool = False, |
|
|
*conditions: str | bool, |
|
|
reason: str = ..., |
|
|
run: bool = ..., |
|
|
raises: None | type[BaseException] | tuple[type[BaseException], ...] = ..., |
|
|
strict: bool = ..., |
|
|
) -> MarkDecorator: ... |
|
|
|
|
|
class _ParametrizeMarkDecorator(MarkDecorator): |
|
|
def __call__( |
|
|
self, |
|
|
argnames: str | Sequence[str], |
|
|
argvalues: Iterable[ParameterSet | Sequence[object] | object], |
|
|
*, |
|
|
indirect: bool | Sequence[str] = ..., |
|
|
ids: Iterable[None | str | float | int | bool] |
|
|
| Callable[[Any], object | None] |
|
|
| None = ..., |
|
|
scope: _ScopeName | None = ..., |
|
|
) -> MarkDecorator: ... |
|
|
|
|
|
class _UsefixturesMarkDecorator(MarkDecorator): |
|
|
def __call__(self, *fixtures: str) -> MarkDecorator: |
|
|
... |
|
|
|
|
|
class _FilterwarningsMarkDecorator(MarkDecorator): |
|
|
def __call__(self, *filters: str) -> MarkDecorator: |
|
|
... |
|
|
|
|
|
|
|
|
@final |
|
|
class MarkGenerator: |
|
|
"""Factory for :class:`MarkDecorator` objects - exposed as |
|
|
a ``pytest.mark`` singleton instance. |
|
|
|
|
|
Example:: |
|
|
|
|
|
import pytest |
|
|
|
|
|
|
|
|
@pytest.mark.slowtest |
|
|
def test_function(): |
|
|
pass |
|
|
|
|
|
applies a 'slowtest' :class:`Mark` on ``test_function``. |
|
|
""" |
|
|
|
|
|
|
|
|
if TYPE_CHECKING: |
|
|
skip: _SkipMarkDecorator |
|
|
skipif: _SkipifMarkDecorator |
|
|
xfail: _XfailMarkDecorator |
|
|
parametrize: _ParametrizeMarkDecorator |
|
|
usefixtures: _UsefixturesMarkDecorator |
|
|
filterwarnings: _FilterwarningsMarkDecorator |
|
|
|
|
|
def __init__(self, *, _ispytest: bool = False) -> None: |
|
|
check_ispytest(_ispytest) |
|
|
self._config: Config | None = None |
|
|
self._markers: set[str] = set() |
|
|
|
|
|
def __getattr__(self, name: str) -> MarkDecorator: |
|
|
"""Generate a new :class:`MarkDecorator` with the given name.""" |
|
|
if name[0] == "_": |
|
|
raise AttributeError("Marker name must NOT start with underscore") |
|
|
|
|
|
if self._config is not None: |
|
|
|
|
|
|
|
|
|
|
|
if name not in self._markers: |
|
|
for line in self._config.getini("markers"): |
|
|
|
|
|
|
|
|
|
|
|
marker = line.split(":")[0].split("(")[0].strip() |
|
|
self._markers.add(marker) |
|
|
|
|
|
|
|
|
|
|
|
if name not in self._markers: |
|
|
if self._config.option.strict_markers or self._config.option.strict: |
|
|
fail( |
|
|
f"{name!r} not found in `markers` configuration option", |
|
|
pytrace=False, |
|
|
) |
|
|
|
|
|
|
|
|
if name in ["parameterize", "parametrise", "parameterise"]: |
|
|
__tracebackhide__ = True |
|
|
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?") |
|
|
|
|
|
warnings.warn( |
|
|
f"Unknown pytest.mark.{name} - is this a typo? You can register " |
|
|
"custom marks to avoid this warning - for details, see " |
|
|
"https://docs.pytest.org/en/stable/how-to/mark.html", |
|
|
PytestUnknownMarkWarning, |
|
|
2, |
|
|
) |
|
|
|
|
|
return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True) |
|
|
|
|
|
|
|
|
MARK_GEN = MarkGenerator(_ispytest=True) |
|
|
|
|
|
|
|
|
@final |
|
|
class NodeKeywords(MutableMapping[str, Any]): |
|
|
__slots__ = ("node", "parent", "_markers") |
|
|
|
|
|
def __init__(self, node: Node) -> None: |
|
|
self.node = node |
|
|
self.parent = node.parent |
|
|
self._markers = {node.name: True} |
|
|
|
|
|
def __getitem__(self, key: str) -> Any: |
|
|
try: |
|
|
return self._markers[key] |
|
|
except KeyError: |
|
|
if self.parent is None: |
|
|
raise |
|
|
return self.parent.keywords[key] |
|
|
|
|
|
def __setitem__(self, key: str, value: Any) -> None: |
|
|
self._markers[key] = value |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __contains__(self, key: object) -> bool: |
|
|
return ( |
|
|
key in self._markers |
|
|
or self.parent is not None |
|
|
and key in self.parent.keywords |
|
|
) |
|
|
|
|
|
def update( |
|
|
self, |
|
|
other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (), |
|
|
**kwds: Any, |
|
|
) -> None: |
|
|
self._markers.update(other) |
|
|
self._markers.update(kwds) |
|
|
|
|
|
def __delitem__(self, key: str) -> None: |
|
|
raise ValueError("cannot delete key in keywords dict") |
|
|
|
|
|
def __iter__(self) -> Iterator[str]: |
|
|
|
|
|
yield from self._markers |
|
|
if self.parent is not None: |
|
|
for keyword in self.parent.keywords: |
|
|
|
|
|
if keyword not in self._markers: |
|
|
yield keyword |
|
|
|
|
|
def __len__(self) -> int: |
|
|
|
|
|
return sum(1 for keyword in self) |
|
|
|
|
|
def __repr__(self) -> str: |
|
|
return f"<NodeKeywords for node {self.node}>" |
|
|
|