| | |
| | 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}>" |
| |
|