Curate Sphinx default-value rendering: source text + xref links#36
Open
Curate Sphinx default-value rendering: source text + xref links#36
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #36 +/- ##
==========================================
+ Coverage 91.57% 91.75% +0.18%
==========================================
Files 205 219 +14
Lines 16814 17693 +879
==========================================
+ Hits 15398 16235 +837
- Misses 1416 1458 +42 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This was referenced May 9, 2026
9d2d8b7 to
8dc90a0
Compare
Member
Author
Code reviewFound 1 issue:
gp-sphinx/tests/ext/typehints_gp/test_default_xref_integration.py Lines 294 to 335 in 8dc90a0 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
tony
added a commit
that referenced
this pull request
May 10, 2026
why: PR #36 review caught a function-scoped Sphinx build inside `test_unsupported_default_falls_back_to_plain_text` — CLAUDE.md requires every integration test's Sphinx build to live in a module- or session-scoped fixture so the build cost is shared across runs. The three sibling tests in the file already follow the rule; this one was the outlier because it was the last test added and used a one-off scenario. what: - Hoist the lambda fixture project's module source into a module-level `_LAMBDA_MODULE_SOURCE` constant alongside the existing `_DATA_ATTRIBUTE_MODULE_SOURCE` and `_CROSS_MODULE_*` constants. - Add `lambda_default_html_result` as a `@pytest.fixture(scope="module")` wrapping `build_shared_sphinx_result`, mirroring the existing three fixtures in shape (parameter list, scenario construction, `purge_modules=("lambda_demo",)` argument). - Convert `test_unsupported_default_falls_back_to_plain_text` to consume the fixture parameter and call `read_output` directly; the two assertions are unchanged so the test still pins the plain-text-fallback contract for unparseable lambda defaults.
tony
added a commit
that referenced
this pull request
May 10, 2026
why: PR #36 review caught a function-scoped Sphinx build inside `test_unsupported_default_falls_back_to_plain_text` — CLAUDE.md requires every integration test's Sphinx build to live in a module- or session-scoped fixture so the build cost is shared across runs. The three sibling tests in the file already follow the rule; this one was the outlier because it was the last test added and used a one-off scenario. what: - Hoist the lambda fixture project's module source into a module-level `_LAMBDA_MODULE_SOURCE` constant alongside the existing `_DATA_ATTRIBUTE_MODULE_SOURCE` and `_CROSS_MODULE_*` constants. - Add `lambda_default_html_result` as a `@pytest.fixture(scope="module")` wrapping `build_shared_sphinx_result`, mirroring the existing three fixtures in shape (parameter list, scenario construction, `purge_modules=("lambda_demo",)` argument). - Convert `test_unsupported_default_falls_back_to_plain_text` to consume the fixture parameter and call `read_output` directly; the two assertions are unchanged so the test still pins the plain-text-fallback contract for unparseable lambda defaults.
49ed000 to
d08129a
Compare
Member
Author
Code reviewFound 1 issue:
CLAUDE.md rule: https://github.com/git-pull/gp-sphinx/blob/d08129a03d466a5246e51ebbfaabad2519c09c0f/CLAUDE.md#L520-L526 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
tony
added a commit
to tmux-python/libtmux
that referenced
this pull request
May 10, 2026
…ources why: gp-sphinx PR git-pull/gp-sphinx#36 ships curated parameter and data-attribute default rendering (source-text reprs, dataclass factory resolution, long-value truncation, and `:py:class:`-styled cross-reference links inside default values). Pinning the gp-sphinx-family deps to that branch via `[tool.uv.sources]` lets this repo's docs preview the user-visible win before the workspace release bump propagates the changes via PyPI. After the audit, libtmux's `<libtmux.constants._DefaultOptionScope object>` defaults across `Pane._show_option`, `Window._show_option`, `Session._show_option`, hook dataclasses, etc. drop from 171 ugly sig-params to 0; `scope=` renders as `DEFAULT_OPTION_SCOPE` linked to the documented constant. what: - Add `[tool.uv.sources]` overrides for gp-sphinx, sphinx-autodoc-typehints-gp, sphinx-autodoc-api-style, and sphinx-autodoc-pytest-fixtures, all pointing at the `improved-defaults-reprs` branch with the appropriate monorepo subdirectory. - Regenerate `uv.lock`; uv resolves all transitive workspace siblings (sphinx-fonts, sphinx-gp-opengraph, sphinx-ux-autodoc-layout, etc.) from the same commit. - Revert this commit when gp-sphinx>=0.0.1a18 lands and the per-package version pins move forward in the usual workspace bump.
why: The improved-defaults-reprs branch needs empirical evidence about which parameter and data-attribute defaults render badly across workspace consumers before any framework lands. An earlier audit using `class="default_value"` as the match pattern silently missed every truly ugly case — when a default's repr contains `<`, `ast.parse` of the arglist fails inside `_parse_arglist` and Sphinx falls back to `_pseudo_parse_arglist`, which emits the whole `name=value` as one `desc_sig_name` text run with no `default_value` span at all. The corrected probe matches `<em class="sig-param">… </em>` instead. what: - Add packages/sphinx-autodoc-typehints-gp/scripts/audit_defaults.py with classify_param() recognising factory_sentinel, instance_sentinel, missing_sentinel, other, and long_data_value buckets. Doctests cover each bucket; pathlib-only IO; ruff/mypy clean. - Add notes/defaults-discovery-d1.md recording the aggregate inventory across libtmux/vcspull/gp-libs/gp-sphinx, the per-tree breakdown, sample defaults per ugliness class, and the resolver- catalog implications (DataclassFactoryRepr, SentinelInstanceRepr, BoundMethodRepr, TruncateLongRepr). - Document the architectural consequence: a docutils post-transform on default_value spans cannot reach the cases that need fixing, because those cases produce no default_value spans. Only an upstream string-level fix via autodoc-before-process-signature + DefaultValue shim applies.
why: Across libtmux, libvcs, vcspull, gp-libs, and gp-sphinx, parameter defaults that hold non-primitive Python objects render as `<libtmux.constants._DefaultOptionScope object>` and similar unreadable repr text. Sphinx already ships a fix — `autodoc_preserve_defaults=True` invokes `update_default_value` which parses each function's source via AST and substitutes a `DefaultValue` shim whose `__repr__` returns the literal source text. The flag is off by default upstream and `gp-sphinx.defaults` never overrode it, so the workspace shipped with the ugly repr behavior. D2 evidence (notes/defaults-discovery-d2.md): the flip clears 81/171 (47%) of libtmux's ugly parameter defaults and is a prerequisite for the forthcoming Stage C cross-reference work that depends on having a parseable arglist. what: - Add `DEFAULT_AUTODOC_PRESERVE_DEFAULTS: bool = True` in `gp_sphinx.defaults` with a docstring documenting the limitation on synthetic dataclass / attrs / NamedTuple `__init__` (covered by D3 / Stage B2 follow-on). - Wire `autodoc_preserve_defaults` into the conf dict produced by `merge_sphinx_config()`. - Add `notes/defaults-discovery-d2.md` with reproducible before/after counts: libtmux's `_show_option(scope=...)` renders `DEFAULT_OPTION_SCOPE` instead of `<libtmux.constants._Default OptionScope object>`; libvcs's `DEFAULT_RULES` line under the combined preserve+no-value flags shrinks from 688 chars to 45. - Document why `autodoc_default_options['no-value']=True` was rejected as a workspace default (suppresses useful short values like `'/'`, `'HEAD'`, `OptionScope.Pane`); per-attribute opt-in via `:no-value:` directive option remains the right tool for individual long values until D4's curated resolver lands.
why: After defaulting `autodoc_preserve_defaults=True` workspace-wide
in the previous commit, the only remaining cluster of ugly
parameter defaults is dataclass synthetic `__init__` — Sphinx's own
`update_default_value` bails out at
`_dynamic/_preserve_defaults.py:107-110` because synthetic init has
no source code for `inspect.getsource()`. The empirical inventory
in notes/defaults-discovery-d1.md has libtmux's 90 `<factory>`
occurrences in this bucket. This commit fills the gap so workspace
consumers see clean source-text rendering for `field(default_factory=…)`.
what:
- Add `_param_defaults.py` with `ResolveContext`, `Resolver`
Protocol, `DataclassFactoryRepr` resolver, and
`update_synthetic_defvalues` listener. The listener walks
`dataclasses.fields(parent)`, runs the resolver chain on each
field's `default_factory`, and replaces matching
`Parameter.default` with `sphinx.util.inspect.DefaultValue` shims
so all downstream stringifiers emit the chosen text verbatim.
- `DataclassFactoryRepr` covers stdlib container constructors
(list/dict/set/frozenset/tuple → `[]`, `{}`, `set()`, etc.) and
named callable types (`Foo` → `Foo()`). Lambdas and unrecognised
factories defer (return `None`) — Sphinx's stock `<factory>`
rendering remains for those.
- Connect the listener via `app.connect("autodoc-before-process-
signature", update_synthetic_defvalues)` in extension.setup().
- Add config flag `gp_typehints_curate_param_defaults` (default
`True`) as a kill-switch for downstream debugging.
- Tests: 18 unit tests (Style A NamedTuple parametrization for
each factory shape; mock-app for listener semantics) plus 2
integration tests via `tests/_sphinx_scenarios.py`. Verify the
rendered HTML's `default_value` spans contain the resolver-chosen
text and that `<factory>` no longer appears.
- gp-sphinx own docs audit: 1 ugly factory_sentinel → 0 after this
change.
why: For module-level constants like libvcs's DEFAULT_RULES (688 chars) and class data like GitURL.rule_map (5 738 chars in the original audit), Sphinx emits a `:value: <objrepr>` line that explodes the signature block. The built-in `:no-value:` baseline (D2) suppresses every value indiscriminately — including useful short ones like `'/'` and `OptionScope.Pane` — so it can't ship as a workspace default. This commit adds the curated alternative: a resolver chain that truncates only long values and leaves short ones untouched. what: - Add `_data_defaults.py` with `TruncateLongRepr` resolver, `_curate_value_line` helper, and `GpDataDocumenter` / `GpAttributeDocumenter` subclasses. The subclasses override `add_line` to route `:value:` lines through the curator; everything else passes through. Reuses `ResolveContext` and `_run_chain` from D3's `_param_defaults` module so the resolver catalog grows uniformly across Site A and Site B. - Register the documenters via `app.add_autodocumenter(..., override=True)` and add the kill-switch config flag `gp_typehints_curate_data_defaults` (default `True`). - Tests: 9 unit tests parametrised in Style A NamedTuple form (TruncateLongRepr threshold edges, kill-switch, kind-filtering, pass-through) plus 2 integration tests via `tests/_sphinx_scenarios.py` that build fixture projects with a short and a long module constant and assert the HTML contains the truncated marker only on the long one. - Resolver scope is intentionally conservative for the prototype (truncation only). Richer resolvers (list-of-dataclasses summary, compiled-regex repr) are deferred to D5 where the shared catalog factoring decision lands.
…lt values why: After Stage A makes parameter arglists parseable (D2 + D3), Sphinx emits clean `default_value` spans containing source text — but the identifiers inside (`Foo`, `mod.Foo`, `libtmux_exc.LibTmuxException`) render as plain text. Sphinx never creates `pending_xref` nodes for default values; only for type annotations. The user asked for parameter defaults to render with the same `<a class="reference internal"><code class="xref py py-class"><span class="pre">…</span></code></a>` HTML shape that inline `:py:class:`Foo`` produces in body prose. This commit ships that. what: - Add `_default_xref_transform.py` with `DefaultValueXrefTransform`, a SphinxPostTransform at priority 5 (below ReferencesResolver's 10 so emitted pending_xref nodes still resolve). Walks every `desc_parameter`'s `default_value` span, AST-parses the text, and replaces children with mixed Text + pending_xref output. The pending_xref's contnode is `nodes.literal(classes=['xref','py', 'py-class'])` — exactly the wrapping XRefRole produces. - Recursive `_ast_to_nodes` walker handles `Name`, `Attribute` (dotted chains), `Constant`, `Tuple`, `List`, `Set`, `Call`, and unary minus. Unsupported shapes (lambdas, comprehensions, dicts) raise SyntaxError so the caller falls back to the original text. - `_enclosing_signode_context` extracts `module` / `class` from the surrounding `desc_signature` and forwards them as `py:module` / `py:class` keys on each `pending_xref`, so unqualified targets like `Foo` in `def f(x=Foo)` resolve against the enclosing module — same machinery type annotations use. - Wire into extension.setup() via `app.add_post_transform`. - Reuses D3's `gp_typehints_curate_param_defaults` flag as the kill-switch (Stage C is meaningless without Stage A). - 22 unit tests covering each AST shape (Style A NamedTuple) plus 3 integration tests via _sphinx_scenarios.py asserting the rendered HTML contains `<a class="reference internal" href="#…"><code class="xref py py-class">…` for class identifier defaults and falls back to plain text for lambdas. - Update D3's integration test to use a span-structure-agnostic plain-text reduction; D6's xref wrapping changes the local HTML layout but the rendered text content is unchanged.
why: After D3 and D4 each landed with their own copies of
ResolveContext, Resolver Protocol, and run_chain, the duplication
was ready to be factored into one home. The empirical inventory
in notes/defaults-discovery-d1.md shows the workspace's resolver
needs are homogeneous (factory + truncation), so this is also the
right moment to decide on a public API: hold private behind the
underscore-prefixed module name pending external demand. Migrating
to a public surface later is mechanical (one app.add_default_resolver
call exposed via extension.setup) and the evidence doesn't justify
shipping that maintenance surface speculatively.
what:
- Add `_resolvers.py` hosting `ResolveContext`, `Resolver`,
`run_chain`, `DataclassFactoryRepr`, and `TruncateLongRepr`.
- Slim `_param_defaults.py` down to its listener
(`update_synthetic_defvalues`, `_walk_to_dataclass`) and its
resolver tuple (`_DEFAULT_RESOLVERS = (DataclassFactoryRepr(),)`)
— imports the shared classes.
- Slim `_data_defaults.py` similarly: keeps
`Gp{Data,Attribute}Documenter`, `_curate_value_line`, and its
resolver tuple; imports `TruncateLongRepr` from the shared
module.
- Update tests to import from `_resolvers` so the assertions
exercise the canonical home.
- Add `notes/defaults-discovery-d5.md` documenting the factoring
outcome plus the "hold public API private" decision and the
triggers that should make us revisit it.
- Behavior unchanged. Same 1 536 tests pass; same docs build.
why: libtmux's `_show_option(scope=DEFAULT_OPTION_SCOPE, …)` rendered the identifier with `xref py py-class` styling but no clickable `<a>` wrapping. Cause: `DEFAULT_OPTION_SCOPE` is a module-level data attribute (libtmux/constants.py:61) declared as `DEFAULT_OPTION_SCOPE: _DefaultOptionScope = _DefaultOptionScope()`, not a class. The Python domain's `class` reftype only resolves documented class definitions; the data-attribute target was looked up under the wrong key, the resolution silently failed, and Sphinx kept the contnode while dropping the `<a>` wrapper. Default values reference arbitrary identifiers (classes, data, functions, enum members), so a class-typed xref is too narrow. what: - Switch `_default_xref_transform._xref` to build `pending_xref(reftype="obj")` — the catch-all reftype that resolves any documented Python identifier. - Update the literal contnode classes from `["xref", "py", "py-class"]` to `["xref", "py", "py-obj"]` so the rendered HTML honestly reflects what's being linked (`<code class="xref py py-obj …">`); the rendered link shape is still `<a class="reference internal" href="…"><code class="xref py py-obj …">…</code></a>`, identical to inline `:py:obj:` roles. - Update unit + integration tests to assert the new reftype and styling. - Add a regression test (`test_data_attribute_default_links_to_ documented_constant`) that builds a fixture project where a default references a module-level data attribute and asserts the `href="#…"` resolves and the `<a class="reference internal" …><code class="xref py py-obj …">` wrapping appears. This pins the libtmux `DEFAULT_OPTION_SCOPE`-shaped case so a future reftype change doesn't silently regress it.
why: After the previous reftype=obj fix landed, libtmux's
`_show_option(scope=DEFAULT_OPTION_SCOPE)` still didn't render as a
clickable link — and the rendering inside the dt header looked like
a broken inline-code chip with a heavy background. Two distinct
issues drove the visible regression:
1. The Python domain only ran `searchmode=1` cross-module fuzzy
lookup when the `pending_xref` had a `refspecific` attribute set
(presence-based check at `sphinx/domains/python/__init__.py:942`,
not value-based). The default-value xref didn't set it, so the
resolver only tried exact `<surrounding-module>.<name>` matches
and failed for any default whose target lived in a sibling module
(libtmux's constant lives in `libtmux.constants` while the method
is documented under `libtmux.session`).
2. When the target was genuinely undocumented (e.g. an instance
that autodoc skipped because it had no docstring), Sphinx
correctly dropped the `<a>` wrapping, but the `<code class="xref
py py-obj">` contnode survived. The result was a styled element
that looked clickable but wasn't — worse than plain text.
Plus the `<code class="xref py py-obj">` contnode hits Furo's
`code.literal` rule (chip background, smaller font, padding) which
clashes visually with the bold sig font, the dt:hover background,
and adjacent default-value text spans.
what:
- Set `xref["refspecific"] = True` in `_xref()` so the Python
domain's fuzzy cross-module search runs. Comment cites the
searchmode-flipping line in upstream Sphinx so the trick is
discoverable next time.
- Add `_is_documented(env, target, py_module)` and call it from
`_ast_to_nodes` for both `ast.Name` and `ast.Attribute` branches.
When the target isn't in `env.domaindata['py']['objects']` (after
trying bare, prefixed, and `.endswith` lookups) emit plain
`nodes.Text` instead of a `pending_xref` — no misleading xref
styling on unresolvable identifiers.
- Add `_static/css/typehints_gp.css` neutralising the inline-code
chip styling for `code.literal` nested inside `.default_value`
spans. Specificity 0,2,1 beats Furo's `code.literal` rule (0,1,1)
without `!important`. Wire it via `app.add_css_file` plus a
`builder-inited` static-path injection — same pattern
`sphinx-autodoc-fastmcp` uses.
- Add two integration tests
(`test_default_xref_integration.py`):
- `test_data_attribute_default_links_to_documented_constant`
pins the libtmux `DEFAULT_OPTION_SCOPE`-shape: a default
referencing a module-level data attribute resolves and renders
the `<a class="reference internal"><code class="xref py py-obj">`
wrapping.
- `test_cross_module_default_resolves_via_refspecific` builds a
project where the default's target lives in a sibling module
of the function being documented, asserting the cross-module
resolution path stays wired (regression guard for the
refspecific flip).
- Update `test_setup_registers_builder_inited_cache_clearing` to
assert `_clear_caches` is *among* the registered handlers rather
than the only one (the new static-path injection is also
connected to `builder-inited`).
…ield lists
why: Field-list rendering across libtmux/libvcs/gp-sphinx mixed three
inconsistent shapes for Python identifier references, none matching
the inline `:py:class:`-role HTML the user identified as the
canonical "code link" target. Sphinx's `TypedField.make_field` wraps
parameter types in `addnodes.literal_emphasis` (`<em>`); its
`GroupedField.make_field` wraps `:raises:` exception names in
`addnodes.literal_strong` (`<strong>`); typehints-gp's
`_annotation_to_nodes` emits `pending_xref(contnode=Text)` (no
wrapping). The visible result was `<a>Server</a>` for param types,
`<a><strong>exc.X</strong></a>` for raises, and `<a>None</a>` for
intersphinx return types — none of them looking like the inline
`<a><code class="xref py py-class">Pane</code></a>` shape used in
body prose. The whole `name (str, optional)` prefix on each
parameter row also rendered in the body sans-serif font instead of
monospace.
what:
- Add `_field_xref_transform.py` with two SphinxPostTransforms:
- `FieldListXrefStyleTransform` (priority 5, before
`ReferencesResolver`) walks every Python-domain `pending_xref`
inside any `nodes.field_list` ancestor and replaces its
contnode with a single
`nodes.literal('', '', Text(title), classes=['xref','py',
'py-class'|'py-exc'])`. Role class is `py-exc` for fields
matching `raise`/`except` (Napoleon's `Raises` heading
included), `py-class` everywhere else. Also sets
`refspecific=True` so the Python domain's cross-module fuzzy
lookup runs (same fix logic as the default-value xref
transform).
- `FieldListPrefixWrapTransform` (priority 6) wraps each field
body's first paragraph prefix (everything before the
Sphinx-emitted en-dash separator, U+2013) in
`nodes.inline(classes=['gp-sphinx-field-prefix'])` so a single
CSS rule can render the prefix in monospace. Skipped on
prose-style fields (`Returns`, `Yields`, `Notes`, `Examples`,
`Warning`, `See Also`, `Tip`, `Summary`, `Description`) —
those bodies are free-form description text and would clash
with embedded inline `<code>` spans like `:any:`None``.
`Return type` / `rtype` / `ytype` are explicitly distinguished
by the trailing `type` token so they DO get wrapped.
- Wire both transforms via `app.add_post_transform` in
`extension.setup()`.
- Extend `_static/css/typehints_gp.css` with two rules: a
`.gp-sphinx-field-prefix { font-family: var(--font-stack--
monospace); }` for the wrapper and a `.field-list code.literal {
background: transparent; ... }` to neutralise Furo's chip
styling on the canonical `<code class="xref py py-X">` nested
inside the prefix.
- Add a unit + integration suite covering every transformation
pathway — Style A NamedTuple parametrization for each contnode
shape and field-name mapping, plus _sphinx_scenarios fixture
projects exercising the prose-skip behavior and idempotency.
- Refresh sphinx-pytest-fixtures doctree snapshots that captured
the pre-transform shape; the new rendering matches the canonical
HTML the user has been aligning to.
why: The existing examples page rendered a single `autofunction:: compact_function` and didn't exercise any of the six rendering improvements landed on `improved-defaults-reprs` (`autodoc_preserve_defaults` flip, dataclass `default_factory` rendering, long-data truncation, default-value cross-references, field-list xref styling, prefix monospace wrapper). Browsing http://localhost:3124/packages/sphinx-autodoc-typehints-gp/examples/ gave no visible signal of what this extension does that stock Sphinx + autodoc doesn't. what: - Add `docs/_ext/api_demo_typehints_gp.py`, a self-contained demo module that exercises every improvement with realistic-feeling content: a `CacheScope` enum, a `_DefaultRetry` sentinel + `DEFAULT_RETRY` instance with attribute docstring, a `Transport` class, a `ConnectionFailure` exception, `SHORT_DEFAULT` / `LONG_DEFAULT_RULES` data attributes, a `HookCounters` dataclass with five `field(default_factory=…)` shapes (list/dict/set/tuple/Foo), an `open_session` function whose `scope=` and `retry=` defaults link to the documented enum member and sentinel, and `with_lambda_default` exercising the plain-text fallback for unparseable defaults. - Rewrite `docs/packages/sphinx-autodoc-typehints-gp/examples.md` from 18 lines to a multi-section showcase: each section opens with a one-line "what's interesting here", autodocs the relevant demo, then ends with a "what to look for" callout pointing at the specific HTML shape worth noticing (factory text without `<factory>`, `<...truncated, N chars>`, `<a><code class="xref py py-class">…</code></a>` wrapping for parameter / return / raises types, `gp-sphinx-field-prefix` monospace wrapper for the prefix portion, plain-text fallback for lambda defaults). - The `:noindex:` flags I initially used were dropped: with them the demo classes / data don't register in `env.domaindata['py']['objects']`, so my Stage C transform's `_is_documented` pre-resolution check returns False and falls back to plain text. Without `:noindex:` the targets register cleanly (no duplicate-id warnings — these names appear only on the examples page) and the `scope=CacheScope.Session` / `retry=DEFAULT_RETRY` defaults render as live cross-references pointing at the documented enum member / sentinel a few paragraphs down on the same page.
why: PR #36 review caught a function-scoped Sphinx build inside `test_unsupported_default_falls_back_to_plain_text` — CLAUDE.md requires every integration test's Sphinx build to live in a module- or session-scoped fixture so the build cost is shared across runs. The three sibling tests in the file already follow the rule; this one was the outlier because it was the last test added and used a one-off scenario. what: - Hoist the lambda fixture project's module source into a module-level `_LAMBDA_MODULE_SOURCE` constant alongside the existing `_DATA_ATTRIBUTE_MODULE_SOURCE` and `_CROSS_MODULE_*` constants. - Add `lambda_default_html_result` as a `@pytest.fixture(scope="module")` wrapping `build_shared_sphinx_result`, mirroring the existing three fixtures in shape (parameter list, scenario construction, `purge_modules=("lambda_demo",)` argument). - Convert `test_unsupported_default_falls_back_to_plain_text` to consume the fixture parameter and call `read_output` directly; the two assertions are unchanged so the test still pins the plain-text-fallback contract for unparseable lambda defaults.
why: post-autosquash review surfaced a doctest gap and several
docstring-drift items; deep-dive against ~/study/{sphinx,docutils,
myst-parser,pytest} confirmed the doctest gap is a real CLAUDE.md
violation, the dataclass scope is narrower than documented, and the
"em-dash" naming should match the `_EN_DASH` constant.
what:
- _field_xref_transform.py: add Examples block to
`_wrap_prefix_in_paragraph`; fix em-dash → en-dash naming in
`_is_em_dash_separator`, `_wrap_prefix_in_paragraph`, and the
module/`FieldListPrefixWrapTransform` docstrings; add a Limitations
paragraph explaining the cosmetic homogenisation of py-fixture and
other sibling-package reftypes (reftype is preserved on the xref;
only contnode classes are normalised).
- _default_xref_transform.py: add Examples blocks to `_wrap_seq` and
`_attr_chain` so every helper in the module has a function-level
doctest matching the rest of the file.
- _param_defaults.py: narrow module docstring to "dataclass synthetic
__init__ specifically" — `_walk_to_dataclass` only handles
dataclasses, NamedTuple defaults need no substitution (their repr
is already source text), and attrs is not handled today; clarify
that the resolver chain only runs over `default_factory` (plain
`default` values pass through with their own correct repr).
- scripts/audit_defaults.py: fix label inference from
`path.parent.name` (parent dir name) to `path.name` (basename) to
match the argparse help text.
why: field-list parameter rows rendered visually heavier than body prose because the monospace prefix wrapper picked up the cascade's default 16px instead of Furo's inline-code size; some responsive layers further nudge that to 110%. Pinning to a root-relative rem keeps the prefix at ≤13px regardless of any percentage scaling upstream while still respecting browser-level accessibility zoom. what: - .gp-sphinx-field-prefix: add `font-size: 0.8125rem` so the prefix matches Furo's inline-code chip size (~13px at default 16px root) without compounding against any cascade-level percentage scaling. - .field-list code.literal: `font-size: inherit` becomes load- bearing — without it, Furo's `var(--font-size--small--2)` (81.25%) would shrink type names to ~10.5px against the new 13px wrapper; document the dependency in the rule's comment.
why: previous commit pinned only the prefix wrapper to ~13px; the description text after the en-dash and the entire Returns/Raises bodies still rendered at the inherited 16px, so a Parameters row jumped from 13px prefix to 16px description and the field-list visually outsized the body prose around the autodoc card. what: - .field-list > dd: pin the dd content to `0.8125rem` so every field-list row reads at one size (13px sans for descriptions, 13px mono for the prefix). dt labels keep their 0.85em rule from api_style.css (~14px), preserving the prose 16 → label 14 → body 13 hierarchy.
2d22ead to
3f669e1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
autodoc_preserve_defaults=Trueworkspace-wide and adds anautodoc-before-process-signaturelistener that fills the synthetic-__init__gap Sphinx itself bails out of (sphinx/ext/autodoc/_dynamic/_preserve_defaults.py:107-110). libtmux'sscope=<libtmux.constants._DefaultOptionScope object>becomesscope=DEFAULT_OPTION_SCOPE; dataclassfield(default_factory=list)becomes=[]instead of=<factory>.GpDataDocumenter/GpAttributeDocumentersubclass routes:value:lines through a resolver chain.TruncateLongRepr(threshold=200)collapses libvcs's 5 738-charDEFAULT_RULESline to<...truncated, N chars>while leaving short useful values ('/','HEAD') untouched.SphinxPostTransformwalks everynodes.inline.default_valueinsidedesc_parameter, AST-parses the text, and replaces identifier nodes withpending_xref(reftype='class')wrapped innodes.literal(classes=['xref','py','py-class'])— matching the exact<a class=\"reference internal\"><code class=\"xref py py-class…\">…</code></a>HTML shape of an inline:py:class:role. Hand-written.. py:function::directives benefit too because the span structure is identical.Background, codepath traces, and per-consumer audit numbers live in
notes/defaults-discovery-d{1..6}.md. The architecture is a singleResolverProtocol consumed by both sites; the publicadd_default_resolverAPI is held private behind_resolvers.pyuntil external demand surfaces (rationale indefaults-discovery-d5.md).Empirical impact (libtmux baseline 171 ugly parameter defaults):
Test plan
uv run ruff check . --fix --show-fixescleanuv run ruff format .no diffuv run mypy— 0 issues across 232 source filesuv run pytest --reruns 0 -vvv— 1 536 passed, 161 skipped (61 new tests added intests/ext/typehints_gp/test_{param_defaults,data_defaults,default_xref}{,_integration}.py)rm -rf docs/_build && just build-docssucceeds with no new warnings<a class=\"reference internal\" href=\"#…\"><code class=\"xref py py-class docutils literal notranslate\"><span class=\"pre\">…</span></code></a>)gp-sphinx==0.0.1a17, so the win surfaces only once the version + sibling pins are updated downstream