55import inspect
66import textwrap
77from contextlib import suppress
8- from typing import TYPE_CHECKING , Any , Iterable , Sequence
8+ from typing import TYPE_CHECKING , Any , Iterable , Literal , TypedDict , TypeVar , cast
99
1010import fieldz
1111from fieldz ._repr import display_as_type
1212from griffe import (
13+ Attribute ,
1314 Class ,
1415 Docstring ,
1516 DocstringAttribute ,
1617 DocstringParameter ,
18+ DocstringSection ,
1719 DocstringSectionAttributes ,
1820 DocstringSectionParameters ,
1921 Extension ,
2931
3032 from griffe import Expr , Inspector , Visitor
3133
32- logger = get_logger (__name__ )
34+ AddFieldsTo = Literal [
35+ "docstring-parameters" , "docstring-attributes" , "class-attributes"
36+ ]
37+
38+ logger = get_logger ("griffe-fieldz" )
3339
3440
3541class FieldzExtension (Extension ):
@@ -40,14 +46,35 @@ def __init__(
4046 object_paths : list [str ] | None = None ,
4147 include_private : bool = False ,
4248 include_inherited : bool = False ,
49+ add_fields_to : AddFieldsTo = "docstring-parameters" ,
50+ remove_fields_from_members : bool = False ,
4351 ** kwargs : Any ,
4452 ) -> None :
4553 self .object_paths = object_paths
4654 self ._kwargs = kwargs
55+ if kwargs :
56+ logger .warning (
57+ "Unknown kwargs passed to FieldzExtension: %s" , ", " .join (kwargs )
58+ )
4759 self .include_private = include_private
4860 self .include_inherited = include_inherited
4961
50- def on_class_instance (
62+ self .remove_fields_from_members = remove_fields_from_members
63+ if add_fields_to not in (
64+ "docstring-parameters" ,
65+ "docstring-attributes" ,
66+ "class-attributes" ,
67+ ): # pragma: no cover
68+ logger .error (
69+ "'add_fields_to' must be one of {'docstring-parameters', "
70+ f"'docstring-attributes', or 'class-attributes'}}, not { add_fields_to } ."
71+ "\n \n Defaulting to 'docstring-parameters'."
72+ )
73+ add_fields_to = "docstring-parameters"
74+
75+ self .add_fields_to : AddFieldsTo = add_fields_to
76+
77+ def on_class_members (
5178 self ,
5279 * ,
5380 node : ast .AST | ObjectNode ,
@@ -70,42 +97,31 @@ def on_class_instance(
7097
7198 try :
7299 fieldz .get_adapter (runtime_obj )
73- except TypeError :
100+ except TypeError : # pragma: no cover
74101 return
75102 self ._inject_fields (cls , runtime_obj )
76103
77104 # ------------------------------
78105
79- def _inject_fields (self , obj : Object , runtime_obj : Any ) -> None :
106+ def _inject_fields (self , griffe_obj : Object , runtime_obj : Any ) -> None :
80107 # update the object instance with the evaluated docstring
81108 docstring = inspect .cleandoc (getattr (runtime_obj , "__doc__" , "" ) or "" )
82- if not obj .docstring :
83- obj .docstring = Docstring (docstring , parent = obj )
84- sections = obj .docstring .parsed
109+ if not griffe_obj .docstring :
110+ griffe_obj .docstring = Docstring (docstring , parent = griffe_obj )
85111
86112 # collect field info
87113 fields = fieldz .fields (runtime_obj )
88114 if not self .include_inherited :
89115 annotations = getattr (runtime_obj , "__annotations__" , {})
90116 fields = tuple (f for f in fields if f .name in annotations )
91117
92- params , attrs = _fields_to_params (fields , obj .docstring , self .include_private )
93-
94- # merge/add field info to docstring
95- if params :
96- for x in sections :
97- if isinstance (x , DocstringSectionParameters ):
98- _merge (x , params )
99- break
100- else :
101- sections .insert (1 , DocstringSectionParameters (params ))
102- if attrs :
103- for x in sections :
104- if isinstance (x , DocstringSectionAttributes ):
105- _merge (x , params )
106- break
107- else :
108- sections .append (DocstringSectionAttributes (attrs ))
118+ _unify_fields (
119+ fields ,
120+ griffe_obj ,
121+ include_private = self .include_private ,
122+ add_fields_to = self .add_fields_to ,
123+ remove_fields_from_members = self .remove_fields_from_members ,
124+ )
109125
110126
111127def _to_annotation (type_ : Any , docstring : Docstring ) -> str | Expr | None :
@@ -119,63 +135,136 @@ def _to_annotation(type_: Any, docstring: Docstring) -> str | Expr | None:
119135
120136def _default_repr (field : fieldz .Field ) -> str | None :
121137 """Return a repr for a field default."""
122- if field .default is not field .MISSING :
123- return repr (field .default )
124- if (factory := field .default_factory ) is not field .MISSING :
125- if len (inspect .signature (factory ).parameters ) == 0 :
126- with suppress (Exception ):
127- return repr (factory ()) # type: ignore[call-arg]
128- return "<dynamic>"
138+ try :
139+ if field .default is not field .MISSING :
140+ return repr (field .default )
141+ if (factory := field .default_factory ) is not field .MISSING :
142+ try :
143+ sig = inspect .signature (factory )
144+ except ValueError :
145+ return repr (factory )
146+ else :
147+ if len (sig .parameters ) == 0 :
148+ with suppress (Exception ):
149+ return repr (factory ()) # type: ignore[call-arg]
150+
151+ return "<dynamic>"
152+ except Exception as exc : # pragma: no cover
153+ logger .warning ("Failed to get default repr for %s: %s" , field .name , exc )
154+ pass
129155 return None
130156
131157
132- def _fields_to_params (
158+ class DocstringNamedElementKwargs (TypedDict ):
159+ """Docstring named element kwargs."""
160+
161+ name : str
162+ description : str
163+ annotation : str | Expr | None
164+ value : str | None
165+
166+
167+ def _unify_fields (
133168 fields : Iterable [fieldz .Field ],
134- docstring : Docstring ,
135- include_private : bool = False ,
136- ) -> tuple [list [DocstringParameter ], list [DocstringAttribute ]]:
137- """Get all docstring attributes and parameters for fields."""
138- params : list [DocstringParameter ] = []
139- attrs : list [DocstringAttribute ] = []
169+ griffe_obj : Object ,
170+ include_private : bool ,
171+ add_fields_to : AddFieldsTo ,
172+ remove_fields_from_members : bool ,
173+ ) -> None :
174+ docstring = cast ("Docstring" , griffe_obj .docstring )
175+ sections = docstring .parsed
176+
140177 for field in fields :
178+ if not include_private and field .name .startswith ("_" ):
179+ continue
180+
141181 try :
142- desc = field .description or field .metadata .get ("description" , "" ) or ""
143- if not desc and (doc := getattr (field .default_factory , "__doc__" , None )):
144- desc = inspect .cleandoc (doc ) or ""
145-
146- kwargs : dict = {
147- "name" : field .name ,
148- "annotation" : _to_annotation (field .type , docstring ),
149- "description" : textwrap .dedent (desc ).strip (),
150- "value" : _default_repr (field ),
151- }
152- if field .init :
153- params .append (DocstringParameter (** kwargs ))
154- elif include_private or not field .name .startswith ("_" ):
155- attrs .append (DocstringAttribute (** kwargs ))
182+ item_kwargs = _merged_kwargs (field , docstring , griffe_obj )
183+
184+ if add_fields_to == "class-attributes" :
185+ if field .name not in griffe_obj .attributes :
186+ griffe_obj .members [field .name ] = Attribute (
187+ name = item_kwargs ["name" ],
188+ value = item_kwargs ["value" ],
189+ annotation = item_kwargs ["annotation" ],
190+ docstring = item_kwargs ["description" ],
191+ )
192+ elif add_fields_to == "docstring-attributes" or (not field .init ):
193+ _add_if_missing (sections , DocstringAttribute (** item_kwargs ))
194+ # remove from parameters if it exists
195+ if p_sect := _get_section (sections , DocstringSectionParameters ):
196+ p_sect .value = [x for x in p_sect .value if x .name != field .name ]
197+ if remove_fields_from_members :
198+ # remove from griffe_obj.parameters
199+ griffe_obj .members .pop (field .name , None )
200+ griffe_obj .inherited_members .pop (field .name , None )
201+ elif add_fields_to == "docstring-parameters" :
202+ _add_if_missing (sections , DocstringParameter (** item_kwargs ))
203+ # remove from attributes if it exists
204+ if a_sect := _get_section (sections , DocstringSectionAttributes ):
205+ a_sect .value = [x for x in a_sect .value if x .name != field .name ]
206+ if remove_fields_from_members :
207+ # remove from griffe_obj.attributes
208+ griffe_obj .members .pop (field .name , None )
209+ griffe_obj .inherited_members .pop (field .name , None )
210+
156211 except Exception as exc :
157212 logger .warning ("Failed to parse field %s: %s" , field .name , exc )
158213
159- return params , attrs
214+
215+ def _merged_kwargs (
216+ field : fieldz .Field , docstring : Docstring , griffe_obj : Object
217+ ) -> DocstringNamedElementKwargs :
218+ desc = field .description or field .metadata .get ("description" , "" ) or ""
219+ if not desc and (doc := getattr (field .default_factory , "__doc__" , None )):
220+ desc = inspect .cleandoc (doc ) or ""
221+
222+ if not desc and field .name in griffe_obj .attributes :
223+ griffe_attr = griffe_obj .attributes [field .name ]
224+ if griffe_attr .docstring :
225+ desc = griffe_attr .docstring .value
226+
227+ return DocstringNamedElementKwargs (
228+ name = field .name ,
229+ description = textwrap .dedent (desc ).strip (),
230+ annotation = _to_annotation (field .type , docstring ),
231+ value = _default_repr (field ),
232+ )
160233
161234
162- def _merge (
163- existing_section : DocstringSectionParameters | DocstringSectionAttributes ,
164- field_params : Sequence [DocstringParameter ],
235+ T = TypeVar ("T" , bound = "DocstringSectionParameters | DocstringSectionAttributes" )
236+
237+
238+ def _get_section (sections : list [DocstringSection ], cls : type [T ]) -> T | None :
239+ for section in sections :
240+ if isinstance (section , cls ):
241+ return section
242+ return None
243+
244+
245+ def _add_if_missing (
246+ sections : list [DocstringSection ], item : DocstringParameter | DocstringAttribute
165247) -> None :
166- """Update DocstringSection with field params (if missing)."""
167- existing_members = {x .name : x for x in existing_section .value }
168-
169- for param in field_params :
170- if existing := existing_members .get (param .name ):
171- # if the field already exists ...
172- # extend missing attributes with the values from the fieldz params
173- if existing .value is None and param .value is not None :
174- existing .value = param .value
175- if existing .description is None and param .description :
176- existing .description = param .description
177- if existing .annotation is None and param .annotation is not None :
178- existing .annotation = param .annotation
179- else :
180- # otherwise, add the missing fields
181- existing_section .value .append (param ) # type: ignore
248+ section : DocstringSectionParameters | DocstringSectionAttributes | None
249+ if isinstance (item , DocstringParameter ):
250+ if not (section := _get_section (sections , DocstringSectionParameters )):
251+ section = DocstringSectionParameters ([])
252+ sections .append (section )
253+ elif isinstance (item , DocstringAttribute ):
254+ if not (section := _get_section (sections , DocstringSectionAttributes )):
255+ section = DocstringSectionAttributes ([])
256+ sections .append (section )
257+ else : # pragma: no cover
258+ raise TypeError (f"Unknown section type: { type (item )} " )
259+
260+ existing = {x .name : x for x in section .value }
261+ if item .name in existing :
262+ current = existing [item .name ]
263+ if current .description is None and item .description :
264+ current .description = item .description
265+ if current .annotation is None and item .annotation :
266+ current .annotation = item .annotation
267+ if current .value is None and item .value is not None :
268+ current .value = item .value
269+ else :
270+ section .value .append (item ) # type: ignore [arg-type]
0 commit comments