Files
pydantic/tests/test_json.py
T
Samuel Colvin 594effa279 Switching to pydantic_core (#4516)
* working on core schema generation

* adapting main.py

* getting tests to run

* fix tests

* disable pyright, fix mypy

* moving to class-based model generation

* working on validators

* change how models are created

* start fixing test_main.py

* fixing mypy

* SelfType

* recursive models working, more tests fixed

* fix tests on <3.10

* get docs build to pass

* starting to cleanup types.py

* starting works on custom types

* working on using annotated-types

* using annoated types for constraints

* lots of cleanup, fixing network tests

* network tests passing 🎉

* working on types

* working on types and cleanup

* fixing UUID type, restructing again

* more types and newer pydantic-core

* working on Iterable

* more test_types tests

* support newer pydantic-core, fixing more test_types.py

* working through more test_types.py

* test_types.py at last passing locally 🎉

* fixing more tests in test_types.py

* fix datetime_parse tests and linting

* get tests running again, rename to test_datetime.py

* renaming internal modules

* working through mypy errors

* fixing mypy

* refactoring _generate_schema.py

* test_main.py passing

* uprev deps

* fix conftest and linting?

* importing Annotated

* ltining

* import Annotated from typing_extensions

* fixing 3.7 compatibility

* fixing tests on 3.9

* fix linting

* fixing SecretField and 3.9 tests

* customising get_type_hints

* ignore warnings on 3.11

* spliting repr out of utils

* removing unused bits of _repr, fix tests for 3.7

* more cleanup, removing many type aliases

* clean up repr

* support namedtuples and typeddicts

* test is_union

* removing errors, uprev pydantic-core

* fix tests on 3.8

* fixing private attributes and model_post_init

* renaming and cleanup

* remove unnecessary PydanticMetadata inheritance

* fixing forward refs and mypy tests

* fix signatures, change how xfail works

* revert mypy tests to 3.7 syntax

* correct model title

* try to fix tests

* fixing ClassVar forward refs

* uprev pydantic-core, new error format

* add "force" argument to model_rebuild

* Apply suggestions from code review

Suggestions from @tiangolo and @hramezani 🙏

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>

* more suggestions from @tiangolo

* extra -> json_schema_extra on Field

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2022-11-02 12:01:17 +00:00

377 lines
12 KiB
Python

import datetime
import json
import re
import sys
from dataclasses import dataclass as vanilla_dataclass
from decimal import Decimal
from enum import Enum
from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
from pathlib import Path
from typing import List, Optional
from uuid import UUID
import pytest
from pydantic import BaseModel, NameEmail, create_model
from pydantic.color import Color
from pydantic.dataclasses import dataclass as pydantic_dataclass
from pydantic.json import pydantic_encoder, timedelta_isoformat
from pydantic.types import DirectoryPath, FilePath, SecretBytes, SecretStr
pytestmark = pytest.mark.xfail(reason='working on V2', strict=False)
class MyEnum(Enum):
foo = 'bar'
snap = 'crackle'
@pytest.mark.parametrize(
'gen_input,output',
[
(lambda: UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'), '"ebcdab58-6eb8-46fb-a190-d07a33e9eac8"'),
(lambda: IPv4Address('192.168.0.1'), '"192.168.0.1"'),
(lambda: Color('#000'), '"black"'),
(lambda: Color((1, 12, 123)), '"#010c7b"'),
(lambda: SecretStr('abcd'), '"**********"'),
(lambda: SecretStr(''), '""'),
(lambda: SecretBytes(b'xyz'), '"**********"'),
(lambda: SecretBytes(b''), '""'),
(lambda: NameEmail('foo bar', 'foobaR@example.com'), '"foo bar <foobaR@example.com>"'),
(lambda: IPv6Address('::1:0:1'), '"::1:0:1"'),
(lambda: IPv4Interface('192.168.0.0/24'), '"192.168.0.0/24"'),
(lambda: IPv6Interface('2001:db00::/120'), '"2001:db00::/120"'),
(lambda: IPv4Network('192.168.0.0/24'), '"192.168.0.0/24"'),
(lambda: IPv6Network('2001:db00::/120'), '"2001:db00::/120"'),
(lambda: datetime.datetime(2032, 1, 1, 1, 1), '"2032-01-01T01:01:00"'),
(lambda: datetime.datetime(2032, 1, 1, 1, 1, tzinfo=datetime.timezone.utc), '"2032-01-01T01:01:00+00:00"'),
(lambda: datetime.datetime(2032, 1, 1), '"2032-01-01T00:00:00"'),
(lambda: datetime.time(12, 34, 56), '"12:34:56"'),
(lambda: datetime.timedelta(days=12, seconds=34, microseconds=56), '1036834.000056'),
(lambda: datetime.timedelta(seconds=-1), '-1.0'),
(lambda: {1, 2, 3}, '[1, 2, 3]'),
(lambda: frozenset([1, 2, 3]), '[1, 2, 3]'),
(lambda: (v for v in range(4)), '[0, 1, 2, 3]'),
(lambda: b'this is bytes', '"this is bytes"'),
(lambda: Decimal('12.34'), '12.34'),
(lambda: create_model('BarModel', a='b', c='d')(), '{"a": "b", "c": "d"}'),
(lambda: MyEnum.foo, '"bar"'),
(lambda: re.compile('^regex$'), '"^regex$"'),
],
)
def test_encoding(gen_input, output):
assert output == json.dumps(gen_input(), default=pydantic_encoder)
@pytest.mark.skipif(sys.platform.startswith('win'), reason='paths look different on windows')
def test_path_encoding(tmpdir):
class PathModel(BaseModel):
path: Path
file_path: FilePath
dir_path: DirectoryPath
tmpdir = Path(tmpdir)
file_path = tmpdir / 'bar'
file_path.touch()
dir_path = tmpdir / 'baz'
dir_path.mkdir()
model = PathModel(path=Path('/path/test/example/'), file_path=file_path, dir_path=dir_path)
expected = f'{{"path": "/path/test/example", "file_path": "{file_path}", "dir_path": "{dir_path}"}}'
assert json.dumps(model, default=pydantic_encoder) == expected
def test_model_encoding():
class ModelA(BaseModel):
x: int
y: str
class Model(BaseModel):
a: float
b: bytes
c: Decimal
d: ModelA
m = Model(a=10.2, b='foobar', c=10.2, d={'x': 123, 'y': '123'})
assert m.dict() == {'a': 10.2, 'b': b'foobar', 'c': Decimal('10.2'), 'd': {'x': 123, 'y': '123'}}
assert m.json() == '{"a": 10.2, "b": "foobar", "c": 10.2, "d": {"x": 123, "y": "123"}}'
assert m.json(exclude={'b'}) == '{"a": 10.2, "c": 10.2, "d": {"x": 123, "y": "123"}}'
def test_subclass_encoding():
class SubDate(datetime.datetime):
pass
class Model(BaseModel):
a: datetime.datetime
b: SubDate
m = Model(a=datetime.datetime(2032, 1, 1, 1, 1), b=SubDate(2020, 2, 29, 12, 30))
assert m.dict() == {'a': datetime.datetime(2032, 1, 1, 1, 1), 'b': SubDate(2020, 2, 29, 12, 30)}
assert m.json() == '{"a": "2032-01-01T01:01:00", "b": "2020-02-29T12:30:00"}'
def test_subclass_custom_encoding():
class SubDate(datetime.datetime):
pass
class SubDelta(datetime.timedelta):
pass
class Model(BaseModel):
a: SubDate
b: SubDelta
class Config:
json_encoders = {
datetime.datetime: lambda v: v.strftime('%a, %d %b %C %H:%M:%S'),
datetime.timedelta: timedelta_isoformat,
}
m = Model(a=SubDate(2032, 1, 1, 1, 1), b=SubDelta(hours=100))
assert m.dict() == {'a': SubDate(2032, 1, 1, 1, 1), 'b': SubDelta(days=4, seconds=14400)}
assert m.json() == '{"a": "Thu, 01 Jan 20 01:01:00", "b": "P4DT4H0M0.000000S"}'
def test_invalid_model():
class Foo:
pass
with pytest.raises(TypeError):
json.dumps(Foo, default=pydantic_encoder)
@pytest.mark.parametrize(
'input,output',
[
(datetime.timedelta(days=12, seconds=34, microseconds=56), 'P12DT0H0M34.000056S'),
(datetime.timedelta(days=1001, hours=1, minutes=2, seconds=3, microseconds=654_321), 'P1001DT1H2M3.654321S'),
(datetime.timedelta(seconds=-1), '-P1DT23H59M59.000000S'),
(datetime.timedelta(), 'P0DT0H0M0.000000S'),
],
)
def test_iso_timedelta(input, output):
assert output == timedelta_isoformat(input)
def test_custom_encoder():
class Model(BaseModel):
x: datetime.timedelta
y: Decimal
z: datetime.date
class Config:
json_encoders = {datetime.timedelta: lambda v: f'{v.total_seconds():0.3f}s', Decimal: lambda v: 'a decimal'}
assert Model(x=123, y=5, z='2032-06-01').json() == '{"x": "123.000s", "y": "a decimal", "z": "2032-06-01"}'
def test_custom_iso_timedelta():
class Model(BaseModel):
x: datetime.timedelta
class Config:
json_encoders = {datetime.timedelta: timedelta_isoformat}
m = Model(x=123)
assert m.json() == '{"x": "P0DT0H2M3.000000S"}'
# def test_con_decimal_encode() -> None:
# """
# Makes sure a decimal with decimal_places = 0, as well as one with places
# can handle a encode/decode roundtrip.
# """
#
# class Id(ConstrainedDecimal):
# max_digits = 22
# decimal_places = 0
# ge = 0
#
# class Obj(BaseModel):
# id: Id
# price: Decimal = Decimal('0.01')
#
# assert Obj(id=1).json() == '{"id": 1, "price": 0.01}'
# assert Obj.parse_raw('{"id": 1, "price": 0.01}') == Obj(id=1)
def test_json_encoder_simple_inheritance():
class Parent(BaseModel):
dt: datetime.datetime = datetime.datetime.now()
timedt: datetime.timedelta = datetime.timedelta(hours=100)
class Config:
json_encoders = {datetime.datetime: lambda _: 'parent_encoder'}
class Child(Parent):
class Config:
json_encoders = {datetime.timedelta: lambda _: 'child_encoder'}
assert Child().json() == '{"dt": "parent_encoder", "timedt": "child_encoder"}'
def test_json_encoder_inheritance_override():
class Parent(BaseModel):
dt: datetime.datetime = datetime.datetime.now()
class Config:
json_encoders = {datetime.datetime: lambda _: 'parent_encoder'}
class Child(Parent):
class Config:
json_encoders = {datetime.datetime: lambda _: 'child_encoder'}
assert Child().json() == '{"dt": "child_encoder"}'
def test_custom_encoder_arg():
class Model(BaseModel):
x: datetime.timedelta
m = Model(x=123)
assert m.json() == '{"x": 123.0}'
assert m.json(encoder=lambda v: '__default__') == '{"x": "__default__"}'
def test_encode_dataclass():
@vanilla_dataclass
class Foo:
bar: int
spam: str
f = Foo(bar=123, spam='apple pie')
assert '{"bar": 123, "spam": "apple pie"}' == json.dumps(f, default=pydantic_encoder)
def test_encode_pydantic_dataclass():
@pydantic_dataclass
class Foo:
bar: int
spam: str
f = Foo(bar=123, spam='apple pie')
assert '{"bar": 123, "spam": "apple pie"}' == json.dumps(f, default=pydantic_encoder)
def test_encode_custom_root():
class Model(BaseModel):
__root__: List[str]
assert Model(__root__=['a', 'b']).json() == '["a", "b"]'
def test_custom_decode_encode():
load_calls, dump_calls = 0, 0
def custom_loads(s):
nonlocal load_calls
load_calls += 1
return json.loads(s.strip('$'))
def custom_dumps(s, default=None, **kwargs):
nonlocal dump_calls
dump_calls += 1
return json.dumps(s, default=default, indent=2)
class Model(BaseModel):
a: int
b: str
class Config:
json_loads = custom_loads
json_dumps = custom_dumps
m = Model.parse_raw('${"a": 1, "b": "foo"}$$')
assert m.dict() == {'a': 1, 'b': 'foo'}
assert m.json() == '{\n "a": 1,\n "b": "foo"\n}'
def test_json_nested_encode_models():
class Phone(BaseModel):
manufacturer: str
number: int
class User(BaseModel):
name: str
SSN: int
birthday: datetime.datetime
phone: Phone
friend: Optional['User'] = None # noqa: F821 # https://github.com/PyCQA/pyflakes/issues/567
class Config:
json_encoders = {
datetime.datetime: lambda v: v.timestamp(),
Phone: lambda v: v.number if v else None,
'User': lambda v: v.SSN,
}
User.model_rebuild()
iphone = Phone(manufacturer='Apple', number=18002752273)
galaxy = Phone(manufacturer='Samsung', number=18007267864)
timon = User(
name='Timon', SSN=123, birthday=datetime.datetime(1993, 6, 1, tzinfo=datetime.timezone.utc), phone=iphone
)
pumbaa = User(
name='Pumbaa', SSN=234, birthday=datetime.datetime(1993, 5, 15, tzinfo=datetime.timezone.utc), phone=galaxy
)
timon.friend = pumbaa
assert iphone.json(models_as_dict=False) == '{"manufacturer": "Apple", "number": 18002752273}'
assert (
pumbaa.json(models_as_dict=False)
== '{"name": "Pumbaa", "SSN": 234, "birthday": 737424000.0, "phone": 18007267864, "friend": null}'
)
assert (
timon.json(models_as_dict=False)
== '{"name": "Timon", "SSN": 123, "birthday": 738892800.0, "phone": 18002752273, "friend": 234}'
)
def test_custom_encode_fallback_basemodel():
class MyExoticType:
pass
def custom_encoder(o):
if isinstance(o, MyExoticType):
return 'exo'
raise TypeError('not serialisable')
class Foo(BaseModel):
x: MyExoticType
class Config:
arbitrary_types_allowed = True
class Bar(BaseModel):
foo: Foo
assert Bar(foo=Foo(x=MyExoticType())).json(encoder=custom_encoder) == '{"foo": {"x": "exo"}}'
def test_custom_encode_error():
class MyExoticType:
pass
def custom_encoder(o):
raise TypeError('not serialisable')
class Foo(BaseModel):
x: MyExoticType
class Config:
arbitrary_types_allowed = True
with pytest.raises(TypeError, match='not serialisable'):
Foo(x=MyExoticType()).json(encoder=custom_encoder)
def test_recursive():
class Model(BaseModel):
value: Optional[str]
nested: Optional[BaseModel]
assert Model(value=None, nested=Model(value=None)).json(exclude_none=True) == '{"nested": {}}'