openai/openai-python

Public

mirrored fromhttps://github.com/openai/openai-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.0.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/test_models.py

573lines · modecode

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