Repository navigation
Python: resolve forward references nested in generic aliases when building schemas #14310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
943e551
461de6e
4e850b1
aa1a276
17c1b46
4a97a68
5046ebf
4d5bad0
85cf895
5d6fa56
9d5bf0d
4a3bebd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| import sys | ||
| import types | ||
| from enum import Enum | ||
| from typing import Any, Union, get_args, get_origin, get_type_hints | ||
| from typing import Annotated, Any, ForwardRef, Literal, Union, get_args, get_origin, get_type_hints | ||
|
|
||
| from semantic_kernel.const import PARSED_ANNOTATION_UNION_DELIMITER | ||
| from semantic_kernel.exceptions.function_exceptions import FunctionInvalidParameterConfiguration | ||
|
|
@@ -86,6 +86,7 @@ def build_model_schema( | |
| hints = get_type_hints(model, globalns=model_module_globals, localns={}) | ||
|
|
||
| for field_name, field_type in hints.items(): | ||
| field_type = cls.resolve_forward_refs(field_type, model_module_globals) | ||
| field_description = None | ||
| if hasattr(model, "model_fields") and field_name in model.model_fields: | ||
| field_info = model.model_fields[field_name] | ||
|
|
@@ -109,6 +110,66 @@ def build_model_schema( | |
|
|
||
| return schema | ||
|
|
||
| @classmethod | ||
| def resolve_forward_refs(cls, annotation: Any, globalns: dict[str, Any]) -> Any: | ||
| """Resolves forward references sitting inside a generic alias. | ||
|
|
||
| On Python 3.10, ``get_type_hints`` evaluates an annotation that *is* a string and | ||
| ``ForwardRef`` objects nested inside a generic alias, but not a bare ``str`` sitting in a | ||
| PEP 585 alias's ``__args__``. ``list["Inner"]`` goes through ``list.__class_getitem__``, | ||
| which stores ``"Inner"`` verbatim with no ``ForwardRef`` wrapper, so nothing resolves it and | ||
| the element type falls through to the ``{"type": "object"}`` placeholder. | ||
| ``typing.Optional["Inner"]`` does wrap the string, which is why the top-level and | ||
| ``Optional`` forms already work. | ||
|
|
||
| Python 3.11 made ``get_type_hints`` wrap those strings itself (gh-85542), so on 3.11+ the | ||
| hints arrive fully resolved and this is a no-op that returns the annotation unchanged. | ||
|
|
||
| Args: | ||
| annotation: The annotation to resolve. | ||
| globalns: The namespace of the module owning the annotation. | ||
|
|
||
| Returns: | ||
| Any: The annotation, rebuilt with resolvable string arguments replaced by their types. | ||
| """ | ||
| origin = get_origin(annotation) | ||
| args = get_args(annotation) | ||
| if origin is None or not args: | ||
| return annotation | ||
| if origin is Literal: | ||
| # Literal members are values, not names; a Literal["Inner"] must stay a string. | ||
| return annotation | ||
| if origin is Annotated: | ||
| # Only the annotated type is a type; the metadata that follows it is left alone. | ||
| return Annotated[(cls._resolve_forward_ref_arg(args[0], globalns), *args[1:])] | ||
|
|
||
| resolved_args = tuple(cls._resolve_forward_ref_arg(arg, globalns) for arg in args) | ||
| if resolved_args == args: | ||
| return annotation | ||
|
|
||
| try: | ||
| return origin[resolved_args] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PEP 604 unions cannot be rebuilt with
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 4a3bebd. PEP 604 unions are now rebuilt with |
||
| except TypeError: | ||
| # Not every alias can be rebuilt from its origin and args; keep the original. | ||
| return annotation | ||
|
Om-singhaI marked this conversation as resolved.
Resolved
|
||
|
|
||
| @classmethod | ||
| def _resolve_forward_ref_arg(cls, arg: Any, globalns: dict[str, Any]) -> Any: | ||
| """Resolves a single generic argument, recursing into nested generic aliases. | ||
|
|
||
| Args: | ||
| arg: The generic argument to resolve. | ||
| globalns: The namespace to resolve the reference against. | ||
|
|
||
| Returns: | ||
| Any: The resolved type, or the argument unchanged when it cannot be resolved. | ||
| """ | ||
| if isinstance(arg, str): | ||
| return globalns.get(arg, arg) | ||
|
Om-singhaI marked this conversation as resolved.
Resolved
|
||
| if isinstance(arg, ForwardRef): | ||
| return globalns.get(arg.__forward_arg__, arg) | ||
| return cls.resolve_forward_refs(arg, globalns) | ||
|
|
||
| @classmethod | ||
| def _is_optional(cls, field_type: Any) -> bool: | ||
| return get_origin(field_type) in {types.UnionType, Union} and type(None) in get_args(field_type) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.