Describe every class member once, with its real signature#72
Merged
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #72 +/- ##
==========================================
+ Coverage 92.80% 92.94% +0.14%
==========================================
Files 275 276 +1
Lines 22123 22753 +630
==========================================
+ Hits 20531 21148 +617
- Misses 1592 1605 +13 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
tony
force-pushed
the
class-variables
branch
4 times, most recently
from
July 26, 2026 16:19
d3694be to
a4c4183
Compare
why: A class variable, an init-only dataclass field, and a property without a docstring all reach the page as a signature with nothing beneath it. Autodoc renders the annotation and value but has no description to attach, so the Attributes entry the author wrote was deleted in favor of an empty entry -- silently, with no warning. what: - Capture each marked field directive's prose while a class documents its members, dropping the type and value the member renders itself - Add FieldDocFallbackMixin, returning that prose from get_doc() when the member has no description of its own, the way autodoc already reattaches a #: source comment - Apply the mixin to the attribute documenter and register a property documenter carrying it - Cover the class variable, InitVar, and undocumented property cases
why: undoc-members is on by default, so autodoc emits a stub for every class variable a class declares whether or not anything describes it. A reference page fills with bare annotations nobody wrote about -- pydantic's model_config on every model, a dataclass's dispatch metadata on every operation -- and the described members are lost in the run. what: - Skip a class variable with no Attributes entry and no #: comment - Add gp_typehints_show_undocumented_class_vars to restore them - Look up source comments the way autodoc does, walking the MRO through ModuleAnalyzer, so a described variable always renders
why: AnnotationDisplay holds its fields in slots, and the exclusion predates the commit that taught the field matcher to recognize a slot-held field. Removing it leaves one description per field with no duplicate-description warning. what: - Document every AnnotationDisplay field through :members: alone
why: A class documented with :no-index: has that option written into each attribute directive its Attributes section emits. Reattaching the description to the member carried the option along with it, so the member rendered a "No-index" field above the author's sentence. what: - Drop :no-index: and :noindex: alongside the type and value when lifting a description out of its directive
why: A property overriding an inherited dataclass field has no docstring of its own, so autodoc walks the MRO and finds the base field's default value. The default is a str, so str.__doc__ arrived as though it described the property -- and, being non-empty, it counted as a description and displaced the author's Attributes entry. The rendered page showed "str(object='') -> str" where the author had written a sentence. what: - Use the Attributes description unless the member itself carries one, either as its own __doc__ or as a #: comment anywhere in the MRO - Cover a property that overrides an inherited dataclass field
why: An Attributes entry naming a method, staticmethod, classmethod, or nested class had its directive deleted once autodoc rendered that member, and the description reattached nowhere -- the fallback reached only the attribute and property documenters. The author's sentence vanished with no warning. what: - Register a method documenter carrying the description fallback - Give the wrapped class documenters the same fallback, so a nested class keeps the entry describing it - Cover a method, a staticmethod, and a nested class
why: A directive written from docstring text knows only what its author typed, so describing a member in an Attributes section cost the reader the annotation and value autodoc renders from the live class. An enum member showed READONLY instead of READONLY = 'readonly', and a plain constant lost its value entirely -- documenting a member made its entry worse than leaving it undocumented. what: - Have autodoc compute :type: and :value: for each surviving field directive, through the registered attribute documenter and the real class, and fold them into the emitted block - Drop the authored type from the body once autodoc supplies a real one - Leave a type alias alone; autodoc documents one as a class and prints "alias of X" rather than a value - Cover the value curation, :meta hide-value:, and absent-member paths
why: The assertions guarding this behavior spell out an expected signature per shape, so they restate what autodoc does rather than check against it. A change in how autodoc renders a shape would need the table edited to match, which is the kind of test that drifts in lockstep with the code it guards. what: - Build one set of class bodies twice, described and undescribed, and require every member's rendered signature to match between them - Cover the enum, constant, generic alias, dataclass, slot, named tuple, typed dictionary, and class variable shapes in one table
why: BadgeSpec and ApiFactRow documented their fields under Parameters, which describes the initializer rather than the attributes -- every field rendered with an empty description beneath it. Mode's members carried none at all. what: - Move the BadgeSpec and ApiFactRow entries to Attributes sections - Describe Mode's DEV and PROD members
why: The previous rule required an Attributes section on NamedTuple and dataclass classes. Both its scope and its stated consequence have since proven wrong. This is a rule we expect to port to other repositories, so the reasoning belongs here rather than inside the commit that enforces it. Scope: autodoc renders one entry per class-level name, whatever shape declares it. TypedDict keys, Enum members, ClassVars, InitVars, plain constants, and nested classes all reach the reference the same way. That list also kept growing while the rule was being applied -- InitVar and nested classes were both found after the fact -- so enumerating shapes dates faster than stating the principle. Consequence: the old rule said an undescribed field ships as "Alias for field number 0" or as a bare entry. An undescribed ClassVar is now withheld from the reference entirely, so it goes missing rather than rendering empty. That is a failure a reader cannot see and a reviewer will not spot in the diff. Styles: the workspace documents fields three ways and autodoc honors all three -- an Attributes entry, a docstring under the assignment, and a #: comment. A rule naming only Attributes marks correctly documented code as broken; two classes here use the docstring form. Trap: a Parameters section documents the initializer, not the attributes. BadgeSpec and ApiFactRow each described every field under Parameters and still rendered every attribute with an empty body. what: - Restate the rule around every class-level name autodoc renders - Name the three accepted styles, preferring Attributes - Warn that Parameters does not describe attributes - Correct what happens to an undescribed class variable
why: Nothing enforced the field-documentation rule. tests/docs held policy tests for hand-authored pages only, so the docstrings the API reference is generated from went unchecked -- and three classes had fields nothing described. what: - Walk every public NamedTuple, dataclass, TypedDict, and Enum under packages/ and fail on a field no supported style describes - Accept an Attributes entry, a docstring under the assignment, or a #: comment, since the workspace uses more than one - Report the offending class and field names in the failure message
why: autodoc-skip-member stops at the first listener returning a decision, and this extension connected at the default priority, so it answered before a project's own conf.py handler. A project could not re-include a class variable the extension withholds -- returning False was silently ignored, which the module docstring already promised would not happen. what: - Answer after the default priority, leaving a project's explicit decision authoritative - Cover a handler returning False for a member the extension withholds
why: Autodoc materializes a class's __annotations__ so a # type: comment can join it. A TypedDict computes its annotations lazily by merging every base's, and materializing one base's mapping severs that merge -- so documenting a base first made every key its subclasses inherit vanish from their own pages. The loss hits any project documenting both, with or without an Attributes section. what: - Leave a typed dictionary's lazy annotations alone; it declares keys by annotation and has no type comments to merge - Cover a subclass whose base is documented first
why: A module docstring describing its own constant rendered the prose and the authored type, never the value. Autodoc reaches for its data documenter only for a name the source annotates or comments, so a plain constant was claimed by no documenter at all and the docstring-derived directive was the only thing rendering it -- as a py:attribute, which is what a class member is, not a module constant. what: - Route a described module constant to the data documenter, so autodoc owns the directive and supplies the value from the live module - Carry the module docstring's prose into it through the existing description fallback - Record the constant as py:data in the inventory - Cover the value, the objtype, and the description
why: An integer-backed enum, a flag enum built with auto(), and an attribute annotated only by a type comment were each verified by hand and then left unheld -- the proof lived in a scratch build that no longer exists. The parity comparison also had a blind spot: it compares two mappings, so a shape that stopped rendering on both sides would drop out of both and still compare equal. what: - Add the three shapes to the differential parity table, where each is measured against its undescribed twin rather than a literal - Pin the table's own coverage, so a shape leaving the roster fails instead of passing empty
why: Routing a described module constant to the data documenter gave it the value it had been missing, but cost it the type. Autodoc renders an annotation only where the source writes one, so for a plain constant the Attributes entry is the only statement of its type anywhere -- and the entry's type was being dropped on the assumption the member would render its own. The reader ended up with prose and a value but no type. what: - Carry the type an entry declares alongside its prose - Reattach it only where the owner annotates nothing itself, so an annotated member still states its type once - Cover an unannotated module constant described with a type
why: A dataclass inherits its bases' fields and a typed dictionary inherits its bases' keys, but the description stays behind on the base. The subclass entry therefore rendered bare while the prose sat one class away -- 134 options on a real consumer, plus 11 typed-dictionary keys. Asking an author to restate every inherited field on every subclass is not a reasonable alternative. what: - Consult the bases when the class being rendered describes nothing, accepting a NumPy Attributes entry or a description at the assignment - Search __mro__ and __orig_bases__ together; a typed dictionary keeps no base in its MRO, so its declaring class is reachable only through __orig_bases__ - Cover both shapes
why: private-members is on by default, so abc, typing.Protocol, and NamedTuple each put names on the page that no author declared and no docstring can reach. A consumer documenting 45 pydantic models ships 45 _abc_impl entries, each rendering as a name and nothing else. what: - Add skip_machinery_members, hiding _abc_impl, _is_protocol, _is_runtime_protocol, _fields, and _field_defaults - Connect it late, so a project wanting one of them keeps it by answering False from its own handler - Leave _replace and _asdict alone; they document themselves - Drop the one machinery entry from the objects.inv baseline, which was never a target anyone could cross-reference
why: Three cross-references in the setup table and its paragraph resolved to nothing. typing.NamedTuple is a class, so the function role could never match and appended a spurious call suffix; pydantic has no intersphinx inventory here; and MACHINERY_MEMBERS was cited before anything documented it. nitpicky is unset, so all three rendered as plain text and the build stayed silent. what: - Document skip_machinery_members and MACHINERY_MEMBERS in the API reference, giving the data role a destination - Use the class role for typing.NamedTuple, and name abc.ABCMeta, which is what actually writes _abc_impl - Link pydantic as an external project, per the docs voice guide
why: The extension decides what every field, key, enum member, and class variable shows on a reference page, and none of that was written down. The consequence a reader most needs to know -- an undescribed class variable is withheld rather than rendered bare -- was reachable only by noticing a member had gone missing. what: - Add a class-level names section covering the three places a description can live and what each shape renders - State that a base class's description carries to its subclasses - Name the withholding trade-off and the config value that opts back in
why: The cross-reference guidance said a docs build catches a broken
reference and nothing else does. That holds for a {ref}, which warns
on an unresolved label, but not for a py-domain role: nitpicky is
unset, so an unresolved {py:class} or {py:data} renders as plain text
in silence. Following the rule as written and trusting a clean build
shipped three dead links on this branch.
what:
- Scope the build-catches-it claim to {ref}
- Name the two ways a py-domain role fails silently: a symbol nothing
autodocs, and a project missing from intersphinx_mapping
- Add a checklist line to confirm each new role rendered as a link
why: The branch changes what a reference page shows for every field, key, enum member, and class variable, against behavior users of the published release already have. what: - Record the withheld undescribed class variable as a breaking change, with the description styles and config value that restore it - Describe inherited descriptions, module constant values, and the machinery-member filter - Limit Fixes to the three regressions a published release exhibited, leaving out repairs to behavior this branch itself introduced - Note the how-to and the packages whose reference fields gained descriptions
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.
A class member described in a NumPy
Attributessection was rendered from docstring text alone. That text knows only what its author typed, so the entry arrived without the annotation and value autodoc reads off the live class — and for several kinds of member it arrived with no description at all, because the author's sentence had been deleted on the way.Both halves are closed here. Describing a member now costs the reader nothing: the entry keeps the author's prose and the signature autodoc would have rendered.
Before:
After:
Descriptions that were being deleted
A member with no docstring of its own reaches the page as a signature with nothing beneath it. Autodoc has no description to attach, so the
Attributesentry describing it was discarded in favour of an empty entry — silently, with no warning.FieldDocFallbackMixinsupplies that description fromget_doc(), which is the same seam autodoc already uses to reattach a#:source comment. It reaches class variables, init-only dataclass fields, properties, methods, and nested classes; anAttributessection is for attributes, so naming a method there is a docstring its author should rework, but rendering the entry keeps that decision theirs.Three narrower cases fall out of the same path. A
:no-index:on the owning class was written into each emitted directive and rode along into the reattached description, rendering as aNo-indexfield above the author's sentence. A property overriding an inherited dataclass field has no docstring, so autodoc walked the MRO and found the base field's default — astr, whose docstring isstr.__doc__; being non-empty it counted as a description and displaced the author's. And a class variable nothing describes is now withheld rather than shipped as a bare annotation, withgp_typehints_show_undocumented_class_varsto restore it.Signatures that were being dropped
Every field directive that survives now has autodoc compute its own
:type:and:value:, through the registered attribute documenter and the real class, folded into the emitted block. The authored type leaves the body once autodoc supplies a real annotation.Asking the registered documenter rather than recomputing the answer is what makes this small. Slot-held fields,
_tuplegetterdescriptors,TypedDictkeys, defaultless dataclass fields, mocks,:meta hide-value:,autodoc_typehints = "none", and this package's own long-value curation all behave correctly because autodoc decides each one, not a table maintained here that would drift against it.A type alias is the exception and is left alone: autodoc documents a
TypeVarorNewTypeas a class and prints alias of X, so its raw repr is not something autodoc would ever render.Verifying
The invariant is stated as a differential build: the same class bodies are documented twice, once with
Attributessections and once without, and every member's rendered signature must match between them. The control build defines what each member is entitled to, so the check cannot drift away from autodoc's own behavior.It covers
enum.Enumandenum.StrEnummembers, annotated and unannotated class constants, dataclass fields with and without defaults, aslots=Truefield,NamedTuplefields,TypedDictrequired andNotRequiredkeys, and both a plain and a#:-commented class variable.Beyond the suite, consumer sites were rebuilt with this branch shadowing the installed package. tmuxp's
Colorsconstants regain their values with their prose and type intact, at an unchanged warning count. An operations base class with ten class variables keeps every description. A pydantic-based site keeps pydantic's own text formodel_config, which is described upstream and so is not an undescribed class variable.Test plan
just ruff-format,just ruff,uv run mypy src tests packagesjust test— full suite greenjust build-docs— clean, no new warnings, no duplicate object descriptionsColorsconstants regain= 'green', warning count unchangedobjects.invunchanged