From cf2a363e5e0c0c208b384b87964c9cbe0d6edb8b Mon Sep 17 00:00:00 2001 From: lixxu Date: Thu, 9 Feb 2017 16:44:23 +0800 Subject: [PATCH 1/3] improve url_for to support multi values for one arg, add _anchor/_external/_scheme options --- sanic/sanic.py | 18 +++++++++++----- sanic/utils.py | 6 +++++- tests/test_url_building.py | 43 +++++++++++++++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 951632be..7b3e5d22 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -230,6 +230,16 @@ class Sanic: matched_params = re.findall( self.router.parameter_pattern, uri) + # _method is only a placeholder now, don't know how to support it + kwargs.pop('_method', None) + anchor = kwargs.pop('_anchor', '') + # _external need SERVER_NAME in config or pass _server arg + external = kwargs.pop('_external', False) + scheme = kwargs.pop('_scheme', '') + if scheme and not external: + raise ValueError('When specifying _scheme, _external must be True') + + netloc = kwargs.pop('_server', self.config.get('SERVER_NAME', '')) for match in matched_params: name, _type, pattern = self.router.parse_parameter_string( match) @@ -271,11 +281,9 @@ class Sanic: # parse the remainder of the keyword arguments into a querystring if kwargs: - query_string = urlencode(kwargs) - out = urlunparse(( - '', '', out, - '', query_string, '' - )) + query_string = urlencode(kwargs, doseq=True) + # scheme://netloc/path;parameters?query#fragment + out = urlunparse((scheme, netloc, out, '', query_string, anchor)) return out diff --git a/sanic/utils.py b/sanic/utils.py index 8e8f8124..da13f478 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -6,7 +6,11 @@ PORT = 42101 async def local_request(method, uri, cookies=None, *args, **kwargs): - url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) + if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')): + url = uri + else: + url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) + log.info(url) async with aiohttp.ClientSession(cookies=cookies) as session: async with getattr( diff --git a/tests/test_url_building.py b/tests/test_url_building.py index 480313b3..f2377c3a 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -5,7 +5,7 @@ from sanic import Sanic from sanic.response import text from sanic.views import HTTPMethodView from sanic.blueprints import Blueprint -from sanic.utils import sanic_endpoint_test +from sanic.utils import sanic_endpoint_test, PORT as test_port from sanic.exceptions import URLBuildError import string @@ -39,6 +39,30 @@ def test_simple_url_for_getting(simple_app): assert response.text == letter +def test_simple_url_for_getting_with_duplicate_params(simple_app): + kw = dict(arg1=['value1', 'value2'], _anchor='anchor') + for letter in string.ascii_letters: + url = simple_app.url_for(letter, **kw) + + assert url == '/{}?arg1=value1&arg1=value2#anchor'.format(letter) + request, response = sanic_endpoint_test(simple_app, uri=url) + assert response.status == 200 + assert response.text == letter + + +def test_simple_url_for_getting_with_special_params(simple_app): + kw = dict(arg1='value1', _anchor='anchor', _scheme='http', + _server='localhost:{}'.format(test_port), _external=True) + url_fmt = 'http://localhost:{}/{}?arg1=value1#anchor' + for letter in string.ascii_letters: + url = simple_app.url_for(letter, **kw) + + assert url == url_fmt.format(test_port, letter) + request, response = sanic_endpoint_test(simple_app, uri=url) + assert response.status == 200 + assert response.text == letter + + def test_fails_if_endpoint_not_found(): app = Sanic('fail_url_build') @@ -75,6 +99,19 @@ def test_fails_url_build_if_param_not_passed(): assert 'Required parameter `Z` was not passed to url_for' in str(e.value) +def test_fails_url_build_if_params_not_passed(): + app = Sanic('fail_url_build') + + @app.route('/fail') + def fail(): + return text('this should fail') + + with pytest.raises(ValueError) as e: + app.url_for('fail', _scheme='http') + + assert str(e.value) == 'When specifying _scheme, _external must be True' + + COMPLEX_PARAM_URL = ( '///' '//') @@ -179,11 +216,11 @@ def blueprint_app(): return text( 'foo from first : {}'.format(param)) - @second_print.route('/foo') # noqa + @second_print.route('/foo') # noqa def foo(): return text('foo from second') - @second_print.route('/foo/') # noqa + @second_print.route('/foo/') # noqa def foo_with_param(request, param): return text( 'foo from second : {}'.format(param)) From fb419eaa3649e1d7367f6f51ea7c5ca42a1f8256 Mon Sep 17 00:00:00 2001 From: lixxu Date: Thu, 9 Feb 2017 18:26:17 +0800 Subject: [PATCH 2/3] fix bug: netloc always in url if SERVER_NAME defined in config even _external not true --- sanic/sanic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 7b3e5d22..821c2b89 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -239,7 +239,10 @@ class Sanic: if scheme and not external: raise ValueError('When specifying _scheme, _external must be True') - netloc = kwargs.pop('_server', self.config.get('SERVER_NAME', '')) + netloc = kwargs.pop('_server', None) + if netloc is None and external: + netloc = self.config.get('SERVER_NAME', '') + for match in matched_params: name, _type, pattern = self.router.parse_parameter_string( match) From 4839ede64f81856d5020541fb4aa79ab3df78a52 Mon Sep 17 00:00:00 2001 From: lixxu Date: Tue, 14 Feb 2017 10:26:30 +0800 Subject: [PATCH 3/3] update test for url_for and update routing.md doc --- docs/sanic/routing.md | 24 +++++++++++++++++++++- sanic/sanic.py | 7 +++---- tests/test_url_building.py | 41 +++++++++++++++++++------------------- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 0cadd707..5d0442e4 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -11,7 +11,7 @@ from sanic.response import json @app.route("/") async def test(request): return json({ "hello": "world" }) -``` +``` When the url `http://server.url/` is accessed (the base url of the server), the final `/` is matched by the router to the handler function, `test`, which then @@ -145,6 +145,28 @@ Other things to keep in mind when using `url_for`: url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two') # /posts/5?arg_one=one&arg_two=two ``` +- Multivalue argument can be passed to `url_for`. For example: +```python +url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two']) +# /posts/5?arg_one=one&arg_one=two +``` +- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not support now and will be ignored). For example: +```python +url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor') +# /posts/5?arg_one=one#anchor + +url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True) +# //server/posts/5?arg_one=one +# _external requires passed argument _server or SERVER_NAME in app.config or url will be same as no _external + +url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True) +# http://server/posts/5?arg_one=one +# when specifying _scheme, _external must be True + +# you can pass all special arguments one time +url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888') +# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor +``` - All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown. diff --git a/sanic/sanic.py b/sanic/sanic.py index 821c2b89..c6b10383 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -283,10 +283,9 @@ class Sanic: replacement_regex, supplied_param, out) # parse the remainder of the keyword arguments into a querystring - if kwargs: - query_string = urlencode(kwargs, doseq=True) - # scheme://netloc/path;parameters?query#fragment - out = urlunparse((scheme, netloc, out, '', query_string, anchor)) + query_string = urlencode(kwargs, doseq=True) if kwargs else '' + # scheme://netloc/path;parameters?query#fragment + out = urlunparse((scheme, netloc, out, '', query_string, anchor)) return out diff --git a/tests/test_url_building.py b/tests/test_url_building.py index f2377c3a..02cbce31 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -10,6 +10,14 @@ from sanic.exceptions import URLBuildError import string +URL_FOR_ARGS1 = dict(arg1=['v1', 'v2']) +URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2' +URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor') +URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor' +URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http', + _server='localhost:{}'.format(test_port), _external=True) +URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port) + def _generate_handlers_from_names(app, l): for name in l: @@ -39,28 +47,21 @@ def test_simple_url_for_getting(simple_app): assert response.text == letter -def test_simple_url_for_getting_with_duplicate_params(simple_app): - kw = dict(arg1=['value1', 'value2'], _anchor='anchor') - for letter in string.ascii_letters: - url = simple_app.url_for(letter, **kw) +@pytest.mark.parametrize('args,url', + [(URL_FOR_ARGS1, URL_FOR_VALUE1), + (URL_FOR_ARGS2, URL_FOR_VALUE2), + (URL_FOR_ARGS3, URL_FOR_VALUE3)]) +def test_simple_url_for_getting_with_more_params(args, url): + app = Sanic('more_url_build') - assert url == '/{}?arg1=value1&arg1=value2#anchor'.format(letter) - request, response = sanic_endpoint_test(simple_app, uri=url) - assert response.status == 200 - assert response.text == letter + @app.route('/myurl') + def passes(request): + return text('this should pass') - -def test_simple_url_for_getting_with_special_params(simple_app): - kw = dict(arg1='value1', _anchor='anchor', _scheme='http', - _server='localhost:{}'.format(test_port), _external=True) - url_fmt = 'http://localhost:{}/{}?arg1=value1#anchor' - for letter in string.ascii_letters: - url = simple_app.url_for(letter, **kw) - - assert url == url_fmt.format(test_port, letter) - request, response = sanic_endpoint_test(simple_app, uri=url) - assert response.status == 200 - assert response.text == letter + assert url == app.url_for('passes', **args) + request, response = sanic_endpoint_test(app, uri=url) + assert response.status == 200 + assert response.text == 'this should pass' def test_fails_if_endpoint_not_found():