From 034e81934ea1e565c4e422de15c0163f9084ae5a Mon Sep 17 00:00:00 2001 From: Dextheking1 Date: Tue, 22 Sep 2026 23:00:32 +0200 Subject: [PATCH 1/3] Skip fixture-discovery scan for fixture-less plugins (#14877) FixtureManager.parsefactories() walked dir() and validated every attribute of every registered plugin on each Config build, even though most builtin plugins define no fixtures. Holders can now declare this with the __pytest_no_fixtures__ marker, in which case the scan is skipped entirely. The marker is applied to all fixture-less builtin plugin modules, the TerminalReporter, and the Config / PytestPluginManager objects (22 holders per session build). Attribute introspections per session build drop from 2258 to 1052 (-53%) with an identical registered fixture set. A meta test guards the marker: it fails if any marked holder actually defines a fixture, so a fixture can never be silently dropped. Closes #14877. --- changelog/14877.improvement.rst | 9 +++++ src/_pytest/assertion/__init__.py | 4 +++ src/_pytest/config/__init__.py | 8 +++++ src/_pytest/debugging.py | 4 +++ src/_pytest/faulthandler.py | 4 +++ src/_pytest/fixtures.py | 8 +++++ src/_pytest/helpconfig.py | 4 +++ src/_pytest/legacypath.py | 4 +++ src/_pytest/main.py | 4 +++ src/_pytest/mark/__init__.py | 4 +++ src/_pytest/pastebin.py | 4 +++ src/_pytest/python.py | 4 +++ src/_pytest/reports.py | 4 +++ src/_pytest/runner.py | 4 +++ src/_pytest/setuponly.py | 4 +++ src/_pytest/setupplan.py | 4 +++ src/_pytest/skipping.py | 4 +++ src/_pytest/stepwise.py | 4 +++ src/_pytest/terminal.py | 8 +++++ src/_pytest/threadexception.py | 4 +++ src/_pytest/unittest.py | 4 +++ src/_pytest/unraisableexception.py | 4 +++ src/_pytest/warnings.py | 4 +++ testing/test_meta.py | 57 ++++++++++++++++++++++++++++++ 24 files changed, 166 insertions(+) create mode 100644 changelog/14877.improvement.rst diff --git a/changelog/14877.improvement.rst b/changelog/14877.improvement.rst new file mode 100644 index 00000000000..1382ff5b15c --- /dev/null +++ b/changelog/14877.improvement.rst @@ -0,0 +1,9 @@ +Reduced per-session startup overhead in fixture discovery: plugins which declare +they define no fixtures via the new internal ``__pytest_no_fixtures__`` marker +(all fixture-less builtin plugins, the terminal reporter, and the config +objects) are no longer scanned attribute-by-attribute by +``FixtureManager.parsefactories`` on every ``Config`` build. This roughly +halves the number of attribute introspections during plugin registration +(e.g. 2258 to 1052 in a default session) and speeds up anything which builds +many configs, such as :fixture:`pytester`-based test suites. A meta test +guards that no holder carrying the marker actually defines a fixture. diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index a171633f320..d625cac6220 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -3,6 +3,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + from collections.abc import Generator import sys from typing import Any diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c7bd3e1afab..ae8f175e7fb 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -497,6 +497,10 @@ class PytestPluginManager(PluginManager): * ``conftest.py`` loading during start-up. """ + # This plugin defines no fixtures: opt out of the fixture-discovery scan + # in FixtureManager.parsefactories (#14877). + __pytest_no_fixtures__ = True + def __init__(self) -> None: from _pytest.assertion import DummyRewriteHook from _pytest.assertion import RewriteHook @@ -1118,6 +1122,10 @@ class Config: invocation. """ + # This object defines no fixtures: opt out of the fixture-discovery scan + # in FixtureManager.parsefactories (#14877). + __pytest_no_fixtures__ = True + @final @dataclasses.dataclass(frozen=True) class InvocationParams: diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index b256f83c8bf..e51c608a0a8 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -4,6 +4,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import argparse from collections.abc import Callable from collections.abc import Generator diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 080cf583813..d0c099778a9 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -1,5 +1,9 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + from collections.abc import Generator import os import sys diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 30f44d44dfc..b57dd20d33d 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2296,6 +2296,14 @@ def parsefactories( else: holderobj_tp = cast("type | types.ModuleType", holderobj) + # Skip the scan entirely for holders which declare that they define + # no fixtures. Walking dir() and validating every attribute of every + # registered plugin on each Config build is pure overhead for + # fixture-less plugins (#14877). The lookup is done safely, like the + # loop below, because the holder may be an arbitrary user object. + if safe_getattr(holderobj, "__pytest_no_fixtures__", False): + return + for name in dir(holderobj): # Read the raw __dict__ entry first so staticmethod/classmethod # wrappers are not hidden by descriptor binding. diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 8d9d44699c1..a9585424233 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -3,6 +3,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import argparse from collections.abc import Generator from collections.abc import Sequence diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index e1fc38d19ed..038b1b1efb3 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -3,6 +3,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import dataclasses from pathlib import Path import shlex diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d43a68b4679..90907c13cde 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -2,6 +2,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import argparse from collections.abc import Callable from collections.abc import Iterable diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 996322d93f1..9bc399a6f07 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -2,6 +2,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import collections from collections.abc import Collection from collections.abc import Iterable diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index e6a1430220a..a163ef29b11 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -3,6 +3,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + from io import StringIO import tempfile from typing import IO diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 792d0d5b4f3..50553df6f1f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -3,6 +3,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import abc from collections import Counter from collections import defaultdict diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 722f71909d4..550568787f9 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,6 +1,10 @@ # mypy: allow-untyped-defs from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + from collections.abc import Iterable from collections.abc import Iterator from collections.abc import Mapping diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 27c5739845a..ce74cfda185 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -3,6 +3,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import bdb from collections.abc import Callable import dataclasses diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 7e6b46bcdb4..68f0ae6125b 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,5 +1,9 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + from collections.abc import Generator from _pytest._io.saferepr import saferepr diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 4e124cce243..5fe3cf1d40a 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -1,5 +1,9 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index aa97e3b7dc6..bd9d553f6f6 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -3,6 +3,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + from collections.abc import Generator from collections.abc import Mapping import dataclasses diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index c14b6fc250b..707ce8ac163 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,5 +1,9 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import dataclasses from datetime import datetime from datetime import timedelta diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 023fdcaabb8..31797600239 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -6,6 +6,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import argparse from collections import Counter from collections.abc import Callable @@ -385,6 +389,10 @@ def get_location(self, config: Config) -> str | None: @final class TerminalReporter: + # This plugin defines no fixtures: opt out of the fixture-discovery scan + # in FixtureManager.parsefactories (#14877). + __pytest_no_fixtures__ = True + def __init__(self, config: Config, file: TextIO | None = None) -> None: import _pytest.config diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py index 74640873259..a684624029d 100644 --- a/src/_pytest/threadexception.py +++ b/src/_pytest/threadexception.py @@ -1,5 +1,9 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import collections from collections.abc import Callable import functools diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 2c9e0dcb80b..048f7ef219b 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -3,6 +3,10 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + from collections.abc import Callable from collections.abc import Generator from collections.abc import Iterable diff --git a/src/_pytest/unraisableexception.py b/src/_pytest/unraisableexception.py index 32f1258038b..a594ff88a1c 100644 --- a/src/_pytest/unraisableexception.py +++ b/src/_pytest/unraisableexception.py @@ -1,5 +1,9 @@ from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + import collections from collections.abc import Callable import functools diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 2c721d6e3c9..aec884fa4dc 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -1,6 +1,10 @@ # mypy: allow-untyped-defs from __future__ import annotations +# This plugin defines no fixtures: opt out of the fixture-discovery scan in +# FixtureManager.parsefactories (#14877). +__pytest_no_fixtures__ = True + from collections.abc import Generator from contextlib import contextmanager from typing import Literal diff --git a/testing/test_meta.py b/testing/test_meta.py index e7d836f7ace..31c60ba2d7c 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -6,13 +6,22 @@ from __future__ import annotations +import importlib import pkgutil import subprocess import sys +import types import _pytest import pytest +from _pytest.compat import safe_getattr +from _pytest.compat import safe_isclass +from _pytest.config import Config +from _pytest.config import PytestPluginManager +from _pytest.fixtures import FixtureFunctionDefinition +from _pytest.terminal import TerminalReporter + def _modules() -> list[str]: pytest_pkg: str = _pytest.__path__ # type: ignore @@ -32,3 +41,51 @@ def test_no_warnings(module: str) -> None: "-c", f"__import__({module!r})", )) # fmt: on + + +def _opted_out_holders() -> list[object]: + """All holders carrying the ``__pytest_no_fixtures__`` opt-out marker.""" + holders: list[object] = [Config, PytestPluginManager, TerminalReporter] + for _, name, _ in pkgutil.walk_packages( + _pytest.__path__, prefix=_pytest.__name__ + "." + ): + module = importlib.import_module(name) + if safe_getattr(module, "__pytest_no_fixtures__", False): + holders.append(module) + return holders + + +def _discoverable_fixture_names(holder: object) -> list[str]: + """Names which ``FixtureManager.parsefactories`` would register as + fixtures on ``holder``. + + Mirrors the detection in ``parsefactories``: fixtures are looked up on the + module itself, or on the class for instance holders. + """ + if not safe_isclass(holder) and not isinstance(holder, types.ModuleType): + holder = type(holder) + return [ + name + for name in dir(holder) + if type(safe_getattr(holder, name, None)) is FixtureFunctionDefinition + ] + + +def test_no_fixtures_opt_out_holders_define_no_fixtures() -> None: + """Guard for the ``__pytest_no_fixtures__`` opt-out (see #14877). + + ``FixtureManager.parsefactories`` skips fixture discovery entirely for + holders carrying the marker, so any fixture defined on such a holder + would silently never be collected. Fail here if that ever happens. + """ + holders = _opted_out_holders() + assert holders, "expected some holders to carry __pytest_no_fixtures__" + offenders = { + getattr(holder, "__name__", repr(holder)): _discoverable_fixture_names(holder) + for holder in holders + } + offenders = {name: found for name, found in offenders.items() if found} + assert not offenders, ( + "holders marked __pytest_no_fixtures__ define fixtures which " + f"parsefactories would silently skip: {offenders}" + ) From 239b0927ae03a44e20995f8c97b282db21f2f252 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:01:25 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/assertion/__init__.py | 1 + src/_pytest/debugging.py | 1 + src/_pytest/faulthandler.py | 1 + src/_pytest/helpconfig.py | 1 + src/_pytest/legacypath.py | 1 + src/_pytest/main.py | 1 + src/_pytest/mark/__init__.py | 1 + src/_pytest/pastebin.py | 1 + src/_pytest/python.py | 1 + src/_pytest/reports.py | 1 + src/_pytest/runner.py | 1 + src/_pytest/setuponly.py | 1 + src/_pytest/setupplan.py | 1 + src/_pytest/skipping.py | 1 + src/_pytest/stepwise.py | 1 + src/_pytest/terminal.py | 1 + src/_pytest/threadexception.py | 1 + src/_pytest/unittest.py | 1 + src/_pytest/unraisableexception.py | 1 + src/_pytest/warnings.py | 1 + testing/test_meta.py | 3 +-- 21 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index d625cac6220..dcca5ccb7ba 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index e51c608a0a8..afc0a457371 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -4,6 +4,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index d0c099778a9..6677f40bd56 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -1,5 +1,6 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index a9585424233..5fdce3e6de1 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -3,6 +3,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 038b1b1efb3..43a9970adb9 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -3,6 +3,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 90907c13cde..dee3534dc2a 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -2,6 +2,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 9bc399a6f07..1de7047514a 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index a163ef29b11..e39ceac800c 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -3,6 +3,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 50553df6f1f..9ba21993e8e 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -3,6 +3,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 550568787f9..7916593443c 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index ce74cfda185..fc52d6db403 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -3,6 +3,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 68f0ae6125b..f43cfbd2907 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,5 +1,6 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 5fe3cf1d40a..bce514d9183 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -1,5 +1,6 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index bd9d553f6f6..db42f67cece 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -3,6 +3,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 707ce8ac163..b451d7be6ff 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,5 +1,6 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 31797600239..4a12b42e9ce 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -6,6 +6,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py index a684624029d..d6aca448942 100644 --- a/src/_pytest/threadexception.py +++ b/src/_pytest/threadexception.py @@ -1,5 +1,6 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 048f7ef219b..79c2fcbd500 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -3,6 +3,7 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/unraisableexception.py b/src/_pytest/unraisableexception.py index a594ff88a1c..3dca2b12766 100644 --- a/src/_pytest/unraisableexception.py +++ b/src/_pytest/unraisableexception.py @@ -1,5 +1,6 @@ from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index aec884fa4dc..21f370af3cc 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs from __future__ import annotations + # This plugin defines no fixtures: opt out of the fixture-discovery scan in # FixtureManager.parsefactories (#14877). __pytest_no_fixtures__ = True diff --git a/testing/test_meta.py b/testing/test_meta.py index 31c60ba2d7c..f77cca85d2f 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -13,14 +13,13 @@ import types import _pytest -import pytest - from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.config import Config from _pytest.config import PytestPluginManager from _pytest.fixtures import FixtureFunctionDefinition from _pytest.terminal import TerminalReporter +import pytest def _modules() -> list[str]: From 9a062debece434f03ae4f723aa5566e648454423 Mon Sep 17 00:00:00 2001 From: Dex Date: Wed, 23 Sep 2026 14:47:03 +0200 Subject: [PATCH 3/3] test: cover instance-holder path in _discoverable_fixture_names --- testing/test_meta.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testing/test_meta.py b/testing/test_meta.py index f77cca85d2f..1ee57a93d14 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -88,3 +88,11 @@ def test_no_fixtures_opt_out_holders_define_no_fixtures() -> None: "holders marked __pytest_no_fixtures__ define fixtures which " f"parsefactories would silently skip: {offenders}" ) + + +def test_discoverable_fixture_names_supports_instance_holders() -> None: + """Instance holders resolve to their class, mirroring parsefactories.""" + holder_cls = type("Plugin", (), {"my_fixture": pytest.fixture(lambda: 1)}) + + assert _discoverable_fixture_names(holder_cls()) == ["my_fixture"] + assert _discoverable_fixture_names(holder_cls) == ["my_fixture"]