@@ -114,6 +114,17 @@ def on_class_members(
114114
115115 # ------------------------------
116116
117+ def on_class (self , * , cls : Class , ** kwargs : Any ) -> None :
118+ """Fill in missing descriptions for inherited fields.
119+
120+ This runs after the full object tree is built, so griffe's
121+ inheritance APIs (mro, attributes, inherited_members) work here.
122+ """
123+ if self .include_inherited and cls .docstring :
124+ _backfill_inherited_descriptions (cls )
125+
126+ # ------------------------------
127+
117128 def _inject_fields (self , griffe_obj : Object , runtime_obj : Any ) -> None :
118129 # update the object instance with the evaluated docstring
119130 docstring = inspect .cleandoc (getattr (runtime_obj , "__doc__" , "" ) or "" )
@@ -136,6 +147,56 @@ def _inject_fields(self, griffe_obj: Object, runtime_obj: Any) -> None:
136147 )
137148
138149
150+ def _backfill_inherited_descriptions (cls : Class ) -> None :
151+ """Fill in missing field descriptions from parent classes.
152+
153+ Uses griffe's MRO to find descriptions from parent docstring sections
154+ and inherited attribute docstrings. Must be called after the full object
155+ tree is built (e.g. from on_class), so that mro() works.
156+ """
157+ sections = cls .docstring .parsed # pyright: ignore[reportOptionalMemberAccess]
158+
159+ # Collect descriptions with empty strings that we need to fill
160+ empty_items : dict [str , DocstringParameter | DocstringAttribute ] = {}
161+ params_or_attrs = (DocstringSectionParameters , DocstringSectionAttributes )
162+ for section in sections :
163+ if isinstance (section , params_or_attrs ):
164+ for item in section .value :
165+ if not item .description :
166+ empty_items [item .name ] = item
167+
168+ if not empty_items :
169+ return
170+
171+ # Walk parent classes looking for descriptions
172+ try :
173+ parents = cls .mro ()
174+ except ValueError : # pragma: no cover
175+ return
176+
177+ for parent in parents :
178+ # Check parent's parsed docstring sections
179+ if parent .docstring :
180+ for section in parent .docstring .parsed :
181+ if isinstance (section , params_or_attrs ):
182+ for item in section .value :
183+ if item .name in empty_items and item .description :
184+ empty_items [item .name ].description = item .description
185+ del empty_items [item .name ]
186+ # Check parent's direct member inline docstrings.
187+ # This is a fallback for parents not processed by this extension
188+ # (e.g. a base class from another package). possibly unreachable
189+ for name in list (empty_items ): # pragma: no cover
190+ if name in parent .members :
191+ member = parent .members [name ]
192+ if member .docstring :
193+ empty_items [name ].description = member .docstring .value
194+ del empty_items [name ]
195+
196+ if not empty_items :
197+ return
198+
199+
139200def _to_annotation (
140201 type_ : Any , docstring : Docstring , * , strip_annotated : bool = False
141202) -> str | Expr | None :
@@ -284,7 +345,11 @@ def _merged_kwargs(
284345 strip_annotated : bool = False ,
285346) -> DocstringNamedElementKwargs :
286347 desc = field .description or field .metadata .get ("description" , "" ) or ""
287- if not desc and (doc := getattr (field .default_factory , "__doc__" , None )):
348+ if (
349+ not desc
350+ and field .default_factory is not field .MISSING
351+ and (doc := getattr (field .default_factory , "__doc__" , None ))
352+ ):
288353 desc = inspect .cleandoc (doc ) or ""
289354
290355 if not desc and field .name in griffe_obj .attributes :
0 commit comments