summaryrefslogtreecommitdiffstats
path: root/contrib/python/pytest/py3/_pytest/mark/structures.py
diff options
context:
space:
mode:
authorarcadia-devtools <[email protected]>2022-02-09 12:00:52 +0300
committerDaniil Cherednik <[email protected]>2022-02-10 15:58:17 +0300
commit8e1413fed79d1e8036e65228af6c93399ccf5502 (patch)
tree502c9df7b2614d20541c7a2d39d390e9a51877cc /contrib/python/pytest/py3/_pytest/mark/structures.py
parent6b813c17d56d1d05f92c61ddc347d0e4d358fe85 (diff)
intermediate changes
ref:614ed510ddd3cdf86a8c5dbf19afd113397e0172
Diffstat (limited to 'contrib/python/pytest/py3/_pytest/mark/structures.py')
-rw-r--r--contrib/python/pytest/py3/_pytest/mark/structures.py397
1 files changed, 281 insertions, 116 deletions
diff --git a/contrib/python/pytest/py3/_pytest/mark/structures.py b/contrib/python/pytest/py3/_pytest/mark/structures.py
index 50ad81baa69..f5736a4c1c0 100644
--- a/contrib/python/pytest/py3/_pytest/mark/structures.py
+++ b/contrib/python/pytest/py3/_pytest/mark/structures.py
@@ -1,39 +1,68 @@
+import collections.abc
import inspect
import warnings
-from collections import namedtuple
-from collections.abc import MutableMapping
+from typing import Any
+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
from typing import Union
import attr
-from .._code.source import getfslineno
+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.outcomes import fail
from _pytest.warning_types import PytestUnknownMarkWarning
+if TYPE_CHECKING:
+ from ..nodes import Node
+
+
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
-def istestfunc(func):
+def istestfunc(func) -> bool:
return (
hasattr(func, "__call__")
and getattr(func, "__name__", "<lambda>") != "<lambda>"
)
-def get_empty_parameterset_mark(config, argnames, func):
+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
+ mark = MARK_GEN.skip(reason=reason)
elif requested_mark == "xfail":
- mark = MARK_GEN.xfail(run=False)
+ mark = MARK_GEN.xfail(reason=reason, run=False)
elif requested_mark == "fail_at_collect":
f_name = func.__name__
_, lineno = getfslineno(func)
@@ -42,23 +71,30 @@ def get_empty_parameterset_mark(config, argnames, func):
)
else:
raise LookupError(requested_mark)
- fs, lineno = getfslineno(func)
- reason = "got empty parameter set %r, function %s at %s:%d" % (
- argnames,
- func.__name__,
- fs,
- lineno,
- )
- return mark(reason=reason)
+ return mark
-class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
+class ParameterSet(
+ NamedTuple(
+ "ParameterSet",
+ [
+ ("values", Sequence[Union[object, NotSetType]]),
+ ("marks", Collection[Union["MarkDecorator", "Mark"]]),
+ ("id", Optional[str]),
+ ],
+ )
+):
@classmethod
- def param(cls, *values, marks=(), id=None):
+ def param(
+ cls,
+ *values: object,
+ marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),
+ id: Optional[str] = None,
+ ) -> "ParameterSet":
if isinstance(marks, MarkDecorator):
marks = (marks,)
else:
- assert isinstance(marks, (tuple, list, set))
+ assert isinstance(marks, collections.abc.Collection)
if id is not None:
if not isinstance(id, str):
@@ -69,15 +105,20 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
return cls(values, marks, id)
@classmethod
- def extract_from(cls, parameterset, force_tuple=False):
- """
+ def extract_from(
+ cls,
+ parameterset: Union["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
+ 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
+ Enforce tuple wrapping so single argument tuple values
+ don't get decomposed and break tests.
"""
if isinstance(parameterset, cls):
@@ -85,10 +126,20 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
if force_tuple:
return cls.param(parameterset)
else:
- return cls(parameterset, marks=[], id=None)
+ # TODO: Refactor to fix this type-ignore. Currently the following
+ # passes type-checking but crashes:
+ #
+ # @pytest.mark.parametrize(('x', 'y'), [1, 2])
+ # def test_foo(x, y): pass
+ return cls(parameterset, marks=[], id=None) # type: ignore[arg-type]
@staticmethod
- def _parse_parametrize_args(argnames, argvalues, *args, **kwargs):
+ def _parse_parametrize_args(
+ argnames: Union[str, List[str], Tuple[str, ...]],
+ argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
+ *args,
+ **kwargs,
+ ) -> Tuple[Union[List[str], Tuple[str, ...]], bool]:
if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1
@@ -97,19 +148,29 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
return argnames, force_tuple
@staticmethod
- def _parse_parametrize_parameters(argvalues, force_tuple):
+ def _parse_parametrize_parameters(
+ argvalues: Iterable[Union["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, argvalues, func, config, function_definition):
+ def _for_parametrize(
+ cls,
+ argnames: Union[str, List[str], Tuple[str, ...]],
+ argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
+ func,
+ config: Config,
+ nodeid: str,
+ ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]:
argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
del argvalues
if parameters:
- # check all parameter sets have the correct number of values
+ # Check all parameter sets have the correct number of values.
for param in parameters:
if len(param.values) != len(argnames):
msg = (
@@ -120,7 +181,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
)
fail(
msg.format(
- nodeid=function_definition.nodeid,
+ nodeid=nodeid,
values=param.values,
names=argnames,
names_len=len(argnames),
@@ -129,8 +190,8 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
pytrace=False,
)
else:
- # empty parameter set (likely computed at runtime): create a single
- # parameter set with NOTSET values, with the "empty parameter set" mark applied to it
+ # Empty parameter set (likely computed at runtime): create a single
+ # 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=None)
@@ -138,35 +199,39 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
return argnames, parameters
+@final
@attr.s(frozen=True)
class Mark:
- #: name of the mark
+ #: Name of the mark.
name = attr.ib(type=str)
- #: positional arguments of the mark decorator
- args = attr.ib() # List[object]
- #: keyword arguments of the mark decorator
- kwargs = attr.ib() # Dict[str, object]
+ #: Positional arguments of the mark decorator.
+ args = attr.ib(type=Tuple[Any, ...])
+ #: Keyword arguments of the mark decorator.
+ kwargs = attr.ib(type=Mapping[str, Any])
- #: source Mark for ids with parametrize Marks
+ #: Source Mark for ids with parametrize Marks.
_param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False)
- #: resolved/generated ids with parametrize Marks
- _param_ids_generated = attr.ib(type=Optional[List[str]], default=None, repr=False)
+ #: Resolved/generated ids with parametrize Marks.
+ _param_ids_generated = attr.ib(
+ type=Optional[Sequence[str]], default=None, repr=False
+ )
- def _has_param_ids(self):
+ def _has_param_ids(self) -> bool:
return "ids" in self.kwargs or len(self.args) >= 4
def combined_with(self, other: "Mark") -> "Mark":
- """
- :param other: the mark to combine with
- :type other: Mark
- :rtype: Mark
+ """Return a new Mark which is a combination of this
+ Mark and another Mark.
+
+ Combines by appending args and merging kwargs.
- combines by appending args and merging the mappings
+ :param Mark other: The mark to combine with.
+ :rtype: Mark
"""
assert self.name == other.name
# Remember source of ids with parametrize Marks.
- param_ids_from = None # type: Optional[Mark]
+ param_ids_from: Optional[Mark] = None
if self.name == "parametrize":
if other._has_param_ids():
param_ids_from = other
@@ -181,13 +246,20 @@ 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=Union[Callable[..., object], type])
+
+
@attr.s
class MarkDecorator:
- """ A decorator for test functions and test classes. When applied
- it will create :class:`Mark` objects which are often created like this::
+ """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
+ mark1 = pytest.mark.NAME # Simple MarkDecorator
+ mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
and can then be applied as decorators to test functions::
@@ -195,64 +267,75 @@ class MarkDecorator:
def test_function():
pass
- When a MarkDecorator instance is called it does the following:
+ 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 itself to the class so it
+ 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 a MarkInfo object to the
- function, containing all the arguments already stored internally in
- the MarkDecorator.
- 3. When called in any other case, it performs a 'fake construction' call,
- i.e. 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 MarkDecorator objects from storing only a
- single function or class reference as their positional argument with no
- additional keyword or positional arguments.
+ 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 MarkDecorators from storing only a single
+ function or class reference as their positional argument with no
+ additional keyword or positional arguments. You can work around this by
+ using `with_args()`.
"""
- mark = attr.ib(validator=attr.validators.instance_of(Mark))
+ mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark))
@property
- def name(self):
- """alias for mark.name"""
+ def name(self) -> str:
+ """Alias for mark.name."""
return self.mark.name
@property
- def args(self):
- """alias for mark.args"""
+ def args(self) -> Tuple[Any, ...]:
+ """Alias for mark.args."""
return self.mark.args
@property
- def kwargs(self):
- """alias for mark.kwargs"""
+ def kwargs(self) -> Mapping[str, Any]:
+ """Alias for mark.kwargs."""
return self.mark.kwargs
@property
- def markname(self):
+ def markname(self) -> str:
return self.name # for backward-compat (2.4.1 had this attr)
- def __repr__(self):
- return "<MarkDecorator {!r}>".format(self.mark)
+ def __repr__(self) -> str:
+ return f"<MarkDecorator {self.mark!r}>"
- def with_args(self, *args, **kwargs):
- """ return a MarkDecorator with extra arguments added
+ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
+ """Return a MarkDecorator with extra arguments added.
- unlike call this can be used even if the sole argument is a callable/class
+ Unlike calling the MarkDecorator, with_args() can be used even
+ if the sole argument is a callable/class.
- :return: MarkDecorator
+ :rtype: MarkDecorator
"""
-
mark = Mark(self.name, args, kwargs)
return self.__class__(self.mark.combined_with(mark))
- def __call__(self, *args, **kwargs):
- """ if passed a single callable argument: decorate it with mark info.
- otherwise add *args/**kwargs in-place to mark information. """
+ # Type ignored because the overloads overlap with an incompatible
+ # 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[misc]
+ 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)
@@ -262,10 +345,8 @@ class MarkDecorator:
return self.with_args(*args, **kwargs)
-def get_unpacked_marks(obj):
- """
- obtain the unpacked marks that are stored on an object
- """
+def get_unpacked_marks(obj) -> List[Mark]:
+ """Obtain the unpacked marks that are stored on an object."""
mark_list = getattr(obj, "pytestmark", [])
if not isinstance(mark_list, list):
mark_list = [mark_list]
@@ -273,10 +354,9 @@ def get_unpacked_marks(obj):
def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]:
- """
- normalizes marker decorating helpers to mark objects
+ """Normalize marker decorating helpers to mark objects.
- :type mark_list: List[Union[Mark, Markdecorator]]
+ :type List[Union[Mark, Markdecorator]] mark_list:
:rtype: List[Mark]
"""
extracted = [
@@ -284,34 +364,118 @@ def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List
] # unpack MarkDecorator
for mark in extracted:
if not isinstance(mark, Mark):
- raise TypeError("got {!r} instead of Mark".format(mark))
+ raise TypeError(f"got {mark!r} instead of Mark")
return [x for x in extracted if isinstance(x, Mark)]
-def store_mark(obj, mark):
- """store a Mark on an object
- this is used to implement the Mark declarations/decorators correctly
+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
- # always reassign name to avoid updating pytestmark
- # in a reference that was only borrowed
+ # Always reassign name to avoid updating pytestmark in a reference that
+ # was only borrowed.
obj.pytestmark = get_unpacked_marks(obj) + [mark]
+# 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.fixtures import _Scope
+
+ class _SkipMarkDecorator(MarkDecorator):
+ @overload # type: ignore[override,misc]
+ def __call__(self, arg: _Markable) -> _Markable:
+ ...
+
+ @overload
+ def __call__(self, reason: str = ...) -> "MarkDecorator":
+ ...
+
+ class _SkipifMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self,
+ condition: Union[str, bool] = ...,
+ *conditions: Union[str, bool],
+ reason: str = ...,
+ ) -> MarkDecorator:
+ ...
+
+ class _XfailMarkDecorator(MarkDecorator):
+ @overload # type: ignore[override,misc]
+ def __call__(self, arg: _Markable) -> _Markable:
+ ...
+
+ @overload
+ def __call__(
+ self,
+ condition: Union[str, bool] = ...,
+ *conditions: Union[str, bool],
+ reason: str = ...,
+ run: bool = ...,
+ raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
+ strict: bool = ...,
+ ) -> MarkDecorator:
+ ...
+
+ class _ParametrizeMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self,
+ argnames: Union[str, List[str], Tuple[str, ...]],
+ argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
+ *,
+ indirect: Union[bool, Sequence[str]] = ...,
+ ids: Optional[
+ Union[
+ Iterable[Union[None, str, float, int, bool]],
+ Callable[[Any], Optional[object]],
+ ]
+ ] = ...,
+ scope: Optional[_Scope] = ...,
+ ) -> MarkDecorator:
+ ...
+
+ class _UsefixturesMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self, *fixtures: str
+ ) -> MarkDecorator:
+ ...
+
+ class _FilterwarningsMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self, *filters: str
+ ) -> MarkDecorator:
+ ...
+
+
+@final
class MarkGenerator:
- """ Factory for :class:`MarkDecorator` objects - exposed as
- a ``pytest.mark`` singleton instance. Example::
+ """Factory for :class:`MarkDecorator` objects - exposed as
+ a ``pytest.mark`` singleton instance.
+
+ Example::
import pytest
+
@pytest.mark.slowtest
def test_function():
pass
- will set a 'slowtest' :class:`MarkInfo` object
- on the ``test_function`` object. """
+ applies a 'slowtest' :class:`Mark` on ``test_function``.
+ """
+
+ _config: Optional[Config] = None
+ _markers: Set[str] = set()
- _config = None
- _markers = set() # type: Set[str]
+ # See TYPE_CHECKING above.
+ if TYPE_CHECKING:
+ skip: _SkipMarkDecorator
+ skipif: _SkipifMarkDecorator
+ xfail: _XfailMarkDecorator
+ parametrize: _ParametrizeMarkDecorator
+ usefixtures: _UsefixturesMarkDecorator
+ filterwarnings: _FilterwarningsMarkDecorator
def __getattr__(self, name: str) -> MarkDecorator:
if name[0] == "_":
@@ -335,21 +499,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:
- if self._config.option.strict_markers:
+ if self._config.option.strict_markers or self._config.option.strict:
fail(
- "{!r} not found in `markers` configuration option".format(name),
+ 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("Unknown '{}' mark, did you mean 'parametrize'?".format(name))
+ fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
warnings.warn(
"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/latest/mark.html" % name,
+ "https://docs.pytest.org/en/stable/mark.html" % name,
PytestUnknownMarkWarning,
2,
)
@@ -360,13 +524,14 @@ class MarkGenerator:
MARK_GEN = MarkGenerator()
-class NodeKeywords(MutableMapping):
- def __init__(self, node):
+@final
+class NodeKeywords(MutableMapping[str, Any]):
+ def __init__(self, node: "Node") -> None:
self.node = node
self.parent = node.parent
self._markers = {node.name: True}
- def __getitem__(self, key):
+ def __getitem__(self, key: str) -> Any:
try:
return self._markers[key]
except KeyError:
@@ -374,24 +539,24 @@ class NodeKeywords(MutableMapping):
raise
return self.parent.keywords[key]
- def __setitem__(self, key, value):
+ def __setitem__(self, key: str, value: Any) -> None:
self._markers[key] = value
- def __delitem__(self, key):
+ def __delitem__(self, key: str) -> None:
raise ValueError("cannot delete key in keywords dict")
- def __iter__(self):
+ def __iter__(self) -> Iterator[str]:
seen = self._seen()
return iter(seen)
- def _seen(self):
+ def _seen(self) -> Set[str]:
seen = set(self._markers)
if self.parent is not None:
seen.update(self.parent.keywords)
return seen
- def __len__(self):
+ def __len__(self) -> int:
return len(self._seen())
- def __repr__(self):
- return "<NodeKeywords for node {}>".format(self.node)
+ def __repr__(self) -> str:
+ return f"<NodeKeywords for node {self.node}>"