|
25 | 25 | ClassVar, |
26 | 26 | Protocol, |
27 | 27 | Required, |
| 28 | + Annotated, |
28 | 29 | ParamSpec, |
| 30 | + TypeAlias, |
29 | 31 | TypedDict, |
30 | 32 | TypeGuard, |
31 | 33 | final, |
|
79 | 81 | from ._constants import RAW_RESPONSE_HEADER |
80 | 82 |
|
81 | 83 | if TYPE_CHECKING: |
| 84 | + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler |
| 85 | + from pydantic_core import CoreSchema, core_schema |
82 | 86 | from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema |
| 87 | +else: |
| 88 | + try: |
| 89 | + from pydantic_core import CoreSchema, core_schema |
| 90 | + except ImportError: |
| 91 | + CoreSchema = None |
| 92 | + core_schema = None |
83 | 93 |
|
84 | 94 | __all__ = ["BaseModel", "GenericModel"] |
85 | 95 |
|
@@ -396,6 +406,76 @@ def model_dump_json( |
396 | 406 | ) |
397 | 407 |
|
398 | 408 |
|
| 409 | +class _EagerIterable(list[_T], Generic[_T]): |
| 410 | + """ |
| 411 | + Accepts any Iterable[T] input (including generators), consumes it |
| 412 | + eagerly, and validates all items upfront. |
| 413 | +
|
| 414 | + Validation preserves the original container type where possible |
| 415 | + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) |
| 416 | + always emits a list — round-tripping through model_dump() will not |
| 417 | + restore the original container type. |
| 418 | + """ |
| 419 | + |
| 420 | + @classmethod |
| 421 | + def __get_pydantic_core_schema__( |
| 422 | + cls, |
| 423 | + source_type: Any, |
| 424 | + handler: GetCoreSchemaHandler, |
| 425 | + ) -> CoreSchema: |
| 426 | + (item_type,) = get_args(source_type) or (Any,) |
| 427 | + item_schema: CoreSchema = handler.generate_schema(item_type) |
| 428 | + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) |
| 429 | + |
| 430 | + return core_schema.no_info_wrap_validator_function( |
| 431 | + cls._validate, |
| 432 | + list_of_items_schema, |
| 433 | + serialization=core_schema.plain_serializer_function_ser_schema( |
| 434 | + cls._serialize, |
| 435 | + info_arg=False, |
| 436 | + ), |
| 437 | + ) |
| 438 | + |
| 439 | + @staticmethod |
| 440 | + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: |
| 441 | + original_type: type[Any] = type(v) |
| 442 | + |
| 443 | + # Normalize to list so list_schema can validate each item |
| 444 | + if isinstance(v, list): |
| 445 | + items: list[_T] = v |
| 446 | + else: |
| 447 | + try: |
| 448 | + items = list(v) |
| 449 | + except TypeError as e: |
| 450 | + raise TypeError("Value is not iterable") from e |
| 451 | + |
| 452 | + # Validate items against the inner schema |
| 453 | + validated: list[_T] = handler(items) |
| 454 | + |
| 455 | + # Reconstruct original container type |
| 456 | + if original_type is list: |
| 457 | + return validated |
| 458 | + # str(list) produces the list's repr, not a string built from items, |
| 459 | + # so skip reconstruction for str and its subclasses. |
| 460 | + if issubclass(original_type, str): |
| 461 | + return validated |
| 462 | + try: |
| 463 | + return original_type(validated) |
| 464 | + except (TypeError, ValueError): |
| 465 | + # If the type cannot be reconstructed, just return the validated list |
| 466 | + return validated |
| 467 | + |
| 468 | + @staticmethod |
| 469 | + def _serialize(v: Iterable[_T]) -> list[_T]: |
| 470 | + """Always serialize as a list so Pydantic's JSON encoder is happy.""" |
| 471 | + if isinstance(v, list): |
| 472 | + return v |
| 473 | + return list(v) |
| 474 | + |
| 475 | + |
| 476 | +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] |
| 477 | + |
| 478 | + |
399 | 479 | def _construct_field(value: object, field: FieldInfo, key: str) -> object: |
400 | 480 | if value is None: |
401 | 481 | return field_get_default(field) |
|
0 commit comments