|
39 | 39 |
|
40 | 40 | T = TypeVar("T") |
41 | 41 |
|
| 42 | +try: |
| 43 | + import pydantic, pydantic_core |
| 44 | +except ImportError: |
| 45 | + pass |
| 46 | + |
42 | 47 |
|
43 | 48 | class ChildFieldPath: |
44 | 49 | """Context manager to append a field to field_path on enter and pop it on exit.""" |
@@ -777,106 +782,69 @@ def load_engine_object(expected_type: Any, v: Any) -> Any: |
777 | 782 | # Structs (dataclass, NamedTuple, or Pydantic) |
778 | 783 | if isinstance(variant, AnalyzedStructType): |
779 | 784 | struct_type = variant.struct_type |
| 785 | + init_kwargs: dict[str, Any] = {} |
| 786 | + missing_fields: list[tuple[str, Any]] = [] |
780 | 787 | if dataclasses.is_dataclass(struct_type): |
781 | 788 | if not isinstance(v, Mapping): |
782 | 789 | raise ValueError(f"Expected dict for dataclass, got {type(v)}") |
783 | | - # Drop auxiliary discriminator "kind" if present |
784 | | - dc_init_kwargs: dict[str, Any] = {} |
785 | | - field_types = {f.name: f.type for f in dataclasses.fields(struct_type)} |
786 | | - dataclass_fields = {f.name: f for f in dataclasses.fields(struct_type)} |
787 | 790 |
|
788 | | - for name, f_type in field_types.items(): |
789 | | - if name in v: |
790 | | - dc_init_kwargs[name] = load_engine_object(f_type, v[name]) |
| 791 | + for dc_field in dataclasses.fields(struct_type): |
| 792 | + if dc_field.name in v: |
| 793 | + init_kwargs[dc_field.name] = load_engine_object( |
| 794 | + dc_field.type, v[dc_field.name] |
| 795 | + ) |
791 | 796 | else: |
792 | | - # Field is missing from input, check if it has a default or can use auto-default |
793 | | - field = dataclass_fields[name] |
794 | | - if field.default is not dataclasses.MISSING: |
795 | | - # Field has an explicit default value |
796 | | - dc_init_kwargs[name] = field.default |
797 | | - elif field.default_factory is not dataclasses.MISSING: |
798 | | - # Field has a default factory |
799 | | - dc_init_kwargs[name] = field.default_factory() |
800 | | - else: |
801 | | - # No explicit default, try to get auto-default |
802 | | - type_info = analyze_type_info(f_type) |
803 | | - auto_default, is_supported = _get_auto_default_for_type( |
804 | | - type_info |
805 | | - ) |
806 | | - if is_supported: |
807 | | - dc_init_kwargs[name] = auto_default |
808 | | - # If not supported, skip the field (let dataclass constructor handle the error) |
809 | | - return struct_type(**dc_init_kwargs) |
| 797 | + if ( |
| 798 | + dc_field.default is dataclasses.MISSING |
| 799 | + and dc_field.default_factory is dataclasses.MISSING |
| 800 | + ): |
| 801 | + missing_fields.append((dc_field.name, dc_field.type)) |
| 802 | + |
810 | 803 | elif is_namedtuple_type(struct_type): |
811 | 804 | if not isinstance(v, Mapping): |
812 | 805 | raise ValueError(f"Expected dict for NamedTuple, got {type(v)}") |
813 | 806 | # Dict format (from dump/load functions) |
814 | 807 | annotations = getattr(struct_type, "__annotations__", {}) |
815 | 808 | field_names = list(getattr(struct_type, "_fields", ())) |
816 | 809 | field_defaults = getattr(struct_type, "_field_defaults", {}) |
817 | | - nt_init_kwargs: dict[str, Any] = {} |
818 | 810 |
|
819 | 811 | for name in field_names: |
820 | 812 | f_type = annotations.get(name, Any) |
821 | 813 | if name in v: |
822 | | - nt_init_kwargs[name] = load_engine_object(f_type, v[name]) |
823 | | - else: |
824 | | - # Field is missing from input, check if it has a default or can use auto-default |
825 | | - if name in field_defaults: |
826 | | - # Field has an explicit default value |
827 | | - nt_init_kwargs[name] = field_defaults[name] |
828 | | - else: |
829 | | - # No explicit default, try to get auto-default |
830 | | - type_info = analyze_type_info(f_type) |
831 | | - auto_default, is_supported = _get_auto_default_for_type( |
832 | | - type_info |
833 | | - ) |
834 | | - if is_supported: |
835 | | - nt_init_kwargs[name] = auto_default |
836 | | - # If not supported, skip the field (let NamedTuple constructor handle the error) |
837 | | - return struct_type(**nt_init_kwargs) |
| 814 | + init_kwargs[name] = load_engine_object(f_type, v[name]) |
| 815 | + elif name not in field_defaults: |
| 816 | + missing_fields.append((name, f_type)) |
| 817 | + |
838 | 818 | elif is_pydantic_model(struct_type): |
839 | 819 | if not isinstance(v, Mapping): |
840 | 820 | raise ValueError(f"Expected dict for Pydantic model, got {type(v)}") |
841 | | - # Drop auxiliary discriminator "kind" if present |
842 | | - pydantic_init_kwargs: dict[str, Any] = {} |
843 | | - # Type guard: ensure we have model_fields attribute |
| 821 | + |
| 822 | + model_fields: dict[str, pydantic.fields.FieldInfo] |
844 | 823 | if hasattr(struct_type, "model_fields"): |
845 | 824 | model_fields = struct_type.model_fields # type: ignore[attr-defined] |
846 | 825 | else: |
847 | 826 | model_fields = {} |
848 | | - field_types = { |
849 | | - name: field.annotation for name, field in model_fields.items() |
850 | | - } |
851 | 827 |
|
852 | | - for name, f_type in field_types.items(): |
| 828 | + for name, pyd_field in model_fields.items(): |
853 | 829 | if name in v: |
854 | | - pydantic_init_kwargs[name] = load_engine_object(f_type, v[name]) |
855 | | - else: |
856 | | - # Field is missing from input, check if it has a default or can use auto-default |
857 | | - field = model_fields[name] |
858 | | - if ( |
859 | | - hasattr(field, "default") and field.default is not ... |
860 | | - ): # ... is Pydantic's sentinel for no default |
861 | | - # Field has an explicit default value |
862 | | - pydantic_init_kwargs[name] = field.default |
863 | | - elif ( |
864 | | - hasattr(field, "default_factory") |
865 | | - and field.default_factory is not None |
866 | | - ): |
867 | | - # Field has a default factory |
868 | | - pydantic_init_kwargs[name] = field.default_factory() |
869 | | - else: |
870 | | - # No explicit default, try to get auto-default |
871 | | - type_info = analyze_type_info(f_type) |
872 | | - auto_default, is_supported = _get_auto_default_for_type( |
873 | | - type_info |
874 | | - ) |
875 | | - if is_supported: |
876 | | - pydantic_init_kwargs[name] = auto_default |
877 | | - # If not supported, skip the field (let Pydantic constructor handle the error) |
878 | | - return struct_type(**pydantic_init_kwargs) |
879 | | - return v |
| 830 | + init_kwargs[name] = load_engine_object( |
| 831 | + pyd_field.annotation, v[name] |
| 832 | + ) |
| 833 | + elif ( |
| 834 | + getattr(pyd_field, "default", pydantic_core.PydanticUndefined) |
| 835 | + is pydantic_core.PydanticUndefined |
| 836 | + and getattr(pyd_field, "default_factory") is None |
| 837 | + ): |
| 838 | + missing_fields.append((name, pyd_field.annotation)) |
| 839 | + else: |
| 840 | + assert False, "Unsupported struct type" |
| 841 | + |
| 842 | + for name, f_type in missing_fields: |
| 843 | + type_info = analyze_type_info(f_type) |
| 844 | + auto_default, is_supported = _get_auto_default_for_type(type_info) |
| 845 | + if is_supported: |
| 846 | + init_kwargs[name] = auto_default |
| 847 | + return struct_type(**init_kwargs) |
880 | 848 |
|
881 | 849 | # Union with discriminator support via "kind" |
882 | 850 | if isinstance(variant, AnalyzedUnionType): |
|
0 commit comments