Skip to content

Commit

Permalink
Don't use deserialize_many; coverage updates
Browse files Browse the repository at this point in the history
  • Loading branch information
jfinkels committed Apr 28, 2016
1 parent 879361c commit f9bef67
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 70 deletions.
23 changes: 11 additions & 12 deletions docs/customizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,6 @@ For deserialization, define your custom deserialization class like this::
def deserialize(self, document):
return Person(...)

def deserialize_many(self, document):
return Person(...)

``document`` is a dictionary representation of the *complete* incoming JSON API
document, where the ``data`` element contains the primary resource object or
objects. The function must return an instance of the model that has the
Expand All @@ -210,12 +207,11 @@ overriding the :meth:`DefaultSerializer.serialize` method, and similarly a
:exc:`~flask.ext.restless.DeserializationException` in the
:meth:`DefaultDeserializer.deserialize` method; Flask-Restless will
automatically catch those exceptions and format a `JSON API error response`_.
For the :meth:`DefaultSerializer.serialize_many` and
:meth:`DefaultDeserializer.deserialize_many` methods, if you wish to collect
multiple exceptions (for example, if several instances provided to the
:meth:`deserialize_many` method fails a validation check) you can raise a
:exc:`~flask.ext.restless.MultipleExceptions` exception, providing a list of
other serialization or deserialization exceptions at instantiation time.
If you wish to collect multiple exceptions (for example, if several fields of a
resource provided to the :meth:`deserialize` method fail validation) you can
raise a :exc:`~flask.ext.restless.MultipleExceptions` exception, providing a
list of other serialization or deserialization exceptions at instantiation
time.

.. note::

Expand Down Expand Up @@ -253,9 +249,12 @@ follows::
person_schema = PersonSchema()
return person_schema.load(instance).data

def deserialize_many(self, document):
person_schema = PersonSchema(many=True)
return person_schema.load(instance).data
# # JSON API doesn't currently allow bulk creation of resources. When
# # it does, either in the specification or in an extension, this is
# # how you would implement it.
# def deserialize_many(self, document):
# person_schema = PersonSchema(many=True)
# return person_schema.load(instance).data

