| """Generic mechanism for marking and selecting python functions.""" |
|
|
| from __future__ import annotations |
|
|
| import collections |
| from collections.abc import Collection |
| from collections.abc import Iterable |
| from collections.abc import Set as AbstractSet |
| import dataclasses |
| from typing import TYPE_CHECKING |
|
|
| from .expression import Expression |
| from .structures import _HiddenParam |
| from .structures import EMPTY_PARAMETERSET_OPTION |
| from .structures import get_empty_parameterset_mark |
| from .structures import HIDDEN_PARAM |
| 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 ExitCode |
| from _pytest.config import hookimpl |
| from _pytest.config import UsageError |
| from _pytest.config.argparsing import NOT_SET |
| from _pytest.config.argparsing import Parser |
| from _pytest.stash import StashKey |
|
|
|
|
| if TYPE_CHECKING: |
| from _pytest.nodes import Item |
|
|
|
|
| __all__ = [ |
| "HIDDEN_PARAM", |
| "MARK_GEN", |
| "Mark", |
| "MarkDecorator", |
| "MarkGenerator", |
| "ParameterSet", |
| "get_empty_parameterset_mark", |
| ] |
|
|
|
|
| old_mark_config_key = StashKey[Config | None]() |
|
|
|
|
| def param( |
| *values: object, |
| marks: MarkDecorator | Collection[MarkDecorator | Mark] = (), |
| id: str | _HiddenParam | None = None, |
| ) -> ParameterSet: |
| """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. |
| |
| :param marks: |
| A single mark or a list of marks to be applied to this parameter set. |
| |
| :ref:`pytest.mark.usefixtures <pytest.mark.usefixtures ref>` cannot be added via this parameter. |
| |
| :type id: str | Literal[pytest.HIDDEN_PARAM] | None |
| :param id: |
| The id to attribute to this parameter set. |
| |
| .. versionadded:: 8.4 |
| :ref:`hidden-param` means to hide the parameter set |
| from the test name. Can only be used at most 1 time, as |
| test names need to be unique. |
| """ |
| return ParameterSet.param(*values, marks=marks, id=id) |
|
|
|
|
| def pytest_addoption(parser: Parser) -> None: |
| 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 evaluable 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. " |
| "For example: -m 'mark1 and not mark2'.", |
| ) |
|
|
| group.addoption( |
| "--markers", |
| action="store_true", |
| help="show markers (builtin, plugin and per-project ones).", |
| ) |
|
|
| parser.addini("markers", "Register new markers for test functions", "linelist") |
| parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets") |
|
|
|
|
| @hookimpl(tryfirst=True) |
| def pytest_cmdline_main(config: Config) -> int | ExitCode | None: |
| 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(f"@pytest.mark.{name}:", bold=True) |
| tw.line(rest) |
| tw.line() |
| config._ensure_unconfigure() |
| return 0 |
|
|
| return None |
|
|
|
|
| @dataclasses.dataclass |
| class KeywordMatcher: |
| """A matcher for keywords. |
| |
| Given a list of names, matches any substring of one of these names. The |
| string inclusion check is case-insensitive. |
| |
| 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. |
| """ |
|
|
| __slots__ = ("_names",) |
|
|
| _names: AbstractSet[str] |
|
|
| @classmethod |
| def from_item(cls, item: Item) -> KeywordMatcher: |
| mapped_names = set() |
|
|
| |
| |
| |
| import pytest |
|
|
| for node in item.listchain(): |
| if isinstance(node, pytest.Session): |
| continue |
| if isinstance(node, pytest.Directory) and isinstance( |
| node.parent, pytest.Session |
| ): |
| continue |
| mapped_names.add(node.name) |
|
|
| |
| mapped_names.update(item.listextrakeywords()) |
|
|
| |
| function_obj = getattr(item, "function", None) |
| if function_obj: |
| mapped_names.update(function_obj.__dict__) |
|
|
| |
| mapped_names.update(mark.name for mark in item.iter_markers()) |
|
|
| return cls(mapped_names) |
|
|
| def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool: |
| if kwargs: |
| raise UsageError("Keyword expressions do not support call parameters.") |
| subname = subname.lower() |
| return any(subname in name.lower() for name in self._names) |
|
|
|
|
| def deselect_by_keyword(items: list[Item], config: Config) -> None: |
| keywordexpr = config.option.keyword.lstrip() |
| if not keywordexpr: |
| return |
|
|
| expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'") |
|
|
| remaining = [] |
| deselected = [] |
| for colitem in items: |
| if not expr.evaluate(KeywordMatcher.from_item(colitem)): |
| deselected.append(colitem) |
| else: |
| remaining.append(colitem) |
|
|
| if deselected: |
| config.hook.pytest_deselected(items=deselected) |
| items[:] = remaining |
|
|
|
|
| @dataclasses.dataclass |
| class MarkMatcher: |
| """A matcher for markers which are present. |
| |
| Tries to match on any marker names, attached to the given colitem. |
| """ |
|
|
| __slots__ = ("own_mark_name_mapping",) |
|
|
| own_mark_name_mapping: dict[str, list[Mark]] |
|
|
| @classmethod |
| def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher: |
| mark_name_mapping = collections.defaultdict(list) |
| for mark in markers: |
| mark_name_mapping[mark.name].append(mark) |
| return cls(mark_name_mapping) |
|
|
| def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: |
| if not (matches := self.own_mark_name_mapping.get(name, [])): |
| return False |
|
|
| for mark in matches: |
| if all(mark.kwargs.get(k, NOT_SET) == v for k, v in kwargs.items()): |
| return True |
| return False |
|
|
|
|
| def deselect_by_mark(items: list[Item], config: Config) -> None: |
| matchexpr = config.option.markexpr |
| if not matchexpr: |
| return |
|
|
| expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'") |
| remaining: list[Item] = [] |
| deselected: list[Item] = [] |
| for item in items: |
| if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())): |
| remaining.append(item) |
| else: |
| deselected.append(item) |
| if deselected: |
| config.hook.pytest_deselected(items=deselected) |
| items[:] = remaining |
|
|
|
|
| def _parse_expression(expr: str, exc_message: str) -> Expression: |
| try: |
| return Expression.compile(expr) |
| except SyntaxError as e: |
| raise UsageError( |
| f"{exc_message}: {e.text}: at column {e.offset}: {e.msg}" |
| ) from None |
|
|
|
|
| def pytest_collection_modifyitems(items: list[Item], config: Config) -> None: |
| deselect_by_keyword(items, config) |
| deselect_by_mark(items, config) |
|
|
|
|
| def pytest_configure(config: Config) -> None: |
| config.stash[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( |
| f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect" |
| f" but it is {empty_parameterset!r}" |
| ) |
|
|
|
|
| def pytest_unconfigure(config: Config) -> None: |
| MARK_GEN._config = config.stash.get(old_mark_config_key, None) |
|
|