Include all annotated fields in order (#715)

* Include all annotated fields in order

* Update docs and changes

* fix field ordering

* update change info

* fix coverage
This commit is contained in:
dmontagu
2019-08-12 04:05:22 -07:00
committed by Samuel Colvin
parent 82ef45c890
commit 321cde0c88
6 changed files with 35 additions and 19 deletions
+1
View File
@@ -0,0 +1 @@
make all annotated fields occur in the order declared
+6 -5
View File
@@ -204,6 +204,12 @@ A few things to note on validators:
- If validation fails on another field (or that field is missing) it will not be included in ``values``, hence
``if 'password1' in values and ...`` in this example.
.. warning::
Be aware that mixing annotated and non-annotated fields may alter the order of your fields in metadata and errors,
and for validation: annotated fields will always come before non-annotated fields.
(Within each group fields remain in the order they were defined.)
.. note::
@@ -917,11 +923,6 @@ Required Fields and mypy
The ellipsis notation ``...`` will not work with mypy, you need to use annotation only fields as in the example above.
.. warning::
Be aware that using annotation only fields will alter the order of your fields in metadata and errors:
annotation only fields will always come first, but still in the order they were defined.
To get round this you can use the ``Required`` (via ``from pydantic import Required``) field as an alias for
ellipses or annotation only.
+10 -6
View File
@@ -175,29 +175,33 @@ class MetaModel(ABCMeta):
f.prepare()
set_extra(config, name)
annotations = resolve_annotations(namespace.get('__annotations__', {}), namespace.get('__module__', None))
class_vars = set()
if (namespace.get('__module__'), namespace.get('__qualname__')) != ('pydantic.main', 'BaseModel'):
annotations = resolve_annotations(namespace.get('__annotations__', {}), namespace.get('__module__', None))
untouched_types = UNTOUCHED_TYPES + config.keep_untouched
# annotation only fields need to come first in fields
for ann_name, ann_type in annotations.items():
if is_classvar(ann_type):
class_vars.add(ann_name)
elif is_valid_field(ann_name) and ann_name not in namespace:
elif is_valid_field(ann_name):
validate_field_name(bases, ann_name)
value = namespace.get(ann_name, ...)
if isinstance(value, untouched_types) and ann_type != PyObject:
continue
fields[ann_name] = Field.infer(
name=ann_name,
value=...,
value=value,
annotation=ann_type,
class_validators=vg.get_validators(ann_name),
config=config,
)
untouched_types = UNTOUCHED_TYPES + config.keep_untouched
for var_name, value in namespace.items():
if (
is_valid_field(var_name)
and (annotations.get(var_name) == PyObject or not isinstance(value, untouched_types))
var_name not in annotations
and is_valid_field(var_name)
and not isinstance(value, untouched_types)
and var_name not in class_vars
):
validate_field_name(bases, var_name)
+11
View File
@@ -930,3 +930,14 @@ def test_init_inspection():
super().__init__(**data)
Foobar(x=1)
def test_ignored_type():
def foobar():
pass
class Model(BaseModel):
a: int = foobar
b: int
assert Model.__fields__.keys() == {'b'}
+1 -2
View File
@@ -312,8 +312,7 @@ def test_field_order():
a: str
d: dict = {}
# fields are ordered as defined except annotation-only fields come last
assert list(Model.__fields__.keys()) == ['c', 'a', 'b', 'd']
assert list(Model.__fields__.keys()) == ['c', 'b', 'a', 'd']
def test_required():
+6 -6
View File
@@ -6,7 +6,7 @@ from datetime import date, datetime, time, timedelta
from decimal import Decimal
from enum import Enum, IntEnum
from pathlib import Path
from typing import Dict, Iterator, List, NewType, Pattern, Sequence, Set, Tuple
from typing import Dict, Iterator, List, NewType, Optional, Pattern, Sequence, Set, Tuple
from uuid import UUID
import pytest
@@ -207,12 +207,12 @@ def test_constrained_str_too_long():
class DsnModel(BaseModel):
db_name = 'foobar'
db_user = 'postgres'
db_name: Optional[str] = 'foobar'
db_user: Optional[str] = 'postgres'
db_password: str = None
db_host = 'localhost'
db_port = '5432'
db_driver = 'postgres'
db_host: Optional[str] = 'localhost'
db_port: Optional[str] = '5432'
db_driver: str = 'postgres'
db_query: dict = None
dsn: DSN = None