Skip to content

Commit 87d74a5

Browse files
committed
API docs: support Optional type annotations
1 parent a8efbe6 commit 87d74a5

1 file changed

Lines changed: 66 additions & 9 deletions

File tree

tools/api/generate-openapi.py

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
112143
def 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

129165
def 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

134179
def 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

Comments
 (0)