mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
better str and repr for ModelField (#912)
* better str and repr for ModelField, fix #505 * better type display, fix tests * correct _type_display signature * fix for python3.6 differences * fix PyObjectStr * fix coverage
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Better `str`/`repr` logic for `ModelField`
|
||||
+23
-2
@@ -24,7 +24,7 @@ from .error_wrappers import ErrorWrapper
|
||||
from .errors import NoneIsNotAllowedError
|
||||
from .types import Json, JsonWrapper
|
||||
from .typing import AnyType, Callable, ForwardRef, display_as_type, is_literal_type
|
||||
from .utils import Representation, lenient_issubclass, sequence_like
|
||||
from .utils import PyObjectStr, Representation, lenient_issubclass, sequence_like
|
||||
from .validators import constant_validator, dict_validator, find_validators, validate_json
|
||||
|
||||
try:
|
||||
@@ -170,6 +170,13 @@ SHAPE_TUPLE = 5
|
||||
SHAPE_TUPLE_ELLIPSIS = 6
|
||||
SHAPE_SEQUENCE = 7
|
||||
SHAPE_FROZENSET = 8
|
||||
SHAPE_NAME_LOOKUP = {
|
||||
SHAPE_LIST: 'List[{}]',
|
||||
SHAPE_SET: 'Set[{}]',
|
||||
SHAPE_TUPLE_ELLIPSIS: 'Tuple[{}, ...]',
|
||||
SHAPE_SEQUENCE: 'Sequence[{}]',
|
||||
SHAPE_FROZENSET: 'FrozenSet[{}]',
|
||||
}
|
||||
|
||||
|
||||
class ModelField(Representation):
|
||||
@@ -600,8 +607,22 @@ class ModelField(Representation):
|
||||
or hasattr(self.type_, '__pydantic_model__') # pydantic dataclass
|
||||
)
|
||||
|
||||
def _type_display(self) -> PyObjectStr:
|
||||
t = display_as_type(self.type_)
|
||||
|
||||
if self.shape == SHAPE_MAPPING:
|
||||
t = f'Mapping[{display_as_type(self.key_field.type_)}, {t}]' # type: ignore
|
||||
elif self.shape == SHAPE_TUPLE:
|
||||
t = 'Tuple[{}]'.format(', '.join(display_as_type(f.type_) for f in self.sub_fields)) # type: ignore
|
||||
elif self.shape != SHAPE_SINGLETON:
|
||||
t = SHAPE_NAME_LOOKUP[self.shape].format(t)
|
||||
|
||||
if self.allow_none and (self.shape != SHAPE_SINGLETON or not self.sub_fields):
|
||||
t = f'Optional[{t}]'
|
||||
return PyObjectStr(t)
|
||||
|
||||
def __repr_args__(self) -> 'ReprArgs':
|
||||
args = [('name', self.name), ('type', display_as_type(self.type_)), ('required', self.required)]
|
||||
args = [('name', self.name), ('type', self._type_display()), ('required', self.required)]
|
||||
|
||||
if not self.required:
|
||||
args.append(('default', self.default))
|
||||
|
||||
+2
-2
@@ -113,8 +113,8 @@ def display_as_type(v: AnyType) -> str:
|
||||
try:
|
||||
return v.__name__
|
||||
except AttributeError:
|
||||
# happens with unions
|
||||
return str(v)
|
||||
# happens with typing objects
|
||||
return str(v).replace('typing.', '')
|
||||
|
||||
|
||||
def resolve_annotations(raw_annotations: Dict[str, AnyType], module_name: Optional[str]) -> Dict[str, AnyType]:
|
||||
|
||||
@@ -118,6 +118,16 @@ def almost_equal_floats(value_1: float, value_2: float, *, delta: float = 1e-8)
|
||||
return abs(value_1 - value_2) <= delta
|
||||
|
||||
|
||||
class PyObjectStr(str):
|
||||
"""
|
||||
String class where repr doesn't include quotes. Useful with Representation when you want to return a string
|
||||
representation of something that valid (or pseudo-valid) python.
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return str(self)
|
||||
|
||||
|
||||
class Representation:
|
||||
"""
|
||||
Mixin to provide __str__, __repr__, and __pretty__ methods. See #884 for more details.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union
|
||||
@@ -27,7 +28,7 @@ def test_str_bytes():
|
||||
|
||||
m = Model(v='s')
|
||||
assert m.v == 's'
|
||||
assert repr(m.__fields__['v']) == "ModelField(name='v', type='typing.Union[str, bytes]', required=True)"
|
||||
assert repr(m.__fields__['v']) == "ModelField(name='v', type=Union[str, bytes], required=True)"
|
||||
|
||||
m = Model(v=b'b')
|
||||
assert m.v == 'b'
|
||||
@@ -325,7 +326,7 @@ def test_infer_alias():
|
||||
|
||||
assert Model(_a='different').a == 'different'
|
||||
assert repr(Model.__fields__['a']) == (
|
||||
"ModelField(name='a', type='str', required=False, default='foobar', alias='_a')"
|
||||
"ModelField(name='a', type=str, required=False, default='foobar', alias='_a')"
|
||||
)
|
||||
|
||||
|
||||
@@ -1119,3 +1120,35 @@ def test_alias_generator_parent():
|
||||
|
||||
assert Child.__fields__['y'].alias == 'y2'
|
||||
assert Child.__fields__['x'].alias == 'x2'
|
||||
|
||||
|
||||
def test_field_str_shape():
|
||||
class Model(BaseModel):
|
||||
a: List[int]
|
||||
|
||||
assert repr(Model.__fields__['a']) == "ModelField(name='a', type=List[int], required=True)"
|
||||
assert str(Model.__fields__['a']) == "name='a' type=List[int] required=True"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason='output slightly different for 3.6')
|
||||
@pytest.mark.parametrize(
|
||||
'type_,expected',
|
||||
[
|
||||
(int, 'int'),
|
||||
(Optional[int], 'Optional[int]'),
|
||||
(Union[None, int, str], 'Union[NoneType, int, str]'),
|
||||
(Union[int, str, bytes], 'Union[int, str, bytes]'),
|
||||
(List[int], 'List[int]'),
|
||||
(Tuple[int, str, bytes], 'Tuple[int, str, bytes]'),
|
||||
(Union[List[int], Set[bytes]], 'Union[List[int], Set[bytes]]'),
|
||||
(List[Tuple[int, int]], 'List[Tuple[int, int]]'),
|
||||
(Dict[int, str], 'Mapping[int, str]'),
|
||||
(Tuple[int, ...], 'Tuple[int, ...]'),
|
||||
(Optional[List[int]], 'Optional[List[int]]'),
|
||||
],
|
||||
)
|
||||
def test_field_type_display(type_, expected):
|
||||
class Model(BaseModel):
|
||||
a: type_
|
||||
|
||||
assert Model.__fields__['a']._type_display() == expected
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
from typing import Dict, List, Optional, Union
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
@@ -63,6 +64,7 @@ def test_interval_validation_error():
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason='output slightly different for 3.6')
|
||||
def test_error_on_optional():
|
||||
class Foobar(BaseModel):
|
||||
foo: Optional[str] = None
|
||||
@@ -74,10 +76,7 @@ def test_error_on_optional():
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Foobar(foo='x')
|
||||
assert exc_info.value.errors() == [{'loc': ('foo',), 'msg': 'custom error', 'type': 'value_error'}]
|
||||
assert repr(exc_info.value.raw_errors[0]) in (
|
||||
"ErrorWrapper(exc=ValueError('custom error'), loc=('foo',))", # python 3.7
|
||||
"ErrorWrapper(exc=ValueError('custom error',), loc=('foo',))", # python 3.6
|
||||
), repr(exc_info.value.raw_errors[0])
|
||||
assert repr(exc_info.value.raw_errors[0]) == "ErrorWrapper(exc=ValueError('custom error'), loc=('foo',))"
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Foobar(foo=None)
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@ def test_ultra_simple_repr():
|
||||
m = UltraSimpleModel(a=10.2)
|
||||
assert str(m) == 'a=10.2 b=10'
|
||||
assert repr(m) == 'UltraSimpleModel(a=10.2, b=10)'
|
||||
assert repr(m.__fields__['a']) == "ModelField(name='a', type='float', required=True)"
|
||||
assert repr(m.__fields__['b']) == "ModelField(name='b', type='int', required=False, default=10)"
|
||||
assert repr(m.__fields__['a']) == "ModelField(name='a', type=float, required=True)"
|
||||
assert repr(m.__fields__['b']) == "ModelField(name='b', type=int, required=False, default=10)"
|
||||
assert dict(m) == {'a': 10.2, 'b': 10}
|
||||
assert m.dict() == {'a': 10.2, 'b': 10}
|
||||
assert m.json() == '{"a": 10.2, "b": 10}'
|
||||
|
||||
+1
-3
@@ -32,9 +32,7 @@ def test_import_no_attr():
|
||||
assert exc_info.value.args[0] == 'Module "os" does not define a "foobar" attribute'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value,expected', ((str, 'str'), ('string', 'str'), (Union[str, int], 'typing.Union[str, int]'))
|
||||
)
|
||||
@pytest.mark.parametrize('value,expected', ((str, 'str'), ('string', 'str'), (Union[str, int], 'Union[str, int]')))
|
||||
def test_display_as_type(value, expected):
|
||||
assert display_as_type(value) == expected
|
||||
|
||||
|
||||
Reference in New Issue
Block a user