97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
652 lines
24 KiB
Python
652 lines
24 KiB
Python
# --------------------------------------------------------------------------------
|
|
# The following code is adapted from a comment on GitHub in the pydantic/pydantic repository by silviumarcu.
|
|
# Source: https://github.com/pydantic/pydantic/issues/6381#issuecomment-1831607091
|
|
#
|
|
# This code is used in accordance with the repository's license, and this reference
|
|
# serves as an acknowledgment of the original author's contribution to this project.
|
|
# --------------------------------------------------------------------------------
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import types
|
|
import warnings
|
|
from collections.abc import AsyncGenerator, Callable, Generator, Iterable
|
|
from copy import deepcopy
|
|
from functools import cache
|
|
from functools import reduce
|
|
from operator import or_
|
|
from typing import ( # noqa: UP035
|
|
Any,
|
|
Generic,
|
|
List, # needed for runtime check against typing.List annotations from user code
|
|
NoReturn,
|
|
Optional,
|
|
TypeVar,
|
|
Union,
|
|
get_args,
|
|
get_origin,
|
|
)
|
|
|
|
from jiter import from_json
|
|
from pydantic import BaseModel, create_model
|
|
from pydantic.fields import FieldInfo
|
|
|
|
from instructor.v2.dsl.json_tracker import JsonCompleteness, is_json_complete
|
|
|
|
T_Model = TypeVar("T_Model", bound=BaseModel)
|
|
|
|
UNION_TYPE = getattr(types, "UnionType", None)
|
|
UNION_ORIGINS = (Union, UNION_TYPE) if UNION_TYPE is not None else (Union,)
|
|
|
|
# Track models currently being processed to prevent infinite recursion
|
|
# with self-referential models (e.g., TreeNode with children: List["TreeNode"])
|
|
_processing_models: set[type] = set()
|
|
|
|
|
|
class MakeFieldsOptional:
|
|
pass
|
|
|
|
|
|
class PartialLiteralMixin:
|
|
"""DEPRECATED: This mixin is no longer necessary.
|
|
|
|
With completeness-based validation, Literal and Enum types are handled
|
|
automatically during streaming:
|
|
- Incomplete JSON: no validation runs, partial values are stored as-is
|
|
- Complete JSON: full validation against original model
|
|
|
|
You can safely remove this mixin from your models.
|
|
"""
|
|
|
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
super().__init_subclass__(**kwargs)
|
|
warnings.warn(
|
|
"PartialLiteralMixin is deprecated and no longer necessary. "
|
|
"Completeness-based validation now handles Literal and Enum types "
|
|
"automatically during streaming. You can safely remove this mixin.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
|
|
def remove_control_chars(s):
|
|
return re.sub(r"[\x00-\x1F\x7F-\x9F]", "", s)
|
|
|
|
|
|
def process_potential_object(potential_object, partial_mode, partial_model, **kwargs):
|
|
"""Process a potential JSON object using completeness-based validation.
|
|
|
|
- If JSON is complete (closed braces/brackets): validate against original model
|
|
- If JSON is incomplete: build partial object using model_construct (no validation)
|
|
|
|
Note: Pydantic v2.10+ has `experimental_allow_partial` but it doesn't support
|
|
BaseModel constraints during partial validation (only TypedDict). If Pydantic
|
|
adds BaseModel support in the future, this could potentially be simplified.
|
|
See: https://docs.pydantic.dev/latest/concepts/partial_validation/
|
|
"""
|
|
json_str = potential_object.strip() or "{}"
|
|
parsed = from_json(json_str.encode(), partial_mode=partial_mode)
|
|
|
|
tracker = JsonCompleteness()
|
|
tracker.analyze(json_str)
|
|
|
|
# Get original model for validation
|
|
original_model = getattr(partial_model, "_original_model", None)
|
|
|
|
# Check if root is complete AND has actual data (not just empty {})
|
|
root_complete = tracker.is_root_complete()
|
|
has_data = bool(parsed) if isinstance(parsed, dict) else True
|
|
|
|
validation_kwargs = {
|
|
key: value
|
|
for key, value in kwargs.items()
|
|
if key
|
|
in {"context", "strict", "extra", "from_attributes", "by_alias", "by_name"}
|
|
}
|
|
|
|
if root_complete and has_data and original_model is not None:
|
|
# Root object is complete with data - validate against original model
|
|
return original_model.model_validate(parsed, **validation_kwargs)
|
|
# Object is incomplete or empty - build instance using model_construct (no validation)
|
|
model_for_construct = (
|
|
original_model if original_model is not None else partial_model
|
|
)
|
|
return _build_partial_object(parsed, model_for_construct, tracker, "", **kwargs)
|
|
|
|
|
|
def _build_partial_object(
|
|
data: Any,
|
|
model: type[BaseModel],
|
|
tracker: JsonCompleteness,
|
|
path: str,
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
"""Build a partial object using model_construct() to skip validation.
|
|
|
|
For each field:
|
|
- If the field's JSON is complete AND it's a nested BaseModel: validate it
|
|
- Otherwise: store without validation
|
|
"""
|
|
if data is None:
|
|
return None
|
|
|
|
if not isinstance(data, dict):
|
|
return data
|
|
|
|
result: dict[str, Any] = {}
|
|
|
|
for field_name in data:
|
|
field_value = data[field_name]
|
|
field_path = f"{path}.{field_name}" if path else field_name
|
|
|
|
if field_value is None:
|
|
result[field_name] = None
|
|
continue
|
|
|
|
field_complete = tracker.is_path_complete(field_path)
|
|
field_info = model.model_fields.get(field_name)
|
|
field_type = field_info.annotation if field_info else None
|
|
|
|
if field_complete and field_type is not None:
|
|
if isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
|
result[field_name] = field_type.model_validate(field_value, **kwargs)
|
|
continue
|
|
|
|
if isinstance(field_value, dict):
|
|
nested_model = None
|
|
if field_type is not None and isinstance(field_type, type):
|
|
if issubclass(field_type, BaseModel):
|
|
nested_model = field_type
|
|
|
|
if nested_model:
|
|
result[field_name] = _build_partial_object(
|
|
field_value, nested_model, tracker, field_path, **kwargs
|
|
)
|
|
else:
|
|
result[field_name] = field_value
|
|
elif isinstance(field_value, list):
|
|
result[field_name] = _build_partial_list(
|
|
field_value, model, field_name, tracker, field_path, **kwargs
|
|
)
|
|
else:
|
|
result[field_name] = field_value
|
|
|
|
# Set missing fields to None or empty nested models
|
|
for field_name, field_info in model.model_fields.items():
|
|
if field_name not in result:
|
|
field_type = field_info.annotation
|
|
if isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
|
result[field_name] = _build_partial_object(
|
|
{}, field_type, tracker, "", **kwargs
|
|
)
|
|
elif field_info.is_required():
|
|
result[field_name] = None
|
|
else:
|
|
result[field_name] = field_info.get_default(call_default_factory=True)
|
|
|
|
return model.model_construct(**result)
|
|
|
|
|
|
def _build_partial_list(
|
|
items: list,
|
|
original_model: type[BaseModel] | None,
|
|
field_name: str,
|
|
tracker: JsonCompleteness,
|
|
path: str,
|
|
**kwargs: Any,
|
|
) -> list:
|
|
"""Build a partial list, validating complete items."""
|
|
result = []
|
|
|
|
item_type = None
|
|
if original_model:
|
|
field_info = original_model.model_fields.get(field_name)
|
|
if field_info:
|
|
field_type = field_info.annotation
|
|
if get_origin(field_type) in (list, List): # noqa: UP006
|
|
args = get_args(field_type)
|
|
if args:
|
|
item_type = args[0]
|
|
|
|
for i, item in enumerate(items):
|
|
item_path = f"{path}[{i}]"
|
|
item_complete = tracker.is_path_complete(item_path)
|
|
|
|
if item_complete and item_type and isinstance(item_type, type):
|
|
if issubclass(item_type, BaseModel) and isinstance(item, dict):
|
|
result.append(item_type.model_validate(item, **kwargs))
|
|
continue
|
|
|
|
result.append(item)
|
|
|
|
return result
|
|
|
|
|
|
def _process_generic_arg(
|
|
arg: Any,
|
|
make_fields_optional: bool = False,
|
|
) -> Any:
|
|
arg_origin = get_origin(arg)
|
|
|
|
if arg_origin is not None:
|
|
# Handle any nested generic type (Union, List, Dict, etc.)
|
|
nested_args = get_args(arg)
|
|
modified_nested_args = tuple(
|
|
_process_generic_arg(
|
|
t,
|
|
make_fields_optional=make_fields_optional,
|
|
)
|
|
for t in nested_args
|
|
)
|
|
# Special handling for Union types (types.UnionType isn't subscriptable)
|
|
if arg_origin in UNION_ORIGINS:
|
|
return _subscript_type(Union, modified_nested_args)
|
|
|
|
return arg_origin[modified_nested_args]
|
|
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
|
# Prevent infinite recursion for self-referential models
|
|
if arg in _processing_models:
|
|
return arg # Already processing this model, return unwrapped
|
|
_processing_models.add(arg)
|
|
try:
|
|
return (
|
|
_make_partial_type(arg, make_fields_optional=True)
|
|
if make_fields_optional
|
|
else Partial[arg]
|
|
)
|
|
finally:
|
|
_processing_models.discard(arg)
|
|
else:
|
|
return arg
|
|
|
|
|
|
def _subscript_type(origin: Any, args: Any) -> Any:
|
|
"""Construct a runtime typing object from dynamically discovered arguments."""
|
|
return origin[args]
|
|
|
|
|
|
def _make_optional_type(annotation: Any) -> Any:
|
|
return _subscript_type(Optional, annotation)
|
|
|
|
|
|
def _make_partial_type(
|
|
annotation: type[BaseModel], *, make_fields_optional: bool = False
|
|
) -> type[BaseModel]:
|
|
key = (annotation, MakeFieldsOptional) if make_fields_optional else annotation
|
|
return Partial.__class_getitem__(key)
|
|
|
|
|
|
def _make_field_optional(
|
|
field: FieldInfo,
|
|
) -> tuple[Any, FieldInfo]:
|
|
tmp_field = deepcopy(field)
|
|
|
|
annotation = field.annotation
|
|
|
|
# Handle generics (like List, Dict, Union, Literal, etc.)
|
|
if get_origin(annotation) is not None:
|
|
# Get the generic base (like List, Dict) and its arguments (like User in List[User])
|
|
generic_base = get_origin(annotation)
|
|
generic_args = get_args(annotation)
|
|
|
|
modified_args = tuple(
|
|
_process_generic_arg(arg, make_fields_optional=True) for arg in generic_args
|
|
)
|
|
|
|
# Reconstruct the generic type with modified arguments
|
|
if generic_base is UNION_TYPE:
|
|
tmp_field.annotation = _make_optional_type(reduce(or_, modified_args))
|
|
else:
|
|
tmp_field.annotation = (
|
|
_make_optional_type(_subscript_type(generic_base, modified_args))
|
|
if generic_base
|
|
else None
|
|
)
|
|
tmp_field.default = None
|
|
tmp_field.default_factory = None
|
|
# If the field is a BaseModel, then recursively convert it's
|
|
# attributes to optionals.
|
|
elif isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
tmp_field.annotation = _make_optional_type(
|
|
_make_partial_type(annotation, make_fields_optional=True)
|
|
)
|
|
tmp_field.default = {}
|
|
tmp_field.default_factory = None
|
|
else:
|
|
tmp_field.annotation = _make_optional_type(field.annotation)
|
|
tmp_field.default = None
|
|
tmp_field.default_factory = None
|
|
|
|
return tmp_field.annotation, tmp_field
|
|
|
|
|
|
class PartialBase(Generic[T_Model]):
|
|
@classmethod
|
|
@cache
|
|
def get_partial_model(cls) -> type[T_Model]:
|
|
"""Return a partial model for holding incomplete streaming data.
|
|
|
|
With completeness-based validation, we use model_construct() to build
|
|
partial objects without validation. This method creates a model with
|
|
all fields optional and stores a reference to the original model
|
|
for validation when JSON is complete.
|
|
"""
|
|
assert issubclass(cls, BaseModel), (
|
|
f"{cls.__name__} must be a subclass of BaseModel"
|
|
)
|
|
|
|
model_name = (
|
|
cls.__name__
|
|
if cls.__name__.startswith("Partial")
|
|
else f"Partial{cls.__name__}"
|
|
)
|
|
|
|
# Create partial model with optional fields
|
|
partial_model = create_model( # ty: ignore[no-matching-overload]
|
|
model_name,
|
|
__base__=cls,
|
|
__module__=cls.__module__,
|
|
**{
|
|
field_name: _make_field_optional(field_info)
|
|
for field_name, field_info in cls.model_fields.items()
|
|
},
|
|
)
|
|
|
|
# Store reference to original model for validation of complete objects
|
|
original = getattr(cls, "_original_model", cls)
|
|
partial_model._original_model = original # type: ignore[attr-defined]
|
|
|
|
return partial_model
|
|
|
|
@classmethod
|
|
def from_streaming_response(
|
|
cls,
|
|
completion: Iterable[Any],
|
|
stream_extractor: Callable[[Iterable[Any]], Generator[str, None, None]],
|
|
chunk_parser: Callable[..., Generator[T_Model, None, None]] | None = None,
|
|
**kwargs: Any,
|
|
) -> Generator[T_Model, None, None]:
|
|
if stream_extractor is None:
|
|
raise ValueError("stream_extractor is required for streaming responses")
|
|
json_chunks = stream_extractor(completion)
|
|
parser = chunk_parser or cls.model_from_chunks
|
|
yield from parser(json_chunks, **kwargs)
|
|
|
|
@classmethod
|
|
async def from_streaming_response_async(
|
|
cls,
|
|
completion: AsyncGenerator[Any, None],
|
|
stream_extractor: Callable[
|
|
[AsyncGenerator[Any, None]], AsyncGenerator[str, None]
|
|
],
|
|
chunk_parser: Callable[..., AsyncGenerator[T_Model, None]] | None = None,
|
|
**kwargs: Any,
|
|
) -> AsyncGenerator[T_Model, None]:
|
|
if stream_extractor is None:
|
|
raise ValueError("stream_extractor is required for streaming responses")
|
|
json_chunks = stream_extractor(completion)
|
|
parser = chunk_parser or cls.model_from_chunks_async
|
|
async for item in parser(json_chunks, **kwargs):
|
|
yield item
|
|
|
|
@classmethod
|
|
def model_from_chunks(
|
|
cls, json_chunks: Iterable[Any], **kwargs: Any
|
|
) -> Generator[T_Model, None, None]:
|
|
potential_object = ""
|
|
partial_model = cls.get_partial_model()
|
|
# Always use trailing-strings mode to preserve incomplete data during streaming
|
|
# PartialLiteralMixin is deprecated - completeness-based validation handles Literals
|
|
partial_mode = "trailing-strings"
|
|
final_obj = None
|
|
for chunk in json_chunks:
|
|
if chunk is None:
|
|
continue
|
|
if not isinstance(chunk, str):
|
|
try:
|
|
chunk = str(chunk)
|
|
except Exception:
|
|
continue
|
|
potential_object += remove_control_chars(chunk)
|
|
obj = process_potential_object(
|
|
potential_object, partial_mode, partial_model, **kwargs
|
|
)
|
|
final_obj = obj
|
|
yield obj
|
|
|
|
# Final validation: only validate if the JSON is structurally complete
|
|
# If JSON is incomplete (stream ended mid-object), skip validation
|
|
if final_obj is not None:
|
|
original_model = getattr(cls, "_original_model", None)
|
|
if original_model is not None:
|
|
if is_json_complete(potential_object.strip() or "{}"):
|
|
original_model.model_validate(
|
|
final_obj.model_dump(exclude_none=True), **kwargs
|
|
)
|
|
|
|
@classmethod
|
|
async def model_from_chunks_async(
|
|
cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any
|
|
) -> AsyncGenerator[T_Model, None]:
|
|
potential_object = ""
|
|
partial_model = cls.get_partial_model()
|
|
# Always use trailing-strings mode to preserve incomplete data during streaming
|
|
# PartialLiteralMixin is deprecated - completeness-based validation handles Literals
|
|
partial_mode = "trailing-strings"
|
|
final_obj = None
|
|
async for chunk in json_chunks:
|
|
if chunk is None:
|
|
continue
|
|
if not isinstance(chunk, str):
|
|
try:
|
|
chunk = str(chunk)
|
|
except Exception:
|
|
continue
|
|
potential_object += remove_control_chars(chunk)
|
|
obj = process_potential_object(
|
|
potential_object, partial_mode, partial_model, **kwargs
|
|
)
|
|
final_obj = obj
|
|
yield obj
|
|
|
|
# Final validation: only validate if the JSON is structurally complete
|
|
# If JSON is incomplete (stream ended mid-object), skip validation
|
|
if final_obj is not None:
|
|
original_model = getattr(cls, "_original_model", None)
|
|
if original_model is not None:
|
|
if is_json_complete(potential_object.strip() or "{}"):
|
|
original_model.model_validate(
|
|
final_obj.model_dump(exclude_none=True), **kwargs
|
|
)
|
|
|
|
@staticmethod
|
|
def extract_json(
|
|
completion: Iterable[Any],
|
|
stream_extractor: Callable[[Iterable[Any]], Generator[str, None, None]] | Any,
|
|
on_event: Callable[..., Any] | None = None,
|
|
) -> Generator[str, None, None]:
|
|
from instructor.v2.core.mode import Mode
|
|
|
|
if stream_extractor in {
|
|
Mode.RESPONSES_TOOLS,
|
|
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
|
|
}:
|
|
from openai.types.responses import (
|
|
ResponseFunctionCallArgumentsDeltaEvent,
|
|
ResponseReasoningSummaryTextDeltaEvent,
|
|
ResponseReasoningSummaryTextDoneEvent,
|
|
)
|
|
|
|
for chunk in completion:
|
|
if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
|
|
yield chunk.delta
|
|
elif on_event and isinstance(
|
|
chunk,
|
|
(
|
|
ResponseReasoningSummaryTextDeltaEvent,
|
|
ResponseReasoningSummaryTextDoneEvent,
|
|
),
|
|
):
|
|
on_event(chunk)
|
|
return
|
|
if stream_extractor is None:
|
|
raise ValueError("stream_extractor is required for streaming responses")
|
|
yield from stream_extractor(completion)
|
|
|
|
@staticmethod
|
|
async def extract_json_async(
|
|
completion: AsyncGenerator[Any, None],
|
|
stream_extractor: Callable[
|
|
[AsyncGenerator[Any, None]], AsyncGenerator[str, None]
|
|
]
|
|
| Any,
|
|
on_event: Callable[..., Any] | None = None,
|
|
) -> AsyncGenerator[str, None]:
|
|
import inspect
|
|
|
|
from instructor.v2.core.mode import Mode
|
|
|
|
if stream_extractor in {
|
|
Mode.RESPONSES_TOOLS,
|
|
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
|
|
}:
|
|
from openai.types.responses import (
|
|
ResponseFunctionCallArgumentsDeltaEvent,
|
|
ResponseReasoningSummaryTextDeltaEvent,
|
|
ResponseReasoningSummaryTextDoneEvent,
|
|
)
|
|
|
|
async for chunk in completion:
|
|
if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
|
|
yield chunk.delta
|
|
elif on_event and isinstance(
|
|
chunk,
|
|
(
|
|
ResponseReasoningSummaryTextDeltaEvent,
|
|
ResponseReasoningSummaryTextDoneEvent,
|
|
),
|
|
):
|
|
maybe_awaitable = on_event(chunk)
|
|
if inspect.isawaitable(maybe_awaitable):
|
|
await maybe_awaitable
|
|
return
|
|
if stream_extractor is None:
|
|
raise ValueError("stream_extractor is required for streaming responses")
|
|
async for chunk in stream_extractor(completion):
|
|
yield chunk
|
|
|
|
|
|
class Partial(Generic[T_Model]):
|
|
"""Generate a new class which has PartialBase as a base class.
|
|
|
|
Notes:
|
|
This will enable partial validation of the model while streaming.
|
|
|
|
Example:
|
|
Partial[SomeModel]
|
|
"""
|
|
|
|
def __new__(
|
|
cls,
|
|
*args: object, # noqa
|
|
**kwargs: object, # noqa
|
|
) -> Partial[T_Model]:
|
|
"""Cannot instantiate.
|
|
|
|
Raises:
|
|
TypeError: Direct instantiation not allowed.
|
|
"""
|
|
raise TypeError("Cannot instantiate abstract Partial class.")
|
|
|
|
def __init_subclass__(
|
|
cls,
|
|
*args: object,
|
|
**kwargs: object,
|
|
) -> NoReturn:
|
|
"""Cannot subclass.
|
|
|
|
Raises:
|
|
TypeError: Subclassing not allowed.
|
|
"""
|
|
raise TypeError(f"Cannot subclass {cls.__module__}.Partial")
|
|
|
|
def __class_getitem__(
|
|
cls,
|
|
wrapped_class: type[T_Model] | tuple[type[T_Model], type[MakeFieldsOptional]],
|
|
) -> type[T_Model]:
|
|
"""Convert model to one that inherits from PartialBase.
|
|
|
|
We don't make the fields optional at this point, we just wrap them with `Partial` so the names of the nested models will be
|
|
`Partial{ModelName}`. We want the output of `model_json_schema()` to
|
|
reflect the name change, but everything else should be the same as the
|
|
original model. During validation, we'll generate a true partial model
|
|
to support partially defined fields.
|
|
|
|
"""
|
|
|
|
make_fields_optional = None
|
|
if isinstance(wrapped_class, tuple):
|
|
wrapped_class, make_fields_optional = wrapped_class
|
|
|
|
def _wrap_models(field: FieldInfo) -> tuple[object, FieldInfo]:
|
|
tmp_field = deepcopy(field)
|
|
|
|
annotation = field.annotation
|
|
|
|
# Handle generics (like List, Dict, etc.)
|
|
if get_origin(annotation) is not None:
|
|
# Get the generic base (like List, Dict) and its arguments (like User in List[User])
|
|
generic_base = get_origin(annotation)
|
|
generic_args = get_args(annotation)
|
|
|
|
modified_args = tuple(_process_generic_arg(arg) for arg in generic_args)
|
|
|
|
# Reconstruct the generic type with modified arguments
|
|
if generic_base is UNION_TYPE:
|
|
tmp_field.annotation = reduce(or_, modified_args)
|
|
else:
|
|
tmp_field.annotation = (
|
|
generic_base[modified_args] if generic_base else None
|
|
)
|
|
# If the field is a BaseModel, then recursively convert it's
|
|
# attributes to optionals.
|
|
elif isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
# Prevent infinite recursion for self-referential models
|
|
if annotation in _processing_models:
|
|
tmp_field.annotation = (
|
|
annotation # Already processing, keep unwrapped
|
|
)
|
|
else:
|
|
_processing_models.add(annotation)
|
|
try:
|
|
tmp_field.annotation = Partial[annotation]
|
|
finally:
|
|
_processing_models.discard(annotation)
|
|
return tmp_field.annotation, tmp_field
|
|
|
|
model_name = (
|
|
wrapped_class.__name__
|
|
if wrapped_class.__name__.startswith("Partial")
|
|
else f"Partial{wrapped_class.__name__}"
|
|
)
|
|
|
|
partial_model = create_model( # ty: ignore[no-matching-overload]
|
|
model_name,
|
|
__base__=(wrapped_class, PartialBase),
|
|
__module__=wrapped_class.__module__,
|
|
**{
|
|
field_name: (
|
|
_make_field_optional(field_info)
|
|
if make_fields_optional is not None
|
|
else _wrap_models(field_info)
|
|
)
|
|
for field_name, field_info in wrapped_class.model_fields.items()
|
|
},
|
|
)
|
|
|
|
# Store reference to original model for final validation
|
|
partial_model._original_model = wrapped_class # type: ignore[attr-defined]
|
|
|
|
return partial_model
|