From 5efa54d80d8a584e4c369e2ccdf30d723e54db3b Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 8 Jul 2017 18:50:59 +0100 Subject: [PATCH] annotation only fields first --- HISTORY.rst | 1 + benchmarks/test_pydantic.py | 18 +++++++++--------- docs/index.rst | 6 +++--- pydantic/main.py | 35 +++++++++++++++++------------------ tests/test_complex.py | 10 +++++----- tests/test_main.py | 2 +- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 242983f..21dcd4d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,7 @@ v0.4.0 (2017-XX-XX) * simplify error display * use unicode ellipsis in ``truncate`` * add ``parse_obj``, ``parse_raw`` and ``parse_file`` helper functions #58 +* switch annotation only fields to come first in fields list not last v0.3.0 (2017-06-21) ................... diff --git a/benchmarks/test_pydantic.py b/benchmarks/test_pydantic.py index 5e3dc21..0c122eb 100644 --- a/benchmarks/test_pydantic.py +++ b/benchmarks/test_pydantic.py @@ -10,9 +10,9 @@ class TestPydantic: def __init__(self, allow_extra): class Model(BaseModel): - id: int = ... - client_name: constr(max_length=255) = ... - sort_index: float = ... + id: int + client_name: constr(max_length=255) + sort_index: float # client_email: EmailStr = None client_phone: constr(max_length=255) = None @@ -23,15 +23,15 @@ class TestPydantic: contractor: PositiveInt = None upstream_http_referrer: constr(max_length=1023) = None - grecaptcha_response: constr(min_length=20, max_length=1000) = ... + grecaptcha_response: constr(min_length=20, max_length=1000) last_updated: datetime = None class Skill(BaseModel): - subject: str = ... - subject_id: int = ... - category: str = ... - qual_level: str = ... - qual_level_id: int = ... + subject: str + subject_id: int + category: str + qual_level: str + qual_level_id: int qual_level_ranking: float = 0 skills: List[Skill] = [] diff --git a/docs/index.rst b/docs/index.rst index a0ba1dc..aeee754 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -234,7 +234,7 @@ The ellipsis notation ``...`` will not work with mypy, you need to use annotatio .. 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 last, but still in the order they were defined. + 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. @@ -247,8 +247,8 @@ a model. .. warning:: - However be warned: immutability in python is never strict. If developers are determined/stupid they can always - modify a so called "immutable" object + Immutability in python is never strict. If developers are determined/stupid they can always + modify a so-called "immutable" object. .. literalinclude:: examples/mutation.py diff --git a/pydantic/main.py b/pydantic/main.py index c3700c1..3b22bc6 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -48,29 +48,18 @@ class MetaModel(type): fields.update(base.__fields__) config = inherit_config(base.config, config) - annotations = namespace.get('__annotations__') config = inherit_config(namespace.get('Config'), config) - class_validators = {n: f for n, f in namespace.items() - if n.startswith('validate_') and isinstance(f, FunctionType)} + class_validators = { + n: f for n, f in namespace.items() if n.startswith('validate_') and isinstance(f, FunctionType) + } for f in fields.values(): f.set_config(config) - for var_name, value in namespace.items(): - if var_name.startswith('_') or isinstance(value, TYPE_BLACKLIST): - continue - fields[var_name] = Field.infer( - name=var_name, - value=value, - annotation=annotations.pop(var_name, None) if annotations else None, - class_validators=class_validators, - config=config, - ) - - if annotations: - for ann_name, ann_type in annotations.items(): - if ann_name.startswith('_'): - continue + annotations = namespace.get('__annotations__', {}) + # annotation only fields need to come first in fields + for ann_name, ann_type in annotations.items(): + if not ann_name.startswith('_') and ann_name not in namespace: fields[ann_name] = Field.infer( name=ann_name, value=..., @@ -79,6 +68,16 @@ class MetaModel(type): config=config, ) + for var_name, value in namespace.items(): + if not var_name.startswith('_') and not isinstance(value, TYPE_BLACKLIST): + fields[var_name] = Field.infer( + name=var_name, + value=value, + annotation=annotations.get(var_name), + class_validators=class_validators, + config=config, + ) + new_namespace = { 'config': config, '__fields__': fields, diff --git a/tests/test_complex.py b/tests/test_complex.py index 70524bf..8cd7af0 100644 --- a/tests/test_complex.py +++ b/tests/test_complex.py @@ -397,16 +397,16 @@ _a: def test_annotation_config(): class Model(BaseModel): - a: float - b: int = 10 + b: float + a: int = 10 _c: str class Config: - fields = {'a': 'foobar'} + fields = {'b': 'foobar'} assert list(Model.__fields__.keys()) == ['b', 'a'] - assert [f.alias for f in Model.__fields__.values()] == ['b', 'foobar'] - assert Model(foobar='123').a == 123.0 + assert [f.alias for f in Model.__fields__.values()] == ['foobar', 'a'] + assert Model(foobar='123').b == 123.0 def test_success_values_include(): diff --git a/tests/test_main.py b/tests/test_main.py index baa96be..7db6d66 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -285,7 +285,7 @@ def test_field_order(): d: dict = {} # fields are ordered as defined except annotation-only fields come last - assert list(Model.__fields__.keys()) == ['b', 'd', 'c', 'a'] + assert list(Model.__fields__.keys()) == ['c', 'a', 'b', 'd'] def test_required():