manager = APIManager(app, session=session)
manager.create_api(Person, methods=['GET', 'POST'],
Expand Down
5 changes: 5 additions & 0 deletions flask_restless/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,11 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS,
if isinstance(attr, STRING_TYPES) and not hasattr(model, attr):
msg = 'no attribute "{0}" on model {1}'.format(attr, model)
raise AttributeError(msg)
if (additional_attributes is not None and exclude is not None and
any(attr in exclude for attr in additional_attributes)):
msg = ('Cannot exclude attributes listed in the'
' `additional_attributes` keyword argument')
raise IllegalArgumentError(msg)
# Create a default serializer and deserializer if none have been
# provided.
if serializer_class is None:
Expand Down
66 changes: 34 additions & 32 deletions flask_restless/serialization/deserializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,38 +192,40 @@ def deserialize(self, document):
data = document['data']
return self._load(data)

def deserialize_many(self, document):
"""Creates and returns a list of instances of the SQLAlchemy
model specified in the constructor whose fields are given in the
JSON API document.
This method assumes that each resource in the given document is
of the same type.
For more information, see the documentation for the
:meth:`Deserializer.deserialize_many` method.
"""
if 'data' not in document:
raise MissingData
data = document['data']
if not isinstance(data, list):
raise NotAList
# Since loading each instance from a given resource object
# representation could theoretically raise a
# DeserializationException, we collect all the errors and wrap
# them in a MultipleExceptions exception object.
result = []
failed = []
for resource in data:
try:
instance = self._load(resource)
result.append(instance)
except DeserializationException as exception:
failed.append(exception)
if failed:
raise MultipleExceptions(failed)
return result
# # TODO JSON API currently doesn't support bulk creation of resources,
# # so this code cannot be accurately used/tested.
# def deserialize_many(self, document):
# """Creates and returns a list of instances of the SQLAlchemy
# model specified in the constructor whose fields are given in the
# JSON API document.
#
# This method assumes that each resource in the given document is
# of the same type.
#
# For more information, see the documentation for the
# :meth:`Deserializer.deserialize_many` method.
#
# """
# if 'data' not in document:
# raise MissingData
# data = document['data']
# if not isinstance(data, list):
# raise NotAList
# # Since loading each instance from a given resource object
# # representation could theoretically raise a
# # DeserializationException, we collect all the errors and wrap
# # them in a MultipleExceptions exception object.
# result = []
# failed = []
# for resource in data:
# try:
# instance = self._load(resource)
# result.append(instance)
# except DeserializationException as exception:
# failed.append(exception)
# if failed:
# raise MultipleExceptions(failed)
# return result


class DefaultRelationshipDeserializer(Deserializer):
Expand Down
12 changes: 7 additions & 5 deletions flask_restless/serialization/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,13 @@ class NotAList(DeserializationException):
"""

def __init__(self, relation_name=None, *args, **kw):
if relation_name is not None:
inner = ('in linkage for relationship'
' "{0}"').format(relation_name)
else:
inner = ''
# # For now, this is only raised when calling deserialize_many()
# # on a relationship, so this extra message should always be
# # inserted.
# if relation_name is not None:
inner = ('in linkage for relationship "{0}" ').format(relation_name)
# else:
# inner = ''

detail = ('"data" element {0}must be a list when calling'
' deserialize_many(); maybe you meant to call'
Expand Down
36 changes: 15 additions & 21 deletions flask_restless/serialization/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from urlparse import urljoin

from flask import request
from sqlalchemy import Column
from sqlalchemy.exc import NoInspectionAvailable
from sqlalchemy.ext.hybrid import HYBRID_PROPERTY
from sqlalchemy.inspection import inspect
Expand Down Expand Up @@ -155,14 +154,13 @@ def get_column_name(column):
options.
"""
# TODO use inspection API here
if hasattr(column, '__clause_element__'):
clause_element = column.__clause_element__()
if not isinstance(clause_element, Column):
msg = 'Expected a column attribute of a SQLAlchemy ORM class'
raise TypeError(msg)
return clause_element.key
return column
try:
inspected_column = inspect(column)
except NoInspectionAvailable:
# In this case, we assume the column is actually just a string.
return column
else:
return inspected_column.key


class Serializer(object):
Expand Down Expand Up @@ -230,23 +228,18 @@ class DefaultSerializer(Serializer):
dictionary. This is useful if your model has an attribute that is
not a SQLAlchemy column but you want it to be exposed.
If both `only` and `exclude` are specified, a :exc:`ValueError` is
raised. Also, if any attributes specified in
`additional_attributes` appears in `exclude`, a :exc:`ValueError` is
raised.
You **must not** specify both `only` and `exclude` lists; if you do,
the behavior of this function is undefined.
You **must not** specify a field in both `exclude` and in
`additional_attributes`; if you do, the behavior of this function is
undefined.
"""

def __init__(self, only=None, exclude=None, additional_attributes=None,
**kw):
super(DefaultSerializer, self).__init__(**kw)
if only is not None and exclude is not None:
raise ValueError('Cannot specify both `only` and `exclude` keyword'
' arguments simultaneously')
if (additional_attributes is not None and exclude is not None and
any(attr in exclude for attr in additional_attributes)):
raise ValueError('Cannot exclude attributes listed in the'
' `additional_attributes` keyword argument')
# Always include at least the type and ID, regardless of what the user
# specified.
if only is not None:
Expand Down Expand Up @@ -275,7 +268,8 @@ def _dump(self, instance, only=None):
try:
inspected_instance = inspect(model)
except NoInspectionAvailable:
return instance
message = 'failed to get columns for model {0}'.format(model)
raise SerializationException(instance, message=message)
column_attrs = inspected_instance.column_attrs.keys()
descriptors = inspected_instance.all_orm_descriptors.items()
# hybrid_columns = [k for k, d in descriptors
Expand Down
11 changes: 11 additions & 0 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ class Person(self.Base):
__tablename__ = 'person'
id = Column(Integer, primary_key=True)
name = Column(Unicode)
extra = 'foo'

class Article(self.Base):
__tablename__ = 'article'
Expand Down Expand Up @@ -466,6 +467,16 @@ def test_additional_attributes_nonexistent(self):
self.manager.create_api(self.Person,
additional_attributes=['bogus'])

def test_exclude_additional_attributes(self):
"""Tests that an attempt to exclude a field that is also
specified in ``additional_attributes`` causes an exception at
the time of API creation.
"""
with self.assertRaises(IllegalArgumentError):
self.manager.create_api(self.Person, exclude=['extra'],
additional_attributes=['extra'])


class TestFSA(FlaskSQLAlchemyTestBase):
"""Tests which use models defined using Flask-SQLAlchemy instead of pure
Expand Down

0 comments on commit f9bef67

Please sign in to comment.