aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/pytest/py3/_pytest/mark/structures.py
diff options
context:
space:
mode:
authorDevtools Arcadia <arcadia-devtools@yandex-team.ru>2022-02-07 18:08:42 +0300
committerDevtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net>2022-02-07 18:08:42 +0300
commit1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch)
treee26c9fed0de5d9873cce7e00bc214573dc2195b7 /contrib/python/pytest/py3/_pytest/mark/structures.py
downloadydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
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, 397 insertions, 0 deletions
diff --git a/contrib/python/pytest/py3/_pytest/mark/structures.py b/contrib/python/pytest/py3/_pytest/mark/structures.py
new file mode 100644
index 0000000000..50ad81baa6
--- /dev/null
+++ b/contrib/python/pytest/py3/_pytest/mark/structures.py
@@ -0,0 +1,397 @@
+import inspect
+import warnings
+from collections import namedtuple
+from collections.abc import MutableMapping
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Set
+from typing import Union
+
+import attr
+
+from .._code.source import getfslineno
+from ..compat import ascii_escaped
+from ..compat import NOTSET
+from _pytest.outcomes import fail
+from _pytest.warning_types import PytestUnknownMarkWarning
+
+EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
+
+
+def istestfunc(func):
+ return (
+ hasattr(func, "__call__")
+ and getattr(func, "__name__", "<lambda>") != "<lambda>"
+ )
+
+
+def get_empty_parameterset_mark(config, argnames, func):
+ from ..nodes import Collector
+
+ requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
+ if requested_mark in ("", None, "skip"):
+ mark = MARK_GEN.skip
+ elif requested_mark == "xfail":
+ mark = MARK_GEN.xfail(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)
+ fs, lineno = getfslineno(func)
+ reason = "got empty parameter set %r, function %s at %s:%d" % (
+ argnames,
+ func.__name__,
+ fs,
+ lineno,
+ )
+ return mark(reason=reason)
+
+
+class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
+ @classmethod
+ def param(cls, *values, marks=(), id=None):
+ if isinstance(marks, MarkDecorator):
+ marks = (marks,)
+ else:
+ assert isinstance(marks, (tuple, list, set))
+
+ if id is not None:
+ if not isinstance(id, str):
+ raise TypeError(
+ "Expected id to be a string, got {}: {!r}".format(type(id), id)
+ )
+ id = ascii_escaped(id)
+ return cls(values, marks, id)
+
+ @classmethod
+ def extract_from(cls, parameterset, force_tuple=False):
+ """
+ :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, argvalues, *args, **kwargs):
+ if not isinstance(argnames, (tuple, list)):
+ 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, force_tuple):
+ return [
+ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
+ ]
+
+ @classmethod
+ def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
+ 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
+ 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=function_definition.nodeid,
+ values=param.values,
+ names=argnames,
+ names_len=len(argnames),
+ values_len=len(param.values),
+ ),
+ 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
+ mark = get_empty_parameterset_mark(config, argnames, func)
+ parameters.append(
+ ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
+ )
+ return argnames, parameters
+
+
+@attr.s(frozen=True)
+class 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]
+
+ #: 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)
+
+ def _has_param_ids(self):
+ 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
+
+ combines by appending args and merging the mappings
+ """
+ assert self.name == other.name
+
+ # Remember source of ids with parametrize Marks.
+ param_ids_from = None # type: Optional[Mark]
+ 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,
+ )
+
+
+@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::
+
+ 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 instance 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
+ 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.
+
+ """
+
+ mark = attr.ib(validator=attr.validators.instance_of(Mark))
+
+ @property
+ def name(self):
+ """alias for mark.name"""
+ return self.mark.name
+
+ @property
+ def args(self):
+ """alias for mark.args"""
+ return self.mark.args
+
+ @property
+ def kwargs(self):
+ """alias for mark.kwargs"""
+ return self.mark.kwargs
+
+ @property
+ def markname(self):
+ return self.name # for backward-compat (2.4.1 had this attr)
+
+ def __repr__(self):
+ return "<MarkDecorator {!r}>".format(self.mark)
+
+ def with_args(self, *args, **kwargs):
+ """ return a MarkDecorator with extra arguments added
+
+ unlike call this can be used even if the sole argument is a callable/class
+
+ :return: 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. """
+ 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)
+ return func
+ return self.with_args(*args, **kwargs)
+
+
+def get_unpacked_marks(obj):
+ """
+ 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]
+ return normalize_mark_list(mark_list)
+
+
+def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]:
+ """
+ normalizes marker decorating helpers to mark objects
+
+ :type mark_list: List[Union[Mark, Markdecorator]]
+ :rtype: List[Mark]
+ """
+ extracted = [
+ getattr(mark, "mark", mark) for mark in mark_list
+ ] # unpack MarkDecorator
+ for mark in extracted:
+ if not isinstance(mark, Mark):
+ raise TypeError("got {!r} instead of Mark".format(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
+ """
+ assert isinstance(mark, Mark), mark
+ # always reassign name to avoid updating pytestmark
+ # in a reference that was only borrowed
+ obj.pytestmark = get_unpacked_marks(obj) + [mark]
+
+
+class MarkGenerator:
+ """ 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. """
+
+ _config = None
+ _markers = set() # type: Set[str]
+
+ def __getattr__(self, name: str) -> MarkDecorator:
+ if name[0] == "_":
+ raise AttributeError("Marker name must NOT start with underscore")
+
+ if self._config is not None:
+ # We store a set of markers as a performance optimisation - if a mark
+ # name is in the set we definitely know it, but a mark may be known and
+ # not in the set. We therefore start by updating the set!
+ if name not in self._markers:
+ for line in self._config.getini("markers"):
+ # example lines: "skipif(condition): skip the given test if..."
+ # or "hypothesis: tests which use Hypothesis", so to get the
+ # marker name we split on both `:` and `(`.
+ if line == "ya:external":
+ marker = line
+ else:
+ marker = line.split(":")[0].split("(")[0].strip()
+ self._markers.add(marker)
+
+ # 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:
+ fail(
+ "{!r} not found in `markers` configuration option".format(name),
+ 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))
+
+ 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,
+ PytestUnknownMarkWarning,
+ 2,
+ )
+
+ return MarkDecorator(Mark(name, (), {}))
+
+
+MARK_GEN = MarkGenerator()
+
+
+class NodeKeywords(MutableMapping):
+ def __init__(self, node):
+ self.node = node
+ self.parent = node.parent
+ self._markers = {node.name: True}
+
+ def __getitem__(self, key):
+ try:
+ return self._markers[key]
+ except KeyError:
+ if self.parent is None:
+ raise
+ return self.parent.keywords[key]
+
+ def __setitem__(self, key, value):
+ self._markers[key] = value
+
+ def __delitem__(self, key):
+ raise ValueError("cannot delete key in keywords dict")
+
+ def __iter__(self):
+ seen = self._seen()
+ return iter(seen)
+
+ def _seen(self):
+ seen = set(self._markers)
+ if self.parent is not None:
+ seen.update(self.parent.keywords)
+ return seen
+
+ def __len__(self):
+ return len(self._seen())
+
+ def __repr__(self):
+ return "<NodeKeywords for node {}>".format(self.node)