This commit is contained in:
Iliyan Angelov
2025-12-06 03:27:35 +02:00
parent 7667eb5eda
commit 5a8ca3c475
2211 changed files with 28086 additions and 37066 deletions

View File

@@ -1,37 +1,36 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import collections.abc
from collections.abc import Callable
from collections.abc import Collection
from collections.abc import Iterable
from collections.abc import Iterator
from collections.abc import Mapping
from collections.abc import MutableMapping
from collections.abc import Sequence
import dataclasses
import enum
import inspect
import warnings
from typing import Any
from typing import final
from typing import Callable
from typing import Collection
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import NamedTuple
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
import warnings
from typing import Union
from .._code import getfslineno
from ..compat import ascii_escaped
from ..compat import final
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.raises import AbstractRaises
from _pytest.scope import _ScopeName
from _pytest.warning_types import PytestUnknownMarkWarning
if TYPE_CHECKING:
from ..nodes import Node
@@ -39,37 +38,33 @@ if TYPE_CHECKING:
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
# Singleton type for HIDDEN_PARAM, as described in:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class _HiddenParam(enum.Enum):
token = 0
#: Can be used as a parameter set id to hide it from the test name.
HIDDEN_PARAM = _HiddenParam.token
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:
) -> "MarkDecorator":
from ..nodes import Collector
argslisting = ", ".join(argnames)
fs, lineno = getfslineno(func)
reason = "got empty parameter set %r, function %s at %s:%d" % (
argnames,
func.__name__,
fs,
lineno,
)
_fs, lineno = getfslineno(func)
reason = f"got empty parameter set for ({argslisting})"
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(
f"Empty parameter set in '{func.__name__}' at line {lineno + 1}"
"Empty parameter set in '%s' at line %d" % (f_name, lineno + 1)
)
else:
raise LookupError(requested_mark)
@@ -77,68 +72,34 @@ def get_empty_parameterset_mark(
class ParameterSet(NamedTuple):
"""A set of values for a set of parameters along with associated marks and
an optional ID for the set.
Examples::
pytest.param(1, 2, 3)
# ParameterSet(values=(1, 2, 3), marks=(), id=None)
pytest.param("hello", id="greeting")
# ParameterSet(values=("hello",), marks=(), id="greeting")
# Parameter set with marks
pytest.param(42, marks=pytest.mark.xfail)
# ParameterSet(values=(42,), marks=(MarkDecorator(...),), id=None)
# From parametrize mark (parameter names + list of parameter sets)
pytest.mark.parametrize(
("a", "b", "expected"),
[
(1, 2, 3),
pytest.param(40, 2, 42, id="everything"),
],
)
# ParameterSet(values=(1, 2, 3), marks=(), id=None)
# ParameterSet(values=(40, 2, 42), marks=(), id="everything")
"""
values: Sequence[object | NotSetType]
marks: Collection[MarkDecorator | Mark]
id: str | _HiddenParam | None
values: Sequence[Union[object, NotSetType]]
marks: Collection[Union["MarkDecorator", "Mark"]]
id: Optional[str]
@classmethod
def param(
cls,
*values: object,
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
id: str | _HiddenParam | None = None,
) -> ParameterSet:
marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),
id: Optional[str] = None,
) -> "ParameterSet":
if isinstance(marks, MarkDecorator):
marks = (marks,)
else:
assert isinstance(marks, collections.abc.Collection)
if any(i.name == "usefixtures" for i in marks):
raise ValueError(
"pytest.param cannot add pytest.mark.usefixtures; see "
"https://docs.pytest.org/en/stable/reference/reference.html#pytest-param"
)
if id is not None:
if not isinstance(id, str) and id is not HIDDEN_PARAM:
raise TypeError(
"Expected id to be a string or a `pytest.HIDDEN_PARAM` sentinel, "
f"got {type(id)}: {id!r}",
)
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,
parameterset: Union["ParameterSet", Sequence[object], object],
force_tuple: bool = False,
) -> ParameterSet:
) -> "ParameterSet":
"""Extract from an object or objects.
:param parameterset:
@@ -149,6 +110,7 @@ class ParameterSet(NamedTuple):
Enforce tuple wrapping so single argument tuple values
don't get decomposed and break tests.
"""
if isinstance(parameterset, cls):
return parameterset
if force_tuple:
@@ -163,11 +125,11 @@ class ParameterSet(NamedTuple):
@staticmethod
def _parse_parametrize_args(
argnames: str | Sequence[str],
argvalues: Iterable[ParameterSet | Sequence[object] | object],
argnames: Union[str, Sequence[str]],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
*args,
**kwargs,
) -> tuple[Sequence[str], bool]:
) -> Tuple[Sequence[str], bool]:
if isinstance(argnames, str):
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1
@@ -177,9 +139,9 @@ class ParameterSet(NamedTuple):
@staticmethod
def _parse_parametrize_parameters(
argvalues: Iterable[ParameterSet | Sequence[object] | object],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
force_tuple: bool,
) -> list[ParameterSet]:
) -> List["ParameterSet"]:
return [
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
]
@@ -187,12 +149,12 @@ class ParameterSet(NamedTuple):
@classmethod
def _for_parametrize(
cls,
argnames: str | Sequence[str],
argvalues: Iterable[ParameterSet | Sequence[object] | object],
argnames: Union[str, Sequence[str]],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
func,
config: Config,
nodeid: str,
) -> tuple[Sequence[str], list[ParameterSet]]:
) -> Tuple[Sequence[str], List["ParameterSet"]]:
argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
del argvalues
@@ -222,9 +184,7 @@ class ParameterSet(NamedTuple):
# parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
mark = get_empty_parameterset_mark(config, argnames, func)
parameters.append(
ParameterSet(
values=(NOTSET,) * len(argnames), marks=[mark], id="NOTSET"
)
ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
)
return argnames, parameters
@@ -237,24 +197,24 @@ class Mark:
#: Name of the mark.
name: str
#: Positional arguments of the mark decorator.
args: tuple[Any, ...]
args: Tuple[Any, ...]
#: Keyword arguments of the mark decorator.
kwargs: Mapping[str, Any]
#: Source Mark for ids with parametrize Marks.
_param_ids_from: Mark | None = dataclasses.field(default=None, repr=False)
_param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False)
#: Resolved/generated ids with parametrize Marks.
_param_ids_generated: Sequence[str] | None = dataclasses.field(
_param_ids_generated: Optional[Sequence[str]] = dataclasses.field(
default=None, repr=False
)
def __init__(
self,
name: str,
args: tuple[Any, ...],
args: Tuple[Any, ...],
kwargs: Mapping[str, Any],
param_ids_from: Mark | None = None,
param_ids_generated: Sequence[str] | None = None,
param_ids_from: Optional["Mark"] = None,
param_ids_generated: Optional[Sequence[str]] = None,
*,
_ispytest: bool = False,
) -> None:
@@ -270,7 +230,7 @@ class Mark:
def _has_param_ids(self) -> bool:
return "ids" in self.kwargs or len(self.args) >= 4
def combined_with(self, other: Mark) -> Mark:
def combined_with(self, other: "Mark") -> "Mark":
"""Return a new Mark which is a combination of this
Mark and another Mark.
@@ -282,7 +242,7 @@ class Mark:
assert self.name == other.name
# Remember source of ids with parametrize Marks.
param_ids_from: Mark | None = None
param_ids_from: Optional[Mark] = None
if self.name == "parametrize":
if other._has_param_ids():
param_ids_from = other
@@ -301,7 +261,7 @@ class Mark:
# A generic parameter designating an object to which a Mark may
# be applied -- a test function (callable) or class.
# Note: a lambda is not allowed, but this can't be represented.
Markable = TypeVar("Markable", bound=Callable[..., object] | type)
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
@dataclasses.dataclass
@@ -310,8 +270,8 @@ class MarkDecorator:
``MarkDecorators`` are created with ``pytest.mark``::
mark1 = pytest.mark.NAME # Simple MarkDecorator
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
mark1 = pytest.mark.NAME # Simple MarkDecorator
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
and can then be applied as decorators to test functions::
@@ -353,7 +313,7 @@ class MarkDecorator:
return self.mark.name
@property
def args(self) -> tuple[Any, ...]:
def args(self) -> Tuple[Any, ...]:
"""Alias for mark.args."""
return self.mark.args
@@ -367,7 +327,7 @@ class MarkDecorator:
""":meta private:"""
return self.name # for backward-compat (2.4.1 had this attr)
def with_args(self, *args: object, **kwargs: object) -> MarkDecorator:
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
@@ -380,11 +340,11 @@ class MarkDecorator:
# return type. Not much we can do about that. Thankfully mypy picks
# the first match so it works out even if we break the rules.
@overload
def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap]
def __call__(self, arg: Markable) -> Markable: # type: ignore[misc]
pass
@overload
def __call__(self, *args: object, **kwargs: object) -> MarkDecorator:
def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator":
pass
def __call__(self, *args: object, **kwargs: object):
@@ -392,22 +352,17 @@ class MarkDecorator:
if args and not kwargs:
func = args[0]
is_class = inspect.isclass(func)
# For staticmethods/classmethods, the marks are eventually fetched from the
# function object, not the descriptor, so unwrap.
unwrapped_func = func
if isinstance(func, staticmethod | classmethod):
unwrapped_func = func.__func__
if len(args) == 1 and (istestfunc(unwrapped_func) or is_class):
store_mark(unwrapped_func, self.mark, stacklevel=3)
if len(args) == 1 and (istestfunc(func) or is_class):
store_mark(func, self.mark)
return func
return self.with_args(*args, **kwargs)
def get_unpacked_marks(
obj: object | type,
obj: Union[object, type],
*,
consider_mro: bool = True,
) -> list[Mark]:
) -> 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
@@ -437,7 +392,7 @@ def get_unpacked_marks(
def normalize_mark_list(
mark_list: Iterable[Mark | MarkDecorator],
mark_list: Iterable[Union[Mark, MarkDecorator]]
) -> Iterable[Mark]:
"""
Normalize an iterable of Mark or MarkDecorator objects into a list of marks
@@ -449,22 +404,16 @@ def normalize_mark_list(
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")
raise TypeError(f"got {repr(mark_obj)} instead of Mark")
yield mark_obj
def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
def store_mark(obj, mark: Mark) -> 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)
# Always reassign name to avoid updating pytestmark in a reference that
# was only borrowed.
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
@@ -473,52 +422,59 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
# Typing for builtin pytest marks. This is cheating; it gives builtin marks
# special privilege, and breaks modularity. But practicality beats purity...
if TYPE_CHECKING:
from _pytest.scope import _ScopeName
class _SkipMarkDecorator(MarkDecorator):
@overload # type: ignore[override,no-overload-impl]
def __call__(self, arg: Markable) -> Markable: ...
@overload # type: ignore[override,misc,no-overload-impl]
def __call__(self, arg: Markable) -> Markable:
...
@overload
def __call__(self, reason: str = ...) -> MarkDecorator: ...
def __call__(self, reason: str = ...) -> "MarkDecorator":
...
class _SkipifMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
self,
condition: str | bool = ...,
*conditions: str | bool,
condition: Union[str, bool] = ...,
*conditions: Union[str, bool],
reason: str = ...,
) -> MarkDecorator: ...
) -> MarkDecorator:
...
class _XfailMarkDecorator(MarkDecorator):
@overload # type: ignore[override,no-overload-impl]
def __call__(self, arg: Markable) -> Markable: ...
@overload # type: ignore[override,misc,no-overload-impl]
def __call__(self, arg: Markable) -> Markable:
...
@overload
def __call__(
self,
condition: str | bool = False,
*conditions: str | bool,
condition: Union[str, bool] = ...,
*conditions: Union[str, bool],
reason: str = ...,
run: bool = ...,
raises: None
| type[BaseException]
| tuple[type[BaseException], ...]
| AbstractRaises[BaseException] = ...,
raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
strict: bool = ...,
) -> MarkDecorator: ...
) -> MarkDecorator:
...
class _ParametrizeMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
self,
argnames: str | Sequence[str],
argvalues: Iterable[ParameterSet | Sequence[object] | object],
argnames: Union[str, Sequence[str]],
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
*,
indirect: bool | Sequence[str] = ...,
ids: Iterable[None | str | float | int | bool]
| Callable[[Any], object | None]
| None = ...,
scope: _ScopeName | None = ...,
) -> MarkDecorator: ...
indirect: Union[bool, Sequence[str]] = ...,
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = ...,
scope: Optional[_ScopeName] = ...,
) -> MarkDecorator:
...
class _UsefixturesMarkDecorator(MarkDecorator):
def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
@@ -538,10 +494,9 @@ class MarkGenerator:
import pytest
@pytest.mark.slowtest
def test_function():
pass
pass
applies a 'slowtest' :class:`Mark` on ``test_function``.
"""
@@ -557,8 +512,8 @@ class MarkGenerator:
def __init__(self, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._config: Config | None = None
self._markers: set[str] = set()
self._config: Optional[Config] = None
self._markers: Set[str] = set()
def __getattr__(self, name: str) -> MarkDecorator:
"""Generate a new :class:`MarkDecorator` with the given name."""
@@ -580,24 +535,21 @@ class MarkGenerator:
# If the name is not in the set of known marks after updating,
# then it really is time to issue a warning or an error.
if name not in self._markers:
# Raise a specific error for common misspellings of "parametrize".
if name in ["parameterize", "parametrise", "parameterise"]:
__tracebackhide__ = True
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
strict_markers = self._config.getini("strict_markers")
if strict_markers is None:
strict_markers = self._config.getini("strict")
if strict_markers:
if self._config.option.strict_markers or self._config.option.strict:
fail(
f"{name!r} not found in `markers` configuration option",
pytrace=False,
)
# Raise a specific error for common misspellings of "parametrize".
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 "
"Unknown pytest.mark.%s - 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",
"https://docs.pytest.org/en/stable/how-to/mark.html" % name,
PytestUnknownMarkWarning,
2,
)
@@ -610,9 +562,9 @@ MARK_GEN = MarkGenerator(_ispytest=True)
@final
class NodeKeywords(MutableMapping[str, Any]):
__slots__ = ("_markers", "node", "parent")
__slots__ = ("node", "parent", "_markers")
def __init__(self, node: Node) -> None:
def __init__(self, node: "Node") -> None:
self.node = node
self.parent = node.parent
self._markers = {node.name: True}
@@ -632,13 +584,15 @@ class NodeKeywords(MutableMapping[str, Any]):
# below and use the collections.abc fallback, but that would be slow.
def __contains__(self, key: object) -> bool:
return key in self._markers or (
self.parent is not None and key in self.parent.keywords
return (
key in self._markers
or self.parent is not None
and key in self.parent.keywords
)
def update( # type: ignore[override]
self,
other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (),
other: Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] = (),
**kwds: Any,
) -> None:
self._markers.update(other)