-
Notifications
You must be signed in to change notification settings - Fork 10
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
Added flask-json-response-type codemod #162
Conversation
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #162 +/- ##
==========================================
+ Coverage 96.31% 96.38% +0.07%
==========================================
Files 77 78 +1
Lines 3500 3680 +180
==========================================
+ Hits 3371 3547 +176
- Misses 129 133 +4
|
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.
This is a nice idea for a codemod. However, my main substantive comment is that make_response
does not appear to accept a kwarg called mimetype
: https://flask.palletsprojects.com/en/2.3.x/api/#flask.Flask.make_response
File "/Users/danieldavella/sandbox/flask_json.py", line 12, in return_json_str
return make_response(json.dumps(response), mimetype="application/json")
TypeError: make_response() got an unexpected keyword argument 'mimetype'
There are a probably a few different ways to implement this codemod. One would involve explicitly setting the header on the generated response:
response = make_response(json.dumps(...))
response.headers['Content-Type'] = 'application/json'
Another (imo cleaner) way would involve using werkzeug.wrappers.Response
to set the content type. werkzeug
is an explicit dependency of flask
so there is no issue with adding this import:
from werkzeug.wrappers import Response
def return_json_str():
response_data = {"message": "This is a JSON response"}
response_json = json.dumps(response_data)
return Response(response_json, content_type='application/json')
It's also worth mentioning that in newer versions of flask
it is possible to return a dict
directly from a view and the response will be properly interpreted as JSON data.
I also think there are also a few other cases we can handle for flask
. You can feel free to add them to this PR or create a ticket/add it in another PR.
- It is possible to return a string directly from a view in Flask:
@app.route("/return-json-str")
def return_json_str():
return json.dumps({ "user_input": request.GET.get("input") })
- The
make_request
function also takes a tuple argument where the body is the first element:
def foo():
data = json.dumps({ "user_input": request.GET.get("input") })
headers = {}
return make_response((data, 200, headers))
960cca0
to
e800225
Compare
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.
Now that this codemod has been restricted only to decorated Flask view functions, we need to revisit the conditions where this fix is actually necessary. Flask will call jsonify
by default on dict
objects that are returned from view functions. Furthermore, it appears that make_response
also calls jsonify
on dict
objects.
This would suggest that the set of problematic conditions is confined to cases where the wrong content type is being set explicitly, either with make_response
, or in a tuple returned by a view function, or when used to construct a Response
object.
@app.route("/test") | ||
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
return (make_response(json_response), {'Content-Type': 'application/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.
Wouldn't the simplest solution in this case to be to just return json_response
since Flask will automatically set the proper context type?
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.
Actually, according to the flask
documentation, make_response
will also correctly handle the content type by calling jsonify
on a dict
: https://flask.palletsprojects.com/en/3.0.x/api/#flask.Flask.make_response
@app.route("/test") | ||
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
return json_response |
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 does not need to be modified as flask will automatically set the correct content type in this case.
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 won't. The return of json.dumps()
is a string and flask
will only apply jsonify
for dict returns.
from textwrap import dedent | ||
|
||
|
||
class TestFlaskJsonResponseType(BaseCodemodTest): |
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 about a test case where a variety of different headers are already set? We need to make sure the only modified header is Content-Type
.
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
- return make_response(json_response) | ||
+ return (make_response(json_response), {'Content-Type':'application/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.
See other comments regarding the overall conditions for this change, but I think it would be more idiomatic and more aligned with the purpose of make_response
to call it like this:
return make_response(json_response, {'Content-Type':'application/json'})
See the documentation and specifically the tuple
parameter case: https://flask.palletsprojects.com/en/3.0.x/api/#flask.Flask.make_response
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.
The reason for the tuple response is because it is more general. It will also work for the direct json.dumps
response. I'll adjust the transformation for the make_response
case.
First, note that the return of If you need to convince yourself, simply run the following small flask app: from flask import Flask, jsonify, make_response
import json
app = Flask(__name__)
@app.route("/")
def hello_world():
json_response = json.dumps({'data':'Hello World dict'})
return make_response(json_response)) with FLASK_APP=flask_test flask run and check the
The most idiomatic response is to actually remove the |
Yes you're right, sorry I forgot that the output of I realize this codemod has become substantially more complicated than you or I originally anticipated, so I'd be okay with descoping it if that makes it easier for you and we can revisit/refine it later. |
I'm already working on making the changes to |
This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation. |
e800225
to
9657c31
Compare
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.
Very nice, thanks for powering through on this one 😅
Overview
Added flask-json-response-type codemod.
Description
The reason why this one is just not a case of django-json-response-type codemod stems from the fact that we would need to identify which pattern rule was active when making the change. This would require the use of the ScopeProvider and would make it expensive.