Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Schema.from_dict for generating schemas from dicts #1322

Merged
merged 2 commits into from Aug 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Expand Up @@ -8,6 +8,7 @@ Features:

- Optimize ``List(Nested(...))`` (:issue:`779`).
- Minor performance improvements and cleanup (:pr:`1328`).
- Add ``Schema.from_dict`` (:issue:`1312`).

Deprecations/Removals:

Expand Down
18 changes: 16 additions & 2 deletions docs/quickstart.rst
Expand Up @@ -41,6 +41,20 @@ Create a schema by defining a class with variables mapping attribute names to :c

For a full reference on the available field classes, see the :ref:`API Docs <api_fields>`.

Creating Schemas From Dictionaries
----------------------------------

You can create a schema from a dictionary of fields using the `from_dict <marshmallow.Schema.from_dict>` method.

.. code-block:: python

from marshmallow import Schema, fields

UserSchema = Schema.from_dict(
{"name": fields.Str(), "email": fields.Email(), "created_at": fields.DateTime()}
)

`from_dict <marshmallow.Schema.from_dict>` is especially useful for generating schemas at runtime.

Serializing Objects ("Dumping")
-------------------------------
Expand Down Expand Up @@ -96,8 +110,8 @@ with a dictionary of validation errors, which we'll :ref:`revisit later <validat

user_data = {
"created_at": "2014-08-11T05:26:03.869245",
"email": u"ken@yahoo.com",
"name": u"Ken",
"email": "ken@yahoo.com",
"name": "Ken",
}
schema = UserSchema()
result = schema.load(user_data)
Expand Down
30 changes: 30 additions & 0 deletions src/marshmallow/schema.py
Expand Up @@ -8,6 +8,7 @@
import copy
import inspect
import json
import typing
import warnings

from marshmallow import base, fields as ma_fields, class_registry
Expand Down Expand Up @@ -409,6 +410,35 @@ def dict_class(self):
def set_class(self):
return OrderedSet if self.ordered else set

@classmethod
def from_dict(
cls, fields: typing.Dict[str, ma_fields.Field], *, name: str = "GeneratedSchema"
) -> typing.Type["Schema"]:
"""Generate a `Schema` class given a dictionary of fields.

.. code-block:: python

from marshmallow import Schema, fields

PersonSchema = Schema.from_dict({"name": fields.Str()})
print(PersonSchema().load({"name": "David"})) # => {'name': 'David'}

Generated schemas are not added to the class registry and therefore cannot
be referred to by name in `Nested` fields.

:param dict fields: Dictionary mapping field names to field instances.
:param str name: Optional name for the class, which will appear in
the ``repr`` for the class.

.. versionadded:: 3.0.0
"""
attrs = fields.copy()
attrs["Meta"] = type(
"GeneratedMeta", (getattr(cls, "Meta", object),), {"register": False}
)
schema_cls = type(name, (cls,), attrs)
return schema_cls

##### Override-able methods #####

def handle_error(self, error, data, *, many, **kwargs):
Expand Down
42 changes: 41 additions & 1 deletion tests/test_schema.py
Expand Up @@ -19,8 +19,13 @@
EXCLUDE,
INCLUDE,
RAISE,
class_registry,
)
from marshmallow.exceptions import (
ValidationError,
StringNotCollectionError,
RegistryError,
)
from marshmallow.exceptions import ValidationError, StringNotCollectionError

from tests.base import (
UserSchema,
Expand Down Expand Up @@ -2644,3 +2649,38 @@ class NoTldTestSchema(Schema):
data_with_no_top_level_domain = {"url": "marshmallow://app/discounts"}
result = schema.load(data_with_no_top_level_domain)
assert result == data_with_no_top_level_domain


class TestFromDict:
def test_generates_schema(self):
MySchema = Schema.from_dict({"foo": fields.Str()})
assert issubclass(MySchema, Schema)

def test_name(self):
MySchema = Schema.from_dict({"foo": fields.Str()})
assert "GeneratedSchema" in repr(MySchema)
SchemaWithName = Schema.from_dict(
{"foo": fields.Int()}, name="MyGeneratedSchema"
)
assert "MyGeneratedSchema" in repr(SchemaWithName)

def test_generated_schemas_are_not_registered(self):
n_registry_entries = len(class_registry._registry)
Schema.from_dict({"foo": fields.Str()})
Schema.from_dict({"bar": fields.Str()}, name="MyGeneratedSchema")
assert len(class_registry._registry) == n_registry_entries
with pytest.raises(RegistryError):
class_registry.get_class("GeneratedSchema")
with pytest.raises(RegistryError):
class_registry.get_class("MyGeneratedSchema")

def test_meta_options_are_applied(self):
class OrderedSchema(Schema):
class Meta:
ordered = True
load_only = ("bar",)

OSchema = OrderedSchema.from_dict({"foo": fields.Int(), "bar": fields.Int()})
dumped = OSchema().dump({"foo": 42, "bar": 24})
assert isinstance(dumped, OrderedDict)
assert "bar" not in dumped