aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/pytest/py3/_pytest/mark
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
downloadydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'contrib/python/pytest/py3/_pytest/mark')
-rw-r--r--contrib/python/pytest/py3/_pytest/mark/__init__.py168
-rw-r--r--contrib/python/pytest/py3/_pytest/mark/evaluate.py132
-rw-r--r--contrib/python/pytest/py3/_pytest/mark/legacy.py116
-rw-r--r--contrib/python/pytest/py3/_pytest/mark/structures.py397
4 files changed, 813 insertions, 0 deletions
diff --git a/contrib/python/pytest/py3/_pytest/mark/__init__.py b/contrib/python/pytest/py3/_pytest/mark/__init__.py
new file mode 100644
index 0000000000..dab0cf149f
--- /dev/null
+++ b/contrib/python/pytest/py3/_pytest/mark/__init__.py
@@ -0,0 +1,168 @@
+""" generic mechanism for marking and selecting python functions. """
+from typing import Optional
+
+from .legacy import matchkeyword
+from .legacy import matchmark
+from .structures import EMPTY_PARAMETERSET_OPTION
+from .structures import get_empty_parameterset_mark
+from .structures import Mark
+from .structures import MARK_GEN
+from .structures import MarkDecorator
+from .structures import MarkGenerator
+from .structures import ParameterSet
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config import UsageError
+from _pytest.store import StoreKey
+
+__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"]
+
+
+old_mark_config_key = StoreKey[Optional[Config]]()
+
+
+def param(*values, **kw):
+ """Specify a parameter in `pytest.mark.parametrize`_ calls or
+ :ref:`parametrized fixtures <fixture-parametrize-marks>`.
+
+ .. code-block:: python
+
+ @pytest.mark.parametrize("test_input,expected", [
+ ("3+5", 8),
+ pytest.param("6*9", 42, marks=pytest.mark.xfail),
+ ])
+ def test_eval(test_input, expected):
+ assert eval(test_input) == expected
+
+ :param values: variable args of the values of the parameter set, in order.
+ :keyword marks: a single mark or a list of marks to be applied to this parameter set.
+ :keyword str id: the id to attribute to this parameter set.
+ """
+ return ParameterSet.param(*values, **kw)
+
+
+def pytest_addoption(parser):
+ group = parser.getgroup("general")
+ group._addoption(
+ "-k",
+ action="store",
+ dest="keyword",
+ default="",
+ metavar="EXPRESSION",
+ help="only run tests which match the given substring expression. "
+ "An expression is a python evaluatable expression "
+ "where all names are substring-matched against test names "
+ "and their parent classes. Example: -k 'test_method or test_"
+ "other' matches all test functions and classes whose name "
+ "contains 'test_method' or 'test_other', while -k 'not test_method' "
+ "matches those that don't contain 'test_method' in their names. "
+ "-k 'not test_method and not test_other' will eliminate the matches. "
+ "Additionally keywords are matched to classes and functions "
+ "containing extra names in their 'extra_keyword_matches' set, "
+ "as well as functions which have names assigned directly to them. "
+ "The matching is case-insensitive.",
+ )
+
+ group._addoption(
+ "-m",
+ action="store",
+ dest="markexpr",
+ default="",
+ metavar="MARKEXPR",
+ help="only run tests matching given mark expression. "
+ "example: -m 'mark1 and not mark2'.",
+ )
+
+ group.addoption(
+ "--markers",
+ action="store_true",
+ help="show markers (builtin, plugin and per-project ones).",
+ )
+
+ parser.addini("markers", "markers for test functions", "linelist")
+ parser.addini(EMPTY_PARAMETERSET_OPTION, "default marker for empty parametersets")
+
+
+@hookimpl(tryfirst=True)
+def pytest_cmdline_main(config):
+ import _pytest.config
+
+ if config.option.markers:
+ config._do_configure()
+ tw = _pytest.config.create_terminal_writer(config)
+ for line in config.getini("markers"):
+ parts = line.split(":", 1)
+ name = parts[0]
+ rest = parts[1] if len(parts) == 2 else ""
+ tw.write("@pytest.mark.%s:" % name, bold=True)
+ tw.line(rest)
+ tw.line()
+ config._ensure_unconfigure()
+ return 0
+
+
+def deselect_by_keyword(items, config):
+ keywordexpr = config.option.keyword.lstrip()
+ if not keywordexpr:
+ return
+
+ if keywordexpr.startswith("-"):
+ keywordexpr = "not " + keywordexpr[1:]
+ selectuntil = False
+ if keywordexpr[-1:] == ":":
+ selectuntil = True
+ keywordexpr = keywordexpr[:-1]
+
+ remaining = []
+ deselected = []
+ for colitem in items:
+ if keywordexpr and not matchkeyword(colitem, keywordexpr):
+ deselected.append(colitem)
+ else:
+ if selectuntil:
+ keywordexpr = None
+ remaining.append(colitem)
+
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+def deselect_by_mark(items, config):
+ matchexpr = config.option.markexpr
+ if not matchexpr:
+ return
+
+ remaining = []
+ deselected = []
+ for item in items:
+ if matchmark(item, matchexpr):
+ remaining.append(item)
+ else:
+ deselected.append(item)
+
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+def pytest_collection_modifyitems(items, config):
+ deselect_by_keyword(items, config)
+ deselect_by_mark(items, config)
+
+
+def pytest_configure(config):
+ config._store[old_mark_config_key] = MARK_GEN._config
+ MARK_GEN._config = config
+
+ empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
+
+ if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
+ raise UsageError(
+ "{!s} must be one of skip, xfail or fail_at_collect"
+ " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)
+ )
+
+
+def pytest_unconfigure(config):
+ MARK_GEN._config = config._store.get(old_mark_config_key, None)
diff --git a/contrib/python/pytest/py3/_pytest/mark/evaluate.py b/contrib/python/pytest/py3/_pytest/mark/evaluate.py
new file mode 100644
index 0000000000..772baf31b6
--- /dev/null
+++ b/contrib/python/pytest/py3/_pytest/mark/evaluate.py
@@ -0,0 +1,132 @@
+import os
+import platform
+import sys
+import traceback
+from typing import Any
+from typing import Dict
+
+from ..outcomes import fail
+from ..outcomes import TEST_OUTCOME
+from _pytest.config import Config
+from _pytest.store import StoreKey
+
+
+evalcache_key = StoreKey[Dict[str, Any]]()
+
+
+def cached_eval(config: Config, expr: str, d: Dict[str, object]) -> Any:
+ default = {} # type: Dict[str, object]
+ evalcache = config._store.setdefault(evalcache_key, default)
+ try:
+ return evalcache[expr]
+ except KeyError:
+ import _pytest._code
+
+ exprcode = _pytest._code.compile(expr, mode="eval")
+ evalcache[expr] = x = eval(exprcode, d)
+ return x
+
+
+class MarkEvaluator:
+ def __init__(self, item, name):
+ self.item = item
+ self._marks = None
+ self._mark = None
+ self._mark_name = name
+
+ def __bool__(self):
+ # don't cache here to prevent staleness
+ return bool(self._get_marks())
+
+ __nonzero__ = __bool__
+
+ def wasvalid(self):
+ return not hasattr(self, "exc")
+
+ def _get_marks(self):
+ return list(self.item.iter_markers(name=self._mark_name))
+
+ def invalidraise(self, exc):
+ raises = self.get("raises")
+ if not raises:
+ return
+ return not isinstance(exc, raises)
+
+ def istrue(self):
+ try:
+ return self._istrue()
+ except TEST_OUTCOME:
+ self.exc = sys.exc_info()
+ if isinstance(self.exc[1], SyntaxError):
+ # TODO: Investigate why SyntaxError.offset is Optional, and if it can be None here.
+ assert self.exc[1].offset is not None
+ msg = [" " * (self.exc[1].offset + 4) + "^"]
+ msg.append("SyntaxError: invalid syntax")
+ else:
+ msg = traceback.format_exception_only(*self.exc[:2])
+ fail(
+ "Error evaluating %r expression\n"
+ " %s\n"
+ "%s" % (self._mark_name, self.expr, "\n".join(msg)),
+ pytrace=False,
+ )
+
+ def _getglobals(self):
+ d = {"os": os, "sys": sys, "platform": platform, "config": self.item.config}
+ if hasattr(self.item, "obj"):
+ d.update(self.item.obj.__globals__)
+ return d
+
+ def _istrue(self):
+ if hasattr(self, "result"):
+ return self.result
+ self._marks = self._get_marks()
+
+ if self._marks:
+ self.result = False
+ for mark in self._marks:
+ self._mark = mark
+ if "condition" in mark.kwargs:
+ args = (mark.kwargs["condition"],)
+ else:
+ args = mark.args
+
+ for expr in args:
+ self.expr = expr
+ if isinstance(expr, str):
+ d = self._getglobals()
+ result = cached_eval(self.item.config, expr, d)
+ else:
+ if "reason" not in mark.kwargs:
+ # XXX better be checked at collection time
+ msg = (
+ "you need to specify reason=STRING "
+ "when using booleans as conditions."
+ )
+ fail(msg)
+ result = bool(expr)
+ if result:
+ self.result = True
+ self.reason = mark.kwargs.get("reason", None)
+ self.expr = expr
+ return self.result
+
+ if not args:
+ self.result = True
+ self.reason = mark.kwargs.get("reason", None)
+ return self.result
+ return False
+
+ def get(self, attr, default=None):
+ if self._mark is None:
+ return default
+ return self._mark.kwargs.get(attr, default)
+
+ def getexplanation(self):
+ expl = getattr(self, "reason", None) or self.get("reason", None)
+ if not expl:
+ if not hasattr(self, "expr"):
+ return ""
+ else:
+ return "condition: " + str(self.expr)
+ return expl
diff --git a/contrib/python/pytest/py3/_pytest/mark/legacy.py b/contrib/python/pytest/py3/_pytest/mark/legacy.py
new file mode 100644
index 0000000000..3d7a194b61
--- /dev/null
+++ b/contrib/python/pytest/py3/_pytest/mark/legacy.py
@@ -0,0 +1,116 @@
+"""
+this is a place where we put datastructures used by legacy apis
+we hope to remove
+"""
+import keyword
+from typing import Set
+
+import attr
+
+from _pytest.compat import TYPE_CHECKING
+from _pytest.config import UsageError
+
+if TYPE_CHECKING:
+ from _pytest.nodes import Item # noqa: F401 (used in type string)
+
+
+@attr.s
+class MarkMapping:
+ """Provides a local mapping for markers where item access
+ resolves to True if the marker is present. """
+
+ own_mark_names = attr.ib()
+
+ @classmethod
+ def from_item(cls, item):
+ mark_names = {mark.name for mark in item.iter_markers()}
+ return cls(mark_names)
+
+ def __getitem__(self, name):
+ return name in self.own_mark_names
+
+
+@attr.s
+class KeywordMapping:
+ """Provides a local mapping for keywords.
+ Given a list of names, map any substring of one of these names to True.
+ """
+
+ _names = attr.ib(type=Set[str])
+
+ @classmethod
+ def from_item(cls, item: "Item") -> "KeywordMapping":
+ mapped_names = set()
+
+ # Add the names of the current item and any parent items
+ import pytest
+
+ for item in item.listchain():
+ if not isinstance(item, pytest.Instance):
+ mapped_names.add(item.name)
+
+ # Add the names added as extra keywords to current or parent items
+ mapped_names.update(item.listextrakeywords())
+
+ # Add the names attached to the current function through direct assignment
+ function_obj = getattr(item, "function", None)
+ if function_obj:
+ mapped_names.update(function_obj.__dict__)
+
+ # add the markers to the keywords as we no longer handle them correctly
+ mapped_names.update(mark.name for mark in item.iter_markers())
+
+ return cls(mapped_names)
+
+ def __getitem__(self, subname: str) -> bool:
+ """Return whether subname is included within stored names.
+
+ The string inclusion check is case-insensitive.
+
+ """
+ subname = subname.lower()
+ names = (name.lower() for name in self._names)
+
+ for name in names:
+ if subname in name:
+ return True
+ return False
+
+
+python_keywords_allowed_list = ["or", "and", "not"]
+
+
+def matchmark(colitem, markexpr):
+ """Tries to match on any marker names, attached to the given colitem."""
+ try:
+ return eval(markexpr, {}, MarkMapping.from_item(colitem))
+ except SyntaxError as e:
+ raise SyntaxError(str(e) + "\nMarker expression must be valid Python!")
+
+
+def matchkeyword(colitem, keywordexpr):
+ """Tries to match given keyword expression to given collector item.
+
+ Will match on the name of colitem, including the names of its parents.
+ Only matches names of items which are either a :class:`Class` or a
+ :class:`Function`.
+ Additionally, matches on names in the 'extra_keyword_matches' set of
+ any item, as well as names directly assigned to test functions.
+ """
+ mapping = KeywordMapping.from_item(colitem)
+ if " " not in keywordexpr:
+ # special case to allow for simple "-k pass" and "-k 1.3"
+ return mapping[keywordexpr]
+ elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]:
+ return not mapping[keywordexpr[4:]]
+ for kwd in keywordexpr.split():
+ if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list:
+ raise UsageError(
+ "Python keyword '{}' not accepted in expressions passed to '-k'".format(
+ kwd
+ )
+ )
+ try:
+ return eval(keywordexpr, {}, mapping)
+ except SyntaxError:
+ raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr))
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)