Skip to content

Commit 191208d

Browse files
authored
feat(_ext[sphinx_pytest_fixtures]): Sphinx extension for pytest fixture documentation (#656)
## Summary - New Sphinx extension (`docs/_ext/sphinx_pytest_fixtures/`) that renders pytest fixtures as first-class API documentation with scope/kind/factory badges, cross-referenced dependencies, and auto-generated usage snippets - `.. autofixture::` directive — autodoc-style documenter for individual fixtures - `.. autofixtures::` directive — bulk discovery and rendering from a module - `.. autofixture-index::` directive — auto-generated summary table with linked return types (via intersphinx) and parsed RST descriptions - `:fixture:` cross-reference role with short-name resolution - Scope, kind, and autouse badges with touch-accessible tooltips and WCAG AA contrast compliance (light + dark mode) - Responsive layout: mobile metadata stacking, badge font scaling, table scroll wrapper - Restructured fixture reference page with Quick Start, decision guide, grouped fixture cards, and configuration reference - Badge demo page for visual QA across themes and viewports - Fix `pytest_plugin.py` docstrings: remove duplicate hand-written fields now auto-generated by the extension, update `:func:` → `:fixture:` xrefs
2 parents a0c0220 + 4a0dc69 commit 191208d

27 files changed

+7297
-22
lines changed

CHANGES

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,34 @@ $ uvx --from 'libtmux' --prerelease allow python
4040
_Notes on the upcoming release will go here._
4141
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
4242

43+
### What's new
44+
45+
#### pytest plugin: fixture reference documentation with autofixture directives (#656)
46+
47+
New Sphinx extension (`docs/_ext/sphinx_pytest_fixtures.py`) renders pytest
48+
fixtures as first-class API documentation with scope/kind/factory badges,
49+
cross-referenced dependencies, and auto-generated usage snippets.
50+
51+
- `.. autofixture::` directive — autodoc-style documenter for individual fixtures
52+
- `.. autofixtures::` directive — bulk discovery and rendering from a module
53+
- `.. autofixture-index::` directive — auto-generated index table with linked
54+
return types (via intersphinx) and parsed RST descriptions
55+
- `:fixture:` cross-reference role with short-name resolution
56+
- Scope, kind, and autouse badges with touch-accessible tooltips (`tabindex`
57+
+ CSS `:focus` popup)
58+
- Responsive layout: mobile metadata stacking, badge font scaling, table scroll
59+
wrapper
60+
- WCAG AA contrast compliance for all badge colors (light and dark mode)
61+
- 109 tests (unit + integration)
62+
63+
### Bug fixes
64+
65+
- Fix `session_params` fixture docstring incorrectly describing return type (#656)
66+
67+
### Development
68+
69+
- Add `types-docutils` to dev dependencies for mypy type checking (#656)
70+
4371
## libtmux 0.55.0 (2026-03-07)
4472

4573
### What's new

docs/_ext/spf_demo_fixtures.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Synthetic fixtures for the sphinx_pytest_fixtures badge demo page.
2+
3+
Each fixture exercises one badge-slot combination so the demo page can show
4+
every permutation side-by-side:
5+
6+
Scope: session | module | class | function (suppressed — no badge)
7+
Kind: resource (suppressed) | factory | override_hook
8+
State: autouse | deprecated (set via RST :deprecated: option)
9+
Combos: session+factory, session+autouse
10+
11+
These fixtures are purely for documentation; they are never collected by
12+
pytest during a real test run (the module is not in the test tree).
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import pytest
18+
19+
20+
@pytest.fixture
21+
def demo_plain() -> str:
22+
"""Plain function-scope resource. Shows FIXTURE badge only."""
23+
return "plain"
24+
25+
26+
@pytest.fixture(scope="session")
27+
def demo_session() -> str:
28+
"""Session-scoped resource. Shows SESSION + FIXTURE badges."""
29+
return "session"
30+
31+
32+
@pytest.fixture(scope="module")
33+
def demo_module() -> str:
34+
"""Module-scoped resource. Shows MODULE + FIXTURE badges."""
35+
return "module"
36+
37+
38+
@pytest.fixture(scope="class")
39+
def demo_class() -> str:
40+
"""Class-scoped resource. Shows CLASS + FIXTURE badges."""
41+
return "class"
42+
43+
44+
@pytest.fixture
45+
def demo_factory() -> type[str]:
46+
"""Return a callable (factory kind). Shows FACTORY + FIXTURE badges."""
47+
return str
48+
49+
50+
@pytest.fixture
51+
def demo_override_hook() -> str:
52+
"""Override hook — customise in conftest.py. Shows OVERRIDE + FIXTURE badges."""
53+
return "override"
54+
55+
56+
@pytest.fixture(autouse=True)
57+
def demo_autouse() -> None:
58+
"""Autouse fixture. Shows AUTO + FIXTURE badges."""
59+
60+
61+
@pytest.fixture
62+
def demo_deprecated() -> str:
63+
"""Return a value (deprecated since 1.0, replaced by :func:`demo_plain`).
64+
65+
This fixture is documented with the ``deprecated`` RST option so the
66+
demo page can show the DEPRECATED + FIXTURE badge combination.
67+
"""
68+
return "deprecated"
69+
70+
71+
@pytest.fixture(scope="session")
72+
def demo_session_factory() -> type[str]:
73+
"""Session-scoped factory. Shows SESSION + FACTORY + FIXTURE badges."""
74+
return str
75+
76+
77+
@pytest.fixture(scope="session", autouse=True)
78+
def demo_session_autouse() -> None:
79+
"""Session-scoped autouse. Shows SESSION + AUTO + FIXTURE badges."""
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Sphinx extension for documenting pytest fixtures as first-class objects.
2+
3+
Registers ``py:fixture`` as a domain directive and ``autofixture::`` as an
4+
autodoc documenter. Fixtures are rendered with their scope, user-visible
5+
dependencies, and an auto-generated usage snippet rather than as plain
6+
callable signatures.
7+
8+
.. note::
9+
10+
This extension self-registers its CSS via ``add_css_file()``. The rules
11+
live in ``_static/css/sphinx_pytest_fixtures.css`` inside this package.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import typing as t
17+
18+
from docutils import nodes
19+
from sphinx.domains import ObjType
20+
from sphinx.domains.python import PythonDomain, PyXRefRole
21+
22+
# ---------------------------------------------------------------------------
23+
# Re-exports for backward compatibility (tests access these via the package)
24+
# ---------------------------------------------------------------------------
25+
from sphinx_pytest_fixtures._badges import (
26+
_BADGE_TOOLTIPS,
27+
_build_badge_group_node,
28+
)
29+
from sphinx_pytest_fixtures._constants import (
30+
_CONFIG_BUILTIN_LINKS,
31+
_CONFIG_EXTERNAL_LINKS,
32+
_CONFIG_HIDDEN_DEPS,
33+
_CONFIG_LINT_LEVEL,
34+
_EXTENSION_KEY,
35+
_EXTENSION_VERSION,
36+
_STORE_VERSION,
37+
PYTEST_BUILTIN_LINKS,
38+
PYTEST_HIDDEN,
39+
SetupDict,
40+
)
41+
from sphinx_pytest_fixtures._css import _CSS
42+
from sphinx_pytest_fixtures._detection import (
43+
_classify_deps,
44+
_get_fixture_fn,
45+
_get_fixture_marker,
46+
_get_return_annotation,
47+
_get_user_deps,
48+
_is_factory,
49+
_is_pytest_fixture,
50+
_iter_injectable_params,
51+
)
52+
from sphinx_pytest_fixtures._directives import (
53+
AutofixtureIndexDirective,
54+
AutofixturesDirective,
55+
PyFixtureDirective,
56+
)
57+
from sphinx_pytest_fixtures._documenter import FixtureDocumenter
58+
from sphinx_pytest_fixtures._metadata import (
59+
_build_usage_snippet,
60+
_has_authored_example,
61+
_register_fixture_meta,
62+
)
63+
from sphinx_pytest_fixtures._models import (
64+
FixtureDep,
65+
FixtureMeta,
66+
autofixture_index_node,
67+
)
68+
from sphinx_pytest_fixtures._store import (
69+
_finalize_store,
70+
_get_spf_store,
71+
_on_env_merge_info,
72+
_on_env_purge_doc,
73+
_on_env_updated,
74+
)
75+
from sphinx_pytest_fixtures._transforms import (
76+
_depart_abbreviation_html,
77+
_on_doctree_resolved,
78+
_on_missing_reference,
79+
_visit_abbreviation_html,
80+
)
81+
82+
if t.TYPE_CHECKING:
83+
from sphinx.application import Sphinx
84+
85+
86+
def setup(app: Sphinx) -> SetupDict:
87+
"""Register the ``sphinx_pytest_fixtures`` extension.
88+
89+
Parameters
90+
----------
91+
app : Sphinx
92+
The Sphinx application instance.
93+
94+
Returns
95+
-------
96+
SetupDict
97+
Extension metadata dict.
98+
"""
99+
app.setup_extension("sphinx.ext.autodoc")
100+
101+
# Register extension CSS so projects adopting this extension get styled
102+
# output without manually copying spf-* rules into their custom.css.
103+
import pathlib
104+
105+
_static_dir = str(pathlib.Path(__file__).parent / "_static")
106+
107+
def _add_static_path(app: Sphinx) -> None:
108+
if _static_dir not in app.config.html_static_path:
109+
app.config.html_static_path.append(_static_dir)
110+
111+
app.connect("builder-inited", _add_static_path)
112+
app.add_css_file("css/sphinx_pytest_fixtures.css")
113+
114+
# Override the built-in abbreviation visitor to emit tabindex when set.
115+
# Sphinx's default visit_abbreviation only passes explanation → title,
116+
# silently dropping all other attributes. This override is a strict
117+
# superset — non-badge abbreviation nodes produce identical output.
118+
app.add_node(
119+
nodes.abbreviation,
120+
override=True,
121+
html=(_visit_abbreviation_html, _depart_abbreviation_html),
122+
)
123+
124+
# --- New config values (v1.1) ---
125+
app.add_config_value(
126+
_CONFIG_HIDDEN_DEPS,
127+
default=PYTEST_HIDDEN,
128+
rebuild="env",
129+
types=[frozenset],
130+
)
131+
app.add_config_value(
132+
_CONFIG_BUILTIN_LINKS,
133+
default=PYTEST_BUILTIN_LINKS,
134+
rebuild="env",
135+
types=[dict],
136+
)
137+
app.add_config_value(
138+
_CONFIG_EXTERNAL_LINKS,
139+
default={},
140+
rebuild="env",
141+
types=[dict],
142+
)
143+
app.add_config_value(
144+
_CONFIG_LINT_LEVEL,
145+
default="warning",
146+
rebuild="env",
147+
types=[str],
148+
)
149+
150+
# Register std:fixture so :external+pytest:std:fixture: intersphinx
151+
# references resolve. Pytest registers this in their own conf.py;
152+
# we mirror it so the role is known locally.
153+
app.add_crossref_type("fixture", "fixture")
154+
155+
# Guard against re-registration when setup() is called multiple times.
156+
if "fixture" not in PythonDomain.object_types:
157+
PythonDomain.object_types["fixture"] = ObjType(
158+
"fixture",
159+
"fixture",
160+
"func",
161+
"obj",
162+
)
163+
app.add_directive_to_domain("py", "fixture", PyFixtureDirective)
164+
app.add_role_to_domain("py", "fixture", PyXRefRole())
165+
166+
app.add_autodocumenter(FixtureDocumenter)
167+
app.add_directive("autofixtures", AutofixturesDirective)
168+
app.add_node(autofixture_index_node)
169+
app.add_directive("autofixture-index", AutofixtureIndexDirective)
170+
171+
app.connect("missing-reference", _on_missing_reference)
172+
app.connect("doctree-resolved", _on_doctree_resolved)
173+
app.connect("env-purge-doc", _on_env_purge_doc)
174+
app.connect("env-merge-info", _on_env_merge_info)
175+
app.connect("env-updated", _on_env_updated)
176+
177+
return {
178+
"version": _EXTENSION_VERSION,
179+
"env_version": _STORE_VERSION,
180+
"parallel_read_safe": True,
181+
"parallel_write_safe": True,
182+
}

0 commit comments

Comments
 (0)