Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog/14877.improvement.rst
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions src/_pytest/assertion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

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
Expand Down
8 changes: 8 additions & 0 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/faulthandler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,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 os
import sys
Expand Down
8 changes: 8 additions & 0 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/helpconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/legacypath.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/mark/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/pastebin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/reports.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# 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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/setuponly.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,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 _pytest._io.saferepr import saferepr
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/setupplan.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,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 _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config.argparsing import Parser
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/skipping.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/stepwise.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,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 datetime import datetime
from datetime import timedelta
Expand Down
9 changes: 9 additions & 0 deletions src/_pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

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
Expand Down Expand Up @@ -385,6 +390,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

Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/threadexception.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,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 Callable
import functools
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

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
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/unraisableexception.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,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 Callable
import functools
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/warnings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# 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
Expand Down
64 changes: 64 additions & 0 deletions testing/test_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@

from __future__ import annotations

import importlib
import pkgutil
import subprocess
import sys
import types

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


Expand All @@ -32,3 +40,59 @@ 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}"
)


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"]
Loading