1+ # TODO: Support `typing.ReadOnly`.
2+ # TODO: Support `extra_items=type`.
3+ # TODO: Support `closed=True/False`.
4+
15from __future__ import annotations
26
3- from typing import TYPE_CHECKING , Any
7+ import ast
8+ from typing import TYPE_CHECKING , Any , TypedDict
49
5- from griffe ._internal .docstrings .models import DocstringParameter , DocstringSectionParameters
10+ from griffe ._internal .docstrings .models import (
11+ DocstringParameter ,
12+ DocstringSectionOtherParameters ,
13+ DocstringSectionParameters ,
14+ )
615from griffe ._internal .enumerations import DocstringSectionKind , ParameterKind
716from griffe ._internal .expressions import Expr , ExprSubscript
817from griffe ._internal .extensions .base import Extension
1221 from collections .abc import Iterable
1322
1423
15- def _update_docstring (func : Function , parameters : Iterable [Parameter ], kwparam : Parameter | None = None ) -> None :
24+ class _TypedDictAttr (TypedDict ):
25+ name : str
26+ annotation : str | Expr | None
27+ docstring : Docstring | None
28+ required : bool
29+
30+
31+ def _get_or_set_attrs (cls : Class ) -> list [_TypedDictAttr ]:
32+ if (attrs := cls .extra .get ("unpack_typeddict" , {}).get ("_attributes" )) is not None :
33+ return attrs
34+
35+ attrs = []
36+ default_required = True
37+ for arg , value in cls .keywords .items ():
38+ if arg == "total" :
39+ try :
40+ total = ast .literal_eval (str (value ))
41+ except (ValueError , SyntaxError ):
42+ break
43+ if total is True :
44+ default_required = True
45+ elif total is False :
46+ default_required = False
47+ break
48+ for attr in cls .attributes .values ():
49+ annotation = attr .annotation
50+ required = default_required
51+ if isinstance (annotation , ExprSubscript ):
52+ if annotation .canonical_path in {
53+ "typing.Required" ,
54+ "typing_extensions.Required" ,
55+ }:
56+ annotation = annotation .slice .elements [0 ] # type: ignore[union-attr]
57+ required = True
58+ elif annotation .canonical_path in {
59+ "typing.NotRequired" ,
60+ "typing_extensions.NotRequired" ,
61+ }:
62+ annotation = annotation .slice .elements [0 ] # type: ignore[union-attr]
63+ required = False
64+ attrs .append (
65+ _TypedDictAttr (
66+ name = attr .name ,
67+ annotation = annotation ,
68+ docstring = attr .docstring ,
69+ required = required ,
70+ ),
71+ )
72+
73+ cls .extra ["unpack_typeddict" ]["_attributes" ] = attrs
74+ return attrs
75+
76+
77+ def _update_docstring (func : Function , attributes : Iterable [_TypedDictAttr ], kwparam : Parameter | None = None ) -> None :
1678 if not func .docstring :
1779 func .docstring = Docstring ("" , parent = func )
80+
81+ required = []
82+ optional = []
83+ for attribute in attributes :
84+ if attribute ["required" ]:
85+ required .append (attribute )
86+ else :
87+ optional .append (attribute )
88+
89+ params_section = None
90+ other_params_section = None
1891 sections = func .docstring .parsed
92+
93+ # Find existing "Parameters" section.
1994 section_gen = (section for section in sections if section .kind is DocstringSectionKind .parameters )
20- if kwparam and (params_section := next (section_gen , None )):
21- # Remove the `**kwargs` entry.
95+ params_section = next (section_gen , None )
96+
97+ # Pop original variadic keyword parameter from section.
98+ varkw_param = None
99+ if kwparam and params_section is not None :
22100 param_gen = (i for i , arg in enumerate (params_section .value ) if arg .name .lstrip ("*" ) == kwparam .name )
23101 if (kwarg_pos := next (param_gen , None )) is not None :
24- params_section .value .pop (kwarg_pos )
25- else :
26- # Create a parameters section if none exists.
27- params_section = DocstringSectionParameters ([])
28- func .docstring .parsed .append (params_section )
29- # Add entries for all parameters.
30- for param in parameters :
31- if param .name != "self" :
102+ varkw_param = params_section .value .pop (kwarg_pos )
103+
104+ # If we have required parameters, add them to the "Parameters" section.
105+ if required :
106+ # Create a "Parameters" section if none exists.
107+ if params_section is None :
108+ params_section = DocstringSectionParameters ([])
109+ func .docstring .parsed .append (params_section )
110+
111+ # Add required parameters to the section.
112+ for attr in required :
32113 params_section .value .append (
33114 DocstringParameter (
34- name = param .name ,
35- description = param .docstring .value if param .docstring else "" ,
36- annotation = param .annotation ,
37- value = param .default ,
115+ name = attr ["name" ],
116+ description = attr ["docstring" ].value if attr ["docstring" ] else "" ,
117+ annotation = attr ["annotation" ],
38118 ),
39119 )
40120
121+ # Add back the original variadic keyword parameter if it was present,
122+ # and if some parameters are optional.
123+ if optional and varkw_param is not None :
124+ params_section .value .append (
125+ DocstringParameter (
126+ name = varkw_param .name ,
127+ description = varkw_param .description ,
128+ annotation = varkw_param .annotation ,
129+ ),
130+ )
41131
42- def _params_from_attrs (attrs : Iterable [Any ]) -> Parameters :
43- return Parameters (
44- Parameter (name = "self" , kind = ParameterKind .positional_or_keyword ),
45- * (
46- Parameter (
47- name = attr .name ,
48- annotation = attr .annotation ,
49- kind = ParameterKind .keyword_only ,
50- default = attr .value ,
51- docstring = attr .docstring ,
132+ # If we have optional parameters, add them to the "Other parameters" section.
133+ if optional :
134+ # Create an "Other parameters" section if none exists.
135+ section_gen = (section for section in sections if section .kind is DocstringSectionKind .other_parameters )
136+ if (other_params_section := next (section_gen , None )) is None :
137+ other_params_section = DocstringSectionOtherParameters ([])
138+ func .docstring .parsed .append (other_params_section )
139+
140+ # Add optional parameters to the section.
141+ for attr in optional :
142+ other_params_section .value .append (
143+ DocstringParameter (
144+ name = attr ["name" ],
145+ description = attr ["docstring" ].value if attr ["docstring" ] else "" ,
146+ annotation = attr ["annotation" ],
147+ ),
52148 )
53- for attr in attrs
54- ),
55- )
149+
150+
151+ def _params_from_attrs (attrs : Iterable [_TypedDictAttr ]) -> Parameters :
152+ parameters = Parameters (Parameter (name = "self" , kind = ParameterKind .positional_or_keyword ))
153+ found_optional = False
154+ for attr in attrs :
155+ if attr ["required" ]:
156+ parameters .add (
157+ Parameter (
158+ name = attr ["name" ],
159+ annotation = attr ["annotation" ],
160+ kind = ParameterKind .keyword_only ,
161+ docstring = attr ["docstring" ],
162+ ),
163+ )
164+ else :
165+ found_optional = True
166+ if found_optional :
167+ name = "_kwargs" if "kwargs" in parameters else "kwargs"
168+ parameters .add (Parameter (name = name , kind = ParameterKind .var_keyword ))
169+ return parameters
56170
57171
58172class UnpackTypedDictExtension (Extension ):
@@ -67,19 +181,19 @@ def on_class(self, *, cls: Class, **kwargs: Any) -> None: # noqa: ARG002
67181 else :
68182 return
69183
70- attributes = cls . attributes . values ( )
184+ attributes = _get_or_set_attrs ( cls )
71185
72186 if "__init__" not in cls .members :
73187 # Build the `__init__` method and add it to the class.
74188 parameters = _params_from_attrs (attributes )
75189 init = Function (name = "__init__" , parameters = parameters , returns = "None" )
76190 cls .set_member ("__init__" , init )
77191 # Update the `__init__` docstring.
78- _update_docstring (init , parameters )
192+ _update_docstring (init , attributes )
79193
80194 # Remove attributes from the class, as they are now in the `__init__` method.
81195 for attr in attributes :
82- cls .del_member (attr . name )
196+ cls .del_member (attr [ " name" ] )
83197
84198 def on_function (self , * , func : Function , ** kwargs : Any ) -> None : # noqa: ARG002
85199 """Expand `**kwargs: Unpack[TypedDict]` in function signatures."""
@@ -102,17 +216,19 @@ def on_function(self, *, func: Function, **kwargs: Any) -> None: # noqa: ARG002
102216 else :
103217 return
104218
219+ attributes = _get_or_set_attrs (typed_dict )
220+
105221 if "__init__" in typed_dict .members :
106222 # The `__init__` was already generated: use its parameters.
107223 parameters = typed_dict ["__init__" ].parameters
108224 else :
109225 # Fallback to building parameters from attributes.
110- parameters = _params_from_attrs (typed_dict . attributes . values () )
226+ parameters = _params_from_attrs (attributes )
111227
112228 # Update any parameter section in the docstring.
113229 # We do this before updating the signature so that
114230 # parsing the docstring doesn't emit warnings.
115- _update_docstring (func , parameters , parameter )
231+ _update_docstring (func , attributes , parameter )
116232
117233 # Update the function parameters.
118234 del func .parameters [parameter .name ]
0 commit comments