jmcarp / flask-apispec

MIT License
653 stars 155 forks source link

'apply' in marshall_with is set for all decorators #194

Open kam193 opened 4 years ago

kam193 commented 4 years ago

Let's assume we declare a resource (for any reason) like this:

class HelloWorld(MethodResource):
    @marshal_with(TheTestSchema)
    @marshal_with(TheSecondSchema, code=300, apply=False)
    def get(self):
        return {'hello': 'world'}

I expected that TheTestSchema will be used in default case, and TheSecondSchema is present only in docs - and it looks in docs like this. But in fact, no one schema is used, in any case. I think apply=False is propagated to all marshal_with decorators for given method.

kam193 commented 4 years ago

I found that this is how Annotation.merge works:

def merge(self, other):
        if self.inherit is False:
            return self
        return self.__class__(
            self.options + other.options,
            inherit=other.inherit,
            apply=self.apply if self.apply is not None else other.apply,
        )

As I see, it's used to merge mutliple decorators as well as inherited values. Furthermore, if I set inherit=False in the first decorator, second will be ignored (in docs and results).

Querela commented 4 years ago

I have the same issue and was puzzled for a very long time because my other methods worked, except for PUT. My main issue is that for responses without content and therefore no schema I can't use apply because then the None in the @marshal_with(None, ...) decorator will be converted into a dict later on (default value for empty schema) and the schema dumping will raise an error because the dict is no schema ...


The only solution I can think of is just using the schema myself to dump the object.

Another way (just to handle None-Schema or empty results) would be an additional check in wrapper.py:L58 (?) marshal_result(self, result, status_code). Here a further check before dumped = schema.dump(result) whether the schema object has a dump(..) method might be enough. That's unfortunately only a dirty fix (possibly?) but I'm not sure what other None-schemas can be supplied to the @marshal_with decorator that will require apply=False and where the functions return non-easily serializable objects. (like no JSON, string, numbers etc.) No real solution for different schemas with different apply states.


My use case below but only the PUT method fails. (Either because apply=True for None-Schema or apply=False for my TaskSchema where the Task-Resource is not serialized.)

@doc(tags=["api"])
class TaskResourceById(MethodResource):
    endpoint = "task"

    @doc(description="A task.")
    @marshal_with(TaskSchema, description="Get task with id.")
    def get(self, task_id):
        return self._get_task(task_id)

    @use_kwargs(TaskSchema)
    @marshal_with(TaskSchema, description="Update task.", apply=True)
    @marshal_with(None, code=404, description="No task found.", apply=False)
    def put(self, task_id, **kwargs):
        task = self._get_task(task_id)
        if not task:
            return None, 404
        for key, value in kwargs.items():
            setattr(task, key, value)

        local_object = db.session.merge(task)
        db.session.add(local_object)
        db.session.commit()

        return (
            local_object,
            200,
            {
                "location": url_for(
                    f"{bp_api.name}.{TaskResourceById.endpoint}",
                    task_id=local_object.id,
                )
            },
        )

    @marshal_with(None, code=204, description="Task deleted.", apply=False)
    @marshal_with(None, code=404, description="No task found.", apply=False)
    def delete(self, task_id):
        task = self._get_task(task_id)
        if not task:
            return None, 404

        local_object = db.session.merge(task)
        db.session.delete(local_object)
        db.session.commit()
        return None, 204

    # --------------------------------

    def _get_task(self, task_id):
        return Task.query.get(task_id)

    # --------------------------------

The error messages for PUT depending on apply:

...
  File "***/venv/lib/python3.7/site-packages/flask_apispec/wrapper.py", line 37, in __call__
    mv = self.marshal_result(rv, status_code)
  File "***/venv/lib/python3.7/site-packages/flask_apispec/wrapper.py", line 67, in marshal_result
    dumped = schema.dump(result)
AttributeError: 'dict' object has no attribute 'dump'
...
  File "***/venv/lib/python3.7/site-packages/flask/json/__init__.py", line 100, in default
    return _json.JSONEncoder.default(self, o)
  File "/usr/lib/python3.7/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type Task is not JSON serializable