fix(schema): add Sequence type support to schema generator

Added support for collections.abc.Sequence types in the schema generator to fix OpenAPI spec generation after changing API types from list to Sequence.

Changes:
- Added is_generic_sequence() and unwrap_generic_sequence() helper functions in inspection.py
- Updated python_type_to_name() in name.py to handle Sequence types (treats them as List for schema purposes)
- Fixed force parameter propagation in recursive python_type_to_name() calls to ensure unions within generic types are handled correctly
- Updated docstring_to_schema() to call python_type_to_name() with force=True
- Regenerated OpenAPI specs with updated type handling

This enables using Sequence instead of list in API definitions while maintaining schema generation compatibility.
This commit is contained in:
Ashwin Bharambe 2025-10-28 14:54:49 -07:00
parent aba98f49db
commit 35e251090b
3 changed files with 43 additions and 7 deletions

View file

@ -430,6 +430,32 @@ def _unwrap_generic_list(typ: type[list[T]]) -> type[T]:
return list_type # type: ignore[no-any-return] return list_type # type: ignore[no-any-return]
def is_generic_sequence(typ: object) -> bool:
"True if the specified type is a generic Sequence, i.e. `Sequence[T]`."
import collections.abc
typ = unwrap_annotated_type(typ)
return typing.get_origin(typ) is collections.abc.Sequence
def unwrap_generic_sequence(typ: object) -> type:
"""
Extracts the item type of a Sequence type.
:param typ: The Sequence type `Sequence[T]`.
:returns: The item type `T`.
"""
return rewrap_annotated_type(_unwrap_generic_sequence, typ) # type: ignore[arg-type]
def _unwrap_generic_sequence(typ: object) -> type:
"Extracts the item type of a Sequence type (e.g. returns `T` for `Sequence[T]`)."
(sequence_type,) = typing.get_args(typ) # unpack single tuple element
return sequence_type # type: ignore[no-any-return]
def is_generic_set(typ: object) -> TypeGuard[type[set]]: def is_generic_set(typ: object) -> TypeGuard[type[set]]:
"True if the specified type is a generic set, i.e. `Set[T]`." "True if the specified type is a generic set, i.e. `Set[T]`."

View file

@ -18,10 +18,12 @@ from .inspection import (
TypeLike, TypeLike,
is_generic_dict, is_generic_dict,
is_generic_list, is_generic_list,
is_generic_sequence,
is_type_optional, is_type_optional,
is_type_union, is_type_union,
unwrap_generic_dict, unwrap_generic_dict,
unwrap_generic_list, unwrap_generic_list,
unwrap_generic_sequence,
unwrap_optional_type, unwrap_optional_type,
unwrap_union_types, unwrap_union_types,
) )
@ -155,24 +157,28 @@ def python_type_to_name(data_type: TypeLike, force: bool = False) -> str:
if metadata is not None: if metadata is not None:
# type is Annotated[T, ...] # type is Annotated[T, ...]
arg = typing.get_args(data_type)[0] arg = typing.get_args(data_type)[0]
return python_type_to_name(arg) return python_type_to_name(arg, force=force)
if force: if force:
# generic types # generic types
if is_type_optional(data_type, strict=True): if is_type_optional(data_type, strict=True):
inner_name = python_type_to_name(unwrap_optional_type(data_type)) inner_name = python_type_to_name(unwrap_optional_type(data_type), force=True)
return f"Optional__{inner_name}" return f"Optional__{inner_name}"
elif is_generic_list(data_type): elif is_generic_list(data_type):
item_name = python_type_to_name(unwrap_generic_list(data_type)) item_name = python_type_to_name(unwrap_generic_list(data_type), force=True)
return f"List__{item_name}"
elif is_generic_sequence(data_type):
# Treat Sequence the same as List for schema generation purposes
item_name = python_type_to_name(unwrap_generic_sequence(data_type), force=True)
return f"List__{item_name}" return f"List__{item_name}"
elif is_generic_dict(data_type): elif is_generic_dict(data_type):
key_type, value_type = unwrap_generic_dict(data_type) key_type, value_type = unwrap_generic_dict(data_type)
key_name = python_type_to_name(key_type) key_name = python_type_to_name(key_type, force=True)
value_name = python_type_to_name(value_type) value_name = python_type_to_name(value_type, force=True)
return f"Dict__{key_name}__{value_name}" return f"Dict__{key_name}__{value_name}"
elif is_type_union(data_type): elif is_type_union(data_type):
member_types = unwrap_union_types(data_type) member_types = unwrap_union_types(data_type)
member_names = "__".join(python_type_to_name(member_type) for member_type in member_types) member_names = "__".join(python_type_to_name(member_type, force=True) for member_type in member_types)
return f"Union__{member_names}" return f"Union__{member_names}"
# named system or user-defined type # named system or user-defined type

View file

@ -111,7 +111,7 @@ def get_class_property_docstrings(
def docstring_to_schema(data_type: type) -> Schema: def docstring_to_schema(data_type: type) -> Schema:
short_description, long_description = get_class_docstrings(data_type) short_description, long_description = get_class_docstrings(data_type)
schema: Schema = { schema: Schema = {
"title": python_type_to_name(data_type), "title": python_type_to_name(data_type, force=True),
} }
description = "\n".join(filter(None, [short_description, long_description])) description = "\n".join(filter(None, [short_description, long_description]))
@ -417,6 +417,10 @@ class JsonSchemaGenerator:
if origin_type is list: if origin_type is list:
(list_type,) = typing.get_args(typ) # unpack single tuple element (list_type,) = typing.get_args(typ) # unpack single tuple element
return {"type": "array", "items": self.type_to_schema(list_type)} return {"type": "array", "items": self.type_to_schema(list_type)}
elif origin_type is collections.abc.Sequence:
# Treat Sequence the same as list for JSON schema (both are arrays)
(sequence_type,) = typing.get_args(typ) # unpack single tuple element
return {"type": "array", "items": self.type_to_schema(sequence_type)}
elif origin_type is dict: elif origin_type is dict:
key_type, value_type = typing.get_args(typ) key_type, value_type = typing.get_args(typ)
if not (key_type is str or key_type is int or is_type_enum(key_type)): if not (key_type is str or key_type is int or is_type_enum(key_type)):