-
-
Notifications
You must be signed in to change notification settings - Fork 773
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
Return tuple for aiohttp #849
Conversation
584e659
to
f5c3f2f
Compare
Hi, I just reviewed your code, firsly thanks to help on this one ! :) What I explained juste before applies also for the tests. In my PR, I want same tests to be applied for each App, to not create duplication. In my point a view, This project needs a base of tests that can be run on all tests and very specific App tests (like using a custom encoder in Flask by passing it to the flask object) Here is how I see this functionality ! :) |
@panpan I got your point. I did more factorization between both Flask and AioHttp. Let me know what you think. |
@Jyhess I looks like the CI is failing because your are trying to read the response body (bytes) as a string. I think you can use |
d0cd874
to
da810b7
Compare
I would like to use tuple returns with aiohttp. What is needed to move this forward? More code review? |
@cognifloyd sorry for dropping the ball here. Can you fix the merge conflicts? |
Sure. Here's the merge commit: |
f090f42
to
b8cd718
Compare
I rebased the branch on top of master. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested this in the connexion-aiohttp project I'm working on.
Before trying this, I was always returning a ConnexionResponse
.
With this change, I successfully returned each of these:
ConnexionResponse
,- a two-tuple with
body, 200
aiohttp.web.Response(body=json.dumps(body), content_type="application/json")
This works for me. 🎉
edited: I hit enter too soon.
connexion/apis/abstract.py
Outdated
:param response: A response to cast. | ||
:param mimetype: The response mimetype. | ||
:param url: The url to wrote in logs |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Could you rebase the PR and fix the conflicts? We will take a look afterwards. |
Hi @Jyhess - I tried to resolve the conflicts against master. Sorry if this messed up your workflow! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✔️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some questions while I diagnose what is causing the test failure.
connexion/apis/flask_api.py
Outdated
@classmethod | ||
def _jsonify_data(cls, data, mimetype): | ||
# TODO: to discuss: Using jsonifier for all type of data, even when mimetype is not json is strange. Why ? | ||
if (isinstance(mimetype, six.string_types) and is_json_mimetype(mimetype)) \ | ||
or not (isinstance(data, six.binary_type) or isinstance(data, six.text_type)): | ||
return cls.jsonifier.dumps(data) | ||
|
||
return data |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
connexion/apis/aiohttp_api.py
Outdated
response.status, extra={'data': response.body, 'url': url}) | ||
# TODO: I let this log here for full compatibility. But if we can modify it, it can go to _get_response() | ||
logger.debug('Got stream response with status code (%d)', | ||
response.status, extra={'url': url}) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
connexion/apis/abstract.py
Outdated
if isinstance(response, tuple): | ||
len_response = len(response) | ||
if len_response == 2: | ||
if isinstance(response[1], (int, Enum)): | ||
data, status_code = response | ||
return cls._build_response(mimetype=mimetype, data=data, status_code=status_code) | ||
else: | ||
data, headers = response | ||
return cls._build_response(mimetype=mimetype, data=data, headers=headers) | ||
elif len_response == 3: | ||
data, status_code, headers = response | ||
return cls._build_response(mimetype=mimetype, data=data, status_code=status_code, headers=headers) | ||
else: | ||
raise TypeError( | ||
'The view function did not return a valid response tuple.' | ||
' The tuple must have the form (body), (body, status, headers),' | ||
' (body, status), or (body, headers).' | ||
) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
notes about six and python3
connexion/apis/abstract.py
Outdated
if isinstance(response, ConnexionResponse): | ||
# If body in ConnexionResponse is not byte, it may not pass schema validation. | ||
# In this case, rebuild response with aiohttp to have consistency | ||
if response.body is None or isinstance(response.body, six.binary_type): |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
connexion/apis/abstract.py
Outdated
if not isinstance(data, six.binary_type): | ||
if isinstance(mimetype, six.string_types) and is_json_mimetype(mimetype): | ||
body = cls.jsonifier.dumps(data) | ||
elif isinstance(data, six.text_type): |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment has been minimized.
This comment has been minimized.
@Jyhess I hope we can work on this PR next. Are you available to help resolve some of my questions? |
@cognifloyd I will ping you on gitter when I can go back on this subject. Maybe this afternoon or tomorrow. |
This is fantastic work @Jyhess and @cognifloyd - I'm inspired by your work on this project, and excited to feel the momentum! It looks like the longer-term plan is to firm up this interface to allow for other frameworks (like quart, sanic).
I'd be excited to collaborate on long term vision and improvements assuming there's bandwidth for review and merging by folks at zalando. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hjacobs I think this is ready to merge. We can skip removing _jsonify_data from flask_api for now.
response = yield from api.get_response({'foo': 'bar'}) | ||
assert isinstance(response, web.Response) | ||
assert response.status == 200 | ||
assert response.body == b"{'foo': 'bar'}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You wouldn't expect this to be json?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I would not expect this to be json. Train of thought follows:
- Everything in this file is testing against an api that has only one endpoint
/greeting/{name}
->fakeapi.aiohttp_handlers.aiohttp_post_greeting
which returns a dict. - But it's not actually using the handler, it's accessing get_response directly.
- So mimetype is not coming from the spec here, which means there is no assumption that the content will be json.
- which means if you pass an object (in this case a dict) it will be stringified by
_jsonify_data
. - the implementation of
_jsonify_data
inabstract
is basically doing the same thing that_cast_body
did inaiohttp_api
- Note that this body is using
'
not"
so it is not valid json. it is just a stringified dict. - So, yes I would expect (without changing behavior) this to merely stringify the dict as no mimetype / content type is defined.
- Consider the next test where a mimetype is explicitly defined, and therefore produces json data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I can follow your logic here - I have a minor pragmatic hangup which is that casting a dict to a string is kind of the least-useful way to serialize that object.
You might argue in practice that folks would notice this, and then include the right content-type, but it seems like an unfriendly default.
Consider the following flask app, which defaults to returning json
import flask
app = flask.Flask(__name__)
@app.route("/")
def a():
return {"a": "b"}
app.run()
Does that mean in that example you'd expect to get a string representation of the dict as text/plain
?
K. So my change is a little more restrictive than flask's _jsonify_data, but hopefully it will be less surprising when people are trying to serialize with something other than json. But, this doesn't automatically change the mimetype as there is no obvious place to "change" a requested mimetype/content_type. I suppose it could be done just for aiohttp in the aiohttp-specific version of |
elif isinstance(data, str): | ||
body = data | ||
else: | ||
body = str(data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are some other datatypes where the string representation is better? I'm having trouble thinking of any.
For both int
and float
, I think I'd prefer the json representation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. Now, when mimetype is None, anything that can be jsonified will be, catching TypeError to rescue with str()
. Duck-typing for the win - if it quacks like JSON and the mimetype is None, then it is JSON.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is getting really close.
Eventually we want to be able to register serializers for arbitrary mimetypes, so I think there's a default path (mimetype is none, try to do json), and then maybe some built-in overridable serializers (text/plain uses str() for example).
So if we can get backwards compatible behavior with flask (default serializer is json), but an escape hatch registering new mimetypes/serializers then I think we're golden.
One thing I don't think we should do is introduce anything opinionated, or anything too magical/surprising.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out that it is magical and surprising no matter how I look at it. From both aiohttp and flask sides, it feels a little off. :(
3d5469f
to
85eeceb
Compare
OK. So, harmonizing the serialization between aiohttp and flask turns out to be ugly. Several of the flask tests specify a mimetype of From the tests it looks like you can currently I also had to adjust one of those aiohttp tests because now that it is jsonified instead of just stringified, there's a newline at the end. So, this is cleaner and more consistent, but harmonizing the jsonification might have to be a separate PR that goes in a major version bump. I can imagine this being part of a pluggable serializers feature. It wouldn't be too much more work to have a chain of serializers that vote based on the mediatype. How far can we harmonize serialization in this PR? |
We could introduce the planned new behavior in |
Rename _jsonify_data to _serialize_data to make its purpose easier to understand (this was also known as _cast_body in aiohttp_api). In exploring how to harmonize json serialization between aiothttp and flask, we needed to be able to adjust the mimetype from within _serialize_data. Harmonizing the actual serialization has to wait until backwards incompatible changes can be made, but we can keep the new interface, as these functions were introduced in this PR (spec-first#849).
85eeceb
to
3a33f74
Compare
Rename _jsonify_data to _serialize_data to make its purpose easier to understand (this was also known as _cast_body in aiohttp_api). In exploring how to harmonize json serialization between aiothttp and flask, we needed to be able to adjust the mimetype from within _serialize_data. Harmonizing the actual serialization has to wait until backwards incompatible changes can be made, but we can keep the new interface, as these functions were introduced in this PR (spec-first#849).
3a33f74
to
e57e6a6
Compare
K. I removed the commits that harmonized serialization between flask and aiohttp. I will push a new PR where we can continue that work, but I expect that will have to wait until a major version bump so we can make backwards incompatible adjustments. |
ok that makes sense. Happy to tackle it in another PR. |
c1926a5
to
f029a01
Compare
f029a01
to
1275511
Compare
I added some
I picked
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work - Thanks @Jyhess and @cognifloyd
👍 |
First step of #828
Changes proposed in this pull request: