openai-python
299 строк · 9.7 Кб
1from __future__ import annotations2
3from typing import Any, List, Union, Iterable, Optional, cast4from datetime import date, datetime5from typing_extensions import Required, Annotated, TypedDict6
7import pytest8
9from openai._utils import PropertyInfo, transform, parse_datetime10from openai._compat import PYDANTIC_V211from openai._models import BaseModel12
13
14class Foo1(TypedDict):15foo_bar: Annotated[str, PropertyInfo(alias="fooBar")]16
17
18def test_top_level_alias() -> None:19assert transform({"foo_bar": "hello"}, expected_type=Foo1) == {"fooBar": "hello"}20
21
22class Foo2(TypedDict):23bar: Bar224
25
26class Bar2(TypedDict):27this_thing: Annotated[int, PropertyInfo(alias="this__thing")]28baz: Annotated[Baz2, PropertyInfo(alias="Baz")]29
30
31class Baz2(TypedDict):32my_baz: Annotated[str, PropertyInfo(alias="myBaz")]33
34
35def test_recursive_typeddict() -> None:36assert transform({"bar": {"this_thing": 1}}, Foo2) == {"bar": {"this__thing": 1}}37assert transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2) == {"bar": {"Baz": {"myBaz": "foo"}}}38
39
40class Foo3(TypedDict):41things: List[Bar3]42
43
44class Bar3(TypedDict):45my_field: Annotated[str, PropertyInfo(alias="myField")]46
47
48def test_list_of_typeddict() -> None:49result = transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, expected_type=Foo3)50assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]}51
52
53class Foo4(TypedDict):54foo: Union[Bar4, Baz4]55
56
57class Bar4(TypedDict):58foo_bar: Annotated[str, PropertyInfo(alias="fooBar")]59
60
61class Baz4(TypedDict):62foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")]63
64
65def test_union_of_typeddict() -> None:66assert transform({"foo": {"foo_bar": "bar"}}, Foo4) == {"foo": {"fooBar": "bar"}}67assert transform({"foo": {"foo_baz": "baz"}}, Foo4) == {"foo": {"fooBaz": "baz"}}68assert transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4) == {"foo": {"fooBaz": "baz", "fooBar": "bar"}}69
70
71class Foo5(TypedDict):72foo: Annotated[Union[Bar4, List[Baz4]], PropertyInfo(alias="FOO")]73
74
75class Bar5(TypedDict):76foo_bar: Annotated[str, PropertyInfo(alias="fooBar")]77
78
79class Baz5(TypedDict):80foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")]81
82
83def test_union_of_list() -> None:84assert transform({"foo": {"foo_bar": "bar"}}, Foo5) == {"FOO": {"fooBar": "bar"}}85assert transform(86{87"foo": [88{"foo_baz": "baz"},89{"foo_baz": "baz"},90]91},92Foo5,93) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]}94
95
96class Foo6(TypedDict):97bar: Annotated[str, PropertyInfo(alias="Bar")]98
99
100def test_includes_unknown_keys() -> None:101assert transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6) == {102"Bar": "bar",103"baz_": {"FOO": 1},104}105
106
107class Foo7(TypedDict):108bar: Annotated[List[Bar7], PropertyInfo(alias="bAr")]109foo: Bar7110
111
112class Bar7(TypedDict):113foo: str114
115
116def test_ignores_invalid_input() -> None:117assert transform({"bar": "<foo>"}, Foo7) == {"bAr": "<foo>"}118assert transform({"foo": "<foo>"}, Foo7) == {"foo": "<foo>"}119
120
121class DatetimeDict(TypedDict, total=False):122foo: Annotated[datetime, PropertyInfo(format="iso8601")]123
124bar: Annotated[Optional[datetime], PropertyInfo(format="iso8601")]125
126required: Required[Annotated[Optional[datetime], PropertyInfo(format="iso8601")]]127
128list_: Required[Annotated[Optional[List[datetime]], PropertyInfo(format="iso8601")]]129
130union: Annotated[Union[int, datetime], PropertyInfo(format="iso8601")]131
132
133class DateDict(TypedDict, total=False):134foo: Annotated[date, PropertyInfo(format="iso8601")]135
136
137def test_iso8601_format() -> None:138dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")139assert transform({"foo": dt}, DatetimeDict) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]140
141dt = dt.replace(tzinfo=None)142assert transform({"foo": dt}, DatetimeDict) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap]143
144assert transform({"foo": None}, DateDict) == {"foo": None} # type: ignore[comparison-overlap]145assert transform({"foo": date.fromisoformat("2023-02-23")}, DateDict) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap]146
147
148def test_optional_iso8601_format() -> None:149dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")150assert transform({"bar": dt}, DatetimeDict) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]151
152assert transform({"bar": None}, DatetimeDict) == {"bar": None}153
154
155def test_required_iso8601_format() -> None:156dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")157assert transform({"required": dt}, DatetimeDict) == {"required": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]158
159assert transform({"required": None}, DatetimeDict) == {"required": None}160
161
162def test_union_datetime() -> None:163dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")164assert transform({"union": dt}, DatetimeDict) == { # type: ignore[comparison-overlap]165"union": "2023-02-23T14:16:36.337692+00:00"166}167
168assert transform({"union": "foo"}, DatetimeDict) == {"union": "foo"}169
170
171def test_nested_list_iso6801_format() -> None:172dt1 = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")173dt2 = parse_datetime("2022-01-15T06:34:23Z")174assert transform({"list_": [dt1, dt2]}, DatetimeDict) == { # type: ignore[comparison-overlap]175"list_": ["2023-02-23T14:16:36.337692+00:00", "2022-01-15T06:34:23+00:00"]176}177
178
179def test_datetime_custom_format() -> None:180dt = parse_datetime("2022-01-15T06:34:23Z")181
182result = transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")])183assert result == "06" # type: ignore[comparison-overlap]184
185
186class DateDictWithRequiredAlias(TypedDict, total=False):187required_prop: Required[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]]188
189
190def test_datetime_with_alias() -> None:191assert transform({"required_prop": None}, DateDictWithRequiredAlias) == {"prop": None} # type: ignore[comparison-overlap]192assert transform({"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias) == {193"prop": "2023-02-23"194} # type: ignore[comparison-overlap]195
196
197class MyModel(BaseModel):198foo: str199
200
201def test_pydantic_model_to_dictionary() -> None:202assert transform(MyModel(foo="hi!"), Any) == {"foo": "hi!"}203assert transform(MyModel.construct(foo="hi!"), Any) == {"foo": "hi!"}204
205
206def test_pydantic_empty_model() -> None:207assert transform(MyModel.construct(), Any) == {}208
209
210def test_pydantic_unknown_field() -> None:211assert transform(MyModel.construct(my_untyped_field=True), Any) == {"my_untyped_field": True}212
213
214def test_pydantic_mismatched_types() -> None:215model = MyModel.construct(foo=True)216if PYDANTIC_V2:217with pytest.warns(UserWarning):218params = transform(model, Any)219else:220params = transform(model, Any)221assert params == {"foo": True}222
223
224def test_pydantic_mismatched_object_type() -> None:225model = MyModel.construct(foo=MyModel.construct(hello="world"))226if PYDANTIC_V2:227with pytest.warns(UserWarning):228params = transform(model, Any)229else:230params = transform(model, Any)231assert params == {"foo": {"hello": "world"}}232
233
234class ModelNestedObjects(BaseModel):235nested: MyModel236
237
238def test_pydantic_nested_objects() -> None:239model = ModelNestedObjects.construct(nested={"foo": "stainless"})240assert isinstance(model.nested, MyModel)241assert transform(model, Any) == {"nested": {"foo": "stainless"}}242
243
244class ModelWithDefaultField(BaseModel):245foo: str246with_none_default: Union[str, None] = None247with_str_default: str = "foo"248
249
250def test_pydantic_default_field() -> None:251# should be excluded when defaults are used252model = ModelWithDefaultField.construct()253assert model.with_none_default is None254assert model.with_str_default == "foo"255assert transform(model, Any) == {}256
257# should be included when the default value is explicitly given258model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo")259assert model.with_none_default is None260assert model.with_str_default == "foo"261assert transform(model, Any) == {"with_none_default": None, "with_str_default": "foo"}262
263# should be included when a non-default value is explicitly given264model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz")265assert model.with_none_default == "bar"266assert model.with_str_default == "baz"267assert transform(model, Any) == {"with_none_default": "bar", "with_str_default": "baz"}268
269
270class TypedDictIterableUnion(TypedDict):271foo: Annotated[Union[Bar8, Iterable[Baz8]], PropertyInfo(alias="FOO")]272
273
274class Bar8(TypedDict):275foo_bar: Annotated[str, PropertyInfo(alias="fooBar")]276
277
278class Baz8(TypedDict):279foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")]280
281
282def test_iterable_of_dictionaries() -> None:283assert transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion) == {"FOO": [{"fooBaz": "bar"}]}284assert cast(Any, transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion)) == {"FOO": [{"fooBaz": "bar"}]}285
286def my_iter() -> Iterable[Baz8]:287yield {"foo_baz": "hello"}288yield {"foo_baz": "world"}289
290assert transform({"foo": my_iter()}, TypedDictIterableUnion) == {"FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}]}291
292
293class TypedDictIterableUnionStr(TypedDict):294foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")]295
296
297def test_iterable_union_str() -> None:298assert transform({"foo": "bar"}, TypedDictIterableUnionStr) == {"FOO": "bar"}299assert cast(Any, transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]])) == [{"fooBaz": "bar"}]300