From a73a7d1e7b4d96d209d029db6d72d2818330bce9 Mon Sep 17 00:00:00 2001 From: Stefano Palazzo Date: Fri, 23 Dec 2016 11:42:00 +0100 Subject: [PATCH 01/19] Make it possible to disable the logo by subclassing Config --- sanic/sanic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..2ef3507e 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -294,7 +294,8 @@ class Sanic: if debug: log.setLevel(logging.DEBUG) - log.debug(self.config.LOGO) + if self.config.LOGO is not None: + log.debug(self.config.LOGO) # Serve log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) From 41da793b5ac91e0118f359c08ad88beea0da756d Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 28 Jan 2017 15:26:44 -0800 Subject: [PATCH 02/19] fix async run, add tests --- sanic/server.py | 3 ++- tests/test_async_run.py | 25 +++++++++++++++++++++++++ tests/test_loop_policy.py | 18 ++++++++++++++++++ tox.ini | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/test_async_run.py create mode 100644 tests/test_loop_policy.py diff --git a/sanic/server.py b/sanic/server.py index 48c3827e..8081cb30 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -297,7 +297,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param protocol: Subclass of asyncio protocol class :return: Nothing """ - loop = async_loop.new_event_loop() + loop = loop or async_loop.new_event_loop() + asyncio.set_event_loop_policy(async_loop.EventLoopPolicy()) asyncio.set_event_loop(loop) if debug: diff --git a/tests/test_async_run.py b/tests/test_async_run.py new file mode 100644 index 00000000..4e7d68cd --- /dev/null +++ b/tests/test_async_run.py @@ -0,0 +1,25 @@ +from sanic import Sanic +from sanic.response import json +import asyncio +import requests +from threading import Thread +import pytest +import sys + +@pytest.mark.skipif(sys.version_info < (3, 6), + reason="fails on python 3.5 with travis") +def test_async_run(): + app = Sanic(__name__) + + @app.route("/") + async def test(request): + return json({"answer": "42"}) + + server = app.create_server(host="0.0.0.0", port=8001) + task = asyncio.ensure_future(server) + loop = asyncio.get_event_loop() + t = Thread(target=loop.run_forever) + t.start() + res = requests.get('http://localhost:8001') + loop.stop() + assert res.json()['answer'] == '42' diff --git a/tests/test_loop_policy.py b/tests/test_loop_policy.py new file mode 100644 index 00000000..7737b6b0 --- /dev/null +++ b/tests/test_loop_policy.py @@ -0,0 +1,18 @@ +from sanic import Sanic +from sanic.response import text +from sanic.utils import sanic_endpoint_test +import asyncio +import uvloop + +def test_loop_policy(): + app = Sanic('test_loop_policy') + + @app.route('/') + def test(request): + return text("OK") + + server = app.create_server() + + request, response = sanic_endpoint_test(app) + assert isinstance(asyncio.get_event_loop_policy(), + uvloop.EventLoopPolicy) diff --git a/tox.ini b/tox.ini index 009d971c..8669e48b 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ deps = aiohttp pytest beautifulsoup4 + requests commands = pytest tests {posargs} From 1501c56bbc464edab4f2397d049bb33754d556c0 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 30 Jan 2017 16:42:43 -0800 Subject: [PATCH 03/19] update route method docs --- docs/sanic/routing.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 9d5856d6..2dda39c4 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -64,23 +64,37 @@ async def folder_handler(request, folder_id): ## HTTP request types -By default, a route defined on a URL will be used for all requests to that URL. +By default, a route defined on a URL will be avaialble for only GET requests to that URL. However, the `@app.route` decorator accepts an optional parameter, `methods`, -which restricts the handler function to the HTTP methods in the given list. +whicl allows the handler function to work with any of the HTTP methods in the list. ```python from sanic.response import text -@app.route('/post') -async def post_handler(request, methods=['POST']): +@app.route('/post', methods=['POST']) +async def post_handler(request): return text('POST request - {}'.format(request.json)) -@app.route('/get') -async def GET_handler(request, methods=['GET']): +@app.route('/get', methods=['GET']) +async def get_handler(request): return text('GET request - {}'.format(request.args)) ``` +There are also shorthand method decorators: + +```python +from sanic.response import text + +@app.post('/post') +async def post_handler(request): + return text('POST request - {}'.format(request.json)) + +@app.get('/get') +async def get_handler(request): + return text('GET request - {}'.format(request.args)) + +``` ## The `add_route` method As we have seen, routes are often specified using the `@app.route` decorator. From 34966fb182850182737172484c49b8d944c3ac30 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 31 Jan 2017 01:24:41 -0800 Subject: [PATCH 04/19] Added sanic-openapi to extensions --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 8303311a..1325ad0f 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -6,3 +6,4 @@ A list of Sanic extensions created by the community. Allows using redis, memcache or an in memory store. - [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. - [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template. +- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. \ No newline at end of file From 487e3352e4c375aac84f80f502eefa6797bcefd2 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 31 Jan 2017 07:30:17 -0600 Subject: [PATCH 05/19] Revert "fix async run, add tests" This reverts commit 41da793b5ac91e0118f359c08ad88beea0da756d. --- sanic/server.py | 3 +-- tests/test_async_run.py | 25 ------------------------- tests/test_loop_policy.py | 18 ------------------ tox.ini | 1 - 4 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 tests/test_async_run.py delete mode 100644 tests/test_loop_policy.py diff --git a/sanic/server.py b/sanic/server.py index 8081cb30..48c3827e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -297,8 +297,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param protocol: Subclass of asyncio protocol class :return: Nothing """ - loop = loop or async_loop.new_event_loop() - asyncio.set_event_loop_policy(async_loop.EventLoopPolicy()) + loop = async_loop.new_event_loop() asyncio.set_event_loop(loop) if debug: diff --git a/tests/test_async_run.py b/tests/test_async_run.py deleted file mode 100644 index 4e7d68cd..00000000 --- a/tests/test_async_run.py +++ /dev/null @@ -1,25 +0,0 @@ -from sanic import Sanic -from sanic.response import json -import asyncio -import requests -from threading import Thread -import pytest -import sys - -@pytest.mark.skipif(sys.version_info < (3, 6), - reason="fails on python 3.5 with travis") -def test_async_run(): - app = Sanic(__name__) - - @app.route("/") - async def test(request): - return json({"answer": "42"}) - - server = app.create_server(host="0.0.0.0", port=8001) - task = asyncio.ensure_future(server) - loop = asyncio.get_event_loop() - t = Thread(target=loop.run_forever) - t.start() - res = requests.get('http://localhost:8001') - loop.stop() - assert res.json()['answer'] == '42' diff --git a/tests/test_loop_policy.py b/tests/test_loop_policy.py deleted file mode 100644 index 7737b6b0..00000000 --- a/tests/test_loop_policy.py +++ /dev/null @@ -1,18 +0,0 @@ -from sanic import Sanic -from sanic.response import text -from sanic.utils import sanic_endpoint_test -import asyncio -import uvloop - -def test_loop_policy(): - app = Sanic('test_loop_policy') - - @app.route('/') - def test(request): - return text("OK") - - server = app.create_server() - - request, response = sanic_endpoint_test(app) - assert isinstance(asyncio.get_event_loop_policy(), - uvloop.EventLoopPolicy) diff --git a/tox.ini b/tox.ini index 8669e48b..009d971c 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,6 @@ deps = aiohttp pytest beautifulsoup4 - requests commands = pytest tests {posargs} From b29f6481484ce5809f748fb555d5f2603acfa301 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 31 Jan 2017 12:46:02 -0800 Subject: [PATCH 06/19] typo: async_run -> run_async --- sanic/sanic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 1d3e252e..eece7bab 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -341,7 +341,7 @@ class Sanic: after_start=after_start, before_stop=before_stop, after_stop=after_stop, ssl=ssl, sock=sock, loop=loop, protocol=protocol, backlog=backlog, stop_event=stop_event, - async_run=True) + run_async=True) # Serve proto = "http" From 6a322ba3f8076d6e914a51f2028f38114f1f0d68 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 1 Feb 2017 09:00:57 -0600 Subject: [PATCH 07/19] Updates static tests to test for issue #374 Adds a test to test for serving a static directory at the root uri '/' to address concerns found in #374. Also rewrites the tests so that they are parametrized and do more with less. --- tests/test_static.py | 44 ++++++++++++++++---------------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/tests/test_static.py b/tests/test_static.py index 82b0d1f9..629ee95c 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -16,47 +16,35 @@ def static_file_directory(): return static_directory -@pytest.fixture(scope='module') -def static_file_path(static_file_directory): - """The path to the static file that we want to serve""" - return os.path.join(static_file_directory, 'test.file') +def get_file_path(static_file_directory, file_name): + return os.path.join(static_file_directory, file_name) -@pytest.fixture(scope='module') -def static_file_content(static_file_path): +def get_file_content(static_file_directory, file_name): """The content of the static file to check""" - with open(static_file_path, 'rb') as file: + with open(get_file_path(static_file_directory, file_name), 'rb') as file: return file.read() -def test_static_file(static_file_path, static_file_content): +@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) +def test_static_file(static_file_directory, file_name): app = Sanic('test_static') - app.static('/testing.file', static_file_path) + app.static( + '/testing.file', get_file_path(static_file_directory, file_name)) request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 - assert response.body == static_file_content + assert response.body == get_file_content(static_file_directory, file_name) -def test_static_directory( - static_file_directory, static_file_path, static_file_content): +@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) +@pytest.mark.parametrize('base_uri', ['/static', '', '/dir']) +def test_static_directory(file_name, base_uri, static_file_directory): app = Sanic('test_static') - app.static('/dir', static_file_directory) + app.static(base_uri, static_file_directory) - request, response = sanic_endpoint_test(app, uri='/dir/test.file') + request, response = sanic_endpoint_test( + app, uri='{}/{}'.format(base_uri, file_name)) assert response.status == 200 - assert response.body == static_file_content - - -def test_static_url_decode_file(static_file_directory): - decode_me_path = os.path.join(static_file_directory, 'decode me.txt') - with open(decode_me_path, 'rb') as file: - decode_me_contents = file.read() - - app = Sanic('test_static') - app.static('/dir', static_file_directory) - - request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt') - assert response.status == 200 - assert response.body == decode_me_contents + assert response.body == get_file_content(static_file_directory, file_name) From 7c09ec29f716670d16429af71d5984bdc0bf9892 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 2 Feb 2017 12:21:14 -0500 Subject: [PATCH 08/19] rebase --- sanic/blueprints.py | 3 + sanic/exceptions.py | 4 + sanic/router.py | 51 ++++++-- sanic/sanic.py | 61 ++++++++- sanic/views.py | 1 + tests/test_url_building.py | 261 +++++++++++++++++++++++++++++++++++++ 6 files changed, 369 insertions(+), 12 deletions(-) create mode 100644 tests/test_url_building.py diff --git a/sanic/blueprints.py b/sanic/blueprints.py index c9a4b8ac..0c14f4bc 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -35,6 +35,9 @@ class Blueprint: # Routes for future in self.routes: + # attach the blueprint name to the handler so that it can be + # prefixed properly in the router + future.handler.__blueprintname__ = self.name # Prepend the blueprint URI prefix if available uri = url_prefix + future.uri if url_prefix else future.uri app.route( diff --git a/sanic/exceptions.py b/sanic/exceptions.py index d986cd08..201edbf6 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -120,6 +120,10 @@ class ServerError(SanicException): status_code = 500 +class URLBuildError(SanicException): + status_code = 500 + + class FileNotFound(NotFound): status_code = 404 diff --git a/sanic/router.py b/sanic/router.py index 5ed21136..25b1b9ee 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -4,8 +4,8 @@ from functools import lru_cache from .exceptions import NotFound, InvalidUsage from .views import CompositionView -Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) -Parameter = namedtuple('Parameter', ['name', 'cast']) +Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters', 'name']) +Parameter = namedtuple('Parameter', ['name', 'cast', 'pattern']) REGEX_TYPES = { 'string': (str, r'[^/]+'), @@ -59,6 +59,7 @@ class Router: routes_static = None routes_dynamic = None routes_always_check = None + parameter_pattern = re.compile(r'<(.+?)>') def __init__(self): self.routes_all = {} @@ -67,6 +68,19 @@ class Router: self.routes_always_check = [] self.hosts = None + def parse_parameter_string(self, parameter_string): + # We could receive NAME or NAME:PATTERN + name = parameter_string + pattern = 'string' + if ':' in parameter_string: + name, pattern = parameter_string.split(':', 1) + + default = (str, pattern) + # Pull from pre-configured types + _type, pattern = REGEX_TYPES.get(pattern, default) + + return name, _type, pattern + def add(self, uri, methods, handler, host=None): """ Adds a handler to the route list @@ -106,14 +120,13 @@ class Router: def add_parameter(match): # We could receive NAME or NAME:PATTERN name = match.group(1) - pattern = 'string' - if ':' in name: - name, pattern = name.split(':', 1) + name, _type, pattern = self.parse_parameter_string(name) - default = (str, pattern) - # Pull from pre-configured types - _type, pattern = REGEX_TYPES.get(pattern, default) - parameter = Parameter(name=name, cast=_type) + # store a regex for matching on a specific parameter + # this will be useful for URL building + specific_parameter_pattern = '^{}$'.format(pattern) + parameter = Parameter( + name=name, cast=_type, pattern=specific_parameter_pattern) parameters.append(parameter) # Mark the whole route as unhashable if it has the hash key in it @@ -125,7 +138,7 @@ class Router: return '({})'.format(pattern) - pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) + pattern_string = re.sub(self.parameter_pattern, add_parameter, uri) pattern = re.compile(r'^{}$'.format(pattern_string)) def merge_route(route, methods, handler): @@ -169,9 +182,17 @@ class Router: if route: route = merge_route(route, methods, handler) else: + # prefix the handler name with the blueprint name + # if available + if hasattr(handler, '__blueprintname__'): + handler_name = '{}.{}'.format( + handler.__blueprintname__, handler.__name__) + else: + handler_name = handler.__name__ + route = Route( handler=handler, methods=methods, pattern=pattern, - parameters=parameters) + parameters=parameters, name=handler_name) self.routes_all[uri] = route if properties['unhashable']: @@ -208,6 +229,14 @@ class Router: if clean_cache: self._get.cache_clear() + @lru_cache(maxsize=ROUTER_CACHE_SIZE) + def find_route_by_view_name(self, view_name): + for uri, route in self.routes_all.items(): + if route.name == view_name: + return uri, route + + return (None, None) + def get(self, request): """ Gets a request handler based on the URL of the request, or raises an diff --git a/sanic/sanic.py b/sanic/sanic.py index 8cfe08dd..11b104ac 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -3,13 +3,15 @@ from asyncio import get_event_loop from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename +import re from traceback import format_exc +from urllib.parse import urlencode, urlunparse import warnings from .config import Config from .constants import HTTP_METHODS from .exceptions import Handler -from .exceptions import ServerError +from .exceptions import ServerError, URLBuildError from .log import log from .response import HTTPResponse from .router import Router @@ -192,6 +194,63 @@ class Sanic: DeprecationWarning) return self.blueprint(*args, **kwargs) + def url_for(self, view_name: str, **kwargs): + uri, route = self.router.find_route_by_view_name(view_name) + + if not uri or not route: + raise URLBuildError( + 'Endpoint with name `{}` was not found'.format( + view_name)) + + out = uri + matched_params = re.findall( + self.router.parameter_pattern, uri) + + for match in matched_params: + name, _type, pattern = self.router.parse_parameter_string( + match) + specific_pattern = '^{}$'.format(pattern) + + supplied_param = None + if kwargs.get(name): + supplied_param = kwargs.get(name) + del kwargs[name] + else: + raise URLBuildError( + 'Required parameter `{}` was not passed to url_for'.format( + name)) + + supplied_param = str(supplied_param) + passes_pattern = re.match(specific_pattern, supplied_param) + + if not passes_pattern: + if _type != str: + msg = ( + 'Value "{}" for parameter `{}` does not ' + 'match pattern for type `{}`: {}'.format( + supplied_param, name, _type.__name__, pattern)) + else: + msg = ( + 'Value "{}" for parameter `{}` ' + 'does not satisfy pattern {}'.format( + supplied_param, name, pattern)) + raise URLBuildError(msg) + + replacement_regex = '(<{}.*?>)'.format(name) + + out = re.sub( + 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, '' + )) + + return out + # -------------------------------------------------------------------- # # Request Handling # -------------------------------------------------------------------- # diff --git a/sanic/views.py b/sanic/views.py index 407ba136..5d8e9d40 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -64,6 +64,7 @@ class HTTPMethodView: view.view_class = cls view.__doc__ = cls.__doc__ view.__module__ = cls.__module__ + view.__name__ = cls.__name__ return view diff --git a/tests/test_url_building.py b/tests/test_url_building.py new file mode 100644 index 00000000..c8209557 --- /dev/null +++ b/tests/test_url_building.py @@ -0,0 +1,261 @@ +import pytest as pytest +from urllib.parse import urlsplit, parse_qsl + +from sanic import Sanic +from sanic.response import text +from sanic.views import HTTPMethodView, CompositionView +from sanic.blueprints import Blueprint +from sanic.utils import sanic_endpoint_test +from sanic.exceptions import URLBuildError + +import string + + +def _generate_handlers_from_names(app, l): + for name in l: + # this is the easiest way to generate functions with dynamic names + exec('@app.route(name)\ndef {}(request):\n\treturn text("{}")'.format( + name, name)) + + +@pytest.fixture +def simple_app(): + app = Sanic('simple_app') + handler_names = list(string.ascii_letters) + + _generate_handlers_from_names(app, handler_names) + + return app + + +def test_simple_url_for_getting(simple_app): + for letter in string.ascii_letters: + url = simple_app.url_for(letter) + + assert url == '/{}'.format(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') + + @app.route('/fail') + def fail(): + return text('this should fail') + + with pytest.raises(URLBuildError) as e: + app.url_for('passes') + + assert str(e.value) == 'Endpoint with name `passes` was not found' + + +def test_fails_url_build_if_param_not_passed(): + url = '/' + + for letter in string.ascii_letters: + url += '<{}>/'.format(letter) + + app = Sanic('fail_url_build') + + @app.route(url) + def fail(): + return text('this should fail') + + fail_args = list(string.ascii_letters) + fail_args.pop() + + fail_kwargs = {l: l for l in fail_args} + + with pytest.raises(URLBuildError) as e: + app.url_for('fail', **fail_kwargs) + + assert 'Required parameter `Z` was not passed to url_for' in str(e.value) + + +COMPLEX_PARAM_URL = ( + '///' + '//') +PASSING_KWARGS = { + 'foo': 4, 'four_letter_string': 'woof', + 'two_letter_string': 'ba', 'normal_string': 'normal', + 'some_number': '1.001'} +EXPECTED_BUILT_URL = '/4/woof/ba/normal/1.001' + + +def test_fails_with_int_message(): + app = Sanic('fail_url_build') + + @app.route(COMPLEX_PARAM_URL) + def fail(): + return text('this should fail') + + failing_kwargs = dict(PASSING_KWARGS) + failing_kwargs['foo'] = 'not_int' + + with pytest.raises(URLBuildError) as e: + app.url_for('fail', **failing_kwargs) + + expected_error = ( + 'Value "not_int" for parameter `foo` ' + 'does not match pattern for type `int`: \d+') + assert str(e.value) == expected_error + + +def test_fails_with_two_letter_string_message(): + app = Sanic('fail_url_build') + + @app.route(COMPLEX_PARAM_URL) + def fail(): + return text('this should fail') + + failing_kwargs = dict(PASSING_KWARGS) + failing_kwargs['two_letter_string'] = 'foobar' + + with pytest.raises(URLBuildError) as e: + app.url_for('fail', **failing_kwargs) + + expected_error = ( + 'Value "foobar" for parameter `two_letter_string` ' + 'does not satisfy pattern [A-z]{2}') + + assert str(e.value) == expected_error + + +def test_fails_with_number_message(): + app = Sanic('fail_url_build') + + @app.route(COMPLEX_PARAM_URL) + def fail(): + return text('this should fail') + + failing_kwargs = dict(PASSING_KWARGS) + failing_kwargs['some_number'] = 'foo' + + with pytest.raises(URLBuildError) as e: + app.url_for('fail', **failing_kwargs) + + expected_error = ( + 'Value "foo" for parameter `some_number` ' + 'does not match pattern for type `float`: [0-9\\\\.]+') + + assert str(e.value) == expected_error + + +def test_adds_other_supplied_values_as_query_string(): + app = Sanic('passes') + + @app.route(COMPLEX_PARAM_URL) + def passes(): + return text('this should pass') + + new_kwargs = dict(PASSING_KWARGS) + new_kwargs['added_value_one'] = 'one' + new_kwargs['added_value_two'] = 'two' + + url = app.url_for('passes', **new_kwargs) + + query = dict(parse_qsl(urlsplit(url).query)) + + assert query['added_value_one'] == 'one' + assert query['added_value_two'] == 'two' + + +@pytest.fixture +def blueprint_app(): + app = Sanic('blueprints') + + first_print = Blueprint('first', url_prefix='/first') + second_print = Blueprint('second', url_prefix='/second') + + @first_print.route('/foo') + def foo(): + return text('foo from first') + + @first_print.route('/foo/') + def foo_with_param(request, param): + return text( + 'foo from first : {}'.format(param)) + + @second_print.route('/foo') # noqa + def foo(): + return text('foo from second') + + @second_print.route('/foo/') # noqa + def foo_with_param(request, param): + return text( + 'foo from second : {}'.format(param)) + + app.blueprint(first_print) + app.blueprint(second_print) + + return app + + +def test_blueprints_are_named_correctly(blueprint_app): + first_url = blueprint_app.url_for('first.foo') + assert first_url == '/first/foo' + + second_url = blueprint_app.url_for('second.foo') + assert second_url == '/second/foo' + + +def test_blueprints_work_with_params(blueprint_app): + first_url = blueprint_app.url_for('first.foo_with_param', param='bar') + assert first_url == '/first/foo/bar' + + second_url = blueprint_app.url_for('second.foo_with_param', param='bar') + assert second_url == '/second/foo/bar' + + +@pytest.fixture +def methodview_app(): + app = Sanic('methodview') + + class ViewOne(HTTPMethodView): + def get(self, request): + return text('I am get method') + + def post(self, request): + return text('I am post method') + + def put(self, request): + return text('I am put method') + + def patch(self, request): + return text('I am patch method') + + def delete(self, request): + return text('I am delete method') + + app.add_route(ViewOne.as_view('view_one'), '/view_one') + + class ViewTwo(HTTPMethodView): + def get(self, request): + return text('I am get method') + + def post(self, request): + return text('I am post method') + + def put(self, request): + return text('I am put method') + + def patch(self, request): + return text('I am patch method') + + def delete(self, request): + return text('I am delete method') + + app.add_route(ViewTwo.as_view(), '/view_two') + + return app + + +def test_methodview_naming(methodview_app): + viewone_url = methodview_app.url_for('ViewOne') + viewtwo_url = methodview_app.url_for('ViewTwo') + + assert viewone_url == '/view_one' + assert viewtwo_url == '/view_two' From f9056099f9cb69e9e7166bcd5ae79974ce37ca94 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 2 Feb 2017 12:52:48 -0500 Subject: [PATCH 09/19] all works --- docs/sanic/blueprints.md | 19 +++++++++++++++++++ docs/sanic/class_based_views.md | 25 ++++++++++++++++++++++++- docs/sanic/routing.md | 32 ++++++++++++++++++++++++++++++++ docs/sanic/static_files.md | 2 ++ sanic/sanic.py | 28 +++++++++++++++++++++++++++- tests/test_url_building.py | 2 +- 6 files changed, 105 insertions(+), 3 deletions(-) diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md index 71849ac1..120aa1dd 100644 --- a/docs/sanic/blueprints.md +++ b/docs/sanic/blueprints.md @@ -158,3 +158,22 @@ app.blueprint(blueprint_v2) app.run(host='0.0.0.0', port=8000, debug=True) ``` + +## URL Building with `url_for` + +If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name +takes the format `.`. For example: + +``` +@blueprint_v1.route('/') +async def root(request): + url = app.url_for('v1.post_handler', post_id=5) + return redirect(url) + + +@blueprint_v1.route('/post/') +async def post_handler(request, post_id): + return text('Post {} in Blueprint V1'.format(post_id)) +``` + + diff --git a/docs/sanic/class_based_views.md b/docs/sanic/class_based_views.md index 5f99a0b5..02b02140 100644 --- a/docs/sanic/class_based_views.md +++ b/docs/sanic/class_based_views.md @@ -67,7 +67,7 @@ app.add_route(NameView.as_view(), '/') If you want to add any decorators to the class, you can set the `decorators` class variable. These will be applied to the class when `as_view` is called. -``` +```python class ViewWithDecorator(HTTPMethodView): decorators = [some_decorator_here] @@ -77,6 +77,27 @@ class ViewWithDecorator(HTTPMethodView): app.add_route(ViewWithDecorator.as_view(), '/url') ``` +#### URL Building + +If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint +that you will pass into `url_for`. For example: + +```python +@app.route('/') +def index(request): + url = app.url_for('SpecialClassView') + return redirect(url) + + +class SpecialClassView(HTTPMethodView): + def get(self, request): + return text('Hello from the Special Class View!') + + +app.add_route(SpecialClassView.as_view(), '/special_class_view') +``` + + ## Using CompositionView As an alternative to the `HTTPMethodView`, you can use `CompositionView` to @@ -106,3 +127,5 @@ view.add(['POST', 'PUT'], lambda request: text('I am a post/put method')) # Use the new view to handle requests to the base URL app.add_route(view, '/') ``` + +Note: currently you cannot build a URL for a CompositionView using `url_for`. diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 2dda39c4..84945904 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -119,3 +119,35 @@ app.add_route(handler1, '/test') app.add_route(handler2, '/folder/') app.add_route(person_handler2, '/person/', methods=['GET']) ``` + +## URL building with `url_for`. + +Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example: + +``` +@app.route('/') +async def index(request): + # generate a URL for the endpoint `post_handler` + url = app.url_for('post_handler', post_id=5) + # the URL is `/posts/5`, redirect to it + return redirect(url) + + +@app.route('/posts/') +async def post_handler(request, post_id): + return text('Post - {}'.format(post_id)) +``` + +Other things to keep in mind when using `url_for`: + +- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example: +``` +url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two') +# /posts/5?arg_one=one&arg_two=two +``` +- 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/docs/sanic/static_files.md b/docs/sanic/static_files.md index d7e4866d..f0ce9d78 100644 --- a/docs/sanic/static_files.md +++ b/docs/sanic/static_files.md @@ -17,3 +17,5 @@ app.static('/the_best.png', '/home/ubuntu/test.png') app.run(host="0.0.0.0", port=8000) ``` + +Note: currently you cannot build a URL for a static file using `url_for`. diff --git a/sanic/sanic.py b/sanic/sanic.py index 11b104ac..2d2f83fe 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -195,6 +195,26 @@ class Sanic: return self.blueprint(*args, **kwargs) def url_for(self, view_name: str, **kwargs): + """Builds a URL based on a view name and the values provided. + + In order to build a URL, all request parameters must be supplied as + keyword arguments, and each parameter must pass the test for the + specified parameter type. If these conditions are not met, a + `URLBuildError` will be thrown. + + Keyword arguments that are not request parameters will be included in + the output URL's query string. + + :param view_name: A string referencing the view name + :param **kwargs: keys and values that are used to build request + parameters and query string arguments. + + :return: the built URL + + Raises: + URLBuildError + """ + # find the route by the supplied view name uri, route = self.router.find_route_by_view_name(view_name) if not uri or not route: @@ -203,15 +223,18 @@ class Sanic: view_name)) out = uri + + # find all the parameters we will need to build in the URL matched_params = re.findall( self.router.parameter_pattern, uri) for match in matched_params: name, _type, pattern = self.router.parse_parameter_string( match) + # we only want to match against each individual parameter specific_pattern = '^{}$'.format(pattern) - supplied_param = None + if kwargs.get(name): supplied_param = kwargs.get(name) del kwargs[name] @@ -221,6 +244,8 @@ class Sanic: name)) supplied_param = str(supplied_param) + # determine if the parameter supplied by the caller passes the test + # in the URL passes_pattern = re.match(specific_pattern, supplied_param) if not passes_pattern: @@ -236,6 +261,7 @@ class Sanic: supplied_param, name, pattern)) raise URLBuildError(msg) + # replace the parameter in the URL with the supplied value replacement_regex = '(<{}.*?>)'.format(name) out = re.sub( diff --git a/tests/test_url_building.py b/tests/test_url_building.py index c8209557..480313b3 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -3,7 +3,7 @@ from urllib.parse import urlsplit, parse_qsl from sanic import Sanic from sanic.response import text -from sanic.views import HTTPMethodView, CompositionView +from sanic.views import HTTPMethodView from sanic.blueprints import Blueprint from sanic.utils import sanic_endpoint_test from sanic.exceptions import URLBuildError From 5632d073bedefac3c70dbaa41d16ee1ec2c4a922 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 2 Feb 2017 13:00:15 -0500 Subject: [PATCH 10/19] update docs --- sanic/router.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 25b1b9ee..c065ffd4 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -4,8 +4,10 @@ from functools import lru_cache from .exceptions import NotFound, InvalidUsage from .views import CompositionView -Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters', 'name']) -Parameter = namedtuple('Parameter', ['name', 'cast', 'pattern']) +Route = namedtuple( + 'Route', + ['handler', 'methods', 'pattern', 'parameters', 'name']) +Parameter = namedtuple('Parameter', ['name', 'cast']) REGEX_TYPES = { 'string': (str, r'[^/]+'), @@ -69,6 +71,16 @@ class Router: self.hosts = None def parse_parameter_string(self, parameter_string): + """ + Parse a parameter string into its constituent name, type, and pattern + For example: + `parse_parameter_string(' + ('param_one', str, '[A-z]') + + :param parameter_string: String to parse + :return: tuple containing + (parameter_name, parameter_type, parameter_pattern) + """ # We could receive NAME or NAME:PATTERN name = parameter_string pattern = 'string' @@ -118,15 +130,11 @@ class Router: properties = {"unhashable": None} def add_parameter(match): - # We could receive NAME or NAME:PATTERN name = match.group(1) name, _type, pattern = self.parse_parameter_string(name) - # store a regex for matching on a specific parameter - # this will be useful for URL building - specific_parameter_pattern = '^{}$'.format(pattern) parameter = Parameter( - name=name, cast=_type, pattern=specific_parameter_pattern) + name=name, cast=_type) parameters.append(parameter) # Mark the whole route as unhashable if it has the hash key in it @@ -231,6 +239,12 @@ class Router: @lru_cache(maxsize=ROUTER_CACHE_SIZE) def find_route_by_view_name(self, view_name): + """ + Find a route in the router based on the specified view name. + + :param view_name: string of view name to search by + :return: tuple containing (uri, Route) + """ for uri, route in self.routes_all.items(): if route.name == view_name: return uri, route From 5c63ce666c1be3b4d5ef9272da459ef10c9770e8 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 2 Feb 2017 14:21:59 -0500 Subject: [PATCH 11/19] punctuation --- docs/sanic/routing.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 84945904..0cadd707 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -120,11 +120,11 @@ app.add_route(handler2, '/folder/') app.add_route(person_handler2, '/person/', methods=['GET']) ``` -## URL building with `url_for`. +## URL building with `url_for` Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example: -``` +```python @app.route('/') async def index(request): # generate a URL for the endpoint `post_handler` @@ -141,7 +141,7 @@ async def post_handler(request, post_id): Other things to keep in mind when using `url_for`: - Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example: -``` +```python url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two') # /posts/5?arg_one=one&arg_two=two ``` From 884749f345b951c7bc4d8d01dcfb405df6999f2e Mon Sep 17 00:00:00 2001 From: Fengyuan Chen Date: Sat, 4 Feb 2017 15:27:46 +0800 Subject: [PATCH 12/19] fix run_async demo --- examples/run_async.py | 4 +++- sanic/server.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/run_async.py b/examples/run_async.py index 71912e5a..d514c7d0 100644 --- a/examples/run_async.py +++ b/examples/run_async.py @@ -3,6 +3,7 @@ from sanic.response import json from multiprocessing import Event from signal import signal, SIGINT import asyncio +import uvloop app = Sanic(__name__) @@ -10,10 +11,11 @@ app = Sanic(__name__) async def test(request): return json({"answer": "42"}) +asyncio.set_event_loop(uvloop.new_event_loop()) server = app.create_server(host="0.0.0.0", port=8001) loop = asyncio.get_event_loop() task = asyncio.ensure_future(server) -signal(SIGINT, lambda s, f: loop.close()) +signal(SIGINT, lambda s, f: loop.stop()) try: loop.run_forever() except: diff --git a/sanic/server.py b/sanic/server.py index 48c3827e..8b9ed532 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -297,8 +297,9 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param protocol: Subclass of asyncio protocol class :return: Nothing """ - loop = async_loop.new_event_loop() - asyncio.set_event_loop(loop) + if not run_async: + loop = async_loop.new_event_loop() + asyncio.set_event_loop(loop) if debug: loop.set_debug(debug) From 29680cb515043da2040e950163ae3f9531c1cd58 Mon Sep 17 00:00:00 2001 From: Fengyuan Chen Date: Sat, 4 Feb 2017 15:54:37 +0800 Subject: [PATCH 13/19] fix always warning loop is passed issue --- sanic/sanic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 8cfe08dd..3f96f27f 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -360,18 +360,18 @@ class Sanic: Helper function used by `run` and `create_server`. """ - self.error_handler.debug = debug - self.debug = debug - self.loop = loop = get_event_loop() - if loop is not None: - if self.debug: + if debug: warnings.simplefilter('default') warnings.warn("Passing a loop will be deprecated in version" " 0.4.0 https://github.com/channelcat/sanic/" "pull/335 has more information.", DeprecationWarning) + self.error_handler.debug = debug + self.debug = debug + self.loop = loop = get_event_loop() + server_settings = { 'protocol': protocol, 'host': host, From a15ee3ad0667193fa358cfd8293d6e758413bb5e Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 2 Feb 2017 18:51:33 +0900 Subject: [PATCH 14/19] Fix sanic_endpoint_test working with redirects Before fix, it raises error like: ``` tests/test_utils.py F ================================= FAILURES ================================= ______________________________ test_redirect _______________________________ app = , method = 'get', uri = '/1', gather_request = True, debug = False server_kwargs = {}, request_args = (), request_kwargs = {} _collect_request = ._collect_request at 0x1045ec950> _collect_response = ._collect_response at 0x1045ec7b8> def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, debug=False, server_kwargs={}, *request_args, **request_kwargs): results = [] exceptions = [] if gather_request: def _collect_request(request): results.append(request) app.request_middleware.appendleft(_collect_request) async def _collect_response(sanic, loop): try: response = await local_request(method, uri, *request_args, **request_kwargs) results.append(response) except Exception as e: exceptions.append(e) app.stop() app.run(host=HOST, debug=debug, port=PORT, after_start=_collect_response, **server_kwargs) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) if gather_request: try: > request, response = results E ValueError: too many values to unpack (expected 2) sanic/utils.py:47: ValueError During handling of the above exception, another exception occurred: utils_app = def test_redirect(utils_app): """Test sanic_endpoint_test is working for redirection""" > request, response = sanic_endpoint_test(utils_app, uri='/1') tests/test_utils.py:33: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ app = , method = 'get', uri = '/1', gather_request = True, debug = False server_kwargs = {}, request_args = (), request_kwargs = {} _collect_request = ._collect_request at 0x1045ec950> _collect_response = ._collect_response at 0x1045ec7b8> def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, debug=False, server_kwargs={}, *request_args, **request_kwargs): results = [] exceptions = [] if gather_request: def _collect_request(request): results.append(request) app.request_middleware.appendleft(_collect_request) async def _collect_response(sanic, loop): try: response = await local_request(method, uri, *request_args, **request_kwargs) results.append(response) except Exception as e: exceptions.append(e) app.stop() app.run(host=HOST, debug=debug, port=PORT, after_start=_collect_response, **server_kwargs) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) if gather_request: try: request, response = results return request, response except: raise ValueError( "Request and response object expected, got ({})".format( > results)) E ValueError: Request and response object expected, got ([{}, {}, {}, E E ]) sanic/utils.py:52: ValueError ``` --- sanic/utils.py | 9 ++-- tests/test_redirect.py | 96 ++++++++++++++++++++++++++++++++++++++++++ tests/test_requests.py | 76 ++------------------------------- 3 files changed, 104 insertions(+), 77 deletions(-) create mode 100644 tests/test_redirect.py diff --git a/sanic/utils.py b/sanic/utils.py index 644a2a22..8e8f8124 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -19,19 +19,20 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, debug=False, server_kwargs={}, *request_args, **request_kwargs): - results = [] + results = [None, None] exceptions = [] if gather_request: def _collect_request(request): - results.append(request) + if results[0] is None: + results[0] = request app.request_middleware.appendleft(_collect_request) async def _collect_response(sanic, loop): try: response = await local_request(method, uri, *request_args, **request_kwargs) - results.append(response) + results[-1] = response except Exception as e: exceptions.append(e) app.stop() @@ -52,7 +53,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, results)) else: try: - return results[0] + return results[-1] except: raise ValueError( "Request object expected, got ({})".format(results)) diff --git a/tests/test_redirect.py b/tests/test_redirect.py new file mode 100644 index 00000000..b2b72c35 --- /dev/null +++ b/tests/test_redirect.py @@ -0,0 +1,96 @@ +import pytest + +from sanic import Sanic +from sanic.response import text, redirect +from sanic.utils import sanic_endpoint_test + + +@pytest.fixture +def redirect_app(): + app = Sanic('test_redirection') + + @app.route('/redirect_init') + async def redirect_init(request): + return redirect("/redirect_target") + + @app.route('/redirect_init_with_301') + async def redirect_init_with_301(request): + return redirect("/redirect_target", status=301) + + @app.route('/redirect_target') + async def redirect_target(request): + return text('OK') + + @app.route('/1') + def handler(request): + return redirect('/2') + + @app.route('/2') + def handler(request): + return redirect('/3') + + @app.route('/3') + def handler(request): + return text('OK') + + return app + + +def test_redirect_default_302(redirect_app): + """ + We expect a 302 default status code and the headers to be set. + """ + request, response = sanic_endpoint_test( + redirect_app, method="get", + uri="/redirect_init", + allow_redirects=False) + + assert response.status == 302 + assert response.headers["Location"] == "/redirect_target" + assert response.headers["Content-Type"] == 'text/html; charset=utf-8' + + +def test_redirect_headers_none(redirect_app): + request, response = sanic_endpoint_test( + redirect_app, method="get", + uri="/redirect_init", + headers=None, + allow_redirects=False) + + assert response.status == 302 + assert response.headers["Location"] == "/redirect_target" + + +def test_redirect_with_301(redirect_app): + """ + Test redirection with a different status code. + """ + request, response = sanic_endpoint_test( + redirect_app, method="get", + uri="/redirect_init_with_301", + allow_redirects=False) + + assert response.status == 301 + assert response.headers["Location"] == "/redirect_target" + + +def test_get_then_redirect_follow_redirect(redirect_app): + """ + With `allow_redirects` we expect a 200. + """ + response = sanic_endpoint_test( + redirect_app, method="get", + uri="/redirect_init", gather_request=False, + allow_redirects=True) + + assert response.status == 200 + assert response.text == 'OK' + + +def test_chained_redirect(redirect_app): + """Test sanic_endpoint_test is working for redirection""" + request, response = sanic_endpoint_test(redirect_app, uri='/1') + assert request.url.endswith('/1') + assert response.status == 200 + assert response.text == 'OK' + assert response.url.endswith('/3') diff --git a/tests/test_requests.py b/tests/test_requests.py index b2ee8e78..985f5b00 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -58,7 +58,7 @@ def test_non_str_headers(): request, response = sanic_endpoint_test(app) assert response.headers.get('answer') == '42' - + def test_invalid_response(): app = Sanic('test_invalid_response') @@ -73,8 +73,8 @@ def test_invalid_response(): request, response = sanic_endpoint_test(app) assert response.status == 500 assert response.text == "Internal Server Error." - - + + def test_json(): app = Sanic('test_json') @@ -189,73 +189,3 @@ def test_post_form_multipart_form_data(): request, response = sanic_endpoint_test(app, data=payload, headers=headers) assert request.form.get('test') == 'OK' - - -@pytest.fixture -def redirect_app(): - app = Sanic('test_redirection') - - @app.route('/redirect_init') - async def redirect_init(request): - return redirect("/redirect_target") - - @app.route('/redirect_init_with_301') - async def redirect_init_with_301(request): - return redirect("/redirect_target", status=301) - - @app.route('/redirect_target') - async def redirect_target(request): - return text('OK') - - return app - - -def test_redirect_default_302(redirect_app): - """ - We expect a 302 default status code and the headers to be set. - """ - request, response = sanic_endpoint_test( - redirect_app, method="get", - uri="/redirect_init", - allow_redirects=False) - - assert response.status == 302 - assert response.headers["Location"] == "/redirect_target" - assert response.headers["Content-Type"] == 'text/html; charset=utf-8' - - -def test_redirect_headers_none(redirect_app): - request, response = sanic_endpoint_test( - redirect_app, method="get", - uri="/redirect_init", - headers=None, - allow_redirects=False) - - assert response.status == 302 - assert response.headers["Location"] == "/redirect_target" - - -def test_redirect_with_301(redirect_app): - """ - Test redirection with a different status code. - """ - request, response = sanic_endpoint_test( - redirect_app, method="get", - uri="/redirect_init_with_301", - allow_redirects=False) - - assert response.status == 301 - assert response.headers["Location"] == "/redirect_target" - - -def test_get_then_redirect_follow_redirect(redirect_app): - """ - With `allow_redirects` we expect a 200. - """ - response = sanic_endpoint_test( - redirect_app, method="get", - uri="/redirect_init", gather_request=False, - allow_redirects=True) - - assert response.status == 200 - assert response.text == 'OK' From 82f383b64f9051d3bba28e3fa67ca8b9d9058c4a Mon Sep 17 00:00:00 2001 From: Brian Bates Date: Sun, 5 Feb 2017 11:44:01 -0800 Subject: [PATCH 15/19] Add missing dependency Added missing `aiofiles` to tox.ini and cleaned up requirements files. --- requirements-dev.txt | 28 ++++++++++++++-------------- requirements.txt | 2 +- tox.ini | 7 ++++--- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 64542931..1f11a90c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,18 +1,18 @@ -httptools -ujson -uvloop -aiohttp aiocache -pytest -coverage -tox -gunicorn -bottle -kyoukai -falcon -tornado aiofiles +aiohttp +beautifulsoup4 +bottle +coverage +falcon +gunicorn +httptools +kyoukai +pytest +recommonmark sphinx sphinx_rtd_theme -recommonmark -beautifulsoup4 +tornado +tox +ujson +uvloop diff --git a/requirements.txt b/requirements.txt index cef8660e..724a5835 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ +aiofiles httptools ujson uvloop -aiofiles diff --git a/tox.ini b/tox.ini index 009d971c..4155e909 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,16 @@ [tox] - envlist = py35, py36, flake8 -[travis] +[travis] python = 3.5: py35, flake8 3.6: py36, flake8 -[testenv] +[testenv] deps = + aiofiles aiohttp pytest beautifulsoup4 @@ -18,6 +18,7 @@ deps = commands = pytest tests {posargs} + [testenv:flake8] deps = flake8 From aa547859186760e933780fb9f76a2405a2b9b8f4 Mon Sep 17 00:00:00 2001 From: Fengyuan Chen Date: Sat, 4 Feb 2017 15:54:37 +0800 Subject: [PATCH 16/19] fix always warning loop is passed issue --- sanic/sanic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 2d2f83fe..a6a65204 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -445,18 +445,18 @@ class Sanic: Helper function used by `run` and `create_server`. """ - self.error_handler.debug = debug - self.debug = debug - self.loop = loop = get_event_loop() - if loop is not None: - if self.debug: + if debug: warnings.simplefilter('default') warnings.warn("Passing a loop will be deprecated in version" " 0.4.0 https://github.com/channelcat/sanic/" "pull/335 has more information.", DeprecationWarning) + self.error_handler.debug = debug + self.debug = debug + self.loop = loop = get_event_loop() + server_settings = { 'protocol': protocol, 'host': host, From 36d519026f3b02772d961820449f8810a2c9b19c Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Fri, 3 Feb 2017 10:12:33 -0500 Subject: [PATCH 17/19] reject unnamed handlers --- sanic/router.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index c065ffd4..24d0438f 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -196,7 +196,7 @@ class Router: handler_name = '{}.{}'.format( handler.__blueprintname__, handler.__name__) else: - handler_name = handler.__name__ + handler_name = getattr(handler, '__name__', None) route = Route( handler=handler, methods=methods, pattern=pattern, @@ -245,6 +245,9 @@ class Router: :param view_name: string of view name to search by :return: tuple containing (uri, Route) """ + if not view_name: + return (None, None) + for uri, route in self.routes_all.items(): if route.name == view_name: return uri, route From 413c92c6314e5f37d6356399fd6cc091cb870042 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Tue, 31 Jan 2017 14:29:16 +0900 Subject: [PATCH 18/19] Let exception handler handle inherited exceptions Original sanic exception handler only could handle exact matching exceptions. New `lookup` method will provide ability to looking up their ancestors without additional cost of performance. --- sanic/exceptions.py | 25 +++++++++++++++++++++---- tests/test_exceptions_handler.py | 12 ++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index d986cd08..dd4beee8 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -139,9 +139,12 @@ class PayloadTooLarge(SanicException): class Handler: handlers = None + cached_handlers = None + _missing = object() def __init__(self): - self.handlers = {} + self.handlers = [] + self.cached_handlers = {} self.debug = False def _render_traceback_html(self, exception, request): @@ -160,7 +163,18 @@ class Handler: uri=request.url) def add(self, exception, handler): - self.handlers[exception] = handler + self.handlers.append((exception, handler)) + + def lookup(self, exception): + handler = self.cached_handlers.get(exception, self._missing) + if handler is self._missing: + for exception_class, handler in self.handlers: + if isinstance(exception, exception_class): + self.cached_handlers[type(exception)] = handler + return handler + self.cached_handlers[type(exception)] = None + handler = None + return handler def response(self, request, exception): """ @@ -170,9 +184,12 @@ class Handler: :param exception: Exception to handle :return: Response object """ - handler = self.handlers.get(type(exception), self.default) + handler = self.lookup(exception) try: - response = handler(request=request, exception=exception) + response = handler and handler( + request=request, exception=exception) + if response is None: + response = self.default(request=request, exception=exception) except: log.error(format_exc()) if self.debug: diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index c56713b6..d11f7380 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -28,6 +28,13 @@ def handler_4(request): return text(foo) +@exception_handler_app.route('/5') +def handler_5(request): + class CustomServerError(ServerError): + pass + raise CustomServerError('Custom server error') + + @exception_handler_app.exception(NotFound, ServerError) def handler_exception(request, exception): return text("OK") @@ -71,3 +78,8 @@ def test_html_traceback_output_in_debug_mode(): assert ( "NameError: name 'bar' " "is not defined while handling uri /4") == summary_text + + +def test_inherited_exception_handler(): + request, response = sanic_endpoint_test(exception_handler_app, uri='/5') + assert response.status == 200 From b7f7883fb78cc57cbcf0e6dcfce1a7a69dba50a9 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 8 Feb 2017 19:22:51 -0600 Subject: [PATCH 19/19] Increment to v0.3.1 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index a0d0552f..90bb85d0 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.3.0' +__version__ = '0.3.1' __all__ = ['Sanic', 'Blueprint']