All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
2 Allows configuring optional test markers in config, see pyproject.toml.
4 Run optional tests with `pytest --run-optional=...`.
6 Mark tests to run only if an optional test ISN'T selected by prepending the mark with
9 You can specify a "no_" prefix straight in config, in which case you can mark tests
10 to run when this tests ISN'T selected by omitting the "no_" prefix.
12 Specifying the name of the default behavior in `--run-optional=` is harmless.
14 Adapted from https://pypi.org/project/pytest-optional-tests/, (c) 2019 Reece Hart
17 from functools import lru_cache
21 from typing import FrozenSet, List, Set, TYPE_CHECKING
26 from pytest import StashKey
29 from _pytest.store import StoreKey as StashKey
31 log = logging.getLogger(__name__)
35 from _pytest.config.argparsing import Parser
36 from _pytest.config import Config
37 from _pytest.mark.structures import MarkDecorator
38 from _pytest.nodes import Node
41 ALL_POSSIBLE_OPTIONAL_MARKERS = StashKey[FrozenSet[str]]()
42 ENABLED_OPTIONAL_MARKERS = StashKey[FrozenSet[str]]()
45 def pytest_addoption(parser: "Parser") -> None:
46 group = parser.getgroup("collect")
52 help="Optional test markers to run; comma-separated",
54 parser.addini("optional-tests", "List of optional tests markers", "linelist")
57 def pytest_configure(config: "Config") -> None:
58 """Optional tests are markers.
60 Use the syntax in https://docs.pytest.org/en/stable/mark.html#registering-marks.
62 ot_ini = config.inicfg.get("optional-tests") or []
64 ot_run: Set[str] = set()
65 if isinstance(ot_ini, str):
66 ot_ini = ot_ini.strip().split("\n")
67 marker_re = re.compile(r"^\s*(?P<no>no_)?(?P<marker>\w+)(:\s*(?P<description>.*))?")
69 m = marker_re.match(ot)
71 raise ValueError(f"{ot!r} doesn't match pytest marker syntax")
73 marker = (m.group("no") or "") + m.group("marker")
74 description = m.group("description")
75 config.addinivalue_line("markers", f"{marker}: {description}")
76 config.addinivalue_line(
77 "markers", f"{no(marker)}: run when `{marker}` not passed"
79 ot_markers.add(marker)
81 # collect requested optional tests
82 passed_args = config.getoption("run_optional")
84 ot_run.update(itertools.chain.from_iterable(a.split(",") for a in passed_args))
85 ot_run |= {no(excluded) for excluded in ot_markers - ot_run}
86 ot_markers |= {no(m) for m in ot_markers}
88 log.info("optional tests to run:", ot_run)
89 unknown_tests = ot_run - ot_markers
91 raise ValueError(f"Unknown optional tests wanted: {unknown_tests!r}")
94 store[ALL_POSSIBLE_OPTIONAL_MARKERS] = frozenset(ot_markers)
95 store[ENABLED_OPTIONAL_MARKERS] = frozenset(ot_run)
98 def pytest_collection_modifyitems(config: "Config", items: "List[Node]") -> None:
100 all_possible_optional_markers = store[ALL_POSSIBLE_OPTIONAL_MARKERS]
101 enabled_optional_markers = store[ENABLED_OPTIONAL_MARKERS]
104 all_markers_on_test = {m.name for m in item.iter_markers()}
105 optional_markers_on_test = all_markers_on_test & all_possible_optional_markers
106 if not optional_markers_on_test or (
107 optional_markers_on_test & enabled_optional_markers
110 log.info("skipping non-requested optional", item)
111 item.add_marker(skip_mark(frozenset(optional_markers_on_test)))
115 def skip_mark(tests: FrozenSet[str]) -> "MarkDecorator":
116 names = ", ".join(sorted(tests))
117 return pytest.mark.skip(reason=f"Marked with disabled optional tests ({names})")
121 def no(name: str) -> str:
122 if name.startswith("no_"):
123 return name[len("no_") :]