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

Improved strict mode; ValidationError; Deprecate UnmarshallingError and MarshallingError (issue #160) #168

Merged
merged 16 commits into from Mar 16, 2015
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
5 changes: 4 additions & 1 deletion CHANGELOG.rst
Expand Up @@ -6,8 +6,9 @@ Changelog

Features:

- *Backwards-incompatible*: When ``many=True``, the errors dictionary returned by ``dump`` and ``load`` will be keyed on the indices of invalid items in the (de)serialized collection (:issue:`75`). Add the ``index_errors`` class Meta option.
- *Backwards-incompatible*: When ``many=True``, the errors dictionary returned by ``dump`` and ``load`` will be keyed on the indices of invalid items in the (de)serialized collection (:issue:`75`). Add the ``index_errors`` class Meta option to disable this behavior.
- *Backwards-incompatible*: By default, required fields will raise a ValidationError if the input is ``None`` or the empty string. The ``allow_none`` and ``allow_blank`` parameters can override this behavior.
- In ``strict`` mode, a ``ValidationError`` is raised. Error messages are accessed via the ``ValidationError's`` ``messages`` attribute (:issue:`128`).
- Add ``allow_none`` parameter to ``fields.Field``. If ``False`` (the default), validation fails when the field's value is ``None`` (:issue:`76`, :issue:`111`). If ``allow_none`` is ``True``, ``None`` is considered valid and will deserialize to ``None``.
- Add ``allow_blank`` parameter to ``fields.String`` fields (incl. ``fields.URL``, ``fields.Email``). If ``False`` (the default), validation fails when the field's value is the empty string (:issue:`76`).
- Schema-level validators can store error messages for multiple fields (:issue:`118`). Thanks :user:`ksesong` for the suggestion.
Expand All @@ -23,6 +24,8 @@ Features:

Deprecation/Removals:

- ``MarshallingError`` and ``UnmarshallingError`` error are deprecated in favor of a single ``ValidationError`` (:issue:`160`).
- Remove ``ForcedError``.
- Remove support for generator functions that yield validators (:issue:`74`). Plain generators of validators are still supported.
- The ``Select/Enum`` field is deprecated in favor of using `validate.OneOf` validator (:issue:`135`).
- Remove legacy, pre-1.0 API (``Schema.data`` and ``Schema.errors`` properties) (:issue:`73`).
Expand Down
4 changes: 2 additions & 2 deletions docs/custom_fields.rst
Expand Up @@ -66,8 +66,8 @@ A :class:`Function <marshmallow.fields.Function>` field will take the value of a

.. _adding-context:

Adding Context to Method and Function Fields
--------------------------------------------
Adding Context to `Method` and `Function` Fields
------------------------------------------------

A :class:`Function <marshmallow.fields.Function>` or :class:`Method <marshmallow.fields.Method>` field may need information about its environment to know how to serialize a value.

Expand Down
4 changes: 2 additions & 2 deletions docs/extending.rst
Expand Up @@ -82,7 +82,7 @@ Normally, unspecified field names are ignored by the validator. If you would lik
Storing Errors on Specific Fields
+++++++++++++++++++++++++++++++++

If you want to store schema-level validation errors on a specific field, you can pass a field name to the :exc:`ValidationError`.
If you want to store schema-level validation errors on a specific field, you can pass a field name (or multiple field names) to the :exc:`ValidationError <marshmallow.exceptions.ValidationError>`.

.. code-block:: python

Expand Down Expand Up @@ -194,7 +194,7 @@ You can register error handlers, validators, and data handlers as optional class
Extending "class Meta" Options
--------------------------------

``class Meta`` options are a way to configure and modify a :class:`Schema's <Schema>` behavior. See the :class:`API docs <Schema>` for a listing of available options.
``class Meta`` options are a way to configure and modify a :class:`Schema's <Schema>` behavior. See the :class:`API docs <Schema.Meta>` for a listing of available options.

You can add custom ``class Meta`` options by subclassing :class:`SchemaOpts`.

Expand Down
2 changes: 1 addition & 1 deletion docs/nesting.rst
Expand Up @@ -24,7 +24,7 @@ Schemas can be nested to represent relationships between objects (e.g. foreign k
self.title = title
self.author = author # A User object

Use a :class:`Nested <marshmallow.fields.Nested>` field to represent the relationship, passing in nested schema class.
Use a :class:`Nested <marshmallow.fields.Nested>` field to represent the relationship, passing in a nested schema class.

.. code-block:: python
:emphasize-lines: 10
Expand Down
43 changes: 24 additions & 19 deletions docs/quickstart.rst
Expand Up @@ -16,13 +16,10 @@ Let's start with a basic user "model".
import datetime as dt

class User(object):
def __init__(self, name, email, age=None):
def __init__(self, name, email):
self.name = name
self.email = email
self.created_at = dt.datetime.now()
self.friends = []
self.employer = None
self.age = age

def __repr__(self):
return '<User(name={self.name!r})>'.format(self=self)
Expand Down Expand Up @@ -168,10 +165,10 @@ Validation
.. code-block:: python

data, errors = UserSchema().load({'email': 'foo'})
errors # => {'email': ['foo is not a valid email address.']}
errors # => {'email': ['"foo" is not a valid email address.']}
# OR, equivalently
result = UserSchema().load({'email': 'foo'})
result.errors # => {'email': ['foo is not a valid email address.']}
result.errors # => {'email': ['"foo" is not a valid email address.']}


When validating a collection, the errors dictionary will be keyed on the indicies of invalid items.
Expand Down Expand Up @@ -207,9 +204,10 @@ You can perform additional validation for a field by passing it a ``validate`` c
result.errors # => {'age': ['Validator <lambda>(71.0) is False']}


Validation functions either return a boolean or raise a :exc:`ValidationError`. If a :exc:`ValidationError` is raised, its message is stored when validation fails.
Validation functions either return a boolean or raise a :exc:`ValidationError`. If a :exc:`ValidationError <marshmallow.exceptions.ValidationError>` is raised, its message is stored when validation fails.

.. code-block:: python
:emphasize-lines: 7,10,14

from marshmallow import Schema, fields, ValidationError

Expand All @@ -228,24 +226,30 @@ Validation functions either return a boolean or raise a :exc:`ValidationError`.

.. note::

If you have multiple validations to perform, you may also pass a collection (list, tuple) or generator of callables to the ``validate`` parameter.
If you have multiple validations to perform, you may also pass a collection (list, tuple, generator) of callables.

.. note::

:meth:`Schema.dump` also validates the format of its fields and returns a dictionary of errors. However, the callables passed to ``validate`` are only applied during deserialization.

.. note::

If you set ``strict=True`` in either the Schema constructor or as a ``class Meta`` option, an error will be raised when invalid data are passed in.
``strict`` Mode
+++++++++++++++

If you set ``strict=True`` in either the Schema constructor or as a ``class Meta`` option, an error will be raised when invalid data are passed in. You can access the dictionary of validation errors from the `ValidationError.messages <marshmallow.exceptions.ValidationError.messages>` attribute.

.. code-block:: python

UserSchema(strict=True).load({'email': 'foo'})
# => UnmarshallingError: "foo" is not a valid email address.
from marshmallow import ValidationError

try:
UserSchema(strict=True).load({'email': 'foo'})
except ValidationError as err:
print(err.messages)# => {'email': ['"foo" is not a valid email address.']}

Alternatively, you can also register a custom error handler function for a schema using the :func:`error_handler <Schema.error_handler>` decorator. See the :ref:`Extending Schemas <extending>` page for more info.
.. seealso::

You can register a custom error handler function for a schema using the :func:`error_handler <Schema.error_handler>` decorator. See the :ref:`Extending Schemas <extending>` page for more info.

.. seealso::

Expand Down Expand Up @@ -310,21 +314,21 @@ By default, `Schemas` will marshal the object attributes that are identical to t
Specifying Deserialization Keys
-------------------------------

By default `Schemas` will unmarshal an input dictionary to an output dictionary whose keys are identical to the field names. However, if you are consuming data that does not exactly match your schema, you can specify additional keys to load values from.
By default `Schemas` will unmarshal an input dictionary to an output dictionary whose keys are identical to the field names. However, if you are consuming data that does not exactly match your schema, you can specify additional keys to load values by passing the `load_from` argument.

.. code-block:: python
:emphasize-lines: 2,3,11,12

class UserSchema(Schema):
name = fields.String()
email = fields.Email(load_from='email_address')
email = fields.Email(load_from='emailAddress')

data = {
'name': 'Mike',
'email_address': 'foo@bar.com'
'emailAddress': 'foo@bar.com'
}
ser = UserSchema()
result, errors = ser.load(data)
s = UserSchema()
result, errors = s.load(data)
#{'name': u'Mike',
# 'email': 'foo@bar.com'}

Expand Down Expand Up @@ -361,7 +365,8 @@ Note that ``name`` will be automatically formatted as a :class:`String <marshmal
class UserSchema(Schema):
uppername = fields.Function(lambda obj: obj.name.upper())
class Meta:
additional = ("name", "email", "created_at") # No need to include 'uppername'
# No need to include 'uppername'
additional = ("name", "email", "created_at")

Ordering Output
---------------
Expand Down
80 changes: 79 additions & 1 deletion docs/upgrading.rst
Expand Up @@ -78,11 +78,88 @@ When validating a collection (i.e. when calling ``load`` or ``dump`` with ``many

You can still get the pre-2.0 behavior by setting the ``index_errors`` *class Meta* option to `False`.

Use ``ValidationError`` instead of ``MarshallingError`` and ``UnmarshallingError``
**********************************************************************************

The :exc:`MarshallingError` and :exc:`UnmarshallingError` exceptions are deprecated in favor of a single :exc:`ValidationError <marshmallow.exceptions.ValidationError>`. Users who have written custom fields or are using ``strict`` mode will need to change their code accordingly.

Custom Fields
-------------

Custom fields should raise :exc:`ValidationError <marshmallow.exceptions.ValidationError>` in their `_deserialize` and `_serialize` methods when a validation error occurs.

.. code-block:: python
:emphasize-lines: 17

from marshmallow import fields, ValidationError
from marshmallow.exceptions import UnmarshallingError

# In 1.0, an UnmarshallingError was raised
class PasswordField(fields.Field):

def _deserialize(self, val):
if not len(val) >= 6:
raise UnmarshallingError('Password to short.')
return val

# In 2.0, an UnmarshallingError is raised
class PasswordField(fields.Field):

def _deserialize(self, val):
if not len(val) >= 6:
raise ValidationError('Password to short.')
return val

Handle ``ValidationError`` in strict mode
-----------------------------------------

When using `strict` mode, you should handle `ValidationErrors` when calling `Schema.dump` and `Schema.load`.

.. code-block:: python
:emphasize-lines: 14

from marshmallow import exceptions as exc

schema = BandMemberSchema(strict=True)

# 1.0
try:
schema.load({'email': 'invalid-email'})
except exc.UnmarshallingError as err:
# ...

# 2.0
try:
schema.load({'email': 'invalid-email'})
except exc.ValidationError as err:
# ...


Accessing error messages in strict mode
***************************************

In 2.0, `strict` mode was improved so that you can access all error messages for a schema (rather than failing early) by accessing a `ValidationError's` ``messages`` attribute.

.. code-block:: python
:emphasize-lines: 6

schema = BandMemberSchema(strict=True)

try:
result = schema.load({'email': 'invalid'})
except ValidationMessage as err:
print(err.messages)
# {
# 'email': ['"invalid" is not a valid email address.'],
# 'name': ['Missing data for required field.']
# }



Use ``OneOf`` instead of ``fields.Select``
******************************************

The `fields.Select` field was deprecated in favor of the newly-added `OneOf` validator.
The `fields.Select` field is deprecated in favor of the newly-added `OneOf` validator.

.. code-block:: python

Expand All @@ -95,6 +172,7 @@ The `fields.Select` field was deprecated in favor of the newly-added `OneOf` val
# 2.0
fields.Str(validate=OneOf(['red', 'blue']))


Upgrading to 1.2
++++++++++++++++

Expand Down
83 changes: 35 additions & 48 deletions marshmallow/exceptions.py
@@ -1,87 +1,74 @@
# -*- coding: utf-8 -*-
"""Exception classes for marshmallow-related errors."""
from marshmallow.compat import text_type, basestring
import warnings

from marshmallow.compat import basestring

class MarshmallowError(Exception):
"""Base class for all marshmallow-related errors."""
pass


class _WrappingException(MarshmallowError):
"""Exception that wraps a different, underlying exception. Used so that
an error in serialization or deserialization can be reraised as a
:exc:`MarshmallowError <MarshmallowError>`.
"""

def __init__(self, underlying_exception, fields=None, field_names=None):
if isinstance(underlying_exception, Exception):
self.underlying_exception = underlying_exception
else:
self.underlying_exception = None
self.fields = fields
self.field_names = field_names
super(_WrappingException, self).__init__(
text_type(underlying_exception)
)


class ForcedError(_WrappingException):
"""Error that always gets raised, even during serialization.
Field classes should raise this error if the error should not be stored in
the Marshaller's error dictionary and should instead be raised.

Must be instantiated with an underlying exception.

Example: ::

def _serialize(self, value, key, obj):
if not isinstace(value, dict):
raise ForcedError(ValueError('Value must be a dict.'))
"""
pass


class ValidationError(MarshmallowError):
"""Raised when validation fails on a field.
"""Raised when validation fails on a field. Validators and custom fields should
raise this exception.

:param message: An error message, list of error messages, or dict of
error messages.
:param str field: Field name (or list of field names) to store the error on.
:param list field_names: Field names to store the error on.
If `None`, the error is stored in its default location.
:param list fields: `Field` objects to which the error applies.
"""

def __init__(self, message, field=None):
def __init__(self, message, field_names=None, fields=None):
if not isinstance(message, dict) and not isinstance(message, list):
messages = [message]
else:
messages = message
#: String, list, or dictionary of error messages.
#: If a `dict`, the keys will be field names and the values will be lists of
#: messages.
self.messages = messages
self.field = field
if isinstance(field, basestring):
self.fields = [field]
else: # field is a list or None
self.fields = field
#: List of field objects which failed validation.
self.fields = fields
if isinstance(field_names, basestring):
#: List of field_names which failed validation.
self.field_names = [field_names]
else: # fields is a list or None
self.field_names = field_names or []
MarshmallowError.__init__(self, message)


class RegistryError(ForcedError, NameError):
class RegistryError(NameError):
"""Raised when an invalid operation is performed on the serializer
class registry.
"""
pass


class MarshallingError(_WrappingException):
class MarshallingError(ValidationError):
"""Raised in case of a marshalling error. If raised during serialization,
the error is caught and the error message is stored in an ``errors``
dictionary (unless ``strict`` mode is turned on).

.. deprecated:: 2.0.0
Use :exc:`ValidationError` instead.
"""
pass
def __init__(self, *args, **kwargs):
warnings.warn('MarshallingError is deprecated. Raise a ValidationError instead',
category=DeprecationWarning)
super(MarshallingError, self).__init__(*args, **kwargs)


class UnmarshallingError(_WrappingException):
class UnmarshallingError(ValidationError):
"""Raised when invalid data are passed to a deserialization function. If
raised during deserialization, the error is caught and the error message
is stored in an ``errors`` dictionary.

.. deprecated:: 2.0.0
Use :exc:`ValidationError` instead.
"""
pass
def __init__(self, *args, **kwargs):
warnings.warn('UnmarshallingError is deprecated. Raise a ValidationError instead',
category=DeprecationWarning)
super(UnmarshallingError, self).__init__(*args, **kwargs)