Files
pydantic/tests/test_model_signature.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

189 lines
5.6 KiB
Python

import sys
from inspect import Parameter, Signature, signature
from typing import Any, Iterable, Optional, Union
import pytest
from typing_extensions import Annotated
from pydantic import BaseModel, Extra, Field, create_model
from pydantic._internal._typing_extra import is_annotated
def _equals(a: Union[str, Iterable[str]], b: Union[str, Iterable[str]]) -> bool:
"""
compare strings with spaces removed
"""
if isinstance(a, str) and isinstance(b, str):
return a.replace(' ', '') == b.replace(' ', '')
elif isinstance(a, Iterable) and isinstance(b, Iterable):
return all(_equals(a_, b_) for a_, b_ in zip(a, b))
else:
raise TypeError(f'arguments must be both strings or both lists, not {type(a)}, {type(b)}')
def test_model_signature():
class Model(BaseModel):
a: float = Field(..., title='A')
b: int = Field(10)
sig = signature(Model)
assert sig != signature(BaseModel)
assert _equals(map(str, sig.parameters.values()), ('a: float', 'b: int = 10'))
assert _equals(str(sig), '(*, a: float, b: int = 10) -> None')
def test_custom_init_signature():
class MyModel(BaseModel):
id: int
name: str = 'John Doe'
f__: str = Field(..., alias='foo')
class Config:
extra = Extra.allow
def __init__(self, id: int = 1, bar=2, *, baz: Any, **data):
super().__init__(id=id, **data)
self.bar = bar
self.baz = baz
sig = signature(MyModel)
assert _equals(
map(str, sig.parameters.values()),
('id: int = 1', 'bar=2', 'baz: Any', "name: str = 'John Doe'", 'foo: str', '**data'),
)
assert _equals(str(sig), "(id: int = 1, bar=2, *, baz: Any, name: str = 'John Doe', foo: str, **data) -> None")
def test_custom_init_signature_with_no_var_kw():
class Model(BaseModel):
a: float
b: int = 2
c: int
def __init__(self, a: float, b: int):
super().__init__(a=a, b=b, c=1)
class Config:
extra = Extra.allow
assert _equals(str(signature(Model)), '(a: float, b: int) -> None')
@pytest.mark.xfail(reason='TODO create_model')
def test_invalid_identifiers_signature():
model = create_model(
'Model', **{'123 invalid identifier!': Field(123, alias='valid_identifier'), '!': Field(0, alias='yeah')}
)
assert _equals(str(signature(model)), '(*, valid_identifier: int = 123, yeah: int = 0) -> None')
model = create_model('Model', **{'123 invalid identifier!': 123, '!': Field(0, alias='yeah')})
assert _equals(str(signature(model)), '(*, yeah: int = 0, **extra_data: Any) -> None')
def test_use_field_name():
class Foo(BaseModel):
foo: str = Field(..., alias='this is invalid')
class Config:
allow_population_by_field_name = True
assert _equals(str(signature(Foo)), '(*, foo: str) -> None')
def test_does_not_use_reserved_word():
class Foo(BaseModel):
from_: str = Field(..., alias='from')
class Config:
allow_population_by_field_name = True
assert _equals(str(signature(Foo)), '(*, from_: str) -> None')
def test_extra_allow_no_conflict():
class Model(BaseModel):
spam: str
class Config:
extra = Extra.allow
assert _equals(str(signature(Model)), '(*, spam: str, **extra_data: Any) -> None')
def test_extra_allow_conflict():
class Model(BaseModel):
extra_data: str
class Config:
extra = Extra.allow
assert _equals(str(signature(Model)), '(*, extra_data: str, **extra_data_: Any) -> None')
def test_extra_allow_conflict_twice():
class Model(BaseModel):
extra_data: str
extra_data_: str
class Config:
extra = Extra.allow
assert _equals(str(signature(Model)), '(*, extra_data: str, extra_data_: str, **extra_data__: Any) -> None')
def test_extra_allow_conflict_custom_signature():
class Model(BaseModel):
extra_data: int
def __init__(self, extra_data: int = 1, **foobar: Any):
super().__init__(extra_data=extra_data, **foobar)
class Config:
extra = Extra.allow
assert _equals(str(signature(Model)), '(extra_data: int = 1, **foobar: Any) -> None')
def test_signature_is_class_only():
class Model(BaseModel):
foo: int = 123
def __call__(self, a: int) -> bool:
pass
assert _equals(str(signature(Model)), '(*, foo: int = 123) -> None')
assert _equals(str(signature(Model())), '(a: int) -> bool')
assert not hasattr(Model(), '__signature__')
def test_optional_field():
class Model(BaseModel):
foo: Optional[int] = None
assert signature(Model) == Signature(
[Parameter('foo', Parameter.KEYWORD_ONLY, default=None, annotation=Optional[int])], return_annotation=None
)
@pytest.mark.skipif(sys.version_info < (3, 10), reason='repr different on older versions')
def test_annotated_field():
from annotated_types import Gt
class Model(BaseModel):
foo: Annotated[int, Gt(1)] = 1
sig = signature(Model)
assert str(sig) == '(*, foo: typing.Annotated[int, Gt(gt=1)] = 1) -> None'
# check that the `Annotated` we created is a valid `Annotated`
assert is_annotated(sig.parameters['foo'].annotation)
@pytest.mark.skipif(sys.version_info < (3, 10), reason='repr different on older versions')
def test_annotated_optional_field():
from annotated_types import Gt
class Model(BaseModel):
foo: Annotated[Optional[int], Gt(1)] = None
assert str(signature(Model)) == '(*, foo: Annotated[Optional[int], Gt(gt=1)] = None) -> None'