diff options
author | arcadia-devtools <[email protected]> | 2022-02-09 12:00:52 +0300 |
---|---|---|
committer | Daniil Cherednik <[email protected]> | 2022-02-10 15:58:17 +0300 |
commit | 8e1413fed79d1e8036e65228af6c93399ccf5502 (patch) | |
tree | 502c9df7b2614d20541c7a2d39d390e9a51877cc /contrib/python/pytest/py3/_pytest/mark/structures.py | |
parent | 6b813c17d56d1d05f92c61ddc347d0e4d358fe85 (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.py | 397 |
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}>" |