From a94a2d46d087f99b1b19bc6b8ad4b95d4dbec26d Mon Sep 17 00:00:00 2001 From: cosven Date: Mon, 15 Jan 2018 14:55:36 +0800 Subject: [PATCH 01/39] use single quote in readme.rst As we use single quote in sanic package, we may be supposed to use single quote in readme also? --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 10bc8920..7f8ba439 100644 --- a/README.rst +++ b/README.rst @@ -21,12 +21,12 @@ Hello World Example app = Sanic() - @app.route("/") + @app.route('/') async def test(request): - return json({"hello": "world"}) + return json({'hello': 'world'}) - if __name__ == "__main__": - app.run(host="0.0.0.0", port=8000) + if __name__ == '__main__': + app.run(host='0.0.0.0', port=8000) Installation ------------ From 11017902be8bfb33d38c773f24bffe33cc741e7f Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 15 Jan 2018 11:23:49 -0800 Subject: [PATCH 02/39] signal handling --- sanic/server.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index e9790dae..2c549be2 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -20,9 +20,10 @@ from httptools import HttpRequestParser from httptools.parser.errors import HttpParserError try: - import uvloop as async_loop + import uvloop + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) except ImportError: - async_loop = asyncio + pass from sanic.log import logger, access_logger from sanic.response import HTTPResponse @@ -509,11 +510,11 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=60, response_timeout=60, keep_alive_timeout=5, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, - register_sys_signals=True, run_async=False, connections=None, - signal=Signal(), request_class=None, access_log=True, - keep_alive=True, is_request_stream=False, router=None, - websocket_max_size=None, websocket_max_queue=None, state=None, - graceful_shutdown_timeout=15.0): + register_sys_signals=True, run_multiple=False, run_async=False, + connections=None, signal=Signal(), request_class=None, + access_log=True, keep_alive=True, is_request_stream=False, + router=None, websocket_max_size=None, websocket_max_queue=None, + state=None, graceful_shutdown_timeout=15.0): """Start asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -547,7 +548,10 @@ def serve(host, port, request_handler, error_handler, before_start=None, :return: Nothing """ if not run_async: - loop = async_loop.new_event_loop() + # create new event_loop after fork + asyncio.get_event_loop().close() + + loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) if debug: @@ -605,7 +609,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, # Register signals for graceful termination if register_sys_signals: - for _signal in (SIGINT, SIGTERM): + _singals = (SIGTERM,) if run_multiple else (SIGINT, SIGTERM) + for _signal in _singals: try: loop.add_signal_handler(_signal, loop.stop) except NotImplementedError: @@ -668,6 +673,7 @@ def serve_multiple(server_settings, workers): :return: """ server_settings['reuse_port'] = True + server_settings['run_multiple'] = True # Handling when custom socket is not provided. if server_settings.get('sock') is None: @@ -682,7 +688,7 @@ def serve_multiple(server_settings, workers): def sig_handler(signal, frame): logger.info("Received signal %s. Shutting down.", Signals(signal).name) for process in processes: - os.kill(process.pid, SIGINT) + os.kill(process.pid, SIGTERM) signal_func(SIGINT, lambda s, f: sig_handler(s, f)) signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) From 6a61fce84e5f14a56ef6c26e72523ab81d4504b4 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 15 Jan 2018 11:53:15 -0800 Subject: [PATCH 03/39] worker process should ignore SIGINT when run_multiple --- sanic/server.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 2c549be2..5855224b 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -5,7 +5,7 @@ from functools import partial from inspect import isawaitable from multiprocessing import Process from signal import ( - SIGTERM, SIGINT, + SIGTERM, SIGINT, SIG_IGN, signal as signal_func, Signals ) @@ -607,6 +607,10 @@ def serve(host, port, request_handler, error_handler, before_start=None, trigger_events(after_start, loop) + # Ignore SIGINT when run_multiple + if run_multiple: + signal_func(SIGINT, SIG_IGN) + # Register signals for graceful termination if register_sys_signals: _singals = (SIGTERM,) if run_multiple else (SIGINT, SIGTERM) @@ -694,6 +698,7 @@ def serve_multiple(server_settings, workers): signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) processes = [] + for _ in range(workers): process = Process(target=serve, kwargs=server_settings) process.daemon = True From 09d6452475dc05ac640cd754d60007f105a18065 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 15 Jan 2018 15:15:08 -0800 Subject: [PATCH 04/39] fixed unit test --- sanic/server.py | 2 -- tests/test_multiprocessing.py | 1 - 2 files changed, 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 5855224b..2f2df444 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -549,8 +549,6 @@ def serve(host, port, request_handler, error_handler, before_start=None, """ if not run_async: # create new event_loop after fork - asyncio.get_event_loop().close() - loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 519ad171..cdbd91a8 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -23,4 +23,3 @@ def test_multiprocessing(): app.run(HOST, app.test_port, workers=num_workers) assert len(process_list) == num_workers - From 6d0b30953a079d45a7b35f1646e6e1181be0229e Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 15 Jan 2018 17:40:44 -0800 Subject: [PATCH 05/39] add unit test which should fail on original code --- tests/test_routes.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_routes.py b/tests/test_routes.py index 84d6b221..969128f8 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -907,3 +907,27 @@ def test_unicode_routes(): request, response = app.test_client.get('/overload/你好') assert response.text == 'OK2 你好' + + +def test_uri_with_different_method_and_different_params(): + app = Sanic('test_uri') + + @app.route('/ads/', methods=['GET']) + async def ad_get(request, ad_id): + return json({'ad_id': ad_id}) + + @app.route('/ads/', methods=['POST']) + async def ad_post(request, action): + return json({'action': action}) + + request, response = app.test_client.get('/ads/1234') + assert response.status == 200 + assert response.json == { + 'ad_id': '/ads/1234' + } + + request, response = app.test_client.post('/ads/post') + assert response.status == 200 + assert response.json == { + 'action': 'post' + } From d9002769cf26be9e36ffb788e2f4e71f03e1ca47 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 15 Jan 2018 17:49:11 -0800 Subject: [PATCH 06/39] fix a typo --- tests/test_routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_routes.py b/tests/test_routes.py index 969128f8..5f3b3376 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -2,7 +2,7 @@ import asyncio import pytest from sanic import Sanic -from sanic.response import text +from sanic.response import text, json from sanic.router import RouteExists, RouteDoesNotExist from sanic.constants import HTTP_METHODS @@ -923,7 +923,7 @@ def test_uri_with_different_method_and_different_params(): request, response = app.test_client.get('/ads/1234') assert response.status == 200 assert response.json == { - 'ad_id': '/ads/1234' + 'ad_id': '1234' } request, response = app.test_client.post('/ads/post') From 7daebc6aea2dad8484a3959494e830e0b2927b65 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 15 Jan 2018 17:53:37 -0800 Subject: [PATCH 07/39] fix Router.check_dynamic_route_exists --- sanic/router.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index bd74c11d..052ff1bf 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -234,11 +234,11 @@ class Router: if properties['unhashable']: routes_to_check = self.routes_always_check ndx, route = self.check_dynamic_route_exists( - pattern, routes_to_check) + pattern, routes_to_check, parameters) else: routes_to_check = self.routes_dynamic[url_hash(uri)] ndx, route = self.check_dynamic_route_exists( - pattern, routes_to_check) + pattern, routes_to_check, parameters) if ndx != -1: # Pop the ndx of the route, no dups of the same route routes_to_check.pop(ndx) @@ -285,9 +285,9 @@ class Router: self.routes_static[uri] = route @staticmethod - def check_dynamic_route_exists(pattern, routes_to_check): + def check_dynamic_route_exists(pattern, routes_to_check, parameters): for ndx, route in enumerate(routes_to_check): - if route.pattern == pattern: + if route.pattern == pattern and route.parameters == parameters: return ndx, route else: return -1, None From 9677158b75e9091578fd58f4a695d622c8f1ad79 Mon Sep 17 00:00:00 2001 From: Matt Fox Date: Wed, 17 Jan 2018 07:31:39 -0800 Subject: [PATCH 08/39] Add request.method to documentation --- docs/sanic/request_data.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index 4f6bc970..a91dd970 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -73,6 +73,8 @@ The following variables are accessible as properties on `Request` objects: - `headers` (dict) - A case-insensitive dictionary that contains the request headers. +- `method` (str) - HTTP method of the request (ie `GET`, `POST`). + - `ip` (str) - IP address of the requester. - `port` (str) - Port address of the requester. From a10d7469cdeeaa4bdd94d508520e84255bdddb0b Mon Sep 17 00:00:00 2001 From: Eli Date: Thu, 18 Jan 2018 17:20:51 -0800 Subject: [PATCH 09/39] Add blueprint groups + nesting --- docs/sanic/blueprints.md | 67 ++++++++++++++++++++++++++++++++++++++++ sanic/app.py | 6 +++- sanic/blueprints.py | 22 ++++++++++++- tests/test_blueprints.py | 41 ++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md index 1a7c5293..53aef5fd 100644 --- a/docs/sanic/blueprints.md +++ b/docs/sanic/blueprints.md @@ -51,6 +51,73 @@ will look like: [Route(handler=, methods=None, pattern=re.compile('^/$'), parameters=[])] ``` +## Blueprint groups and nesting + +Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example: + +``` +api/ +├──content/ +│ ├──authors.py +│ ├──static.py +│ └──__init__.py +├──info.py +└──__init__.py +app.py +``` + +Initialization of this app's blueprint hierarchy could go as follows: + +```python +# api/content/authors.py +from sanic import Blueprint + +authors = Blueprint('content_authors', url_prefix='/authors') +``` +```python +# api/content/static.py +from sanic import Blueprint + +static = Blueprint('content_static', url_prefix='/static') +``` +```python +# api/content/__init__.py +from sanic import Blueprint + +from .static import static +from .authors import authors + +content = Blueprint.group(assets, authors, url_prefix='/content') +``` +```python +# api/info.py +from sanic import Blueprint + +info = Blueprint('info', url_prefix='/info') +``` +```python +# api/__init__.py +from sanic import Blueprint + +from .content import content +from .info import info + +api = Blueprint.group(content, info, url_prefix='/api') +``` + +And registering these blueprints in `app.py` can now be done like so: + +```python +# app.py +from sanic import Sanic + +from .api import api + +app = Sanic(__name__) + +app.blueprint(api) +``` + ## Using blueprints Blueprints have much the same functionality as an application instance. diff --git a/sanic/app.py b/sanic/app.py index 26ef83b0..92a4dfb2 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -372,10 +372,14 @@ class Sanic: def blueprint(self, blueprint, **options): """Register a blueprint on the application. - :param blueprint: Blueprint object + :param blueprint: Blueprint object or (list, tuple) thereof :param options: option dictionary with blueprint defaults :return: Nothing """ + if isinstance(blueprint, (list, tuple)): + for item in blueprint: + self.blueprint(item, **options) + return if blueprint.name in self.blueprints: assert self.blueprints[blueprint.name] is blueprint, \ 'A blueprint with the name "%s" is already registered. ' \ diff --git a/sanic/blueprints.py b/sanic/blueprints.py index f9159168..084013e1 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -14,7 +14,6 @@ FutureStatic = namedtuple('Route', class Blueprint: - def __init__(self, name, url_prefix=None, host=None, version=None, @@ -38,6 +37,27 @@ class Blueprint: self.version = version self.strict_slashes = strict_slashes + @staticmethod + def group(*blueprints, url_prefix=''): + """Create a list of blueprints, optionally + grouping them under a general URL prefix. + + :param blueprints: blueprints to be registered as a group + :param url_prefix: URL route to be prepended to all sub-prefixes + """ + def chain(nested): + """itertools.chain() but leaves strings untouched""" + for i in nested: + if isinstance(i, (list, tuple)): + yield from chain(i) + else: + yield i + bps = [] + for bp in chain(blueprints): + bp.url_prefix = url_prefix + bp.url_prefix + bps.append(bp) + return bps + def register(self, app, options): """Register the blueprint to the sanic app.""" diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 7e713da6..4c321646 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -446,3 +446,44 @@ def test_bp_shorthand(): 'Sec-WebSocket-Version': '13'}) assert response.status == 101 assert ev.is_set() + +def test_bp_group(): + app = Sanic('test_nested_bp_groups') + + deep_0 = Blueprint('deep_0', url_prefix='/deep') + deep_1 = Blueprint('deep_1', url_prefix = '/deep1') + + @deep_0.route('/') + def handler(request): + return text('D0_OK') + + @deep_1.route('/bottom') + def handler(request): + return text('D1B_OK') + + mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid') + mid_1 = Blueprint('mid_tier', url_prefix='/mid1') + + @mid_1.route('/') + def handler(request): + return text('M1_OK') + + top = Blueprint.group(mid_0, mid_1) + + app.blueprint(top) + + @app.route('/') + def handler(request): + return text('TOP_OK') + + request, response = app.test_client.get('/') + assert response.text == 'TOP_OK' + + request, response = app.test_client.get('/mid1') + assert response.text == 'M1_OK' + + request, response = app.test_client.get('/mid/deep') + assert response.text == 'D0_OK' + + request, response = app.test_client.get('/mid/deep1/bottom') + assert response.text == 'D1B_OK' From 4036f1c1211910d84fab848d9550652d7df767fe Mon Sep 17 00:00:00 2001 From: caitinggui <1029645297@qq.com> Date: Fri, 19 Jan 2018 16:20:07 +0800 Subject: [PATCH 10/39] update class_based_views --- docs/sanic/class_based_views.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/sanic/class_based_views.md b/docs/sanic/class_based_views.md index ace8bf9c..b7a5a101 100644 --- a/docs/sanic/class_based_views.md +++ b/docs/sanic/class_based_views.md @@ -92,10 +92,27 @@ class ViewWithDecorator(HTTPMethodView): def get(self, request, name): return text('Hello I have a decorator') + def post(self, request, name): + return text("Hello I also have a decorator") + app.add_route(ViewWithDecorator.as_view(), '/url') ``` -#### URL Building +But if you just want to decorator some functions and not all functions, you can use as followed. + +```python +class ViewWithSomeDecorator(HTTPMethodView): + + @staticmethod + @some_decorator_here + def get(request, name): + return text("Hello I have a decorator") + + def post(self, request, name): + return text("Hello I don't have any decorators") +``` + +## 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: From f8dedcaa1e2120d511766c63946690716ced335e Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 22 Jan 2018 15:55:29 +1100 Subject: [PATCH 11/39] Typo in readme? --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7f8ba439..01801ddd 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,7 @@ Installation - ``pip install sanic`` -To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables +To install sanic without uvloop or ujson using bash, you can provide either or both of these environmental variables using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features installation. From 3844cec7a4dc8cf436e001c49ee4269842a6f250 Mon Sep 17 00:00:00 2001 From: howie6879 Date: Mon, 22 Jan 2018 14:12:41 +0800 Subject: [PATCH 12/39] Add parameter check --- sanic/app.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 310d9059..592d04b4 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -5,7 +5,7 @@ import warnings from asyncio import get_event_loop, ensure_future, CancelledError from collections import deque, defaultdict from functools import partial -from inspect import isawaitable, stack, getmodulename +from inspect import getmodulename, isawaitable, signature, stack from traceback import format_exc from urllib.parse import urlencode, urlunparse from ssl import create_default_context, Purpose @@ -25,7 +25,6 @@ from sanic.websocket import WebSocketProtocol, ConnectionClosed class Sanic: - def __init__(self, name=None, router=None, error_handler=None, load_env=True, request_class=None, strict_slashes=False, log_config=None, @@ -111,9 +110,11 @@ class Sanic: :param event: event to listen to """ + def decorator(listener): self.listeners[event].append(listener) return listener + return decorator # Decorator @@ -143,12 +144,17 @@ class Sanic: strict_slashes = self.strict_slashes def response(handler): - if stream: - handler.is_stream = stream - self.router.add(uri=uri, methods=methods, handler=handler, - host=host, strict_slashes=strict_slashes, - version=version, name=name) - return handler + args = [key for key in signature(handler).parameters.keys()] + if args: + if stream: + handler.is_stream = stream + + self.router.add(uri=uri, methods=methods, handler=handler, + host=host, strict_slashes=strict_slashes, + version=version, name=name) + return handler + else: + raise ValueError('Required parameter `request` missing in the {0}()'.format(handler.__name__)) return response @@ -432,7 +438,7 @@ class Sanic: uri, route = self.router.find_route_by_view_name(view_name, **kw) if not (uri and route): raise URLBuildError('Endpoint with name `{}` was not found'.format( - view_name)) + view_name)) if view_name == 'static' or view_name.endswith('.static'): filename = kwargs.pop('filename', None) From f20b854dd2ed7998b4832dee388dcb6570b686e1 Mon Sep 17 00:00:00 2001 From: howie6879 Date: Mon, 22 Jan 2018 14:52:30 +0800 Subject: [PATCH 13/39] Add parameter check --- sanic/app.py | 4 +++- tests/test_url_building.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 592d04b4..dcdf443c 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -154,7 +154,9 @@ class Sanic: version=version, name=name) return handler else: - raise ValueError('Required parameter `request` missing in the {0}()'.format(handler.__name__)) + raise ValueError( + 'Required parameter `request` missing in the {0}()'.format( + handler.__name__)) return response diff --git a/tests/test_url_building.py b/tests/test_url_building.py index ed41b017..670cafa5 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -75,7 +75,7 @@ def test_fails_if_endpoint_not_found(): app = Sanic('fail_url_build') @app.route('/fail') - def fail(): + def fail(request): return text('this should fail') with pytest.raises(URLBuildError) as e: @@ -93,7 +93,7 @@ def test_fails_url_build_if_param_not_passed(): app = Sanic('fail_url_build') @app.route(url) - def fail(): + def fail(request): return text('this should fail') fail_args = list(string.ascii_letters) @@ -111,7 +111,7 @@ def test_fails_url_build_if_params_not_passed(): app = Sanic('fail_url_build') @app.route('/fail') - def fail(): + def fail(request): return text('this should fail') with pytest.raises(ValueError) as e: @@ -134,7 +134,7 @@ def test_fails_with_int_message(): app = Sanic('fail_url_build') @app.route(COMPLEX_PARAM_URL) - def fail(): + def fail(request): return text('this should fail') failing_kwargs = dict(PASSING_KWARGS) @@ -153,7 +153,7 @@ def test_fails_with_two_letter_string_message(): app = Sanic('fail_url_build') @app.route(COMPLEX_PARAM_URL) - def fail(): + def fail(request): return text('this should fail') failing_kwargs = dict(PASSING_KWARGS) @@ -173,7 +173,7 @@ def test_fails_with_number_message(): app = Sanic('fail_url_build') @app.route(COMPLEX_PARAM_URL) - def fail(): + def fail(request): return text('this should fail') failing_kwargs = dict(PASSING_KWARGS) @@ -193,7 +193,7 @@ def test_adds_other_supplied_values_as_query_string(): app = Sanic('passes') @app.route(COMPLEX_PARAM_URL) - def passes(): + def passes(request): return text('this should pass') new_kwargs = dict(PASSING_KWARGS) @@ -216,7 +216,7 @@ def blueprint_app(): second_print = Blueprint('second', url_prefix='/second') @first_print.route('/foo') - def foo(): + def foo(request): return text('foo from first') @first_print.route('/foo/') @@ -225,7 +225,7 @@ def blueprint_app(): 'foo from first : {}'.format(param)) @second_print.route('/foo') # noqa - def foo(): + def foo(request): return text('foo from second') @second_print.route('/foo/') # noqa From 040c85a43b7d6b09121b32a90056efd154d15f87 Mon Sep 17 00:00:00 2001 From: howie6879 Date: Wed, 24 Jan 2018 08:11:47 +0800 Subject: [PATCH 14/39] Add parameter check --- sanic/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index dcdf443c..eefcfc23 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -155,7 +155,7 @@ class Sanic: return handler else: raise ValueError( - 'Required parameter `request` missing in the {0}()'.format( + 'Required parameter `request` missing in the {0}() route?'.format( handler.__name__)) return response From 6c0fbef843c52b9848a46ec64bc2e6a070294237 Mon Sep 17 00:00:00 2001 From: howie6879 Date: Wed, 24 Jan 2018 08:17:55 +0800 Subject: [PATCH 15/39] Add parameter check --- sanic/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index eefcfc23..a81bb4a8 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -155,7 +155,8 @@ class Sanic: return handler else: raise ValueError( - 'Required parameter `request` missing in the {0}() route?'.format( + 'Required parameter `request` missing' + 'in the {0}() route?'.format( handler.__name__)) return response From ec4339bd47659df6e3dcab3c5e75f31ec5d5cad7 Mon Sep 17 00:00:00 2001 From: caitinggui <1029645297@qq.com> Date: Wed, 24 Jan 2018 09:02:07 +0800 Subject: [PATCH 16/39] update description --- docs/sanic/class_based_views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/class_based_views.md b/docs/sanic/class_based_views.md index b7a5a101..c3304df6 100644 --- a/docs/sanic/class_based_views.md +++ b/docs/sanic/class_based_views.md @@ -98,7 +98,7 @@ class ViewWithDecorator(HTTPMethodView): app.add_route(ViewWithDecorator.as_view(), '/url') ``` -But if you just want to decorator some functions and not all functions, you can use as followed. +But if you just want to decorate some functions and not all functions, you can do as follows: ```python class ViewWithSomeDecorator(HTTPMethodView): From 16f5914c908f5de671bac0810f694ad801c8f545 Mon Sep 17 00:00:00 2001 From: Shahin Date: Wed, 24 Jan 2018 16:46:45 +0330 Subject: [PATCH 17/39] Install Python 3.5 and 3.6 on docker container To cover all supported versions of Python using Tox --- Dockerfile | 6 ------ Makefile | 2 +- docker/Dockerfile | 28 ++++++++++++++++++++++++++++ docker/bin/entrypoint.sh | 11 +++++++++++ docker/bin/install_python.sh | 17 +++++++++++++++++ 5 files changed, 57 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile create mode 100644 docker/Dockerfile create mode 100755 docker/bin/entrypoint.sh create mode 100755 docker/bin/install_python.sh diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ee8ca2be..00000000 --- a/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM python:3.6 - -ADD . /app -WORKDIR /app - -RUN pip install tox diff --git a/Makefile b/Makefile index ad64412f..6fc1c8fa 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ test: find . -name "*.pyc" -delete - docker build -t sanic/test-image . + docker build -t sanic/test-image -f docker/Dockerfile . docker run -t sanic/test-image tox diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..dc7832ff --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,28 @@ +FROM alpine:3.7 + +RUN apk add --no-cache --update \ + curl \ + bash \ + build-base \ + ca-certificates \ + git \ + bzip2-dev \ + linux-headers \ + ncurses-dev \ + openssl \ + openssl-dev \ + readline-dev \ + sqlite-dev + +RUN update-ca-certificates +RUN rm -rf /var/cache/apk/* + +ENV PYENV_ROOT="/root/.pyenv" +ENV PATH="$PYENV_ROOT/bin:$PATH" + +ADD . /app +WORKDIR /app + +RUN /app/docker/bin/install_python.sh 3.5.4 3.6.4 + +ENTRYPOINT ["./docker/bin/entrypoint.sh"] diff --git a/docker/bin/entrypoint.sh b/docker/bin/entrypoint.sh new file mode 100755 index 00000000..762d2155 --- /dev/null +++ b/docker/bin/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +eval "$(pyenv init -)" +eval "$(pyenv virtualenv-init -)" +source /root/.pyenv/completions/pyenv.bash + +pip install tox + +exec $@ + diff --git a/docker/bin/install_python.sh b/docker/bin/install_python.sh new file mode 100755 index 00000000..e7c4aa1f --- /dev/null +++ b/docker/bin/install_python.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +export CFLAGS='-O2' +export EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000" + +curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash +eval "$(pyenv init -)" + +for ver in $@ +do + pyenv install $ver +done + +pyenv global $@ +pip install --upgrade pip +pyenv rehash From 285ad9bdc123cdc83480441d86ae9a3e083b17e7 Mon Sep 17 00:00:00 2001 From: NyanKiyoshi Date: Fri, 26 Jan 2018 21:13:43 +0100 Subject: [PATCH 18/39] No longer raising a missing parameter when value is null When passing a null value as parameter (ex.: 0, None or False), Sanic said "Error: Required parameter `param` was not passed to url_for" Example: ``` @app.route("/") def route(rq, idx): pass ``` ``` url_for("route", idx=0) ``` No longer raises: `Error: Required parameter `idx` was not passed to url_for` --- sanic/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index a81bb4a8..6e8377f5 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -495,7 +495,7 @@ class Sanic: specific_pattern = '^{}$'.format(pattern) supplied_param = None - if kwargs.get(name): + if name in kwargs: supplied_param = kwargs.get(name) del kwargs[name] else: From 37eb2c1db681b8b7003e9d0a93edb1eaf55b6bca Mon Sep 17 00:00:00 2001 From: SirEdvin Date: Sat, 27 Jan 2018 10:28:53 +0200 Subject: [PATCH 19/39] Provide information about sanic-oauth extension --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 03feb90c..d719a28f 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -19,6 +19,7 @@ A list of Sanic extensions created by the community. `Babel` library - [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter. - [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers. +- [sanic-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support. - [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose. - [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic - [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic From cabcf50fbe2a2c018c94062fc9e0bc4832fcc399 Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Date: Tue, 30 Jan 2018 11:26:15 +0100 Subject: [PATCH 20/39] KeepAlive Timeout log level change to debug --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 2f2df444..10a9040a 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -195,7 +195,7 @@ class HttpProtocol(asyncio.Protocol): self.keep_alive_timeout_callback) ) else: - logger.info('KeepAlive Timeout. Closing connection.') + logger.debug('KeepAlive Timeout. Closing connection.') self.transport.close() self.transport = None From f23c3da4ff8919891417391aabae50a0d05e1459 Mon Sep 17 00:00:00 2001 From: manisenkov Date: Wed, 31 Jan 2018 22:58:48 +0100 Subject: [PATCH 21/39] Pin pytest version to 3.3.2 --- requirements-dev.txt | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2efa853e..cf00aad4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ beautifulsoup4 coverage httptools flake8 -pytest +pytest==3.3.2 tox ujson uvloop diff --git a/tox.ini b/tox.ini index 0b573f6b..88e31f22 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ setenv = {py35,py36}-no-ext: SANIC_NO_UVLOOP=1 deps = coverage - pytest + pytest==3.3.2 pytest-cov pytest-sanic pytest-sugar From 49c29e68622e41ce39d350b132609cefbb10121c Mon Sep 17 00:00:00 2001 From: manisenkov Date: Wed, 31 Jan 2018 08:19:01 +0100 Subject: [PATCH 22/39] Upgrade development status to beta --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a258d58a..45aa7b6f 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ setup_kwargs = { 'packages': ['sanic'], 'platforms': 'any', 'classifiers': [ - 'Development Status :: 2 - Pre-Alpha', + 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3.5', From ed1c563d1fc610a9b4cc6dac4036c72e6c3b3916 Mon Sep 17 00:00:00 2001 From: Dirk Guijt Date: Thu, 1 Feb 2018 11:30:24 +0100 Subject: [PATCH 23/39] fixed bug in multipart/form-data parser Sanic automatically assumes that a form field is a file if it has a content-type header, even though the header is text/plain or application/json. This is a fix for it, I took into account the RFC7578 specification regarding the defaults. --- sanic/request.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index ecc41d13..98bb049f 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -284,7 +284,8 @@ def parse_multipart_form(body, boundary): form_parts = body.split(boundary) for form_part in form_parts[1:-1]: file_name = None - file_type = None + content_type = "text/plain" + content_charset = "utf-8" field_name = None line_index = 2 line_end_index = 0 @@ -304,19 +305,21 @@ def parse_multipart_form(body, boundary): if form_header_field == 'content-disposition': if 'filename' in form_parameters: file_name = form_parameters['filename'] - field_name = form_parameters.get('name') + field_name = form_parameters['name'] elif form_header_field == 'content-type': - file_type = form_header_value + content_type = form_header_value + if 'charset' in form_parameters: + content_charset = form_parameters['charset'] post_data = form_part[line_index:-4] - if file_name or file_type: - file = File(type=file_type, name=file_name, body=post_data) + if file_name: + file = File(type=content_type, name=file_name, body=post_data) if field_name in files: files[field_name].append(file) else: files[field_name] = [file] else: - value = post_data.decode('utf-8') + value = post_data.decode(content_charset) if field_name in fields: fields[field_name].append(value) else: From 2135294e2ef30328cd514668bc3ce7bcde5f34ce Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Date: Thu, 1 Feb 2018 11:52:55 +0100 Subject: [PATCH 24/39] changed None to return empty string instead of null string --- sanic/response.py | 3 ++- tests/test_requests.py | 5 +++-- tests/test_response.py | 12 ++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 3873d90f..2402b432 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -239,7 +239,8 @@ def json(body, status=200, headers=None, :param headers: Custom Headers. :param kwargs: Remaining arguments that are passed to the json encoder. """ - return HTTPResponse(dumps(body, **kwargs), headers=headers, + _body = dumps(body, **kwargs) if body else None + return HTTPResponse(_body, headers=headers, status=status, content_type=content_type) diff --git a/tests/test_requests.py b/tests/test_requests.py index 9eb88243..49e73a55 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -109,12 +109,13 @@ def test_empty_json(): @app.route('/') async def handler(request): - assert request.json == None + assert request.json is None return json(request.json) request, response = app.test_client.get('/') assert response.status == 200 - assert response.text == 'null' + assert response.text == '' + def test_invalid_json(): app = Sanic('test_json') diff --git a/tests/test_response.py b/tests/test_response.py index 6ac77cec..88bd8a3a 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -63,6 +63,10 @@ def json_app(): async def test(request): return json(JSON_DATA) + @app.delete("/") + async def test_delete(request): + return json(None, status=204) + return app @@ -73,6 +77,14 @@ def test_json_response(json_app): assert response.text == json_dumps(JSON_DATA) assert response.json == JSON_DATA + +def test_no_content(json_app): + request, response = json_app.test_client.delete('/') + assert response.status == 204 + assert response.text == '' + assert response.headers['Content-Length'] == '0' + + @pytest.fixture def streaming_app(): app = Sanic('streaming') From a76d8108fe519182e5972c702c94024ea3b2899f Mon Sep 17 00:00:00 2001 From: DirkGuijt <922322+DirkGuijt@users.noreply.github.com> Date: Thu, 1 Feb 2018 11:55:30 +0100 Subject: [PATCH 25/39] small code style change changed double quotes to single quotes to match the coding style --- sanic/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 98bb049f..908f8c7a 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -284,8 +284,8 @@ def parse_multipart_form(body, boundary): form_parts = body.split(boundary) for form_part in form_parts[1:-1]: file_name = None - content_type = "text/plain" - content_charset = "utf-8" + content_type = 'text/plain' + content_charset = 'utf-8' field_name = None line_index = 2 line_end_index = 0 From 580666694918e29fb5b09f2a46653ee29616051f Mon Sep 17 00:00:00 2001 From: Bob Olde Hampsink Date: Thu, 1 Feb 2018 16:23:10 +0100 Subject: [PATCH 26/39] Extend WebSocketProtocol arguments to accept all arguments of websockets.protocol.WebSocketCommonProtocol --- sanic/websocket.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/sanic/websocket.py b/sanic/websocket.py index bc78c76f..99408af5 100644 --- a/sanic/websocket.py +++ b/sanic/websocket.py @@ -6,12 +6,18 @@ from websockets import ConnectionClosed # noqa class WebSocketProtocol(HttpProtocol): - def __init__(self, *args, websocket_max_size=None, - websocket_max_queue=None, **kwargs): + def __init__(self, *args, websocket_timeout=10, + websocket_max_size=None, + websocket_max_queue=None, + websocket_read_limit=2 ** 16, + websocket_write_limit=2 ** 16, **kwargs): super().__init__(*args, **kwargs) self.websocket = None + self.websocket_timeout = websocket_timeout self.websocket_max_size = websocket_max_size self.websocket_max_queue = websocket_max_queue + self.websocket_read_limit = websocket_read_limit + self.websocket_write_limit = websocket_write_limit # timeouts make no sense for websocket routes def request_timeout_callback(self): @@ -85,8 +91,11 @@ class WebSocketProtocol(HttpProtocol): # hook up the websocket protocol self.websocket = WebSocketCommonProtocol( + timeout=self.websocket_timeout, max_size=self.websocket_max_size, - max_queue=self.websocket_max_queue + max_queue=self.websocket_max_queue, + read_limit=self.websocket_read_limit, + write_limit=self.websocket_write_limit ) self.websocket.subprotocol = subprotocol self.websocket.connection_made(request.transport) From 68fd1b66b50e5a1ea6348570b9a31fac43ec8c8e Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Date: Thu, 1 Feb 2018 17:51:51 +0100 Subject: [PATCH 27/39] Response model now handles the 204 no content --- sanic/response.py | 13 ++++++++----- tests/test_requests.py | 3 ++- tests/test_response.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 2402b432..7a490ffe 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -195,8 +195,12 @@ class HTTPResponse(BaseHTTPResponse): timeout_header = b'' if keep_alive and keep_alive_timeout is not None: timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout - self.headers['Content-Length'] = self.headers.get( - 'Content-Length', len(self.body)) + + content_length = 0 + if self.status is not 204: + content_length = self.headers.get('Content-Length', len(self.body)) + self.headers['Content-Length'] = content_length + self.headers['Content-Type'] = self.headers.get( 'Content-Type', self.content_type) @@ -218,7 +222,7 @@ class HTTPResponse(BaseHTTPResponse): b'keep-alive' if keep_alive else b'close', timeout_header, headers, - self.body + self.body if self.status is not 204 else b'' ) @property @@ -239,8 +243,7 @@ def json(body, status=200, headers=None, :param headers: Custom Headers. :param kwargs: Remaining arguments that are passed to the json encoder. """ - _body = dumps(body, **kwargs) if body else None - return HTTPResponse(_body, headers=headers, + return HTTPResponse(dumps(body, **kwargs), headers=headers, status=status, content_type=content_type) diff --git a/tests/test_requests.py b/tests/test_requests.py index 49e73a55..14da4b0b 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -104,6 +104,7 @@ def test_json(): assert results.get('test') == True + def test_empty_json(): app = Sanic('test_json') @@ -114,7 +115,7 @@ def test_empty_json(): request, response = app.test_client.get('/') assert response.status == 200 - assert response.text == '' + assert response.text == 'null' def test_invalid_json(): diff --git a/tests/test_response.py b/tests/test_response.py index 88bd8a3a..475da8b6 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -43,7 +43,7 @@ def test_method_not_allowed(): return response.json({'hello': 'world'}) request, response = app.test_client.head('/') - assert response.headers['Allow']== 'GET' + assert response.headers['Allow'] == 'GET' @app.post('/') async def test(request): From 4b6e89a5266ccf2e471252fc2e4a5ae4441d8fab Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Date: Thu, 1 Feb 2018 20:00:32 +0100 Subject: [PATCH 28/39] added one more test --- tests/test_response.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_response.py b/tests/test_response.py index 475da8b6..e7690c11 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -63,8 +63,12 @@ def json_app(): async def test(request): return json(JSON_DATA) + @app.get("/no-content") + async def no_content_handler(request): + return json(JSON_DATA, status=204) + @app.delete("/") - async def test_delete(request): + async def delete_handler(request): return json(None, status=204) return app @@ -79,6 +83,11 @@ def test_json_response(json_app): def test_no_content(json_app): + request, response = json_app.test_client.get('/no-content') + assert response.status == 204 + assert response.text == '' + assert response.headers['Content-Length'] == '0' + request, response = json_app.test_client.delete('/') assert response.status == 204 assert response.text == '' From 788253cbe8e2da5d0b5756885f74b49b00195363 Mon Sep 17 00:00:00 2001 From: Dirk Guijt Date: Fri, 2 Feb 2018 00:55:51 +0100 Subject: [PATCH 29/39] changes based on discussion on PR #1109 --- sanic/request.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 98bb049f..33844cd7 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -303,13 +303,11 @@ def parse_multipart_form(body, boundary): form_line[colon_index + 2:]) if form_header_field == 'content-disposition': - if 'filename' in form_parameters: - file_name = form_parameters['filename'] - field_name = form_parameters['name'] + file_name = form_parameters.get('filename') + field_name = form_parameters.get('name') elif form_header_field == 'content-type': content_type = form_header_value - if 'charset' in form_parameters: - content_charset = form_parameters['charset'] + content_charset = form_parameters.get('charset', 'utf-8') post_data = form_part[line_index:-4] if file_name: From 0ab64e9803fbf2f82caa1c8e10115d4add376fbb Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Date: Fri, 2 Feb 2018 09:29:54 +0100 Subject: [PATCH 30/39] simplified logic when handling the body --- sanic/response.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 7a490ffe..01d0843d 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -196,11 +196,13 @@ class HTTPResponse(BaseHTTPResponse): if keep_alive and keep_alive_timeout is not None: timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout + body = b'' content_length = 0 if self.status is not 204: + body = self.body content_length = self.headers.get('Content-Length', len(self.body)) - self.headers['Content-Length'] = content_length + self.headers['Content-Length'] = content_length self.headers['Content-Type'] = self.headers.get( 'Content-Type', self.content_type) @@ -222,7 +224,7 @@ class HTTPResponse(BaseHTTPResponse): b'keep-alive' if keep_alive else b'close', timeout_header, headers, - self.body if self.status is not 204 else b'' + body ) @property From 5c341a2b00c2ef2f7898bcc1e0783c0f373e0421 Mon Sep 17 00:00:00 2001 From: Dirk Guijt Date: Fri, 2 Feb 2018 09:43:42 +0100 Subject: [PATCH 31/39] made field name mandatory in multipart/form-data headers A field name in the Content-Disposition header is required by the multipart/form-data spec. If one field/part does not have it, it will be omitted from the request. When this happens, we log it to DEBUG. --- sanic/request.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index b37f9f9d..0660337f 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -18,7 +18,7 @@ except ImportError: json_loads = json.loads from sanic.exceptions import InvalidUsage -from sanic.log import error_logger +from sanic.log import error_logger, logger DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" @@ -309,18 +309,21 @@ def parse_multipart_form(body, boundary): content_type = form_header_value content_charset = form_parameters.get('charset', 'utf-8') - post_data = form_part[line_index:-4] - if file_name: - file = File(type=content_type, name=file_name, body=post_data) - if field_name in files: - files[field_name].append(file) + if field_name: + post_data = form_part[line_index:-4] + if file_name: + file = File(type=content_type, name=file_name, body=post_data) + if field_name in files: + files[field_name].append(file) + else: + files[field_name] = [file] else: - files[field_name] = [file] + value = post_data.decode(content_charset) + if field_name in fields: + fields[field_name].append(value) + else: + fields[field_name] = [value] else: - value = post_data.decode(content_charset) - if field_name in fields: - fields[field_name].append(value) - else: - fields[field_name] = [value] + logger.debug('Form-data field does not have a name parameter in the Content-Disposition header') return fields, files From 1eecffce9726ccaa1d5b67c80ae6c59e2711e182 Mon Sep 17 00:00:00 2001 From: Dirk Guijt Date: Fri, 2 Feb 2018 09:57:06 +0100 Subject: [PATCH 32/39] fixed minor flake8 style problem --- sanic/request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index 0660337f..e330e085 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -324,6 +324,7 @@ def parse_multipart_form(body, boundary): else: fields[field_name] = [value] else: - logger.debug('Form-data field does not have a name parameter in the Content-Disposition header') + logger.debug('Form-data field does not have a \'name\' parameter \ + in the Content-Disposition header') return fields, files From 7ca3ad5d4cc25e16a45a98ec1eaa42d2eb4af6c2 Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Date: Fri, 2 Feb 2018 13:24:51 +0100 Subject: [PATCH 33/39] no body and content length to 0 when 304 response is returned --- sanic/response.py | 2 +- tests/test_response.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 01d0843d..26da64ae 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -198,7 +198,7 @@ class HTTPResponse(BaseHTTPResponse): body = b'' content_length = 0 - if self.status is not 204: + if self.status is not 204 and self.status != 304: body = self.body content_length = self.headers.get('Content-Length', len(self.body)) diff --git a/tests/test_response.py b/tests/test_response.py index e7690c11..79f2b74f 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -67,6 +67,14 @@ def json_app(): async def no_content_handler(request): return json(JSON_DATA, status=204) + @app.get("/no-content/unmodified") + async def no_content_unmodified_handler(request): + return json(None, status=304) + + @app.get("/unmodified") + async def unmodified_handler(request): + return json(JSON_DATA, status=304) + @app.delete("/") async def delete_handler(request): return json(None, status=204) @@ -88,6 +96,16 @@ def test_no_content(json_app): assert response.text == '' assert response.headers['Content-Length'] == '0' + request, response = json_app.test_client.get('/no-content/unmodified') + assert response.status == 304 + assert response.text == '' + assert response.headers['Content-Length'] == '0' + + request, response = json_app.test_client.get('/unmodified') + assert response.status == 304 + assert response.text == '' + assert response.headers['Content-Length'] == '0' + request, response = json_app.test_client.delete('/') assert response.status == 204 assert response.text == '' From 86fed12d913ee407ca35fb93a007e354d41d580f Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Date: Fri, 2 Feb 2018 14:05:57 +0100 Subject: [PATCH 34/39] less flake8 warnings in response test --- tests/test_response.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_response.py b/tests/test_response.py index 79f2b74f..b9ad9fb4 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -16,7 +16,6 @@ from unittest.mock import MagicMock JSON_DATA = {'ok': True} - def test_response_body_not_a_string(): """Test when a response body sent from the application is not a string""" app = Sanic('response_body_not_a_string') @@ -35,6 +34,7 @@ async def sample_streaming_fn(response): await asyncio.sleep(.001) response.write('bar') + def test_method_not_allowed(): app = Sanic('method_not_allowed') @@ -195,9 +195,11 @@ def get_file_content(static_file_directory, file_name): with open(os.path.join(static_file_directory, file_name), 'rb') as file: return file.read() + @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) def test_file_response(file_name, static_file_directory): app = Sanic('test_file_helper') + @app.route('/files/', methods=['GET']) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) @@ -209,10 +211,12 @@ def test_file_response(file_name, static_file_directory): assert response.body == get_file_content(static_file_directory, file_name) assert 'Content-Disposition' not in response.headers + @pytest.mark.parametrize('source,dest', [ ('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')]) def test_file_response_custom_filename(source, dest, static_file_directory): app = Sanic('test_file_helper') + @app.route('/files/', methods=['GET']) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) @@ -224,9 +228,11 @@ def test_file_response_custom_filename(source, dest, static_file_directory): assert response.body == get_file_content(static_file_directory, source) assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest) + @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) def test_file_head_response(file_name, static_file_directory): app = Sanic('test_file_helper') + @app.route('/files/', methods=['GET', 'HEAD']) async def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) @@ -251,25 +257,29 @@ def test_file_head_response(file_name, static_file_directory): 'Content-Length']) == len( get_file_content(static_file_directory, file_name)) + @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) def test_file_stream_response(file_name, static_file_directory): app = Sanic('test_file_helper') + @app.route('/files/', methods=['GET']) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) file_path = os.path.abspath(unquote(file_path)) return file_stream(file_path, chunk_size=32, - mime_type=guess_type(file_path)[0] or 'text/plain') + mime_type=guess_type(file_path)[0] or 'text/plain') request, response = app.test_client.get('/files/{}'.format(file_name)) assert response.status == 200 assert response.body == get_file_content(static_file_directory, file_name) assert 'Content-Disposition' not in response.headers + @pytest.mark.parametrize('source,dest', [ ('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')]) def test_file_stream_response_custom_filename(source, dest, static_file_directory): app = Sanic('test_file_helper') + @app.route('/files/', methods=['GET']) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) @@ -281,9 +291,11 @@ def test_file_stream_response_custom_filename(source, dest, static_file_director assert response.body == get_file_content(static_file_directory, source) assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest) + @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) def test_file_stream_head_response(file_name, static_file_directory): app = Sanic('test_file_helper') + @app.route('/files/', methods=['GET', 'HEAD']) async def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) From f5a2d191993da337d499327459433de5073c8adb Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Date: Fri, 2 Feb 2018 14:13:14 +0100 Subject: [PATCH 35/39] touch commit --- tests/test_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_response.py b/tests/test_response.py index b9ad9fb4..57e01cb6 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -312,7 +312,7 @@ def test_file_stream_head_response(file_name, static_file_directory): content_type=guess_type(file_path)[0] or 'text/plain') else: return file_stream(file_path, chunk_size=32, headers=headers, - mime_type=guess_type(file_path)[0] or 'text/plain') + mime_type=guess_type(file_path)[0] or 'text/plain') request, response = app.test_client.head('/files/{}'.format(file_name)) assert response.status == 200 From f2c0489452cd3bf2a5f2ba0af55f7346273e8e08 Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Date: Fri, 2 Feb 2018 20:19:15 +0100 Subject: [PATCH 36/39] replaced comparison for in operator --- sanic/response.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 26da64ae..9349ce81 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -72,6 +72,8 @@ STATUS_CODES = { 511: b'Network Authentication Required' } +EMPTY_STATUS_CODES = [204, 304] + class BaseHTTPResponse: def _encode_body(self, data): @@ -198,7 +200,7 @@ class HTTPResponse(BaseHTTPResponse): body = b'' content_length = 0 - if self.status is not 204 and self.status != 304: + if self.status not in EMPTY_STATUS_CODES: body = self.body content_length = self.headers.get('Content-Length', len(self.body)) From ddf2a604d1a9cfb30b89fdfb33f47ac56ba5e930 Mon Sep 17 00:00:00 2001 From: Dirk Guijt Date: Sat, 3 Feb 2018 03:07:07 +0100 Subject: [PATCH 37/39] changed 'file' variable to 'form_file' to prevent overwriting the reserved word --- sanic/request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index e330e085..01863bd0 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -312,11 +312,11 @@ def parse_multipart_form(body, boundary): if field_name: post_data = form_part[line_index:-4] if file_name: - file = File(type=content_type, name=file_name, body=post_data) + form_file = File(type=content_type, name=file_name, body=post_data) if field_name in files: - files[field_name].append(file) + files[field_name].append(form_file) else: - files[field_name] = [file] + files[field_name] = [form_file] else: value = post_data.decode(content_charset) if field_name in fields: From 48d45f1ca4a682487e57689f172f888ac646cfc2 Mon Sep 17 00:00:00 2001 From: Dirk Guijt Date: Sat, 3 Feb 2018 03:14:04 +0100 Subject: [PATCH 38/39] sorry, style issue again --- sanic/request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index 01863bd0..4b27d7e8 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -312,7 +312,8 @@ def parse_multipart_form(body, boundary): if field_name: post_data = form_part[line_index:-4] if file_name: - form_file = File(type=content_type, name=file_name, body=post_data) + form_file = \ + File(type=content_type, name=file_name, body=post_data) if field_name in files: files[field_name].append(form_file) else: From e083224df1ee5d320dd1d2148e1c37bdbf00b9eb Mon Sep 17 00:00:00 2001 From: Dirk Guijt Date: Wed, 7 Feb 2018 09:29:44 +0100 Subject: [PATCH 39/39] changed bewline formatting --- sanic/request.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 4b27d7e8..cd7071d7 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -312,8 +312,9 @@ def parse_multipart_form(body, boundary): if field_name: post_data = form_part[line_index:-4] if file_name: - form_file = \ - File(type=content_type, name=file_name, body=post_data) + form_file = File(type=content_type, + name=file_name, + body=post_data) if field_name in files: files[field_name].append(form_file) else: