--- /dev/null
+"""
+Allows configuring optional test markers in config, see pyproject.toml.
+
+Run optional tests with `pytest --run-optional=...`.
+
+Mark tests to run only if an optional test ISN'T selected by prepending the mark with
+"no_".
+
+You can specify a "no_" prefix straight in config, in which case you can mark tests
+to run when this tests ISN'T selected by omitting the "no_" prefix.
+
+Specifying the name of the default behavior in `--run-optional=` is harmless.
+
+Adapted from https://pypi.org/project/pytest-optional-tests/, (c) 2019 Reece Hart
+"""
+
+from functools import lru_cache
+import itertools
+import logging
+import re
+from typing import FrozenSet, List, Set, TYPE_CHECKING
+
+import pytest
+from _pytest.store import StoreKey
+
+log = logging.getLogger(__name__)
+
+
+if TYPE_CHECKING:
+ from _pytest.config.argparsing import Parser
+ from _pytest.config import Config
+ from _pytest.mark.structures import MarkDecorator
+ from _pytest.nodes import Node
+
+
+ALL_POSSIBLE_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]()
+ENABLED_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]()
+
+
+def pytest_addoption(parser: "Parser") -> None:
+ group = parser.getgroup("collect")
+ group.addoption(
+ "--run-optional",
+ action="append",
+ dest="run_optional",
+ default=None,
+ help="Optional test markers to run; comma-separated",
+ )
+ parser.addini("optional-tests", "List of optional tests markers", "linelist")
+
+
+def pytest_configure(config: "Config") -> None:
+ """Optional tests are markers.
+
+ Use the syntax in https://docs.pytest.org/en/stable/mark.html#registering-marks.
+ """
+ ot_ini = config.inicfg.get("optional-tests") or []
+ ot_markers = set()
+ ot_run: Set[str] = set()
+ if isinstance(ot_ini, str):
+ ot_ini = ot_ini.strip().split("\n")
+ marker_re = re.compile(r"^\s*(?P<no>no_)?(?P<marker>\w+)(:\s*(?P<description>.*))?")
+ for ot in ot_ini:
+ m = marker_re.match(ot)
+ if not m:
+ raise ValueError(f"{ot!r} doesn't match pytest marker syntax")
+
+ marker = (m.group("no") or "") + m.group("marker")
+ description = m.group("description")
+ config.addinivalue_line("markers", f"{marker}: {description}")
+ config.addinivalue_line(
+ "markers", f"{no(marker)}: run when `{marker}` not passed"
+ )
+ ot_markers.add(marker)
+
+ # collect requested optional tests
+ passed_args = config.getoption("run_optional")
+ if passed_args:
+ ot_run.update(itertools.chain.from_iterable(a.split(",") for a in passed_args))
+ ot_run |= {no(excluded) for excluded in ot_markers - ot_run}
+ ot_markers |= {no(m) for m in ot_markers}
+
+ log.info("optional tests to run:", ot_run)
+ unknown_tests = ot_run - ot_markers
+ if unknown_tests:
+ raise ValueError(f"Unknown optional tests wanted: {unknown_tests!r}")
+
+ store = config._store
+ store[ALL_POSSIBLE_OPTIONAL_MARKERS] = frozenset(ot_markers)
+ store[ENABLED_OPTIONAL_MARKERS] = frozenset(ot_run)
+
+
+def pytest_collection_modifyitems(config: "Config", items: "List[Node]") -> None:
+ store = config._store
+ all_possible_optional_markers = store[ALL_POSSIBLE_OPTIONAL_MARKERS]
+ enabled_optional_markers = store[ENABLED_OPTIONAL_MARKERS]
+
+ for item in items:
+ all_markers_on_test = set(m.name for m in item.iter_markers())
+ optional_markers_on_test = all_markers_on_test & all_possible_optional_markers
+ if not optional_markers_on_test or (
+ optional_markers_on_test & enabled_optional_markers
+ ):
+ continue
+ log.info("skipping non-requested optional", item)
+ item.add_marker(skip_mark(frozenset(optional_markers_on_test)))
+
+
+@lru_cache()
+def skip_mark(tests: FrozenSet[str]) -> "MarkDecorator":
+ names = ", ".join(sorted(tests))
+ return pytest.mark.skip(reason=f"Marked with disabled optional tests ({names})")
+
+
+@lru_cache()
+def no(name: str) -> str:
+ if name.startswith("no_"):
+ return name[len("no_") :]
+ return "no_" + name