From 82ef45c89064bfb3bc1126f00f2756a841b35d7a Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 12 Aug 2019 11:31:35 +0100 Subject: [PATCH] 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 --- changes/733-samuelcolvin.rst | 2 + .../examples/{copy_dict.py => export_copy.py} | 13 -- docs/examples/export_dict.py | 21 +++ docs/examples/export_iterate.py | 21 +++ docs/examples/{ex_json.py => export_json.py} | 0 .../{ex_pickle.py => export_pickle.py} | 2 - docs/index.rst | 138 ++++++++++++------ pydantic/main.py | 5 +- tests/test_main.py | 15 ++ 9 files changed, 153 insertions(+), 64 deletions(-) create mode 100644 changes/733-samuelcolvin.rst rename docs/examples/{copy_dict.py => export_copy.py} (68%) create mode 100644 docs/examples/export_dict.py create mode 100644 docs/examples/export_iterate.py rename docs/examples/{ex_json.py => export_json.py} (100%) rename docs/examples/{ex_pickle.py => export_pickle.py} (99%) diff --git a/changes/733-samuelcolvin.rst b/changes/733-samuelcolvin.rst new file mode 100644 index 0000000..090fbe7 --- /dev/null +++ b/changes/733-samuelcolvin.rst @@ -0,0 +1,2 @@ +**Breaking Change:** alter the behaviour of ``dict(model)`` so that sub-models are nolonger +converted to dictionaries diff --git a/docs/examples/copy_dict.py b/docs/examples/export_copy.py similarity index 68% rename from docs/examples/copy_dict.py rename to docs/examples/export_copy.py index a58e24f..208b225 100644 --- a/docs/examples/copy_dict.py +++ b/docs/examples/export_copy.py @@ -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= - print(m.copy(include={'foo', 'bar'})) # > FooBarModel foo='hello' bar= diff --git a/docs/examples/export_dict.py b/docs/examples/export_dict.py new file mode 100644 index 0000000..2408cfe --- /dev/null +++ b/docs/examples/export_dict.py @@ -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} diff --git a/docs/examples/export_iterate.py b/docs/examples/export_iterate.py new file mode 100644 index 0000000..9d9dcad --- /dev/null +++ b/docs/examples/export_iterate.py @@ -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': } + +for name, value in m: + print(f'{name}: {value}') + +#> banana: 3.14 +#> foo: hello +#> bar: BarModel whatever=123 diff --git a/docs/examples/ex_json.py b/docs/examples/export_json.py similarity index 100% rename from docs/examples/ex_json.py rename to docs/examples/export_json.py diff --git a/docs/examples/ex_pickle.py b/docs/examples/export_pickle.py similarity index 99% rename from docs/examples/ex_pickle.py rename to docs/examples/export_pickle.py index a9be99c..54e83e3 100644 --- a/docs/examples/ex_pickle.py +++ b/docs/examples/export_pickle.py @@ -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 diff --git a/docs/index.rst b/docs/index.rst index 7ffb584..1d1167f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 `) 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 ` +* ``exclude``: fields to exclude from the returned dictionary, see :ref:`below ` +* ``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 ` +* ``exclude``: fields to exclude from the returned dictionary, see :ref:`below ` +* ``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 ` +* ``exclude``: fields to exclude from the returned dictionary, see :ref:`below ` +* ``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 ..................... diff --git a/pydantic/main.py b/pydantic/main.py index cbf5f1a..0aef775 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -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, diff --git a/tests/test_main.py b/tests/test_main.py index 0ea0073..c713867 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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()}