Skip to content
Merged
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
Empty file.
43 changes: 43 additions & 0 deletions packages/sphinx-autodoc-pytest-fixtures/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[project]
name = "sphinx-autodoc-pytest-fixtures"
version = "0.0.1a0"
description = "Sphinx extension for documenting pytest fixtures as first-class objects"
requires-python = ">=3.10,<4.0"
authors = [
{name = "Tony Narlock", email = "tony@git-pull.com"}
]
license = { text = "MIT" }
classifiers = [
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: MIT License",
"Framework :: Sphinx",
"Framework :: Sphinx :: Extension",
"Framework :: Pytest",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Documentation",
"Topic :: Documentation :: Sphinx",
"Topic :: Software Development :: Testing",
"Typing :: Typed",
]
readme = "README.md"
keywords = ["sphinx", "pytest", "fixtures", "documentation", "autodoc"]
dependencies = [
"sphinx",
"pytest",
]

[project.urls]
Repository = "https://github.com/git-pull/gp-sphinx"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/sphinx_autodoc_pytest_fixtures"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""Sphinx extension for documenting pytest fixtures as first-class objects.

Registers ``py:fixture`` as a domain directive and ``autofixture::`` as an
autodoc documenter. Fixtures are rendered with their scope, user-visible
dependencies, and an auto-generated usage snippet rather than as plain
callable signatures.

.. note::

This extension self-registers its CSS via ``add_css_file()``. The rules
live in ``_static/css/sphinx_autodoc_pytest_fixtures.css`` inside this package.
"""

from __future__ import annotations

import logging
import typing as t

from docutils import nodes
from sphinx.domains import ObjType
from sphinx.domains.python import PythonDomain, PyXRefRole

# ---------------------------------------------------------------------------
# Re-exports for backward compatibility (tests access these via the package)
# ---------------------------------------------------------------------------
from sphinx_autodoc_pytest_fixtures._badges import (
_BADGE_TOOLTIPS,
_build_badge_group_node,
)
from sphinx_autodoc_pytest_fixtures._constants import (
_CONFIG_BUILTIN_LINKS,
_CONFIG_EXTERNAL_LINKS,
_CONFIG_HIDDEN_DEPS,
_CONFIG_LINT_LEVEL,
_EXTENSION_KEY,
_EXTENSION_VERSION,
_STORE_VERSION,
PYTEST_BUILTIN_LINKS,
PYTEST_HIDDEN,
SetupDict,
)
from sphinx_autodoc_pytest_fixtures._css import _CSS
from sphinx_autodoc_pytest_fixtures._detection import (
_classify_deps,
_get_fixture_fn,
_get_fixture_marker,
_get_return_annotation,
_get_user_deps,
_is_factory,
_is_pytest_fixture,
_iter_injectable_params,
)
from sphinx_autodoc_pytest_fixtures._directives import (
AutofixtureIndexDirective,
AutofixturesDirective,
PyFixtureDirective,
)
from sphinx_autodoc_pytest_fixtures._documenter import FixtureDocumenter
from sphinx_autodoc_pytest_fixtures._metadata import (
_build_usage_snippet,
_has_authored_example,
_register_fixture_meta,
)
from sphinx_autodoc_pytest_fixtures._models import (
FixtureDep,
FixtureMeta,
autofixture_index_node,
)
from sphinx_autodoc_pytest_fixtures._store import (
_finalize_store,
_get_spf_store,
_on_env_merge_info,
_on_env_purge_doc,
_on_env_updated,
)
from sphinx_autodoc_pytest_fixtures._transforms import (
_depart_abbreviation_html,
_on_doctree_resolved,
_on_missing_reference,
_visit_abbreviation_html,
)

if t.TYPE_CHECKING:
from sphinx.application import Sphinx

logging.getLogger(__name__).addHandler(logging.NullHandler())


def setup(app: Sphinx) -> SetupDict:
"""Register the ``sphinx_autodoc_pytest_fixtures`` extension.

Parameters
----------
app : Sphinx
The Sphinx application instance.

Returns
-------
SetupDict
Extension metadata dict.
"""
app.setup_extension("sphinx.ext.autodoc")

# Register extension CSS so projects adopting this extension get styled
# output without manually copying spf-* rules into their custom.css.
import pathlib

_static_dir = str(pathlib.Path(__file__).parent / "_static")

def _add_static_path(app: Sphinx) -> None:
if _static_dir not in app.config.html_static_path:
app.config.html_static_path.append(_static_dir)

app.connect("builder-inited", _add_static_path)
app.add_css_file("css/sphinx_autodoc_pytest_fixtures.css")

# Override the built-in abbreviation visitor to emit tabindex when set.
# Sphinx's default visit_abbreviation only passes explanation → title,
# silently dropping all other attributes. This override is a strict
# superset — non-badge abbreviation nodes produce identical output.
app.add_node(
nodes.abbreviation,
override=True,
html=(_visit_abbreviation_html, _depart_abbreviation_html),
)

# --- New config values (v1.1) ---
app.add_config_value(
_CONFIG_HIDDEN_DEPS,
default=PYTEST_HIDDEN,
rebuild="env",
types=[frozenset],
)
app.add_config_value(
_CONFIG_BUILTIN_LINKS,
default=PYTEST_BUILTIN_LINKS,
rebuild="env",
types=[dict],
)
app.add_config_value(
_CONFIG_EXTERNAL_LINKS,
default={},
rebuild="env",
types=[dict],
)
app.add_config_value(
_CONFIG_LINT_LEVEL,
default="warning",
rebuild="env",
types=[str],
)

# Register std:fixture so :external+pytest:std:fixture: intersphinx
# references resolve. Pytest registers this in their own conf.py;
# we mirror it so the role is known locally.
app.add_crossref_type("fixture", "fixture")

# Guard against re-registration when setup() is called multiple times.
if "fixture" not in PythonDomain.object_types:
PythonDomain.object_types["fixture"] = ObjType(
"fixture",
"fixture",
"func",
"obj",
)
app.add_directive_to_domain("py", "fixture", PyFixtureDirective)
app.add_role_to_domain("py", "fixture", PyXRefRole())

app.add_autodocumenter(FixtureDocumenter)
app.add_directive("autofixtures", AutofixturesDirective)
app.add_node(autofixture_index_node)
app.add_directive("autofixture-index", AutofixtureIndexDirective)

app.connect("missing-reference", _on_missing_reference)
app.connect("doctree-resolved", _on_doctree_resolved)
app.connect("env-purge-doc", _on_env_purge_doc)
app.connect("env-merge-info", _on_env_merge_info)
app.connect("env-updated", _on_env_updated)

return {
"version": _EXTENSION_VERSION,
"env_version": _STORE_VERSION,
"parallel_read_safe": True,
"parallel_write_safe": True,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Badge group rendering helpers for sphinx_autodoc_pytest_fixtures."""

