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 3cfdaf4b..e27f25d8 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -275,6 +275,19 @@ 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', 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) @@ -315,12 +328,9 @@ class Sanic: replacement_regex, supplied_param, out) # 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) if kwargs else '' + # 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 272ec8a8..298f952c 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..02cbce31 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -5,11 +5,19 @@ 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 +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,6 +47,23 @@ def test_simple_url_for_getting(simple_app): assert response.text == letter +@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') + + @app.route('/myurl') + def passes(request): + return text('this should pass') + + 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(): app = Sanic('fail_url_build') @@ -75,6 +100,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 +217,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))