@@ -109,11 +109,46 @@ def get_openapi_template(ruleset_description: Union[str, None], ruleset_version:
109109 return spec
110110
111111
112+ def _extract_optional_inner (input : str ) -> str :
113+ """Convert Optional[T] / typing.Optional[T] into T."""
114+ s = input .strip ()
115+
116+ # Optional[str] / typing.Optional[str].
117+ m = re .match (r'^(?:typing\.)?Optional\[(.+)\]$' , s )
118+ if m :
119+ return m .group (1 ).strip ()
120+
121+ return input
122+
123+
124+ def is_optional_annotation (input : str | None ) -> bool :
125+ if not input :
126+ return False
127+
128+ s = input .strip ()
129+
130+ # Optional[T] / typing.Optional[T]
131+ if re .match (r'^(?:typing\.)?Optional\[(.+)\]$' , s ):
132+ return True
133+
134+ # T | None or None | T (PEP604 union)
135+ if re .search (r'^(\w+)\s*\|\s*[Nn]one$' , s ):
136+ return True
137+ if re .search (r'^[Nn]one\s*\|\s*(\w+)$' , s ):
138+ return True
139+
140+ return False
141+
142+
112143def get_json_type (input : str ) -> str :
113144 """Translate Python type to JSON type if a translation is available, otherwise use the Python type."""
114- match_nullable_type = re .search (r'^(\w+)\s*\|\s*[Nn]one$' , input )
145+ # Handle Optional[T] / typing.Optional[T] first.
146+ inner = _extract_optional_inner (input )
147+
148+ # Handle the union form: T | None.
149+ match_nullable_type = re .search (r'^(\w+)\s*\|\s*[Nn]one$' , inner )
115150 if match_nullable_type :
116- input = match_nullable_type [1 ]
151+ inner = match_nullable_type [1 ]
117152
118153 types_lookup_table = {
119154 'str' : 'string' ,
@@ -122,13 +157,23 @@ def get_json_type(input: str) -> str:
122157 'dict' : 'object' ,
123158 'Dict' : 'object' ,
124159 'list' : 'array' ,
125- 'List' : 'array' }
126- return types_lookup_table .get (input , input )
160+ 'List' : 'array' ,
161+ }
162+ return types_lookup_table .get (inner , inner )
127163
128164
129165def is_nullable_type (input : str ) -> bool :
130- return bool (re .search (r'^(\w+)\s*\|\s*[Nn]one$' , input )
131- or re .search (r'^[Nn]one\s*\|\s*(\w+)$' , input ))
166+ s = input .strip ()
167+
168+ # Optional[str] / typing.Optional[str]
169+ if re .match (r'^(?:typing\.)?Optional\[(.+)\]$' , s ):
170+ return True
171+
172+ # T | None
173+ return bool (
174+ re .search (r'^(\w+)\s*\|\s*[Nn]one$' , s )
175+ or re .search (r'^[Nn]one\s*\|\s*(\w+)$' , s )
176+ )
132177
133178
134179def gen_fn_spec (function_name : str , function_properties : Dict ):
@@ -300,21 +345,33 @@ def _get_argument_data(node):
300345
301346 default_value = None
302347 required = True
348+
349+ # Determine required from whether a default is present in the signature.
303350 if i >= len (node .args .args ) - len (node .args .defaults ):
304351 default_index = i - (len (node .args .args ) - len (node .args .defaults ))
305352 default_value = ast .unparse (node .args .defaults [default_index ]) if hasattr (ast , 'unparse' ) else None
306353 required = False
307354
308- argdata [arg_name ] = {"annotation" : annotation , "default_value" : default_value , "required" : required }
355+ # Optional[...] / T | None implies not required.
356+ if is_optional_annotation (annotation ):
357+ required = False
358+
359+ argdata [arg_name ] = {
360+ "annotation" : annotation ,
361+ "default_value" : default_value ,
362+ "required" : required
363+ }
309364
310365 return argdata
311366
312367 function_properties : dict [str , Any ] = {}
313368 function_properties ["doc" ] = ast .get_docstring (node )
314369 function_properties ["args" ] = _get_argument_data (node )
315370 function_properties ["tag" ] = os .path .basename (source_file )[:- 3 ]
316- function_properties ["decorators" ] = [ast .unparse (decorator ) if hasattr (ast , 'unparse' ) else None
317- for decorator in node .decorator_list ]
371+ function_properties ["decorators" ] = [
372+ ast .unparse (decorator ) if hasattr (ast , 'unparse' ) else None
373+ for decorator in node .decorator_list
374+ ]
318375 if "api.make()" in function_properties ["decorators" ]:
319376 result [function_name ] = function_properties
320377 return result
0 commit comments