alter the behaviour of dict(model) so that sub-models are nolonger converted to dictionaries (#733)

* fix iteration to not convert to dict by default

* add change

* remove extra newline
This commit is contained in:
Samuel Colvin
2019-08-12 11:31:35 +01:00
committed by GitHub
parent 5837acb288
commit 82ef45c890
9 changed files with 153 additions and 64 deletions
+2
View File
@@ -0,0 +1,2 @@
**Breaking Change:** alter the behaviour of ``dict(model)`` so that sub-models are nolonger
converted to dictionaries
@@ -10,19 +10,6 @@ class FooBarModel(BaseModel):
m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 123})
print(m.dict())
# (returns a dictionary)
# > {'banana': 3.14, 'foo': 'hello', 'bar': {'whatever': 123}}
print(m.dict(include={'foo', 'bar'}))
# > {'foo': 'hello', 'bar': {'whatever': 123}}
print(m.dict(exclude={'foo', 'bar'}))
# > {'banana': 3.14}
print(m.copy())
# > FooBarModel banana=3.14 foo='hello' bar=<BarModel whatever=123>
print(m.copy(include={'foo', 'bar'}))
# > FooBarModel foo='hello' bar=<BarModel whatever=123>
+21
View File
@@ -0,0 +1,21 @@
from pydantic import BaseModel
class BarModel(BaseModel):
whatever: int
class FooBarModel(BaseModel):
banana: float
foo: str
bar: BarModel
m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 123})
print(m.dict())
# (returns a dictionary)
# > {'banana': 3.14, 'foo': 'hello', 'bar': {'whatever': 123}}
print(m.dict(include={'foo', 'bar'}))
# > {'foo': 'hello', 'bar': {'whatever': 123}}
print(m.dict(exclude={'foo', 'bar'}))
# > {'banana': 3.14}
+21
View File
@@ -0,0 +1,21 @@
from pydantic import BaseModel
class BarModel(BaseModel):
whatever: int
class FooBarModel(BaseModel):
banana: float
foo: str
bar: BarModel
m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 123})
print(dict(m))
#> {'banana': 3.14, 'foo': 'hello', 'bar': <BarModel whatever=123>}
for name, value in m:
print(f'{name}: {value}')
#> banana: 3.14
#> foo: hello
#> bar: BarModel whatever=123
@@ -1,12 +1,10 @@
import pickle
from pydantic import BaseModel
class FooBarModel(BaseModel):
a: str
b: int
m = FooBarModel(a='hello', b=123)
print(m)
# > FooBarModel a='hello' b=123
+91 -47
View File
@@ -941,23 +941,102 @@ a model.
Trying to change ``a`` caused an error and it remains unchanged, however the dict ``b`` is mutable and the
immutability of ``foobar`` doesn't stop being changed.
Copying
.......
Exporting Models
................
The ``dict`` function returns a dictionary containing the attributes of a model. Sub-models are recursively
converted to dicts, ``copy`` allows models to be duplicated, this is particularly useful for immutable models.
As well as accessing model attributes directly via their names (eg. ``model.foobar``), models can be converted
and exported in a number of ways:
``model.dict(...)``
~~~~~~~~~~~~~~~~~~~
``dict``, ``copy``, and ``json`` (described :ref:`below <json_dump>`) all take the optional
``include`` and ``exclude`` keyword arguments to control which attributes are returned or copied,
respectively. ``copy`` accepts extra keyword arguments, ``update``, which accepts a ``dict`` mapping attributes
to new values that will be applied as the model is duplicated and ``deep`` to make a deep copy of the model.
The primary way of converting a model to a dictionary. Sub-models will be recursively converted to dictionaries.
``dict`` and ``json`` take the optional ``skip_defaults`` keyword argument which will skip attributes that were
not explicitly set. This is useful to reduce the serialized size of models thats have many default fields that
are not often changed.
Arguments:
.. literalinclude:: examples/copy_dict.py
* ``include``: fields to include in the returned dictionary, see :ref:`below <include_exclude>`
* ``exclude``: fields to exclude from the returned dictionary, see :ref:`below <include_exclude>`
* ``by_alias``: whether field aliases should be used as keys in the returned dictionary, default ``False``
* ``skip_defaults``: whether fields which were not set when creating the model and have their default values should
be excluded from the returned dictionary, default ``False``
Example:
.. literalinclude:: examples/export_dict.py
(This script is complete, it should run "as is")
``dict(model)`` and iteration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*pydantic* models can also be converted to dictionaries using ``dict(model)``, you can also
iterate over a model's field using ``for field_name, value in model:``. Here the raw field values are returned, eg.
sub-models will not be converted to dictionaries.
Example:
.. literalinclude:: examples/export_iterate.py
(This script is complete, it should run "as is")
``model.copy(...)``
~~~~~~~~~~~~~~~~~~~
``copy()`` allows models to be duplicated, this is particularly useful for immutable models.
Arguments:
* ``include``: fields to include in the returned dictionary, see :ref:`below <include_exclude>`
* ``exclude``: fields to exclude from the returned dictionary, see :ref:`below <include_exclude>`
* ``update``: dictionaries of values to change when creating the new model
* ``deep``: whether to make a deep copy of the new model, default ``False``
Example:
.. literalinclude:: examples/export_copy.py
(This script is complete, it should run "as is")
.. _json_dump:
``model.json(...)``
~~~~~~~~~~~~~~~~~~~
The ``json()`` method will serialise a model to JSON, ``json()`` in turn calls ``dict()`` and serialises its result.
Serialisation can be customised on a model using the ``json_encoders`` config property, the keys should be types and
the values should be functions which serialise that type, see the example below.
Arguments:
* ``include``: fields to include in the returned dictionary, see :ref:`below <include_exclude>`
* ``exclude``: fields to exclude from the returned dictionary, see :ref:`below <include_exclude>`
* ``by_alias``: whether field aliases should be used as keys in the returned dictionary, default ``False``
* ``skip_defaults``: whether fields which were not set when creating the model and have their default values should
be excluded from the returned dictionary, default ``False``
* ``encoder``: a custom encoder function passed to the ``default`` argument of ``json.dumps()``, defaults to a custom
encoder designed to take care of all common types
* ``**dumps_kwargs``: any other keyword argument are passed to ``json.dumps()``, eg. ``indent``.
Example:
.. literalinclude:: examples/export_json.py
(This script is complete, it should run "as is")
By default timedelta's are encoded as a simple float of total seconds. The ``timedelta_isoformat`` is provided
as an optional alternative which implements ISO 8601 time diff encoding.
``pickle.dumps(model)``
~~~~~~~~~~~~~~~~~~~~~~~
Using the same plumbing as ``copy()`` *pydantic* models support efficient pickling and unpicking.
.. literalinclude:: examples/export_pickle.py
(This script is complete, it should run "as is")
.. _include_exclude:
Advanced include and exclude
............................
@@ -975,41 +1054,6 @@ Of course same can be done on any depth level:
Same goes for ``json`` and ``copy`` methods.
Serialisation
.............
*pydantic* has native support for serialisation to **JSON** and **Pickle**, you can of course serialise to any
other format you like by processing the result of ``dict()``.
.. _json_dump:
JSON Serialisation
~~~~~~~~~~~~~~~~~~
The ``json()`` method will serialise a model to JSON, ``json()`` in turn calls ``dict()`` and serialises its result.
Serialisation can be customised on a model using the ``json_encoders`` config property, the keys should be types and
the values should be functions which serialise that type, see the example below.
If this is not sufficient, ``json()`` takes an optional ``encoder`` argument which allows complete control
over how non-standard types are encoded to JSON.
.. literalinclude:: examples/ex_json.py
(This script is complete, it should run "as is")
By default timedelta's are encoded as a simple float of total seconds. The ``timedelta_isoformat`` is provided
as an optional alternative which implements ISO 8601 time diff encoding.
Pickle Serialisation
~~~~~~~~~~~~~~~~~~~~
Using the same plumbing as ``copy()`` *pydantic* models support efficient pickling and unpicking.
.. literalinclude:: examples/ex_pickle.py
(This script is complete, it should run "as is")
Abstract Base Classes
.....................
+3 -2
View File
@@ -294,6 +294,7 @@ class BaseModel(metaclass=MetaModel):
return {
get_key(k): v
for k, v in self._iter(
to_dict=True,
by_alias=by_alias,
allowed_keys=allowed_keys,
include=include,
@@ -544,7 +545,7 @@ class BaseModel(metaclass=MetaModel):
for f in cls.__fields__.values():
update_field_forward_refs(f, globalns=globalns, localns=localns)
def __iter__(self) -> 'AnyGenerator':
def __iter__(self) -> 'TupleGenerator':
"""
so `dict(model)` works
"""
@@ -552,7 +553,7 @@ class BaseModel(metaclass=MetaModel):
def _iter(
self,
to_dict: bool = True,
to_dict: bool = False,
by_alias: bool = False,
allowed_keys: Optional['SetStr'] = None,
include: Union['SetIntStr', 'DictIntStrAny'] = None,
+15
View File
@@ -806,3 +806,18 @@ def test_custom_types_fail_without_keep_untouched():
with pytest.raises(AttributeError) as e:
Model.class_name
assert str(e.value) == "type object 'Model' has no attribute 'class_name'"
def test_model_iteration():
class Foo(BaseModel):
a: int = 1
b: int = 2
class Bar(BaseModel):
c: int
d: Foo
m = Bar(c=3, d={})
assert m.dict() == {'c': 3, 'd': {'a': 1, 'b': 2}}
assert list(m) == [('c', 3), ('d', Foo())]
assert dict(m) == {'c': 3, 'd': Foo()}