More liberal flask number converters for float and int in paths (#1306)

* Use more liberal flask converters for float and int

These don't try to enforce a "single representation" of paths but instead try to convert the numbers that callers pass in.

Addresses #1040 and #1041

* Use f-strings instead of string concat or %-formats

* Complying with style rules added long after this PR was made
This commit is contained in:
Logi Ragnarsson
2021-07-14 21:02:54 +02:00
committed by GitHub
parent 443bd20087
commit a8375a1beb
3 changed files with 63 additions and 8 deletions

View File

@@ -27,6 +27,8 @@ class FlaskApp(AbstractApp):
def create_app(self): def create_app(self):
app = flask.Flask(self.import_name, **self.server_args) app = flask.Flask(self.import_name, **self.server_args)
app.json_encoder = FlaskJSONEncoder app.json_encoder = FlaskJSONEncoder
app.url_map.converters['float'] = NumberConverter
app.url_map.converters['int'] = IntegerConverter
return app return app
def get_root_path(self): def get_root_path(self):
@@ -142,3 +144,19 @@ class FlaskJSONEncoder(json.JSONEncoder):
return float(o) return float(o)
return json.JSONEncoder.default(self, o) return json.JSONEncoder.default(self, o)
class NumberConverter(werkzeug.routing.BaseConverter):
""" Flask converter for OpenAPI number type """
regex = r"[+-]?[0-9]*(\.[0-9]*)?"
def to_python(self, value):
return float(value)
class IntegerConverter(werkzeug.routing.BaseConverter):
""" Flask converter for OpenAPI integer type """
regex = r"[+-]?[0-9]+"
def to_python(self, value):
return int(value)

View File

@@ -1,6 +1,8 @@
import json import json
from io import BytesIO from io import BytesIO
import pytest
def test_parameter_validation(simple_app): def test_parameter_validation(simple_app):
app_client = simple_app.app.test_client() app_client = simple_app.app.test_client()
@@ -124,22 +126,57 @@ def test_strict_formdata_param(strict_app):
assert resp.status_code == 200 assert resp.status_code == 200
def test_path_parameter_someint(simple_app): @pytest.mark.parametrize('arg, result', [
# The cases accepted by the Flask/Werkzeug converter
['123', 'int 123'],
['0', 'int 0'],
['0000', 'int 0'],
# Additional cases that we want to support
['+123', 'int 123'],
['+0', 'int 0'],
['-0', 'int 0'],
['-123', 'int -123'],
])
def test_path_parameter_someint(simple_app, arg, result):
assert isinstance(arg, str) # sanity check
app_client = simple_app.app.test_client() app_client = simple_app.app.test_client()
resp = app_client.get('/v1.0/test-int-path/123') # type: flask.Response resp = app_client.get(f'/v1.0/test-int-path/{arg}') # type: flask.Response
assert resp.data.decode('utf-8', 'replace') == '"int"\n' assert resp.data.decode('utf-8', 'replace') == f'"{result}"\n'
def test_path_parameter_someint__bad(simple_app):
# non-integer values will not match Flask route # non-integer values will not match Flask route
app_client = simple_app.app.test_client()
resp = app_client.get('/v1.0/test-int-path/foo') # type: flask.Response resp = app_client.get('/v1.0/test-int-path/foo') # type: flask.Response
assert resp.status_code == 404 assert resp.status_code == 404
def test_path_parameter_somefloat(simple_app): @pytest.mark.parametrize('arg, result', [
# The cases accepted by the Flask/Werkzeug converter
['123.45', 'float 123.45'],
['123.0', 'float 123'],
['0.999999999999999999', 'float 1'],
# Additional cases that we want to support
['+123.45', 'float 123.45'],
['-123.45', 'float -123.45'],
['123.', 'float 123'],
['.45', 'float 0.45'],
['123', 'float 123'],
['0', 'float 0'],
['0000', 'float 0'],
['-0.000000001', 'float -1e-09'],
['100000000000', 'float 1e+11'],
])
def test_path_parameter_somefloat(simple_app, arg, result):
assert isinstance(arg, str) # sanity check
app_client = simple_app.app.test_client() app_client = simple_app.app.test_client()
resp = app_client.get('/v1.0/test-float-path/123.45') # type: flask.Response resp = app_client.get(f'/v1.0/test-float-path/{arg}') # type: flask.Response
assert resp.data.decode('utf-8' , 'replace') == '"float"\n' assert resp.data.decode('utf-8', 'replace') == f'"{result}"\n'
def test_path_parameter_somefloat__bad(simple_app):
# non-float values will not match Flask route # non-float values will not match Flask route
app_client = simple_app.app.test_client()
resp = app_client.get('/v1.0/test-float-path/123,45') # type: flask.Response resp = app_client.get('/v1.0/test-float-path/123,45') # type: flask.Response
assert resp.status_code == 404 assert resp.status_code == 404

View File

@@ -266,11 +266,11 @@ def test_schema_int(test_int):
def test_get_someint(someint): def test_get_someint(someint):
return type(someint).__name__ return f'{type(someint).__name__} {someint:g}'
def test_get_somefloat(somefloat): def test_get_somefloat(somefloat):
return type(somefloat).__name__ return f'{type(somefloat).__name__} {somefloat:g}'
def test_default_param(name): def test_default_param(name):