Files
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

520 lines
15 KiB
Python

import collections.abc
import os
import pickle
import sys
from copy import copy, deepcopy
from typing import Callable, Dict, Generic, List, NewType, Tuple, TypeVar, Union
import pytest
from pydantic_core import PydanticCustomError
from typing_extensions import Annotated, Literal
from pydantic import BaseModel
from pydantic._internal import _repr
from pydantic._internal._typing_extra import all_literal_values, get_origin, is_new_type
from pydantic._internal._utils import (
BUILTIN_COLLECTIONS,
ClassAttribute,
LimitedDict,
ValueItems,
all_identical,
deep_update,
get_model,
lenient_issubclass,
smart_deepcopy,
to_lower_camel,
unique_list,
)
from pydantic._internal._validators import import_string
from pydantic.color import Color
from pydantic.dataclasses import dataclass
from pydantic.fields import Undefined
try:
import devtools
except ImportError:
devtools = None
def test_import_module():
assert import_string('os.path') == os.path
def test_import_module_invalid():
with pytest.raises(PydanticCustomError, match='Invalid python path: "xx" doesn\'t look like a module path'):
import_string('xx')
def test_import_no_attr():
with pytest.raises(PydanticCustomError, match='Module "os" does not define a "foobar" attribute'):
import_string('os.foobar')
def foobar(a, b, c=4):
pass
T = TypeVar('T')
class LoggedVar(Generic[T]):
def get(self) -> T:
...
@pytest.mark.parametrize(
'value,expected',
[
(str, 'str'),
('foobar', 'str'),
(Union[str, int], 'Union[str, int]'),
(list, 'list'),
(List, 'List'),
([1, 2, 3], 'list'),
(List[Dict[str, int]], 'List[Dict[str, int]]'),
(Tuple[str, int, float], 'Tuple[str, int, float]'),
(Tuple[str, ...], 'Tuple[str, ...]'),
(Union[int, List[str], Tuple[str, int]], 'Union[int, List[str], Tuple[str, int]]'),
(foobar, 'foobar'),
(LoggedVar, 'LoggedVar'),
(LoggedVar(), 'LoggedVar'),
],
)
def test_display_as_type(value, expected):
assert _repr.display_as_type(value) == expected
@pytest.mark.skipif(sys.version_info < (3, 10), reason='requires python 3.10 or higher')
@pytest.mark.parametrize(
'value_gen,expected',
[
(lambda: str, 'str'),
(lambda: 'string', 'str'),
(lambda: str | int, 'Union[str, int]'),
(lambda: list, 'list'),
(lambda: List, 'List'),
(lambda: list[int], 'list[int]'),
(lambda: List[int], 'List[int]'),
(lambda: list[dict[str, int]], 'list[dict[str, int]]'),
(lambda: list[Union[str, int]], 'list[Union[str, int]]'),
(lambda: list[str | int], 'list[Union[str, int]]'),
(lambda: LoggedVar[int], 'LoggedVar[int]'),
(lambda: LoggedVar[Dict[int, str]], 'LoggedVar[Dict[int, str]]'),
],
)
def test_display_as_type_310(value_gen, expected):
value = value_gen()
assert _repr.display_as_type(value) == expected
def test_lenient_issubclass():
class A(str):
pass
assert lenient_issubclass(A, str) is True
@pytest.mark.skipif(sys.version_info < (3, 9), reason='generic aliases are not available in python < 3.9')
def test_lenient_issubclass_with_generic_aliases():
from collections.abc import Mapping
# should not raise an error here:
assert lenient_issubclass(list[str], Mapping) is False
def test_lenient_issubclass_is_lenient():
assert lenient_issubclass('a', 'a') is False
@pytest.mark.parametrize(
'input_value,output',
[
([], []),
([1, 1, 1, 2, 1, 2, 3, 2, 3, 1, 4, 2, 3, 1], [1, 2, 3, 4]),
(['a', 'a', 'b', 'a', 'b', 'c', 'b', 'c', 'a'], ['a', 'b', 'c']),
],
)
def test_unique_list(input_value, output):
assert unique_list(input_value) == output
assert unique_list(unique_list(input_value)) == unique_list(input_value)
def test_value_items():
v = ['a', 'b', 'c']
vi = ValueItems(v, {0, -1})
assert vi.is_excluded(2)
assert [v_ for i, v_ in enumerate(v) if not vi.is_excluded(i)] == ['b']
assert vi.is_included(2)
assert [v_ for i, v_ in enumerate(v) if vi.is_included(i)] == ['a', 'c']
v2 = {'a': v, 'b': {'a': 1, 'b': (1, 2)}, 'c': 1}
vi = ValueItems(v2, {'a': {0, -1}, 'b': {'a': ..., 'b': -1}})
assert not vi.is_excluded('a')
assert vi.is_included('a')
assert not vi.is_excluded('c')
assert not vi.is_included('c')
assert str(vi) == "{'a': {0, -1}, 'b': {'a': Ellipsis, 'b': -1}}"
assert repr(vi) == "ValueItems({'a': {0, -1}, 'b': {'a': Ellipsis, 'b': -1}})"
excluded = {k_: v_ for k_, v_ in v2.items() if not vi.is_excluded(k_)}
assert excluded == {'a': v, 'b': {'a': 1, 'b': (1, 2)}, 'c': 1}
included = {k_: v_ for k_, v_ in v2.items() if vi.is_included(k_)}
assert included == {'a': v, 'b': {'a': 1, 'b': (1, 2)}}
sub_v = included['a']
sub_vi = ValueItems(sub_v, vi.for_element('a'))
assert repr(sub_vi) == 'ValueItems({0: Ellipsis, 2: Ellipsis})'
assert sub_vi.is_excluded(2)
assert [v_ for i, v_ in enumerate(sub_v) if not sub_vi.is_excluded(i)] == ['b']
assert sub_vi.is_included(2)
assert [v_ for i, v_ in enumerate(sub_v) if sub_vi.is_included(i)] == ['a', 'c']
@pytest.mark.parametrize(
'base,override,intersect,expected',
[
# Check in default (union) mode
(..., ..., False, ...),
(None, None, False, None),
({}, {}, False, {}),
(..., None, False, ...),
(None, ..., False, ...),
(None, {}, False, {}),
({}, None, False, {}),
(..., {}, False, {}),
({}, ..., False, ...),
({'a': None}, {'a': None}, False, {}),
({'a'}, ..., False, ...),
({'a'}, {}, False, {'a': ...}),
({'a'}, {'b'}, False, {'a': ..., 'b': ...}),
({'a': ...}, {'b': {'c'}}, False, {'a': ..., 'b': {'c': ...}}),
({'a': ...}, {'a': {'c'}}, False, {'a': {'c': ...}}),
({'a': {'c': ...}, 'b': {'d'}}, {'a': ...}, False, {'a': ..., 'b': {'d': ...}}),
# Check in intersection mode
(..., ..., True, ...),
(None, None, True, None),
({}, {}, True, {}),
(..., None, True, ...),
(None, ..., True, ...),
(None, {}, True, {}),
({}, None, True, {}),
(..., {}, True, {}),
({}, ..., True, {}),
({'a': None}, {'a': None}, True, {}),
({'a'}, ..., True, {'a': ...}),
({'a'}, {}, True, {}),
({'a'}, {'b'}, True, {}),
({'a': ...}, {'b': {'c'}}, True, {}),
({'a': ...}, {'a': {'c'}}, True, {'a': {'c': ...}}),
({'a': {'c': ...}, 'b': {'d'}}, {'a': ...}, True, {'a': {'c': ...}}),
# Check usage of `True` instead of `...`
(..., True, False, True),
(True, ..., False, ...),
(True, None, False, True),
({'a': {'c': True}, 'b': {'d'}}, {'a': True}, False, {'a': True, 'b': {'d': ...}}),
],
)
def test_value_items_merge(base, override, intersect, expected):
actual = ValueItems.merge(base, override, intersect=intersect)
assert actual == expected
def test_value_items_error():
with pytest.raises(TypeError) as e:
ValueItems(1, (1, 2, 3))
assert str(e.value) == "Unexpected type of exclude value <class 'tuple'>"
def test_is_new_type():
new_type = NewType('new_type', str)
new_new_type = NewType('new_new_type', new_type)
assert is_new_type(new_type)
assert is_new_type(new_new_type)
assert not is_new_type(str)
def test_pretty():
class MyTestModel(BaseModel):
a: int = 1
b: List[int] = [1, 2, 3]
m = MyTestModel()
assert m.__repr_name__() == 'MyTestModel'
assert str(m) == 'a=1 b=[1, 2, 3]'
assert repr(m) == 'MyTestModel(a=1, b=[1, 2, 3])'
assert list(m.__pretty__(lambda x: f'fmt: {x!r}')) == [
'MyTestModel(',
1,
'a=',
'fmt: 1',
',',
0,
'b=',
'fmt: [1, 2, 3]',
',',
0,
-1,
')',
]
def test_pretty_color():
c = Color('red')
assert str(c) == 'red'
assert repr(c) == "Color('red', rgb=(255, 0, 0))"
assert list(c.__pretty__(lambda x: f'fmt: {x!r}')) == [
'Color(',
1,
"fmt: 'red'",
',',
0,
'rgb=',
'fmt: (255, 0, 0)',
',',
0,
-1,
')',
]
@pytest.mark.skipif(not devtools, reason='devtools not installed')
def test_devtools_output():
class MyTestModel(BaseModel):
a: int = 1
b: List[int] = [1, 2, 3]
assert devtools.pformat(MyTestModel()) == 'MyTestModel(\n a=1,\n b=[1, 2, 3],\n)'
@pytest.mark.parametrize(
'mapping, updating_mapping, expected_mapping, msg',
[
(
{'key': {'inner_key': 0}},
{'other_key': 1},
{'key': {'inner_key': 0}, 'other_key': 1},
'extra keys are inserted',
),
(
{'key': {'inner_key': 0}, 'other_key': 1},
{'key': [1, 2, 3]},
{'key': [1, 2, 3], 'other_key': 1},
'values that can not be merged are updated',
),
(
{'key': {'inner_key': 0}},
{'key': {'other_key': 1}},
{'key': {'inner_key': 0, 'other_key': 1}},
'values that have corresponding keys are merged',
),
(
{'key': {'inner_key': {'deep_key': 0}}},
{'key': {'inner_key': {'other_deep_key': 1}}},
{'key': {'inner_key': {'deep_key': 0, 'other_deep_key': 1}}},
'deeply nested values that have corresponding keys are merged',
),
],
)
def test_deep_update(mapping, updating_mapping, expected_mapping, msg):
assert deep_update(mapping, updating_mapping) == expected_mapping, msg
def test_deep_update_is_not_mutating():
mapping = {'key': {'inner_key': {'deep_key': 1}}}
updated_mapping = deep_update(mapping, {'key': {'inner_key': {'other_deep_key': 1}}})
assert updated_mapping == {'key': {'inner_key': {'deep_key': 1, 'other_deep_key': 1}}}
assert mapping == {'key': {'inner_key': {'deep_key': 1}}}
def test_undefined_repr():
assert repr(Undefined) == 'PydanticUndefined'
def test_undefined_copy():
assert copy(Undefined) is Undefined
assert deepcopy(Undefined) is Undefined
@pytest.mark.xfail(reason='not implemented')
def test_get_model():
class A(BaseModel):
a: str
assert get_model(A) == A
@dataclass
class B:
a: str
assert get_model(B) == B.__pydantic_model__
class C:
pass
with pytest.raises(TypeError):
get_model(C)
def test_class_attribute():
class Foo:
attr = ClassAttribute('attr', 'foo')
assert Foo.attr == 'foo'
with pytest.raises(AttributeError, match="'attr' attribute of 'Foo' is class-only"):
Foo().attr
f = Foo()
f.attr = 'not foo'
assert f.attr == 'not foo'
def test_all_literal_values():
L1 = Literal['1']
assert all_literal_values(L1) == ('1',)
L2 = Literal['2']
L12 = Literal[L1, L2]
assert sorted(all_literal_values(L12)) == sorted(('1', '2'))
L312 = Literal['3', Literal[L1, L2]]
assert sorted(all_literal_values(L312)) == sorted(('1', '2', '3'))
@pytest.mark.parametrize(
'obj',
(1, 1.0, '1', b'1', int, None, test_all_literal_values, len, test_all_literal_values.__code__, lambda: ..., ...),
)
def test_smart_deepcopy_immutable_non_sequence(obj, mocker):
# make sure deepcopy is not used
# (other option will be to use obj.copy(), but this will produce error as none of given objects have this method)
mocker.patch('pydantic._internal._utils.deepcopy', side_effect=RuntimeError)
assert smart_deepcopy(obj) is deepcopy(obj) is obj
@pytest.mark.parametrize('empty_collection', (collection() for collection in BUILTIN_COLLECTIONS))
def test_smart_deepcopy_empty_collection(empty_collection, mocker):
mocker.patch('pydantic._internal._utils.deepcopy', side_effect=RuntimeError) # make sure deepcopy is not used
if not isinstance(empty_collection, (tuple, frozenset)): # empty tuple or frozenset are always the same object
assert smart_deepcopy(empty_collection) is not empty_collection
@pytest.mark.parametrize(
'collection', (c.fromkeys((1,)) if issubclass(c, dict) else c((1,)) for c in BUILTIN_COLLECTIONS)
)
def test_smart_deepcopy_collection(collection, mocker):
expected_value = object()
mocker.patch('pydantic._internal._utils.deepcopy', return_value=expected_value)
assert smart_deepcopy(collection) is expected_value
@pytest.mark.parametrize('error', [TypeError, ValueError, RuntimeError])
def test_smart_deepcopy_error(error, mocker):
class RaiseOnBooleanOperation(str):
def __bool__(self):
raise error('raised error')
obj = RaiseOnBooleanOperation()
expected_value = deepcopy(obj)
assert smart_deepcopy(obj) == expected_value
T = TypeVar('T')
@pytest.mark.parametrize(
'input_value,output_value',
[
(Annotated[int, 10] if Annotated else None, Annotated),
(Callable[[], T][int], collections.abc.Callable),
(Dict[str, int], dict),
(List[str], list),
(Union[int, str], Union),
(int, None),
],
)
def test_get_origin(input_value, output_value):
if input_value is None:
pytest.skip('Skipping undefined hint for this python version')
assert get_origin(input_value) is output_value
def test_all_identical():
a, b = object(), object()
c = [b]
assert all_identical([a, b], [a, b]) is True
assert all_identical([a, b], [a, b]) is True
assert all_identical([a, b, b], [a, b, b]) is True
assert all_identical([a, c, b], [a, c, b]) is True
assert all_identical([], [a]) is False, 'Expected iterables with different lengths to evaluate to `False`'
assert all_identical([a], []) is False, 'Expected iterables with different lengths to evaluate to `False`'
assert (
all_identical([a, [b], b], [a, [b], b]) is False
), 'New list objects are different objects and should therefore not be identical.'
def test_undefined_pickle():
undefined2 = pickle.loads(pickle.dumps(Undefined))
assert undefined2 is Undefined
def test_on_lower_camel_zero_length():
assert to_lower_camel('') == ''
def test_on_lower_camel_one_length():
assert to_lower_camel('a') == 'a'
def test_on_lower_camel_many_length():
assert to_lower_camel('i_like_turtles') == 'iLikeTurtles'
def test_limited_dict():
d = LimitedDict(10)
d[1] = '1'
d[2] = '2'
assert list(d.items()) == [(1, '1'), (2, '2')]
for no in '34567890':
d[int(no)] = no
assert list(d.items()) == [
(1, '1'),
(2, '2'),
(3, '3'),
(4, '4'),
(5, '5'),
(6, '6'),
(7, '7'),
(8, '8'),
(9, '9'),
(0, '0'),
]
d[11] = '11'
# reduce size to 9 after setting 11
assert len(d) == 9
assert list(d.items()) == [
(3, '3'),
(4, '4'),
(5, '5'),
(6, '6'),
(7, '7'),
(8, '8'),
(9, '9'),
(0, '0'),
(11, '11'),
]
d[12] = '12'
assert len(d) == 10
d[13] = '13'
assert len(d) == 9