Merge pull request #399 from lixxu/master

improve url_for to support multi values and special options
This commit is contained in:
Eli Uriegas 2017-02-14 10:27:54 -06:00 committed by GitHub
commit 286dc3c32b
4 changed files with 85 additions and 11 deletions

View File

@ -11,7 +11,7 @@ from sanic.response import json
@app.route("/") @app.route("/")
async def test(request): async def test(request):
return json({ "hello": "world" }) return json({ "hello": "world" })
``` ```
When the url `http://server.url/` is accessed (the base url of the server), the 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 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') url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
# /posts/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. - 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.

View File

@ -275,6 +275,19 @@ class Sanic:
matched_params = re.findall( matched_params = re.findall(
self.router.parameter_pattern, uri) 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: for match in matched_params:
name, _type, pattern = self.router.parse_parameter_string( name, _type, pattern = self.router.parse_parameter_string(
match) match)
@ -315,12 +328,9 @@ class Sanic:
replacement_regex, supplied_param, out) replacement_regex, supplied_param, out)
# parse the remainder of the keyword arguments into a querystring # parse the remainder of the keyword arguments into a querystring
if kwargs: query_string = urlencode(kwargs, doseq=True) if kwargs else ''
query_string = urlencode(kwargs) # scheme://netloc/path;parameters?query#fragment
out = urlunparse(( out = urlunparse((scheme, netloc, out, '', query_string, anchor))
'', '', out,
'', query_string, ''
))
return out return out

View File

@ -6,7 +6,11 @@ PORT = 42101
async def local_request(method, uri, cookies=None, *args, **kwargs): 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) log.info(url)
async with aiohttp.ClientSession(cookies=cookies) as session: async with aiohttp.ClientSession(cookies=cookies) as session:
async with getattr( async with getattr(

View File

@ -5,11 +5,19 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint 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 from sanic.exceptions import URLBuildError
import string 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): def _generate_handlers_from_names(app, l):
for name in l: for name in l:
@ -39,6 +47,23 @@ def test_simple_url_for_getting(simple_app):
assert response.text == letter 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(): def test_fails_if_endpoint_not_found():
app = Sanic('fail_url_build') 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) 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 = ( COMPLEX_PARAM_URL = (
'/<foo:int>/<four_letter_string:[A-z]{4}>/' '/<foo:int>/<four_letter_string:[A-z]{4}>/'
'<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>') '<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>')
@ -179,11 +217,11 @@ def blueprint_app():
return text( return text(
'foo from first : {}'.format(param)) 'foo from first : {}'.format(param))
@second_print.route('/foo') # noqa @second_print.route('/foo') # noqa
def foo(): def foo():
return text('foo from second') return text('foo from second')
@second_print.route('/foo/<param>') # noqa @second_print.route('/foo/<param>') # noqa
def foo_with_param(request, param): def foo_with_param(request, param):
return text( return text(
'foo from second : {}'.format(param)) 'foo from second : {}'.format(param))