from __future__ import annotations

from docutils import nodes

from sphinx_autodoc_pytest_fixtures._constants import _SUPPRESSED_SCOPES
from sphinx_autodoc_pytest_fixtures._css import _CSS

_BADGE_TOOLTIPS: dict[str, str] = {
"session": "Scope: session \u2014 created once per test session",
"module": "Scope: module \u2014 created once per test module",
"class": "Scope: class \u2014 created once per test class",
"factory": "Factory \u2014 returns a callable that creates instances",
"override_hook": "Override hook \u2014 customize in conftest.py",
"fixture": "pytest fixture \u2014 injected by name into test functions",
"autouse": "Runs automatically for every test (autouse=True)",
"deprecated": "Deprecated \u2014 see docs for replacement",
}


def _build_badge_group_node(
scope: str,
kind: str,
autouse: bool,
*,
deprecated: bool = False,
show_fixture_badge: bool = True,
) -> nodes.inline:
"""Return a badge group as portable ``nodes.abbreviation`` nodes.

Each badge renders as ``<abbr title="...">`` in HTML, providing hover
tooltips. Non-HTML builders fall back to plain text.

Badge slots (left-to-right in visual order):

* Slot 0 (deprecated): shown when fixture is deprecated
* Slot 1 (scope): shown when ``scope != "function"``
* Slot 2 (kind): shown for ``"factory"`` / ``"override_hook"``; or
state badge (``"autouse"``) when ``autouse=True``
* Slot 3 (FIXTURE): shown when ``show_fixture_badge=True`` (default)

Parameters
----------
scope : str
Fixture scope string.
kind : str
Fixture kind string.
autouse : bool
When True, renders AUTO state badge instead of a kind badge.
deprecated : bool
When True, renders a deprecated badge at slot 0 (leftmost).
show_fixture_badge : bool
When False, suppresses the FIXTURE badge at slot 3. Use in contexts
where the fixture type is already implied (e.g. an index table).

Returns
-------
nodes.inline
Badge group container with abbreviation badge children.
"""
group = nodes.inline(classes=[_CSS.BADGE_GROUP])
badges: list[nodes.abbreviation] = []

# Slot 0 — deprecated badge (leftmost when present)
if deprecated:
badges.append(
nodes.abbreviation(
"deprecated",
"deprecated",
explanation=_BADGE_TOOLTIPS["deprecated"],
classes=[_CSS.BADGE, _CSS.BADGE_STATE, _CSS.DEPRECATED],
)
)

# Slot 1 — scope badge (only non-function scope)
if scope and scope not in _SUPPRESSED_SCOPES:
badges.append(
nodes.abbreviation(
scope,
scope,
explanation=_BADGE_TOOLTIPS.get(scope, f"Scope: {scope}"),
classes=[_CSS.BADGE, _CSS.BADGE_SCOPE, _CSS.scope(scope)],
)
)

# Slot 2 — kind or autouse badge
if autouse:
badges.append(
nodes.abbreviation(
"auto",
"auto",
explanation=_BADGE_TOOLTIPS["autouse"],
classes=[_CSS.BADGE, _CSS.BADGE_STATE, _CSS.AUTOUSE],
)
)
elif kind == "factory":
badges.append(
nodes.abbreviation(
"factory",
"factory",
explanation=_BADGE_TOOLTIPS["factory"],
classes=[_CSS.BADGE, _CSS.BADGE_KIND, _CSS.FACTORY],
)
)
elif kind == "override_hook":
badges.append(
nodes.abbreviation(
"override",
"override",
explanation=_BADGE_TOOLTIPS["override_hook"],
classes=[_CSS.BADGE, _CSS.BADGE_KIND, _CSS.OVERRIDE],
)
)

# Slot 3 — fixture badge (rightmost, suppressed in index table context)
if show_fixture_badge:
badges.append(
nodes.abbreviation(
"fixture",
"fixture",
explanation=_BADGE_TOOLTIPS["fixture"],
classes=[_CSS.BADGE, _CSS.BADGE_FIXTURE],
)
)

# Make badges focusable for touch/keyboard tooltip accessibility.
# Sphinx's built-in visit_abbreviation does NOT emit tabindex — our
# custom visitor override (_visit_abbreviation_html) handles it.
for badge in badges:
badge["tabindex"] = "0"

# Interleave with text separators for non-HTML builders (CSS gap
# handles spacing in HTML; text/LaTeX/man builders need explicit spaces).
for i, badge in enumerate(badges):
group += badge
if i < len(badges) - 1:
group += nodes.Text(" ")

return group
Loading
Loading