openai/openai-python

Public

mirrored from https://github.com/openai/openai-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.8.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

tests/test_models.py

573lines · modeblame

08b8179aDavid Schnurr2 years ago1import json
2from typing import Any, Dict, List, Union, Optional, cast
3from datetime import datetime, timezone
4from typing_extensions import Literal
5
6import pytest
7import pydantic
8from pydantic import Field
9
10from openai._compat import PYDANTIC_V2, parse_obj, model_dump, model_json
11from openai._models import BaseModel
12
13
14class BasicModel(BaseModel):
15foo: str
16
17
18@pytest.mark.parametrize("value", ["hello", 1], ids=["correct type", "mismatched"])
19def test_basic(value: object) -> None:
20m = BasicModel.construct(foo=value)
21assert m.foo == value
22
23
24def test_directly_nested_model() -> None:
25class NestedModel(BaseModel):
26nested: BasicModel
27
28m = NestedModel.construct(nested={"foo": "Foo!"})
29assert m.nested.foo == "Foo!"
30
31# mismatched types
32m = NestedModel.construct(nested="hello!")
33assert m.nested == "hello!"
34
35
36def test_optional_nested_model() -> None:
37class NestedModel(BaseModel):
38nested: Optional[BasicModel]
39
40m1 = NestedModel.construct(nested=None)
41assert m1.nested is None
42
43m2 = NestedModel.construct(nested={"foo": "bar"})
44assert m2.nested is not None
45assert m2.nested.foo == "bar"
46
47# mismatched types
48m3 = NestedModel.construct(nested={"foo"})
49assert isinstance(cast(Any, m3.nested), set)
50assert m3.nested == {"foo"}
51
52
53def test_list_nested_model() -> None:
54class NestedModel(BaseModel):
55nested: List[BasicModel]
56
57m = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}])
58assert m.nested is not None
59assert isinstance(m.nested, list)
60assert len(m.nested) == 2
61assert m.nested[0].foo == "bar"
62assert m.nested[1].foo == "2"
63
64# mismatched types
65m = NestedModel.construct(nested=True)
66assert cast(Any, m.nested) is True
67
68m = NestedModel.construct(nested=[False])
69assert cast(Any, m.nested) == [False]
70
71
72def test_optional_list_nested_model() -> None:
73class NestedModel(BaseModel):
74nested: Optional[List[BasicModel]]
75
76m1 = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}])
77assert m1.nested is not None
78assert isinstance(m1.nested, list)
79assert len(m1.nested) == 2
80assert m1.nested[0].foo == "bar"
81assert m1.nested[1].foo == "2"
82
83m2 = NestedModel.construct(nested=None)
84assert m2.nested is None
85
86# mismatched types
87m3 = NestedModel.construct(nested={1})
88assert cast(Any, m3.nested) == {1}
89
90m4 = NestedModel.construct(nested=[False])
91assert cast(Any, m4.nested) == [False]
92
93
94def test_list_optional_items_nested_model() -> None:
95class NestedModel(BaseModel):
96nested: List[Optional[BasicModel]]
97
98m = NestedModel.construct(nested=[None, {"foo": "bar"}])
99assert m.nested is not None
100assert isinstance(m.nested, list)
101assert len(m.nested) == 2
102assert m.nested[0] is None
103assert m.nested[1] is not None
104assert m.nested[1].foo == "bar"
105
106# mismatched types
107m3 = NestedModel.construct(nested="foo")
108assert cast(Any, m3.nested) == "foo"
109
110m4 = NestedModel.construct(nested=[False])
111assert cast(Any, m4.nested) == [False]
112
113
114def test_list_mismatched_type() -> None:
115class NestedModel(BaseModel):
116nested: List[str]
117
118m = NestedModel.construct(nested=False)
119assert cast(Any, m.nested) is False
120
121
122def test_raw_dictionary() -> None:
123class NestedModel(BaseModel):
124nested: Dict[str, str]
125
126m = NestedModel.construct(nested={"hello": "world"})
127assert m.nested == {"hello": "world"}
128
129# mismatched types
130m = NestedModel.construct(nested=False)
131assert cast(Any, m.nested) is False
132
133
134def test_nested_dictionary_model() -> None:
135class NestedModel(BaseModel):
136nested: Dict[str, BasicModel]
137
138m = NestedModel.construct(nested={"hello": {"foo": "bar"}})
139assert isinstance(m.nested, dict)
140assert m.nested["hello"].foo == "bar"
141
142# mismatched types
143m = NestedModel.construct(nested={"hello": False})
144assert cast(Any, m.nested["hello"]) is False
145
146
147def test_unknown_fields() -> None:
148m1 = BasicModel.construct(foo="foo", unknown=1)
149assert m1.foo == "foo"
150assert cast(Any, m1).unknown == 1
151
152m2 = BasicModel.construct(foo="foo", unknown={"foo_bar": True})
153assert m2.foo == "foo"
154assert cast(Any, m2).unknown == {"foo_bar": True}
155
156assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}}
157
158
159def test_strict_validation_unknown_fields() -> None:
160class Model(BaseModel):
161foo: str
162
163model = parse_obj(Model, dict(foo="hello!", user="Robert"))
164assert model.foo == "hello!"
165assert cast(Any, model).user == "Robert"
166
167assert model_dump(model) == {"foo": "hello!", "user": "Robert"}
168
169
170def test_aliases() -> None:
171class Model(BaseModel):
172my_field: int = Field(alias="myField")
173
174m = Model.construct(myField=1)
175assert m.my_field == 1
176
177# mismatched types
178m = Model.construct(myField={"hello": False})
179assert cast(Any, m.my_field) == {"hello": False}
180
181
182def test_repr() -> None:
183model = BasicModel(foo="bar")
184assert str(model) == "BasicModel(foo='bar')"
185assert repr(model) == "BasicModel(foo='bar')"
186
187
188def test_repr_nested_model() -> None:
189class Child(BaseModel):
190name: str
191age: int
192
193class Parent(BaseModel):
194name: str
195child: Child
196
197model = Parent(name="Robert", child=Child(name="Foo", age=5))
198assert str(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))"
199assert repr(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))"
200
201
202def test_optional_list() -> None:
203class Submodel(BaseModel):
204name: str
205
206class Model(BaseModel):
207items: Optional[List[Submodel]]
208
209m = Model.construct(items=None)
210assert m.items is None
211
212m = Model.construct(items=[])
213assert m.items == []
214
215m = Model.construct(items=[{"name": "Robert"}])
216assert m.items is not None
217assert len(m.items) == 1
218assert m.items[0].name == "Robert"
219
220
221def test_nested_union_of_models() -> None:
222class Submodel1(BaseModel):
223bar: bool
224
225class Submodel2(BaseModel):
226thing: str
227
228class Model(BaseModel):
229foo: Union[Submodel1, Submodel2]
230
231m = Model.construct(foo={"thing": "hello"})
232assert isinstance(m.foo, Submodel2)
233assert m.foo.thing == "hello"
234
235
236def test_nested_union_of_mixed_types() -> None:
237class Submodel1(BaseModel):
238bar: bool
239
240class Model(BaseModel):
241foo: Union[Submodel1, Literal[True], Literal["CARD_HOLDER"]]
242
243m = Model.construct(foo=True)
244assert m.foo is True
245
246m = Model.construct(foo="CARD_HOLDER")
247assert m.foo is "CARD_HOLDER"
248
249m = Model.construct(foo={"bar": False})
250assert isinstance(m.foo, Submodel1)
251assert m.foo.bar is False
252
253
254def test_nested_union_multiple_variants() -> None:
255class Submodel1(BaseModel):
256bar: bool
257
258class Submodel2(BaseModel):
259thing: str
260
261class Submodel3(BaseModel):
262foo: int
263
264class Model(BaseModel):
265foo: Union[Submodel1, Submodel2, None, Submodel3]
266
267m = Model.construct(foo={"thing": "hello"})
268assert isinstance(m.foo, Submodel2)
269assert m.foo.thing == "hello"
270
271m = Model.construct(foo=None)
272assert m.foo is None
273
274m = Model.construct()
275assert m.foo is None
276
277m = Model.construct(foo={"foo": "1"})
278assert isinstance(m.foo, Submodel3)
279assert m.foo.foo == 1
280
281
282def test_nested_union_invalid_data() -> None:
283class Submodel1(BaseModel):
284level: int
285
286class Submodel2(BaseModel):
287name: str
288
289class Model(BaseModel):
290foo: Union[Submodel1, Submodel2]
291
292m = Model.construct(foo=True)
293assert cast(bool, m.foo) is True
294
295m = Model.construct(foo={"name": 3})
296if PYDANTIC_V2:
297assert isinstance(m.foo, Submodel1)
298assert m.foo.name == 3 # type: ignore
299else:
300assert isinstance(m.foo, Submodel2)
301assert m.foo.name == "3"
302
303
304def test_list_of_unions() -> None:
305class Submodel1(BaseModel):
306level: int
307
308class Submodel2(BaseModel):
309name: str
310
311class Model(BaseModel):
312items: List[Union[Submodel1, Submodel2]]
313
314m = Model.construct(items=[{"level": 1}, {"name": "Robert"}])
315assert len(m.items) == 2
316assert isinstance(m.items[0], Submodel1)
317assert m.items[0].level == 1
318assert isinstance(m.items[1], Submodel2)
319assert m.items[1].name == "Robert"
320
321m = Model.construct(items=[{"level": -1}, 156])
322assert len(m.items) == 2
323assert isinstance(m.items[0], Submodel1)
324assert m.items[0].level == -1
325assert m.items[1] == 156
326
327
328def test_union_of_lists() -> None:
329class SubModel1(BaseModel):
330level: int
331
332class SubModel2(BaseModel):
333name: str
334
335class Model(BaseModel):
336items: Union[List[SubModel1], List[SubModel2]]
337
338# with one valid entry
339m = Model.construct(items=[{"name": "Robert"}])
340assert len(m.items) == 1
341assert isinstance(m.items[0], SubModel2)
342assert m.items[0].name == "Robert"
343
344# with two entries pointing to different types
345m = Model.construct(items=[{"level": 1}, {"name": "Robert"}])
346assert len(m.items) == 2
347assert isinstance(m.items[0], SubModel1)
348assert m.items[0].level == 1
349assert isinstance(m.items[1], SubModel1)
350assert cast(Any, m.items[1]).name == "Robert"
351
352# with two entries pointing to *completely* different types
353m = Model.construct(items=[{"level": -1}, 156])
354assert len(m.items) == 2
355assert isinstance(m.items[0], SubModel1)
356assert m.items[0].level == -1
357assert m.items[1] == 156
358
359
360def test_dict_of_union() -> None:
361class SubModel1(BaseModel):
362name: str
363
364class SubModel2(BaseModel):
365foo: str
366
367class Model(BaseModel):
368data: Dict[str, Union[SubModel1, SubModel2]]
369
370m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}})
371assert len(list(m.data.keys())) == 2
372assert isinstance(m.data["hello"], SubModel1)
373assert m.data["hello"].name == "there"
374assert isinstance(m.data["foo"], SubModel2)
375assert m.data["foo"].foo == "bar"
376
377# TODO: test mismatched type
378
379
380def test_double_nested_union() -> None:
381class SubModel1(BaseModel):
382name: str
383
384class SubModel2(BaseModel):
385bar: str
386
387class Model(BaseModel):
388data: Dict[str, List[Union[SubModel1, SubModel2]]]
389
390m = Model.construct(data={"foo": [{"bar": "baz"}, {"name": "Robert"}]})
391assert len(m.data["foo"]) == 2
392
393entry1 = m.data["foo"][0]
394assert isinstance(entry1, SubModel2)
395assert entry1.bar == "baz"
396
397entry2 = m.data["foo"][1]
398assert isinstance(entry2, SubModel1)
399assert entry2.name == "Robert"
400
401# TODO: test mismatched type
402
403
404def test_union_of_dict() -> None:
405class SubModel1(BaseModel):
406name: str
407
408class SubModel2(BaseModel):
409foo: str
410
411class Model(BaseModel):
412data: Union[Dict[str, SubModel1], Dict[str, SubModel2]]
413
414m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}})
415assert len(list(m.data.keys())) == 2
416assert isinstance(m.data["hello"], SubModel1)
417assert m.data["hello"].name == "there"
418assert isinstance(m.data["foo"], SubModel1)
419assert cast(Any, m.data["foo"]).foo == "bar"
420
421
422def test_iso8601_datetime() -> None:
423class Model(BaseModel):
424created_at: datetime
425
426expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc)
427
428if PYDANTIC_V2:
429expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}'
430else:
431expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}'
432
433model = Model.construct(created_at="2019-12-27T18:11:19.117Z")
434assert model.created_at == expected
435assert model_json(model) == expected_json
436
437model = parse_obj(Model, dict(created_at="2019-12-27T18:11:19.117Z"))
438assert model.created_at == expected
439assert model_json(model) == expected_json
440
441
442def test_does_not_coerce_int() -> None:
443class Model(BaseModel):
444bar: int
445
446assert Model.construct(bar=1).bar == 1
447assert Model.construct(bar=10.9).bar == 10.9
448assert Model.construct(bar="19").bar == "19" # type: ignore[comparison-overlap]
449assert Model.construct(bar=False).bar is False
450
451
452def test_int_to_float_safe_conversion() -> None:
453class Model(BaseModel):
454float_field: float
455
456m = Model.construct(float_field=10)
457assert m.float_field == 10.0
458assert isinstance(m.float_field, float)
459
460m = Model.construct(float_field=10.12)
461assert m.float_field == 10.12
462assert isinstance(m.float_field, float)
463
464# number too big
465m = Model.construct(float_field=2**53 + 1)
466assert m.float_field == 2**53 + 1
467assert isinstance(m.float_field, int)
468
469
470def test_deprecated_alias() -> None:
471class Model(BaseModel):
472resource_id: str = Field(alias="model_id")
473
474@property
475def model_id(self) -> str:
476return self.resource_id
477
478m = Model.construct(model_id="id")
479assert m.model_id == "id"
480assert m.resource_id == "id"
481assert m.resource_id is m.model_id
482
483m = parse_obj(Model, {"model_id": "id"})
484assert m.model_id == "id"
485assert m.resource_id == "id"
486assert m.resource_id is m.model_id
487
488
489def test_omitted_fields() -> None:
490class Model(BaseModel):
491resource_id: Optional[str] = None
492
493m = Model.construct()
494assert "resource_id" not in m.model_fields_set
495
496m = Model.construct(resource_id=None)
497assert "resource_id" in m.model_fields_set
498
499m = Model.construct(resource_id="foo")
500assert "resource_id" in m.model_fields_set
501
502
503def test_forwards_compat_model_dump_method() -> None:
504class Model(BaseModel):
505foo: Optional[str] = Field(alias="FOO", default=None)
506
507m = Model(FOO="hello")
508assert m.model_dump() == {"foo": "hello"}
509assert m.model_dump(include={"bar"}) == {}
510assert m.model_dump(exclude={"foo"}) == {}
511assert m.model_dump(by_alias=True) == {"FOO": "hello"}
512
513m2 = Model()
514assert m2.model_dump() == {"foo": None}
515assert m2.model_dump(exclude_unset=True) == {}
516assert m2.model_dump(exclude_none=True) == {}
517assert m2.model_dump(exclude_defaults=True) == {}
518
519m3 = Model(FOO=None)
520assert m3.model_dump() == {"foo": None}
521assert m3.model_dump(exclude_none=True) == {}
522
523if not PYDANTIC_V2:
524with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"):
525m.model_dump(mode="json")
526
527with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"):
528m.model_dump(round_trip=True)
529
530with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"):
531m.model_dump(warnings=False)
532
533
534def test_forwards_compat_model_dump_json_method() -> None:
535class Model(BaseModel):
536foo: Optional[str] = Field(alias="FOO", default=None)
537
538m = Model(FOO="hello")
539assert json.loads(m.model_dump_json()) == {"foo": "hello"}
540assert json.loads(m.model_dump_json(include={"bar"})) == {}
541assert json.loads(m.model_dump_json(include={"foo"})) == {"foo": "hello"}
542assert json.loads(m.model_dump_json(by_alias=True)) == {"FOO": "hello"}
543
544assert m.model_dump_json(indent=2) == '{\n "foo": "hello"\n}'
545
546m2 = Model()
547assert json.loads(m2.model_dump_json()) == {"foo": None}
548assert json.loads(m2.model_dump_json(exclude_unset=True)) == {}
549assert json.loads(m2.model_dump_json(exclude_none=True)) == {}
550assert json.loads(m2.model_dump_json(exclude_defaults=True)) == {}
551
552m3 = Model(FOO=None)
553assert json.loads(m3.model_dump_json()) == {"foo": None}
554assert json.loads(m3.model_dump_json(exclude_none=True)) == {}
555
556if not PYDANTIC_V2:
557with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"):
558m.model_dump_json(round_trip=True)
559
560with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"):
561m.model_dump_json(warnings=False)
562
563
564def test_type_compat() -> None:
565# our model type can be assigned to Pydantic's model type
566
567def takes_pydantic(model: pydantic.BaseModel) -> None: # noqa: ARG001
568...
569
570class OurModel(BaseModel):
571foo: Optional[str] = None
572
573takes_pydantic(OurModel())