From 0e0d4dd3bcd2e413a8004a98b66be355b6d08309 Mon Sep 17 00:00:00 2001 From: Generic Error Date: Mon, 17 Oct 2016 20:30:42 +1100 Subject: [PATCH 001/977] Improved grammar Improved the grammar and the capitalisation consistency --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e302a66c..61dfb187 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,15 @@ [![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/) [![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/) -Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based off the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/. +Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/. -On top of being flask-like, sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. +On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. ## Benchmarks -All tests were run on a AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for falcon and flask, but did not speed up requests. +All tests were run on an AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for falcon and flask but did not speed up requests. + + | Server | Implementation | Requests/sec | Avg Latency | | ------- | ------------------- | ------------:| -----------:| From 625af9a21dd6e315246650ba01daec08075bfc5c Mon Sep 17 00:00:00 2001 From: Generic Error Date: Tue, 18 Oct 2016 07:04:24 +1100 Subject: [PATCH 002/977] Updated capitalisation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 61dfb187..9510a178 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ On top of being Flask-like, Sanic supports async request handlers. This means y ## Benchmarks -All tests were run on an AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for falcon and flask but did not speed up requests. +All tests were run on an AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for Falcon and Flask but did not speed up requests. From 18aa937f2999743fd38920b281715bcaf956c96b Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 17 Oct 2016 23:34:07 -0700 Subject: [PATCH 003/977] Fix slowdown --- sanic/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 0a10f5fd..38bfadf1 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -122,8 +122,8 @@ class HttpProtocol(asyncio.Protocol): def write_response(self, response): try: - keep_alive = all( - [self.parser.should_keep_alive(), self.signal.stopped]) + keep_alive = self.parser.should_keep_alive() \ + and not self.signal.stopped self.transport.write( response.output( self.request.version, keep_alive, self.request_timeout)) From 6f105a647eff1f0efeaa188fd34a15ed6f33269f Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 18 Oct 2016 01:22:49 -0700 Subject: [PATCH 004/977] Added multiprocessing --- README.md | 1 + docs/deploying.md | 35 ++++++++++ requirements-dev.txt | 1 + sanic/__main__.py | 36 ++++++++++ sanic/sanic.py | 81 +++++++++++++++++++---- sanic/server.py | 7 +- test.py | 52 +++++++++++++++ tests/performance/falcon/simple_server.py | 11 +++ tests/performance/sanic/simple_server.py | 4 +- tests/test_multiprocessing.py | 53 +++++++++++++++ 10 files changed, 262 insertions(+), 19 deletions(-) create mode 100644 docs/deploying.md create mode 100644 sanic/__main__.py create mode 100644 test.py create mode 100644 tests/performance/falcon/simple_server.py create mode 100644 tests/test_multiprocessing.py diff --git a/README.md b/README.md index e302a66c..00580950 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ app.run(host="0.0.0.0", port=8000) * [Middleware](docs/middleware.md) * [Exceptions](docs/exceptions.md) * [Blueprints](docs/blueprints.md) + * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/deploying.md b/docs/deploying.md new file mode 100644 index 00000000..d759bb3c --- /dev/null +++ b/docs/deploying.md @@ -0,0 +1,35 @@ +# Deploying + +When it comes to deploying Sanic, there's not much to it, but there are +a few things to take note of. + +## Workers + +By default, Sanic listens in the main process using only 1 CPU core. +To crank up the juice, just specify the number of workers in the run +arguments like so: + +```python +app.run(host='0.0.0.0', port=1337, workers=4) +``` + +Sanic will automatically spin up multiple processes and route +traffic between them. We recommend as many workers as you have +available cores. + +## Running via Command + +If you like using command line arguments, you can launch a sanic server +by executing the module. For example, if you initialized sanic as +app in a file named server.py, you could run the server like so: + +`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4` + +With this way of running sanic, it is not necessary to run app.run in +your python file. If you do, just make sure you wrap it in name == main +like so: + +```python +if __name__ == '__main__': + app.run(host='0.0.0.0', port=1337, workers=4) +``` \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 66246850..ac3b05fb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,3 +8,4 @@ tox gunicorn bottle kyoukai +falcon \ No newline at end of file diff --git a/sanic/__main__.py b/sanic/__main__.py new file mode 100644 index 00000000..96652294 --- /dev/null +++ b/sanic/__main__.py @@ -0,0 +1,36 @@ +from argparse import ArgumentParser +from importlib import import_module + +from .log import log +from .sanic import Sanic + +if __name__ == "__main__": + parser = ArgumentParser(prog='sanic') + parser.add_argument('--host', dest='host', type=str, default='127.0.0.1') + parser.add_argument('--port', dest='port', type=int, default=8000) + parser.add_argument('--workers', dest='workers', type=int, default=1, ) + parser.add_argument('--debug', dest='debug', action="store_true") + parser.add_argument('module') + args = parser.parse_args() + + try: + module_parts = args.module.split(".") + module_name = ".".join(module_parts[:-1]) + app_name = module_parts[-1] + + module = import_module(module_name) + app = getattr(module, app_name, None) + if type(app) is not Sanic: + raise ValueError("Module is not a Sanic app, it is a {}. " + "Perhaps you meant {}.app?" + .format(type(app).__name__, args.module)) + + app.run(host=args.host, port=args.port, + workers=args.workers, debug=args.debug) + except ImportError: + log.error("No module named {} found.\n" + " Example File: project/sanic_server.py -> app\n" + " Example Module: project.sanic_server.app" + .format(module_name)) + except ValueError as e: + log.error("{}".format(e)) \ No newline at end of file diff --git a/sanic/sanic.py b/sanic/sanic.py index f67edc7b..cba19b9d 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,5 +1,9 @@ -import asyncio +from argparse import ArgumentParser +from asyncio import get_event_loop from inspect import isawaitable +from multiprocessing import Process, Event +from signal import signal, SIGTERM, SIGINT +from time import sleep from traceback import format_exc from .config import Config @@ -167,7 +171,7 @@ class Sanic: # -------------------------------------------------------------------- # def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, - before_stop=None): + before_stop=None, sock=None, workers=1): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -178,11 +182,24 @@ class Sanic: listening :param before_stop: Function to be executed when a stop signal is received before it is respected + :param sock: Socket for the server to accept connections from + :param workers: Number of processes + received before it is respected :return: Nothing """ self.error_handler.debug = True self.debug = debug + server_settings = { + 'host': host, + 'port': port, + 'sock': sock, + 'debug': debug, + 'request_handler': self.handle_request, + 'request_timeout': self.config.REQUEST_TIMEOUT, + 'request_max_size': self.config.REQUEST_MAX_SIZE, + } + if debug: log.setLevel(logging.DEBUG) log.debug(self.config.LOGO) @@ -191,23 +208,61 @@ class Sanic: log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) try: - serve( - host=host, - port=port, - debug=debug, - after_start=after_start, - before_stop=before_stop, - request_handler=self.handle_request, - request_timeout=self.config.REQUEST_TIMEOUT, - request_max_size=self.config.REQUEST_MAX_SIZE, - ) + if workers == 1: + server_settings['after_start'] = after_start + server_settings['before_stop'] = before_stop + serve(**server_settings) + else: + log.info('Spinning up {} workers...'.format(workers)) + + self.serve_multiple(server_settings, workers) + except Exception as e: log.exception( 'Experienced exception while trying to serve: {}'.format(e)) pass + log.info("Server Stopped") + def stop(self): """ This kills the Sanic """ - asyncio.get_event_loop().stop() + get_event_loop().stop() + + @staticmethod + def serve_multiple(server_settings, workers, stop_event=None): + """ + Starts multiple server processes simultaneously. Stops on interrupt + and terminate signals, and drains connections when complete. + :param server_settings: kw arguments to be passed to the serve function + :param workers: number of workers to launch + :param stop_event: if provided, is used as a stop signal + :return: + """ + server_settings['reuse_port'] = True + + # Create a stop event to be triggered by a signal + if not stop_event: + stop_event = Event() + signal(SIGINT, lambda s, f: stop_event.set()) + signal(SIGTERM, lambda s, f: stop_event.set()) + + processes = [] + for w in range(workers): + process = Process(target=serve, kwargs=server_settings) + process.start() + processes.append(process) + + # Infinitely wait for the stop event + try: + while not stop_event.is_set(): + sleep(0.3) + except: + pass + + log.info('Spinning down workers...') + for process in processes: + process.terminate() + for process in processes: + process.join() diff --git a/sanic/server.py b/sanic/server.py index 38bfadf1..7379ade1 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -158,8 +158,8 @@ class HttpProtocol(asyncio.Protocol): def serve(host, port, request_handler, after_start=None, before_stop=None, - debug=False, request_timeout=60, - request_max_size=None): + debug=False, request_timeout=60, sock=None, + request_max_size=None, reuse_port=False): # Create Event Loop loop = async_loop.new_event_loop() asyncio.set_event_loop(loop) @@ -176,7 +176,7 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, request_handler=request_handler, request_timeout=request_timeout, request_max_size=request_max_size, - ), host, port) + ), host, port, reuse_port=reuse_port, sock=sock) try: http_server = loop.run_until_complete(server_coroutine) except Exception as e: @@ -217,4 +217,3 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, loop.run_until_complete(asyncio.sleep(0.1)) loop.close() - log.info("Server Stopped") diff --git a/test.py b/test.py new file mode 100644 index 00000000..263aeae3 --- /dev/null +++ b/test.py @@ -0,0 +1,52 @@ +from multiprocessing import Array, Event, Process +from time import sleep +from ujson import loads as json_loads + +from sanic import Sanic +from sanic.response import json +from sanic.utils import local_request, HOST, PORT + + +# ------------------------------------------------------------ # +# GET +# ------------------------------------------------------------ # + +def test_json(): + app = Sanic('test_json') + + response = Array('c', 50) + @app.route('/') + async def handler(request): + return json({"test": True}) + + stop_event = Event() + async def after_start(*args, **kwargs): + http_response = await local_request('get', '/') + response.value = http_response.text.encode() + stop_event.set() + + def rescue_crew(): + sleep(5) + stop_event.set() + + rescue_process = Process(target=rescue_crew) + rescue_process.start() + + app.serve_multiple({ + 'host': HOST, + 'port': PORT, + 'after_start': after_start, + 'request_handler': app.handle_request, + 'request_max_size': 100000, + }, workers=2, stop_event=stop_event) + + rescue_process.terminate() + + try: + results = json_loads(response.value) + except: + raise ValueError("Expected JSON response but got '{}'".format(response)) + + assert results.get('test') == True + +test_json() \ No newline at end of file diff --git a/tests/performance/falcon/simple_server.py b/tests/performance/falcon/simple_server.py new file mode 100644 index 00000000..4403ac14 --- /dev/null +++ b/tests/performance/falcon/simple_server.py @@ -0,0 +1,11 @@ +# Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker falc:app + +import falcon +import ujson as json + +class TestResource: + def on_get(self, req, resp): + resp.body = json.dumps({"test": True}) + +app = falcon.API() +app.add_route('/', TestResource()) diff --git a/tests/performance/sanic/simple_server.py b/tests/performance/sanic/simple_server.py index 823b7b82..5cf86afd 100644 --- a/tests/performance/sanic/simple_server.py +++ b/tests/performance/sanic/simple_server.py @@ -15,5 +15,5 @@ app = Sanic("test") async def test(request): return json({"test": True}) - -app.run(host="0.0.0.0", port=sys.argv[1]) +if __name__ == '__main__': + app.run(host="0.0.0.0", port=sys.argv[1]) diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py new file mode 100644 index 00000000..545ecee7 --- /dev/null +++ b/tests/test_multiprocessing.py @@ -0,0 +1,53 @@ +from multiprocessing import Array, Event, Process +from time import sleep +from ujson import loads as json_loads + +from sanic import Sanic +from sanic.response import json +from sanic.utils import local_request, HOST, PORT + + +# ------------------------------------------------------------ # +# GET +# ------------------------------------------------------------ # + +# TODO: Figure out why this freezes on pytest but not when +# executed via interpreter + +def skip_test_multiprocessing(): + app = Sanic('test_json') + + response = Array('c', 50) + @app.route('/') + async def handler(request): + return json({"test": True}) + + stop_event = Event() + async def after_start(*args, **kwargs): + http_response = await local_request('get', '/') + response.value = http_response.text.encode() + stop_event.set() + + def rescue_crew(): + sleep(5) + stop_event.set() + + rescue_process = Process(target=rescue_crew) + rescue_process.start() + + app.serve_multiple({ + 'host': HOST, + 'port': PORT, + 'after_start': after_start, + 'request_handler': app.handle_request, + 'request_max_size': 100000, + }, workers=2, stop_event=stop_event) + + rescue_process.terminate() + + try: + results = json_loads(response.value) + except: + raise ValueError("Expected JSON response but got '{}'".format(response)) + + assert results.get('test') == True From c539933e3831e735fa4d82904c4f57b2035d1ccf Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 18 Oct 2016 01:31:09 -0700 Subject: [PATCH 005/977] Fixed unused import, added change log --- CHANGES | 7 +++++++ sanic/sanic.py | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 CHANGES diff --git a/CHANGES b/CHANGES new file mode 100644 index 00000000..1897bbba --- /dev/null +++ b/CHANGES @@ -0,0 +1,7 @@ +Version 0.1 +----------- + - 0.1.4 - Multiprocessing + - 0.1.3 - Blueprint support + - 0.1.1 - 0.1.2 - Struggling to update pypi via CI + +Released to public. \ No newline at end of file diff --git a/sanic/sanic.py b/sanic/sanic.py index cba19b9d..b05c47ea 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,4 +1,3 @@ -from argparse import ArgumentParser from asyncio import get_event_loop from inspect import isawaitable from multiprocessing import Process, Event From 4ecb4d2ccedba39dbf96bd51e8522317c3f45f6f Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 18 Oct 2016 01:38:50 -0700 Subject: [PATCH 006/977] Added newline to fix flake8 error --- CHANGES | 2 +- sanic/__main__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 1897bbba..a972c6b8 100644 --- a/CHANGES +++ b/CHANGES @@ -4,4 +4,4 @@ Version 0.1 - 0.1.3 - Blueprint support - 0.1.1 - 0.1.2 - Struggling to update pypi via CI -Released to public. \ No newline at end of file +Released to public. diff --git a/sanic/__main__.py b/sanic/__main__.py index 96652294..8bede98f 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -33,4 +33,4 @@ if __name__ == "__main__": " Example Module: project.sanic_server.app" .format(module_name)) except ValueError as e: - log.error("{}".format(e)) \ No newline at end of file + log.error("{}".format(e)) From 8142121c90c884346b50724a5326e7106fac4a9c Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 18 Oct 2016 01:51:17 -0700 Subject: [PATCH 007/977] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 03889fc5..78a8e9df 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup setup( name='Sanic', - version="0.1.3", + version="0.1.4", url='http://github.com/channelcat/sanic/', license='MIT', author='Channel Cat', From 452438dc0757fe2194c78af4b03ac6d2c2ea1ae1 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 18 Oct 2016 02:52:35 -0700 Subject: [PATCH 008/977] Delete test.py, not needed --- test.py | 52 ---------------------------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index 263aeae3..00000000 --- a/test.py +++ /dev/null @@ -1,52 +0,0 @@ -from multiprocessing import Array, Event, Process -from time import sleep -from ujson import loads as json_loads - -from sanic import Sanic -from sanic.response import json -from sanic.utils import local_request, HOST, PORT - - -# ------------------------------------------------------------ # -# GET -# ------------------------------------------------------------ # - -def test_json(): - app = Sanic('test_json') - - response = Array('c', 50) - @app.route('/') - async def handler(request): - return json({"test": True}) - - stop_event = Event() - async def after_start(*args, **kwargs): - http_response = await local_request('get', '/') - response.value = http_response.text.encode() - stop_event.set() - - def rescue_crew(): - sleep(5) - stop_event.set() - - rescue_process = Process(target=rescue_crew) - rescue_process.start() - - app.serve_multiple({ - 'host': HOST, - 'port': PORT, - 'after_start': after_start, - 'request_handler': app.handle_request, - 'request_max_size': 100000, - }, workers=2, stop_event=stop_event) - - rescue_process.terminate() - - try: - results = json_loads(response.value) - except: - raise ValueError("Expected JSON response but got '{}'".format(response)) - - assert results.get('test') == True - -test_json() \ No newline at end of file From 7c3faea0dde53163628462db4fe688fa47cc63ab Mon Sep 17 00:00:00 2001 From: yishibashi Date: Tue, 18 Oct 2016 18:50:28 +0900 Subject: [PATCH 009/977] comment fixed --- sanic/sanic.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index b05c47ea..56ea02ce 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -47,9 +47,8 @@ class Sanic: # Decorator def exception(self, *exceptions): """ - Decorates a function to be registered as a route - :param uri: path of the URL - :param methods: list or tuple of methods allowed + Decorates a function to be registered as a handler for exceptions + :param *exceptions: exceptions :return: decorated function """ From 3c05382e073d8c0661018ae3b7638b633413fc04 Mon Sep 17 00:00:00 2001 From: Kyle Frost Date: Tue, 18 Oct 2016 08:13:37 -0400 Subject: [PATCH 010/977] Fix routing doc typo --- docs/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/routing.md b/docs/routing.md index c07e1b81..4dbccadf 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -19,7 +19,7 @@ async def person_handler(request, integer_arg): @app.route('/number/') async def person_handler(request, number_arg): - return text('Number - {}'.format(number)) + return text('Number - {}'.format(number_arg)) @app.route('/person/') async def person_handler(request, name): From cbb1f99ccbb903374efa4941509b71230e3eb3fa Mon Sep 17 00:00:00 2001 From: Blake VandeMerwe Date: Tue, 18 Oct 2016 09:41:45 -0600 Subject: [PATCH 011/977] Adds `tornado` test server for speed comparison (#13) --- requirements-dev.txt | 3 ++- tests/performance/tornado/simple_server.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/performance/tornado/simple_server.py diff --git a/requirements-dev.txt b/requirements-dev.txt index ac3b05fb..9593b0cf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,4 +8,5 @@ tox gunicorn bottle kyoukai -falcon \ No newline at end of file +falcon +tornado \ No newline at end of file diff --git a/tests/performance/tornado/simple_server.py b/tests/performance/tornado/simple_server.py new file mode 100644 index 00000000..32192900 --- /dev/null +++ b/tests/performance/tornado/simple_server.py @@ -0,0 +1,19 @@ +# Run with: python simple_server.py +import ujson +from tornado import ioloop, web + + +class MainHandler(web.RequestHandler): + def get(self): + self.write(ujson.dumps({'test': True})) + + +app = web.Application([ + (r'/', MainHandler) +], debug=False, + compress_response=False, + static_hash_cache=True +) + +app.listen(8000) +ioloop.IOLoop.current().start() From 5e459cb69d6c5e1db662fcd634b58981878aef9c Mon Sep 17 00:00:00 2001 From: Blake VandeMerwe Date: Tue, 18 Oct 2016 10:05:29 -0600 Subject: [PATCH 012/977] Exposes `loop`in sanic `serve` and `run` functions (#64) --- sanic/sanic.py | 4 +++- sanic/server.py | 28 ++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index b05c47ea..1defff1e 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -170,7 +170,7 @@ class Sanic: # -------------------------------------------------------------------- # def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, - before_stop=None, sock=None, workers=1): + before_stop=None, sock=None, workers=1, loop=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -184,6 +184,7 @@ class Sanic: :param sock: Socket for the server to accept connections from :param workers: Number of processes received before it is respected + :param loop: asyncio compatible event loop :return: Nothing """ self.error_handler.debug = True @@ -197,6 +198,7 @@ class Sanic: 'request_handler': self.handle_request, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, + 'loop': loop } if debug: diff --git a/sanic/server.py b/sanic/server.py index 7379ade1..f8312f12 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -159,13 +159,29 @@ class HttpProtocol(asyncio.Protocol): def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60, sock=None, - request_max_size=None, reuse_port=False): - # Create Event Loop - loop = async_loop.new_event_loop() + request_max_size=None, reuse_port=False, loop=None): + """ + Starts asynchronous HTTP Server on an individual process. + :param host: Address to host on + :param port: Port to host on + :param request_handler: Sanic request handler with middleware + :param after_start: Function to be executed after the server starts + listening. Takes single argument `loop` + :param before_stop: Function to be executed when a stop signal is + received before it is respected. Takes single argumenet `loop` + :param debug: Enables debug output (slows server) + :param request_timeout: time in seconds + :param sock: Socket for the server to accept connections from + :param request_max_size: size in bytes, `None` for no limit + :param reuse_port: `True` for multiple workers + :param loop: asyncio compatible event loop + :return: Nothing + """ + loop = loop or async_loop.new_event_loop() asyncio.set_event_loop(loop) - # I don't think we take advantage of this - # And it slows everything waaayyy down - # loop.set_debug(debug) + + if debug: + loop.set_debug(debug) connections = {} signal = Signal() From 7b0f524fb3e44e9ac22b41f34f5e5c60f3ad1d7c Mon Sep 17 00:00:00 2001 From: Eshin Kunishima Date: Wed, 19 Oct 2016 01:53:11 +0900 Subject: [PATCH 013/977] Added HTTP status codes Based on http.HTTPStatus --- sanic/response.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/sanic/response.py b/sanic/response.py index 2bf9b167..9288df9e 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,18 +1,63 @@ import ujson STATUS_CODES = { + 100: b'Continue', + 101: b'Switching Protocols', + 102: b'Processing', 200: b'OK', + 201: b'Created', + 202: b'Accepted', + 203: b'Non-Authoritative Information', + 204: b'No Content', + 205: b'Reset Content', + 206: b'Partial Content', + 207: b'Multi-Status', + 208: b'Already Reported', + 226: b'IM Used', + 300: b'Multiple Choices', + 301: b'Moved Permanently', + 302: b'Found', + 303: b'See Other', + 304: b'Not Modified', + 305: b'Use Proxy', + 307: b'Temporary Redirect', + 308: b'Permanent Redirect', 400: b'Bad Request', 401: b'Unauthorized', 402: b'Payment Required', 403: b'Forbidden', 404: b'Not Found', 405: b'Method Not Allowed', + 406: b'Not Acceptable', + 407: b'Proxy Authentication Required', + 408: b'Request Timeout', + 409: b'Conflict', + 410: b'Gone', + 411: b'Length Required', + 412: b'Precondition Failed', + 413: b'Request Entity Too Large', + 414: b'Request-URI Too Long', + 415: b'Unsupported Media Type', + 416: b'Requested Range Not Satisfiable', + 417: b'Expectation Failed', + 422: b'Unprocessable Entity', + 423: b'Locked', + 424: b'Failed Dependency', + 426: b'Upgrade Required', + 428: b'Precondition Required', + 429: b'Too Many Requests', + 431: b'Request Header Fields Too Large', 500: b'Internal Server Error', 501: b'Not Implemented', 502: b'Bad Gateway', 503: b'Service Unavailable', 504: b'Gateway Timeout', + 505: b'HTTP Version Not Supported', + 506: b'Variant Also Negotiates', + 507: b'Insufficient Storage', + 508: b'Loop Detected', + 510: b'Not Extended', + 511: b'Network Authentication Required' } From c58741fe7a91969b49f6947faf015511549bc0df Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 18 Oct 2016 16:50:14 -0700 Subject: [PATCH 014/977] Changed start failure to print exception --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 7379ade1..99032472 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -180,7 +180,7 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, try: http_server = loop.run_until_complete(server_coroutine) except Exception as e: - log.error("Unable to start server: {}".format(e)) + log.exception("Unable to start server") return # Run the on_start function if provided From 6d2d9d3afc299e48f6b3ff6a4a9446d6c71f4b38 Mon Sep 17 00:00:00 2001 From: Eshin Kunishima Date: Wed, 19 Oct 2016 16:23:44 +0900 Subject: [PATCH 015/977] Added tests for Request.form --- tests/test_requests.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_requests.py b/tests/test_requests.py index 42dc3e8e..290c9b99 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -80,3 +80,38 @@ def test_post_json(): assert request.json.get('test') == 'OK' assert response.text == 'OK' + + +def test_post_form_urlencoded(): + app = Sanic('test_post_form_urlencoded') + + @app.route('/') + async def handler(request): + return text('OK') + + payload = 'test=OK' + headers = {'content-type': 'application/x-www-form-urlencoded'} + + request, response = sanic_endpoint_test(app, data=payload, headers=headers) + + assert request.form.get('test') == 'OK' + + +def test_post_form_multipart_form_data(): + app = Sanic('test_post_form_multipart_form_data') + + @app.route('/') + async def handler(request): + return text('OK') + + payload = '------sanic\r\n' \ + 'Content-Disposition: form-data; name="test"\r\n' \ + '\r\n' \ + 'OK\r\n' \ + '------sanic--\r\n' + + headers = {'content-type': 'multipart/form-data; boundary=----sanic'} + + request, response = sanic_endpoint_test(app, data=payload, headers=headers) + + assert request.form.get('test') == 'OK' From 3d00ca09b9baf3482c17a23936ef836ea3fad3f2 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 19 Oct 2016 08:37:35 +0000 Subject: [PATCH 016/977] Added fast lookup dict for common response codes --- sanic/response.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 9288df9e..0b744c0a 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,6 +1,12 @@ import ujson -STATUS_CODES = { +COMMON_STATUS_CODES = { + 200: b'OK', + 400: b'Bad Request', + 404: b'Not Found', + 500: b'Internal Server Error', +} +ALL_STATUS_CODES = { 100: b'Continue', 101: b'Switching Protocols', 102: b'Processing', @@ -89,6 +95,13 @@ class HTTPResponse: b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) for name, value in self.headers.items() ) + + # Try to pull from the common codes first + # Speeds up response rate 6% over pulling from all + status = COMMON_STATUS_CODES.get(self.status) + if not status: + status = ALL_STATUS_CODES.get(self.status) + return (b'HTTP/%b %d %b\r\n' b'Content-Type: %b\r\n' b'Content-Length: %d\r\n' @@ -97,7 +110,7 @@ class HTTPResponse: b'%b') % ( version.encode(), self.status, - STATUS_CODES.get(self.status, b'FAIL'), + status, self.content_type.encode(), len(self.body), b'keep-alive' if keep_alive else b'close', From 0327e6efba2346e19861d8ffd87ca08a45196531 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Wed, 19 Oct 2016 01:47:12 -0700 Subject: [PATCH 017/977] Added tornado benchmarks --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1f6c4718..73a9006e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E | Flask | gunicorn + meinheld | 4,988 | 20.08ms | | Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms | | Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms | +| Tornado | Python 3.5 | 2,138 | 46.66ms | ## Hello World From 7dcdc6208dc8b69d2ba1ea9826f7bd7b226c690c Mon Sep 17 00:00:00 2001 From: "Ludovic Gasc (GMLudo)" Date: Thu, 20 Oct 2016 01:01:51 +0200 Subject: [PATCH 018/977] Enable after_start and before_stop callbacks for multiprocess --- sanic/sanic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 7198d854..310a2175 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -197,6 +197,8 @@ class Sanic: 'request_handler': self.handle_request, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, + 'after_start': after_start, + 'before_stop': before_stop, 'loop': loop } @@ -209,8 +211,6 @@ class Sanic: try: if workers == 1: - server_settings['after_start'] = after_start - server_settings['before_stop'] = before_stop serve(**server_settings) else: log.info('Spinning up {} workers...'.format(workers)) From f2cc404d7f003ec7043623a1ab3a0e262dc3931e Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Wed, 19 Oct 2016 23:41:22 -0400 Subject: [PATCH 019/977] Remove simple router --- sanic/router.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index e6c580d7..a4ac68bb 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -113,34 +113,3 @@ class Router: return route.handler, args, kwargs else: raise NotFound("Requested URL {} not found".format(request.url)) - - -class SimpleRouter: - """ - Simple router records and reads all routes from a dictionary - It does not support parameters in routes, but is very fast - """ - routes = None - - def __init__(self): - self.routes = {} - - def add(self, uri, methods, handler): - # Dict for faster lookups of method allowed - methods_dict = None - if methods: - methods_dict = {method: True for method in methods} - self.routes[uri] = Route( - handler=handler, methods=methods_dict, pattern=uri, - parameters=None) - - def get(self, request): - route = self.routes.get(request.url) - if route: - if route.methods and request.method not in route.methods: - raise InvalidUsage( - "Method {} not allowed for URL {}".format( - request.method, request.url), status_code=405) - return route.handler, [], {} - else: - raise NotFound("Requested URL {} not found".format(request.url)) From 50e4dd167e465af2c26ff4d092b49bbf5edd71d6 Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Wed, 19 Oct 2016 23:43:31 -0400 Subject: [PATCH 020/977] Extract constant --- sanic/router.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index a4ac68bb..0d04365a 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -5,6 +5,13 @@ from .exceptions import NotFound, InvalidUsage Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) Parameter = namedtuple("Parameter", ['name', 'cast']) +REGEX_TYPES = { + "string": (None, "[^/]+"), + "int": (int, "\d+"), + "number": (float, "[0-9\\.]+"), + "alpha": (None, "[A-Za-z]+"), +} + class Router: """ @@ -25,12 +32,6 @@ class Router: I should feel bad """ routes = None - regex_types = { - "string": (None, "[^/]+"), - "int": (int, "\d+"), - "number": (float, "[0-9\\.]+"), - "alpha": (None, "[A-Za-z]+"), - } def __init__(self): self.routes = [] @@ -63,7 +64,7 @@ class Router: parameter_pattern = 'string' # Pull from pre-configured types - parameter_regex = self.regex_types.get(parameter_pattern) + parameter_regex = REGEX_TYPES.get(parameter_pattern) if parameter_regex: parameter_type, parameter_pattern = parameter_regex else: From 04a6cc9416b77ae99a1fe41d20e01e015a20ab3e Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Wed, 19 Oct 2016 23:51:40 -0400 Subject: [PATCH 021/977] Refactor add parameter --- sanic/router.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 0d04365a..2b997abe 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -56,24 +56,16 @@ class Router: def add_parameter(match): # We could receive NAME or NAME:PATTERN - parts = match.group(1).split(':') - if len(parts) == 2: - parameter_name, parameter_pattern = parts - else: - parameter_name = parts[0] - parameter_pattern = 'string' + parameter_name = match.group(1) + parameter_pattern = 'string' + if ':' in parameter_name: + parameter_name, parameter_pattern = parameter_name.split(':', 1) + default = (None, parameter_pattern) # Pull from pre-configured types - parameter_regex = REGEX_TYPES.get(parameter_pattern) - if parameter_regex: - parameter_type, parameter_pattern = parameter_regex - else: - parameter_type = None - - parameter = Parameter(name=parameter_name, cast=parameter_type) - parameters.append(parameter) - - return "({})".format(parameter_pattern) + parameter_type, parameter_pattern = REGEX_TYPES.get(parameter_pattern, default) + parameters.append(Parameter(name=parameter_name, cast=parameter_type)) + return '({})'.format(parameter_pattern) pattern_string = re.sub("<(.+?)>", add_parameter, uri) pattern = re.compile("^{}$".format(pattern_string)) From e25e1c0e4ba9a841216ba7c003d1cd5154cba46c Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Wed, 19 Oct 2016 23:56:23 -0400 Subject: [PATCH 022/977] Convert string formats --- sanic/router.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 2b997abe..65ade57a 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -2,14 +2,14 @@ import re from collections import namedtuple from .exceptions import NotFound, InvalidUsage -Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) -Parameter = namedtuple("Parameter", ['name', 'cast']) +Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) +Parameter = namedtuple('Parameter', ['name', 'cast']) REGEX_TYPES = { - "string": (None, "[^/]+"), - "int": (int, "\d+"), - "number": (float, "[0-9\\.]+"), - "alpha": (None, "[A-Za-z]+"), + 'string': (None, r'[^/]+'), + 'int': (int, r'\d+'), + 'number': (float, r'[0-9\\.]+'), + 'alpha': (None, r'[A-Za-z]+'), } @@ -67,8 +67,8 @@ class Router: parameters.append(Parameter(name=parameter_name, cast=parameter_type)) return '({})'.format(parameter_pattern) - pattern_string = re.sub("<(.+?)>", add_parameter, uri) - pattern = re.compile("^{}$".format(pattern_string)) + pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) + pattern = re.compile(r'^{}$'.format(pattern_string)) route = Route( handler=handler, methods=methods_dict, pattern=pattern, @@ -101,8 +101,8 @@ class Router: if route: if route.methods and request.method not in route.methods: raise InvalidUsage( - "Method {} not allowed for URL {}".format( + 'Method {} not allowed for URL {}'.format( request.method, request.url), status_code=405) return route.handler, args, kwargs else: - raise NotFound("Requested URL {} not found".format(request.url)) + raise NotFound('Requested URL {} not found'.format(request.url)) From baf1ce95b18176dfd452dbafecc0552cdc1487ef Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Thu, 20 Oct 2016 00:05:55 -0400 Subject: [PATCH 023/977] Refactor get --- sanic/router.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 65ade57a..ece5caad 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -6,10 +6,10 @@ Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) Parameter = namedtuple('Parameter', ['name', 'cast']) REGEX_TYPES = { - 'string': (None, r'[^/]+'), + 'string': (str, r'[^/]+'), 'int': (int, r'\d+'), 'number': (float, r'[0-9\\.]+'), - 'alpha': (None, r'[A-Za-z]+'), + 'alpha': (str, r'[A-Za-z]+'), } @@ -61,7 +61,7 @@ class Router: if ':' in parameter_name: parameter_name, parameter_pattern = parameter_name.split(':', 1) - default = (None, parameter_pattern) + default = (str, parameter_pattern) # Pull from pre-configured types parameter_type, parameter_pattern = REGEX_TYPES.get(parameter_pattern, default) parameters.append(Parameter(name=parameter_name, cast=parameter_type)) @@ -82,27 +82,18 @@ class Router: :param request: Request object :return: handler, arguments, keyword arguments """ - route = None - args = [] - kwargs = {} - for _route in self.routes: - match = _route.pattern.match(request.url) + for route in self.routes: + match = route.pattern.match(request.url) if match: - for index, parameter in enumerate(_route.parameters, start=1): - value = match.group(index) - if parameter.cast: - kwargs[parameter.name] = parameter.cast(value) - else: - kwargs[parameter.name] = value - route = _route break - - if route: - if route.methods and request.method not in route.methods: - raise InvalidUsage( - 'Method {} not allowed for URL {}'.format( - request.method, request.url), status_code=405) - return route.handler, args, kwargs else: raise NotFound('Requested URL {} not found'.format(request.url)) + + if route.methods and request.method not in route.methods: + raise InvalidUsage( + 'Method {} not allowed for URL {}'.format( + request.method, request.url), status_code=405) + + kwargs = {p.name: p.cast(value) for value, p in zip(match.groups(1), route.parameters)} + return route.handler, [], kwargs From d1beabfc8fde95811c205ed2360871ef3ccf5814 Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Thu, 20 Oct 2016 00:07:07 -0400 Subject: [PATCH 024/977] Add lru_cache to get --- sanic/config.py | 1 + sanic/router.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/sanic/config.py b/sanic/config.py index 8261c2c0..3dbf06c8 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -22,3 +22,4 @@ class Config: """ REQUEST_MAX_SIZE = 100000000 # 100 megababies REQUEST_TIMEOUT = 60 # 60 seconds + ROUTER_CACHE_SIZE = 1024 diff --git a/sanic/router.py b/sanic/router.py index ece5caad..45248cde 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -1,5 +1,7 @@ import re from collections import namedtuple +from functools import lru_cache +from .config import Config from .exceptions import NotFound, InvalidUsage Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) @@ -75,6 +77,7 @@ class Router: parameters=parameters) self.routes.append(route) + @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) def get(self, request): """ Gets a request handler based on the URL of the request, or raises an From f4b45deb7ffa299b5f17fde787c7b9152ba96b14 Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Thu, 20 Oct 2016 00:16:16 -0400 Subject: [PATCH 025/977] Convert dict to set --- sanic/router.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 45248cde..2d04c9b9 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -50,9 +50,8 @@ class Router: """ # Dict for faster lookups of if method allowed - methods_dict = None if methods: - methods_dict = {method: True for method in methods} + methods = frozenset(methods) parameters = [] @@ -73,7 +72,7 @@ class Router: pattern = re.compile(r'^{}$'.format(pattern_string)) route = Route( - handler=handler, methods=methods_dict, pattern=pattern, + handler=handler, methods=methods, pattern=pattern, parameters=parameters) self.routes.append(route) From fc4c192237f484dae03ead98174a7ec081009d4e Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Thu, 20 Oct 2016 01:07:16 -0400 Subject: [PATCH 026/977] Add simple uri hash to lookup --- sanic/router.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 2d04c9b9..f0f7de43 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -1,5 +1,5 @@ import re -from collections import namedtuple +from collections import defaultdict, namedtuple from functools import lru_cache from .config import Config from .exceptions import NotFound, InvalidUsage @@ -15,6 +15,10 @@ REGEX_TYPES = { } +def url_hash(url): + return '/'.join(':' for s in url.split('/')) + + class Router: """ Router supports basic routing with parameters and method checks @@ -36,7 +40,7 @@ class Router: routes = None def __init__(self): - self.routes = [] + self.routes = defaultdict(list) def add(self, uri, methods, handler): """ @@ -74,7 +78,10 @@ class Router: route = Route( handler=handler, methods=methods, pattern=pattern, parameters=parameters) - self.routes.append(route) + + if parameters: + uri = url_hash(uri) + self.routes[uri].append(route) @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) def get(self, request): @@ -85,17 +92,22 @@ class Router: :return: handler, arguments, keyword arguments """ route = None - for route in self.routes: - match = route.pattern.match(request.url) - if match: - break + url = request.url + if url in self.routes: + route = self.routes[url][0] + match = route.pattern.match(url) else: - raise NotFound('Requested URL {} not found'.format(request.url)) + for route in self.routes[url_hash(url)]: + match = route.pattern.match(url) + if match: + break + else: + raise NotFound('Requested URL {} not found'.format(url)) if route.methods and request.method not in route.methods: raise InvalidUsage( 'Method {} not allowed for URL {}'.format( - request.method, request.url), status_code=405) + request.method, url), status_code=405) kwargs = {p.name: p.cast(value) for value, p in zip(match.groups(1), route.parameters)} return route.handler, [], kwargs From f51055088857e004c48f24577415b1d8bf12b94a Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Thu, 20 Oct 2016 01:33:59 -0400 Subject: [PATCH 027/977] Fix flake8 --- sanic/router.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index f0f7de43..67b4ca30 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -61,16 +61,16 @@ class Router: def add_parameter(match): # We could receive NAME or NAME:PATTERN - parameter_name = match.group(1) - parameter_pattern = 'string' - if ':' in parameter_name: - parameter_name, parameter_pattern = parameter_name.split(':', 1) + name = match.group(1) + pattern = 'string' + if ':' in name: + name, pattern = name.split(':', 1) - default = (str, parameter_pattern) + default = (str, pattern) # Pull from pre-configured types - parameter_type, parameter_pattern = REGEX_TYPES.get(parameter_pattern, default) - parameters.append(Parameter(name=parameter_name, cast=parameter_type)) - return '({})'.format(parameter_pattern) + _type, pattern = REGEX_TYPES.get(pattern, default) + parameters.append(Parameter(name=name, cast=_type)) + return '({})'.format(pattern) pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) pattern = re.compile(r'^{}$'.format(pattern_string)) @@ -109,5 +109,7 @@ class Router: 'Method {} not allowed for URL {}'.format( request.method, url), status_code=405) - kwargs = {p.name: p.cast(value) for value, p in zip(match.groups(1), route.parameters)} + kwargs = {p.name: p.cast(value) + for value, p + in zip(match.groups(1), route.parameters)} return route.handler, [], kwargs From d4e2d94816f56dbbd5264da22219855e3cbf047e Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Thu, 20 Oct 2016 11:33:28 +0000 Subject: [PATCH 028/977] Added support for routes with / in custom regexes and updated lru to use url and method --- sanic/router.py | 75 +++++++++++++++++++++++++++++++------------- tests/test_routes.py | 66 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 118 insertions(+), 23 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 67b4ca30..951b49bc 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -16,7 +16,11 @@ REGEX_TYPES = { def url_hash(url): - return '/'.join(':' for s in url.split('/')) + return url.count('/') + + +class RouteExists(Exception): + pass class Router: @@ -31,16 +35,16 @@ class Router: function provided Parameters can also have a type by appending :type to the . If no type is provided, a string is expected. A regular expression can also be passed in as the type - - TODO: - This probably needs optimization for larger sets of routes, - since it checks every route until it finds a match which is bad and - I should feel bad """ - routes = None + routes_static = None + routes_dynamic = None + routes_always_check = None def __init__(self): - self.routes = defaultdict(list) + self.routes_all = {} + self.routes_static = {} + self.routes_dynamic = defaultdict(list) + self.routes_always_check = [] def add(self, uri, methods, handler): """ @@ -52,12 +56,15 @@ class Router: When executed, it should provide a response object. :return: Nothing """ + if uri in self.routes_all: + raise RouteExists("Route already registered: {}".format(uri)) # Dict for faster lookups of if method allowed if methods: methods = frozenset(methods) parameters = [] + properties = {"unhashable": None} def add_parameter(match): # We could receive NAME or NAME:PATTERN @@ -69,7 +76,13 @@ class Router: default = (str, pattern) # Pull from pre-configured types _type, pattern = REGEX_TYPES.get(pattern, default) - parameters.append(Parameter(name=name, cast=_type)) + parameter = Parameter(name=name, cast=_type) + parameters.append(parameter) + + # Mark the whole route as unhashable if it has the hash key in it + if re.search('(^|[^^]){1}/', pattern): + properties['unhashable'] = True + return '({})'.format(pattern) pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) @@ -79,11 +92,14 @@ class Router: handler=handler, methods=methods, pattern=pattern, parameters=parameters) - if parameters: - uri = url_hash(uri) - self.routes[uri].append(route) + self.routes_all[uri] = route + if properties['unhashable']: + self.routes_always_check.append(route) + elif parameters: + self.routes_dynamic[url_hash(uri)].append(route) + else: + self.routes_static[uri] = route - @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) def get(self, request): """ Gets a request handler based on the URL of the request, or raises an @@ -91,23 +107,40 @@ class Router: :param request: Request object :return: handler, arguments, keyword arguments """ - route = None - url = request.url - if url in self.routes: - route = self.routes[url][0] + return self._get(request.url, request.method) + + @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) + def _get(self, url, method): + """ + Gets a request handler based on the URL of the request, or raises an + error. Internal method for caching. + :param url: Request URL + :param method: Request method + :return: handler, arguments, keyword arguments + """ + # Check against known static routes + route = self.routes_static.get(url) + if route: match = route.pattern.match(url) else: - for route in self.routes[url_hash(url)]: + # Move on to testing all regex routes + for route in self.routes_dynamic[url_hash(url)]: match = route.pattern.match(url) if match: break else: - raise NotFound('Requested URL {} not found'.format(url)) + # Lastly, check against all regex routes that cannot be hashed + for route in self.routes_always_check: + match = route.pattern.match(url) + if match: + break + else: + raise NotFound('Requested URL {} not found'.format(url)) - if route.methods and request.method not in route.methods: + if route.methods and method not in route.methods: raise InvalidUsage( 'Method {} not allowed for URL {}'.format( - request.method, url), status_code=405) + method, url), status_code=405) kwargs = {p.name: p.cast(value) for value, p diff --git a/tests/test_routes.py b/tests/test_routes.py index 640f3422..4759e450 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -1,6 +1,8 @@ -from json import loads as json_loads, dumps as json_dumps +import pytest + from sanic import Sanic -from sanic.response import json, text +from sanic.response import text +from sanic.router import RouteExists from sanic.utils import sanic_endpoint_test @@ -8,6 +10,24 @@ from sanic.utils import sanic_endpoint_test # UTF-8 # ------------------------------------------------------------ # +def test_static_routes(): + app = Sanic('test_dynamic_route') + + @app.route('/test') + async def handler1(request): + return text('OK1') + + @app.route('/pizazz') + async def handler2(request): + return text('OK2') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.text == 'OK1' + + request, response = sanic_endpoint_test(app, uri='/pizazz') + assert response.text == 'OK2' + + def test_dynamic_route(): app = Sanic('test_dynamic_route') @@ -102,3 +122,45 @@ def test_dynamic_route_regex(): request, response = sanic_endpoint_test(app, uri='/folder/') assert response.status == 200 + + +def test_dynamic_route_unhashable(): + app = Sanic('test_dynamic_route_unhashable') + + @app.route('/folder//end/') + async def handler(request, unhashable): + return text('OK') + + request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/nope/') + assert response.status == 404 + + +def test_route_duplicate(): + app = Sanic('test_dynamic_route') + + with pytest.raises(RouteExists): + @app.route('/test') + async def handler1(request): + pass + + @app.route('/test') + async def handler2(request): + pass + + with pytest.raises(RouteExists): + @app.route('/test//') + async def handler1(request, dynamic): + pass + + @app.route('/test//') + async def handler2(request, dynamic): + pass From c256825de6b77027290bd29dedd756fe8567eaef Mon Sep 17 00:00:00 2001 From: Hyungtae Kim Date: Thu, 20 Oct 2016 13:38:03 -0700 Subject: [PATCH 029/977] Content Type of JSON response should not have a charset --- sanic/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 0b744c0a..be471078 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -122,7 +122,7 @@ class HTTPResponse: def json(body, status=200, headers=None): return HTTPResponse(ujson.dumps(body), headers=headers, status=status, - content_type="application/json; charset=utf-8") + content_type="application/json") def text(body, status=200, headers=None): From e060dbfec88f67db7f74164d72cf506bc114d99d Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 21 Oct 2016 00:11:52 -0700 Subject: [PATCH 030/977] Moved changelog and posted new benchmarks in readme --- CHANGES => CHANGELOG.md | 0 README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename CHANGES => CHANGELOG.md (100%) diff --git a/CHANGES b/CHANGELOG.md similarity index 100% rename from CHANGES rename to CHANGELOG.md diff --git a/README.md b/README.md index 73a9006e..3b4ba359 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E | Server | Implementation | Requests/sec | Avg Latency | | ------- | ------------------- | ------------:| -----------:| -| Sanic | Python 3.5 + uvloop | 30,601 | 3.23ms | +| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms | | Wheezy | gunicorn + meinheld | 20,244 | 4.94ms | | Falcon | gunicorn + meinheld | 18,972 | 5.27ms | | Bottle | gunicorn + meinheld | 13,596 | 7.36ms | From 2312a176fe70d697eb6ccd25fa0a12cb28412976 Mon Sep 17 00:00:00 2001 From: pcdinh Date: Fri, 21 Oct 2016 17:55:30 +0700 Subject: [PATCH 031/977] Document `request.body` as a way to get raw POST body --- docs/request_data.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/request_data.md b/docs/request_data.md index bc89bb88..8891d07f 100644 --- a/docs/request_data.md +++ b/docs/request_data.md @@ -8,6 +8,7 @@ The following request variables are accessible as properties: `request.json` (any) - JSON body `request.args` (dict) - Query String variables. Use getlist to get multiple of the same name `request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name +`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type See request.py for more information @@ -15,7 +16,7 @@ See request.py for more information ```python from sanic import Sanic -from sanic.response import json +from sanic.response import json, text @app.route("/json") def post_json(request): @@ -40,4 +41,9 @@ def post_json(request): @app.route("/query_string") def query_string(request): return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) + + +@app.route("/users", methods=["POST",]) +def create_user(request): + return text("You are trying to create a user with the following POST: %s" % request.body) ``` From a5614f688082ab98051e32354d139fbb77aceab3 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 21 Oct 2016 04:11:18 -0700 Subject: [PATCH 032/977] Added server start/stop listeners and reverse ordering on response middleware to blueprints --- docs/blueprints.md | 23 +++++++++++++++ sanic/blueprints.py | 19 +++++++++++- sanic/sanic.py | 63 +++++++++++++++++++++++++++++++++++----- sanic/server.py | 33 ++++++++++++++------- sanic/utils.py | 2 +- tests/test_blueprints.py | 38 +++++++++++++++++++++++- 6 files changed, 157 insertions(+), 21 deletions(-) diff --git a/docs/blueprints.md b/docs/blueprints.md index 7a4567ee..e80a125e 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -80,3 +80,26 @@ Exceptions can also be applied exclusively to blueprints globally. def ignore_404s(request, exception): return text("Yep, I totally found the page: {}".format(request.url)) ``` + +## Start and Stop +Blueprints and run functions during the start and stop process of the server. +If running in multiprocessor mode (more than 1 worker), these are triggered +Available events are: + + * before_server_start - Executed before the server begins to accept connections + * after_server_start - Executed after the server begins to accept connections + * before_server_stop - Executed before the server stops accepting connections + * after_server_stop - Executed after the server is stopped and all requests are complete + +```python +bp = Blueprint('my_blueprint') + +@bp.listen('before_server_start') +async def setup_connection(): + global database + database = mysql.connect(host='127.0.0.1'...) + +@bp.listen('after_server_stop') +async def close_connection(): + await database.close() +``` diff --git a/sanic/blueprints.py b/sanic/blueprints.py index f1aa2afc..37cfa1c3 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -1,3 +1,6 @@ +from collections import defaultdict + + class BlueprintSetup: """ """ @@ -22,7 +25,7 @@ class BlueprintSetup: if self.url_prefix: uri = self.url_prefix + uri - self.app.router.add(uri, methods, handler) + self.app.route(uri=uri, methods=methods)(handler) def add_exception(self, handler, *args, **kwargs): """ @@ -42,9 +45,15 @@ class BlueprintSetup: class Blueprint: def __init__(self, name, url_prefix=None): + """ + Creates a new blueprint + :param name: Unique name of the blueprint + :param url_prefix: URL to be prefixed before all route URLs + """ self.name = name self.url_prefix = url_prefix self.deferred_functions = [] + self.listeners = defaultdict(list) def record(self, func): """ @@ -73,6 +82,14 @@ class Blueprint: return handler return decorator + def listener(self, event): + """ + """ + def decorator(listener): + self.listeners[event].append(listener) + return listener + return decorator + def middleware(self, *args, **kwargs): """ """ diff --git a/sanic/sanic.py b/sanic/sanic.py index 310a2175..f8189d77 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,4 +1,5 @@ from asyncio import get_event_loop +from functools import partial from inspect import isawaitable from multiprocessing import Process, Event from signal import signal, SIGTERM, SIGINT @@ -24,6 +25,8 @@ class Sanic: self.response_middleware = [] self.blueprints = {} self._blueprint_order = [] + self.loop = None + self.debug = None # -------------------------------------------------------------------- # # Registration @@ -71,7 +74,7 @@ class Sanic: if attach_to == 'request': self.request_middleware.append(middleware) if attach_to == 'response': - self.response_middleware.append(middleware) + self.response_middleware.insert(0, middleware) return middleware # Detect which way this was called, @middleware or @middleware('AT') @@ -102,6 +105,9 @@ class Sanic: # Request Handling # -------------------------------------------------------------------- # + def converted_response_type(self, response): + pass + async def handle_request(self, request, response_callback): """ Takes a request from the HTTP Server and returns a response object to @@ -113,7 +119,10 @@ class Sanic: :return: Nothing """ try: - # Middleware process_request + # -------------------------------------------- # + # Request Middleware + # -------------------------------------------- # + response = False # The if improves speed. I don't know why if self.request_middleware: @@ -126,6 +135,10 @@ class Sanic: # No middleware results if not response: + # -------------------------------------------- # + # Execute Handler + # -------------------------------------------- # + # Fetch handler from router handler, args, kwargs = self.router.get(request) if handler is None: @@ -138,7 +151,10 @@ class Sanic: if isawaitable(response): response = await response - # Middleware process_response + # -------------------------------------------- # + # Response Middleware + # -------------------------------------------- # + if self.response_middleware: for middleware in self.response_middleware: _response = middleware(request, response) @@ -149,6 +165,10 @@ class Sanic: break except Exception as e: + # -------------------------------------------- # + # Response Generation Failed + # -------------------------------------------- # + try: response = self.error_handler.response(request, e) if isawaitable(response): @@ -168,18 +188,23 @@ class Sanic: # Execution # -------------------------------------------------------------------- # - def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, - before_stop=None, sock=None, workers=1, loop=None): + def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, + after_start=None, before_stop=None, after_stop=None, sock=None, + workers=1, loop=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. :param host: Address to host on :param port: Port to host on :param debug: Enables debug output (slows server) + :param before_start: Function to be executed before the server starts + accepting connections :param after_start: Function to be executed after the server starts - listening + accepting connections :param before_stop: Function to be executed when a stop signal is received before it is respected + :param after_stop: Function to be executed when all requests are + complete :param sock: Socket for the server to accept connections from :param workers: Number of processes received before it is respected @@ -188,6 +213,7 @@ class Sanic: """ self.error_handler.debug = True self.debug = debug + self.loop = loop server_settings = { 'host': host, @@ -197,11 +223,32 @@ class Sanic: 'request_handler': self.handle_request, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, - 'after_start': after_start, - 'before_stop': before_stop, 'loop': loop } + # -------------------------------------------- # + # Register start/stop events + # -------------------------------------------- # + + for event_name, settings_name, args, reverse in ( + ("before_server_start", "before_start", before_start, False), + ("after_server_start", "after_start", after_start, False), + ("before_server_stop", "before_stop", before_stop, True), + ("after_server_stop", "after_stop", after_stop, True), + ): + listeners = [] + for blueprint in self.blueprints.values(): + listeners += blueprint.listeners[event_name] + if args: + if type(args) is not list: + args = [args] + listeners += args + if reverse: + listeners.reverse() + # Prepend sanic to the arguments when listeners are triggered + listeners = [partial(listener, self) for listener in listeners] + server_settings[settings_name] = listeners + if debug: log.setLevel(logging.DEBUG) log.debug(self.config.LOGO) diff --git a/sanic/server.py b/sanic/server.py index 63563269..eddff48c 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -157,7 +157,22 @@ class HttpProtocol(asyncio.Protocol): return False -def serve(host, port, request_handler, after_start=None, before_stop=None, +def trigger_events(events, loop): + """ + :param events: one or more sync or async functions to execute + :param loop: event loop + """ + if events: + if type(events) is not list: + events = [events] + for event in events: + result = event(loop) + if isawaitable(result): + loop.run_until_complete(result) + + +def serve(host, port, request_handler, before_start=None, after_start=None, + before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, request_max_size=None, reuse_port=False, loop=None): """ @@ -183,6 +198,8 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, if debug: loop.set_debug(debug) + trigger_events(before_start, loop) + connections = {} signal = Signal() server_coroutine = loop.create_server(lambda: HttpProtocol( @@ -193,17 +210,14 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, request_timeout=request_timeout, request_max_size=request_max_size, ), host, port, reuse_port=reuse_port, sock=sock) + try: http_server = loop.run_until_complete(server_coroutine) except Exception as e: log.exception("Unable to start server") return - # Run the on_start function if provided - if after_start: - result = after_start(loop) - if isawaitable(result): - loop.run_until_complete(result) + trigger_events(after_start, loop) # Register signals for graceful termination for _signal in (SIGINT, SIGTERM): @@ -215,10 +229,7 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, log.info("Stop requested, draining connections...") # Run the on_stop function if provided - if before_stop: - result = before_stop(loop) - if isawaitable(result): - loop.run_until_complete(result) + trigger_events(before_stop, loop) # Wait for event loop to finish and all connections to drain http_server.close() @@ -232,4 +243,6 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, while connections: loop.run_until_complete(asyncio.sleep(0.1)) + trigger_events(after_stop, loop) + loop.close() diff --git a/sanic/utils.py b/sanic/utils.py index c39f03ab..e731112e 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -24,7 +24,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, def _collect_request(request): results.append(request) - async def _collect_response(loop): + async def _collect_response(sanic, loop): try: response = await local_request(method, uri, *request_args, **request_kwargs) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 8068160f..1b88795d 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -108,4 +108,40 @@ def test_bp_exception_handler(): assert response.text == 'OK' request, response = sanic_endpoint_test(app, uri='/3') - assert response.status == 200 \ No newline at end of file + assert response.status == 200 + +def test_bp_listeners(): + app = Sanic('test_middleware') + blueprint = Blueprint('test_middleware') + + order = [] + + @blueprint.listener('before_server_start') + def handler_1(sanic, loop): + order.append(1) + + @blueprint.listener('after_server_start') + def handler_2(sanic, loop): + order.append(2) + + @blueprint.listener('after_server_start') + def handler_3(sanic, loop): + order.append(3) + + @blueprint.listener('before_server_stop') + def handler_4(sanic, loop): + order.append(5) + + @blueprint.listener('before_server_stop') + def handler_5(sanic, loop): + order.append(4) + + @blueprint.listener('after_server_stop') + def handler_6(sanic, loop): + order.append(6) + + app.register_blueprint(blueprint) + + request, response = sanic_endpoint_test(app, uri='/') + + assert order == [1,2,3,4,5,6] \ No newline at end of file From 77c69e3810124db4cf11b3f682adbfee0a6a21ea Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 21 Oct 2016 04:11:40 -0700 Subject: [PATCH 033/977] . --- docs/blueprints.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/blueprints.md b/docs/blueprints.md index e80a125e..029e61d8 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -84,6 +84,7 @@ def ignore_404s(request, exception): ## Start and Stop Blueprints and run functions during the start and stop process of the server. If running in multiprocessor mode (more than 1 worker), these are triggered +after forking Available events are: * before_server_start - Executed before the server begins to accept connections From 9b561e83e35b61a9633d3fd31260fe9f0be9c69c Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 21 Oct 2016 04:14:50 -0700 Subject: [PATCH 034/977] Revert "." This reverts commit 77c69e3810124db4cf11b3f682adbfee0a6a21ea. --- docs/blueprints.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/blueprints.md b/docs/blueprints.md index 029e61d8..e80a125e 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -84,7 +84,6 @@ def ignore_404s(request, exception): ## Start and Stop Blueprints and run functions during the start and stop process of the server. If running in multiprocessor mode (more than 1 worker), these are triggered -after forking Available events are: * before_server_start - Executed before the server begins to accept connections From f540f1e7c459faf0e3ded7e328058de1da362ea1 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 21 Oct 2016 04:32:05 -0700 Subject: [PATCH 035/977] reverting reverted change --- docs/blueprints.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blueprints.md b/docs/blueprints.md index e80a125e..e0fb09c7 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -83,7 +83,7 @@ def ignore_404s(request, exception): ## Start and Stop Blueprints and run functions during the start and stop process of the server. -If running in multiprocessor mode (more than 1 worker), these are triggered +If running in multiprocessor mode (more than 1 worker), these are triggered after the workers fork Available events are: * before_server_start - Executed before the server begins to accept connections From 452764a8eb88117c6c71ed77dca059f25ab67cf4 Mon Sep 17 00:00:00 2001 From: pcdinh Date: Fri, 21 Oct 2016 17:55:30 +0700 Subject: [PATCH 036/977] Document `request.body` as a way to get raw POST body --- docs/request_data.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/request_data.md b/docs/request_data.md index bc89bb88..8891d07f 100644 --- a/docs/request_data.md +++ b/docs/request_data.md @@ -8,6 +8,7 @@ The following request variables are accessible as properties: `request.json` (any) - JSON body `request.args` (dict) - Query String variables. Use getlist to get multiple of the same name `request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name +`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type See request.py for more information @@ -15,7 +16,7 @@ See request.py for more information ```python from sanic import Sanic -from sanic.response import json +from sanic.response import json, text @app.route("/json") def post_json(request): @@ -40,4 +41,9 @@ def post_json(request): @app.route("/query_string") def query_string(request): return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) + + +@app.route("/users", methods=["POST",]) +def create_user(request): + return text("You are trying to create a user with the following POST: %s" % request.body) ``` From 268a87e3b44a70800ee0feb44d2391abfe0e241c Mon Sep 17 00:00:00 2001 From: Roger Erens Date: Fri, 21 Oct 2016 23:47:13 +0200 Subject: [PATCH 037/977] Fix typos I guess renaming was forgotten in a copy-n-paste frenzy?! --- docs/routing.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/routing.md b/docs/routing.md index 4dbccadf..bca55919 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -10,15 +10,15 @@ from sanic import Sanic from sanic.response import text @app.route('/tag/') -async def person_handler(request, tag): +async def tag_handler(request, tag): return text('Tag - {}'.format(tag)) @app.route('/number/') -async def person_handler(request, integer_arg): +async def integer_handler(request, integer_arg): return text('Integer - {}'.format(integer_arg)) @app.route('/number/') -async def person_handler(request, number_arg): +async def number_handler(request, number_arg): return text('Number - {}'.format(number_arg)) @app.route('/person/') From 113047d450e53000e588dd158d715f3e993ac38f Mon Sep 17 00:00:00 2001 From: narzeja Date: Sat, 22 Oct 2016 07:13:14 +0200 Subject: [PATCH 038/977] Simple blueprint was missing the 'request' parameter --- docs/blueprints.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blueprints.md b/docs/blueprints.md index 7a4567ee..4fcbcffb 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -29,7 +29,7 @@ from sanic import Blueprint bp = Blueprint('my_blueprint') @bp.route('/') -async def bp_root(): +async def bp_root(request): return json({'my': 'blueprint'}) ``` From 22876b31b16d4f8497d83eb24913c76b1f5a780a Mon Sep 17 00:00:00 2001 From: narzeja Date: Sat, 22 Oct 2016 08:36:46 +0200 Subject: [PATCH 039/977] Provide example of using peewee_async with Sanic --- examples/sanic_peewee.py | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 examples/sanic_peewee.py diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py new file mode 100644 index 00000000..05527198 --- /dev/null +++ b/examples/sanic_peewee.py @@ -0,0 +1,70 @@ +## You need the following additional packages for this example +# aiopg +# peewee_async +# peewee + + +## sanic imports +from sanic import Sanic +from sanic.response import json + +## peewee_async related imports +import uvloop +import peewee +from peewee_async import Manager, PostgresqlDatabase + + # we instantiate a custom loop so we can pass it to our db manager +loop = uvloop.new_event_loop() + +database = PostgresqlDatabase(database='test', + host='127.0.0.1', + user='postgres', + password='mysecretpassword') + +objects = Manager(database, loop=loop) + +## from peewee_async docs: +# Also there’s no need to connect and re-connect before executing async queries +# with manager! It’s all automatic. But you can run Manager.connect() or +# Manager.close() when you need it. + + +# let's create a simple key value store: +class KeyValue(peewee.Model): + key = peewee.CharField(max_length=40, unique=True) + text = peewee.TextField(default='') + + class Meta: + database = database + +# create table synchronously +KeyValue.create_table(True) + +# OPTIONAL: close synchronous connection +database.close() + +# OPTIONAL: disable any future syncronous calls +objects.database.allow_sync = False # this will raise AssertionError on ANY sync call + + +app = Sanic('peewee_example') + +@app.route('/post') +async def root(request): + await objects.create(KeyValue, key='my_first_async_db', text="I was inserted asynchronously!") + return json({'success': True}) + + +@app.route('/get') +async def root(request): + all_objects = await objects.execute(KeyValue.select()) + serialized_obj = [] + for obj in all_objects: + serialized_obj.append({obj.key: obj.text}) + + return json({'objects': serialized_obj}) + + +if __name__ == "__main__": + app.run(host='0.0.0.0', port=8000, loop=loop) + From 0e2c092ce3472bf26db7d3b836eb230cfb002656 Mon Sep 17 00:00:00 2001 From: narzeja Date: Sat, 22 Oct 2016 08:40:24 +0200 Subject: [PATCH 040/977] fix method naming conflict --- examples/sanic_peewee.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index 05527198..fed03e02 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -50,13 +50,13 @@ objects.database.allow_sync = False # this will raise AssertionError on ANY sync app = Sanic('peewee_example') @app.route('/post') -async def root(request): +async def post(request): await objects.create(KeyValue, key='my_first_async_db', text="I was inserted asynchronously!") return json({'success': True}) @app.route('/get') -async def root(request): +async def get(request): all_objects = await objects.execute(KeyValue.select()) serialized_obj = [] for obj in all_objects: From ac9770dd8926cb52fd0d38c27bd18a930dc312ea Mon Sep 17 00:00:00 2001 From: narzeja Date: Sat, 22 Oct 2016 08:46:26 +0200 Subject: [PATCH 041/977] a bit more informative return value when posting --- examples/sanic_peewee.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index fed03e02..a364d3ce 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -51,8 +51,8 @@ app = Sanic('peewee_example') @app.route('/post') async def post(request): - await objects.create(KeyValue, key='my_first_async_db', text="I was inserted asynchronously!") - return json({'success': True}) + obj = await objects.create(KeyValue, key='my_first_async_db', text="I was inserted asynchronously!") + return json({'object_id': obj.id}) @app.route('/get') From 96c13fe23c84193da0e2cf8c0a7756cc021578b9 Mon Sep 17 00:00:00 2001 From: narzeja Date: Sat, 22 Oct 2016 08:47:51 +0200 Subject: [PATCH 042/977] post method requires 'GET' --- examples/sanic_peewee.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index a364d3ce..038c5249 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -51,6 +51,8 @@ app = Sanic('peewee_example') @app.route('/post') async def post(request): + """ This is actually a GET request, you probably want POST in real life, + with some data parameters""" obj = await objects.create(KeyValue, key='my_first_async_db', text="I was inserted asynchronously!") return json({'object_id': obj.id}) From c3628407ebe9a5619bc8984c2b8682fc1abe5aa3 Mon Sep 17 00:00:00 2001 From: narzeja Date: Sat, 22 Oct 2016 08:48:19 +0200 Subject: [PATCH 043/977] post method doc --- examples/sanic_peewee.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index 038c5249..61ca6327 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -51,8 +51,8 @@ app = Sanic('peewee_example') @app.route('/post') async def post(request): - """ This is actually a GET request, you probably want POST in real life, - with some data parameters""" + """ This actually requires a GET request, you probably want POST in real + life, with some data parameters""" obj = await objects.create(KeyValue, key='my_first_async_db', text="I was inserted asynchronously!") return json({'object_id': obj.id}) From b048f1bad31ab471b2010d110b5c91eca1f2f0c3 Mon Sep 17 00:00:00 2001 From: narzeja Date: Sat, 22 Oct 2016 08:50:56 +0200 Subject: [PATCH 044/977] better POST example --- examples/sanic_peewee.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index 61ca6327..b1ce4f38 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -49,16 +49,20 @@ objects.database.allow_sync = False # this will raise AssertionError on ANY sync app = Sanic('peewee_example') -@app.route('/post') -async def post(request): - """ This actually requires a GET request, you probably want POST in real - life, with some data parameters""" - obj = await objects.create(KeyValue, key='my_first_async_db', text="I was inserted asynchronously!") +@app.route('/post//') +async def post(request, key, value): + """ + Save get parameters to database + """ + obj = await objects.create(KeyValue, key=key, text=value) return json({'object_id': obj.id}) @app.route('/get') async def get(request): + """ + Load all objects from database + """ all_objects = await objects.execute(KeyValue.select()) serialized_obj = [] for obj in all_objects: From be0739614d8675169ebe192dba7ceb0b7580ef07 Mon Sep 17 00:00:00 2001 From: narzeja Date: Sat, 22 Oct 2016 08:52:37 +0200 Subject: [PATCH 045/977] better get example --- examples/sanic_peewee.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index b1ce4f38..d5479193 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -66,7 +66,11 @@ async def get(request): all_objects = await objects.execute(KeyValue.select()) serialized_obj = [] for obj in all_objects: - serialized_obj.append({obj.key: obj.text}) + serialized_obj.append({ + 'id': obj.id, + 'key': obj.key, + 'value': obj.text} + ) return json({'objects': serialized_obj}) From 3802141007df8b0d3fc6a10f1e1a5dbe50548361 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 23 Oct 2016 01:32:16 -0700 Subject: [PATCH 046/977] Adding cookie capabilities for issue #74 --- README.md | 1 + docs/cookies.md | 50 +++++++++++++++++++++++++++++++++++++++++++ sanic/request.py | 16 +++++++++++++- sanic/response.py | 17 ++++++++++++++- sanic/utils.py | 4 ++-- tests/test_cookies.py | 44 +++++++++++++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 docs/cookies.md create mode 100644 tests/test_cookies.py diff --git a/README.md b/README.md index 3b4ba359..cc62e6bf 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ app.run(host="0.0.0.0", port=8000) * [Middleware](docs/middleware.md) * [Exceptions](docs/exceptions.md) * [Blueprints](docs/blueprints.md) + * [Cookies](docs/cookies.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/cookies.md b/docs/cookies.md new file mode 100644 index 00000000..ead5f157 --- /dev/null +++ b/docs/cookies.md @@ -0,0 +1,50 @@ +# Cookies + +## Request + +Request cookies can be accessed via the request.cookie dictionary + +### Example + +```python +from sanic import Sanic +from sanic.response import text + +@app.route("/cookie") +async def test(request): + test_cookie = request.cookies.get('test') + return text("Test cookie set to: {}".format(test_cookie)) +``` + +## Response + +Response cookies can be set like dictionary values and +have the following parameters available: + +* expires - datetime - Time for cookie to expire on the client's browser +* path - string - The Path attribute specifies the subset of URLs to + which this cookie applies +* comment - string - Cookie comment (metadata) +* domain - string - Specifies the domain for which the + cookie is valid. An explicitly specified domain must always + start with a dot. +* max-age - number - Number of seconds the cookie should live for +* secure - boolean - Specifies whether the cookie will only be sent via + HTTPS +* httponly - boolean - Specifies whether the cookie cannot be read + by javascript + +### Example + +```python +from sanic import Sanic +from sanic.response import text + +@app.route("/cookie") +async def test(request): + response = text("There's a cookie up in this response") + response.cookies['test'] = 'It worked!' + response.cookies['test']['domain'] = '.gotta-go-fast.com' + response.cookies['test']['httponly'] = True + return response +``` \ No newline at end of file diff --git a/sanic/request.py b/sanic/request.py index 31b73ed8..2687d86b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -1,5 +1,6 @@ from cgi import parse_header from collections import namedtuple +from http.cookies import SimpleCookie from httptools import parse_url from urllib.parse import parse_qs from ujson import loads as json_loads @@ -30,7 +31,7 @@ class Request: Properties of an HTTP request such as URL, headers, etc. """ __slots__ = ( - 'url', 'headers', 'version', 'method', + 'url', 'headers', 'version', 'method', '_cookies', 'query_string', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', ) @@ -52,6 +53,7 @@ class Request: self.parsed_form = None self.parsed_files = None self.parsed_args = None + self._cookies = None @property def json(self): @@ -105,6 +107,18 @@ class Request: return self.parsed_args + @property + def cookies(self): + if self._cookies is None: + if 'Cookie' in self.headers: + cookies = SimpleCookie() + cookies.load(self.headers['Cookie']) + self._cookies = {name: cookie.value + for name, cookie in cookies.items()} + else: + self._cookies = {} + return self._cookies + File = namedtuple('File', ['type', 'body', 'name']) diff --git a/sanic/response.py b/sanic/response.py index be471078..20e69eff 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,3 +1,5 @@ +from datetime import datetime +from http.cookies import SimpleCookie import ujson COMMON_STATUS_CODES = { @@ -68,7 +70,7 @@ ALL_STATUS_CODES = { class HTTPResponse: - __slots__ = ('body', 'status', 'content_type', 'headers') + __slots__ = ('body', 'status', 'content_type', 'headers', '_cookies') def __init__(self, body=None, status=200, headers=None, content_type='text/plain', body_bytes=b''): @@ -81,6 +83,7 @@ class HTTPResponse: self.status = status self.headers = headers or {} + self._cookies = None def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way @@ -95,6 +98,12 @@ class HTTPResponse: b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) for name, value in self.headers.items() ) + if self._cookies: + for cookie in self._cookies.values(): + if type(cookie['expires']) is datetime: + cookie['expires'] = \ + cookie['expires'].strftime("%a, %d-%b-%Y %T GMT") + headers += (str(self._cookies) + "\r\n").encode('utf-8') # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all @@ -119,6 +128,12 @@ class HTTPResponse: self.body ) + @property + def cookies(self): + if self._cookies is None: + self._cookies = SimpleCookie() + return self._cookies + def json(body, status=200, headers=None): return HTTPResponse(ujson.dumps(body), headers=headers, status=status, diff --git a/sanic/utils.py b/sanic/utils.py index c39f03ab..4c2680e9 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -5,10 +5,10 @@ HOST = '127.0.0.1' PORT = 42101 -async def local_request(method, uri, *args, **kwargs): +async def local_request(method, uri, cookies=None, *args, **kwargs): url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) log.info(url) - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(cookies=cookies) as session: async with getattr(session, method)(url, *args, **kwargs) as response: response.text = await response.text() return response diff --git a/tests/test_cookies.py b/tests/test_cookies.py new file mode 100644 index 00000000..5b27c2e7 --- /dev/null +++ b/tests/test_cookies.py @@ -0,0 +1,44 @@ +from datetime import datetime, timedelta +from http.cookies import SimpleCookie +from sanic import Sanic +from sanic.response import json, text +from sanic.utils import sanic_endpoint_test + + +# ------------------------------------------------------------ # +# GET +# ------------------------------------------------------------ # + +def test_cookies(): + app = Sanic('test_text') + + @app.route('/') + def handler(request): + response = text('Cookies are: {}'.format(request.cookies['test'])) + response.cookies['right_back'] = 'at you' + return response + + request, response = sanic_endpoint_test(app, cookies={"test": "working!"}) + response_cookies = SimpleCookie() + response_cookies.load(response.headers.get('Set-Cookie', {})) + + assert response.text == 'Cookies are: working!' + assert response_cookies['right_back'].value == 'at you' + +def test_cookie_options(): + app = Sanic('test_text') + + @app.route('/') + def handler(request): + response = text("OK") + response.cookies['test'] = 'at you' + response.cookies['test']['httponly'] = True + response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10) + return response + + request, response = sanic_endpoint_test(app) + response_cookies = SimpleCookie() + response_cookies.load(response.headers.get('Set-Cookie', {})) + + assert response_cookies['test'].value == 'at you' + assert response_cookies['test']['httponly'] == True \ No newline at end of file From 41ea40fc35f477179e355108b582015ba0b0b525 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 23 Oct 2016 01:51:46 -0700 Subject: [PATCH 047/977] increased server event handler type flexibility --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index eddff48c..93e3322a 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -163,7 +163,7 @@ def trigger_events(events, loop): :param loop: event loop """ if events: - if type(events) is not list: + if not isinstance(events, list): events = [events] for event in events: result = event(loop) From 47ec026536fb40818237afaecb543f3a09e4c087 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 23 Oct 2016 03:30:13 -0700 Subject: [PATCH 048/977] Fix incomplete request body being read --- sanic/server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 93e3322a..baacc10b 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -110,7 +110,10 @@ class HttpProtocol(asyncio.Protocol): ) def on_body(self, body): - self.request.body = body + if self.request.body: + self.request.body += body + else: + self.request.body = body def on_message_complete(self): self.loop.create_task( From 201e232a0d8ddf4e767a1002ffc9fcb924ec35b7 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 23 Oct 2016 03:43:01 -0700 Subject: [PATCH 049/977] Releasing 0.1.5 --- CHANGELOG.md | 20 +++++++++++++++----- setup.py | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a972c6b8..3e9a3994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,17 @@ Version 0.1 ----------- - - 0.1.4 - Multiprocessing - - 0.1.3 - Blueprint support - - 0.1.1 - 0.1.2 - Struggling to update pypi via CI - -Released to public. + - 0.1.5 + - Cookies + - Blueprint listeners and ordering + - Faster Router + - Fix: Incomplete file reads on medium+ sized post requests + - Breaking: after_start and before_stop now pass sanic as their first argument + - 0.1.4 + - Multiprocessing + - 0.1.3 + - Blueprint support + - Faster Response processing + - 0.1.1 - 0.1.2 + - Struggling to update pypi via CI + - 0.1.0 + - Released to public \ No newline at end of file diff --git a/setup.py b/setup.py index 78a8e9df..77210534 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup setup( name='Sanic', - version="0.1.4", + version="0.1.5", url='http://github.com/channelcat/sanic/', license='MIT', author='Channel Cat', From 963aef19e03854c36bfb9f1f979e221dfab94e0a Mon Sep 17 00:00:00 2001 From: pcdinh Date: Sun, 23 Oct 2016 19:36:08 +0700 Subject: [PATCH 050/977] w is unused variable to it is safe to suppress Pylint warning using _ (underscore) --- sanic/sanic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index f8189d77..d73c7b1e 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -296,7 +296,7 @@ class Sanic: signal(SIGTERM, lambda s, f: stop_event.set()) processes = [] - for w in range(workers): + for _ in range(workers): process = Process(target=serve, kwargs=server_settings) process.start() processes.append(process) From 5361c6f243144207096c66279a444eac55877b57 Mon Sep 17 00:00:00 2001 From: pcdinh Date: Sun, 23 Oct 2016 19:38:28 +0700 Subject: [PATCH 051/977] e is an unused variable. Safe to remove --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index baacc10b..e4bff6fc 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -216,7 +216,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, try: http_server = loop.run_until_complete(server_coroutine) - except Exception as e: + except Exception: log.exception("Unable to start server") return From 9051e985a027dc53de4521a6eafa959b148de888 Mon Sep 17 00:00:00 2001 From: chhsiao90 Date: Sun, 23 Oct 2016 21:58:57 +0800 Subject: [PATCH 052/977] Add test for method not allow situation --- tests/test_routes.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_routes.py b/tests/test_routes.py index 4759e450..8b0fd9f6 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -164,3 +164,17 @@ def test_route_duplicate(): @app.route('/test//') async def handler2(request, dynamic): pass + + +def test_method_not_allowed(): + app = Sanic('test_method_not_allowed') + + @app.route('/test', methods=['GET']) + async def handler(request): + return text('OK') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, method='post', uri='/test') + assert response.status == 405 From d7fff12b71cb234efb937d08af357095780641d0 Mon Sep 17 00:00:00 2001 From: imbolc Date: Mon, 24 Oct 2016 02:17:03 +0700 Subject: [PATCH 053/977] Static middleware --- sanic/static.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 sanic/static.py diff --git a/sanic/static.py b/sanic/static.py new file mode 100644 index 00000000..f351bc79 --- /dev/null +++ b/sanic/static.py @@ -0,0 +1,48 @@ +import re +import os +from zlib import adler32 +import mimetypes + +from sanic.response import HTTPResponse + + +def setup(app, dirname, url_prefix): + @app.middleware + async def static_middleware(request): + url = request.url + if url.startswith(url_prefix): + filename = url[len(url_prefix):] + if filename: + filename = secure_filename(filename) + filename = os.path.join(dirname, filename) + if os.path.isfile(filename): + return sendfile(filename) + + +_split = re.compile(r'[\0%s]' % re.escape(''.join( + [os.path.sep, os.path.altsep or '']))) + + +def secure_filename(path): + return _split.sub('', path) + + +def sendfile(location, mimetype=None, add_etags=True): + headers = {} + filename = os.path.split(location)[-1] + + with open(location, 'rb') as ins_file: + out_stream = ins_file.read() + + if add_etags: + headers['ETag'] = '{}-{}-{}'.format( + int(os.path.getmtime(location)), + hex(os.path.getsize(location)), + adler32(location.encode('utf-8')) & 0xffffffff) + + mimetype = mimetype or mimetypes.guess_type(filename)[0] or 'text/plain' + + return HTTPResponse(status=200, + headers=headers, + content_type=mimetype, + body_bytes=out_stream) From bf6879e46ffc5c932b40b18f87b8e3e1f3c9ccc1 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 24 Oct 2016 01:21:06 -0700 Subject: [PATCH 054/977] Made static file serving part of Sanic Added sanic.static, blueprint.static, documentation, and testing --- CHANGELOG.md | 2 + README.md | 1 + docs/blueprints.md | 8 +++- docs/static_files.md | 18 ++++++++ sanic/blueprints.py | 15 +++++++ sanic/exceptions.py | 9 ++++ sanic/response.py | 21 +++++++++- sanic/router.py | 3 ++ sanic/sanic.py | 27 +++++++++++- sanic/static.py | 91 ++++++++++++++++++++++------------------ sanic/utils.py | 1 + setup.py | 1 + tests/test_blueprints.py | 34 +++++++++++---- tests/test_static.py | 30 +++++++++++++ 14 files changed, 209 insertions(+), 52 deletions(-) create mode 100644 docs/static_files.md create mode 100644 tests/test_static.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e9a3994..f6123d44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ Version 0.1 ----------- + - 0.1.6 (not released) + - Static files - 0.1.5 - Cookies - Blueprint listeners and ordering diff --git a/README.md b/README.md index cc62e6bf..a02dc703 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ app.run(host="0.0.0.0", port=8000) * [Exceptions](docs/exceptions.md) * [Blueprints](docs/blueprints.md) * [Cookies](docs/cookies.md) + * [Static Files](docs/static_files.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/blueprints.md b/docs/blueprints.md index 1a516356..adc40dfa 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -42,7 +42,7 @@ from sanic import Sanic from my_blueprint import bp app = Sanic(__name__) -app.register_blueprint(bp) +app.blueprint(bp) app.run(host='0.0.0.0', port=8000, debug=True) ``` @@ -79,6 +79,12 @@ Exceptions can also be applied exclusively to blueprints globally. @bp.exception(NotFound) def ignore_404s(request, exception): return text("Yep, I totally found the page: {}".format(request.url)) + +## Static files +Static files can also be served globally, under the blueprint prefix. + +```python +bp.static('/folder/to/serve', '/web/path') ``` ## Start and Stop diff --git a/docs/static_files.md b/docs/static_files.md new file mode 100644 index 00000000..284d747d --- /dev/null +++ b/docs/static_files.md @@ -0,0 +1,18 @@ +# Static Files + +Both directories and files can be served by registering with static + +## Example + +```python +app = Sanic(__name__) + +# Serves files from the static folder to the URL /static +app.static('./static', '/static') + +# Serves the file /home/ubuntu/test.png when the URL /the_best.png +# is requested +app.static('/home/ubuntu/test.png', '/the_best.png') + +app.run(host="0.0.0.0", port=8000) +``` diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 37cfa1c3..d619b574 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -33,6 +33,15 @@ class BlueprintSetup: """ self.app.exception(*args, **kwargs)(handler) + def add_static(self, file_or_directory, uri, *args, **kwargs): + """ + Registers static files to sanic + """ + if self.url_prefix: + uri = self.url_prefix + uri + + self.app.static(file_or_directory, uri, *args, **kwargs) + def add_middleware(self, middleware, *args, **kwargs): """ Registers middleware to sanic @@ -112,3 +121,9 @@ class Blueprint: self.record(lambda s: s.add_exception(handler, *args, **kwargs)) return handler return decorator + + def static(self, file_or_directory, uri, *args, **kwargs): + """ + """ + self.record( + lambda s: s.add_static(file_or_directory, uri, *args, **kwargs)) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 3ed5ab25..e21aca63 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -21,6 +21,15 @@ class ServerError(SanicException): status_code = 500 +class FileNotFound(NotFound): + status_code = 404 + + def __init__(self, message, path, relative_url): + super().__init__(message) + self.path = path + self.relative_url = relative_url + + class Handler: handlers = None diff --git a/sanic/response.py b/sanic/response.py index 20e69eff..d0e64cea 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,6 +1,9 @@ +from aiofiles import open as open_async from datetime import datetime from http.cookies import SimpleCookie -import ujson +from mimetypes import guess_type +from os import path +from ujson import dumps as json_dumps COMMON_STATUS_CODES = { 200: b'OK', @@ -136,7 +139,7 @@ class HTTPResponse: def json(body, status=200, headers=None): - return HTTPResponse(ujson.dumps(body), headers=headers, status=status, + return HTTPResponse(json_dumps(body), headers=headers, status=status, content_type="application/json") @@ -148,3 +151,17 @@ def text(body, status=200, headers=None): def html(body, status=200, headers=None): return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8") + + +async def file(location, mime_type=None, headers=None): + filename = path.split(location)[-1] + + async with open_async(location, mode='rb') as _file: + out_stream = await _file.read() + + mime_type = mime_type or guess_type(filename)[0] or 'text/plain' + + return HTTPResponse(status=200, + headers=headers, + content_type=mime_type, + body_bytes=out_stream) diff --git a/sanic/router.py b/sanic/router.py index 951b49bc..8392dcd8 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -82,6 +82,9 @@ class Router: # Mark the whole route as unhashable if it has the hash key in it if re.search('(^|[^^]){1}/', pattern): properties['unhashable'] = True + # Mark the route as unhashable if it matches the hash key + elif re.search(pattern, '/'): + properties['unhashable'] = True return '({})'.format(pattern) diff --git a/sanic/sanic.py b/sanic/sanic.py index f8189d77..a26b48f1 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -12,6 +12,7 @@ from .log import log, logging from .response import HTTPResponse from .router import Router from .server import serve +from .static import register as static_register from .exceptions import ServerError @@ -28,6 +29,9 @@ class Sanic: self.loop = None self.debug = None + # Register alternative method names + self.go_fast = self.run + # -------------------------------------------------------------------- # # Registration # -------------------------------------------------------------------- # @@ -41,6 +45,11 @@ class Sanic: :return: decorated function """ + # Fix case where the user did not prefix the URL with a / + # and will probably get confused as to why it's not working + if not uri.startswith('/'): + uri = '/' + uri + def response(handler): self.router.add(uri=uri, methods=methods, handler=handler) return handler @@ -84,7 +93,17 @@ class Sanic: attach_to = args[0] return register_middleware - def register_blueprint(self, blueprint, **options): + # Static Files + def static(self, file_or_directory, uri, pattern='.+', + use_modified_since=True): + """ + Registers a root to serve files from. The input can either be a file + or a directory. See + """ + static_register(self, file_or_directory, uri, pattern, + use_modified_since) + + def blueprint(self, blueprint, **options): """ Registers a blueprint on the application. :param blueprint: Blueprint object @@ -101,6 +120,12 @@ class Sanic: self._blueprint_order.append(blueprint) blueprint.register(self, options) + def register_blueprint(self, *args, **kwargs): + # TODO: deprecate 1.0 + log.warning("Use of register_blueprint will be deprecated in " + "version 1.0. Please use the blueprint method instead") + return self.blueprint(*args, **kwargs) + # -------------------------------------------------------------------- # # Request Handling # -------------------------------------------------------------------- # diff --git a/sanic/static.py b/sanic/static.py index f351bc79..7c3a529f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -1,48 +1,59 @@ -import re -import os -from zlib import adler32 -import mimetypes +from aiofiles.os import stat +from os import path +from re import sub +from time import strftime, gmtime -from sanic.response import HTTPResponse +from .exceptions import FileNotFound, InvalidUsage +from .response import file, HTTPResponse -def setup(app, dirname, url_prefix): - @app.middleware - async def static_middleware(request): - url = request.url - if url.startswith(url_prefix): - filename = url[len(url_prefix):] - if filename: - filename = secure_filename(filename) - filename = os.path.join(dirname, filename) - if os.path.isfile(filename): - return sendfile(filename) +def register(app, file_or_directory, uri, pattern, use_modified_since): + # TODO: Though sanic is not a file server, I feel like we should atleast + # make a good effort here. Modified-since is nice, but we could + # also look into etags, expires, and caching + """ + Registers a static directory handler with Sanic by adding a route to the + router and registering a handler. + :param app: Sanic + :param file_or_directory: File or directory path to serve from + :param uri: URL to serve from + :param pattern: regular expression used to match files in the URL + :param use_modified_since: If true, send file modified time, and return + not modified if the browser's matches the server's + """ + # If we're not trying to match a file directly, + # serve from the folder + if not path.isfile(file_or_directory): + uri += '' -_split = re.compile(r'[\0%s]' % re.escape(''.join( - [os.path.sep, os.path.altsep or '']))) + async def _handler(request, file_uri=None): + # Using this to determine if the URL is trying to break out of the path + # served. os.path.realpath seems to be very slow + if file_uri and '../' in file_uri: + raise InvalidUsage("Invalid URL") + # Merge served directory and requested file if provided + # Strip all / that in the beginning of the URL to help prevent python + # from herping a derp and treating the uri as an absolute path + file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ + if file_uri else file_or_directory + try: + headers = {} + # Check if the client has been sent this file before + # and it has not been modified since + if use_modified_since: + stats = await stat(file_path) + modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(stats.st_mtime)) + if request.headers.get('If-Modified-Since') == modified_since: + return HTTPResponse(status=304) + headers['Last-Modified'] = modified_since -def secure_filename(path): - return _split.sub('', path) + return await file(file_path, headers=headers) + except: + raise FileNotFound('File not found', + path=file_or_directory, + relative_url=file_uri) - -def sendfile(location, mimetype=None, add_etags=True): - headers = {} - filename = os.path.split(location)[-1] - - with open(location, 'rb') as ins_file: - out_stream = ins_file.read() - - if add_etags: - headers['ETag'] = '{}-{}-{}'.format( - int(os.path.getmtime(location)), - hex(os.path.getsize(location)), - adler32(location.encode('utf-8')) & 0xffffffff) - - mimetype = mimetype or mimetypes.guess_type(filename)[0] or 'text/plain' - - return HTTPResponse(status=200, - headers=headers, - content_type=mimetype, - body_bytes=out_stream) + app.route(uri, methods=['GET'])(_handler) diff --git a/sanic/utils.py b/sanic/utils.py index 88aa8eae..04a7803a 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -11,6 +11,7 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): async with aiohttp.ClientSession(cookies=cookies) as session: async with getattr(session, method)(url, *args, **kwargs) as response: response.text = await response.text() + response.body = await response.read() return response diff --git a/setup.py b/setup.py index 77210534..2e4e67e7 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ setup( 'uvloop>=0.5.3', 'httptools>=0.0.9', 'ujson>=1.35', + 'aiofiles>=0.3.0', ], classifiers=[ 'Development Status :: 2 - Pre-Alpha', diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 1b88795d..39303ff4 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -1,3 +1,5 @@ +import inspect + from sanic import Sanic from sanic.blueprints import Blueprint from sanic.response import json, text @@ -17,7 +19,7 @@ def test_bp(): def handler(request): return text('Hello') - app.register_blueprint(bp) + app.blueprint(bp) request, response = sanic_endpoint_test(app) assert response.text == 'Hello' @@ -30,7 +32,7 @@ def test_bp_with_url_prefix(): def handler(request): return text('Hello') - app.register_blueprint(bp) + app.blueprint(bp) request, response = sanic_endpoint_test(app, uri='/test1/') assert response.text == 'Hello' @@ -49,8 +51,8 @@ def test_several_bp_with_url_prefix(): def handler2(request): return text('Hello2') - app.register_blueprint(bp) - app.register_blueprint(bp2) + app.blueprint(bp) + app.blueprint(bp2) request, response = sanic_endpoint_test(app, uri='/test1/') assert response.text == 'Hello' @@ -70,7 +72,7 @@ def test_bp_middleware(): async def handler(request): return text('FAIL') - app.register_blueprint(blueprint) + app.blueprint(blueprint) request, response = sanic_endpoint_test(app) @@ -97,7 +99,7 @@ def test_bp_exception_handler(): def handler_exception(request, exception): return text("OK") - app.register_blueprint(blueprint) + app.blueprint(blueprint) request, response = sanic_endpoint_test(app, uri='/1') assert response.status == 400 @@ -140,8 +142,24 @@ def test_bp_listeners(): def handler_6(sanic, loop): order.append(6) - app.register_blueprint(blueprint) + app.blueprint(blueprint) request, response = sanic_endpoint_test(app, uri='/') - assert order == [1,2,3,4,5,6] \ No newline at end of file + assert order == [1,2,3,4,5,6] + +def test_bp_static(): + current_file = inspect.getfile(inspect.currentframe()) + with open(current_file, 'rb') as file: + current_file_contents = file.read() + + app = Sanic('test_static') + blueprint = Blueprint('test_static') + + blueprint.static(current_file, '/testing.file') + + app.blueprint(blueprint) + + request, response = sanic_endpoint_test(app, uri='/testing.file') + assert response.status == 200 + assert response.body == current_file_contents \ No newline at end of file diff --git a/tests/test_static.py b/tests/test_static.py new file mode 100644 index 00000000..314a0927 --- /dev/null +++ b/tests/test_static.py @@ -0,0 +1,30 @@ +import inspect +import os + +from sanic import Sanic +from sanic.utils import sanic_endpoint_test + +def test_static_file(): + current_file = inspect.getfile(inspect.currentframe()) + with open(current_file, 'rb') as file: + current_file_contents = file.read() + + app = Sanic('test_static') + app.static(current_file, '/testing.file') + + request, response = sanic_endpoint_test(app, uri='/testing.file') + assert response.status == 200 + assert response.body == current_file_contents + +def test_static_directory(): + current_file = inspect.getfile(inspect.currentframe()) + current_directory = os.path.dirname(os.path.abspath(current_file)) + with open(current_file, 'rb') as file: + current_file_contents = file.read() + + app = Sanic('test_static') + app.static(current_directory, '/dir') + + request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') + assert response.status == 200 + assert response.body == current_file_contents \ No newline at end of file From 53e00b2b4c4a21b9607b489df257a5d3ca32a26c Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 24 Oct 2016 02:09:07 -0700 Subject: [PATCH 055/977] Added blueprint order test and used deques to add blueprints --- sanic/sanic.py | 7 ++++--- tests/test_middleware.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index d73c7b1e..fc85c71b 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,4 +1,5 @@ from asyncio import get_event_loop +from collections import deque from functools import partial from inspect import isawaitable from multiprocessing import Process, Event @@ -21,8 +22,8 @@ class Sanic: self.router = router or Router() self.error_handler = error_handler or Handler(self) self.config = Config() - self.request_middleware = [] - self.response_middleware = [] + self.request_middleware = deque() + self.response_middleware = deque() self.blueprints = {} self._blueprint_order = [] self.loop = None @@ -74,7 +75,7 @@ class Sanic: if attach_to == 'request': self.request_middleware.append(middleware) if attach_to == 'response': - self.response_middleware.insert(0, middleware) + self.response_middleware.appendleft(middleware) return middleware # Detect which way this was called, @middleware or @middleware('AT') diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 1b338d31..5ff9e9b5 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -86,3 +86,43 @@ def test_middleware_override_response(): assert response.status == 200 assert response.text == 'OK' + + + +def test_middleware_order(): + app = Sanic('test_middleware_order') + + order = [] + + @app.middleware('request') + async def request1(request): + order.append(1) + + @app.middleware('request') + async def request2(request): + order.append(2) + + @app.middleware('request') + async def request3(request): + order.append(3) + + @app.middleware('response') + async def response1(request, response): + order.append(6) + + @app.middleware('response') + async def response2(request, response): + order.append(5) + + @app.middleware('response') + async def response3(request, response): + order.append(4) + + @app.route('/') + async def handler(request): + return text('OK') + + request, response = sanic_endpoint_test(app) + + assert response.status == 200 + assert order == [1,2,3,4,5,6] From c50aa34dd97a1e02da4707c00f02f402b4a8a405 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 25 Oct 2016 01:27:54 -0700 Subject: [PATCH 056/977] Lazy cookie creation --- sanic/cookies.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++ sanic/response.py | 11 +---- 2 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 sanic/cookies.py diff --git a/sanic/cookies.py b/sanic/cookies.py new file mode 100644 index 00000000..a70776db --- /dev/null +++ b/sanic/cookies.py @@ -0,0 +1,123 @@ +from datetime import datetime +import re +import string + +# ------------------------------------------------------------ # +# SimpleCookie +# ------------------------------------------------------------ # + +# Straight up copied this section of dark magic from SimpleCookie + +_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" +_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}' + +_Translator = {n: '\\%03o' % n + for n in set(range(256)) - set(map(ord, _UnescapedChars))} +_Translator.update({ + ord('"'): '\\"', + ord('\\'): '\\\\', +}) + +def _quote(str): + r"""Quote a string for use in a cookie header. + If the string does not need to be double-quoted, then just return the + string. Otherwise, surround the string in doublequotes and quote + (with a \) special characters. + """ + if str is None or _is_legal_key(str): + return str + else: + return '"' + str.translate(_Translator) + '"' + +_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch + +# ------------------------------------------------------------ # +# Custom SimpleCookie +# ------------------------------------------------------------ # + +class CookieJar(dict): + """ + CookieJar dynamically writes headers as cookies are added and removed + It gets around the limitation of one header per name by using the + MultiHeader class to provide a unique key that encodes to Set-Cookie + """ + def __init__(self, headers): + super().__init__() + self.headers = headers + self.cookie_headers = {} + def __setitem__(self, key, value): + # If this cookie doesn't exist, add it to the header keys + cookie_header = self.cookie_headers.get(key) + if not cookie_header: + cookie = Cookie(key, value) + cookie_header = MultiHeader("Set-Cookie") + self.cookie_headers[key] = cookie_header + self.headers[cookie_header] = cookie + return super().__setitem__(key, cookie) + else: + self[key].value = value + + def __delitem__(self, key): + del self.cookie_headers[key] + return super().__delitem__(key) + +class Cookie(dict): + """ + This is a stripped down version of Morsel from SimpleCookie #gottagofast + """ + _keys = { + "expires" : "expires", + "path" : "Path", + "comment" : "Comment", + "domain" : "Domain", + "max-age" : "Max-Age", + "secure" : "Secure", + "httponly" : "HttpOnly", + "version" : "Version", + } + _flags = {'secure', 'httponly'} + + def __init__(self, key, value): + if key in self._keys: + raise KeyError("Cookie name is a reserved word") + if not _is_legal_key(key): + raise KeyError("Cookie key contains illegal characters") + self.key = key + self.value = value + super().__init__() + + def __setitem__(self, key, value): + if not key in self._keys: + raise KeyError("Unknown cookie property") + return super().__setitem__(key, value) + + def encode(self, encoding): + output = ['%s=%s' % (self.key, _quote(self.value))] + for key, value in self.items(): + if key == 'max-age' and isinstance(value, int): + output.append('%s=%d' % (self._keys[key], value)) + elif key == 'expires' and isinstance(value, datetime): + output.append('%s=%s' % ( + self._keys[key], + value.strftime("%a, %d-%b-%Y %T GMT") + )) + elif key in self._flags: + output.append(self._keys[key]) + else: + output.append('%s=%s' % (self._keys[key], value)) + + return "; ".join(output).encode(encoding) + +# ------------------------------------------------------------ # +# Header Trickery +# ------------------------------------------------------------ # + +class MultiHeader: + """ + Allows us to set a header within response that has a unique key, + but may contain duplicate header names + """ + def __init__(self, name): + self.name = name + def encode(self): + return self.name.encode() \ No newline at end of file diff --git a/sanic/response.py b/sanic/response.py index d0e64cea..15130edd 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,6 +1,5 @@ from aiofiles import open as open_async -from datetime import datetime -from http.cookies import SimpleCookie +from .cookies import CookieJar from mimetypes import guess_type from os import path from ujson import dumps as json_dumps @@ -101,12 +100,6 @@ class HTTPResponse: b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) for name, value in self.headers.items() ) - if self._cookies: - for cookie in self._cookies.values(): - if type(cookie['expires']) is datetime: - cookie['expires'] = \ - cookie['expires'].strftime("%a, %d-%b-%Y %T GMT") - headers += (str(self._cookies) + "\r\n").encode('utf-8') # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all @@ -134,7 +127,7 @@ class HTTPResponse: @property def cookies(self): if self._cookies is None: - self._cookies = SimpleCookie() + self._cookies = CookieJar(self.headers) return self._cookies From 9c16f6dbea1e13517ad3c723e8094de603b10f9e Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 25 Oct 2016 01:36:12 -0700 Subject: [PATCH 057/977] Fix flake8 issues --- sanic/cookies.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/sanic/cookies.py b/sanic/cookies.py index a70776db..622a5a08 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -18,6 +18,7 @@ _Translator.update({ ord('\\'): '\\\\', }) + def _quote(str): r"""Quote a string for use in a cookie header. If the string does not need to be double-quoted, then just return the @@ -35,6 +36,7 @@ _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch # Custom SimpleCookie # ------------------------------------------------------------ # + class CookieJar(dict): """ CookieJar dynamically writes headers as cookies are added and removed @@ -45,6 +47,7 @@ class CookieJar(dict): super().__init__() self.headers = headers self.cookie_headers = {} + def __setitem__(self, key, value): # If this cookie doesn't exist, add it to the header keys cookie_header = self.cookie_headers.get(key) @@ -61,19 +64,20 @@ class CookieJar(dict): del self.cookie_headers[key] return super().__delitem__(key) + class Cookie(dict): """ This is a stripped down version of Morsel from SimpleCookie #gottagofast """ _keys = { - "expires" : "expires", - "path" : "Path", - "comment" : "Comment", - "domain" : "Domain", - "max-age" : "Max-Age", - "secure" : "Secure", - "httponly" : "HttpOnly", - "version" : "Version", + "expires": "expires", + "path": "Path", + "comment": "Comment", + "domain": "Domain", + "max-age": "Max-Age", + "secure": "Secure", + "httponly": "HttpOnly", + "version": "Version", } _flags = {'secure', 'httponly'} @@ -87,7 +91,7 @@ class Cookie(dict): super().__init__() def __setitem__(self, key, value): - if not key in self._keys: + if key not in self._keys: raise KeyError("Unknown cookie property") return super().__setitem__(key, value) @@ -112,6 +116,7 @@ class Cookie(dict): # Header Trickery # ------------------------------------------------------------ # + class MultiHeader: """ Allows us to set a header within response that has a unique key, @@ -119,5 +124,6 @@ class MultiHeader: """ def __init__(self, name): self.name = name + def encode(self): - return self.name.encode() \ No newline at end of file + return self.name.encode() From ee70f1e55e57589babe41183f37f0a7478d4da44 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 25 Oct 2016 01:49:43 -0700 Subject: [PATCH 058/977] Upped to version 0.1.6 --- sanic/__init__.py | 2 ++ setup.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index b7be9aaf..618368d4 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,4 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint +__version__ = '0.1.6' + __all__ = ['Sanic', 'Blueprint'] diff --git a/setup.py b/setup.py index 2e4e67e7..60606ad4 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,23 @@ """ Sanic """ +import codecs +import os +import re from setuptools import setup + +with codecs.open(os.path.join(os.path.abspath(os.path.dirname( + __file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp: + try: + version = re.findall(r"^__version__ = '([^']+)'\r?$", + fp.read(), re.M)[0] + except IndexError: + raise RuntimeError('Unable to determine version.') + setup( name='Sanic', - version="0.1.5", + version=version, url='http://github.com/channelcat/sanic/', license='MIT', author='Channel Cat', From 74ae0007d3a7bfea562e8b4a29880184815f9155 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Tue, 25 Oct 2016 02:45:28 -0700 Subject: [PATCH 059/977] Reverse static arguments --- CHANGELOG.md | 5 ++++- docs/static_files.md | 4 ++-- sanic/__init__.py | 2 +- sanic/blueprints.py | 8 ++++---- sanic/sanic.py | 4 ++-- sanic/static.py | 2 +- tests/test_blueprints.py | 2 +- tests/test_static.py | 4 ++-- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6123d44..84e7be78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ Version 0.1 ----------- - - 0.1.6 (not released) + - 0.1.7 + - Reversed static url and directory arguments to meet spec + - 0.1.6 - Static files + - Lazy Cookie Loading - 0.1.5 - Cookies - Blueprint listeners and ordering diff --git a/docs/static_files.md b/docs/static_files.md index 284d747d..fca8d251 100644 --- a/docs/static_files.md +++ b/docs/static_files.md @@ -8,11 +8,11 @@ Both directories and files can be served by registering with static app = Sanic(__name__) # Serves files from the static folder to the URL /static -app.static('./static', '/static') +app.static('/static', './static') # Serves the file /home/ubuntu/test.png when the URL /the_best.png # is requested -app.static('/home/ubuntu/test.png', '/the_best.png') +app.static('/the_best.png', '/home/ubuntu/test.png') app.run(host="0.0.0.0", port=8000) ``` diff --git a/sanic/__init__.py b/sanic/__init__.py index 618368d4..d8a9e56e 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.6' +__version__ = '0.1.7' __all__ = ['Sanic', 'Blueprint'] diff --git a/sanic/blueprints.py b/sanic/blueprints.py index d619b574..c9c54b62 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -33,14 +33,14 @@ class BlueprintSetup: """ self.app.exception(*args, **kwargs)(handler) - def add_static(self, file_or_directory, uri, *args, **kwargs): + def add_static(self, uri, file_or_directory, *args, **kwargs): """ Registers static files to sanic """ if self.url_prefix: uri = self.url_prefix + uri - self.app.static(file_or_directory, uri, *args, **kwargs) + self.app.static(uri, file_or_directory, *args, **kwargs) def add_middleware(self, middleware, *args, **kwargs): """ @@ -122,8 +122,8 @@ class Blueprint: return handler return decorator - def static(self, file_or_directory, uri, *args, **kwargs): + def static(self, uri, file_or_directory, *args, **kwargs): """ """ self.record( - lambda s: s.add_static(file_or_directory, uri, *args, **kwargs)) + lambda s: s.add_static(uri, file_or_directory, *args, **kwargs)) diff --git a/sanic/sanic.py b/sanic/sanic.py index 73f20dfb..edb3a973 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -95,13 +95,13 @@ class Sanic: return register_middleware # Static Files - def static(self, file_or_directory, uri, pattern='.+', + def static(self, uri, file_or_directory, pattern='.+', use_modified_since=True): """ Registers a root to serve files from. The input can either be a file or a directory. See """ - static_register(self, file_or_directory, uri, pattern, + static_register(self, uri, file_or_directory, pattern, use_modified_since) def blueprint(self, blueprint, **options): diff --git a/sanic/static.py b/sanic/static.py index 7c3a529f..72361a9a 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -7,7 +7,7 @@ from .exceptions import FileNotFound, InvalidUsage from .response import file, HTTPResponse -def register(app, file_or_directory, uri, pattern, use_modified_since): +def register(app, uri, file_or_directory, pattern, use_modified_since): # TODO: Though sanic is not a file server, I feel like we should atleast # make a good effort here. Modified-since is nice, but we could # also look into etags, expires, and caching diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 39303ff4..f7b9b8ef 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -156,7 +156,7 @@ def test_bp_static(): app = Sanic('test_static') blueprint = Blueprint('test_static') - blueprint.static(current_file, '/testing.file') + blueprint.static('/testing.file', current_file) app.blueprint(blueprint) diff --git a/tests/test_static.py b/tests/test_static.py index 314a0927..6dafac2b 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -10,7 +10,7 @@ def test_static_file(): current_file_contents = file.read() app = Sanic('test_static') - app.static(current_file, '/testing.file') + app.static('/testing.file', current_file) request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 @@ -23,7 +23,7 @@ def test_static_directory(): current_file_contents = file.read() app = Sanic('test_static') - app.static(current_directory, '/dir') + app.static('/dir', current_directory) request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') assert response.status == 200 From 5171cdd305ffde7ebf1e4d23b886489d038795b8 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Wed, 26 Oct 2016 16:53:34 -0400 Subject: [PATCH 060/977] add example with async http requests --- examples/aiohttp_example.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 examples/aiohttp_example.py diff --git a/examples/aiohttp_example.py b/examples/aiohttp_example.py new file mode 100644 index 00000000..8e7892a7 --- /dev/null +++ b/examples/aiohttp_example.py @@ -0,0 +1,33 @@ +from sanic import Sanic +from sanic.response import json + +import uvloop +import aiohttp + +#Create an event loop manually so that we can use it for both sanic & aiohttp +loop = uvloop.new_event_loop() + +app = Sanic(__name__) + +async def fetch(session, url): + """ + Use session object to perform 'get' request on url + """ + async with session.get(url) as response: + return await response.json() + + +@app.route("/") +async def test(request): + """ + Download and serve example JSON + """ + url = "https://api.github.com/repos/channelcat/sanic" + + async with aiohttp.ClientSession(loop=loop) as session: + response = await fetch(session, url) + return json(response) + + +app.run(host="0.0.0.0", port=8000, loop=loop) + From f3fc958a0c8bea34df8b278804411dd4aa761ef0 Mon Sep 17 00:00:00 2001 From: Clenimar Filemon Date: Thu, 27 Oct 2016 11:09:36 -0300 Subject: [PATCH 061/977] Fix comments over-indentation --- sanic/server.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index e4bff6fc..ca5b2974 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -45,8 +45,7 @@ class HttpProtocol(asyncio.Protocol): self._total_request_size = 0 self._timeout_handler = None - # -------------------------------------------- # - + # -------------------------------------------- # # Connection # -------------------------------------------- # @@ -64,8 +63,7 @@ class HttpProtocol(asyncio.Protocol): def connection_timeout(self): self.bail_out("Request timed out, connection closed") - # -------------------------------------------- # - + # -------------------------------------------- # # Parsing # -------------------------------------------- # From bd28da0abc16bb37384f72d6b45ac8c4c34ce35c Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 28 Oct 2016 02:56:32 -0700 Subject: [PATCH 062/977] Keep-alive requests stay open if communicating --- sanic/server.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index e4bff6fc..94f59b37 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,4 +1,5 @@ import asyncio +from functools import partial from inspect import isawaitable from signal import SIGINT, SIGTERM @@ -16,7 +17,6 @@ from .request import Request class Signal: stopped = False - class HttpProtocol(asyncio.Protocol): __slots__ = ( # event loop, connection @@ -26,7 +26,7 @@ class HttpProtocol(asyncio.Protocol): # request config 'request_handler', 'request_timeout', 'request_max_size', # connection management - '_total_request_size', '_timeout_handler') + '_total_request_size', '_timeout_handler', '_last_communication_time') def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60, @@ -44,6 +44,7 @@ class HttpProtocol(asyncio.Protocol): self.request_max_size = request_max_size self._total_request_size = 0 self._timeout_handler = None + self._last_request_time = None # -------------------------------------------- # @@ -55,6 +56,7 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = self.loop.call_later( self.request_timeout, self.connection_timeout) self.transport = transport + self._last_request_time = current_time def connection_lost(self, exc): del self.connections[self] @@ -62,7 +64,14 @@ class HttpProtocol(asyncio.Protocol): self.cleanup() def connection_timeout(self): - self.bail_out("Request timed out, connection closed") + # Check if + time_elapsed = current_time - self._last_request_time + if time_elapsed < self.request_timeout: + time_left = self.request_timeout - time_elapsed + self._timeout_handler = \ + self.loop.call_later(time_left, self.connection_timeout) + else: + self.bail_out("Request timed out, connection closed") # -------------------------------------------- # @@ -133,13 +142,15 @@ class HttpProtocol(asyncio.Protocol): if not keep_alive: self.transport.close() else: + # Record that we received data + self._last_request_time = current_time self.cleanup() except Exception as e: self.bail_out( "Writing request failed, connection closed {}".format(e)) def bail_out(self, message): - log.error(message) + log.debug(message) self.transport.close() def cleanup(self): @@ -159,6 +170,19 @@ class HttpProtocol(asyncio.Protocol): return True return False +# Keep check on the current time +current_time = None +def update_current_time(loop): + """ + Caches the current time, since it is needed + at the end of every keep-alive request to update the request timeout time + :param loop: + :return: + """ + global current_time + current_time = loop.time() + loop.call_later(0.5, partial(update_current_time, loop)) + def trigger_events(events, loop): """ @@ -173,7 +197,6 @@ def trigger_events(events, loop): if isawaitable(result): loop.run_until_complete(result) - def serve(host, port, request_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, @@ -214,6 +237,10 @@ def serve(host, port, request_handler, before_start=None, after_start=None, request_max_size=request_max_size, ), host, port, reuse_port=reuse_port, sock=sock) + # Instead of pulling time at the end of every request, + # pull it once per minute + loop.call_soon(partial(update_current_time, loop)) + try: http_server = loop.run_until_complete(server_coroutine) except Exception: From c44b5551bcc0b7726fe695dbcd03ece5123a4905 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 28 Oct 2016 03:13:03 -0700 Subject: [PATCH 063/977] time.time faster than loop.time? --- sanic/server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 94f59b37..5440b706 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -2,6 +2,7 @@ import asyncio from functools import partial from inspect import isawaitable from signal import SIGINT, SIGTERM +from time import time import httptools @@ -180,8 +181,8 @@ def update_current_time(loop): :return: """ global current_time - current_time = loop.time() - loop.call_later(0.5, partial(update_current_time, loop)) + current_time = time() + loop.call_later(1, partial(update_current_time, loop)) def trigger_events(events, loop): From 707c55fbe71a5e1fc8aad7b1ff41cd0733a8f909 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 28 Oct 2016 03:35:30 -0700 Subject: [PATCH 064/977] Fix flake8 --- sanic/server.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 5440b706..9c2ea749 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -18,6 +18,10 @@ from .request import Request class Signal: stopped = False + +current_time = None + + class HttpProtocol(asyncio.Protocol): __slots__ = ( # event loop, connection @@ -171,8 +175,7 @@ class HttpProtocol(asyncio.Protocol): return True return False -# Keep check on the current time -current_time = None + def update_current_time(loop): """ Caches the current time, since it is needed @@ -198,6 +201,7 @@ def trigger_events(events, loop): if isawaitable(result): loop.run_until_complete(result) + def serve(host, port, request_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, From 96fcd8443f7994f73d6e82525d14009995a18f6a Mon Sep 17 00:00:00 2001 From: Ryan Kung Date: Tue, 1 Nov 2016 14:35:06 +0800 Subject: [PATCH 065/977] Update README.md via flake8 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a02dc703..65c3eda7 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ app = Sanic(__name__) @app.route("/") async def test(request): - return json({ "hello": "world" }) + return json({"hello": "world"}) app.run(host="0.0.0.0", port=8000) ``` From 80fcacaf8b8458a7243c989a39442bc25ae7b940 Mon Sep 17 00:00:00 2001 From: Marcin Baran Date: Wed, 2 Nov 2016 12:27:58 +0100 Subject: [PATCH 066/977] Add loop kwargs to sanic_endpoint_test --- sanic/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/utils.py b/sanic/utils.py index 04a7803a..0749464b 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,7 +16,7 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, - *request_args, **request_kwargs): + loop=None, *request_args, **request_kwargs): results = [] exceptions = [] @@ -34,7 +34,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, port=42101, after_start=_collect_response) + app.run(host=HOST, port=42101, after_start=_collect_response, loop=loop) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) From 3cd3b2d9b7c463b8490d9409674fc4fb63b347a9 Mon Sep 17 00:00:00 2001 From: imbolc Date: Thu, 3 Nov 2016 12:34:55 +0700 Subject: [PATCH 067/977] Fix upload without content-type --- sanic/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index 2687d86b..c2ab7260 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -71,7 +71,7 @@ class Request: self.parsed_form = {} self.parsed_files = {} content_type, parameters = parse_header( - self.headers.get('Content-Type')) + self.headers.get('Content-Type', '')) try: is_url_encoded = ( content_type == 'application/x-www-form-urlencoded') From df2f91b82f9a2b666e0da0c9aa179c4de01b4a72 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Thu, 3 Nov 2016 09:35:14 -0500 Subject: [PATCH 068/977] Add aiofiles to requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e0feec8..cef8660e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ httptools ujson -uvloop \ No newline at end of file +uvloop +aiofiles From 3a2eeb97095330a95be790c048d982a25d6ec70a Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 5 Nov 2016 13:12:55 -0500 Subject: [PATCH 069/977] Fix value error for query string test --- tests/test_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index 290c9b99..756113b2 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -56,7 +56,7 @@ def test_query_string(): async def handler(request): return text('OK') - request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")]) + request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")]) assert request.args.get('test1') == '1' assert request.args.get('test2') == 'false' From ce8742c60562eb1f718704b317101542505471ca Mon Sep 17 00:00:00 2001 From: Manuel Miranda Date: Sun, 6 Nov 2016 16:26:15 +0100 Subject: [PATCH 070/977] Caching example using aiocache (#140) * Keep-alive requests stay open if communicating * time.time faster than loop.time? * Fix flake8 * Add aiofiles to requirements.txt * Caching example using aiocache * Caching example using aiocache * Added aiocache to requirements --- examples/cache_example.py | 38 ++++++++++++++++++++++++++++++++++++++ requirements-dev.txt | 3 ++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 examples/cache_example.py diff --git a/examples/cache_example.py b/examples/cache_example.py new file mode 100644 index 00000000..44cc7140 --- /dev/null +++ b/examples/cache_example.py @@ -0,0 +1,38 @@ +""" +Example of caching using aiocache package. To run it you will need a Redis +instance running in localhost:6379. + +Running this example you will see that the first call lasts 3 seconds and +the rest are instant because the value is retrieved from the Redis. + +If you want more info about the package check +https://github.com/argaen/aiocache +""" + +import asyncio +import aiocache + +from sanic import Sanic +from sanic.response import json +from sanic.log import log +from aiocache import RedisCache, cached +from aiocache.serializers import JsonSerializer + +app = Sanic(__name__) +aiocache.set_defaults(cache=RedisCache) + + +@cached(key="my_custom_key", serializer=JsonSerializer()) +async def expensive_call(): + log.info("Expensive has been called") + await asyncio.sleep(3) + return {"test": True} + + +@app.route("/") +async def test(request): + log.info("Received GET /") + return json(await expensive_call()) + + +app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop()) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9593b0cf..00feb17d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,6 +2,7 @@ httptools ujson uvloop aiohttp +aiocache pytest coverage tox @@ -9,4 +10,4 @@ gunicorn bottle kyoukai falcon -tornado \ No newline at end of file +tornado From 1b65b2e0c658dc0e87a5a93f3e3665782b01edef Mon Sep 17 00:00:00 2001 From: Pahaz Blinov Date: Sun, 6 Nov 2016 21:08:55 +0500 Subject: [PATCH 071/977] fix(blueprints): @middleware IndexError (#139) --- sanic/blueprints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index c9c54b62..bfef8557 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -109,8 +109,9 @@ class Blueprint: # Detect which way this was called, @middleware or @middleware('AT') if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + middleware = args[0] args = [] - return register_middleware(args[0]) + return register_middleware(middleware) else: return register_middleware From 5efe51b66129d5bc902f0a70f0290b94fbd17c84 Mon Sep 17 00:00:00 2001 From: Pahaz Blinov Date: Tue, 8 Nov 2016 02:27:50 +0500 Subject: [PATCH 072/977] fix(request.py): problem in case of request without content-type header (#142) * fix(request.py): exception if access request.form on GET request * fix(request): just make a unification (parsed_form and parsed_files) + RFC fixes parsed_form and parsed_files must be a RequestParameters type in all cases! --- sanic/request.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index c2ab7260..109b1483 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -8,6 +8,12 @@ from ujson import loads as json_loads from .log import log +DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" +# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 +# > If the media type remains unknown, the recipient SHOULD treat it +# > as type "application/octet-stream" + + class RequestParameters(dict): """ Hosts a dict with lists as values where get returns the first @@ -68,14 +74,13 @@ class Request: @property def form(self): if self.parsed_form is None: - self.parsed_form = {} - self.parsed_files = {} - content_type, parameters = parse_header( - self.headers.get('Content-Type', '')) + self.parsed_form = RequestParameters() + self.parsed_files = RequestParameters() + content_type = self.headers.get( + 'Content-Type', DEFAULT_HTTP_CONTENT_TYPE) + content_type, parameters = parse_header(content_type) try: - is_url_encoded = ( - content_type == 'application/x-www-form-urlencoded') - if content_type is None or is_url_encoded: + if content_type == 'application/x-www-form-urlencoded': self.parsed_form = RequestParameters( parse_qs(self.body.decode('utf-8'))) elif content_type == 'multipart/form-data': @@ -86,7 +91,6 @@ class Request: except Exception as e: log.exception(e) pass - return self.parsed_form @property From 0e9819fba168ee2aa6b32827db751bb4c0a70dab Mon Sep 17 00:00:00 2001 From: Pahaz Blinov Date: Wed, 9 Nov 2016 00:36:37 +0500 Subject: [PATCH 073/977] fix(request): parse_multipart_form should return RequestParameters I have this code: ``` form = FileForm(request.files) ``` and it raise error because the `request.files` is `dict` but `RequestParameters` is expected =/ --- sanic/request.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 109b1483..a843b73b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -134,8 +134,8 @@ def parse_multipart_form(body, boundary): :param boundary: Bytes multipart boundary :return: fields (dict), files (dict) """ - files = {} - fields = {} + files = RequestParameters() + fields = RequestParameters() form_parts = body.split(boundary) for form_part in form_parts[1:-1]: @@ -166,9 +166,16 @@ def parse_multipart_form(body, boundary): post_data = form_part[line_index:-4] if file_name or file_type: - files[field_name] = File( - type=file_type, name=file_name, body=post_data) + file = File(type=file_type, name=file_name, body=post_data) + if field_name in files: + files[field_name].append(file) + else: + files[field_name] = [file] else: - fields[field_name] = post_data.decode('utf-8') + value = post_data.decode('utf-8') + if field_name in fields: + fields[field_name].append(value) + else: + fields[field_name] = [value] return fields, files From 0d9fb2f9279e3303f51a0b18204a88d6cfbb1a0e Mon Sep 17 00:00:00 2001 From: Pahaz Blinov Date: Wed, 9 Nov 2016 18:04:15 +0500 Subject: [PATCH 074/977] docs(request): return value docstring --- sanic/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index a843b73b..7373a104 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -132,7 +132,7 @@ def parse_multipart_form(body, boundary): Parses a request body and returns fields and files :param body: Bytes request body :param boundary: Bytes multipart boundary - :return: fields (dict), files (dict) + :return: fields (RequestParameters), files (RequestParameters) """ files = RequestParameters() fields = RequestParameters() From be5588d5d80958778166e3cb321c875f260b0a17 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Thu, 10 Nov 2016 12:53:00 +0100 Subject: [PATCH 075/977] Add the client address to the request header --- sanic/server.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sanic/server.py b/sanic/server.py index 70dca448..729e1044 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -114,6 +114,11 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((name.decode(), value.decode('utf-8'))) def on_headers_complete(self): + + ra = self.transport.get_extra_info('peername') + if ra: + self.headers.append(('Remote-Addr','%s:%s' % ra)) + self.request = Request( url_bytes=self.url, headers=dict(self.headers), From b92e46df4018aa9288e1918eca2f65e2820b61c7 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Thu, 10 Nov 2016 13:06:27 +0100 Subject: [PATCH 076/977] fix whitespace --- sanic/server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 729e1044..ec4e5780 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -114,11 +114,10 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((name.decode(), value.decode('utf-8'))) def on_headers_complete(self): - ra = self.transport.get_extra_info('peername') if ra: self.headers.append(('Remote-Addr','%s:%s' % ra)) - + self.request = Request( url_bytes=self.url, headers=dict(self.headers), From 8ebc92c236652f357394e618640505a11ba49283 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Thu, 10 Nov 2016 13:09:37 +0100 Subject: [PATCH 077/977] pass flake8 tests --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index ec4e5780..e802b5df 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -116,7 +116,7 @@ class HttpProtocol(asyncio.Protocol): def on_headers_complete(self): ra = self.transport.get_extra_info('peername') if ra: - self.headers.append(('Remote-Addr','%s:%s' % ra)) + self.headers.append(('Remote-Addr', '%s:%s' % ra)) self.request = Request( url_bytes=self.url, From 28ce2447ef9c4bd75ad6e21bc9f3a660714a3b02 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Thu, 10 Nov 2016 15:28:16 -0600 Subject: [PATCH 078/977] Update variable name Give `ra` a more explicit name --- sanic/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index e802b5df..0fd85440 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -114,9 +114,9 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((name.decode(), value.decode('utf-8'))) def on_headers_complete(self): - ra = self.transport.get_extra_info('peername') - if ra: - self.headers.append(('Remote-Addr', '%s:%s' % ra)) + remote_addr = self.transport.get_extra_info('peername') + if remote_addr: + self.headers.append(('Remote-Addr', '%s:%s' % remote_addr)) self.request = Request( url_bytes=self.url, From 695f8733bbb9c35f82ec61f4c977e4828128cc19 Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Fri, 11 Nov 2016 04:11:07 +0000 Subject: [PATCH 079/977] Add Gitter badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 65c3eda7..cc309c8c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Sanic +[![Join the chat at https://gitter.im/sanic-python/Lobby](https://badges.gitter.im/sanic-python/Lobby.svg)](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + [![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic) [![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/) [![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/) From 0822674f70b05d42b87dfd82769cbc8222756468 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 11 Nov 2016 22:36:49 +0200 Subject: [PATCH 080/977] aiohttp is slightly faster actually Disabling access log increases RPS a lot --- tests/performance/aiohttp/simple_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/performance/aiohttp/simple_server.py b/tests/performance/aiohttp/simple_server.py index 8cb97b33..7c61f723 100644 --- a/tests/performance/aiohttp/simple_server.py +++ b/tests/performance/aiohttp/simple_server.py @@ -15,4 +15,4 @@ async def handle(request): app = web.Application(loop=loop) app.router.add_route('GET', '/', handle) -web.run_app(app, port=sys.argv[1]) +web.run_app(app, port=sys.argv[1], access_log=None) From edb25f799d2e85f3d715582fd2005cc80c8a6dd5 Mon Sep 17 00:00:00 2001 From: Manuel Miranda Date: Mon, 14 Nov 2016 00:11:31 +0100 Subject: [PATCH 081/977] Caching example (#150) * Caching example using aiocache * Caching example using aiocache * Added aiocache to requirements * Fixed example with newest aiocache --- examples/cache_example.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/cache_example.py b/examples/cache_example.py index 44cc7140..60823366 100644 --- a/examples/cache_example.py +++ b/examples/cache_example.py @@ -15,11 +15,14 @@ import aiocache from sanic import Sanic from sanic.response import json from sanic.log import log -from aiocache import RedisCache, cached +from aiocache import cached from aiocache.serializers import JsonSerializer app = Sanic(__name__) -aiocache.set_defaults(cache=RedisCache) + +aiocache.settings.set_defaults( + cache="aiocache.RedisCache" +) @cached(key="my_custom_key", serializer=JsonSerializer()) From 9e0747db15282f6f8f8474f32e6b9ff0f1bf9174 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Tue, 15 Nov 2016 19:37:40 -0500 Subject: [PATCH 082/977] Example for using error_handler --- examples/exception_monitoring.py | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/exception_monitoring.py diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py new file mode 100644 index 00000000..51f8bfba --- /dev/null +++ b/examples/exception_monitoring.py @@ -0,0 +1,59 @@ +""" +Example intercepting uncaught exceptions using Sanic's error handler framework. + +This may be useful for developers wishing to use Sentry, Airbrake, etc. +or a custom system to log and monitor unexpected errors in production. + +First we create our own class inheriting from Handler in sanic.exceptions, +and pass in an instance of it when we create our Sanic instance. Inside this +class' default handler, we can do anything including sending exceptions to +an external service. +""" + + + +""" +Imports and code relevant for our CustomHandler class +(Ordinarily this would be in a separate file) +""" +from sanic.response import text +from sanic.exceptions import Handler, SanicException + +class CustomHandler(Handler): + def default(self, request, exception): + # Here, we have access to the exception object + # and can do anything with it (log, send to external service, etc) + + # Some exceptions are trivial and built into Sanic (404s, etc) + if not issubclass(type(exception), SanicException): + print(exception) + + # Then, we must finish handling the exception by + # returning our response to the client + return text("An error occured", status=500) + + + + +""" +This is an ordinary Sanic server, with the exception that we set the +server's error_handler to an instance of our CustomHandler +""" + +from sanic import Sanic +from sanic.response import json + +app = Sanic(__name__) + +handler = CustomHandler(sanic=app) +app.error_handler = handler + +@app.route("/") +async def test(request): + # Here, something occurs which causes an unexpected exception + # This exception will flow to our custom handler. + x = 1 / 0 + return json({"test": True}) + + +app.run(host="0.0.0.0", port=8000) From d9f6846c7641e6ae9c4ea4c688a4d24f56e83a8b Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Wed, 16 Nov 2016 07:55:54 -0500 Subject: [PATCH 083/977] improved default handling --- examples/exception_monitoring.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py index 51f8bfba..34b46a14 100644 --- a/examples/exception_monitoring.py +++ b/examples/exception_monitoring.py @@ -28,9 +28,10 @@ class CustomHandler(Handler): if not issubclass(type(exception), SanicException): print(exception) - # Then, we must finish handling the exception by - # returning our response to the client - return text("An error occured", status=500) + # Then, we must finish handling the exception by returning + # our response to the client + # For this we can just call the super class' default handler + return super.default(self, request, exception) @@ -56,4 +57,4 @@ async def test(request): return json({"test": True}) -app.run(host="0.0.0.0", port=8000) +app.run(host="0.0.0.0", port=8000, debug=True) From edb12da154f30b208571f43aac018bd56e587b19 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 16 Nov 2016 12:55:13 -0600 Subject: [PATCH 084/977] Fix the flake8 error caused by new flake8 version --- sanic/cookies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/cookies.py b/sanic/cookies.py index 622a5a08..b7669e76 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -30,6 +30,7 @@ def _quote(str): else: return '"' + str.translate(_Translator) + '"' + _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch # ------------------------------------------------------------ # From f16ea20de5fa844c53639f91cf521d2993806fb8 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Fri, 18 Nov 2016 17:06:16 -0800 Subject: [PATCH 085/977] provide default app name --- sanic/sanic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index edb3a973..cbc12278 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,7 +1,7 @@ from asyncio import get_event_loop from collections import deque from functools import partial -from inspect import isawaitable +from inspect import isawaitable, stack, getmodulename from multiprocessing import Process, Event from signal import signal, SIGTERM, SIGINT from time import sleep @@ -18,7 +18,10 @@ from .exceptions import ServerError class Sanic: - def __init__(self, name, router=None, error_handler=None): + def __init__(self, name=None, router=None, error_handler=None): + if name is None: + frame_records = stack()[1] + name = getmodulename(frame_records[1]) self.name = name self.router = router or Router() self.error_handler = error_handler or Handler(self) From 8be4dc8fb52f27a4a51cc5d31a1aa6a767450450 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Fri, 18 Nov 2016 17:22:16 -0800 Subject: [PATCH 086/977] update readme example to use default --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc309c8c..4b6d87de 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E from sanic import Sanic from sanic.response import json -app = Sanic(__name__) +app = Sanic() @app.route("/") async def test(request): From 9eb4cecbc1674321b3e37a7ae08afb996d5bd890 Mon Sep 17 00:00:00 2001 From: jiajunhuang Date: Sat, 19 Nov 2016 15:16:20 +0800 Subject: [PATCH 087/977] fix the way using logging.exception --- sanic/request.py | 8 ++++---- sanic/sanic.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 7373a104..82fc64db 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -67,7 +67,7 @@ class Request: try: self.parsed_json = json_loads(self.body) except Exception: - pass + log.exception("failed when parsing body as json") return self.parsed_json @@ -88,9 +88,9 @@ class Request: boundary = parameters['boundary'].encode('utf-8') self.parsed_form, self.parsed_files = ( parse_multipart_form(self.body, boundary)) - except Exception as e: - log.exception(e) - pass + except Exception: + log.exception("failed when parsing form") + return self.parsed_form @property diff --git a/sanic/sanic.py b/sanic/sanic.py index cbc12278..af284c00 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -295,8 +295,7 @@ class Sanic: except Exception as e: log.exception( - 'Experienced exception while trying to serve: {}'.format(e)) - pass + 'Experienced exception while trying to serve') log.info("Server Stopped") From 635921adc71c8d27bc315f3b18ad927b6e14a5a5 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 19 Nov 2016 16:03:09 -0800 Subject: [PATCH 088/977] Update headers to use CIMultiDict instead of dict --- sanic/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 0fd85440..0afeca23 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -3,7 +3,7 @@ from functools import partial from inspect import isawaitable from signal import SIGINT, SIGTERM from time import time - +from aiohttp import CIMultiDict import httptools try: @@ -120,7 +120,7 @@ class HttpProtocol(asyncio.Protocol): self.request = Request( url_bytes=self.url, - headers=dict(self.headers), + headers=CIMultiDict(self.headers), version=self.parser.get_http_version(), method=self.parser.get_method().decode() ) From a97e554f8f4bd2443d6a9d68c60351a8f86c10bc Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 17:48:28 -0800 Subject: [PATCH 089/977] Added shared request data --- sanic/request.py | 2 +- tests/test_request_data.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/test_request_data.py diff --git a/sanic/request.py b/sanic/request.py index c2ab7260..ff6e8b51 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -26,7 +26,7 @@ class RequestParameters(dict): return self.super.get(name, default) -class Request: +class Request(dict): """ Properties of an HTTP request such as URL, headers, etc. """ diff --git a/tests/test_request_data.py b/tests/test_request_data.py new file mode 100644 index 00000000..161a7836 --- /dev/null +++ b/tests/test_request_data.py @@ -0,0 +1,24 @@ +from sanic import Sanic +from sanic.response import json +from sanic.utils import sanic_endpoint_test +from ujson import loads + + +def test_storage(): + app = Sanic('test_text') + + @app.middleware('request') + def store(request): + request['a'] = 'test' + request['b'] = 'zest' + del request['b'] + + @app.route('/') + def handler(request): + return json({ 'a': request.get('a'), 'b': request.get('b') }) + + request, response = sanic_endpoint_test(app) + + response_json = loads(response.text) + assert response_json['a'] == 'test' + assert response_json.get('b') is None \ No newline at end of file From 3ce6434532fcf7994a50f0d780320cb3b1e6e586 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 18:04:35 -0800 Subject: [PATCH 090/977] Fix flake8 --- sanic/cookies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/cookies.py b/sanic/cookies.py index 622a5a08..b7669e76 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -30,6 +30,7 @@ def _quote(str): else: return '"' + str.translate(_Translator) + '"' + _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch # ------------------------------------------------------------ # From 01681599ff63384e8f87680df641c178b3c2484f Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 18:13:02 -0800 Subject: [PATCH 091/977] Fixed new test error with aiohttp --- tests/test_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index 290c9b99..0098797d 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -56,7 +56,7 @@ def test_query_string(): async def handler(request): return text('OK') - request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")]) + request, response = sanic_endpoint_test(app, params={"test1":1, "test2":"false"}) assert request.args.get('test1') == '1' assert request.args.get('test2') == 'false' From 922c96e3c1ca28572b631b63145f717774f5efd2 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 18:26:03 -0800 Subject: [PATCH 092/977] Updated test terminology --- tests/test_request_data.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_request_data.py b/tests/test_request_data.py index 161a7836..fca67ba9 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -9,16 +9,16 @@ def test_storage(): @app.middleware('request') def store(request): - request['a'] = 'test' - request['b'] = 'zest' - del request['b'] + request['user'] = 'sanic' + request['sidekick'] = 'tails' + del request['sidekick'] @app.route('/') def handler(request): - return json({ 'a': request.get('a'), 'b': request.get('b') }) + return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') }) request, response = sanic_endpoint_test(app) response_json = loads(response.text) - assert response_json['a'] == 'test' - assert response_json.get('b') is None \ No newline at end of file + assert response_json['user'] == 'sanic' + assert response_json.get('sidekick') is None \ No newline at end of file From d02fffb6b84d4d9e7e728c38c6115ee3b417d662 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 18:41:40 -0800 Subject: [PATCH 093/977] Fixing import of CIMultiDict --- requirements.txt | 1 + sanic/server.py | 2 +- setup.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cef8660e..3acfbb1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ httptools ujson uvloop aiofiles +multidict diff --git a/sanic/server.py b/sanic/server.py index 0afeca23..9081b729 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,9 +1,9 @@ import asyncio from functools import partial from inspect import isawaitable +from multidict import CIMultiDict from signal import SIGINT, SIGTERM from time import time -from aiohttp import CIMultiDict import httptools try: diff --git a/setup.py b/setup.py index 60606ad4..e6e9b4cc 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ setup( 'httptools>=0.0.9', 'ujson>=1.35', 'aiofiles>=0.3.0', + 'multidict>=2.0', ], classifiers=[ 'Development Status :: 2 - Pre-Alpha', From f7f578ed4494298170b885c9631274432dcc5ed5 Mon Sep 17 00:00:00 2001 From: abhishek7 Date: Sun, 20 Nov 2016 21:37:01 -0500 Subject: [PATCH 094/977] Fixed Exception error log on line 157 of server.py --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 9081b729..b6233031 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -154,7 +154,7 @@ class HttpProtocol(asyncio.Protocol): self.cleanup() except Exception as e: self.bail_out( - "Writing request failed, connection closed {}".format(e)) + "Writing response failed, connection closed {}".format(e)) def bail_out(self, message): log.debug(message) From aa0f15fbb2de8bd644eaff869e6ee94880d19802 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 23 Nov 2016 11:03:00 -0600 Subject: [PATCH 095/977] Adding a new line --- tests/test_request_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_request_data.py b/tests/test_request_data.py index fca67ba9..098878e7 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -21,4 +21,4 @@ def test_storage(): response_json = loads(response.text) assert response_json['user'] == 'sanic' - assert response_json.get('sidekick') is None \ No newline at end of file + assert response_json.get('sidekick') is None From 9f2d73e2f152456df27941efc3d5fe2695e0af6b Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:10:25 +0200 Subject: [PATCH 096/977] class based views implementation for sanic --- docs/class_based_views.md | 44 +++++++++ docs/routing.md | 12 +++ requirements-dev.txt | 1 + sanic/blueprints.py | 6 ++ sanic/sanic.py | 11 +++ sanic/utils.py | 4 +- sanic/views.py | 33 +++++++ tests/test_routes.py | 184 +++++++++++++++++++++++++++++++++++++- tests/test_views.py | 155 ++++++++++++++++++++++++++++++++ 9 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 docs/class_based_views.md create mode 100644 sanic/views.py create mode 100644 tests/test_views.py diff --git a/docs/class_based_views.md b/docs/class_based_views.md new file mode 100644 index 00000000..b5f8ee02 --- /dev/null +++ b/docs/class_based_views.md @@ -0,0 +1,44 @@ +# Class based views + +Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone try to use not implemented method, there will be 405 response. + +## Examples +```python +from sanic import Sanic +from sanic.views import MethodView + +app = Sanic('some_name') + + +class SimpleView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def post(self, request, *args, **kwargs): + return text('I am post method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + + def patch(self, request, *args, **kwargs): + return text('I am patch method') + + def delete(self, request, *args, **kwargs): + return text('I am delete method') + +app.add_route(SimpleView(), '/') + +``` + +If you need any url params just mention them in method definition: + +```python +class NameView(MethodView): + + def get(self, request, name, *args, **kwargs): + return text('Hello {}'.format(name)) + +app.add_route(NameView(), '/') + +async def person_handler(request, name): + return text('Person - {}'.format(name)) +app.add_route(handler, '/person/') + ``` diff --git a/requirements-dev.txt b/requirements-dev.txt index 00feb17d..1c34d695 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ bottle kyoukai falcon tornado +aiofiles diff --git a/sanic/blueprints.py b/sanic/blueprints.py index bfef8557..92e376f1 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -91,6 +91,12 @@ class Blueprint: return handler return decorator + def add_route(self, handler, uri, methods=None): + """ + """ + self.record(lambda s: s.add_route(handler, uri, methods)) + return handler + def listener(self, event): """ """ diff --git a/sanic/sanic.py b/sanic/sanic.py index af284c00..7a9c35ad 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -60,6 +60,17 @@ class Sanic: return response + def add_route(self, handler, uri, methods=None): + """ + A helper method to register class instance or functions as a handler to the application url routes. + :param handler: function or class instance + :param uri: path of the URL + :param methods: list or tuple of methods allowed + :return: function or class instance + """ + self.route(uri=uri, methods=methods)(handler) + return handler + # Decorator def exception(self, *exceptions): """ diff --git a/sanic/utils.py b/sanic/utils.py index 0749464b..8190c1d0 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,7 +16,7 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, - loop=None, *request_args, **request_kwargs): + loop=None, debug=False, *request_args, **request_kwargs): results = [] exceptions = [] @@ -34,7 +34,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, port=42101, after_start=_collect_response, loop=loop) + app.run(host=HOST, debug=debug, port=42101, after_start=_collect_response, loop=loop) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/sanic/views.py b/sanic/views.py new file mode 100644 index 00000000..9cb04247 --- /dev/null +++ b/sanic/views.py @@ -0,0 +1,33 @@ +from .exceptions import InvalidUsage + + +class MethodView: + """ Simple class based implementation of view for the sanic. + You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. + For example: + class DummyView(View): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + etc. + If someone try use not implemented method, there will be 405 response + + If you need any url params just mention them in method definition like: + class DummyView(View): + + def get(self, request, my_param_here, *args, **kwargs): + return text('I am get method with %s' % my_param_here) + + To add the view into the routing you could use + 1) app.add_route(DummyView(), '/') + 2) app.route('/')(DummyView()) + """ + + def __call__(self, request, *args, **kwargs): + handler = getattr(self, request.method.lower(), None) + if handler: + return handler(request, *args, **kwargs) + raise InvalidUsage('Method {} not allowed for URL {}'.format(request.method, request.url), status_code=405) diff --git a/tests/test_routes.py b/tests/test_routes.py index 8b0fd9f6..38591e53 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -84,7 +84,7 @@ def test_dynamic_route_int(): def test_dynamic_route_number(): - app = Sanic('test_dynamic_route_int') + app = Sanic('test_dynamic_route_number') results = [] @@ -105,7 +105,7 @@ def test_dynamic_route_number(): def test_dynamic_route_regex(): - app = Sanic('test_dynamic_route_int') + app = Sanic('test_dynamic_route_regex') @app.route('/folder/') async def handler(request, folder_id): @@ -145,7 +145,7 @@ def test_dynamic_route_unhashable(): def test_route_duplicate(): - app = Sanic('test_dynamic_route') + app = Sanic('test_route_duplicate') with pytest.raises(RouteExists): @app.route('/test') @@ -178,3 +178,181 @@ def test_method_not_allowed(): request, response = sanic_endpoint_test(app, method='post', uri='/test') assert response.status == 405 + + +def test_static_add_route(): + app = Sanic('test_static_add_route') + + async def handler1(request): + return text('OK1') + + async def handler2(request): + return text('OK2') + + app.add_route(handler1, '/test') + app.add_route(handler2, '/test2') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.text == 'OK1' + + request, response = sanic_endpoint_test(app, uri='/test2') + assert response.text == 'OK2' + + +def test_dynamic_add_route(): + app = Sanic('test_dynamic_add_route') + + results = [] + + async def handler(request, name): + results.append(name) + return text('OK') + + app.add_route(handler, '/folder/') + request, response = sanic_endpoint_test(app, uri='/folder/test123') + + assert response.text == 'OK' + assert results[0] == 'test123' + + +def test_dynamic_add_route_string(): + app = Sanic('test_dynamic_add_route_string') + + results = [] + + async def handler(request, name): + results.append(name) + return text('OK') + + app.add_route(handler, '/folder/') + request, response = sanic_endpoint_test(app, uri='/folder/test123') + + assert response.text == 'OK' + assert results[0] == 'test123' + + request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico') + + assert response.text == 'OK' + assert results[1] == 'favicon.ico' + + +def test_dynamic_add_route_int(): + app = Sanic('test_dynamic_add_route_int') + + results = [] + + async def handler(request, folder_id): + results.append(folder_id) + return text('OK') + + app.add_route(handler, '/folder/') + + request, response = sanic_endpoint_test(app, uri='/folder/12345') + assert response.text == 'OK' + assert type(results[0]) is int + + request, response = sanic_endpoint_test(app, uri='/folder/asdf') + assert response.status == 404 + + +def test_dynamic_add_route_number(): + app = Sanic('test_dynamic_add_route_number') + + results = [] + + async def handler(request, weight): + results.append(weight) + return text('OK') + + app.add_route(handler, '/weight/') + + request, response = sanic_endpoint_test(app, uri='/weight/12345') + assert response.text == 'OK' + assert type(results[0]) is float + + request, response = sanic_endpoint_test(app, uri='/weight/1234.56') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/weight/1234-56') + assert response.status == 404 + + +def test_dynamic_add_route_regex(): + app = Sanic('test_dynamic_route_int') + + async def handler(request, folder_id): + return text('OK') + + app.add_route(handler, '/folder/') + + request, response = sanic_endpoint_test(app, uri='/folder/test') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test1') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/test-123') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/') + assert response.status == 200 + + +def test_dynamic_add_route_unhashable(): + app = Sanic('test_dynamic_add_route_unhashable') + + async def handler(request, unhashable): + return text('OK') + + app.add_route(handler, '/folder//end/') + + request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/nope/') + assert response.status == 404 + + +def test_add_route_duplicate(): + app = Sanic('test_add_route_duplicate') + + with pytest.raises(RouteExists): + async def handler1(request): + pass + + async def handler2(request): + pass + + app.add_route(handler1, '/test') + app.add_route(handler2, '/test') + + with pytest.raises(RouteExists): + async def handler1(request, dynamic): + pass + + async def handler2(request, dynamic): + pass + + app.add_route(handler1, '/test//') + app.add_route(handler2, '/test//') + + +def test_add_route_method_not_allowed(): + app = Sanic('test_add_route_method_not_allowed') + + async def handler(request): + return text('OK') + + app.add_route(handler, '/test', methods=['GET']) + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, method='post', uri='/test') + assert response.status == 405 diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 00000000..251b7a10 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,155 @@ +from sanic import Sanic +from sanic.response import text, HTTPResponse +from sanic.views import MethodView +from sanic.blueprints import Blueprint +from sanic.request import Request +from sanic.utils import sanic_endpoint_test + + +def test_methods(): + app = Sanic('test_methods') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def post(self, request, *args, **kwargs): + return text('I am post method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + + def patch(self, request, *args, **kwargs): + return text('I am patch method') + + def delete(self, request, *args, **kwargs): + return text('I am delete method') + + app.add_route(DummyView(), '/') + + request, response = sanic_endpoint_test(app, method="get") + assert response.text == 'I am get method' + request, response = sanic_endpoint_test(app, method="post") + assert response.text == 'I am post method' + request, response = sanic_endpoint_test(app, method="put") + assert response.text == 'I am put method' + request, response = sanic_endpoint_test(app, method="patch") + assert response.text == 'I am patch method' + request, response = sanic_endpoint_test(app, method="delete") + assert response.text == 'I am delete method' + + +def test_unexisting_methods(): + app = Sanic('test_unexisting_methods') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + app.add_route(DummyView(), '/') + request, response = sanic_endpoint_test(app, method="get") + assert response.text == 'I am get method' + request, response = sanic_endpoint_test(app, method="post") + assert response.text == 'Error: Method POST not allowed for URL /' + + +def test_argument_methods(): + app = Sanic('test_argument_methods') + + class DummyView(MethodView): + + def get(self, request, my_param_here, *args, **kwargs): + return text('I am get method with %s' % my_param_here) + + app.add_route(DummyView(), '/') + + request, response = sanic_endpoint_test(app, uri='/test123') + + assert response.text == 'I am get method with test123' + + +def test_with_bp(): + app = Sanic('test_with_bp') + bp = Blueprint('test_text') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + bp.add_route(DummyView(), '/') + + app.blueprint(bp) + request, response = sanic_endpoint_test(app) + + assert response.text == 'I am get method' + + +def test_with_bp_with_url_prefix(): + app = Sanic('test_with_bp_with_url_prefix') + bp = Blueprint('test_text', url_prefix='/test1') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + bp.add_route(DummyView(), '/') + + app.blueprint(bp) + request, response = sanic_endpoint_test(app, uri='/test1/') + + assert response.text == 'I am get method' + + +def test_with_middleware(): + app = Sanic('test_with_middleware') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + app.add_route(DummyView(), '/') + + results = [] + + @app.middleware + async def handler(request): + results.append(request) + + request, response = sanic_endpoint_test(app) + + assert response.text == 'I am get method' + assert type(results[0]) is Request + + +def test_with_middleware_response(): + app = Sanic('test_with_middleware_response') + + results = [] + + @app.middleware('request') + async def process_response(request): + results.append(request) + + @app.middleware('response') + async def process_response(request, response): + results.append(request) + results.append(response) + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + app.add_route(DummyView(), '/') + + request, response = sanic_endpoint_test(app) + + assert response.text == 'I am get method' + assert type(results[0]) is Request + assert type(results[1]) is Request + assert issubclass(type(results[2]), HTTPResponse) From fca0221d911dac1f53dd94b60ed51f06835472b8 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:14:37 +0200 Subject: [PATCH 097/977] update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b6d87de..2ece97d8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ app.run(host="0.0.0.0", port=8000) * [Middleware](docs/middleware.md) * [Exceptions](docs/exceptions.md) * [Blueprints](docs/blueprints.md) + * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) * [Deploying](docs/deploying.md) @@ -72,7 +73,7 @@ app.run(host="0.0.0.0", port=8000) ▄▄▄▄▄ ▀▀▀██████▄▄▄ _______________ ▄▄▄▄▄ █████████▄ / \ - ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | + ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | ▀▀█████▄▄ ▀██████▄██ | _________________/ ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ ▀▀▀▄ ▀▀███ ▀ ▄▄ From c3c7964e2e49fc5bac044551700365eeaf8ba006 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:29:25 +0200 Subject: [PATCH 098/977] pep8 fixes --- sanic/sanic.py | 4 +++- sanic/utils.py | 6 ++++-- sanic/views.py | 7 +++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 7a9c35ad..33e16af7 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -62,7 +62,9 @@ class Sanic: def add_route(self, handler, uri, methods=None): """ - A helper method to register class instance or functions as a handler to the application url routes. + A helper method to register class instance or + functions as a handler to the application url + routes. :param handler: function or class instance :param uri: path of the URL :param methods: list or tuple of methods allowed diff --git a/sanic/utils.py b/sanic/utils.py index 8190c1d0..5d896312 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,7 +16,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, - loop=None, debug=False, *request_args, **request_kwargs): + loop=None, debug=False, *request_args, + **request_kwargs): results = [] exceptions = [] @@ -34,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, debug=debug, port=42101, after_start=_collect_response, loop=loop) + app.run(host=HOST, debug=debug, port=42101, + after_start=_collect_response, loop=loop) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/sanic/views.py b/sanic/views.py index 9cb04247..2c4dcce2 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -3,7 +3,8 @@ from .exceptions import InvalidUsage class MethodView: """ Simple class based implementation of view for the sanic. - You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. + You should implement methods(get, post, put, patch, delete) for the class + to every HTTP method you want to support. For example: class DummyView(View): @@ -30,4 +31,6 @@ class MethodView: handler = getattr(self, request.method.lower(), None) if handler: return handler(request, *args, **kwargs) - raise InvalidUsage('Method {} not allowed for URL {}'.format(request.method, request.url), status_code=405) + raise InvalidUsage( + 'Method {} not allowed for URL {}'.format( + request.method, request.url), status_code=405) From 13808bf282493fccf81458da129514d5e28e88ac Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Fri, 25 Nov 2016 14:53:18 -0500 Subject: [PATCH 099/977] Convert server lambda to partial Partials are faster then lambdas for repeated calls. --- sanic/server.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index b6233031..edc96968 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -235,14 +235,23 @@ def serve(host, port, request_handler, before_start=None, after_start=None, connections = {} signal = Signal() - server_coroutine = loop.create_server(lambda: HttpProtocol( + server = partial( + HttpProtocol, loop=loop, connections=connections, signal=signal, request_handler=request_handler, request_timeout=request_timeout, request_max_size=request_max_size, - ), host, port, reuse_port=reuse_port, sock=sock) + ) + + server_coroutine = loop.create_server( + server, + host, + port, + reuse_port=reuse_port, + sock=sock + ) # Instead of pulling time at the end of every request, # pull it once per minute From 47927608b2c02cab80270388c0ecff4f0ccb493c Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Fri, 25 Nov 2016 15:05:17 -0500 Subject: [PATCH 100/977] Convert connections dict to set Connections don't need to be a dict since the value is never used --- sanic/server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index b6233031..dd3c86e0 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -34,7 +34,7 @@ class HttpProtocol(asyncio.Protocol): '_total_request_size', '_timeout_handler', '_last_communication_time') def __init__(self, *, loop, request_handler, signal=Signal(), - connections={}, request_timeout=60, + connections=set(), request_timeout=60, request_max_size=None): self.loop = loop self.transport = None @@ -56,14 +56,14 @@ class HttpProtocol(asyncio.Protocol): # -------------------------------------------- # def connection_made(self, transport): - self.connections[self] = True + self.connections.add(self) self._timeout_handler = self.loop.call_later( self.request_timeout, self.connection_timeout) self.transport = transport self._last_request_time = current_time def connection_lost(self, exc): - del self.connections[self] + self.connections.discard(self) self._timeout_handler.cancel() self.cleanup() @@ -233,7 +233,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, trigger_events(before_start, loop) - connections = {} + connections = set() signal = Signal() server_coroutine = loop.create_server(lambda: HttpProtocol( loop=loop, @@ -274,7 +274,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, # Complete all tasks on the loop signal.stopped = True - for connection in connections.keys(): + for connection in connections: connection.close_if_idle() while connections: From 0ca5c4eeff5e4eab6f986703acc7f3b7f3ef18c2 Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Fri, 25 Nov 2016 14:33:17 -0500 Subject: [PATCH 101/977] Use explicit import for httptools Explicit importing the parser and the exception to save a name lookup. --- sanic/server.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index b6233031..6301d18f 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -4,7 +4,8 @@ from inspect import isawaitable from multidict import CIMultiDict from signal import SIGINT, SIGTERM from time import time -import httptools +from httptools import HttpRequestParser +from httptools.parser.errors import HttpParserError try: import uvloop as async_loop @@ -94,12 +95,12 @@ class HttpProtocol(asyncio.Protocol): if self.parser is None: assert self.request is None self.headers = [] - self.parser = httptools.HttpRequestParser(self) + self.parser = HttpRequestParser(self) # Parse request chunk or close connection try: self.parser.feed_data(data) - except httptools.parser.errors.HttpParserError as e: + except HttpParserError as e: self.bail_out( "Invalid request data, connection closed ({})".format(e)) From c01cbb3a8c93e90fd22a3a58ec93804ee369a5dc Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 13:55:45 +0900 Subject: [PATCH 102/977] Change Request timeout process This add a request timeout exception. It cancels task, when request is timeout. --- examples/request_timeout.py | 21 ++++++++++++++++++ sanic/exceptions.py | 4 ++++ sanic/sanic.py | 1 + sanic/server.py | 28 ++++++++++++++++++------ tests/test_request_timeout.py | 40 +++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 examples/request_timeout.py create mode 100644 tests/test_request_timeout.py diff --git a/examples/request_timeout.py b/examples/request_timeout.py new file mode 100644 index 00000000..496864cd --- /dev/null +++ b/examples/request_timeout.py @@ -0,0 +1,21 @@ +from sanic import Sanic +import asyncio +from sanic.response import text +from sanic.config import Config +from sanic.exceptions import RequestTimeout + +Config.REQUEST_TIMEOUT = 1 +app = Sanic(__name__) + + +@app.route("/") +async def test(request): + await asyncio.sleep(3) + return text('Hello, world!') + + +@app.exception(RequestTimeout) +def timeout(request, exception): + return text('RequestTimeout from error_handler.') + +app.run(host="0.0.0.0", port=8000) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index e21aca63..bc052fbd 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -30,6 +30,10 @@ class FileNotFound(NotFound): self.relative_url = relative_url +class RequestTimeout(SanicException): + status_code = 408 + + class Handler: handlers = None diff --git a/sanic/sanic.py b/sanic/sanic.py index af284c00..128e3d28 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -250,6 +250,7 @@ class Sanic: 'sock': sock, 'debug': debug, 'request_handler': self.handle_request, + 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, 'loop': loop diff --git a/sanic/server.py b/sanic/server.py index b6233031..4b804353 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -13,6 +13,8 @@ except ImportError: from .log import log from .request import Request +from .response import HTTPResponse +from .exceptions import RequestTimeout class Signal: @@ -33,8 +35,8 @@ class HttpProtocol(asyncio.Protocol): # connection management '_total_request_size', '_timeout_handler', '_last_communication_time') - def __init__(self, *, loop, request_handler, signal=Signal(), - connections={}, request_timeout=60, + def __init__(self, *, loop, request_handler, error_handler, + signal=Signal(), connections={}, request_timeout=60, request_max_size=None): self.loop = loop self.transport = None @@ -45,11 +47,13 @@ class HttpProtocol(asyncio.Protocol): self.signal = signal self.connections = connections self.request_handler = request_handler + self.error_handler = error_handler self.request_timeout = request_timeout self.request_max_size = request_max_size self._total_request_size = 0 self._timeout_handler = None self._last_request_time = None + self._request_handler_task = None # -------------------------------------------- # # Connection @@ -75,7 +79,17 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = \ self.loop.call_later(time_left, self.connection_timeout) else: - self.bail_out("Request timed out, connection closed") + self._request_handler_task.cancel() + try: + response = self.error_handler.response( + self.request, RequestTimeout('Request Timeout')) + except Exception as e: + response = HTTPResponse( + 'Request Timeout', RequestTimeout.status_code) + self.transport.write( + response.output( + self.request.version, False, self.request_timeout)) + self.transport.close() # -------------------------------------------- # # Parsing @@ -132,7 +146,7 @@ class HttpProtocol(asyncio.Protocol): self.request.body = body def on_message_complete(self): - self.loop.create_task( + self._request_handler_task = self.loop.create_task( self.request_handler(self.request, self.write_response)) # -------------------------------------------- # @@ -165,6 +179,7 @@ class HttpProtocol(asyncio.Protocol): self.request = None self.url = None self.headers = None + self._request_handler_task = None self._total_request_size = 0 def close_if_idle(self): @@ -204,8 +219,8 @@ def trigger_events(events, loop): loop.run_until_complete(result) -def serve(host, port, request_handler, before_start=None, after_start=None, - before_stop=None, after_stop=None, +def serve(host, port, request_handler, error_handler, before_start=None, + after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, request_max_size=None, reuse_port=False, loop=None): """ @@ -240,6 +255,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, connections=connections, signal=signal, request_handler=request_handler, + error_handler=error_handler, request_timeout=request_timeout, request_max_size=request_max_size, ), host, port, reuse_port=reuse_port, sock=sock) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py new file mode 100644 index 00000000..8cf3a680 --- /dev/null +++ b/tests/test_request_timeout.py @@ -0,0 +1,40 @@ +from sanic import Sanic +import asyncio +from sanic.response import text +from sanic.exceptions import RequestTimeout +from sanic.utils import sanic_endpoint_test +from sanic.config import Config + +Config.REQUEST_TIMEOUT = 1 +request_timeout_app = Sanic('test_request_timeout') +request_timeout_default_app = Sanic('test_request_timeout_default') + + +@request_timeout_app.route('/1') +async def handler_1(request): + await asyncio.sleep(2) + return text('OK') + + +@request_timeout_app.exception(RequestTimeout) +def handler_exception(request, exception): + return text('Request Timeout from error_handler.', 408) + + +def test_server_error_request_timeout(): + request, response = sanic_endpoint_test(request_timeout_app, uri='/1') + assert response.status == 408 + assert response.text == 'Request Timeout from error_handler.' + + +@request_timeout_default_app.route('/1') +async def handler_2(request): + await asyncio.sleep(2) + return text('OK') + + +def test_default_server_error_request_timeout(): + request, response = sanic_endpoint_test( + request_timeout_default_app, uri='/1') + assert response.status == 408 + assert response.text == 'Error: Request Timeout' From 0bd61f6a57948c94cb9deff0e378b95fc8f0cb4f Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 14:14:30 +0900 Subject: [PATCH 103/977] Use write_response --- sanic/server.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 4b804353..339b8132 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -86,10 +86,7 @@ class HttpProtocol(asyncio.Protocol): except Exception as e: response = HTTPResponse( 'Request Timeout', RequestTimeout.status_code) - self.transport.write( - response.output( - self.request.version, False, self.request_timeout)) - self.transport.close() + self.write_response(response) # -------------------------------------------- # # Parsing From d8e480ab4889891906807696f36180086f00aa70 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 14:47:42 +0900 Subject: [PATCH 104/977] Change sleep time --- examples/request_timeout.py | 4 ++-- tests/test_request_timeout.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/request_timeout.py b/examples/request_timeout.py index 496864cd..ddae7688 100644 --- a/examples/request_timeout.py +++ b/examples/request_timeout.py @@ -8,7 +8,7 @@ Config.REQUEST_TIMEOUT = 1 app = Sanic(__name__) -@app.route("/") +@app.route('/') async def test(request): await asyncio.sleep(3) return text('Hello, world!') @@ -18,4 +18,4 @@ async def test(request): def timeout(request, exception): return text('RequestTimeout from error_handler.') -app.run(host="0.0.0.0", port=8000) +app.run(host='0.0.0.0', port=8000) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 8cf3a680..7b8cfb21 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -12,7 +12,7 @@ request_timeout_default_app = Sanic('test_request_timeout_default') @request_timeout_app.route('/1') async def handler_1(request): - await asyncio.sleep(2) + await asyncio.sleep(1) return text('OK') @@ -29,7 +29,7 @@ def test_server_error_request_timeout(): @request_timeout_default_app.route('/1') async def handler_2(request): - await asyncio.sleep(2) + await asyncio.sleep(1) return text('OK') From 9010a6573fea7f855b0597986248a2f3d79d1ba4 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 15:21:57 +0900 Subject: [PATCH 105/977] Add status code --- examples/request_timeout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/request_timeout.py b/examples/request_timeout.py index ddae7688..261f423a 100644 --- a/examples/request_timeout.py +++ b/examples/request_timeout.py @@ -16,6 +16,6 @@ async def test(request): @app.exception(RequestTimeout) def timeout(request, exception): - return text('RequestTimeout from error_handler.') + return text('RequestTimeout from error_handler.', 408) app.run(host='0.0.0.0', port=8000) From da4567eea5f84d208f7fbadb1bac7e310297a319 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 26 Nov 2016 08:44:46 +0200 Subject: [PATCH 106/977] changes in doc --- docs/class_based_views.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index b5f8ee02..c4ceeb0c 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -1,30 +1,30 @@ # Class based views -Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone try to use not implemented method, there will be 405 response. +Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response. ## Examples ```python from sanic import Sanic -from sanic.views import MethodView +from sanic.views import HTTPMethodView app = Sanic('some_name') -class SimpleView(MethodView): +class SimpleView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') - def post(self, request, *args, **kwargs): + def post(self, request): return text('I am post method') - def put(self, request, *args, **kwargs): + def put(self, request): return text('I am put method') - def patch(self, request, *args, **kwargs): + def patch(self, request): return text('I am patch method') - def delete(self, request, *args, **kwargs): + def delete(self, request): return text('I am delete method') app.add_route(SimpleView(), '/') @@ -34,9 +34,9 @@ app.add_route(SimpleView(), '/') If you need any url params just mention them in method definition: ```python -class NameView(MethodView): +class NameView(HTTPMethodView): - def get(self, request, name, *args, **kwargs): + def get(self, request, name): return text('Hello {}'.format(name)) app.add_route(NameView(), '/ Date: Sat, 26 Nov 2016 08:45:08 +0200 Subject: [PATCH 107/977] rename&remove redundant code --- sanic/views.py | 2 +- tests/test_views.py | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sanic/views.py b/sanic/views.py index 2c4dcce2..980a5f74 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -1,7 +1,7 @@ from .exceptions import InvalidUsage -class MethodView: +class HTTPMethodView: """ Simple class based implementation of view for the sanic. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. diff --git a/tests/test_views.py b/tests/test_views.py index 251b7a10..59acb847 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,6 +1,6 @@ from sanic import Sanic from sanic.response import text, HTTPResponse -from sanic.views import MethodView +from sanic.views import HTTPMethodView from sanic.blueprints import Blueprint from sanic.request import Request from sanic.utils import sanic_endpoint_test @@ -9,21 +9,21 @@ from sanic.utils import sanic_endpoint_test def test_methods(): app = Sanic('test_methods') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') - def post(self, request, *args, **kwargs): + def post(self, request): return text('I am post method') - def put(self, request, *args, **kwargs): + def put(self, request): return text('I am put method') - def patch(self, request, *args, **kwargs): + def patch(self, request): return text('I am patch method') - def delete(self, request, *args, **kwargs): + def delete(self, request): return text('I am delete method') app.add_route(DummyView(), '/') @@ -43,9 +43,9 @@ def test_methods(): def test_unexisting_methods(): app = Sanic('test_unexisting_methods') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') app.add_route(DummyView(), '/') @@ -58,9 +58,9 @@ def test_unexisting_methods(): def test_argument_methods(): app = Sanic('test_argument_methods') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, my_param_here, *args, **kwargs): + def get(self, request, my_param_here): return text('I am get method with %s' % my_param_here) app.add_route(DummyView(), '/') @@ -74,9 +74,9 @@ def test_with_bp(): app = Sanic('test_with_bp') bp = Blueprint('test_text') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') bp.add_route(DummyView(), '/') @@ -91,9 +91,9 @@ def test_with_bp_with_url_prefix(): app = Sanic('test_with_bp_with_url_prefix') bp = Blueprint('test_text', url_prefix='/test1') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') bp.add_route(DummyView(), '/') @@ -107,9 +107,9 @@ def test_with_bp_with_url_prefix(): def test_with_middleware(): app = Sanic('test_with_middleware') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') app.add_route(DummyView(), '/') @@ -140,9 +140,9 @@ def test_with_middleware_response(): results.append(request) results.append(response) - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') app.add_route(DummyView(), '/') From a5e6d6d2e8a9e3f879fba5cd0d8e175c774e5ceb Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 16:02:44 +0900 Subject: [PATCH 108/977] Use default error process --- sanic/server.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 339b8132..b68524f8 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -13,7 +13,6 @@ except ImportError: from .log import log from .request import Request -from .response import HTTPResponse from .exceptions import RequestTimeout @@ -80,12 +79,8 @@ class HttpProtocol(asyncio.Protocol): self.loop.call_later(time_left, self.connection_timeout) else: self._request_handler_task.cancel() - try: - response = self.error_handler.response( - self.request, RequestTimeout('Request Timeout')) - except Exception as e: - response = HTTPResponse( - 'Request Timeout', RequestTimeout.status_code) + response = self.error_handler.response( + self.request, RequestTimeout('Request Timeout')) self.write_response(response) # -------------------------------------------- # From ee89b6ad03839bbe526f7e84958f9720e9a30e3f Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 16:47:16 +0900 Subject: [PATCH 109/977] before process --- sanic/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index b68524f8..dd582325 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -78,7 +78,8 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = \ self.loop.call_later(time_left, self.connection_timeout) else: - self._request_handler_task.cancel() + if self._request_handler_task: + self._request_handler_task.cancel() response = self.error_handler.response( self.request, RequestTimeout('Request Timeout')) self.write_response(response) From d86ac5e3e03f3a1469e9f4e1bce9af1919fbaa54 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Sat, 26 Nov 2016 11:20:29 -0500 Subject: [PATCH 110/977] fix for cookie header capitalization bug --- sanic/request.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index 8023fd9c..676eaa51 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -114,6 +114,8 @@ class Request(dict): @property def cookies(self): if self._cookies is None: + if 'cookie' in self.headers: #HTTP2 cookie header + self.headers['Cookie'] = self.headers.pop('cookie') if 'Cookie' in self.headers: cookies = SimpleCookie() cookies.load(self.headers['Cookie']) From 0c215685f2e08ef4d5ff3b16d849194d81557133 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Sun, 27 Nov 2016 08:30:46 -0500 Subject: [PATCH 111/977] refactoring cookies --- sanic/request.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 676eaa51..e5da4ce3 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -114,11 +114,10 @@ class Request(dict): @property def cookies(self): if self._cookies is None: - if 'cookie' in self.headers: #HTTP2 cookie header - self.headers['Cookie'] = self.headers.pop('cookie') - if 'Cookie' in self.headers: + cookie = self.headers.get('Cookie') or self.headers.get('cookie') + if cookie is not None: cookies = SimpleCookie() - cookies.load(self.headers['Cookie']) + cookies.load(cookie) self._cookies = {name: cookie.value for name, cookie in cookies.items()} else: From 190b7a607610551d1071dae5ee682c45efb7c00a Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:00:39 -0500 Subject: [PATCH 112/977] improving comments and examples --- sanic/request.py | 4 ++-- sanic/router.py | 12 +++++++++--- sanic/utils.py | 4 ++-- sanic/views.py | 8 +++++--- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 8023fd9c..bc7fcabb 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -67,7 +67,7 @@ class Request(dict): try: self.parsed_json = json_loads(self.body) except Exception: - log.exception("failed when parsing body as json") + log.exception("Failed when parsing body as json") return self.parsed_json @@ -89,7 +89,7 @@ class Request(dict): self.parsed_form, self.parsed_files = ( parse_multipart_form(self.body, boundary)) except Exception: - log.exception("failed when parsing form") + log.exception("Failed when parsing form") return self.parsed_form diff --git a/sanic/router.py b/sanic/router.py index 8392dcd8..0a1faec5 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -30,11 +30,17 @@ class Router: @sanic.route('/my/url/', methods=['GET', 'POST', ...]) def my_route(request, my_parameter): do stuff... + or + @sanic.route('/my/url/:int', methods['GET', 'POST', ...]) + def my_route_with_type(request, my_parameter): + do stuff... Parameters will be passed as keyword arguments to the request handling - function provided Parameters can also have a type by appending :type to - the . If no type is provided, a string is expected. A regular - expression can also be passed in as the type + function. Provided parameters can also have a type by appending :type to + the . Given parameter must be able to be type-casted to this. + If no type is provided, a string is expected. A regular expression can + also be passed in as the type. The argument given to the function will + always be a string, independent of the type. """ routes_static = None routes_dynamic = None diff --git a/sanic/utils.py b/sanic/utils.py index 5d896312..88444b3c 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -47,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, return request, response except: raise ValueError( - "request and response object expected, got ({})".format( + "Request and response object expected, got ({})".format( results)) else: try: return results[0] except: raise ValueError( - "request object expected, got ({})".format(results)) + "Request object expected, got ({})".format(results)) diff --git a/sanic/views.py b/sanic/views.py index 980a5f74..440702bd 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -3,8 +3,9 @@ from .exceptions import InvalidUsage class HTTPMethodView: """ Simple class based implementation of view for the sanic. - You should implement methods(get, post, put, patch, delete) for the class + You should implement methods (get, post, put, patch, delete) for the class to every HTTP method you want to support. + For example: class DummyView(View): @@ -14,9 +15,10 @@ class HTTPMethodView: def put(self, request, *args, **kwargs): return text('I am put method') etc. - If someone try use not implemented method, there will be 405 response - If you need any url params just mention them in method definition like: + If someone tries to use a non-implemented method, there will be a 405 response. + + If you need any url params just mention them in method definition: class DummyView(View): def get(self, request, my_param_here, *args, **kwargs): From 209b7633025e63f1c6f163f3024139d50bd9dabd Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:05:47 -0500 Subject: [PATCH 113/977] fix typo --- sanic/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index 0a1faec5..4cc1f073 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -31,7 +31,7 @@ class Router: def my_route(request, my_parameter): do stuff... or - @sanic.route('/my/url/:int', methods['GET', 'POST', ...]) + @sanic.route('/my/url/:type', methods['GET', 'POST', ...]) def my_route_with_type(request, my_parameter): do stuff... From 70c56b7db33b558ff5fd5a95883fcb558b9621ba Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:22:07 -0500 Subject: [PATCH 114/977] fixing line length --- sanic/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/views.py b/sanic/views.py index 440702bd..9387bcf6 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -16,7 +16,8 @@ class HTTPMethodView: return text('I am put method') etc. - If someone tries to use a non-implemented method, there will be a 405 response. + If someone tries to use a non-implemented method, there will be a + 405 response. If you need any url params just mention them in method definition: class DummyView(View): From 39f3a63cede12ee40e17073a95578d37a730c158 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 29 Nov 2016 15:59:03 -0600 Subject: [PATCH 115/977] Increment version to 0.1.8 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index d8a9e56e..6e7f8d23 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.7' +__version__ = '0.1.8' __all__ = ['Sanic', 'Blueprint'] From 9b466db5c9d1d717cfb136cacb741f59333445c3 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Sat, 3 Dec 2016 15:19:24 -0500 Subject: [PATCH 116/977] test for http2 lowercase header cookies --- tests/test_cookies.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 5b27c2e7..cf6a4259 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -25,6 +25,19 @@ def test_cookies(): assert response.text == 'Cookies are: working!' assert response_cookies['right_back'].value == 'at you' +def test_http2_cookies(): + app = Sanic('test_http2_cookies') + + @app.route('/') + async def handler(request): + response = text('Cookies are: {}'.format(request.cookies['test'])) + return response + + headers = {'cookie': 'test=working!'} + request, response = sanic_endpoint_test(app, headers=headers) + + assert response.text == 'Cookies are: working!' + def test_cookie_options(): app = Sanic('test_text') From 662e0c9965b71a1b18bf689b3ca972ce36ec2ec2 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sun, 4 Dec 2016 10:50:32 +0900 Subject: [PATCH 117/977] Change Payload Too Large process When Payload Too Large occurs, it uses error handler. --- sanic/exceptions.py | 4 +++ sanic/server.py | 26 ++++++++++------ tests/test_payload_too_large.py | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 tests/test_payload_too_large.py diff --git a/sanic/exceptions.py b/sanic/exceptions.py index bc052fbd..369a87a2 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -34,6 +34,10 @@ class RequestTimeout(SanicException): status_code = 408 +class PayloadTooLarge(SanicException): + status_code = 413 + + class Handler: handlers = None diff --git a/sanic/server.py b/sanic/server.py index a3074ecf..534436fa 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -14,7 +14,7 @@ except ImportError: from .log import log from .request import Request -from .exceptions import RequestTimeout +from .exceptions import RequestTimeout, PayloadTooLarge class Signal: @@ -81,9 +81,8 @@ class HttpProtocol(asyncio.Protocol): else: if self._request_handler_task: self._request_handler_task.cancel() - response = self.error_handler.response( - self.request, RequestTimeout('Request Timeout')) - self.write_response(response) + exception = RequestTimeout('Request Timeout') + self.write_error(exception) # -------------------------------------------- # # Parsing @@ -94,9 +93,8 @@ class HttpProtocol(asyncio.Protocol): # memory limits self._total_request_size += len(data) if self._total_request_size > self.request_max_size: - return self.bail_out( - "Request too large ({}), connection closed".format( - self._total_request_size)) + exception = PayloadTooLarge('Payload Too Large') + self.write_error(exception) # Create parser if this is the first time we're receiving data if self.parser is None: @@ -116,8 +114,8 @@ class HttpProtocol(asyncio.Protocol): def on_header(self, name, value): if name == b'Content-Length' and int(value) > self.request_max_size: - return self.bail_out( - "Request body too large ({}), connection closed".format(value)) + exception = PayloadTooLarge('Payload Too Large') + self.write_error(exception) self.headers.append((name.decode(), value.decode('utf-8'))) @@ -164,6 +162,16 @@ class HttpProtocol(asyncio.Protocol): self.bail_out( "Writing response failed, connection closed {}".format(e)) + def write_error(self, exception): + try: + response = self.error_handler.response(self.request, exception) + version = self.request.version if self.request else '1.1' + self.transport.write(response.output(version)) + self.transport.close() + except Exception as e: + self.bail_out( + "Writing error failed, connection closed {}".format(e)) + def bail_out(self, message): log.debug(message) self.transport.close() diff --git a/tests/test_payload_too_large.py b/tests/test_payload_too_large.py new file mode 100644 index 00000000..e8eec09e --- /dev/null +++ b/tests/test_payload_too_large.py @@ -0,0 +1,54 @@ +from sanic import Sanic +from sanic.response import text +from sanic.exceptions import PayloadTooLarge +from sanic.utils import sanic_endpoint_test + +data_received_app = Sanic('data_received') +data_received_app.config.REQUEST_MAX_SIZE = 1 +data_received_default_app = Sanic('data_received_default') +data_received_default_app.config.REQUEST_MAX_SIZE = 1 +on_header_default_app = Sanic('on_header') +on_header_default_app.config.REQUEST_MAX_SIZE = 500 + + +@data_received_app.route('/1') +async def handler1(request): + return text('OK') + + +@data_received_app.exception(PayloadTooLarge) +def handler_exception(request, exception): + return text('Payload Too Large from error_handler.', 413) + + +def test_payload_too_large_from_error_handler(): + response = sanic_endpoint_test( + data_received_app, uri='/1', gather_request=False) + assert response.status == 413 + assert response.text == 'Payload Too Large from error_handler.' + + +@data_received_default_app.route('/1') +async def handler2(request): + return text('OK') + + +def test_payload_too_large_at_data_received_default(): + response = sanic_endpoint_test( + data_received_default_app, uri='/1', gather_request=False) + assert response.status == 413 + assert response.text == 'Error: Payload Too Large' + + +@on_header_default_app.route('/1') +async def handler3(request): + return text('OK') + + +def test_payload_too_large_at_on_header_default(): + data = 'a' * 1000 + response = sanic_endpoint_test( + on_header_default_app, method='post', uri='/1', + gather_request=False, data=data) + assert response.status == 413 + assert response.text == 'Error: Payload Too Large' From fac4bca4f49c3dc34ff57851d59fba64790d1e31 Mon Sep 17 00:00:00 2001 From: 1a23456789 Date: Tue, 6 Dec 2016 10:44:08 +0900 Subject: [PATCH 118/977] Fix test_request_timeout.py This increases sleep time, Because sometimes timeout error does not occur. --- tests/test_request_timeout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 7b8cfb21..8cf3a680 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -12,7 +12,7 @@ request_timeout_default_app = Sanic('test_request_timeout_default') @request_timeout_app.route('/1') async def handler_1(request): - await asyncio.sleep(1) + await asyncio.sleep(2) return text('OK') @@ -29,7 +29,7 @@ def test_server_error_request_timeout(): @request_timeout_default_app.route('/1') async def handler_2(request): - await asyncio.sleep(1) + await asyncio.sleep(2) return text('OK') From 457507d8dc2772f59a144f713f01bd15f73183eb Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 7 Dec 2016 20:33:56 -0800 Subject: [PATCH 119/977] return 400 on invalid json post data --- sanic/request.py | 3 ++- tests/test_requests.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index d3c11cd0..62d89781 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -4,6 +4,7 @@ from http.cookies import SimpleCookie from httptools import parse_url from urllib.parse import parse_qs from ujson import loads as json_loads +from sanic.exceptions import InvalidUsage from .log import log @@ -67,7 +68,7 @@ class Request(dict): try: self.parsed_json = json_loads(self.body) except Exception: - log.exception("Failed when parsing body as json") + raise InvalidUsage("Failed when parsing body as json") return self.parsed_json diff --git a/tests/test_requests.py b/tests/test_requests.py index 756113b2..81895c8c 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -49,6 +49,19 @@ def test_json(): assert results.get('test') == True +def test_invalid_json(): + app = Sanic('test_json') + + @app.route('/') + async def handler(request): + return json(request.json()) + + data = "I am not json" + request, response = sanic_endpoint_test(app, data=data) + + assert response.status == 400 + + def test_query_string(): app = Sanic('test_query_string') From 0464d31a9c91f70699b3ad5706f82927dc442623 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Sat, 10 Dec 2016 12:16:37 +0100 Subject: [PATCH 120/977] Find URL encoded filenames on the fs by decoding them first --- sanic/static.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sanic/static.py b/sanic/static.py index 72361a9a..ed7d6f8c 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -2,6 +2,7 @@ from aiofiles.os import stat from os import path from re import sub from time import strftime, gmtime +from urllib.parse import unquote from .exceptions import FileNotFound, InvalidUsage from .response import file, HTTPResponse @@ -38,6 +39,8 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # from herping a derp and treating the uri as an absolute path file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory + + file_path = unquote(file_path) try: headers = {} # Check if the client has been sent this file before From 154f8570f073c94bec55f1c2681162d6b1beec16 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 13:43:31 +0200 Subject: [PATCH 121/977] add sanic aiopg example with raw sql --- examples/sanic_aiopg_example.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/sanic_aiopg_example.py diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py new file mode 100644 index 00000000..7f4901e6 --- /dev/null +++ b/examples/sanic_aiopg_example.py @@ -0,0 +1,58 @@ +import os +import asyncio +import datetime + +import uvloop +import aiopg + +from sanic import Sanic +from sanic.response import json + +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + +database_name = os.environ['DATABASE_NAME'] +database_host = os.environ['DATABASE_HOST'] +database_user = os.environ['DATABASE_USER'] +database_password = os.environ['DATABASE_PASSWORD'] + +connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, database_password, database_host, database_name) +loop = asyncio.get_event_loop() + + +async def get_pool(): + return await aiopg.create_pool(connection) + +app = Sanic(name=__name__) +pool = loop.run_until_complete(get_pool()) + + +async def prepare_db(): + """ Let's create some table and add some data + + """ + async with pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute("""CREATE TABLE sanic_poll ( + id integer primary key, + question varchar(50), + pub_date timestamp + );""") + for i in range(0, 100): + await cur.execute("""INSERT INTO sanic_poll (id, question, pub_date) VALUES ({}, {}, now()) + """.format(i, i)) + + +@app.route("/") +async def handle(request): + async with pool.acquire() as conn: + async with conn.cursor() as cur: + result = [] + await cur.execute("SELECT question, pub_date FROM sanic_poll") + async for row in cur: + result.append({"question": row[0], "pub_date": row[1]}) + return json({"polls": result}) + + +if __name__ == '__main__': + loop.run_until_complete(prepare_db()) + app.run(host="0.0.0.0", port=8100, workers=3, loop=loop) From 721044b3786f4ac190256a3d0d870cf51077e2a8 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 14:04:24 +0200 Subject: [PATCH 122/977] improvements for aiopg example --- examples/sanic_aiopg_example.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index 7f4901e6..539917df 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -1,6 +1,5 @@ import os import asyncio -import datetime import uvloop import aiopg @@ -32,13 +31,14 @@ async def prepare_db(): """ async with pool.acquire() as conn: async with conn.cursor() as cur: - await cur.execute("""CREATE TABLE sanic_poll ( + await cur.execute('DROP TABLE IF EXISTS sanic_polls') + await cur.execute("""CREATE TABLE sanic_polls ( id integer primary key, question varchar(50), pub_date timestamp );""") for i in range(0, 100): - await cur.execute("""INSERT INTO sanic_poll (id, question, pub_date) VALUES ({}, {}, now()) + await cur.execute("""INSERT INTO sanic_polls (id, question, pub_date) VALUES ({}, {}, now()) """.format(i, i)) @@ -47,7 +47,7 @@ async def handle(request): async with pool.acquire() as conn: async with conn.cursor() as cur: result = [] - await cur.execute("SELECT question, pub_date FROM sanic_poll") + await cur.execute("SELECT question, pub_date FROM sanic_polls") async for row in cur: result.append({"question": row[0], "pub_date": row[1]}) return json({"polls": result}) From f9176bfdea547bcc3202316c3eb1fd563ace01e7 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 14:14:03 +0200 Subject: [PATCH 123/977] pep8&improvements --- examples/sanic_aiopg_example.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index 539917df..ff9ec65e 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -14,7 +14,10 @@ database_host = os.environ['DATABASE_HOST'] database_user = os.environ['DATABASE_USER'] database_password = os.environ['DATABASE_PASSWORD'] -connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, database_password, database_host, database_name) +connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, + database_password, + database_host, + database_name) loop = asyncio.get_event_loop() @@ -33,12 +36,13 @@ async def prepare_db(): async with conn.cursor() as cur: await cur.execute('DROP TABLE IF EXISTS sanic_polls') await cur.execute("""CREATE TABLE sanic_polls ( - id integer primary key, + id serial primary key, question varchar(50), pub_date timestamp );""") for i in range(0, 100): - await cur.execute("""INSERT INTO sanic_polls (id, question, pub_date) VALUES ({}, {}, now()) + await cur.execute("""INSERT INTO sanic_polls + (id, question, pub_date) VALUES ({}, {}, now()) """.format(i, i)) From b44e9baaecee1ec757409e9d1ce263f58e22fc86 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 14:21:02 +0200 Subject: [PATCH 124/977] aiopg with sqlalchemy example --- examples/sanic_aiopg_example.py | 5 +- examples/sanic_aiopg_sqlalchemy_example.py | 73 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 examples/sanic_aiopg_sqlalchemy_example.py diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index ff9ec65e..73ef6c64 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -1,3 +1,6 @@ +""" To run this example you need additional aiopg package + +""" import os import asyncio @@ -59,4 +62,4 @@ async def handle(request): if __name__ == '__main__': loop.run_until_complete(prepare_db()) - app.run(host="0.0.0.0", port=8100, workers=3, loop=loop) + app.run(host='0.0.0.0', port=8000, loop=loop) diff --git a/examples/sanic_aiopg_sqlalchemy_example.py b/examples/sanic_aiopg_sqlalchemy_example.py new file mode 100644 index 00000000..cb9f6c57 --- /dev/null +++ b/examples/sanic_aiopg_sqlalchemy_example.py @@ -0,0 +1,73 @@ +""" To run this example you need additional aiopg package + +""" +import os +import asyncio +import datetime + +import uvloop +from aiopg.sa import create_engine +import sqlalchemy as sa + +from sanic import Sanic +from sanic.response import json + +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + +database_name = os.environ['DATABASE_NAME'] +database_host = os.environ['DATABASE_HOST'] +database_user = os.environ['DATABASE_USER'] +database_password = os.environ['DATABASE_PASSWORD'] + +connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, + database_password, + database_host, + database_name) +loop = asyncio.get_event_loop() + + +metadata = sa.MetaData() + +polls = sa.Table('sanic_polls', metadata, + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('question', sa.String(50)), + sa.Column("pub_date", sa.DateTime)) + + +async def get_engine(): + return await create_engine(connection) + +app = Sanic(name=__name__) +engine = loop.run_until_complete(get_engine()) + + +async def prepare_db(): + """ Let's add some data + + """ + async with engine.acquire() as conn: + await conn.execute('DROP TABLE IF EXISTS sanic_polls') + await conn.execute("""CREATE TABLE sanic_polls ( + id serial primary key, + question varchar(50), + pub_date timestamp + );""") + for i in range(0, 100): + await conn.execute( + polls.insert().values(question=i, + pub_date=datetime.datetime.now()) + ) + + +@app.route("/") +async def handle(request): + async with engine.acquire() as conn: + result = [] + async for row in conn.execute(polls.select()): + result.append({"question": row.question, "pub_date": row.pub_date}) + return json({"polls": result}) + + +if __name__ == '__main__': + loop.run_until_complete(prepare_db()) + app.run(host='0.0.0.0', port=8000, loop=loop) From 6ef6d9a9051dca5d1896f3a03f2a8e13ce0bbc44 Mon Sep 17 00:00:00 2001 From: kamyar Date: Sun, 11 Dec 2016 16:34:22 +0200 Subject: [PATCH 125/977] url params docs typo fix add missing '>' in url params docs example --- docs/class_based_views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index c4ceeb0c..223304ae 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -39,6 +39,6 @@ class NameView(HTTPMethodView): def get(self, request, name): return text('Hello {}'.format(name)) -app.add_route(NameView(), '/') ``` From 9ba2f99ea26c366aedea8f94ea0af152fcb43b99 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Tue, 13 Dec 2016 01:10:24 +0100 Subject: [PATCH 126/977] added a comment on why to decode the file_path --- sanic/static.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sanic/static.py b/sanic/static.py index ed7d6f8c..b02786a4 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -40,6 +40,8 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory + # URL decode the path sent by the browser otherwise we won't be able to + # match filenames which got encoded (filenames with spaces etc) file_path = unquote(file_path) try: headers = {} From 93b45e9598cfa758559b1205ea399c6198bf3c73 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 12 Dec 2016 22:18:33 -0800 Subject: [PATCH 127/977] add jinja example --- examples/jinja_example.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 examples/jinja_example.py diff --git a/examples/jinja_example.py b/examples/jinja_example.py new file mode 100644 index 00000000..1f9bb1ba --- /dev/null +++ b/examples/jinja_example.py @@ -0,0 +1,18 @@ +## To use this example: +# curl -d '{"name": "John Doe"}' localhost:8000 + +from sanic import Sanic +from sanic.response import html +from jinja2 import Template + +template = Template('Hello {{ name }}!') + +app = Sanic(__name__) + +@app.route('/') +async def test(request): + data = request.json + return html(template.render(**data)) + + +app.run(host="0.0.0.0", port=8000) From 2003eceba19618fcb20d78b19af072726113cdfc Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Tue, 13 Dec 2016 10:41:39 +0100 Subject: [PATCH 128/977] remove trailing space --- sanic/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index b02786a4..a70bff2f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -40,7 +40,7 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory - # URL decode the path sent by the browser otherwise we won't be able to + # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) file_path = unquote(file_path) try: From 8957e4ec25fb2b6b1b20e36a78f5576b6a7736f1 Mon Sep 17 00:00:00 2001 From: Sam Agnew Date: Tue, 13 Dec 2016 12:35:46 -0500 Subject: [PATCH 129/977] Fix PEP8 in Hello World example --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ece97d8..669ed8d9 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,17 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E from sanic import Sanic from sanic.response import json + app = Sanic() + @app.route("/") async def test(request): return json({"hello": "world"}) -app.run(host="0.0.0.0", port=8000) +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000) + ``` ## Installation From 435d5585e9d1d623b522647daf6296089dc6db85 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 14 Dec 2016 11:29:09 -0600 Subject: [PATCH 130/977] Fix leftover blank line flake8 build failed here: https://travis-ci.org/channelcat/sanic/builds/183991976 --- sanic/server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index d9136a65..a86da9fc 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -35,7 +35,6 @@ class HttpProtocol(asyncio.Protocol): # connection management '_total_request_size', '_timeout_handler', '_last_communication_time') - def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections={}, request_timeout=60, request_max_size=None): From a9b67c3028d85fd3f95e9df11129d0cf623eb2f4 Mon Sep 17 00:00:00 2001 From: Sam Agnew Date: Wed, 14 Dec 2016 12:36:33 -0500 Subject: [PATCH 131/977] Fix quotes in sample code for consistency --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 669ed8d9..5aded700 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ app = Sanic() async def test(request): return json({"hello": "world"}) -if __name__ == '__main__': +if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) ``` From 04798cbf5b0976b840430f19c61cd870df28a650 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Fri, 16 Dec 2016 16:07:14 +0100 Subject: [PATCH 132/977] added methods to load config from a file --- sanic/config.py | 73 +++++++++++++++++++++++++++++++++++++++++++++---- sanic/router.py | 17 ++++-------- 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 3dbf06c8..55cebc61 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,5 +1,11 @@ -class Config: - LOGO = """ +import os +import types + + +class Config(dict): + def __init__(self, defaults=None): + dict.__init__(self, defaults or {}) + self.LOGO = """ ▄▄▄▄▄ ▀▀▀██████▄▄▄ _______________ ▄▄▄▄▄ █████████▄ / \\ @@ -20,6 +26,63 @@ class Config: ▌ ▐ ▀▀▄▄▄▀ ▀▀▄▄▀ """ - REQUEST_MAX_SIZE = 100000000 # 100 megababies - REQUEST_TIMEOUT = 60 # 60 seconds - ROUTER_CACHE_SIZE = 1024 + self.REQUEST_MAX_SIZE = 100000000 # 100 megababies + self.REQUEST_TIMEOUT = 60 # 60 seconds + self.ROUTER_CACHE_SIZE = 1024 + + def __getattr__(self, attr): + return self[attr] + + def __setattr__(self, attr, value): + self[attr] = value + + def from_envvar(self, variable_name): + """Loads a configuration from an environment variable pointing to + a configuration file. + :param variable_name: name of the environment variable + :return: bool. ``True`` if able to load config, ``False`` otherwise. + """ + config_file = os.environ.get(variable_name) + if not config_file: + raise RuntimeError('The environment variable %r is not set and ' + 'thus configuration could not be loaded.' % + variable_name) + return self.from_pyfile(config_file) + + def from_pyfile(self, filename): + """Updates the values in the config from a Python file. Only the uppercase + varibales in that module are stored in the config. + :param filename: an absolute path to the config file + """ + module = types.ModuleType('config') + module.__file__ = filename + try: + with open(filename) as config_file: + exec(compile(config_file.read(), filename, 'exec'), + module.__dict__) + except IOError as e: + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise + self.from_object(module) + return True + + def from_object(self, obj): + """Updates the values from the given object. + Objects are usually either modules or classes. + + Just the uppercase variables in that object are stored in the config. + Example usage:: + + from yourapplication import default_config + app.config.from_object(default_config) + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + :param obj: an object + """ + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) diff --git a/sanic/router.py b/sanic/router.py index 4cc1f073..29bebc3e 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -1,7 +1,6 @@ import re from collections import defaultdict, namedtuple from functools import lru_cache -from .config import Config from .exceptions import NotFound, InvalidUsage Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) @@ -14,6 +13,8 @@ REGEX_TYPES = { 'alpha': (str, r'[A-Za-z]+'), } +ROUTER_CACHE_SIZE = 1024 + def url_hash(url): return url.count('/') @@ -30,17 +31,11 @@ class Router: @sanic.route('/my/url/', methods=['GET', 'POST', ...]) def my_route(request, my_parameter): do stuff... - or - @sanic.route('/my/url/:type', methods['GET', 'POST', ...]) - def my_route_with_type(request, my_parameter): - do stuff... Parameters will be passed as keyword arguments to the request handling - function. Provided parameters can also have a type by appending :type to - the . Given parameter must be able to be type-casted to this. - If no type is provided, a string is expected. A regular expression can - also be passed in as the type. The argument given to the function will - always be a string, independent of the type. + function provided Parameters can also have a type by appending :type to + the . If no type is provided, a string is expected. A regular + expression can also be passed in as the type """ routes_static = None routes_dynamic = None @@ -118,7 +113,7 @@ class Router: """ return self._get(request.url, request.method) - @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) + @lru_cache(maxsize=ROUTER_CACHE_SIZE) def _get(self, url, method): """ Gets a request handler based on the URL of the request, or raises an From a550b5c112e4744880e97439c2df41ea2d7f6a41 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Fri, 16 Dec 2016 18:46:07 +0100 Subject: [PATCH 133/977] added tests and small fixes for config --- sanic/config.py | 7 ++-- tests/test_config.py | 76 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 tests/test_config.py diff --git a/sanic/config.py b/sanic/config.py index 55cebc61..feb3b484 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -4,7 +4,7 @@ import types class Config(dict): def __init__(self, defaults=None): - dict.__init__(self, defaults or {}) + super().__init__(defaults or {}) self.LOGO = """ ▄▄▄▄▄ ▀▀▀██████▄▄▄ _______________ @@ -31,7 +31,10 @@ class Config(dict): self.ROUTER_CACHE_SIZE = 1024 def __getattr__(self, attr): - return self[attr] + try: + return self[attr] + except KeyError as ke: + raise AttributeError("Config has no '{}'".format(ke.args[0])) def __setattr__(self, attr, value): self[attr] = value diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..c7e41ade --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,76 @@ +from os import environ +import pytest +from tempfile import NamedTemporaryFile + +from sanic import Sanic + + +def test_load_from_object(): + app = Sanic('test_load_from_object') + class Config: + not_for_config = 'should not be used' + CONFIG_VALUE = 'should be used' + + app.config.from_object(Config) + assert 'CONFIG_VALUE' in app.config + assert app.config.CONFIG_VALUE == 'should be used' + assert 'not_for_config' not in app.config + + +def test_load_from_file(): + app = Sanic('test_load_from_file') + config = b""" +VALUE = 'some value' +condition = 1 == 1 +if condition: + CONDITIONAL = 'should be set' + """ + with NamedTemporaryFile() as config_file: + config_file.write(config) + config_file.seek(0) + app.config.from_pyfile(config_file.name) + assert 'VALUE' in app.config + assert app.config.VALUE == 'some value' + assert 'CONDITIONAL' in app.config + assert app.config.CONDITIONAL == 'should be set' + assert 'condition' not in app.config + + +def test_load_from_missing_file(): + app = Sanic('test_load_from_missing_file') + with pytest.raises(IOError): + app.config.from_pyfile('non-existent file') + + +def test_load_from_envvar(): + app = Sanic('test_load_from_envvar') + config = b"VALUE = 'some value'" + with NamedTemporaryFile() as config_file: + config_file.write(config) + config_file.seek(0) + environ['APP_CONFIG'] = config_file.name + app.config.from_envvar('APP_CONFIG') + assert 'VALUE' in app.config + assert app.config.VALUE == 'some value' + + +def test_load_from_missing_envvar(): + app = Sanic('test_load_from_missing_envvar') + with pytest.raises(RuntimeError): + app.config.from_envvar('non-existent variable') + + +def test_overwrite_exisiting_config(): + app = Sanic('test_overwrite_exisiting_config') + app.config.DEFAULT = 1 + class Config: + DEFAULT = 2 + + app.config.from_object(Config) + assert app.config.DEFAULT == 2 + + +def test_missing_config(): + app = Sanic('test_missing_config') + with pytest.raises(AttributeError): + app.config.NON_EXISTENT From 234a7925c611e8f80b94dc183f1f4d86b40a8a84 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Fri, 16 Dec 2016 18:51:44 +0100 Subject: [PATCH 134/977] restored accidentally degraded doc string --- sanic/config.py | 2 +- sanic/router.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index feb3b484..f7db0465 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -54,7 +54,7 @@ class Config(dict): def from_pyfile(self, filename): """Updates the values in the config from a Python file. Only the uppercase - varibales in that module are stored in the config. + variables in that module are stored in the config. :param filename: an absolute path to the config file """ module = types.ModuleType('config') diff --git a/sanic/router.py b/sanic/router.py index 29bebc3e..2045dfe1 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -31,11 +31,17 @@ class Router: @sanic.route('/my/url/', methods=['GET', 'POST', ...]) def my_route(request, my_parameter): do stuff... + or + @sanic.route('/my/url/:type', methods['GET', 'POST', ...]) + def my_route_with_type(request, my_parameter): + do stuff... Parameters will be passed as keyword arguments to the request handling - function provided Parameters can also have a type by appending :type to - the . If no type is provided, a string is expected. A regular - expression can also be passed in as the type + function. Provided parameters can also have a type by appending :type to + the . Given parameter must be able to be type-casted to this. + If no type is provided, a string is expected. A regular expression can + also be passed in as the type. The argument given to the function will + always be a string, independent of the type. """ routes_static = None routes_dynamic = None From ef9edfd160915c287624f30f0493521ccc533076 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Sat, 17 Dec 2016 20:20:07 +0100 Subject: [PATCH 135/977] added documentation for configuration --- README.md | 1 + docs/config.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ sanic/config.py | 3 +- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 docs/config.md diff --git a/README.md b/README.md index 5aded700..f9695cf6 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ if __name__ == "__main__": * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) + * [Configuration](docs/config.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 00000000..6dd67cf2 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,78 @@ +# Configuration + +Any reasonably complex application will need configuration that is not baked into the acutal code. Settings might be different for different environments or installations. + +## Basics + +Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary: + +``` +app = Sanic('myapp') +app.config.DB_NAME = 'appdb' +app.config.DB_USER = 'appuser' +``` + +Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once: + +``` +db_settings = { + 'DB_HOST': 'localhost', + 'DB_NAME': 'appdb', + 'DB_USER': 'appuser' +} +app.config.update(db_settings) +``` + +In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters. + +## Loading Configuration + +There are several ways how to load configuration. + +### From an Object + +If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module: + +``` +import myapp.default_settings + +app = Sanic('myapp') +app.config.from_object(myapp.default_settings) +``` + +You could use a class or any other object as well. + +### From a File + +Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_file(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file: + +``` +app = Sanic('myapp') +app.config.from_envvar('MYAPP_SETTINGS') +``` + +Then you can run your application with the `MYAPP_SETTINGS` environment variable set: + +``` +$ MYAPP_SETTINGS=/path/to/config_file; python3 myapp.py +INFO: Goin' Fast @ http://0.0.0.0:8000 +``` + +The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase varibales are added to the configuration. Most commonly the configuration consists of simple key value pairs: + +``` +# config_file +DB_HOST = 'localhost' +DB_NAME = 'appdb' +DB_USER = 'appuser' +``` + +## Builtin Configuration Values + +Out of the box there are just a few predefined values which can be overwritten when creating the application. + +| Variable | Default | Description | +| ----------------- | --------- | --------------------------------- | +| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | +| REQUEST_TIMEOUT | 60 | How long a request can take (sec) | + diff --git a/sanic/config.py b/sanic/config.py index f7db0465..59d68f52 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -28,7 +28,6 @@ class Config(dict): """ self.REQUEST_MAX_SIZE = 100000000 # 100 megababies self.REQUEST_TIMEOUT = 60 # 60 seconds - self.ROUTER_CACHE_SIZE = 1024 def __getattr__(self, attr): try: @@ -84,7 +83,7 @@ class Config(dict): with :meth:`from_pyfile` and ideally from a location not within the package because the package might be installed system wide. - :param obj: an object + :param obj: an object holding the configuration """ for key in dir(obj): if key.isupper(): From 75fc9f91b942f82cd9e25161e076afa28ae10472 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sun, 18 Dec 2016 09:25:39 +0900 Subject: [PATCH 136/977] Change HttpParserError process --- sanic/server.py | 8 ++++---- tests/test_bad_request.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 tests/test_bad_request.py diff --git a/sanic/server.py b/sanic/server.py index a86da9fc..9340f374 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -14,7 +14,7 @@ except ImportError: from .log import log from .request import Request -from .exceptions import RequestTimeout, PayloadTooLarge +from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage class Signal: @@ -105,9 +105,9 @@ class HttpProtocol(asyncio.Protocol): # Parse request chunk or close connection try: self.parser.feed_data(data) - except HttpParserError as e: - self.bail_out( - "Invalid request data, connection closed ({})".format(e)) + except HttpParserError: + exception = InvalidUsage('Bad Request') + self.write_error(exception) def on_url(self, url): self.url = url diff --git a/tests/test_bad_request.py b/tests/test_bad_request.py new file mode 100644 index 00000000..095f4ab1 --- /dev/null +++ b/tests/test_bad_request.py @@ -0,0 +1,20 @@ +import asyncio +from sanic import Sanic + + +def test_bad_request_response(): + app = Sanic('test_bad_request_response') + lines = [] + async def _request(sanic, loop): + connect = asyncio.open_connection('127.0.0.1', 42101) + reader, writer = await connect + writer.write(b'not http') + while True: + line = await reader.readline() + if not line: + break + lines.append(line) + app.stop() + app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request) + assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n' + assert lines[-1] == b'Error: Bad Request' From c657c531b482039c18bcf926052fcc1f6c8377d0 Mon Sep 17 00:00:00 2001 From: 38elements Date: Fri, 23 Dec 2016 00:13:38 +0900 Subject: [PATCH 137/977] Customizable protocol --- sanic/sanic.py | 10 ++++++---- sanic/server.py | 9 +++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..08894871 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -12,7 +12,7 @@ from .exceptions import Handler from .log import log, logging from .response import HTTPResponse from .router import Router -from .server import serve +from .server import serve, HttpProtocol from .static import register as static_register from .exceptions import ServerError @@ -230,14 +230,15 @@ class Sanic: # Execution # -------------------------------------------------------------------- # - def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, - after_start=None, before_stop=None, after_stop=None, sock=None, - workers=1, loop=None): + def run(self, host="127.0.0.1", port=8000, protocol=HttpProtocol, + debug=False, before_start=None, after_start=None, before_stop=None, + after_stop=None, sock=None, workers=1, loop=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. :param host: Address to host on :param port: Port to host on + :param protocol: Subclass of asyncio.Protocol :param debug: Enables debug output (slows server) :param before_start: Function to be executed before the server starts accepting connections @@ -258,6 +259,7 @@ class Sanic: self.loop = loop server_settings = { + 'protocol': protocol, 'host': host, 'port': port, 'sock': sock, diff --git a/sanic/server.py b/sanic/server.py index 9340f374..cad957f0 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -221,12 +221,13 @@ def trigger_events(events, loop): loop.run_until_complete(result) -def serve(host, port, request_handler, error_handler, before_start=None, - after_start=None, before_stop=None, after_stop=None, - debug=False, request_timeout=60, sock=None, +def serve(protocol, host, port, request_handler, error_handler, + before_start=None, after_start=None, before_stop=None, + after_stop=None, debug=False, request_timeout=60, sock=None, request_max_size=None, reuse_port=False, loop=None): """ Starts asynchronous HTTP Server on an individual process. + :param protocol: subclass of asyncio.Protocol :param host: Address to host on :param port: Port to host on :param request_handler: Sanic request handler with middleware @@ -253,7 +254,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, connections = set() signal = Signal() server = partial( - HttpProtocol, + protocol, loop=loop, connections=connections, signal=signal, From 841125570095693a7fa8af5dbef3bfb62bf68dc5 Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Fri, 23 Dec 2016 11:08:04 +1100 Subject: [PATCH 138/977] Create documentation for testing server endpoints. Currently the sanic.utils functionality is undocumented. This provides information on the interface as well as a complete example of testing a server endpoint. --- README.md | 1 + docs/testing.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 docs/testing.md diff --git a/README.md b/README.md index 5aded700..e417b4a1 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ if __name__ == "__main__": * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) + * [Testing](docs/testing.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..79c719e8 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,51 @@ +# Testing + +Sanic endpoints can be tested locally using the `sanic.utils` module, which +depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/) +library. The `sanic_endpoint_test` function runs a local server, issues a +configurable request to an endpoint, and returns the result. It takes the +following arguments: + +- `app` An instance of a Sanic app. +- `method` *(default `'get'`)* A string representing the HTTP method to use. +- `uri` *(default `'/'`)* A string representing the endpoint to test. +- `gather_request` *(default `True`)* A boolean which determines whether the + original request will be returned by the function. If set to `True`, the + return value is a tuple of `(request, response)`, if `False` only the + response is returned. +- `loop` *(default `None`)* The event loop to use. +- `debug` *(default `False`)* A boolean which determines whether to run the + server in debug mode. + +The function further takes the `*request_args` and `**request_kwargs`, which +are passed directly to the aiohttp ClientSession request. For example, to +supply data with a GET request, `method` would be `get` and the keyword +argument `params={'value', 'key'}` would be supplied. More information about +the available arguments to aiohttp can be found +[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session). + +Below is a complete example of an endpoint test, +using [pytest](http://doc.pytest.org/en/latest/). The test checks that the +`/challenge` endpoint responds to a GET request with a supplied challenge +string. + +```python +import pytest +import aiohttp +from sanic.utils import sanic_endpoint_test + +# Import the Sanic app, usually created with Sanic(__name__) +from external_server import app + +def test_endpoint_challenge(): + # Create the challenge data + request_data = {'challenge': 'dummy_challenge'} + + # Send the request to the endpoint, using the default `get` method + request, response = sanic_endpoint_test(app, + uri='/challenge', + params=request_data) + + # Assert that the server responds with the challenge string + assert response.text == request_data['challenge'] +``` From 5c1ef2c1cfabcadbc7d4527d05d654e7ad23ab17 Mon Sep 17 00:00:00 2001 From: Romano Bodha Date: Fri, 23 Dec 2016 01:42:05 +0100 Subject: [PATCH 139/977] Fixed import error --- docs/class_based_views.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index 223304ae..ee410b1d 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -6,6 +6,7 @@ Sanic has simple class based implementation. You should implement methods(get, p ```python from sanic import Sanic from sanic.views import HTTPMethodView +from sanic.response import text app = Sanic('some_name') From a73a7d1e7b4d96d209d029db6d72d2818330bce9 Mon Sep 17 00:00:00 2001 From: Stefano Palazzo Date: Fri, 23 Dec 2016 11:42:00 +0100 Subject: [PATCH 140/977] 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 f091d82badc35116a45d59560f85cc6176980780 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 13:12:59 +0100 Subject: [PATCH 141/977] Improvement improvement: support fo binary data as a input. This do that the response process has more performance because not encoding needed. --- sanic/response.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 15130edd..1f86a807 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -79,7 +79,9 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body.encode('utf-8') + self.body = body + if type(body) is str: + self.body = body.encode('utf-8') else: self.body = body_bytes From 5afae986a0aeae6191a5532a11244cb7cc405f94 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 15:59:04 +0100 Subject: [PATCH 142/977] Apply response Middleware always Response middleware are useful to apply some post-process information, just before send to the user. For example: Add some HTTP headers (security headers, for example), remove "Server" banner (for security reasons) or cookie management. The change is very very simple: although an "request" middleware has produced any response, we'll even apply the response middlewares. --- sanic/sanic.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..f48b2bd5 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -193,18 +193,18 @@ class Sanic: if isawaitable(response): response = await response - # -------------------------------------------- # - # Response Middleware - # -------------------------------------------- # + # -------------------------------------------- # + # Response Middleware + # -------------------------------------------- # - if self.response_middleware: - for middleware in self.response_middleware: - _response = middleware(request, response) - if isawaitable(_response): - _response = await _response - if _response: - response = _response - break + if self.response_middleware: + for middleware in self.response_middleware: + _response = middleware(request, response) + if isawaitable(_response): + _response = await _response + if _response: + response = _response + break except Exception as e: # -------------------------------------------- # From 3add40625dc1b195c3129c34b88724e1a1869638 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 16:07:59 +0100 Subject: [PATCH 143/977] Explain how to chain two (or more) middlewares A funny and useful examples about how to chain middlewares. --- docs/middleware.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/middleware.md b/docs/middleware.md index 0b27443c..88d4e535 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -27,3 +27,23 @@ async def handler(request): app.run(host="0.0.0.0", port=8000) ``` + +## Middleware chain + +If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that **no return** any response in your middleware: + +```python +app = Sanic(__name__) + +@app.middleware('response') +async def custom_banner(request, response): + response.headers["Server"] = "Fake-Server" + +@app.middleware('response') +async def prevent_xss(request, response): + response.headers["x-xss-protection"] = "1; mode=block" + +app.run(host="0.0.0.0", port=8000) +``` + +The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks. From cd17a42234c6465f70795f78436e0589badc5a25 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 23 Dec 2016 09:59:28 -0800 Subject: [PATCH 144/977] Fix some verbage --- docs/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/middleware.md b/docs/middleware.md index 88d4e535..39930e3e 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -30,7 +30,7 @@ app.run(host="0.0.0.0", port=8000) ## Middleware chain -If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that **no return** any response in your middleware: +If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware: ```python app = Sanic(__name__) From 32ea45d403ddb1e1b143ba2fbae0675f32adbc50 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 22 Dec 2016 21:00:57 -0800 Subject: [PATCH 145/977] allow overriding logging.basicConfig --- examples/override_logging.py | 23 +++++++++++++++++++++++ sanic/log.py | 2 -- sanic/sanic.py | 11 +++++++++-- tests/test_logging.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 examples/override_logging.py create mode 100644 tests/test_logging.py diff --git a/examples/override_logging.py b/examples/override_logging.py new file mode 100644 index 00000000..25fd78de --- /dev/null +++ b/examples/override_logging.py @@ -0,0 +1,23 @@ +from sanic import Sanic +from sanic.response import text +import json +import logging + +logging_format = "[%(asctime)s] %(process)d-%(levelname)s " +logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " +logging_format += "%(message)s" + +logging.basicConfig( + format=logging_format, + level=logging.DEBUG +) +log = logging.getLogger() + +# Set logger to override default basicConfig +sanic = Sanic(logger=True) +@sanic.route("/") +def test(request): + log.info("received request; responding with 'hey'") + return text("hey") + +sanic.run(host="0.0.0.0", port=8000) diff --git a/sanic/log.py b/sanic/log.py index bd2e499e..3988bf12 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,5 +1,3 @@ import logging -logging.basicConfig( - level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") log = logging.getLogger(__name__) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..c79dca43 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -6,10 +6,11 @@ from multiprocessing import Process, Event from signal import signal, SIGTERM, SIGINT from time import sleep from traceback import format_exc +import logging from .config import Config from .exceptions import Handler -from .log import log, logging +from .log import log from .response import HTTPResponse from .router import Router from .server import serve @@ -18,7 +19,13 @@ from .exceptions import ServerError class Sanic: - def __init__(self, name=None, router=None, error_handler=None): + def __init__(self, name=None, router=None, + error_handler=None, logger=None): + if logger is None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s: %(levelname)s: %(message)s" + ) if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 00000000..65de28c2 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,33 @@ +import asyncio +from sanic.response import text +from sanic import Sanic +from io import StringIO +from sanic.utils import sanic_endpoint_test +import logging + +logging_format = '''module: %(module)s; \ +function: %(funcName)s(); \ +message: %(message)s''' + +def test_log(): + log_stream = StringIO() + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + logging.basicConfig( + format=logging_format, + level=logging.DEBUG, + stream=log_stream + ) + log = logging.getLogger() + app = Sanic('test_logging', logger=True) + @app.route('/') + def handler(request): + log.info('hello world') + return text('hello') + + request, response = sanic_endpoint_test(app) + log_text = log_stream.getvalue().strip().split('\n')[-3] + assert log_text == "module: test_logging; function: handler(); message: hello world" + +if __name__ =="__main__": + test_log() From 39211f8fbddb997f001f8ffc2059a0a0ad729125 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 24 Dec 2016 11:40:07 +0900 Subject: [PATCH 146/977] Refactor arguments of serve function --- sanic/server.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index cad957f0..74e834b4 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -221,26 +221,31 @@ def trigger_events(events, loop): loop.run_until_complete(result) -def serve(protocol, host, port, request_handler, error_handler, - before_start=None, after_start=None, before_stop=None, - after_stop=None, debug=False, request_timeout=60, sock=None, - request_max_size=None, reuse_port=False, loop=None): +def serve(host, port, request_handler, error_handler, before_start=None, + after_start=None, before_stop=None, after_stop=None, debug=False, + request_timeout=60, sock=None, request_max_size=None, + reuse_port=False, loop=None, protocol=HttpProtocol): """ Starts asynchronous HTTP Server on an individual process. - :param protocol: subclass of asyncio.Protocol :param host: Address to host on :param port: Port to host on :param request_handler: Sanic request handler with middleware + :param error_handler: Sanic error handler with middleware + :param before_start: Function to be executed before the server starts + listening. Takes single argument `loop` :param after_start: Function to be executed after the server starts listening. Takes single argument `loop` :param before_stop: Function to be executed when a stop signal is received before it is respected. Takes single argumenet `loop` + :param after_stop: Function to be executed when a stop signal is + received after it is respected. Takes single argumenet `loop` :param debug: Enables debug output (slows server) :param request_timeout: time in seconds :param sock: Socket for the server to accept connections from :param request_max_size: size in bytes, `None` for no limit :param reuse_port: `True` for multiple workers :param loop: asyncio compatible event loop + :param protocol: Subclass of asyncio.Protocol :return: Nothing """ loop = loop or async_loop.new_event_loop() From 2f0a582aa782f224e59911fa94d2f45c8c761e18 Mon Sep 17 00:00:00 2001 From: Konstantin Hantsov Date: Sat, 24 Dec 2016 10:28:34 +0100 Subject: [PATCH 147/977] Make golang performance test return JSON instead of string --- tests/performance/golang/golang.http.go | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/performance/golang/golang.http.go b/tests/performance/golang/golang.http.go index fb13cc8b..5aeedb61 100644 --- a/tests/performance/golang/golang.http.go +++ b/tests/performance/golang/golang.http.go @@ -1,16 +1,30 @@ package main import ( - "fmt" - "os" - "net/http" + "encoding/json" + "net/http" + "os" ) +type TestJSONResponse struct { + Test bool +} + func handler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) + response := TestJSONResponse{true} + + js, err := json.Marshal(response) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(js) } func main() { - http.HandleFunc("/", handler) - http.ListenAndServe(":" + os.Args[1], nil) + http.HandleFunc("/", handler) + http.ListenAndServe(":"+os.Args[1], nil) } From 2d05243c4a2a6d10a868cacddc103bbd25921d0b Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 24 Dec 2016 22:49:48 +0900 Subject: [PATCH 148/977] Refactor arguments of run function --- sanic/sanic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 08894871..87f12ac3 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -230,15 +230,14 @@ class Sanic: # Execution # -------------------------------------------------------------------- # - def run(self, host="127.0.0.1", port=8000, protocol=HttpProtocol, - debug=False, before_start=None, after_start=None, before_stop=None, - after_stop=None, sock=None, workers=1, loop=None): + def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, + after_start=None, before_stop=None, after_stop=None, sock=None, + workers=1, loop=None, protocol=HttpProtocol): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. :param host: Address to host on :param port: Port to host on - :param protocol: Subclass of asyncio.Protocol :param debug: Enables debug output (slows server) :param before_start: Function to be executed before the server starts accepting connections @@ -252,6 +251,7 @@ class Sanic: :param workers: Number of processes received before it is respected :param loop: asyncio compatible event loop + :param protocol: Subclass of asyncio.Protocol :return: Nothing """ self.error_handler.debug = True From cc982c5a61ef9523e3565b9d3d014f404d4141ec Mon Sep 17 00:00:00 2001 From: cr0hn Date: Sat, 24 Dec 2016 15:24:25 +0100 Subject: [PATCH 149/977] Update response.py Type check by isinstance --- sanic/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 1f86a807..c09c8dbd 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -80,7 +80,7 @@ class HTTPResponse: if body is not None: self.body = body - if type(body) is str: + if isinstance(body, str): self.body = body.encode('utf-8') else: self.body = body_bytes From 74f305cfb78bc23e2dd647d83e26d658f29d1e19 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 14:06:53 -0800 Subject: [PATCH 150/977] Adds python36 to tox.ini and .travis.yml --- .travis.yml | 1 + tox.ini | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 942a5df2..5e41a68e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - '3.5' + - '3.6' install: - pip install -r requirements.txt - pip install -r requirements-dev.txt diff --git a/tox.ini b/tox.ini index 258395ed..ecb7ca87 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,30 @@ [tox] -envlist = py35, report +envlist = py35, py36 [testenv] deps = aiohttp pytest - # pytest-cov coverage commands = - coverage run -m pytest tests {posargs} + coverage run -m pytest -v tests {posargs} mv .coverage .coverage.{envname} -basepython: - py35: python3.5 - whitelist_externals = coverage mv echo +[testenv:flake8] +deps = + flake8 + +commands = + flake8 sanic + [testenv:report] commands = @@ -29,6 +32,3 @@ commands = coverage report coverage html echo "Open file://{toxinidir}/coverage/index.html" - -basepython = - python3.5 \ No newline at end of file From c2622511ce94f4a46599c23e6540128c65daacaf Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 13 Dec 2016 12:20:16 -0800 Subject: [PATCH 151/977] Raise error if response is malformed. Issue #115 --- sanic/server.py | 6 ++++-- tests/test_requests.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 9340f374..11756005 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -6,6 +6,7 @@ from signal import SIGINT, SIGTERM from time import time from httptools import HttpRequestParser from httptools.parser.errors import HttpParserError +from .exceptions import ServerError try: import uvloop as async_loop @@ -173,8 +174,9 @@ class HttpProtocol(asyncio.Protocol): "Writing error failed, connection closed {}".format(e)) def bail_out(self, message): - log.debug(message) - self.transport.close() + exception = ServerError(message) + self.write_error(exception) + log.error(message) def cleanup(self): self.parser = None diff --git a/tests/test_requests.py b/tests/test_requests.py index 81895c8c..5895e3d5 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps from sanic import Sanic from sanic.response import json, text from sanic.utils import sanic_endpoint_test +from sanic.exceptions import ServerError # ------------------------------------------------------------ # @@ -32,6 +33,22 @@ def test_text(): assert response.text == 'Hello' +def test_invalid_response(): + app = Sanic('test_invalid_response') + + @app.exception(ServerError) + def handler_exception(request, exception): + return text('Internal Server Error.', 500) + + @app.route('/') + async def handler(request): + return 'This should fail' + + request, response = sanic_endpoint_test(app) + assert response.status == 500 + assert response.text == "Internal Server Error." + + def test_json(): app = Sanic('test_json') From 29f3c22fede7716cdebd06b8f4f44c48dfb0814e Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:11:12 -0800 Subject: [PATCH 152/977] Rework conditionals to not be inline --- sanic/static.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sanic/static.py b/sanic/static.py index a70bff2f..e39dd76f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -33,12 +33,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # served. os.path.realpath seems to be very slow if file_uri and '../' in file_uri: raise InvalidUsage("Invalid URL") - + # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path - file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ - if file_uri else file_or_directory + file_path = file_or_directory + if file_uri: + file_path = path.join( + file_or_directory, sub('^[/]*', '', file_uri)) # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) From 16182472fa73b6b5035ce4904bbb3edf3e1bf8a8 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:11:46 -0800 Subject: [PATCH 153/977] Remove trailing whitespace --- sanic/static.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index e39dd76f..9f5f2d52 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -33,7 +33,6 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # served. os.path.realpath seems to be very slow if file_uri and '../' in file_uri: raise InvalidUsage("Invalid URL") - # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path From 8be849cc40dc5f8f55536c462f9dff0c657f6a0f Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:16:19 -0800 Subject: [PATCH 154/977] Rewrite static files tests Relates to PR #188 Changes include: - Rewriting to work with pytest fixtures and an actual static directory - Addition of a test that covers file paths that must be unquoted as a uri --- tests/static/decode me.txt | 1 + tests/static/test.file | 1 + tests/test_static.py | 60 +++++++++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 tests/static/decode me.txt create mode 100644 tests/static/test.file diff --git a/tests/static/decode me.txt b/tests/static/decode me.txt new file mode 100644 index 00000000..e5d05ac1 --- /dev/null +++ b/tests/static/decode me.txt @@ -0,0 +1 @@ +I need to be decoded as a uri diff --git a/tests/static/test.file b/tests/static/test.file new file mode 100644 index 00000000..0725a6ef --- /dev/null +++ b/tests/static/test.file @@ -0,0 +1 @@ +I am just a regular static file diff --git a/tests/test_static.py b/tests/test_static.py index 6dafac2b..82b0d1f9 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -1,30 +1,62 @@ import inspect import os +import pytest + from sanic import Sanic from sanic.utils import sanic_endpoint_test -def test_static_file(): - current_file = inspect.getfile(inspect.currentframe()) - with open(current_file, 'rb') as file: - current_file_contents = file.read() +@pytest.fixture(scope='module') +def static_file_directory(): + """The static directory to serve""" + current_file = inspect.getfile(inspect.currentframe()) + current_directory = os.path.dirname(os.path.abspath(current_file)) + static_directory = os.path.join(current_directory, 'static') + 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') + + +@pytest.fixture(scope='module') +def static_file_content(static_file_path): + """The content of the static file to check""" + with open(static_file_path, 'rb') as file: + return file.read() + + +def test_static_file(static_file_path, static_file_content): app = Sanic('test_static') - app.static('/testing.file', current_file) + app.static('/testing.file', static_file_path) request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 - assert response.body == current_file_contents + assert response.body == static_file_content -def test_static_directory(): - current_file = inspect.getfile(inspect.currentframe()) - current_directory = os.path.dirname(os.path.abspath(current_file)) - with open(current_file, 'rb') as file: - current_file_contents = file.read() + +def test_static_directory( + static_file_directory, static_file_path, static_file_content): app = Sanic('test_static') - app.static('/dir', current_directory) + app.static('/dir', static_file_directory) - request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') + request, response = sanic_endpoint_test(app, uri='/dir/test.file') assert response.status == 200 - assert response.body == current_file_contents \ No newline at end of file + 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 From d7e94473f3c350a1c3ec49e79f3a7e6c711b8842 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:37:55 -0800 Subject: [PATCH 155/977] Use a try/except, it's a bit faster Also reorder some imports and add some comments --- sanic/response.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index c09c8dbd..2c4c7f27 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,9 +1,11 @@ from aiofiles import open as open_async -from .cookies import CookieJar from mimetypes import guess_type from os import path + from ujson import dumps as json_dumps +from .cookies import CookieJar + COMMON_STATUS_CODES = { 200: b'OK', 400: b'Bad Request', @@ -79,9 +81,12 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body - if isinstance(body, str): + try: + # Try to encode it regularly self.body = body.encode('utf-8') + except AttributeError: + # Convert it to a str if you can't + self.body = str(body).encode('utf-8') else: self.body = body_bytes From f1f38c24da6d845701801c6f55ba9e5f24fd6acb Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:47:15 -0800 Subject: [PATCH 156/977] Add test for PR: #215 --- tests/test_response.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_response.py diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 00000000..f35f10e9 --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,18 @@ +from random import choice + +from sanic import Sanic +from sanic.response import HTTPResponse +from sanic.utils import sanic_endpoint_test + + +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') + random_num = choice(range(1000)) + + @app.route('/hello') + async def hello_route(request): + return HTTPResponse(body=random_num) + + request, response = sanic_endpoint_test(app, uri='/hello') + assert response.text == str(random_num) From cf7616ebe5216007699cb264667079a3c739e29a Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:51:16 -0800 Subject: [PATCH 157/977] Increment version to 0.1.9 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 6e7f8d23..6b9b3a80 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.8' +__version__ = '0.1.9' __all__ = ['Sanic', 'Blueprint'] From 7e6c92dc52525cdf30babc9f40125cddafab8b43 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 13 Dec 2016 21:24:26 -0800 Subject: [PATCH 158/977] convert header values to strings --- sanic/response.py | 4 +++- tests/test_requests.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 15130edd..f4e24e99 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,5 +1,5 @@ from aiofiles import open as open_async -from .cookies import CookieJar +from .cookies import CookieJar, Cookie from mimetypes import guess_type from os import path from ujson import dumps as json_dumps @@ -97,6 +97,8 @@ class HTTPResponse: headers = b'' if self.headers: headers = b''.join( + b'%b: %b\r\n' % (name.encode(), str(value).encode('utf-8')) + if type(value) != Cookie else b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) for name, value in self.headers.items() ) diff --git a/tests/test_requests.py b/tests/test_requests.py index 81895c8c..a8ab26ad 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -32,6 +32,32 @@ def test_text(): assert response.text == 'Hello' +def test_headers(): + app = Sanic('test_text') + + @app.route('/') + async def handler(request): + headers = {"spam": "great"} + return text('Hello', headers=headers) + + request, response = sanic_endpoint_test(app) + + assert response.headers.get('spam') == 'great' + + +def test_invalid_headers(): + app = Sanic('test_text') + + @app.route('/') + async def handler(request): + headers = {"answer": 42} + return text('Hello', headers=headers) + + request, response = sanic_endpoint_test(app) + + assert response.headers.get('answer') == '42' + + def test_json(): app = Sanic('test_json') From 00b5a496dd8d6ddc13632aa357b25c525001786a Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 17 Dec 2016 21:15:20 -0800 Subject: [PATCH 159/977] type -> isinstance --- sanic/response.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index f4e24e99..d5b2beae 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -97,9 +97,10 @@ class HTTPResponse: headers = b'' if self.headers: headers = b''.join( - b'%b: %b\r\n' % (name.encode(), str(value).encode('utf-8')) - if type(value) != Cookie else b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) + if isinstance(value, str) or isinstance(value, Cookie) + else b'%b: %b\r\n' % (name.encode(), + str(value).encode('utf-8')) for name, value in self.headers.items() ) From 7d7cbaacf1505a02b2e2a652cb32f212b3d3a64b Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 17 Dec 2016 21:32:48 -0800 Subject: [PATCH 160/977] header format function --- sanic/response.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index d5b2beae..21add8f6 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -94,13 +94,15 @@ class HTTPResponse: if keep_alive and keep_alive_timeout: timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout + format_headers = lambda name, value: b'%b: %b\r\n' %\ + (name.encode(), value.encode('utf-8')) + headers = b'' if self.headers: headers = b''.join( - b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) + format_headers(name, value) if isinstance(value, str) or isinstance(value, Cookie) - else b'%b: %b\r\n' % (name.encode(), - str(value).encode('utf-8')) + else format_headers(name, str(value)) for name, value in self.headers.items() ) From a03f216f42f36cb7acdaca7259d031e4aa10f21a Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Sun, 25 Dec 2016 00:47:51 -0500 Subject: [PATCH 161/977] Added additional docstrings to blueprints.py --- sanic/blueprints.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 92e376f1..af5ab337 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -3,6 +3,7 @@ from collections import defaultdict class BlueprintSetup: """ + Creates a blueprint state like object. """ def __init__(self, blueprint, app, options): @@ -29,13 +30,13 @@ class BlueprintSetup: def add_exception(self, handler, *args, **kwargs): """ - Registers exceptions to sanic + Registers exceptions to sanic. """ self.app.exception(*args, **kwargs)(handler) def add_static(self, uri, file_or_directory, *args, **kwargs): """ - Registers static files to sanic + Registers static files to sanic. """ if self.url_prefix: uri = self.url_prefix + uri @@ -44,7 +45,7 @@ class BlueprintSetup: def add_middleware(self, middleware, *args, **kwargs): """ - Registers middleware to sanic + Registers middleware to sanic. """ if args or kwargs: self.app.middleware(*args, **kwargs)(middleware) @@ -73,11 +74,13 @@ class Blueprint: def make_setup_state(self, app, options): """ + Returns a new BlueprintSetup object """ return BlueprintSetup(self, app, options) def register(self, app, options): """ + Registers the blueprint to the sanic app. """ state = self.make_setup_state(app, options) for deferred in self.deferred_functions: @@ -85,6 +88,9 @@ class Blueprint: def route(self, uri, methods=None): """ + Creates a blueprint route from a decorated function. + :param uri: Endpoint at which the route will be accessible. + :param methods: List of acceptable HTTP methods. """ def decorator(handler): self.record(lambda s: s.add_route(handler, uri, methods)) @@ -93,12 +99,18 @@ class Blueprint: def add_route(self, handler, uri, methods=None): """ + Creates a blueprint route from a function. + :param handler: Function to handle uri request. + :param uri: Endpoint at which the route will be accessible. + :param methods: List of acceptable HTTP methods. """ self.record(lambda s: s.add_route(handler, uri, methods)) return handler def listener(self, event): """ + Create a listener from a decorated function. + :param event: Event to listen to. """ def decorator(listener): self.listeners[event].append(listener) @@ -107,6 +119,7 @@ class Blueprint: def middleware(self, *args, **kwargs): """ + Creates a blueprint middleware from a decorated function. """ def register_middleware(middleware): self.record( @@ -123,6 +136,7 @@ class Blueprint: def exception(self, *args, **kwargs): """ + Creates a blueprint exception from a decorated function. """ def decorator(handler): self.record(lambda s: s.add_exception(handler, *args, **kwargs)) @@ -131,6 +145,9 @@ class Blueprint: def static(self, uri, file_or_directory, *args, **kwargs): """ + Creates a blueprint static route from a decorated function. + :param uri: Endpoint at which the route will be accessible. + :param file_or_directory: Static asset. """ self.record( lambda s: s.add_static(uri, file_or_directory, *args, **kwargs)) From 2b10860c32a6a96dbb66199027480068b0d9b49a Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Sun, 25 Dec 2016 01:05:26 -0500 Subject: [PATCH 162/977] Added docstrings to sanic.response.py for issue 41 --- sanic/response.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sanic/response.py b/sanic/response.py index 2c4c7f27..5031b1c8 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -139,21 +139,45 @@ class HTTPResponse: def json(body, status=200, headers=None): + """ + Returns serialized python object to json format. + :param body: Response data to be serialized. + :param status: Response code. + :param headers: Custom Headers. + """ return HTTPResponse(json_dumps(body), headers=headers, status=status, content_type="application/json") def text(body, status=200, headers=None): + """ + Returns body in text format. + :param body: Response data to be encoded. + :param status: Response code. + :param headers: Custom Headers. + """ return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8") def html(body, status=200, headers=None): + """ + Returns body in html format. + :param body: Response data to be encoded. + :param status: Response code. + :param headers: Custom Headers. + """ return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8") async def file(location, mime_type=None, headers=None): + """ + Returns file with mime_type. + :param location: Location of file on system. + :param mime_type: Specific mime_type. + :param headers: Custom Headers. + """ filename = path.split(location)[-1] async with open_async(location, mode='rb') as _file: From a486fb99a9cdc381ac39c0ae9f733432aa0af837 Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Sun, 25 Dec 2016 01:06:40 -0500 Subject: [PATCH 163/977] Updated json function docstrings to be more consistent. --- sanic/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 5031b1c8..1bae348d 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -140,7 +140,7 @@ class HTTPResponse: def json(body, status=200, headers=None): """ - Returns serialized python object to json format. + Returns body in json format. :param body: Response data to be serialized. :param status: Response code. :param headers: Custom Headers. From d5ad5e46da53bb47a204585929178892aa162053 Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Sun, 25 Dec 2016 01:24:17 -0500 Subject: [PATCH 164/977] Update response docstrings to be explicit on whats returned. --- sanic/response.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 1bae348d..407ec9f2 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -140,7 +140,7 @@ class HTTPResponse: def json(body, status=200, headers=None): """ - Returns body in json format. + Returns response object with body in json format. :param body: Response data to be serialized. :param status: Response code. :param headers: Custom Headers. @@ -151,7 +151,7 @@ def json(body, status=200, headers=None): def text(body, status=200, headers=None): """ - Returns body in text format. + Returns response object with body in text format. :param body: Response data to be encoded. :param status: Response code. :param headers: Custom Headers. @@ -162,7 +162,7 @@ def text(body, status=200, headers=None): def html(body, status=200, headers=None): """ - Returns body in html format. + Returns response object with body in html format. :param body: Response data to be encoded. :param status: Response code. :param headers: Custom Headers. @@ -173,7 +173,7 @@ def html(body, status=200, headers=None): async def file(location, mime_type=None, headers=None): """ - Returns file with mime_type. + Returns response object with file data. :param location: Location of file on system. :param mime_type: Specific mime_type. :param headers: Custom Headers. From be9eca2d63845df1dab69133b1db88f264f93ac9 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 24 Dec 2016 20:56:07 -0800 Subject: [PATCH 165/977] use try/except --- sanic/response.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 21add8f6..1202b101 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -87,6 +87,15 @@ class HTTPResponse: self.headers = headers or {} self._cookies = None + @staticmethod + def format_header(name, value): + try: + return b'%b: %b\r\n' %\ + (name.encode(), value.encode('utf-8')) + except: + return b'%b: %b\r\n' %\ + (name.encode(), str(value).encode('utf-8')) + def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python @@ -94,15 +103,10 @@ class HTTPResponse: if keep_alive and keep_alive_timeout: timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout - format_headers = lambda name, value: b'%b: %b\r\n' %\ - (name.encode(), value.encode('utf-8')) - headers = b'' if self.headers: headers = b''.join( - format_headers(name, value) - if isinstance(value, str) or isinstance(value, Cookie) - else format_headers(name, str(value)) + self.format_header(name, value) for name, value in self.headers.items() ) From 7654c2f90209a6e4cad4bd8bac6fd9a428c9b41f Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Sun, 25 Dec 2016 20:20:48 +1100 Subject: [PATCH 166/977] Use Sphinx for documentation. This commit creates configuration files and an index page for documentation using Sphinx. The recommonmark package is used to enable Markdown support for Sphinx, using the Common Mark specification. This means that the current documentation doesn't need to be rewritten. --- .gitignore | 3 + docs/conf.py | 155 +++++++++++++++++++++++++++++++++++++++++++ docs/contributing.md | 17 ++++- docs/index.rst | 90 +++++++++++++++++++++++++ requirements-dev.txt | 2 + 5 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 docs/conf.py create mode 100644 docs/index.rst diff --git a/.gitignore b/.gitignore index d7872c5c..535c6a2f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ settings.py *.pyc .idea/* .cache/* +docs/_build/ +docs/sanic.rst +docs/modules.rst \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..caf79901 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Sanic documentation build configuration file, created by +# sphinx-quickstart on Sun Dec 25 18:07:21 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. + +import os +import sys + +# Add support for Markdown documentation using Recommonmark +from recommonmark.parser import CommonMarkParser + +# Ensure that sanic is present in the path, to allow sphinx-apidoc to +# autogenerate documentation from docstrings +root_directory = os.path.dirname(os.getcwd()) +sys.path.insert(0, root_directory) + +import sanic + +# -- General configuration ------------------------------------------------ + +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages'] + +templates_path = ['_templates'] + +# Enable support for both Restructured Text and Markdown +source_parsers = {'.md': CommonMarkParser} +source_suffix = ['.rst', '.md'] + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Sanic' +copyright = '2016, Sanic contributors' +author = 'Sanic contributors' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = sanic.__version__ +# The full version, including alpha/beta/rc tags. +release = sanic.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +# +# modules.rst is generated by sphinx-apidoc but is unused. This suppresses +# a warning about it. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'modules.rst'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Sanicdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Sanic.tex', 'Sanic Documentation', + 'Sanic contributors', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'sanic', 'Sanic Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Sanic', 'Sanic Documentation', + author, 'Sanic', 'One line description of project.', + 'Miscellaneous'), +] + + + +# -- Options for Epub output ---------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + diff --git a/docs/contributing.md b/docs/contributing.md index e39a7247..c046bd39 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -6,5 +6,20 @@ Thank you for your interest! * `python -m pip install pytest` * `python -m pytest tests` +## Documentation + +Sanic's documentation is built using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in Markdown and can be found in the `docs` folder, while the module reference is automatically generated using `sphinx-apidoc`. + +To generate the documentation from scratch: + +```bash +rm -f docs/sanic.rst +rm -f docs/modules.rst +sphinx-apidoc -o docs/ sanic +sphinx-build -b html docs docs/_build +``` + +The HTML documentation will be created in the `docs/_build` folder. + ## Warning -One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged. \ No newline at end of file +One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..1e815582 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,90 @@ +Welcome to Sanic's documentation! +================================= + +Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article `_. + +On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. + +Sanic is developed `on GitHub `_. Contributions are welcome! + +Benchmarks +---------- + +All tests were run on an AWS medium instance running ubuntu, using 1 +process. Each script delivered a small JSON response and was tested with +wrk using 100 connections. Pypy was tested for Falcon and Flask but did +not speed up requests. + ++-----------+-----------------------+----------------+---------------+ +| Server | Implementation | Requests/sec | Avg Latency | ++===========+=======================+================+===============+ +| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms | ++-----------+-----------------------+----------------+---------------+ +| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms | ++-----------+-----------------------+----------------+---------------+ +| Falcon | gunicorn + meinheld | 18,972 | 5.27ms | ++-----------+-----------------------+----------------+---------------+ +| Bottle | gunicorn + meinheld | 13,596 | 7.36ms | ++-----------+-----------------------+----------------+---------------+ +| Flask | gunicorn + meinheld | 4,988 | 20.08ms | ++-----------+-----------------------+----------------+---------------+ +| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms | ++-----------+-----------------------+----------------+---------------+ +| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms | ++-----------+-----------------------+----------------+---------------+ +| Tornado | Python 3.5 | 2,138 | 46.66ms | ++-----------+-----------------------+----------------+---------------+ + +Hello World Example +------------------- + +.. code:: python + + from sanic import Sanic + from sanic.response import json + + + app = Sanic() + + + @app.route("/") + async def test(request): + return json({"hello": "world"}) + + if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) + +Installation +------------ + +- ``python -m pip install sanic`` + +Guides +====== + +.. toctree:: + :maxdepth: 2 + + getting_started + request_data + routing + middleware + exceptions + blueprints + class_based_views + cookies + static_files + testing + deploying + contributing + + +Module Documentation +==================== + +.. toctree:: + + Module Reference + +* :ref:`genindex` +* :ref:`search` diff --git a/requirements-dev.txt b/requirements-dev.txt index 1c34d695..780c8677 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,3 +12,5 @@ kyoukai falcon tornado aiofiles +sphinx +recommonmark From 52c59e71333ac81b0afbee6d92973f85be7d71c9 Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Sun, 25 Dec 2016 20:43:45 +1100 Subject: [PATCH 167/977] Fix all docstring errors. When generating documentation with sphinx-apidoc, there are currently many docstring errors, mostly minor. This commit fixes all errors. --- sanic/exceptions.py | 1 + sanic/request.py | 1 + sanic/router.py | 14 ++++++++++++-- sanic/sanic.py | 21 ++++++++++++++------- sanic/server.py | 7 +++++-- sanic/static.py | 4 +++- sanic/views.py | 10 +++++++--- 7 files changed, 43 insertions(+), 15 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 369a87a2..68051ebe 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -51,6 +51,7 @@ class Handler: def response(self, request, exception): """ Fetches and executes an exception handler and returns a response object + :param request: Request :param exception: Exception to handle :return: Response object diff --git a/sanic/request.py b/sanic/request.py index 62d89781..adbb1e0d 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -132,6 +132,7 @@ File = namedtuple('File', ['type', 'body', 'name']) def parse_multipart_form(body, boundary): """ Parses a request body and returns fields and files + :param body: Bytes request body :param boundary: Bytes multipart boundary :return: fields (RequestParameters), files (RequestParameters) diff --git a/sanic/router.py b/sanic/router.py index 4cc1f073..57d92dd5 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -26,11 +26,19 @@ class RouteExists(Exception): class Router: """ Router supports basic routing with parameters and method checks + Usage: + + .. code-block:: python + @sanic.route('/my/url/', methods=['GET', 'POST', ...]) def my_route(request, my_parameter): do stuff... + or + + .. code-block:: python + @sanic.route('/my/url/:type', methods['GET', 'POST', ...]) def my_route_with_type(request, my_parameter): do stuff... @@ -55,11 +63,12 @@ class Router: def add(self, uri, methods, handler): """ Adds a handler to the route list + :param uri: Path to match :param methods: Array of accepted method names. - If none are provided, any method is allowed + If none are provided, any method is allowed :param handler: Request handler function. - When executed, it should provide a response object. + When executed, it should provide a response object. :return: Nothing """ if uri in self.routes_all: @@ -113,6 +122,7 @@ class Router: """ Gets a request handler based on the URL of the request, or raises an error + :param request: Request object :return: handler, arguments, keyword arguments """ diff --git a/sanic/sanic.py b/sanic/sanic.py index f48b2bd5..67132f45 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -44,6 +44,7 @@ class Sanic: def route(self, uri, methods=None): """ Decorates a function to be registered as a route + :param uri: path of the URL :param methods: list or tuple of methods allowed :return: decorated function @@ -65,6 +66,7 @@ class Sanic: A helper method to register class instance or functions as a handler to the application url routes. + :param handler: function or class instance :param uri: path of the URL :param methods: list or tuple of methods allowed @@ -77,7 +79,8 @@ class Sanic: def exception(self, *exceptions): """ Decorates a function to be registered as a handler for exceptions - :param *exceptions: exceptions + + :param \*exceptions: exceptions :return: decorated function """ @@ -123,6 +126,7 @@ class Sanic: def blueprint(self, blueprint, **options): """ Registers a blueprint on the application. + :param blueprint: Blueprint object :param options: option dictionary with blueprint defaults :return: Nothing @@ -155,9 +159,10 @@ class Sanic: Takes a request from the HTTP Server and returns a response object to be sent back The HTTP Server only expects a response object, so exception handling must be done here + :param request: HTTP Request object :param response_callback: Response function to be called with the - response as the only argument + response as the only argument :return: Nothing """ try: @@ -236,20 +241,21 @@ class Sanic: """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. + :param host: Address to host on :param port: Port to host on :param debug: Enables debug output (slows server) :param before_start: Function to be executed before the server starts - accepting connections + accepting connections :param after_start: Function to be executed after the server starts - accepting connections + accepting connections :param before_stop: Function to be executed when a stop signal is - received before it is respected + received before it is respected :param after_stop: Function to be executed when all requests are - complete + complete :param sock: Socket for the server to accept connections from :param workers: Number of processes - received before it is respected + received before it is respected :param loop: asyncio compatible event loop :return: Nothing """ @@ -324,6 +330,7 @@ class Sanic: """ Starts multiple server processes simultaneously. Stops on interrupt and terminate signals, and drains connections when complete. + :param server_settings: kw arguments to be passed to the serve function :param workers: number of workers to launch :param stop_event: if provided, is used as a stop signal diff --git a/sanic/server.py b/sanic/server.py index 11756005..725104f0 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -201,6 +201,7 @@ def update_current_time(loop): """ Caches the current time, since it is needed at the end of every keep-alive request to update the request timeout time + :param loop: :return: """ @@ -229,13 +230,15 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_max_size=None, reuse_port=False, loop=None): """ Starts asynchronous HTTP Server on an individual process. + :param host: Address to host on :param port: Port to host on :param request_handler: Sanic request handler with middleware :param after_start: Function to be executed after the server starts - listening. Takes single argument `loop` + listening. Takes single argument `loop` :param before_stop: Function to be executed when a stop signal is - received before it is respected. Takes single argumenet `loop` + received before it is respected. Takes single + argumenet `loop` :param debug: Enables debug output (slows server) :param request_timeout: time in seconds :param sock: Socket for the server to accept connections from diff --git a/sanic/static.py b/sanic/static.py index 9f5f2d52..1d0bff0f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -15,12 +15,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): """ Registers a static directory handler with Sanic by adding a route to the router and registering a handler. + :param app: Sanic :param file_or_directory: File or directory path to serve from :param uri: URL to serve from :param pattern: regular expression used to match files in the URL :param use_modified_since: If true, send file modified time, and return - not modified if the browser's matches the server's + not modified if the browser's matches the + server's """ # If we're not trying to match a file directly, diff --git a/sanic/views.py b/sanic/views.py index 9387bcf6..9509b5ee 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -7,21 +7,25 @@ class HTTPMethodView: to every HTTP method you want to support. For example: - class DummyView(View): + .. code-block:: python + + class DummyView(View): def get(self, request, *args, **kwargs): return text('I am get method') - def put(self, request, *args, **kwargs): return text('I am put method') + etc. If someone tries to use a non-implemented method, there will be a 405 response. If you need any url params just mention them in method definition: - class DummyView(View): + .. code-block:: python + + class DummyView(View): def get(self, request, my_param_here, *args, **kwargs): return text('I am get method with %s' % my_param_here) From 56d6c2a92910b160a3519c0402721323a21218dc Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 25 Dec 2016 18:55:25 -0800 Subject: [PATCH 168/977] Change travis job to use tox --- .travis.yml | 10 ++-------- tox.ini | 4 +++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e41a68e..a215a57b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,14 +2,8 @@ language: python python: - '3.5' - '3.6' -install: - - pip install -r requirements.txt - - pip install -r requirements-dev.txt - - python setup.py install - - pip install flake8 - - pip install pytest -before_script: flake8 sanic -script: py.test -v tests +install: pip install tox-travis +script: tox deploy: provider: pypi user: channelcat diff --git a/tox.ini b/tox.ini index ecb7ca87..222e17c6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist = py35, py36 +envlist = py35, py36, flake8, report [testenv] @@ -26,6 +26,8 @@ commands = flake8 sanic [testenv:report] +deps= + coverage commands = coverage combine From 15578547553254728b9cd4f9fd46d92ab4d0ffe0 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 25 Dec 2016 19:05:11 -0800 Subject: [PATCH 169/977] Update to make flake8 actually work --- .travis.yml | 1 + tox.ini | 27 ++++++++------------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index a215a57b..1b31c4f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: python python: - '3.5' diff --git a/tox.ini b/tox.ini index 222e17c6..a2f89206 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,21 @@ [tox] -envlist = py35, py36, flake8, report +envlist = py35, py36, flake8 + +[travis] + +python = + 3.5: py35, flake8 + 3.6: py36, flake8 [testenv] deps = aiohttp pytest - coverage commands = - coverage run -m pytest -v tests {posargs} - mv .coverage .coverage.{envname} - -whitelist_externals = - coverage - mv - echo + pytest tests {posargs} [testenv:flake8] deps = @@ -24,13 +23,3 @@ deps = commands = flake8 sanic - -[testenv:report] -deps= - coverage - -commands = - coverage combine - coverage report - coverage html - echo "Open file://{toxinidir}/coverage/index.html" From 01b42fb39946d6ad93ce622fba14ef81a5d9492d Mon Sep 17 00:00:00 2001 From: Hyunjun Kim Date: Mon, 26 Dec 2016 20:37:16 +0900 Subject: [PATCH 170/977] Allow Sanic-inherited application --- sanic/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__main__.py b/sanic/__main__.py index 8bede98f..8653cd55 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -20,7 +20,7 @@ if __name__ == "__main__": module = import_module(module_name) app = getattr(module, app_name, None) - if type(app) is not Sanic: + if not isinstance(app, Sanic): raise ValueError("Module is not a Sanic app, it is a {}. " "Perhaps you meant {}.app?" .format(type(app).__name__, args.module)) From 986b0aa106702ffc015044b55746b8eb7d86ef9e Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Mon, 26 Dec 2016 06:41:41 -0500 Subject: [PATCH 171/977] Added token property to request object. --- sanic/request.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index 62d89781..4309bbed 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -72,6 +72,17 @@ class Request(dict): return self.parsed_json + @property + def token(self): + """ + Attempts to return the auth header token. + :return: token related to request + """ + auth_header = self.headers.get('Authorization') + if auth_header is not None: + return auth_header.split()[1] + return auth_header + @property def form(self): if self.parsed_form is None: From 548458c3e0e8201434dd9160cbfb4b151010eaf4 Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Mon, 26 Dec 2016 06:48:53 -0500 Subject: [PATCH 172/977] Added test for new token property on request object. --- tests/test_requests.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_requests.py b/tests/test_requests.py index 5895e3d5..4f81b9a0 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -92,6 +92,24 @@ def test_query_string(): assert request.args.get('test2') == 'false' +def test_token(): + app = Sanic('test_post_token') + + @app.route('/') + async def handler(request): + return text('OK') + + # uuid4 generated token. + token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' + headers = { + 'content-type': 'application/json', + 'Authorization': 'Token {}'.format(token) + } + + request, response = sanic_endpoint_test(app, headers=headers) + + assert request.token == token + # ------------------------------------------------------------ # # POST # ------------------------------------------------------------ # From ac44900fc40f3296ba858eaf371a1213339f2db2 Mon Sep 17 00:00:00 2001 From: 38elements Date: Mon, 26 Dec 2016 23:41:10 +0900 Subject: [PATCH 173/977] Add test and example for custom protocol --- examples/custom_protocol.py | 24 ++++++++++++++++++++++++ sanic/utils.py | 6 +++--- tests/test_custom_protocol.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 examples/custom_protocol.py create mode 100644 tests/test_custom_protocol.py diff --git a/examples/custom_protocol.py b/examples/custom_protocol.py new file mode 100644 index 00000000..b5e20ee6 --- /dev/null +++ b/examples/custom_protocol.py @@ -0,0 +1,24 @@ +from sanic import Sanic +from sanic.server import HttpProtocol +from sanic.response import text + +app = Sanic(__name__) + + +class CustomHttpProtocol(HttpProtocol): + + def write_response(self, response): + if isinstance(response, str): + response = text(response) + self.transport.write( + response.output(self.request.version) + ) + self.transport.close() + + +@app.route("/") +async def test(request): + return 'Hello, world!' + + +app.run(host="0.0.0.0", port=8000, protocol=CustomHttpProtocol) diff --git a/sanic/utils.py b/sanic/utils.py index 88444b3c..9f4a97d8 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,8 +16,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, - loop=None, debug=False, *request_args, - **request_kwargs): + loop=None, debug=False, server_kwargs={}, + *request_args, **request_kwargs): results = [] exceptions = [] @@ -36,7 +36,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, app.stop() app.run(host=HOST, debug=debug, port=42101, - after_start=_collect_response, loop=loop) + after_start=_collect_response, loop=loop, **server_kwargs) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/tests/test_custom_protocol.py b/tests/test_custom_protocol.py new file mode 100644 index 00000000..88202428 --- /dev/null +++ b/tests/test_custom_protocol.py @@ -0,0 +1,32 @@ +from sanic import Sanic +from sanic.server import HttpProtocol +from sanic.response import text +from sanic.utils import sanic_endpoint_test + +app = Sanic('test_custom_porotocol') + + +class CustomHttpProtocol(HttpProtocol): + + def write_response(self, response): + if isinstance(response, str): + response = text(response) + self.transport.write( + response.output(self.request.version) + ) + self.transport.close() + + +@app.route('/1') +async def handler_1(request): + return 'OK' + + +def test_use_custom_protocol(): + server_kwargs = { + 'protocol': CustomHttpProtocol + } + request, response = sanic_endpoint_test(app, uri='/1', + server_kwargs=server_kwargs) + assert response.status == 200 + assert response.text == 'OK' From 39b279f0f2aa20b90e15cb569535207341f303eb Mon Sep 17 00:00:00 2001 From: 38elements Date: Mon, 26 Dec 2016 23:54:59 +0900 Subject: [PATCH 174/977] Improve examples/custom_protocol.py --- examples/custom_protocol.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/custom_protocol.py b/examples/custom_protocol.py index b5e20ee6..d1df8fde 100644 --- a/examples/custom_protocol.py +++ b/examples/custom_protocol.py @@ -16,9 +16,13 @@ class CustomHttpProtocol(HttpProtocol): self.transport.close() -@app.route("/") -async def test(request): - return 'Hello, world!' +@app.route('/') +async def string(request): + return 'string' -app.run(host="0.0.0.0", port=8000, protocol=CustomHttpProtocol) +@app.route('/1') +async def response(request): + return text('response') + +app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) From a4f77984b79e52441c07557e05ccc86cd2e82727 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Dec 2016 14:37:05 -0800 Subject: [PATCH 175/977] stop multiple worker server without sleep loop; issue #73 --- sanic/sanic.py | 5 ++--- tests/test_multiprocessing.py | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index ecf5b652..033b1e9d 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -3,8 +3,8 @@ from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename from multiprocessing import Process, Event +from select import select from signal import signal, SIGTERM, SIGINT -from time import sleep from traceback import format_exc import logging @@ -352,8 +352,7 @@ class Sanic: # Infinitely wait for the stop event try: - while not stop_event.is_set(): - sleep(0.3) + select(stop_event) except: pass diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 545ecee7..cc967ef1 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,5 +1,5 @@ from multiprocessing import Array, Event, Process -from time import sleep +from time import sleep, time from ujson import loads as json_loads from sanic import Sanic @@ -51,3 +51,27 @@ def skip_test_multiprocessing(): raise ValueError("Expected JSON response but got '{}'".format(response)) assert results.get('test') == True + + +def test_drain_connections(): + app = Sanic('test_json') + + @app.route('/') + async def handler(request): + return json({"test": True}) + + stop_event = Event() + async def after_start(*args, **kwargs): + http_response = await local_request('get', '/') + stop_event.set() + + start = time() + app.serve_multiple({ + 'host': HOST, + 'port': PORT, + 'after_start': after_start, + 'request_handler': app.handle_request, + }, workers=2, stop_event=stop_event) + end = time() + + assert end - start < 0.05 From 15e7d8ab2eac51a4b0858efe762403de3d4bed2b Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 28 Dec 2016 18:03:12 +0900 Subject: [PATCH 176/977] Handle hooks parameters in more debuggable way 1. not list() -> callable() The args of hooking parameters of Sanic have to be callables. For wrong parameters, errors will be generated from: ``` listeners += args ``` By checking just list type, the raised error will be associated with `[args]` instead of `args`, which is not given by users. With this patch, the raised error will be associated with `args`. Then users can notice their argument was neither callable nor list in the easier way. 2. Function -> Functions in document Regarding the parameter as a list is harmless to the user code. But unawareness of its type can be list can limit the potent of the user code. --- sanic/sanic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index ecf5b652..7ec3260e 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -246,13 +246,13 @@ class Sanic: :param host: Address to host on :param port: Port to host on :param debug: Enables debug output (slows server) - :param before_start: Function to be executed before the server starts + :param before_start: Functions to be executed before the server starts accepting connections - :param after_start: Function to be executed after the server starts + :param after_start: Functions to be executed after the server starts accepting connections - :param before_stop: Function to be executed when a stop signal is + :param before_stop: Functions to be executed when a stop signal is received before it is respected - :param after_stop: Function to be executed when all requests are + :param after_stop: Functions to be executed when all requests are complete :param sock: Socket for the server to accept connections from :param workers: Number of processes @@ -290,7 +290,7 @@ class Sanic: for blueprint in self.blueprints.values(): listeners += blueprint.listeners[event_name] if args: - if type(args) is not list: + if callable(args): args = [args] listeners += args if reverse: From 83e9d0885373e6d5f43a328e861d2321c769e62b Mon Sep 17 00:00:00 2001 From: 38elements Date: Thu, 29 Dec 2016 13:11:27 +0900 Subject: [PATCH 177/977] Add document for custom protocol --- README.md | 1 + docs/custom_protocol.md | 68 +++++++++++++++++++++++++++++++++++++ examples/custom_protocol.py | 28 --------------- sanic/sanic.py | 2 +- sanic/server.py | 2 +- 5 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 docs/custom_protocol.md delete mode 100644 examples/custom_protocol.py diff --git a/README.md b/README.md index 5aded700..61b154ff 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ if __name__ == "__main__": * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) + * [Custom Protocol](docs/custom_protocol.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/custom_protocol.md b/docs/custom_protocol.md new file mode 100644 index 00000000..a92f1b53 --- /dev/null +++ b/docs/custom_protocol.md @@ -0,0 +1,68 @@ +# Custom Protocol + +You can change the behavior of protocol by using custom protocol. +If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic. + +* loop +`loop` is an asyncio compatible event loop. + +* connections +`connections` is a `set object` to store protocol objects. +When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections. + +* signal +`signal` is a `sanic.server.Signal object` with `stopped attribute`. +When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`. + +* request_handler +`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments. + +* error_handler +`error_handler` is a `sanic.exceptions.Handler` object. + +* request_timeout +`request_timeout` is seconds for timeout. + +* request_max_size +`request_max_size` is bytes of max request size. + +## Example + +```python +from sanic import Sanic +from sanic.server import HttpProtocol +from sanic.response import text + +app = Sanic(__name__) + + +class CustomHttpProtocol(HttpProtocol): + + def __init__(self, *, loop, request_handler, error_handler, + signal, connections, request_timeout, request_max_size): + super().__init__( + loop=loop, request_handler=request_handler, + error_handler=error_handler, signal=signal, + connections=connections, request_timeout=request_timeout, + request_max_size=request_max_size) + + def write_response(self, response): + if isinstance(response, str): + response = text(response) + self.transport.write( + response.output(self.request.version) + ) + self.transport.close() + + +@app.route('/') +async def string(request): + return 'string' + + +@app.route('/1') +async def response(request): + return text('response') + +app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) +``` diff --git a/examples/custom_protocol.py b/examples/custom_protocol.py deleted file mode 100644 index d1df8fde..00000000 --- a/examples/custom_protocol.py +++ /dev/null @@ -1,28 +0,0 @@ -from sanic import Sanic -from sanic.server import HttpProtocol -from sanic.response import text - -app = Sanic(__name__) - - -class CustomHttpProtocol(HttpProtocol): - - def write_response(self, response): - if isinstance(response, str): - response = text(response) - self.transport.write( - response.output(self.request.version) - ) - self.transport.close() - - -@app.route('/') -async def string(request): - return 'string' - - -@app.route('/1') -async def response(request): - return text('response') - -app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) diff --git a/sanic/sanic.py b/sanic/sanic.py index 87f12ac3..e3115a69 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -251,7 +251,7 @@ class Sanic: :param workers: Number of processes received before it is respected :param loop: asyncio compatible event loop - :param protocol: Subclass of asyncio.Protocol + :param protocol: Subclass of asyncio protocol class :return: Nothing """ self.error_handler.debug = True diff --git a/sanic/server.py b/sanic/server.py index 74e834b4..ffaf2d22 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -245,7 +245,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param request_max_size: size in bytes, `None` for no limit :param reuse_port: `True` for multiple workers :param loop: asyncio compatible event loop - :param protocol: Subclass of asyncio.Protocol + :param protocol: Subclass of asyncio protocol class :return: Nothing """ loop = loop or async_loop.new_event_loop() From 6bb4dae5e041e38065f403eea759baeea12067d0 Mon Sep 17 00:00:00 2001 From: 38elements Date: Thu, 29 Dec 2016 13:25:04 +0900 Subject: [PATCH 178/977] Fix format in custom_protocol.md --- docs/custom_protocol.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/custom_protocol.md b/docs/custom_protocol.md index a92f1b53..d4f14dee 100644 --- a/docs/custom_protocol.md +++ b/docs/custom_protocol.md @@ -1,29 +1,29 @@ # Custom Protocol -You can change the behavior of protocol by using custom protocol. +You can change the behavior of protocol by using custom protocol. If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic. -* loop +* loop `loop` is an asyncio compatible event loop. -* connections +* connections `connections` is a `set object` to store protocol objects. When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections. -* signal +* signal `signal` is a `sanic.server.Signal object` with `stopped attribute`. When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`. -* request_handler +* request_handler `request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments. -* error_handler +* error_handler `error_handler` is a `sanic.exceptions.Handler` object. -* request_timeout +* request_timeout `request_timeout` is seconds for timeout. -* request_max_size +* request_max_size `request_max_size` is bytes of max request size. ## Example From 64e0e2d19f74af9b50da86f12a151a2304de0cd1 Mon Sep 17 00:00:00 2001 From: 38elements Date: Thu, 29 Dec 2016 16:41:04 +0900 Subject: [PATCH 179/977] Improve custom_protocol.md --- docs/custom_protocol.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/custom_protocol.md b/docs/custom_protocol.md index d4f14dee..7381a3cb 100644 --- a/docs/custom_protocol.md +++ b/docs/custom_protocol.md @@ -27,6 +27,8 @@ When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`. `request_max_size` is bytes of max request size. ## Example +By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`. +In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`. ```python from sanic import Sanic From e7314d17753b9d566ab85c66c059a98e1d7f0d6e Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Thu, 29 Dec 2016 19:22:11 +0200 Subject: [PATCH 180/977] fix misprints&renaming --- docs/routing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/routing.md b/docs/routing.md index d15ba4e9..92ac2290 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -33,12 +33,12 @@ async def handler1(request): return text('OK') app.add_route(handler1, '/test') -async def handler(request, name): +async def handler2(request, name): return text('Folder - {}'.format(name)) -app.add_route(handler, '/folder/') +app.add_route(handler2, '/folder/') -async def person_handler(request, name): +async def person_handler2(request, name): return text('Person - {}'.format(name)) -app.add_route(handler, '/person/') +app.add_route(person_handler2, '/person/') ``` From 6d1d4ade19a6b5aaf572dbba68540b2976a43a53 Mon Sep 17 00:00:00 2001 From: Diogo Dutra Date: Thu, 29 Dec 2016 19:35:41 -0200 Subject: [PATCH 181/977] add a flag to skip SIGINT and SIGTERM signals registration --- sanic/sanic.py | 5 +++-- sanic/server.py | 8 +++++--- tests/test_sanic.py | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 tests/test_sanic.py diff --git a/sanic/sanic.py b/sanic/sanic.py index ecf5b652..6c9eb0fd 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -239,7 +239,7 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, sock=None, - workers=1, loop=None): + workers=1, loop=None, register_sys_signals=True): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -273,7 +273,8 @@ class Sanic: 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, - 'loop': loop + 'loop': loop, + 'register_sys_signals': register_sys_signals } # -------------------------------------------- # diff --git a/sanic/server.py b/sanic/server.py index 11756005..17f5965e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -226,7 +226,8 @@ def trigger_events(events, loop): def serve(host, port, request_handler, error_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, - request_max_size=None, reuse_port=False, loop=None): + request_max_size=None, reuse_port=False, loop=None, + register_sys_signals=True): """ Starts asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -286,8 +287,9 @@ def serve(host, port, request_handler, error_handler, before_start=None, trigger_events(after_start, loop) # Register signals for graceful termination - for _signal in (SIGINT, SIGTERM): - loop.add_signal_handler(_signal, loop.stop) + if register_sys_signals: + for _signal in (SIGINT, SIGTERM): + loop.add_signal_handler(_signal, loop.stop) try: loop.run_forever() diff --git a/tests/test_sanic.py b/tests/test_sanic.py new file mode 100644 index 00000000..589253f7 --- /dev/null +++ b/tests/test_sanic.py @@ -0,0 +1,41 @@ +from sanic import Sanic +from sanic.response import HTTPResponse +from sanic.utils import HOST, PORT +from unittest.mock import MagicMock +import pytest +import asyncio + + +async def stop(app): + await asyncio.sleep(0.2) + app.stop() + + +def test_register_system_signals(): + """Test if sanic register system signals""" + app = Sanic('test_register_system_signals') + + @app.route('/hello') + async def hello_route(request): + return HTTPResponse() + + loop = asyncio.new_event_loop() + loop.add_signal_handler = MagicMock() + asyncio.ensure_future(stop(app), loop=loop) + app.run(HOST, PORT, loop=loop) + assert loop.add_signal_handler.called == True + + +def test_dont_register_system_signals(): + """Test if sanic don't register system signals""" + app = Sanic('test_register_system_signals') + + @app.route('/hello') + async def hello_route(request): + return HTTPResponse() + + loop = asyncio.new_event_loop() + loop.add_signal_handler = MagicMock() + asyncio.ensure_future(stop(app), loop=loop) + app.run(HOST, PORT, loop=loop, register_sys_signals=False) + assert loop.add_signal_handler.called == False From 0f6ed642daa5294e40bda11024c7c48950f5c4c8 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 30 Dec 2016 07:36:57 -0200 Subject: [PATCH 182/977] created methods to remove a route from api/router --- sanic/router.py | 21 +++++++++ sanic/sanic.py | 3 ++ tests/test_routes.py | 109 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index 4cc1f073..12f13240 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -23,6 +23,10 @@ class RouteExists(Exception): pass +class RouteDoesNotExist(Exception): + pass + + class Router: """ Router supports basic routing with parameters and method checks @@ -109,6 +113,23 @@ class Router: else: self.routes_static[uri] = route + def remove(self, uri, clean_cache=True): + try: + route = self.routes_all.pop(uri) + except KeyError: + raise RouteDoesNotExist("Route was not registered: {}".format(uri)) + + if route in self.routes_always_check: + self.routes_always_check.remove(route) + elif url_hash(uri) in self.routes_dynamic \ + and route in self.routes_dynamic[url_hash(uri)]: + self.routes_dynamic[url_hash(uri)].remove(route) + else: + self.routes_static.pop(uri) + + if clean_cache: + self._get.cache_clear() + 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 ecf5b652..4a78a223 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -80,6 +80,9 @@ class Sanic: self.route(uri=uri, methods=methods)(handler) return handler + def remove_route(self, uri, clean_cache=True): + self.router.remove(uri, clean_cache) + # Decorator def exception(self, *exceptions): """ diff --git a/tests/test_routes.py b/tests/test_routes.py index 38591e53..149c71f9 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -2,7 +2,7 @@ import pytest from sanic import Sanic from sanic.response import text -from sanic.router import RouteExists +from sanic.router import RouteExists, RouteDoesNotExist from sanic.utils import sanic_endpoint_test @@ -356,3 +356,110 @@ def test_add_route_method_not_allowed(): request, response = sanic_endpoint_test(app, method='post', uri='/test') assert response.status == 405 + + +def test_remove_static_route(): + app = Sanic('test_remove_static_route') + + async def handler1(request): + return text('OK1') + + async def handler2(request): + return text('OK2') + + app.add_route(handler1, '/test') + app.add_route(handler2, '/test2') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/test2') + assert response.status == 200 + + app.remove_route('/test') + app.remove_route('/test2') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/test2') + assert response.status == 404 + + +def test_remove_dynamic_route(): + app = Sanic('test_remove_dynamic_route') + + async def handler(request, name): + return text('OK') + + app.add_route(handler, '/folder/') + + request, response = sanic_endpoint_test(app, uri='/folder/test123') + assert response.status == 200 + + app.remove_route('/folder/') + request, response = sanic_endpoint_test(app, uri='/folder/test123') + assert response.status == 404 + + +def test_remove_inexistent_route(): + app = Sanic('test_remove_inexistent_route') + + with pytest.raises(RouteDoesNotExist): + app.remove_route('/test') + + +def test_remove_unhashable_route(): + app = Sanic('test_remove_unhashable_route') + + async def handler(request, unhashable): + return text('OK') + + app.add_route(handler, '/folder//end/') + + request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/end/') + assert response.status == 200 + + app.remove_route('/folder//end/') + + request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/test/end/') + assert response.status == 404 + + +def test_remove_route_without_clean_cache(): + app = Sanic('test_remove_static_route') + + async def handler(request): + return text('OK') + + app.add_route(handler, '/test') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + app.remove_route('/test', clean_cache=True) + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 404 + + app.add_route(handler, '/test') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + app.remove_route('/test', clean_cache=False) + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 From 87559a34f8a06d401821beec3bc9f0d1dcb93d20 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 30 Dec 2016 12:13:16 -0600 Subject: [PATCH 183/977] Include more explicit loop for headers conversion Also merges master changes into this PR for this branch --- sanic/response.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 1202b101..f2eb02e5 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,9 +1,11 @@ from aiofiles import open as open_async -from .cookies import CookieJar, Cookie from mimetypes import guess_type from os import path + from ujson import dumps as json_dumps +from .cookies import CookieJar + COMMON_STATUS_CODES = { 200: b'OK', 400: b'Bad Request', @@ -79,7 +81,12 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body.encode('utf-8') + try: + # Try to encode it regularly + self.body = body.encode('utf-8') + except AttributeError: + # Convert it to a str if you can't + self.body = str(body).encode('utf-8') else: self.body = body_bytes @@ -87,15 +94,6 @@ class HTTPResponse: self.headers = headers or {} self._cookies = None - @staticmethod - def format_header(name, value): - try: - return b'%b: %b\r\n' %\ - (name.encode(), value.encode('utf-8')) - except: - return b'%b: %b\r\n' %\ - (name.encode(), str(value).encode('utf-8')) - def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python @@ -105,10 +103,14 @@ class HTTPResponse: headers = b'' if self.headers: - headers = b''.join( - self.format_header(name, value) - for name, value in self.headers.items() - ) + for name, value in self.headers.items(): + try: + headers += ( + b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) + except AttributeError: + headers += ( + b'%b: %b\r\n' % ( + str(name).encode(), str(value).encode('utf-8'))) # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all From 7a8fd6b0df9567a019039b8f82ed1296b4307bf9 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 30 Dec 2016 13:48:17 -0600 Subject: [PATCH 184/977] Add more verbose error handling * Adds logging to error messages in debug mode as pointed out in PR #249, while also improving the debug message. --- sanic/exceptions.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 369a87a2..b9e6bf00 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -1,4 +1,5 @@ from .response import text +from .log import log from traceback import format_exc @@ -56,18 +57,31 @@ class Handler: :return: Response object """ handler = self.handlers.get(type(exception), self.default) - response = handler(request=request, exception=exception) + try: + response = handler(request=request, exception=exception) + except: + if self.sanic.debug: + response_message = ( + 'Exception raised in exception handler "{}" ' + 'for uri: "{}"\n{}').format( + handler.__name__, request.url, format_exc()) + log.error(response_message) + return text(response_message, 500) + else: + return text('An error occurred while handling an error', 500) return response def default(self, request, exception): if issubclass(type(exception), SanicException): return text( - "Error: {}".format(exception), + 'Error: {}'.format(exception), status=getattr(exception, 'status_code', 500)) elif self.sanic.debug: - return text( - "Error: {}\nException: {}".format( - exception, format_exc()), status=500) + response_message = ( + 'Exception occurred while handling uri: "{}"\n{}'.format( + request.url, format_exc())) + log.error(response_message) + return text(response_message, status=500) else: return text( - "An error occurred while generating the request", status=500) + 'An error occurred while generating the response', status=500) From 15c965c08c68807b38121a9ab229214949ffd592 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 30 Dec 2016 13:50:12 -0600 Subject: [PATCH 185/977] Make exception tests test unhandled exceptions * Adds tests for unhandled exceptions * Adds tests for unhandled exceptions in exception handlers * Rewrites tests to utilize pytest fixtures (No need to create the app on import) --- tests/test_exceptions.py | 87 ++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 28e766cd..5cebfb87 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,51 +1,86 @@ +import pytest + from sanic import Sanic from sanic.response import text from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.utils import sanic_endpoint_test -# ------------------------------------------------------------ # -# GET -# ------------------------------------------------------------ # -exception_app = Sanic('test_exceptions') +class SanicExceptionTestException(Exception): + pass -@exception_app.route('/') -def handler(request): - return text('OK') +@pytest.fixture(scope='module') +def exception_app(): + app = Sanic('test_exceptions') + + @app.route('/') + def handler(request): + return text('OK') + + @app.route('/error') + def handler_error(request): + raise ServerError("OK") + + @app.route('/404') + def handler_404(request): + raise NotFound("OK") + + @app.route('/invalid') + def handler_invalid(request): + raise InvalidUsage("OK") + + @app.route('/divide_by_zero') + def handle_unhandled_exception(request): + 1 / 0 + + @app.route('/error_in_error_handler_handler') + def custom_error_handler(request): + raise SanicExceptionTestException('Dummy message!') + + @app.exception(SanicExceptionTestException) + def error_in_error_handler_handler(request, exception): + 1 / 0 + + return app -@exception_app.route('/error') -def handler_error(request): - raise ServerError("OK") - - -@exception_app.route('/404') -def handler_404(request): - raise NotFound("OK") - - -@exception_app.route('/invalid') -def handler_invalid(request): - raise InvalidUsage("OK") - - -def test_no_exception(): +def test_no_exception(exception_app): + """Test that a route works without an exception""" request, response = sanic_endpoint_test(exception_app) assert response.status == 200 assert response.text == 'OK' -def test_server_error_exception(): +def test_server_error_exception(exception_app): + """Test the built-in ServerError exception works""" request, response = sanic_endpoint_test(exception_app, uri='/error') assert response.status == 500 -def test_invalid_usage_exception(): +def test_invalid_usage_exception(exception_app): + """Test the built-in InvalidUsage exception works""" request, response = sanic_endpoint_test(exception_app, uri='/invalid') assert response.status == 400 -def test_not_found_exception(): +def test_not_found_exception(exception_app): + """Test the built-in NotFound exception works""" request, response = sanic_endpoint_test(exception_app, uri='/404') assert response.status == 404 + + +def test_handled_unhandled_exception(exception_app): + """Test that an exception not built into sanic is handled""" + request, response = sanic_endpoint_test( + exception_app, uri='/divide_by_zero') + assert response.status == 500 + assert response.body == b'An error occurred while generating the response' + + +def test_exception_in_exception_handler(exception_app): + """Test that an exception thrown in an error handler is handled""" + request, response = sanic_endpoint_test( + exception_app, uri='/error_in_error_handler_handler') + assert response.status == 500 + assert response.body == b'An error occurred while handling an error' From 87c24e5a7cd629c25f100e77b35bbebb1b9fe210 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Mon, 2 Jan 2017 11:57:51 +0900 Subject: [PATCH 186/977] Prevent flooding of meaningless traceback in `sanic_endpoint_test` When Sanic has an exception in a request middleware, it fails to save request object in `results`. In `sanic_endpoint_test`, because it always requires `results` to have both `request` and `response` objects, it prints traceback like attached example. It is not a user code and it doesn't give any information to users, it is better to suppress to print this kind of error. To fix it, this patch insert collect hook as first request middleware to guarantee to successfully run it always. ``` app = , method = 'get', uri = '/ping/', gather_request = True, loop = None debug = True, request_args = (), request_kwargs = {} _collect_request = ._collect_request at 0x11286c158> _collect_response = ._collect_response at 0x11286c378> def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, loop=None, debug=False, *request_args, **request_kwargs): results = [] exceptions = [] if gather_request: @app.middleware def _collect_request(request): results.append(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=42101, after_start=_collect_response, loop=loop) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) if gather_request: try: > request, response = results E ValueError: not enough values to unpack (expected 2, got 1) ../sanic/sanic/utils.py:46: ValueError ``` --- sanic/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/utils.py b/sanic/utils.py index 88444b3c..cb6d70c4 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -22,9 +22,9 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions = [] if gather_request: - @app.middleware def _collect_request(request): results.append(request) + app.request_middleware.appendleft(_collect_request) async def _collect_response(sanic, loop): try: From 31e92a8b4f371141d5e3f2eec0d7971796a75b7e Mon Sep 17 00:00:00 2001 From: Hyunjun Kim Date: Mon, 2 Jan 2017 13:32:14 +0900 Subject: [PATCH 187/977] Update .gitignore * .python-version is generated by `pyenv local` command * .eggs/ directory contains *.egg files --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d7872c5c..7fb5634f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ *~ *.egg-info *.egg +*.eggs +*.pyc .coverage .coverage.* coverage .tox settings.py -*.pyc .idea/* .cache/* +.python-version From 035cbf84ae4454422b6dbd1861401404963f7e6b Mon Sep 17 00:00:00 2001 From: Hyunjun Kim Date: Mon, 2 Jan 2017 14:20:20 +0900 Subject: [PATCH 188/977] Cache request.json even when it's null or empty In case of request body is set to `{}`, `[]` or `null`, even it's already processed, parsed_json won't be used due to its boolean evaluation. --- sanic/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 4309bbed..a9f0364d 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -56,7 +56,7 @@ class Request(dict): # Init but do not inhale self.body = None - self.parsed_json = None + self.parsed_json = ... self.parsed_form = None self.parsed_files = None self.parsed_args = None @@ -64,7 +64,7 @@ class Request(dict): @property def json(self): - if not self.parsed_json: + if self.parsed_json is ...: try: self.parsed_json = json_loads(self.body) except Exception: From cfdd9f66d1a7eae4d5fe091c1499f5c77d735ccd Mon Sep 17 00:00:00 2001 From: Hyunjun Kim Date: Mon, 2 Jan 2017 13:29:31 +0900 Subject: [PATCH 189/977] Correct sanic.router.Router documentation --- sanic/router.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 12f13240..081b5da6 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -31,12 +31,12 @@ class Router: """ Router supports basic routing with parameters and method checks Usage: - @sanic.route('/my/url/', methods=['GET', 'POST', ...]) - def my_route(request, my_parameter): + @app.route('/my_url/', methods=['GET', 'POST', ...]) + def my_route(request, my_param): do stuff... or - @sanic.route('/my/url/:type', methods['GET', 'POST', ...]) - def my_route_with_type(request, my_parameter): + @app.route('/my_url/', methods=['GET', 'POST', ...]) + def my_route_with_type(request, my_param: my_type): do stuff... Parameters will be passed as keyword arguments to the request handling From e6eb697bb2ba8840f8a92746426abb2f213a15d1 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 4 Jan 2017 05:40:13 +0900 Subject: [PATCH 190/977] Use constant PORT rather than literal in test code (#266) --- sanic/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/utils.py b/sanic/utils.py index 9f4a97d8..47214872 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -35,7 +35,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, debug=debug, port=42101, + app.run(host=HOST, debug=debug, port=PORT, after_start=_collect_response, loop=loop, **server_kwargs) if exceptions: From e7922c1b547d58e605cf5a877ace8214c992d987 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 3 Jan 2017 18:35:11 -0800 Subject: [PATCH 191/977] add configurable backlog #263 --- sanic/sanic.py | 6 ++++-- sanic/server.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 22ed234e..d0674360 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -20,7 +20,7 @@ from .exceptions import ServerError class Sanic: def __init__(self, name=None, router=None, - error_handler=None, logger=None): + error_handler=None, logger=None, backlog=100): if logger is None: logging.basicConfig( level=logging.INFO, @@ -29,6 +29,7 @@ class Sanic: if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) + self.backlog = backlog self.name = name self.router = router or Router() self.error_handler = error_handler or Handler(self) @@ -278,7 +279,8 @@ class Sanic: 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, - 'loop': loop + 'loop': loop, + 'backlog': self.backlog } # -------------------------------------------- # diff --git a/sanic/server.py b/sanic/server.py index e789f173..ec207d26 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -226,7 +226,7 @@ def trigger_events(events, loop): def serve(host, port, request_handler, error_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, request_max_size=None, - reuse_port=False, loop=None, protocol=HttpProtocol): + reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100): """ Starts asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -276,7 +276,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, host, port, reuse_port=reuse_port, - sock=sock + sock=sock, + backlog=backlog ) # Instead of pulling time at the end of every request, From 06911a8d2e4fdec7b811c5d2a6fc86b309586086 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 4 Jan 2017 00:23:35 -0600 Subject: [PATCH 192/977] Add tests for server start/stop event functions --- tests/tests_server_events.py | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/tests_server_events.py diff --git a/tests/tests_server_events.py b/tests/tests_server_events.py new file mode 100644 index 00000000..27a5af29 --- /dev/null +++ b/tests/tests_server_events.py @@ -0,0 +1,59 @@ +from io import StringIO +from random import choice +from string import ascii_letters +import signal + +import pytest + +from sanic import Sanic + +AVAILABLE_LISTENERS = [ + 'before_start', + 'after_start', + 'before_stop', + 'after_stop' +] + + +def create_listener(listener_name, in_list): + async def _listener(app, loop): + print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name)) + in_list.insert(0, app.name + listener_name) + return _listener + + +def start_stop_app(random_name_app, **run_kwargs): + + def stop_on_alarm(signum, frame): + raise KeyboardInterrupt('SIGINT for sanic to stop gracefully') + + signal.signal(signal.SIGALRM, stop_on_alarm) + signal.alarm(1) + try: + random_name_app.run(**run_kwargs) + except KeyboardInterrupt: + pass + + +@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS) +def test_single_listener(listener_name): + """Test that listeners on their own work""" + random_name_app = Sanic(''.join( + [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) + output = list() + start_stop_app( + random_name_app, + **{listener_name: create_listener(listener_name, output)}) + assert random_name_app.name + listener_name == output.pop() + + +def test_all_listeners(): + random_name_app = Sanic(''.join( + [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) + output = list() + start_stop_app( + random_name_app, + **{listener_name: create_listener(listener_name, output) + for listener_name in AVAILABLE_LISTENERS}) + for listener_name in AVAILABLE_LISTENERS: + assert random_name_app.name + listener_name == output.pop() From 9c91b09ab1425eb486a19abaf94da63173e109cb Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 4 Jan 2017 00:23:59 -0600 Subject: [PATCH 193/977] Fix this to actually reflect current behavior --- examples/try_everything.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/try_everything.py b/examples/try_everything.py index 80358ddb..f386fb03 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -64,11 +64,11 @@ def query_string(request): # Run Server # ----------------------------------------------- # -def after_start(loop): +def after_start(app, loop): log.info("OH OH OH OH OHHHHHHHH") -def before_stop(loop): +def before_stop(app, loop): log.info("TRIED EVERYTHING") From b67482de9b5272bf88aef59ac3c685f52fbf5771 Mon Sep 17 00:00:00 2001 From: DanielChien Date: Wed, 4 Jan 2017 23:29:09 +0800 Subject: [PATCH 194/977] add example for asyncpg --- examples/sanic_asyncpg_example.py | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 examples/sanic_asyncpg_example.py diff --git a/examples/sanic_asyncpg_example.py b/examples/sanic_asyncpg_example.py new file mode 100644 index 00000000..01ac1984 --- /dev/null +++ b/examples/sanic_asyncpg_example.py @@ -0,0 +1,64 @@ +""" To run this example you need additional asyncpg package + +""" +import os +import asyncio + +import uvloop +from asyncpg import create_pool +import sqlalchemy as sa + +from sanic import Sanic +from sanic.response import json + +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + +DB_CONFIG = { + 'host': 'localhost', + 'user': 'tachien', + 'database': 'tachien' +} + +def jsonify(records): + """ Parse asyncpg record response into JSON format + + """ + return [{key: value for key, value in + zip(r.keys(), r.values())} for r in records] + +loop = asyncio.get_event_loop() + +async def make_pool(): + return await create_pool(**DB_CONFIG) + +app = Sanic(__name__) +pool = loop.run_until_complete(make_pool()) + +async def create_db(): + """ Create some table and add some data + + """ + async with pool.acquire() as connection: + async with connection.transaction(): + await connection.execute('DROP TABLE IF EXISTS sanic_post') + await connection.execute("""CREATE TABLE sanic_post ( + id serial primary key, + content varchar(50), + post_date timestamp + );""") + for i in range(0, 100): + await connection.execute(f"""INSERT INTO sanic_post + (id, content, post_date) VALUES ({i}, {i}, now())""") + + +@app.route("/") +async def handler(request): + async with pool.acquire() as connection: + async with connection.transaction(): + results = await connection.fetch('SELECT * FROM sanic_post') + return json({'posts': jsonify(results)}) + + +if __name__ == '__main__': + loop.run_until_complete(create_db()) + app.run(host='0.0.0.0', port=8000, loop=loop) From 19426444344d428ac0a59c676f5ecd4aff5a35ae Mon Sep 17 00:00:00 2001 From: DanielChien Date: Wed, 4 Jan 2017 23:30:29 +0800 Subject: [PATCH 195/977] modify config to varbles --- examples/sanic_asyncpg_example.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/sanic_asyncpg_example.py b/examples/sanic_asyncpg_example.py index 01ac1984..9817ff57 100644 --- a/examples/sanic_asyncpg_example.py +++ b/examples/sanic_asyncpg_example.py @@ -14,9 +14,11 @@ from sanic.response import json asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) DB_CONFIG = { - 'host': 'localhost', - 'user': 'tachien', - 'database': 'tachien' + 'host': '', + 'user': '', + 'password': '', + 'port': '', + 'database': '' } def jsonify(records): From 5c7c2cf85e1f742b1793ec7c20f167197e917883 Mon Sep 17 00:00:00 2001 From: easydaniel Date: Wed, 4 Jan 2017 23:35:06 +0800 Subject: [PATCH 196/977] Update sanic_asyncpg_example.py Remove unused library --- examples/sanic_asyncpg_example.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/sanic_asyncpg_example.py b/examples/sanic_asyncpg_example.py index 9817ff57..142480e1 100644 --- a/examples/sanic_asyncpg_example.py +++ b/examples/sanic_asyncpg_example.py @@ -6,7 +6,6 @@ import asyncio import uvloop from asyncpg import create_pool -import sqlalchemy as sa from sanic import Sanic from sanic.response import json From 616e20d4674de93f89b3f0b509e206c31ae9a2bc Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 4 Jan 2017 09:31:06 -0800 Subject: [PATCH 197/977] move backlog to run() --- sanic/sanic.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index d0674360..a3f49197 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -20,7 +20,7 @@ from .exceptions import ServerError class Sanic: def __init__(self, name=None, router=None, - error_handler=None, logger=None, backlog=100): + error_handler=None, logger=None): if logger is None: logging.basicConfig( level=logging.INFO, @@ -29,7 +29,6 @@ class Sanic: if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) - self.backlog = backlog self.name = name self.router = router or Router() self.error_handler = error_handler or Handler(self) @@ -243,7 +242,7 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, sock=None, - workers=1, loop=None, protocol=HttpProtocol): + workers=1, loop=None, protocol=HttpProtocol, backlog=100): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -280,7 +279,7 @@ class Sanic: 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, 'loop': loop, - 'backlog': self.backlog + 'backlog': backlog } # -------------------------------------------- # From baf8254907720cb35212f00afa48787fa8558b6b Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Thu, 5 Jan 2017 15:29:57 -0600 Subject: [PATCH 198/977] Change Ellipsis to None for consistency --- sanic/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index a9f0364d..7ca5523c 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -56,7 +56,7 @@ class Request(dict): # Init but do not inhale self.body = None - self.parsed_json = ... + self.parsed_json = None self.parsed_form = None self.parsed_files = None self.parsed_args = None @@ -64,7 +64,7 @@ class Request(dict): @property def json(self): - if self.parsed_json is ...: + if self.parsed_json is None: try: self.parsed_json = json_loads(self.body) except Exception: From fcae4a9f0a5217b3b5ea6495821a85773aa33c55 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 7 Jan 2017 06:30:23 +0200 Subject: [PATCH 199/977] added as_view --- sanic/views.py | 25 ++++++++++++++++++++++++- tests/test_views.py | 14 +++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/sanic/views.py b/sanic/views.py index 9387bcf6..45a09ef1 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -28,12 +28,35 @@ class HTTPMethodView: To add the view into the routing you could use 1) app.add_route(DummyView(), '/') 2) app.route('/')(DummyView()) + + TODO: add doc about devorators """ - def __call__(self, request, *args, **kwargs): + decorators = () + + def dispatch_request(self, request, *args, **kwargs): handler = getattr(self, request.method.lower(), None) if handler: return handler(request, *args, **kwargs) raise InvalidUsage( 'Method {} not allowed for URL {}'.format( request.method, request.url), status_code=405) + + @classmethod + def as_view(cls, *class_args, **class_kwargs): + """ TODO: add docs + + """ + def view(*args, **kwargs): + self = view.view_class(*class_args, **class_kwargs) + return self.dispatch_request(*args, **kwargs) + + if cls.decorators: + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + + view.view_class = cls + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + return view diff --git a/tests/test_views.py b/tests/test_views.py index 59acb847..af38277e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -26,7 +26,7 @@ def test_methods(): def delete(self, request): return text('I am delete method') - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') request, response = sanic_endpoint_test(app, method="get") assert response.text == 'I am get method' @@ -48,7 +48,7 @@ def test_unexisting_methods(): def get(self, request): return text('I am get method') - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') request, response = sanic_endpoint_test(app, method="get") assert response.text == 'I am get method' request, response = sanic_endpoint_test(app, method="post") @@ -63,7 +63,7 @@ def test_argument_methods(): def get(self, request, my_param_here): return text('I am get method with %s' % my_param_here) - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') request, response = sanic_endpoint_test(app, uri='/test123') @@ -79,7 +79,7 @@ def test_with_bp(): def get(self, request): return text('I am get method') - bp.add_route(DummyView(), '/') + bp.add_route(DummyView.as_view(), '/') app.blueprint(bp) request, response = sanic_endpoint_test(app) @@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix(): def get(self, request): return text('I am get method') - bp.add_route(DummyView(), '/') + bp.add_route(DummyView.as_view(), '/') app.blueprint(bp) request, response = sanic_endpoint_test(app, uri='/test1/') @@ -112,7 +112,7 @@ def test_with_middleware(): def get(self, request): return text('I am get method') - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') results = [] @@ -145,7 +145,7 @@ def test_with_middleware_response(): def get(self, request): return text('I am get method') - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') request, response = sanic_endpoint_test(app) From 1317b1799ccf50f01d30b71c4b6c21a1c31dfc29 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 7 Jan 2017 06:57:07 +0200 Subject: [PATCH 200/977] add docstrings&updated docs --- docs/class_based_views.md | 17 +++++++++++++++-- sanic/views.py | 13 +++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index ee410b1d..27972e82 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -28,7 +28,7 @@ class SimpleView(HTTPMethodView): def delete(self, request): return text('I am delete method') -app.add_route(SimpleView(), '/') +app.add_route(SimpleView.as_view(), '/') ``` @@ -40,6 +40,19 @@ class NameView(HTTPMethodView): def get(self, request, name): return text('Hello {}'.format(name)) -app.add_route(NameView(), '/') +app.add_route(NameView.as_view(), '/') + +``` + +If you want to add decorator for class, you could set decorators variable + +``` +class ViewWithDecorator(HTTPMethodView): + decorators = (some_decorator_here) + + def get(self, request, name): + return text('Hello I have a decorator') + +app.add_route(ViewWithDecorator.as_view(), '/url') ``` diff --git a/sanic/views.py b/sanic/views.py index 45a09ef1..eeaa8d38 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -7,7 +7,7 @@ class HTTPMethodView: to every HTTP method you want to support. For example: - class DummyView(View): + class DummyView(HTTPMethodView): def get(self, request, *args, **kwargs): return text('I am get method') @@ -20,16 +20,16 @@ class HTTPMethodView: 405 response. If you need any url params just mention them in method definition: - class DummyView(View): + class DummyView(HTTPMethodView): def get(self, request, my_param_here, *args, **kwargs): return text('I am get method with %s' % my_param_here) To add the view into the routing you could use - 1) app.add_route(DummyView(), '/') - 2) app.route('/')(DummyView()) + 1) app.add_route(DummyView.as_view(), '/') + 2) app.route('/')(DummyView.as_view()) - TODO: add doc about devorators + To add any decorator you could set it into decorators variable """ decorators = () @@ -44,7 +44,8 @@ class HTTPMethodView: @classmethod def as_view(cls, *class_args, **class_kwargs): - """ TODO: add docs + """ Converts the class into an actual view function that can be used + with the routing system. """ def view(*args, **kwargs): From 47a4f34cdff4417a746f56ced29b698be7550fce Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 7 Jan 2017 07:13:49 +0200 Subject: [PATCH 201/977] tests&small update --- docs/class_based_views.md | 2 +- sanic/views.py | 2 +- tests/test_views.py | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index 27972e82..84a5b952 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -48,7 +48,7 @@ If you want to add decorator for class, you could set decorators variable ``` class ViewWithDecorator(HTTPMethodView): - decorators = (some_decorator_here) + decorators = [some_decorator_here] def get(self, request, name): return text('Hello I have a decorator') diff --git a/sanic/views.py b/sanic/views.py index eeaa8d38..0222b96f 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -32,7 +32,7 @@ class HTTPMethodView: To add any decorator you could set it into decorators variable """ - decorators = () + decorators = [] def dispatch_request(self, request, *args, **kwargs): handler = getattr(self, request.method.lower(), None) diff --git a/tests/test_views.py b/tests/test_views.py index af38277e..9447ab61 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -153,3 +153,44 @@ def test_with_middleware_response(): assert type(results[0]) is Request assert type(results[1]) is Request assert issubclass(type(results[2]), HTTPResponse) + + +def test_with_custom_class_methods(): + app = Sanic('test_with_custom_class_methods') + + class DummyView(HTTPMethodView): + global_var = 0 + + def _iternal_method(self): + self.global_var += 10 + + def get(self, request): + self._iternal_method() + return text('I am get method and global var is {}'.format(self.global_var)) + + app.add_route(DummyView.as_view(), '/') + request, response = sanic_endpoint_test(app, method="get") + assert response.text == 'I am get method and global var is 10' + + +def test_with_decorator(): + app = Sanic('test_with_decorator') + + results = [] + + def stupid_decorator(view): + def decorator(*args, **kwargs): + results.append(1) + return view(*args, **kwargs) + return decorator + + class DummyView(HTTPMethodView): + decorators = [stupid_decorator] + + def get(self, request): + return text('I am get method') + + app.add_route(DummyView.as_view(), '/') + request, response = sanic_endpoint_test(app, method="get", debug=True) + assert response.text == 'I am get method' + assert results[0] == 1 From 434fa74e67045ec397b5d7b5fdbdf2e25b9163a6 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 7 Jan 2017 07:14:27 +0200 Subject: [PATCH 202/977] removed debug from test --- tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_views.py b/tests/test_views.py index 9447ab61..592893a4 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -191,6 +191,6 @@ def test_with_decorator(): return text('I am get method') app.add_route(DummyView.as_view(), '/') - request, response = sanic_endpoint_test(app, method="get", debug=True) + request, response = sanic_endpoint_test(app, method="get") assert response.text == 'I am get method' assert results[0] == 1 From 77c04c4cf9ac5d5a84de025713423bf766f577bf Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Fri, 6 Jan 2017 18:32:30 -0800 Subject: [PATCH 203/977] fix multiple worker problem --- sanic/sanic.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index a3f49197..e5873236 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -16,6 +16,8 @@ from .router import Router from .server import serve, HttpProtocol from .static import register as static_register from .exceptions import ServerError +from socket import socket +from os import set_inheritable class Sanic: @@ -350,19 +352,19 @@ class Sanic: signal(SIGINT, lambda s, f: stop_event.set()) signal(SIGTERM, lambda s, f: stop_event.set()) + sock = socket() + sock.bind((server_settings['host'], server_settings['port'])) + set_inheritable(sock.fileno(), True) + server_settings['sock'] = sock + server_settings['host'] = None + server_settings['port'] = None + processes = [] for _ in range(workers): process = Process(target=serve, kwargs=server_settings) process.start() processes.append(process) - # Infinitely wait for the stop event - try: - select(stop_event) - except: - pass - - log.info('Spinning down workers...') for process in processes: process.terminate() for process in processes: From ed8e3f237cd94f2a0406ad9db2fb99f556640022 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 7 Jan 2017 15:28:21 -0800 Subject: [PATCH 204/977] this branch is broken --- sanic/sanic.py | 12 ++++++++---- tests/test_multiprocessing.py | 37 +++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index e5873236..6d855fc2 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -16,7 +16,7 @@ from .router import Router from .server import serve, HttpProtocol from .static import register as static_register from .exceptions import ServerError -from socket import socket +from socket import socket, SOL_SOCKET, SO_REUSEADDR from os import set_inheritable @@ -244,7 +244,8 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, sock=None, - workers=1, loop=None, protocol=HttpProtocol, backlog=100): + workers=1, loop=None, protocol=HttpProtocol, backlog=100, + stop_event=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -320,7 +321,7 @@ class Sanic: else: log.info('Spinning up {} workers...'.format(workers)) - self.serve_multiple(server_settings, workers) + self.serve_multiple(server_settings, workers, stop_event) except Exception as e: log.exception( @@ -335,7 +336,7 @@ class Sanic: get_event_loop().stop() @staticmethod - def serve_multiple(server_settings, workers, stop_event=None): + def serve_multiple(self, server_settings, workers, stop_event=None): """ Starts multiple server processes simultaneously. Stops on interrupt and terminate signals, and drains connections when complete. @@ -353,6 +354,7 @@ class Sanic: signal(SIGTERM, lambda s, f: stop_event.set()) sock = socket() + #sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.bind((server_settings['host'], server_settings['port'])) set_inheritable(sock.fileno(), True) server_settings['sock'] = sock @@ -362,10 +364,12 @@ class Sanic: processes = [] for _ in range(workers): process = Process(target=serve, kwargs=server_settings) + process.daemon = True process.start() processes.append(process) for process in processes: process.terminate() + for process in processes: process.join() diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index cc967ef1..52a68fd1 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,9 +1,13 @@ from multiprocessing import Array, Event, Process from time import sleep, time from ujson import loads as json_loads +from asyncio import get_event_loop +from os import killpg, kill +from signal import SIGUSR1, signal, SIGINT, SIGTERM, SIGKILL from sanic import Sanic -from sanic.response import json +from sanic.response import json, text +from sanic.exceptions import Handler from sanic.utils import local_request, HOST, PORT @@ -50,11 +54,12 @@ def skip_test_multiprocessing(): except: raise ValueError("Expected JSON response but got '{}'".format(response)) + stop_event.set() assert results.get('test') == True def test_drain_connections(): - app = Sanic('test_json') + app = Sanic('test_stop') @app.route('/') async def handler(request): @@ -75,3 +80,31 @@ def test_drain_connections(): end = time() assert end - start < 0.05 + +def skip_test_workers(): + app = Sanic('test_workers') + + @app.route('/') + async def handler(request): + return text('ok') + + stop_event = Event() + + d = [] + async def after_start(*args, **kwargs): + http_response = await local_request('get', '/') + d.append(http_response.text) + stop_event.set() + + p = Process(target=app.run, kwargs={'host':HOST, + 'port':PORT, + 'after_start': after_start, + 'workers':2, + 'stop_event':stop_event}) + p.start() + loop = get_event_loop() + loop.run_until_complete(after_start()) + #killpg(p.pid, SIGUSR1) + kill(p.pid, SIGUSR1) + + assert d[0] == 1 From dd28d70680e79a918d069313351c1a79297fcc2f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 7 Jan 2017 15:46:43 -0800 Subject: [PATCH 205/977] fix stop event --- sanic/sanic.py | 29 ++++++++++++++------------- tests/test_multiprocessing.py | 37 ++--------------------------------- 2 files changed, 17 insertions(+), 49 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 6d855fc2..b3487b53 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -3,7 +3,6 @@ from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename from multiprocessing import Process, Event -from select import select from signal import signal, SIGTERM, SIGINT from traceback import format_exc import logging @@ -41,6 +40,8 @@ class Sanic: self._blueprint_order = [] self.loop = None self.debug = None + self.sock = None + self.processes = None # Register alternative method names self.go_fast = self.run @@ -333,9 +334,12 @@ class Sanic: """ This kills the Sanic """ + if self.processes is not None: + for process in self.processes: + process.terminate() + self.sock.close() get_event_loop().stop() - @staticmethod def serve_multiple(self, server_settings, workers, stop_event=None): """ Starts multiple server processes simultaneously. Stops on interrupt @@ -348,28 +352,25 @@ class Sanic: server_settings['reuse_port'] = True # Create a stop event to be triggered by a signal - if not stop_event: + if stop_event is None: stop_event = Event() signal(SIGINT, lambda s, f: stop_event.set()) signal(SIGTERM, lambda s, f: stop_event.set()) - sock = socket() - #sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) - sock.bind((server_settings['host'], server_settings['port'])) - set_inheritable(sock.fileno(), True) - server_settings['sock'] = sock + self.sock = socket() + self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + self.sock.bind((server_settings['host'], server_settings['port'])) + set_inheritable(self.sock.fileno(), True) + server_settings['sock'] = self.sock server_settings['host'] = None server_settings['port'] = None - processes = [] + self.processes = [] for _ in range(workers): process = Process(target=serve, kwargs=server_settings) process.daemon = True process.start() - processes.append(process) + self.processes.append(process) - for process in processes: - process.terminate() - - for process in processes: + for process in self.processes: process.join() diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 52a68fd1..cc967ef1 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,13 +1,9 @@ from multiprocessing import Array, Event, Process from time import sleep, time from ujson import loads as json_loads -from asyncio import get_event_loop -from os import killpg, kill -from signal import SIGUSR1, signal, SIGINT, SIGTERM, SIGKILL from sanic import Sanic -from sanic.response import json, text -from sanic.exceptions import Handler +from sanic.response import json from sanic.utils import local_request, HOST, PORT @@ -54,12 +50,11 @@ def skip_test_multiprocessing(): except: raise ValueError("Expected JSON response but got '{}'".format(response)) - stop_event.set() assert results.get('test') == True def test_drain_connections(): - app = Sanic('test_stop') + app = Sanic('test_json') @app.route('/') async def handler(request): @@ -80,31 +75,3 @@ def test_drain_connections(): end = time() assert end - start < 0.05 - -def skip_test_workers(): - app = Sanic('test_workers') - - @app.route('/') - async def handler(request): - return text('ok') - - stop_event = Event() - - d = [] - async def after_start(*args, **kwargs): - http_response = await local_request('get', '/') - d.append(http_response.text) - stop_event.set() - - p = Process(target=app.run, kwargs={'host':HOST, - 'port':PORT, - 'after_start': after_start, - 'workers':2, - 'stop_event':stop_event}) - p.start() - loop = get_event_loop() - loop.run_until_complete(after_start()) - #killpg(p.pid, SIGUSR1) - kill(p.pid, SIGUSR1) - - assert d[0] == 1 From f8e6becb9e694a11b7ea6b049453399f2235daa8 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 7 Jan 2017 18:58:02 -0800 Subject: [PATCH 206/977] skip multiprocessing tests --- sanic/sanic.py | 3 +++ tests/test_multiprocessing.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index b3487b53..3dab3e47 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -374,3 +374,6 @@ class Sanic: for process in self.processes: process.join() + + # the above processes will block this until they're stopped + self.stop() diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index cc967ef1..7a5fd1c9 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -53,7 +53,7 @@ def skip_test_multiprocessing(): assert results.get('test') == True -def test_drain_connections(): +def skip_test_drain_connections(): app = Sanic('test_json') @app.route('/') From 5566668a5f6aeab044a5f1b2f2394b63fcdfa554 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 8 Jan 2017 11:55:08 -0600 Subject: [PATCH 207/977] Change the skips to actual pytest skips By using the builtin pytest skips we can identify that the tests are still there but are being currently skipped. Will update later to remove the skips once we figure out why they freeze with pytest (I experienced this same issue with multiprocessing when testing start/stop events). --- tests/test_multiprocessing.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 7a5fd1c9..e39c3d24 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -2,6 +2,8 @@ from multiprocessing import Array, Event, Process from time import sleep, time from ujson import loads as json_loads +import pytest + from sanic import Sanic from sanic.response import json from sanic.utils import local_request, HOST, PORT @@ -13,8 +15,9 @@ from sanic.utils import local_request, HOST, PORT # TODO: Figure out why this freezes on pytest but not when # executed via interpreter - -def skip_test_multiprocessing(): +@pytest.mark.skip( + reason="Freezes with pytest not on interpreter") +def test_multiprocessing(): app = Sanic('test_json') response = Array('c', 50) @@ -52,8 +55,9 @@ def skip_test_multiprocessing(): assert results.get('test') == True - -def skip_test_drain_connections(): +@pytest.mark.skip( + reason="Freezes with pytest not on interpreter") +def test_drain_connections(): app = Sanic('test_json') @app.route('/') From 4f832ac9afdba11f276b42ccbfbf7aa83c60dc47 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 8 Jan 2017 15:48:12 -0800 Subject: [PATCH 208/977] add support for virtual hosts --- examples/vhosts.py | 18 ++++++++++++++++++ sanic/router.py | 27 +++++++++++++++++++++++---- sanic/sanic.py | 13 +++++++------ tests/test_vhosts.py | 23 +++++++++++++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 examples/vhosts.py create mode 100644 tests/test_vhosts.py diff --git a/examples/vhosts.py b/examples/vhosts.py new file mode 100644 index 00000000..50e85494 --- /dev/null +++ b/examples/vhosts.py @@ -0,0 +1,18 @@ +from sanic.response import text +from sanic import Sanic + +# Usage +# curl -H "Host: example.com" localhost:8000 +# curl -H "Host: sub.example.com" localhost:8000 + +app = Sanic() + +@app.route('/', host="example.com") +async def hello(request): + return text("Answer") +@app.route('/', host="sub.example.com") +async def hello(request): + return text("42") + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000) diff --git a/sanic/router.py b/sanic/router.py index 081b5da6..a1f47230 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -55,8 +55,9 @@ class Router: self.routes_static = {} self.routes_dynamic = defaultdict(list) self.routes_always_check = [] + self.hosts = None - def add(self, uri, methods, handler): + def add(self, uri, methods, handler, host=None): """ Adds a handler to the route list :param uri: Path to match @@ -66,6 +67,17 @@ class Router: When executed, it should provide a response object. :return: Nothing """ + + if host is not None: + # we want to track if there are any + # vhosts on the Router instance so that we can + # default to the behavior without vhosts + if self.hosts is None: + self.hosts = set(host) + else: + self.hosts.add(host) + uri = host + uri + if uri in self.routes_all: raise RouteExists("Route already registered: {}".format(uri)) @@ -113,7 +125,9 @@ class Router: else: self.routes_static[uri] = route - def remove(self, uri, clean_cache=True): + def remove(self, uri, clean_cache=True, host=None): + if host is not None: + uri = host + uri try: route = self.routes_all.pop(uri) except KeyError: @@ -137,10 +151,14 @@ class Router: :param request: Request object :return: handler, arguments, keyword arguments """ - return self._get(request.url, request.method) + if self.hosts is None: + return self._get(request.url, request.method, '') + else: + return self._get(request.url, request.method, + request.headers.get("Host", '')) @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) - def _get(self, url, method): + def _get(self, url, method, host=None): """ Gets a request handler based on the URL of the request, or raises an error. Internal method for caching. @@ -148,6 +166,7 @@ class Router: :param method: Request method :return: handler, arguments, keyword arguments """ + url = host + url # Check against known static routes route = self.routes_static.get(url) if route: diff --git a/sanic/sanic.py b/sanic/sanic.py index 3dab3e47..6926050c 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -51,7 +51,7 @@ class Sanic: # -------------------------------------------------------------------- # # Decorator - def route(self, uri, methods=None): + def route(self, uri, methods=None, host=None): """ Decorates a function to be registered as a route :param uri: path of the URL @@ -65,12 +65,13 @@ class Sanic: uri = '/' + uri def response(handler): - self.router.add(uri=uri, methods=methods, handler=handler) + self.router.add(uri=uri, methods=methods, handler=handler, + host=host) return handler return response - def add_route(self, handler, uri, methods=None): + def add_route(self, handler, uri, methods=None, host=None): """ A helper method to register class instance or functions as a handler to the application url @@ -80,11 +81,11 @@ class Sanic: :param methods: list or tuple of methods allowed :return: function or class instance """ - self.route(uri=uri, methods=methods)(handler) + self.route(uri=uri, methods=methods, host=host)(handler) return handler - def remove_route(self, uri, clean_cache=True): - self.router.remove(uri, clean_cache) + def remove_route(self, uri, clean_cache=True, host=None): + self.router.remove(uri, clean_cache, host) # Decorator def exception(self, *exceptions): diff --git a/tests/test_vhosts.py b/tests/test_vhosts.py new file mode 100644 index 00000000..7bbbb813 --- /dev/null +++ b/tests/test_vhosts.py @@ -0,0 +1,23 @@ +from sanic import Sanic +from sanic.response import json, text +from sanic.utils import sanic_endpoint_test + + +def test_vhosts(): + app = Sanic('test_text') + + @app.route('/', host="example.com") + async def handler(request): + return text("You're at example.com!") + + @app.route('/', host="subdomain.example.com") + async def handler(request): + return text("You're at subdomain.example.com!") + + headers = {"Host": "example.com"} + request, response = sanic_endpoint_test(app, headers=headers) + assert response.text == "You're at example.com!" + + headers = {"Host": "subdomain.example.com"} + request, response = sanic_endpoint_test(app, headers=headers) + assert response.text == "You're at subdomain.example.com!" From 34d1e5e67ed9b302c4d4824ea09f948a02ead69e Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Tue, 10 Jan 2017 09:28:33 +1100 Subject: [PATCH 209/977] Convert README to RestructuredText and include it in the documentation index. --- README.md | 96 -------------------------------------------------- README.rst | 76 +++++++++++++++++++++++++++++++++++++++ docs/index.rst | 61 +------------------------------- 3 files changed, 77 insertions(+), 156 deletions(-) delete mode 100644 README.md create mode 100644 README.rst diff --git a/README.md b/README.md deleted file mode 100644 index e417b4a1..00000000 --- a/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Sanic - -[![Join the chat at https://gitter.im/sanic-python/Lobby](https://badges.gitter.im/sanic-python/Lobby.svg)](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic) -[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/) -[![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/) - -Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/. - -On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. - -## Benchmarks - -All tests were run on an AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for Falcon and Flask but did not speed up requests. - - - -| Server | Implementation | Requests/sec | Avg Latency | -| ------- | ------------------- | ------------:| -----------:| -| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms | -| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms | -| Falcon | gunicorn + meinheld | 18,972 | 5.27ms | -| Bottle | gunicorn + meinheld | 13,596 | 7.36ms | -| Flask | gunicorn + meinheld | 4,988 | 20.08ms | -| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms | -| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms | -| Tornado | Python 3.5 | 2,138 | 46.66ms | - -## Hello World - -```python -from sanic import Sanic -from sanic.response import json - - -app = Sanic() - - -@app.route("/") -async def test(request): - return json({"hello": "world"}) - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=8000) - -``` - -## Installation - * `python -m pip install sanic` - -## Documentation - * [Getting started](docs/getting_started.md) - * [Request Data](docs/request_data.md) - * [Routing](docs/routing.md) - * [Middleware](docs/middleware.md) - * [Exceptions](docs/exceptions.md) - * [Blueprints](docs/blueprints.md) - * [Class Based Views](docs/class_based_views.md) - * [Cookies](docs/cookies.md) - * [Static Files](docs/static_files.md) - * [Testing](docs/testing.md) - * [Deploying](docs/deploying.md) - * [Contributing](docs/contributing.md) - * [License](LICENSE) - -## TODO: - * Streamed file processing - * File output - * Examples of integrations with 3rd-party modules - * RESTful router - -## Limitations: - * No wheels for uvloop and httptools on Windows :( - -## Final Thoughts: - - ▄▄▄▄▄ - ▀▀▀██████▄▄▄ _______________ - ▄▄▄▄▄ █████████▄ / \ - ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | - ▀▀█████▄▄ ▀██████▄██ | _________________/ - ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ - ▀▀▀▄ ▀▀███ ▀ ▄▄ - ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ - ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██ - ▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀ - ▌ ▐▀████▐███▒▒▒▒▒▐██▌ - ▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀ - ▀▀█████████▀ - ▄▄██▀██████▀█ - ▄██▀ ▀▀▀ █ - ▄█ ▐▌ - ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄ - ▌ ▐ ▀▀▄▄▄▀ - ▀▀▄▄▀ diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..eb04b6a5 --- /dev/null +++ b/README.rst @@ -0,0 +1,76 @@ +Sanic +================================= + +|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version| + +Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article `_. + +On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. + +Sanic is developed `on GitHub `_. Contributions are welcome! + +Benchmarks +---------- + +All tests were run on an AWS medium instance running ubuntu, using 1 +process. Each script delivered a small JSON response and was tested with +wrk using 100 connections. Pypy was tested for Falcon and Flask but did +not speed up requests. + ++-----------+-----------------------+----------------+---------------+ +| Server | Implementation | Requests/sec | Avg Latency | ++===========+=======================+================+===============+ +| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms | ++-----------+-----------------------+----------------+---------------+ +| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms | ++-----------+-----------------------+----------------+---------------+ +| Falcon | gunicorn + meinheld | 18,972 | 5.27ms | ++-----------+-----------------------+----------------+---------------+ +| Bottle | gunicorn + meinheld | 13,596 | 7.36ms | ++-----------+-----------------------+----------------+---------------+ +| Flask | gunicorn + meinheld | 4,988 | 20.08ms | ++-----------+-----------------------+----------------+---------------+ +| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms | ++-----------+-----------------------+----------------+---------------+ +| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms | ++-----------+-----------------------+----------------+---------------+ +| Tornado | Python 3.5 | 2,138 | 46.66ms | ++-----------+-----------------------+----------------+---------------+ + +Hello World Example +------------------- + +.. code:: python + + from sanic import Sanic + from sanic.response import json + + + app = Sanic() + + + @app.route("/") + async def test(request): + return json({"hello": "world"}) + + if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) + +Installation +------------ + +- ``python -m pip install sanic`` + +Documentation +------------- + +Documentation can be found in the ``docs`` directory. + +.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg + :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master + :target: https://travis-ci.org/channelcat/sanic +.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg + :target: https://pypi.python.org/pypi/sanic/ +.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg + :target: https://pypi.python.org/pypi/sanic/ diff --git a/docs/index.rst b/docs/index.rst index 1e815582..264ebc93 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,63 +1,4 @@ -Welcome to Sanic's documentation! -================================= - -Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article `_. - -On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. - -Sanic is developed `on GitHub `_. Contributions are welcome! - -Benchmarks ----------- - -All tests were run on an AWS medium instance running ubuntu, using 1 -process. Each script delivered a small JSON response and was tested with -wrk using 100 connections. Pypy was tested for Falcon and Flask but did -not speed up requests. - -+-----------+-----------------------+----------------+---------------+ -| Server | Implementation | Requests/sec | Avg Latency | -+===========+=======================+================+===============+ -| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms | -+-----------+-----------------------+----------------+---------------+ -| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms | -+-----------+-----------------------+----------------+---------------+ -| Falcon | gunicorn + meinheld | 18,972 | 5.27ms | -+-----------+-----------------------+----------------+---------------+ -| Bottle | gunicorn + meinheld | 13,596 | 7.36ms | -+-----------+-----------------------+----------------+---------------+ -| Flask | gunicorn + meinheld | 4,988 | 20.08ms | -+-----------+-----------------------+----------------+---------------+ -| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms | -+-----------+-----------------------+----------------+---------------+ -| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms | -+-----------+-----------------------+----------------+---------------+ -| Tornado | Python 3.5 | 2,138 | 46.66ms | -+-----------+-----------------------+----------------+---------------+ - -Hello World Example -------------------- - -.. code:: python - - from sanic import Sanic - from sanic.response import json - - - app = Sanic() - - - @app.route("/") - async def test(request): - return json({"hello": "world"}) - - if __name__ == "__main__": - app.run(host="0.0.0.0", port=8000) - -Installation ------------- - -- ``python -m pip install sanic`` +.. include:: ../README.rst Guides ====== From 385328ffd5e640b063b7b0ae3a1a00eedc79308e Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Tue, 10 Jan 2017 09:41:00 +1100 Subject: [PATCH 210/977] Generate API documentation in the _api folder --- .gitignore | 3 +-- docs/contributing.md | 5 ++--- docs/index.rst | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 535c6a2f..9050c413 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,4 @@ settings.py .idea/* .cache/* docs/_build/ -docs/sanic.rst -docs/modules.rst \ No newline at end of file +docs/_api/ diff --git a/docs/contributing.md b/docs/contributing.md index c046bd39..a40654cc 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -13,9 +13,8 @@ Sanic's documentation is built using [sphinx](http://www.sphinx-doc.org/en/1.5.1 To generate the documentation from scratch: ```bash -rm -f docs/sanic.rst -rm -f docs/modules.rst -sphinx-apidoc -o docs/ sanic +rm -f docs/_api/* +sphinx-apidoc -o docs/_api/ sanic sphinx-build -b html docs docs/_build ``` diff --git a/docs/index.rst b/docs/index.rst index 264ebc93..b28d97f7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,7 +25,7 @@ Module Documentation .. toctree:: - Module Reference + Module Reference <_api/sanic> * :ref:`genindex` * :ref:`search` From 055430d4b848a8a7e60947e49a2170e58af158bd Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 10 Jan 2017 16:01:21 -0800 Subject: [PATCH 211/977] remove default from host in _get method --- sanic/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index a1f47230..9ab01b1c 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -158,7 +158,7 @@ class Router: request.headers.get("Host", '')) @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) - def _get(self, url, method, host=None): + def _get(self, url, method, host): """ Gets a request handler based on the URL of the request, or raises an error. Internal method for caching. From 62df50e22bcba5cae476b57b4fa175cfea9625f4 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 10 Jan 2017 18:07:58 -0800 Subject: [PATCH 212/977] add vhosts to blueprints --- examples/vhosts.py | 14 ++++++++++ sanic/blueprints.py | 10 ++++--- tests/test_blueprints.py | 58 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/examples/vhosts.py b/examples/vhosts.py index 50e85494..40dc7ba5 100644 --- a/examples/vhosts.py +++ b/examples/vhosts.py @@ -1,11 +1,15 @@ from sanic.response import text from sanic import Sanic +from sanic.blueprints import Blueprint # Usage # curl -H "Host: example.com" localhost:8000 # curl -H "Host: sub.example.com" localhost:8000 +# curl -H "Host: bp.example.com" localhost:8000/question +# curl -H "Host: bp.example.com" localhost:8000/answer app = Sanic() +bp = Blueprint("bp", host="bp.example.com") @app.route('/', host="example.com") async def hello(request): @@ -14,5 +18,15 @@ async def hello(request): async def hello(request): return text("42") +@bp.route("/question") +async def hello(request): + return text("What is the meaning of life?") + +@bp.route("/answer") +async def hello(request): + return text("42") + +app.register_blueprint(bp) + if __name__ == '__main__': app.run(host="0.0.0.0", port=8000) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 92e376f1..4cf460f3 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -18,14 +18,17 @@ class BlueprintSetup: #: blueprint. self.url_prefix = url_prefix - def add_route(self, handler, uri, methods): + def add_route(self, handler, uri, methods, host=None): """ A helper method to register a handler to the application url routes. """ if self.url_prefix: uri = self.url_prefix + uri - self.app.route(uri=uri, methods=methods)(handler) + if host is None: + host = self.blueprint.host + + self.app.route(uri=uri, methods=methods, host=host)(handler) def add_exception(self, handler, *args, **kwargs): """ @@ -53,7 +56,7 @@ class BlueprintSetup: class Blueprint: - def __init__(self, name, url_prefix=None): + def __init__(self, name, url_prefix=None, host=None): """ Creates a new blueprint :param name: Unique name of the blueprint @@ -63,6 +66,7 @@ class Blueprint: self.url_prefix = url_prefix self.deferred_functions = [] self.listeners = defaultdict(list) + self.host = host def record(self, func): """ diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index f7b9b8ef..04c5c59d 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -59,6 +59,62 @@ def test_several_bp_with_url_prefix(): request, response = sanic_endpoint_test(app, uri='/test2/') assert response.text == 'Hello2' +def test_bp_with_host(): + app = Sanic('test_bp_host') + bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com") + + @bp.route('/') + def handler(request): + return text('Hello') + + app.blueprint(bp) + headers = {"Host": "example.com"} + request, response = sanic_endpoint_test(app, uri='/test1/', + headers=headers) + + assert response.text == 'Hello' + + +def test_several_bp_with_host(): + app = Sanic('test_text') + bp = Blueprint('test_text', + url_prefix='/test', + host="example.com") + bp2 = Blueprint('test_text2', + url_prefix='/test', + host="sub.example.com") + + @bp.route('/') + def handler(request): + return text('Hello') + + @bp2.route('/') + def handler2(request): + return text('Hello2') + + @bp2.route('/other/') + def handler2(request): + return text('Hello3') + + + app.blueprint(bp) + app.blueprint(bp2) + + assert bp.host == "example.com" + headers = {"Host": "example.com"} + request, response = sanic_endpoint_test(app, uri='/test/', + headers=headers) + assert response.text == 'Hello' + + assert bp2.host == "sub.example.com" + headers = {"Host": "sub.example.com"} + request, response = sanic_endpoint_test(app, uri='/test/', + headers=headers) + + assert response.text == 'Hello2' + request, response = sanic_endpoint_test(app, uri='/test/other/', + headers=headers) + assert response.text == 'Hello3' def test_bp_middleware(): app = Sanic('test_middleware') @@ -162,4 +218,4 @@ def test_bp_static(): request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 - assert response.body == current_file_contents \ No newline at end of file + assert response.body == current_file_contents From 15e4ec7ffb25e33a5dafa5950c18a25062e16425 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 10 Jan 2017 22:08:15 -0800 Subject: [PATCH 213/977] add ability to override default host in blueprint --- sanic/blueprints.py | 8 ++++---- tests/test_blueprints.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 4cf460f3..583aa244 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -87,18 +87,18 @@ class Blueprint: for deferred in self.deferred_functions: deferred(state) - def route(self, uri, methods=None): + def route(self, uri, methods=None, host=None): """ """ def decorator(handler): - self.record(lambda s: s.add_route(handler, uri, methods)) + self.record(lambda s: s.add_route(handler, uri, methods, host)) return handler return decorator - def add_route(self, handler, uri, methods=None): + def add_route(self, handler, uri, methods=None, host=None): """ """ - self.record(lambda s: s.add_route(handler, uri, methods)) + self.record(lambda s: s.add_route(handler, uri, methods, host)) return handler def listener(self, event): diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 04c5c59d..75109e2c 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -67,13 +67,22 @@ def test_bp_with_host(): def handler(request): return text('Hello') + @bp.route('/', host="sub.example.com") + def handler(request): + return text('Hello subdomain!') + app.blueprint(bp) headers = {"Host": "example.com"} request, response = sanic_endpoint_test(app, uri='/test1/', headers=headers) - assert response.text == 'Hello' + headers = {"Host": "sub.example.com"} + request, response = sanic_endpoint_test(app, uri='/test1/', + headers=headers) + + assert response.text == 'Hello subdomain!' + def test_several_bp_with_host(): app = Sanic('test_text') From 9dd954bccde50d942173aaf861fdedb51a502e71 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 11 Jan 2017 16:55:34 -0600 Subject: [PATCH 214/977] Update request.form to work with __getitem__ --- sanic/request.py | 3 +++ tests/{tests_server_events.py => test_server_events.py} | 0 2 files changed, 3 insertions(+) rename tests/{tests_server_events.py => test_server_events.py} (100%) diff --git a/sanic/request.py b/sanic/request.py index 7ca5523c..5c4a7db4 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -25,6 +25,9 @@ class RequestParameters(dict): self.super = super() self.super.__init__(*args, **kwargs) + def __getitem__(self, name): + return self.get(name) + def get(self, name, default=None): values = self.super.get(name) return values[0] if values else default diff --git a/tests/tests_server_events.py b/tests/test_server_events.py similarity index 100% rename from tests/tests_server_events.py rename to tests/test_server_events.py From 8c5e214131b5f1e5b54af439103cff7b8a1e5973 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 12 Jan 2017 19:54:34 -0500 Subject: [PATCH 215/977] html and tests pass --- requirements-dev.txt | 1 + sanic/exceptions.py | 124 +++++++++++++++++++++++++++++-- tests/test_exceptions.py | 9 ++- tests/test_exceptions_handler.py | 24 ++++++ 4 files changed, 152 insertions(+), 6 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1c34d695..8e0ffac1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,3 +12,4 @@ kyoukai falcon tornado aiofiles +beautifulsoup4 diff --git a/sanic/exceptions.py b/sanic/exceptions.py index b9e6bf00..430f1d29 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -1,6 +1,104 @@ -from .response import text +from .response import text, html from .log import log -from traceback import format_exc +from traceback import format_exc, extract_tb +import sys + +TRACEBACK_STYLE = ''' + +''' + +TRACEBACK_WRAPPER_HTML = ''' + + + {style} + + +

{exc_name}

+

{exc_value}

+
+

Traceback (most recent call last):

+ {frame_html} +

+ {exc_name}: {exc_value} + while handling uri {uri} +

+
+ + +''' + +TRACEBACK_LINE_HTML = ''' +
+

+ File {0.filename}, line {0.lineno}, + in {0.name} +

+

{0.line}

+
+''' + +INTERNAL_SERVER_ERROR_HTML = ''' +

Internal Server Error

+

+ The server encountered an internal error and cannot complete + your request. +

+''' class SanicException(Exception): @@ -46,6 +144,21 @@ class Handler: self.handlers = {} self.sanic = sanic + def _render_traceback_html(self, exception, request): + exc_type, exc_value, tb = sys.exc_info() + frames = extract_tb(tb) + + frame_html = [] + for frame in frames: + frame_html.append(TRACEBACK_LINE_HTML.format(frame)) + + return TRACEBACK_WRAPPER_HTML.format( + style=TRACEBACK_STYLE, + exc_name=exc_type.__name__, + exc_value=exc_value, + frame_html=''.join(frame_html), + uri=request.url) + def add(self, exception, handler): self.handlers[exception] = handler @@ -77,11 +190,12 @@ class Handler: 'Error: {}'.format(exception), status=getattr(exception, 'status_code', 500)) elif self.sanic.debug: + html_output = self._render_traceback_html(exception, request) + response_message = ( 'Exception occurred while handling uri: "{}"\n{}'.format( request.url, format_exc())) log.error(response_message) - return text(response_message, status=500) + return html(html_output, status=500) else: - return text( - 'An error occurred while generating the response', status=500) + return html(INTERNAL_SERVER_ERROR_HTML, status=500) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 5cebfb87..a81e0d09 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,4 +1,5 @@ import pytest +from bs4 import BeautifulSoup from sanic import Sanic from sanic.response import text @@ -75,7 +76,13 @@ def test_handled_unhandled_exception(exception_app): request, response = sanic_endpoint_test( exception_app, uri='/divide_by_zero') assert response.status == 500 - assert response.body == b'An error occurred while generating the response' + soup = BeautifulSoup(response.body, 'html.parser') + assert soup.h1.text == 'Internal Server Error' + + message = " ".join(soup.p.text.split()) + assert message == ( + "The server encountered an internal error and " + "cannot complete your request.") def test_exception_in_exception_handler(exception_app): diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index 2e8bc359..c56713b6 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -2,6 +2,7 @@ from sanic import Sanic from sanic.response import text from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.utils import sanic_endpoint_test +from bs4 import BeautifulSoup exception_handler_app = Sanic('test_exception_handler') @@ -21,6 +22,12 @@ def handler_3(request): raise NotFound("OK") +@exception_handler_app.route('/4') +def handler_4(request): + foo = bar + return text(foo) + + @exception_handler_app.exception(NotFound, ServerError) def handler_exception(request, exception): return text("OK") @@ -47,3 +54,20 @@ def test_text_exception__handler(): exception_handler_app, uri='/random') assert response.status == 200 assert response.text == 'OK' + + +def test_html_traceback_output_in_debug_mode(): + request, response = sanic_endpoint_test( + exception_handler_app, uri='/4', debug=True) + assert response.status == 500 + soup = BeautifulSoup(response.body, 'html.parser') + html = str(soup) + + assert 'response = handler(request, *args, **kwargs)' in html + assert 'handler_4' in html + assert 'foo = bar' in html + + summary_text = " ".join(soup.select('.summary')[0].text.split()) + assert ( + "NameError: name 'bar' " + "is not defined while handling uri /4") == summary_text From 6e53fa03f5ea4ef74b95758244c98c1f0ec243f0 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 12 Jan 2017 20:03:35 -0500 Subject: [PATCH 216/977] add beautifulsoup4 to tox --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index a2f89206..009d971c 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ python = deps = aiohttp pytest + beautifulsoup4 commands = pytest tests {posargs} From 974c857a97af70e47a7c9a55d66b620d9b2aa8d8 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 12 Jan 2017 19:22:47 -0800 Subject: [PATCH 217/977] add a list of extensions --- README.md | 1 + docs/extensions.md | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 docs/extensions.md diff --git a/README.md b/README.md index 34565545..e73fefd3 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ if __name__ == "__main__": * [Custom Protocol](docs/custom_protocol.md) * [Testing](docs/testing.md) * [Deploying](docs/deploying.md) + * [Extensions](docs/extensions.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 00000000..dbd379b8 --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,6 @@ +# Sanic Extensions + +A list of Sanic extensions created by the community. + + * [Sessions](https://github.com/subyraman/sanic_session) -- Support for sessions. Allows using redis, memcache or an in memory store. + * [CORS](https://github.com/ashleysommer/sanic-cors) -- A port of flask-cors. From a86cc32dff6850cf3ebec8f3df07a8d9492e65fc Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 12 Jan 2017 19:30:22 -0800 Subject: [PATCH 218/977] Update extensions.md Since there have been a few extensions discussed in open issues, it makes sense to start keeping track of them. --- docs/extensions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index dbd379b8..829ccf99 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -2,5 +2,5 @@ A list of Sanic extensions created by the community. - * [Sessions](https://github.com/subyraman/sanic_session) -- Support for sessions. Allows using redis, memcache or an in memory store. - * [CORS](https://github.com/ashleysommer/sanic-cors) -- A port of flask-cors. + * [Sessions](https://github.com/subyraman/sanic_session) — Support for sessions. Allows using redis, memcache or an in memory store. + * [CORS](https://github.com/ashleysommer/sanic-cors) — A port of flask-cors. From 02b9a0a29765f75e92760558975656d0b361f9b7 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Sat, 14 Jan 2017 00:41:54 -0500 Subject: [PATCH 219/977] add redirect code from @pcdinh --- sanic/response.py | 26 ++++++++++++++++++ tests/test_requests.py | 62 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index f2eb02e5..f6422d5f 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -169,3 +169,29 @@ async def file(location, mime_type=None, headers=None): headers=headers, content_type=mime_type, body_bytes=out_stream) + + +def redirect(to, headers=None, status=302, + content_type=None): + """ + Aborts execution and causes a 302 redirect (by default). + + :param to: path or fully qualified URL to redirect to + :param headers: optional dict of headers to include in the new request + :param status: status code (int) of the new request, defaults to 302 + :param content_type: + the content type (string) of the response, defaults to None + :returns: the redirecting Response + """ + if not content_type: + content_type = "text/html; charset=utf-8" + + headers = headers or {} + + # According to RFC 7231, a relative URI is now permitted. + headers['Location'] = to + + return HTTPResponse( + status=status, + headers=headers, + content_type=content_type) diff --git a/tests/test_requests.py b/tests/test_requests.py index ead76424..2cb41636 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,9 +1,10 @@ from json import loads as json_loads, dumps as json_dumps from sanic import Sanic -from sanic.response import json, text +from sanic.response import json, text, redirect from sanic.utils import sanic_endpoint_test from sanic.exceptions import ServerError +import pytest # ------------------------------------------------------------ # # GET @@ -188,3 +189,62 @@ 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_get_then_redirect_01') + + @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_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 7de3f7aa789bfc4fada08acb8b0b5a3bfc203553 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Sat, 14 Jan 2017 00:43:30 -0500 Subject: [PATCH 220/977] rename test app --- tests/test_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index 2cb41636..e88b5e28 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -193,7 +193,7 @@ def test_post_form_multipart_form_data(): @pytest.fixture def redirect_app(): - app = Sanic('test_get_then_redirect_01') + app = Sanic('test_redirection') @app.route('/redirect_init') async def redirect_init(request): From 7a1e089725ed103201bce187fba9d435e9c20d2c Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Sat, 14 Jan 2017 00:45:04 -0500 Subject: [PATCH 221/977] add headers none test --- tests/test_requests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_requests.py b/tests/test_requests.py index e88b5e28..b2ee8e78 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -224,6 +224,17 @@ def test_redirect_default_302(redirect_app): 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. From b5bbef09c5649e403e045d049c2df7592fca0dba Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Sat, 14 Jan 2017 00:47:28 -0500 Subject: [PATCH 222/977] add redirect method --- sanic/response.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index f6422d5f..9c7bd2b5 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -172,7 +172,7 @@ async def file(location, mime_type=None, headers=None): def redirect(to, headers=None, status=302, - content_type=None): + content_type="text/html; charset=utf-8"): """ Aborts execution and causes a 302 redirect (by default). @@ -180,12 +180,9 @@ def redirect(to, headers=None, status=302, :param headers: optional dict of headers to include in the new request :param status: status code (int) of the new request, defaults to 302 :param content_type: - the content type (string) of the response, defaults to None + the content type (string) of the response :returns: the redirecting Response """ - if not content_type: - content_type = "text/html; charset=utf-8" - headers = headers or {} # According to RFC 7231, a relative URI is now permitted. From 49fdc6563f5394a44f88cf4095de9e0e96fd5698 Mon Sep 17 00:00:00 2001 From: Matt Daue Date: Sat, 14 Jan 2017 07:16:59 -0500 Subject: [PATCH 223/977] Add SSL to server Add ssl variable passthrough to following: -- sanic.run -- server.serve Add ssl variable to loop.create_server to enable built-in async context socket wrapper Update documentation Tested with worker = 1, and worker = 2. Signed-off-by: Matt Daue --- README.md | 12 ++++++++++++ sanic/sanic.py | 14 ++++++++++---- sanic/server.py | 4 +++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 34565545..1d9a6c9f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,18 @@ if __name__ == "__main__": ## Installation * `python -m pip install sanic` +## Use SSL + * Optionally pass in an SSLContext: +``` +import ssl +certificate = "/path/to/certificate" +keyfile = "/path/to/keyfile" +context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) +context.load_cert_chain(certificate, keyfile=keyfile) + +app.run(host="0.0.0.0", port=8443, ssl=context) +``` + ## Documentation * [Getting started](docs/getting_started.md) * [Request Data](docs/request_data.md) diff --git a/sanic/sanic.py b/sanic/sanic.py index 6926050c..ff6e468e 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -245,9 +245,9 @@ class Sanic: # -------------------------------------------------------------------- # def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, - after_start=None, before_stop=None, after_stop=None, sock=None, - workers=1, loop=None, protocol=HttpProtocol, backlog=100, - stop_event=None): + after_start=None, before_stop=None, after_stop=None, ssl=None, + sock=None, workers=1, loop=None, protocol=HttpProtocol, + backlog=100, stop_event=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -262,6 +262,7 @@ class Sanic: received before it is respected :param after_stop: Functions to be executed when all requests are complete + :param ssl: SSLContext for SSL encryption of worker(s) :param sock: Socket for the server to accept connections from :param workers: Number of processes received before it is respected @@ -278,6 +279,7 @@ class Sanic: 'host': host, 'port': port, 'sock': sock, + 'ssl': ssl, 'debug': debug, 'request_handler': self.handle_request, 'error_handler': self.error_handler, @@ -315,7 +317,11 @@ class Sanic: log.debug(self.config.LOGO) # Serve - log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) + if ssl is None: + proto = "http" + else: + proto = "https" + log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) try: if workers == 1: diff --git a/sanic/server.py b/sanic/server.py index ec207d26..4f0cfa97 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -225,7 +225,7 @@ def trigger_events(events, loop): def serve(host, port, request_handler, error_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, - request_timeout=60, sock=None, request_max_size=None, + request_timeout=60, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100): """ Starts asynchronous HTTP Server on an individual process. @@ -243,6 +243,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, received after it is respected. Takes single argumenet `loop` :param debug: Enables debug output (slows server) :param request_timeout: time in seconds + :param ssl: SSLContext :param sock: Socket for the server to accept connections from :param request_max_size: size in bytes, `None` for no limit :param reuse_port: `True` for multiple workers @@ -275,6 +276,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, server, host, port, + ssl=ssl, reuse_port=reuse_port, sock=sock, backlog=backlog From e2a16f96a8638fdb50deae6221dba37be6277242 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 14 Jan 2017 11:24:31 -0600 Subject: [PATCH 224/977] Increment version to 0.2.0 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 6b9b3a80..6f529eea 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.9' +__version__ = '0.2.0' __all__ = ['Sanic', 'Blueprint'] From 2cf4baddfbfa9589ca99c95df68dc24c91fe9541 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 16 Jan 2017 23:27:50 +0000 Subject: [PATCH 225/977] Moved Remote-Addr header to request.ip so it can be pulled on-demand --- docs/request_data.md | 1 + sanic/request.py | 9 +++++++-- sanic/server.py | 7 ++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/request_data.md b/docs/request_data.md index 8891d07f..bcb62ef9 100644 --- a/docs/request_data.md +++ b/docs/request_data.md @@ -9,6 +9,7 @@ The following request variables are accessible as properties: `request.args` (dict) - Query String variables. Use getlist to get multiple of the same name `request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name `request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type +`request.ip` (str) - IP address of the requester See request.py for more information diff --git a/sanic/request.py b/sanic/request.py index 5c4a7db4..52ec469b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -41,18 +41,19 @@ class Request(dict): Properties of an HTTP request such as URL, headers, etc. """ __slots__ = ( - 'url', 'headers', 'version', 'method', '_cookies', + 'url', 'headers', 'version', 'method', '_cookies', 'transport', 'query_string', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', ) - def __init__(self, url_bytes, headers, version, method): + def __init__(self, url_bytes, headers, version, method, transport): # TODO: Content-Encoding detection url_parsed = parse_url(url_bytes) self.url = url_parsed.path.decode('utf-8') self.headers = headers self.version = version self.method = method + self.transport = transport self.query_string = None if url_parsed.query: self.query_string = url_parsed.query.decode('utf-8') @@ -139,6 +140,10 @@ class Request(dict): self._cookies = {} return self._cookies + @property + def ip(self): + return self.transport.get_extra_info('peername') + File = namedtuple('File', ['type', 'body', 'name']) diff --git a/sanic/server.py b/sanic/server.py index ec207d26..ade02564 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -121,15 +121,12 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((name.decode(), value.decode('utf-8'))) def on_headers_complete(self): - remote_addr = self.transport.get_extra_info('peername') - if remote_addr: - self.headers.append(('Remote-Addr', '%s:%s' % remote_addr)) - self.request = Request( url_bytes=self.url, headers=CIMultiDict(self.headers), version=self.parser.get_http_version(), - method=self.parser.get_method().decode() + method=self.parser.get_method().decode(), + transport=self.transport ) def on_body(self, body): From 213580ea7808b3d35e12dcaead03cb443b8a5e49 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 16 Jan 2017 15:45:29 -0800 Subject: [PATCH 226/977] cache the remote IP property --- sanic/request.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index 52ec469b..fbd41696 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -65,6 +65,7 @@ class Request(dict): self.parsed_files = None self.parsed_args = None self._cookies = None + self._ip = None @property def json(self): @@ -142,7 +143,11 @@ class Request(dict): @property def ip(self): - return self.transport.get_extra_info('peername') + if self._ip is None: + self._ip = self.transport.get_extra_info('peername') + return self._ip + else: + return self._ip File = namedtuple('File', ['type', 'body', 'name']) From 5c344f7efa7baaa951f1a4703fa7c739285a3e10 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Mon, 16 Jan 2017 17:51:56 -0600 Subject: [PATCH 227/977] Remove redundant else --- sanic/request.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index fbd41696..7f3b87eb 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -145,9 +145,7 @@ class Request(dict): def ip(self): if self._ip is None: self._ip = self.transport.get_extra_info('peername') - return self._ip - else: - return self._ip + return self._ip File = namedtuple('File', ['type', 'body', 'name']) From 41918eaf0a8d04d566fb0adbbcbc14f5f5bfe176 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 16 Jan 2017 16:12:42 -0800 Subject: [PATCH 228/977] Trimmed down features of CIMultiDict --- requirements.txt | 1 - sanic/response.py | 4 ++-- sanic/server.py | 24 +++++++++++++++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3acfbb1f..cef8660e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,3 @@ httptools ujson uvloop aiofiles -multidict diff --git a/sanic/response.py b/sanic/response.py index 9c7bd2b5..bb3a1f9d 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -106,11 +106,11 @@ class HTTPResponse: for name, value in self.headers.items(): try: headers += ( - b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) + b'%b: %b\r\n' % (name.title().encode(), value.encode('utf-8'))) except AttributeError: headers += ( b'%b: %b\r\n' % ( - str(name).encode(), str(value).encode('utf-8'))) + str(name).title().encode(), str(value).encode('utf-8'))) # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all diff --git a/sanic/server.py b/sanic/server.py index ec207d26..62a4c1a7 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,7 +1,6 @@ import asyncio from functools import partial from inspect import isawaitable -from multidict import CIMultiDict from signal import SIGINT, SIGTERM from time import time from httptools import HttpRequestParser @@ -18,11 +17,30 @@ from .request import Request from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage +current_time = None + + class Signal: stopped = False -current_time = None +class CIDict(dict): + """ + Case Insensitive dict where all keys are converted to lowercase + This does not maintain the inputted case when calling items() or keys() + in favor of speed, since headers are case insensitive + """ + def get(self, key, default=None): + return super().get(key.casefold(), default) + + def __getitem__(self, key): + return super().__getitem__(key.casefold()) + + def __setitem__(self, key, value): + return super().__setitem__(key.casefold(), value) + + def __contains__(self, key): + return super().__contains__(key.casefold()) class HttpProtocol(asyncio.Protocol): @@ -127,7 +145,7 @@ class HttpProtocol(asyncio.Protocol): self.request = Request( url_bytes=self.url, - headers=CIMultiDict(self.headers), + headers=CIDict(self.headers), version=self.parser.get_http_version(), method=self.parser.get_method().decode() ) From ccbbce003648a8f76e83b84f05e4621070651d44 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 16 Jan 2017 16:55:55 -0800 Subject: [PATCH 229/977] Fix header capitalization on input also removed redundant utf-8 in encodes/decodes --- sanic/response.py | 8 ++++---- sanic/server.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index bb3a1f9d..9ec2d91a 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -83,10 +83,10 @@ class HTTPResponse: if body is not None: try: # Try to encode it regularly - self.body = body.encode('utf-8') + self.body = body.encode() except AttributeError: # Convert it to a str if you can't - self.body = str(body).encode('utf-8') + self.body = str(body).encode() else: self.body = body_bytes @@ -106,11 +106,11 @@ class HTTPResponse: for name, value in self.headers.items(): try: headers += ( - b'%b: %b\r\n' % (name.title().encode(), value.encode('utf-8'))) + b'%b: %b\r\n' % (name.encode(), value.encode())) except AttributeError: headers += ( b'%b: %b\r\n' % ( - str(name).title().encode(), str(value).encode('utf-8'))) + str(name).encode(), str(value).encode())) # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all diff --git a/sanic/server.py b/sanic/server.py index 62a4c1a7..6d924374 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -136,7 +136,7 @@ class HttpProtocol(asyncio.Protocol): exception = PayloadTooLarge('Payload Too Large') self.write_error(exception) - self.headers.append((name.decode(), value.decode('utf-8'))) + self.headers.append((name.decode().casefold(), value.decode())) def on_headers_complete(self): remote_addr = self.transport.get_extra_info('peername') From 638fbcb619d8a316c4d4c9440999c886d7e5b9ab Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 16 Jan 2017 17:03:55 -0800 Subject: [PATCH 230/977] Encoding needs a default --- sanic/response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 9ec2d91a..ba10b8c4 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -106,11 +106,11 @@ class HTTPResponse: for name, value in self.headers.items(): try: headers += ( - b'%b: %b\r\n' % (name.encode(), value.encode())) + b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) except AttributeError: headers += ( b'%b: %b\r\n' % ( - str(name).encode(), str(value).encode())) + str(name).encode(), str(value).encode('utf-8'))) # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all From 9bc69f7de96f030c95bc4d1861bf25d2ec96e294 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 16 Jan 2017 17:21:57 -0800 Subject: [PATCH 231/977] use hasattr --- sanic/request.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 7f3b87eb..26176687 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -65,7 +65,6 @@ class Request(dict): self.parsed_files = None self.parsed_args = None self._cookies = None - self._ip = None @property def json(self): @@ -143,7 +142,7 @@ class Request(dict): @property def ip(self): - if self._ip is None: + if not hasattr(self, '_ip'): self._ip = self.transport.get_extra_info('peername') return self._ip From 5903dd293909c56c697fa7972303028afb7e3884 Mon Sep 17 00:00:00 2001 From: Guilherme Polo Date: Tue, 17 Jan 2017 02:58:45 -0200 Subject: [PATCH 232/977] cannot use the new .ip without updating __slots__ --- sanic/request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/request.py b/sanic/request.py index 26176687..0cd7c738 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -44,6 +44,7 @@ class Request(dict): 'url', 'headers', 'version', 'method', '_cookies', 'transport', 'query_string', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', + '_ip', ) def __init__(self, url_bytes, headers, version, method, transport): From ba1e00658578b27316d676dcaeedd1acb9bd23b6 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 17 Jan 2017 15:38:20 -0800 Subject: [PATCH 233/977] update logging placement --- examples/override_logging.py | 2 +- sanic/sanic.py | 22 ++++++++++++++-------- tests/test_logging.py | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/examples/override_logging.py b/examples/override_logging.py index 25fd78de..117d63bf 100644 --- a/examples/override_logging.py +++ b/examples/override_logging.py @@ -14,7 +14,7 @@ logging.basicConfig( log = logging.getLogger() # Set logger to override default basicConfig -sanic = Sanic(logger=True) +sanic = Sanic() @sanic.route("/") def test(request): log.info("received request; responding with 'hey'") diff --git a/sanic/sanic.py b/sanic/sanic.py index 6926050c..5f8aeeb2 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -21,12 +21,7 @@ from os import set_inheritable class Sanic: def __init__(self, name=None, router=None, - error_handler=None, logger=None): - if logger is None: - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s: %(levelname)s: %(message)s" - ) + error_handler=None): if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) @@ -154,7 +149,8 @@ class Sanic: def register_blueprint(self, *args, **kwargs): # TODO: deprecate 1.0 log.warning("Use of register_blueprint will be deprecated in " - "version 1.0. Please use the blueprint method instead") + "version 1.0. Please use the blueprint method instead", + DeprecationWarning) return self.blueprint(*args, **kwargs) # -------------------------------------------------------------------- # @@ -247,7 +243,7 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, - stop_event=None): + stop_event=None, logger=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -269,6 +265,10 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s: %(levelname)s: %(message)s" + ) self.error_handler.debug = True self.debug = debug self.loop = loop @@ -350,6 +350,12 @@ class Sanic: :param stop_event: if provided, is used as a stop signal :return: """ + # In case this is called directly, we configure logging here too. + # This won't interfere with the same call from run() + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s: %(levelname)s: %(message)s" + ) server_settings['reuse_port'] = True # Create a stop event to be triggered by a signal diff --git a/tests/test_logging.py b/tests/test_logging.py index 65de28c2..b3e3c1fc 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -19,7 +19,7 @@ def test_log(): stream=log_stream ) log = logging.getLogger() - app = Sanic('test_logging', logger=True) + app = Sanic('test_logging') @app.route('/') def handler(request): log.info('hello world') From 573d1da0efa1812b78a137a78e28b5db8cbb8559 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 17 Jan 2017 18:28:22 -0600 Subject: [PATCH 234/977] Fixes write_error loop from bail_out function Fixes stack-overflow found in #307 --- sanic/server.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 711117dc..a36c47aa 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,4 +1,5 @@ import asyncio +import traceback from functools import partial from inspect import isawaitable from signal import SIGINT, SIGTERM @@ -189,9 +190,15 @@ class HttpProtocol(asyncio.Protocol): "Writing error failed, connection closed {}".format(e)) def bail_out(self, message): - exception = ServerError(message) - self.write_error(exception) - log.error(message) + if self.transport.is_closing(): + log.error( + "Connection closed before error was sent to user @ {}".format( + self.transport.get_extra_info('peername'))) + log.debug('Error experienced:\n{}'.format(traceback.format_exc())) + else: + exception = ServerError(message) + self.write_error(exception) + log.error(message) def cleanup(self): self.parser = None From ebce7b01c7b94521a01f891ca51f435ac398aae3 Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Thu, 19 Jan 2017 08:54:20 +1100 Subject: [PATCH 235/977] Add new guides to documentation index --- docs/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index b28d97f7..202a6d50 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,8 +15,10 @@ Guides class_based_views cookies static_files + custom_protocol testing deploying + extensions contributing From 5cfd8b9aa8c68c6a65cf39009dbdf647402ed48b Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Thu, 19 Jan 2017 08:58:13 +1100 Subject: [PATCH 236/977] Fix formatting of 'Final Word' in README --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 20352071..a95081cc 100644 --- a/README.rst +++ b/README.rst @@ -89,6 +89,8 @@ Limitations Final Thoughts -------------- +:: + ▄▄▄▄▄ ▀▀▀██████▄▄▄ _______________ ▄▄▄▄▄ █████████▄ / \ From 11f3c79a77c3afb63c3d21c51ceec96f818d40e9 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 13 Jan 2017 21:18:28 +0900 Subject: [PATCH 237/977] Feature: Routing overload When user specifies HTTP methods to function handlers, it automatically will be overloaded unless they duplicate. Example: # This is a new route. It works as before. @app.route('/overload', methods=['GET']) async def handler1(request): return text('OK1') # This is the exiting route but a new method. They are merged and # work as combined. The route will serve all of GET, POST and PUT. @app.route('/overload', methods=['POST', 'PUT']) async def handler2(request): return text('OK2') # This is the existing route and PUT method is the duplicated method. # It raises RouteExists. @app.route('/overload', methods=['PUT', 'DELETE']) async def handler3(request): return text('Duplicated') --- sanic/router.py | 36 ++++++++++++++++++++----- sanic/views.py | 35 ++++++++++++++++++++++++ tests/test_routes.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 6 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 9ab01b1c..a8ad7c13 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -3,6 +3,7 @@ from collections import defaultdict, namedtuple from functools import lru_cache from .config import Config from .exceptions import NotFound, InvalidUsage +from .views import CompositionView Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) Parameter = namedtuple('Parameter', ['name', 'cast']) @@ -78,9 +79,6 @@ class Router: self.hosts.add(host) uri = host + uri - if uri in self.routes_all: - raise RouteExists("Route already registered: {}".format(uri)) - # Dict for faster lookups of if method allowed if methods: methods = frozenset(methods) @@ -113,9 +111,35 @@ class Router: pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) pattern = re.compile(r'^{}$'.format(pattern_string)) - route = Route( - handler=handler, methods=methods, pattern=pattern, - parameters=parameters) + def merge_route(route, methods, handler): + # merge to the existing route when possible. + if not route.methods or not methods: + # method-unspecified routes are not mergeable. + raise RouteExists( + "Route already registered: {}".format(uri)) + elif route.methods.intersection(methods): + # already existing method is not overloadable. + duplicated = methods.intersection(route.methods) + raise RouteExists( + "Route already registered: {} [{}]".format( + uri, ','.join(list(duplicated)))) + if isinstance(route.handler, CompositionView): + view = route.handler + else: + view = CompositionView() + view.add(route.methods, route.handler) + view.add(methods, handler) + route = route._replace( + handler=view, methods=methods.union(route.methods)) + return route + + route = self.routes_all.get(uri) + if route: + route = merge_route(route, methods, handler) + else: + route = Route( + handler=handler, methods=methods, pattern=pattern, + parameters=parameters) self.routes_all[uri] = route if properties['unhashable']: diff --git a/sanic/views.py b/sanic/views.py index 0222b96f..640165fe 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -61,3 +61,38 @@ class HTTPMethodView: view.__doc__ = cls.__doc__ view.__module__ = cls.__module__ return view + + +class CompositionView: + """ Simple method-function mapped view for the sanic. + You can add handler functions to methods (get, post, put, patch, delete) + for every HTTP method you want to support. + + For example: + view = CompositionView() + view.add(['GET'], lambda request: text('I am get method')) + view.add(['POST', 'PUT'], lambda request: text('I am post/put method')) + + etc. + + If someone tries to use a non-implemented method, there will be a + 405 response. + """ + + def __init__(self): + self.handlers = {} + + def add(self, methods, handler): + for method in methods: + if method in self.handlers: + raise KeyError( + 'Method {} already is registered.'.format(method)) + self.handlers[method] = handler + + def __call__(self, request, *args, **kwargs): + handler = self.handlers.get(request.method.upper(), None) + if handler is None: + raise InvalidUsage( + 'Method {} not allowed for URL {}'.format( + request.method, request.url), status_code=405) + return handler(request, *args, **kwargs) diff --git a/tests/test_routes.py b/tests/test_routes.py index 149c71f9..9c671829 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -463,3 +463,67 @@ def test_remove_route_without_clean_cache(): request, response = sanic_endpoint_test(app, uri='/test') assert response.status == 200 + + +def test_overload_routes(): + app = Sanic('test_dynamic_route') + + @app.route('/overload', methods=['GET']) + async def handler1(request): + return text('OK1') + + @app.route('/overload', methods=['POST', 'PUT']) + async def handler2(request): + return text('OK2') + + request, response = sanic_endpoint_test(app, 'get', uri='/overload') + assert response.text == 'OK1' + + request, response = sanic_endpoint_test(app, 'post', uri='/overload') + assert response.text == 'OK2' + + request, response = sanic_endpoint_test(app, 'put', uri='/overload') + assert response.text == 'OK2' + + request, response = sanic_endpoint_test(app, 'delete', uri='/overload') + assert response.status == 405 + + with pytest.raises(RouteExists): + @app.route('/overload', methods=['PUT', 'DELETE']) + async def handler3(request): + return text('Duplicated') + + +def test_unmergeable_overload_routes(): + app = Sanic('test_dynamic_route') + + @app.route('/overload_whole') + async def handler1(request): + return text('OK1') + + with pytest.raises(RouteExists): + @app.route('/overload_whole', methods=['POST', 'PUT']) + async def handler2(request): + return text('Duplicated') + + request, response = sanic_endpoint_test(app, 'get', uri='/overload_whole') + assert response.text == 'OK1' + + request, response = sanic_endpoint_test(app, 'post', uri='/overload_whole') + assert response.text == 'OK1' + + + @app.route('/overload_part', methods=['GET']) + async def handler1(request): + return text('OK1') + + with pytest.raises(RouteExists): + @app.route('/overload_part') + async def handler2(request): + return text('Duplicated') + + request, response = sanic_endpoint_test(app, 'get', uri='/overload_part') + assert response.text == 'OK1' + + request, response = sanic_endpoint_test(app, 'post', uri='/overload_part') + assert response.status == 405 From 2c1ff5bf5df980ea23465b189ce3deddac672248 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 18 Jan 2017 19:40:20 -0800 Subject: [PATCH 238/977] allow using a list of hosts on a route --- examples/vhosts.py | 7 +++++++ sanic/router.py | 9 ++++++++- tests/test_vhosts.py | 18 +++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/examples/vhosts.py b/examples/vhosts.py index 40dc7ba5..810dc513 100644 --- a/examples/vhosts.py +++ b/examples/vhosts.py @@ -11,9 +11,16 @@ from sanic.blueprints import Blueprint app = Sanic() bp = Blueprint("bp", host="bp.example.com") +@app.route('/', host=["example.com", + "somethingelse.com", + "therestofyourdomains.com"]) +async def hello(request): + return text("Some defaults") + @app.route('/', host="example.com") async def hello(request): return text("Answer") + @app.route('/', host="sub.example.com") async def hello(request): return text("42") diff --git a/sanic/router.py b/sanic/router.py index a8ad7c13..e817be9c 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -76,8 +76,15 @@ class Router: if self.hosts is None: self.hosts = set(host) else: + if isinstance(host, list): + host = set(host) self.hosts.add(host) - uri = host + uri + if isinstance(host, str): + uri = host + uri + else: + for h in host: + self.add(uri, methods, handler, h) + return # Dict for faster lookups of if method allowed if methods: diff --git a/tests/test_vhosts.py b/tests/test_vhosts.py index 7bbbb813..660ebb5f 100644 --- a/tests/test_vhosts.py +++ b/tests/test_vhosts.py @@ -4,7 +4,7 @@ from sanic.utils import sanic_endpoint_test def test_vhosts(): - app = Sanic('test_text') + app = Sanic('test_vhosts') @app.route('/', host="example.com") async def handler(request): @@ -21,3 +21,19 @@ def test_vhosts(): headers = {"Host": "subdomain.example.com"} request, response = sanic_endpoint_test(app, headers=headers) assert response.text == "You're at subdomain.example.com!" + + +def test_vhosts_with_list(): + app = Sanic('test_vhosts') + + @app.route('/', host=["hello.com", "world.com"]) + async def handler(request): + return text("Hello, world!") + + headers = {"Host": "hello.com"} + request, response = sanic_endpoint_test(app, headers=headers) + assert response.text == "Hello, world!" + + headers = {"Host": "world.com"} + request, response = sanic_endpoint_test(app, headers=headers) + assert response.text == "Hello, world!" From bb83a25a52d187cd1577341e709f661ef4215dde Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 18 Jan 2017 21:45:30 -0800 Subject: [PATCH 239/977] remove logger from run --- sanic/sanic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 5f8aeeb2..94fcd983 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -243,7 +243,7 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, - stop_event=None, logger=None): + stop_event=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. From e9bfa30c1d97f6290fbb52e0153958728ec43474 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 18 Jan 2017 23:46:22 -0600 Subject: [PATCH 240/977] Add exception handling for closed transports Adds handling for closed transports in the server for `write_response` as well as `write_error`, letting it all flow to `bail_out` seemed to be a little light handed in terms of telling the logs where the error actually occured. Handling to fix the infinite `write_error` loop is still there and those exceptions will get reported on in the debug logs. --- sanic/server.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index a36c47aa..1b574c21 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -89,15 +89,14 @@ class HttpProtocol(asyncio.Protocol): def connection_lost(self, exc): self.connections.discard(self) self._timeout_handler.cancel() - self.cleanup() def connection_timeout(self): # Check if time_elapsed = current_time - self._last_request_time if time_elapsed < self.request_timeout: time_left = self.request_timeout - time_elapsed - self._timeout_handler = \ - self.loop.call_later(time_left, self.connection_timeout) + self._timeout_handler = ( + self.loop.call_later(time_left, self.connection_timeout)) else: if self._request_handler_task: self._request_handler_task.cancel() @@ -164,8 +163,8 @@ class HttpProtocol(asyncio.Protocol): def write_response(self, response): try: - keep_alive = self.parser.should_keep_alive() \ - and not self.signal.stopped + keep_alive = ( + self.parser.should_keep_alive() and not self.signal.stopped) self.transport.write( response.output( self.request.version, keep_alive, self.request_timeout)) @@ -175,6 +174,10 @@ class HttpProtocol(asyncio.Protocol): # Record that we received data self._last_request_time = current_time self.cleanup() + except RuntimeError: + log.error( + 'Connection lost before response written @ {}'.format( + self.request.ip)) except Exception as e: self.bail_out( "Writing response failed, connection closed {}".format(e)) @@ -185,16 +188,23 @@ class HttpProtocol(asyncio.Protocol): version = self.request.version if self.request else '1.1' self.transport.write(response.output(version)) self.transport.close() + except RuntimeError: + log.error( + 'Connection lost before error written @ {}'.format( + self.request.ip)) except Exception as e: self.bail_out( - "Writing error failed, connection closed {}".format(e)) + "Writing error failed, connection closed {}".format(e), + from_error=True) - def bail_out(self, message): - if self.transport.is_closing(): + def bail_out(self, message, from_error=False): + if from_error and self.transport.is_closing(): log.error( - "Connection closed before error was sent to user @ {}".format( + ("Transport closed @ {} and exception " + "experienced during error handling").format( self.transport.get_extra_info('peername'))) - log.debug('Error experienced:\n{}'.format(traceback.format_exc())) + log.debug( + 'Exception:\n{}'.format(traceback.format_exc())) else: exception = ServerError(message) self.write_error(exception) From cc43ee3b3db08c8b05ca8c9d67514f4b404e5a49 Mon Sep 17 00:00:00 2001 From: zkanda Date: Tue, 17 Jan 2017 19:17:42 +0800 Subject: [PATCH 241/977] Always log of there's an exception occured. --- sanic/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 430f1d29..f1d81878 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -173,6 +173,7 @@ class Handler: try: response = handler(request=request, exception=exception) except: + log.error(format_exc()) if self.sanic.debug: response_message = ( 'Exception raised in exception handler "{}" ' @@ -185,6 +186,7 @@ class Handler: return response def default(self, request, exception): + log.error(format_exc()) if issubclass(type(exception), SanicException): return text( 'Error: {}'.format(exception), From ba606598941d4c77771112158d44daaffb5dd440 Mon Sep 17 00:00:00 2001 From: James Michael DuPont Date: Thu, 19 Jan 2017 04:04:16 -0500 Subject: [PATCH 242/977] untie --- sanic/sanic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 94fcd983..5e00b55d 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -232,7 +232,7 @@ class Sanic: e, format_exc())) else: response = HTTPResponse( - "An error occured while handling an error") + "An error occurred while handling an error") response_callback(response) From 0a160c4a0b7581a51229efc10928541e8752f6b9 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 19 Jan 2017 23:56:51 +0900 Subject: [PATCH 243/977] For function decorators, ['GET'] is the default methods --- sanic/blueprints.py | 2 +- sanic/sanic.py | 2 +- tests/test_routes.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 583aa244..101c698f 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -87,7 +87,7 @@ class Blueprint: for deferred in self.deferred_functions: deferred(state) - def route(self, uri, methods=None, host=None): + def route(self, uri, methods=frozenset({'GET'}), host=None): """ """ def decorator(handler): diff --git a/sanic/sanic.py b/sanic/sanic.py index 94fcd983..5182b9fb 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -46,7 +46,7 @@ class Sanic: # -------------------------------------------------------------------- # # Decorator - def route(self, uri, methods=None, host=None): + def route(self, uri, methods=frozenset({'GET'}), host=None): """ Decorates a function to be registered as a route :param uri: path of the URL diff --git a/tests/test_routes.py b/tests/test_routes.py index 9c671829..be1fb29e 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -497,7 +497,7 @@ def test_overload_routes(): def test_unmergeable_overload_routes(): app = Sanic('test_dynamic_route') - @app.route('/overload_whole') + @app.route('/overload_whole', methods=None) async def handler1(request): return text('OK1') From 30862c0a3e90dcaa0c19890a919ea89f1e5970c8 Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Fri, 20 Jan 2017 09:32:08 +1100 Subject: [PATCH 244/977] Update documentation generation instructions. --- docs/contributing.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index a40654cc..667978ca 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -13,8 +13,7 @@ Sanic's documentation is built using [sphinx](http://www.sphinx-doc.org/en/1.5.1 To generate the documentation from scratch: ```bash -rm -f docs/_api/* -sphinx-apidoc -o docs/_api/ sanic +sphinx-apidoc -fo docs/_api/ sanic sphinx-build -b html docs docs/_build ``` From ed4752bbc00f420d65a9314f21b380598b801b09 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Thu, 19 Jan 2017 16:35:48 -0600 Subject: [PATCH 245/977] Move transport close to finally statment --- sanic/server.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 1b574c21..4b303d41 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -168,12 +168,6 @@ class HttpProtocol(asyncio.Protocol): self.transport.write( response.output( self.request.version, keep_alive, self.request_timeout)) - if not keep_alive: - self.transport.close() - else: - # Record that we received data - self._last_request_time = current_time - self.cleanup() except RuntimeError: log.error( 'Connection lost before response written @ {}'.format( @@ -181,13 +175,19 @@ class HttpProtocol(asyncio.Protocol): except Exception as e: self.bail_out( "Writing response failed, connection closed {}".format(e)) + finally: + if not keep_alive: + self.transport.close() + else: + # Record that we received data + self._last_request_time = current_time + self.cleanup() def write_error(self, exception): try: response = self.error_handler.response(self.request, exception) version = self.request.version if self.request else '1.1' self.transport.write(response.output(version)) - self.transport.close() except RuntimeError: log.error( 'Connection lost before error written @ {}'.format( @@ -196,6 +196,8 @@ class HttpProtocol(asyncio.Protocol): self.bail_out( "Writing error failed, connection closed {}".format(e), from_error=True) + finally: + self.transport.close() def bail_out(self, message, from_error=False): if from_error and self.transport.is_closing(): From 6176964bdfddc0c10a5f51ca3b9461f2ecf9788b Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Fri, 20 Jan 2017 14:18:52 +1100 Subject: [PATCH 246/977] Clarify, reformat, and add to documentation guides (#318) * Reorder and clarify the 'Request Data' guide, adding a section on RequestParameters * Clarify routing guide, adding introduction and HTTP types sections * Clarify the use-cases of middleware * Clean up formatting in the exceptions guide and add some common exceptions. * Fix formatting of blueprints and add use-case example. * Clarify the class-based views guide * Clarify and fix formatting of cookies guide * Clarify static files guide * Clarify the custom protocols guide. * Add more information to the deploying guide * Fix broken list in the community extensions list. * Add introduction and improve warning to contributing guide * Expand getting started guide * Reorder guides and add links between them * Standardise heading capitalisation --- docs/blueprints.md | 103 ++++++++++++++++++++++++-------- docs/class_based_views.md | 40 ++++++++++--- docs/contributing.md | 20 +++++-- docs/cookies.md | 50 ++++++++-------- docs/custom_protocol.md | 56 ++++++++++-------- docs/deploying.md | 56 +++++++++++++----- docs/exceptions.md | 35 ++++++++--- docs/extensions.md | 9 ++- docs/getting_started.md | 40 +++++++------ docs/index.rst | 6 +- docs/middleware.md | 65 +++++++++++++------- docs/request_data.md | 121 ++++++++++++++++++++++++++------------ docs/routing.md | 81 ++++++++++++++++++++++--- docs/static_files.md | 11 +++- docs/testing.md | 4 ++ 15 files changed, 495 insertions(+), 202 deletions(-) diff --git a/docs/blueprints.md b/docs/blueprints.md index adc40dfa..ee3eab46 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -1,25 +1,20 @@ # Blueprints Blueprints are objects that can be used for sub-routing within an application. -Instead of adding routes to the application object, blueprints define similar +Instead of adding routes to the application instance, blueprints define similar methods for adding routes, which are then registered with the application in a flexible and pluggable manner. -## Why? - -Blueprints are especially useful for larger applications, where your application -logic can be broken down into several groups or areas of responsibility. - -It is also useful for API versioning, where one blueprint may point at -`/v1/`, and another pointing at `/v2/`. - +Blueprints are especially useful for larger applications, where your +application logic can be broken down into several groups or areas of +responsibility. ## My First Blueprint The following shows a very simple blueprint that registers a handler-function at the root `/` of your application. -Suppose you save this file as `my_blueprint.py`, this can be imported in your +Suppose you save this file as `my_blueprint.py`, which can be imported into your main application later. ```python @@ -34,7 +29,8 @@ async def bp_root(request): ``` -## Registering Blueprints +## Registering blueprints + Blueprints must be registered with the application. ```python @@ -48,14 +44,19 @@ app.run(host='0.0.0.0', port=8000, debug=True) ``` This will add the blueprint to the application and register any routes defined -by that blueprint. -In this example, the registered routes in the `app.router` will look like: +by that blueprint. In this example, the registered routes in the `app.router` +will look like: ```python [Route(handler=, methods=None, pattern=re.compile('^/$'), parameters=[])] ``` -## Middleware +## Using blueprints + +Blueprints have much the same functionality as an application instance. + +### Middleware + Using blueprints allows you to also register middleware globally. ```python @@ -72,30 +73,36 @@ async def halt_response(request, response): return text('I halted the response') ``` -## Exceptions -Exceptions can also be applied exclusively to blueprints globally. +### Exceptions + +Exceptions can be applied exclusively to blueprints globally. ```python @bp.exception(NotFound) def ignore_404s(request, exception): return text("Yep, I totally found the page: {}".format(request.url)) +``` -## Static files -Static files can also be served globally, under the blueprint prefix. +### Static files + +Static files can be served globally, under the blueprint prefix. ```python bp.static('/folder/to/serve', '/web/path') ``` -## Start and Stop -Blueprints and run functions during the start and stop process of the server. -If running in multiprocessor mode (more than 1 worker), these are triggered after the workers fork +## Start and stop + +Blueprints can run functions during the start and stop process of the server. +If running in multiprocessor mode (more than 1 worker), these are triggered +after the workers fork. + Available events are: - * before_server_start - Executed before the server begins to accept connections - * after_server_start - Executed after the server begins to accept connections - * before_server_stop - Executed before the server stops accepting connections - * after_server_stop - Executed after the server is stopped and all requests are complete +- `before_server_start`: Executed before the server begins to accept connections +- `after_server_start`: Executed after the server begins to accept connections +- `before_server_stop`: Executed before the server stops accepting connections +- `after_server_stop`: Executed after the server is stopped and all requests are complete ```python bp = Blueprint('my_blueprint') @@ -109,3 +116,49 @@ async def setup_connection(): async def close_connection(): await database.close() ``` + +## Use-case: API versioning + +Blueprints can be very useful for API versioning, where one blueprint may point +at `/v1/`, and another pointing at `/v2/`. + +When a blueprint is initialised, it can take an optional `url_prefix` argument, +which will be prepended to all routes defined on the blueprint. This feature +can be used to implement our API versioning scheme. + +```python +# blueprints.py +from sanic.response import text +from sanic import Blueprint + +blueprint_v1 = Blueprint('v1') +blueprint_v2 = Blueprint('v2') + +@blueprint_v1.route('/') +async def api_v1_root(request): + return text('Welcome to version 1 of our documentation') + +@blueprint_v2.route('/') +async def api_v2_root(request): + return text('Welcome to version 2 of our documentation') +``` + +When we register our blueprints on the app, the routes `/v1` and `/v2` will now +point to the individual blueprints, which allows the creation of *sub-sites* +for each API version. + +```python +# main.py +from sanic import Sanic +from blueprints import blueprint_v1, blueprint_v2 + +app = Sanic(__name__) +app.blueprint(blueprint_v1) +app.blueprint(blueprint_v2) + +app.run(host='0.0.0.0', port=8000, debug=True) +``` + +**Previous:** [Exceptions](exceptions.html) + +**Next:** [Class-based views](class_based_views.html) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index 84a5b952..e34f432b 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -1,8 +1,25 @@ -# Class based views +# Class-Based Views -Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response. +Class-based views are simply classes which implement response behaviour to +requests. They provide a way to compartmentalise handling of different HTTP +request types at the same endpoint. Rather than defining and decorating three +different handler functions, one for each of an endpoint's supported request +type, the endpoint can be assigned a class-based view. + +## Defining views + +A class-based view should subclass `HTTPMethodView`. You can then implement +class methods for every HTTP request type you want to support. If a request is +received that has no defined method, a `405: Method not allowed` response will +be generated. + +To register a class-based view on an endpoint, the `app.add_route` method is +used. The first argument should be the defined class with the method `as_view` +invoked, and the second should be the URL endpoint. + +The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class +using all these methods would look like the following. -## Examples ```python from sanic import Sanic from sanic.views import HTTPMethodView @@ -10,7 +27,6 @@ from sanic.response import text app = Sanic('some_name') - class SimpleView(HTTPMethodView): def get(self, request): @@ -32,7 +48,10 @@ app.add_route(SimpleView.as_view(), '/') ``` -If you need any url params just mention them in method definition: +## URL parameters + +If you need any URL parameters, as discussed in the routing guide, include them +in the method definition. ```python class NameView(HTTPMethodView): @@ -41,10 +60,12 @@ class NameView(HTTPMethodView): return text('Hello {}'.format(name)) app.add_route(NameView.as_view(), '/') - ``` -If you want to add decorator for class, you could set decorators variable +## Decorators + +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. ``` class ViewWithDecorator(HTTPMethodView): @@ -54,5 +75,8 @@ class ViewWithDecorator(HTTPMethodView): return text('Hello I have a decorator') app.add_route(ViewWithDecorator.as_view(), '/url') - ``` + +**Previous:** [Blueprints](blueprints.html) + +**Next:** [Cookies](cookies.html) diff --git a/docs/contributing.md b/docs/contributing.md index 667978ca..a8fabc69 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,14 +1,20 @@ -# How to contribute to Sanic +# Contributing -Thank you for your interest! +Thank you for your interest! Sanic is always looking for contributors. If you +don't feel comfortable contributing code, adding docstrings to the source files +is very appreciated. ## Running tests + * `python -m pip install pytest` * `python -m pytest tests` ## Documentation -Sanic's documentation is built using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in Markdown and can be found in the `docs` folder, while the module reference is automatically generated using `sphinx-apidoc`. +Sanic's documentation is built +using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in +Markdown and can be found in the `docs` folder, while the module reference is +automatically generated using `sphinx-apidoc`. To generate the documentation from scratch: @@ -20,4 +26,10 @@ sphinx-build -b html docs docs/_build The HTML documentation will be created in the `docs/_build` folder. ## Warning -One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged. + +One of the main goals of Sanic is speed. Code that lowers the performance of +Sanic without significant gains in usability, security, or features may not be +merged. Please don't let this intimidate you! If you have any concerns about an +idea, open an issue for discussion and help. + +**Previous:** [Sanic extensions](extensions.html) diff --git a/docs/cookies.md b/docs/cookies.md index ead5f157..5a933de0 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -1,13 +1,13 @@ # Cookies -## Request +Cookies are pieces of data which persist inside a user's browser. Sanic can +both read and write cookies, which are stored as key-value pairs. -Request cookies can be accessed via the request.cookie dictionary +## Reading cookies -### Example +A user's cookies can be accessed `Request` object's `cookie` dictionary. ```python -from sanic import Sanic from sanic.response import text @app.route("/cookie") @@ -16,28 +16,11 @@ async def test(request): return text("Test cookie set to: {}".format(test_cookie)) ``` -## Response +## Writing cookies -Response cookies can be set like dictionary values and -have the following parameters available: - -* expires - datetime - Time for cookie to expire on the client's browser -* path - string - The Path attribute specifies the subset of URLs to - which this cookie applies -* comment - string - Cookie comment (metadata) -* domain - string - Specifies the domain for which the - cookie is valid. An explicitly specified domain must always - start with a dot. -* max-age - number - Number of seconds the cookie should live for -* secure - boolean - Specifies whether the cookie will only be sent via - HTTPS -* httponly - boolean - Specifies whether the cookie cannot be read - by javascript - -### Example +When returning a response, cookies can be set on the `Response` object. ```python -from sanic import Sanic from sanic.response import text @app.route("/cookie") @@ -47,4 +30,23 @@ async def test(request): response.cookies['test']['domain'] = '.gotta-go-fast.com' response.cookies['test']['httponly'] = True return response -``` \ No newline at end of file +``` + +Response cookies can be set like dictionary values and have the following +parameters available: + +- `expires` (datetime): The time for the cookie to expire on the + client's browser. +- `path` (string): The subset of URLs to which this cookie applies. +- `comment` (string): A comment (metadata). +- `domain` (string): Specifies the domain for which the cookie is valid. An + explicitly specified domain must always start with a dot. +- `max-age` (number): Number of seconds the cookie should live for. +- `secure` (boolean): Specifies whether the cookie will only be sent via + HTTPS. +- `httponly` (boolean): Specifies whether the cookie cannot be read by + Javascript. + +**Previous:** [Class-based views](class_based_views.html) + +**Next:** [Custom protocols](custom_protocol.html) diff --git a/docs/custom_protocol.md b/docs/custom_protocol.md index 7381a3cb..42e0135a 100644 --- a/docs/custom_protocol.md +++ b/docs/custom_protocol.md @@ -1,34 +1,36 @@ -# Custom Protocol +# Custom Protocols -You can change the behavior of protocol by using custom protocol. -If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic. +*Note: this is advanced usage, and most readers will not need such functionality.* -* loop -`loop` is an asyncio compatible event loop. +You can change the behavior of Sanic's protocol by specifying a custom +protocol, which should be a subclass +of +[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes). +This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method. -* connections -`connections` is a `set object` to store protocol objects. -When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections. +The constructor of the custom protocol class receives the following keyword +arguments from Sanic. -* signal -`signal` is a `sanic.server.Signal object` with `stopped attribute`. -When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`. - -* request_handler -`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments. - -* error_handler -`error_handler` is a `sanic.exceptions.Handler` object. - -* request_timeout -`request_timeout` is seconds for timeout. - -* request_max_size -`request_max_size` is bytes of max request size. +- `loop`: an `asyncio`-compatible event loop. +- `connections`: a `set` to store protocol objects. When Sanic receives + `SIGINT` or `SIGTERM`, it executes `protocol.close_if_idle` for all protocol + objects stored in this set. +- `signal`: a `sanic.server.Signal` object with the `stopped` attribute. When + Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` is assigned `True`. +- `request_handler`: a coroutine that takes a `sanic.request.Request` object + and a `response` callback as arguments. +- `error_handler`: a `sanic.exceptions.Handler` which is called when exceptions + are raised. +- `request_timeout`: the number of seconds before a request times out. +- `request_max_size`: an integer specifying the maximum size of a request, in bytes. ## Example -By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`. -In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`. + +An error occurs in the default protocol if a handler function does not return +an `HTTPResponse` object. + +By overriding the `write_response` protocol method, if a handler returns a +string it will be converted to an `HTTPResponse object`. ```python from sanic import Sanic @@ -68,3 +70,7 @@ async def response(request): app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) ``` + +**Previous:** [Cookies](cookies.html) + +**Next:** [Testing](testing.html) diff --git a/docs/deploying.md b/docs/deploying.md index d759bb3c..8d0dcde6 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -1,35 +1,59 @@ # Deploying -When it comes to deploying Sanic, there's not much to it, but there are -a few things to take note of. +Deploying Sanic is made simple by the inbuilt webserver. After defining an +instance of `sanic.Sanic`, we can call the `run` method with the following +keyword arguments: + +- `host` *(default `"127.0.0.1"`)*: Address to host the server on. +- `port` *(default `8000`)*: Port to host the server on. +- `debug` *(default `False`)*: Enables debug output (slows server). +- `before_start` *(default `None`)*: Function or list of functions to be executed + before the server starts accepting connections. +- `after_start` *(default `None`)*: Function or list of functions to be executed + after the server starts accepting connections. +- `before_stop` *(default `None`)*: Function or list of functions to be + executed when a stop signal is received before it is + respected. +- `after_stop` *(default `None`)*: Function or list of functions to be executed + when all requests are complete. +- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s). +- `sock` *(default `None`)*: Socket for the server to accept connections from. +- `workers` *(default `1`)*: Number of worker processes to spawn. +- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is + specified, Sanic creates its own event loop. +- `protocol` *(default `HttpProtocol`)*: Subclass + of + [asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes). ## Workers -By default, Sanic listens in the main process using only 1 CPU core. -To crank up the juice, just specify the number of workers in the run -arguments like so: +By default, Sanic listens in the main process using only one CPU core. To crank +up the juice, just specify the number of workers in the `run` arguments. ```python app.run(host='0.0.0.0', port=1337, workers=4) ``` -Sanic will automatically spin up multiple processes and route -traffic between them. We recommend as many workers as you have -available cores. +Sanic will automatically spin up multiple processes and route traffic between +them. We recommend as many workers as you have available cores. -## Running via Command +## Running via command -If you like using command line arguments, you can launch a sanic server -by executing the module. For example, if you initialized sanic as -app in a file named server.py, you could run the server like so: +If you like using command line arguments, you can launch a Sanic server by +executing the module. For example, if you initialized Sanic as `app` in a file +named `server.py`, you could run the server like so: `python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4` -With this way of running sanic, it is not necessary to run app.run in -your python file. If you do, just make sure you wrap it in name == main -like so: +With this way of running sanic, it is not necessary to invoke `app.run` in your +Python file. If you do, make sure you wrap it so that it only executes when +directly run by the interpreter. ```python if __name__ == '__main__': app.run(host='0.0.0.0', port=1337, workers=4) -``` \ No newline at end of file +``` + +**Previous:** [Request Data](request_data.html) + +**Next:** [Static Files](static_files.html) diff --git a/docs/exceptions.md b/docs/exceptions.md index 4889cd7b..add0a3e4 100644 --- a/docs/exceptions.md +++ b/docs/exceptions.md @@ -1,28 +1,49 @@ # Exceptions -Exceptions can be thrown from within request handlers and will automatically be handled by Sanic. Exceptions take a message as their first argument, and can also take a status_code to be passed back in the HTTP response. Check sanic.exceptions for the full list of exceptions to throw. +Exceptions can be thrown from within request handlers and will automatically be +handled by Sanic. Exceptions take a message as their first argument, and can +also take a status code to be passed back in the HTTP response. ## Throwing an exception +To throw an exception, simply `raise` the relevant exception from the +`sanic.exceptions` module. + ```python -from sanic import Sanic from sanic.exceptions import ServerError @app.route('/killme') def i_am_ready_to_die(request): - raise ServerError("Something bad happened") + raise ServerError("Something bad happened", status_code=500) ``` -## Handling Exceptions +## Handling exceptions -Just use the @exception decorator. The decorator expects a list of exceptions to handle as arguments. You can pass SanicException to catch them all! The exception handler must expect a request and exception object as arguments. +To override Sanic's default handling of an exception, the `@app.exception` +decorator is used. The decorator expects a list of exceptions to handle as +arguments. You can pass `SanicException` to catch them all! The decorated +exception handler function must take a `Request` and `Exception` object as +arguments. ```python -from sanic import Sanic from sanic.response import text from sanic.exceptions import NotFound @app.exception(NotFound) def ignore_404s(request, exception): return text("Yep, I totally found the page: {}".format(request.url)) -``` \ No newline at end of file +``` + +## Useful exceptions + +Some of the most useful exceptions are presented below: + +- `NotFound`: called when a suitable route for the request isn't found. +- `ServerError`: called when something goes wrong inside the server. This + usually occurs if there is an exception raised in user code. + +See the `sanic.exceptions` module for the full list of exceptions to throw. + +**Previous:** [Middleware](middleware.html) + +**Next:** [Blueprints](blueprints.html) diff --git a/docs/extensions.md b/docs/extensions.md index 829ccf99..b775f4d2 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -2,5 +2,10 @@ A list of Sanic extensions created by the community. - * [Sessions](https://github.com/subyraman/sanic_session) — Support for sessions. Allows using redis, memcache or an in memory store. - * [CORS](https://github.com/ashleysommer/sanic-cors) — A port of flask-cors. +- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. + Allows using redis, memcache or an in memory store. +- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. + +**Previous:** [Testing](testing.html) + +**Next:** [Contributing](contributing.html) diff --git a/docs/getting_started.md b/docs/getting_started.md index c7a437d3..4a062df3 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -1,25 +1,29 @@ # Getting Started -Make sure you have pip and python 3.5 before starting +Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at +least version 3.5 of Python before starting. Sanic uses the new `async`/`await` +syntax, so earlier versions of python won't work. -## Benchmarks - * Install Sanic - * `python3 -m pip install sanic` - * Edit main.py to include: -```python -from sanic import Sanic -from sanic.response import json +1. Install Sanic: `python3 -m pip install sanic` +2. Create a file called `main.py` with the following code: -app = Sanic(__name__) + ```python + from sanic import Sanic + from sanic.response import text -@app.route("/") -async def test(request): - return json({ "hello": "world" }) + app = Sanic(__name__) -app.run(host="0.0.0.0", port=8000, debug=True) -``` - * Run `python3 main.py` + @app.route("/") + async def test(request): + return text('Hello world!') -You now have a working Sanic server! To continue on, check out: - * [Request Data](request_data.md) - * [Routing](routing.md) \ No newline at end of file + app.run(host="0.0.0.0", port=8000, debug=True) + ``` + +3. Run the server: `python3 main.py` +4. Open the address `http://0.0.0.0:8000` in your web browser. You should see + the message *Hello world!*. + +You now have a working Sanic server! + +**Next:** [Routing](routing.html) diff --git a/docs/index.rst b/docs/index.rst index 202a6d50..c0b565c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,17 +7,17 @@ Guides :maxdepth: 2 getting_started - request_data routing + request_data + deploying + static_files middleware exceptions blueprints class_based_views cookies - static_files custom_protocol testing - deploying extensions contributing diff --git a/docs/middleware.md b/docs/middleware.md index 39930e3e..aef9e093 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -1,36 +1,32 @@ # Middleware -Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware returns a response object, the request will stop processing and a response will be returned. +Middleware are functions which are executed before or after requests to the +server. They can be used to modify the *request to* or *response from* +user-defined handler functions. -Middleware is registered via the middleware decorator, and can either be added as 'request' or 'response' middleware, based on the argument provided in the decorator. Response middleware receives both the request and the response as arguments. +There are two types of middleware: request and response. Both are declared +using the `@app.middleware` decorator, with the decorator's parameter being a +string representing its type: `'request'` or `'response'`. Response middleware +receives both the request and the response as arguments. -## Examples + +The simplest middleware doesn't modify the request or response at all: ```python -app = Sanic(__name__) - -@app.middleware -async def halt_request(request): - print("I am a spy") - @app.middleware('request') -async def halt_request(request): - return text('I halted the request') +async def print_on_request(request): + print("I print when a request is received by the server") @app.middleware('response') -async def halt_response(request, response): - return text('I halted the response') - -@app.route('/') -async def handler(request): - return text('I would like to speak now please') - -app.run(host="0.0.0.0", port=8000) +async def print_on_response(request, response): + print("I print when a response is returned by the server") ``` -## Middleware chain +## Modifying the request or response -If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware: +Middleware can modify the request or response parameter it is given, *as long +as it does not return it*. The following example shows a practical use-case for +this. ```python app = Sanic(__name__) @@ -46,4 +42,29 @@ async def prevent_xss(request, response): app.run(host="0.0.0.0", port=8000) ``` -The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks. +The above code will apply the two middleware in order. First, the middleware +**custom_banner** will change the HTTP response header *Server* to +*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP +header for preventing Cross-Site-Scripting (XSS) attacks. These two functions +are invoked *after* a user function returns a response. + +## Responding early + +If middleware returns a `HTTPResponse` object, the request will stop processing +and the response will be returned. If this occurs to a request before the +relevant user route handler is reached, the handler will never be called. +Returning a response will also prevent any further middleware from running. + +```python +@app.middleware('request') +async def halt_request(request): + return text('I halted the request') + +@app.middleware('response') +async def halt_response(request, response): + return text('I halted the response') +``` + +**Previous:** [Static Files](static_files.html) + +**Next:** [Exceptions](exceptions.html) diff --git a/docs/request_data.md b/docs/request_data.md index bcb62ef9..0826a1c7 100644 --- a/docs/request_data.md +++ b/docs/request_data.md @@ -1,50 +1,95 @@ # Request Data -## Properties +When an endpoint receives a HTTP request, the route function is passed a +`Request` object. -The following request variables are accessible as properties: +The following variables are accessible as properties on `Request` objects: -`request.files` (dictionary of File objects) - List of files that have a name, body, and type -`request.json` (any) - JSON body -`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name -`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name -`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type -`request.ip` (str) - IP address of the requester +- `json` (any) - JSON body -See request.py for more information + ```python + from sanic.response import json + + @app.route("/json") + def post_json(request): + return json({ "received": True, "message": request.json }) + ``` + +- `args` (dict) - Query string variables. A query string is the section of a + URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed, + the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`. + The request's `query_string` variable holds the unparsed string value. -## Examples + ```python + from sanic.response import json + + @app.route("/query_string") + def query_string(request): + return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) + ``` + +- `files` (dictionary of `File` objects) - List of files that have a name, body, and type + + ```python + from sanic.response import json + + @app.route("/files") + def post_json(request): + test_file = request.files.get('test') + + file_parameters = { + 'body': test_file.body, + 'name': test_file.name, + 'type': test_file.type, + } + + return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters }) + ``` + +- `form` (dict) - Posted form variables. + + ```python + from sanic.response import json + + @app.route("/form") + def post_json(request): + return json({ "received": True, "form_data": request.form, "test": request.form.get('test') }) + ``` + +- `body` (bytes) - Posted raw body. This property allows retrieval of the + request's raw data, regardless of content type. + + ```python + from sanic.response import text + + @app.route("/users", methods=["POST",]) + def create_user(request): + return text("You are trying to create a user with the following POST: %s" % request.body) + ``` + +- `ip` (str) - IP address of the requester. + +## Accessing values using `get` and `getlist` + +The request properties which return a dictionary actually return a subclass of +`dict` called `RequestParameters`. The key difference when using this object is +the distinction between the `get` and `getlist` methods. + +- `get(key, default=None)` operates as normal, except that when the value of + the given key is a list, *only the first item is returned*. +- `getlist(key, default=None)` operates as normal, *returning the entire list*. ```python -from sanic import Sanic -from sanic.response import json, text +from sanic.request import RequestParameters -@app.route("/json") -def post_json(request): - return json({ "received": True, "message": request.json }) +args = RequestParameters() +args['titles'] = ['Post 1', 'Post 2'] -@app.route("/form") -def post_json(request): - return json({ "received": True, "form_data": request.form, "test": request.form.get('test') }) +args.get('titles') # => 'Post 1' -@app.route("/files") -def post_json(request): - test_file = request.files.get('test') - - file_parameters = { - 'body': test_file.body, - 'name': test_file.name, - 'type': test_file.type, - } - - return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters }) - -@app.route("/query_string") -def query_string(request): - return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) - - -@app.route("/users", methods=["POST",]) -def create_user(request): - return text("You are trying to create a user with the following POST: %s" % request.body) +args.getlist('titles') # => ['Post 1', 'Post 2'] ``` + +**Previous:** [Routing](routing.html) + +**Next:** [Deploying](deploying.html) diff --git a/docs/routing.md b/docs/routing.md index 92ac2290..6d5982a1 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -1,17 +1,48 @@ # Routing -Sanic comes with a basic router that supports request parameters. To specify a parameter, surround it with carrots like so: ``. Request parameters will be passed to the request handler functions as keyword arguments. To specify a type, add a :type after the parameter name, in the carrots. If the parameter does not match the type supplied, Sanic will throw a NotFound exception, resulting in a 404 page not found error. +Routing allows the user to specify handler functions for different URL endpoints. - -## Examples +A basic route looks like the following, where `app` is an instance of the +`Sanic` class: + +```python +from sanic.response import json + +@app.route("/") +async def test(request): + return json({ "hello": "world" }) +``` + +When the url `http://server.url/` is accessed (the base url of the server), the +final `/` is matched by the router to the handler function, `test`, which then +returns a JSON object. + +Sanic handler functions must be defined using the `async def` syntax, as they +are asynchronous functions. + +## Request parameters + +Sanic comes with a basic router that supports request parameters. + +To specify a parameter, surround it with angle quotes like so: ``. +Request parameters will be passed to the route handler functions as keyword +arguments. ```python -from sanic import Sanic from sanic.response import text @app.route('/tag/') async def tag_handler(request, tag): return text('Tag - {}'.format(tag)) +``` + +To specify a type for the parameter, add a `:type` after the parameter name, +inside the quotes. If the parameter does not match the specified type, Sanic +will throw a `NotFound` exception, resulting in a `404: Page not found` error +on the URL. + +```python +from sanic.response import text @app.route('/number/') async def integer_handler(request, integer_arg): @@ -29,16 +60,52 @@ async def person_handler(request, name): async def folder_handler(request, folder_id): return text('Folder - {}'.format(folder_id)) +``` + +## HTTP request types + +By default, a route defined on a URL will be used for all 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. + +```python +from sanic.response import text + +@app.route('/post') +async def post_handler(request, methods=['POST']): + return text('POST request - {}'.format(request.json)) + +@app.route('/get') +async def GET_handler(request, methods=['GET']): + return text('GET request - {}'.format(request.args)) + +``` + +## The `add_route` method + +As we have seen, routes are often specified using the `@app.route` decorator. +However, this decorator is really just a wrapper for the `app.add_route` +method, which is used as follows: + +```python +from sanic.response import text + +# Define the handler functions async def handler1(request): return text('OK') -app.add_route(handler1, '/test') async def handler2(request, name): return text('Folder - {}'.format(name)) -app.add_route(handler2, '/folder/') async def person_handler2(request, name): return text('Person - {}'.format(name)) -app.add_route(person_handler2, '/person/') +# Add each handler function as a route +app.add_route(handler1, '/test') +app.add_route(handler2, '/folder/') +app.add_route(person_handler2, '/person/', methods=['GET']) ``` + +**Previous:** [Getting Started](getting_started.html) + +**Next:** [Request Data](request_data.html) diff --git a/docs/static_files.md b/docs/static_files.md index fca8d251..126b2ed1 100644 --- a/docs/static_files.md +++ b/docs/static_files.md @@ -1,10 +1,11 @@ # Static Files -Both directories and files can be served by registering with static - -## Example +Static files and directories, such as an image file, are served by Sanic when +registered with the `app.static` method. The method takes an endpoint URL and a +filename. The file specified will then be accessible via the given endpoint. ```python +from sanic import Sanic app = Sanic(__name__) # Serves files from the static folder to the URL /static @@ -16,3 +17,7 @@ app.static('/the_best.png', '/home/ubuntu/test.png') app.run(host="0.0.0.0", port=8000) ``` + +**Previous:** [Deploying](deploying.html) + +**Next:** [Middleware](middleware.html) diff --git a/docs/testing.md b/docs/testing.md index 79c719e8..47f6564a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -49,3 +49,7 @@ def test_endpoint_challenge(): # Assert that the server responds with the challenge string assert response.text == request_data['challenge'] ``` + +**Previous:** [Custom protocols](custom_protocol.html) + +**Next:** [Sanic extensions](extensions.html) From 7554e8737468334df78e0ff6d92a83fa897859a9 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Thu, 19 Jan 2017 21:24:08 -0600 Subject: [PATCH 247/977] Fixes doc link extensions from .html to .md Whoops! Totally missed that all the links pointed to `.html` files instead of `.md` files --- docs/blueprints.md | 4 ++-- docs/class_based_views.md | 4 ++-- docs/contributing.md | 2 +- docs/cookies.md | 4 ++-- docs/custom_protocol.md | 6 +++--- docs/deploying.md | 6 +++--- docs/exceptions.md | 4 ++-- docs/extensions.md | 4 ++-- docs/getting_started.md | 2 +- docs/middleware.md | 4 ++-- docs/request_data.md | 4 ++-- docs/routing.md | 4 ++-- docs/static_files.md | 4 ++-- docs/testing.md | 4 ++-- 14 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/blueprints.md b/docs/blueprints.md index ee3eab46..65907750 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -159,6 +159,6 @@ app.blueprint(blueprint_v2) app.run(host='0.0.0.0', port=8000, debug=True) ``` -**Previous:** [Exceptions](exceptions.html) +**Previous:** [Exceptions](exceptions.md) -**Next:** [Class-based views](class_based_views.html) +**Next:** [Class-based views](class_based_views.md) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index e34f432b..0cf7f770 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -77,6 +77,6 @@ class ViewWithDecorator(HTTPMethodView): app.add_route(ViewWithDecorator.as_view(), '/url') ``` -**Previous:** [Blueprints](blueprints.html) +**Previous:** [Blueprints](blueprints.md) -**Next:** [Cookies](cookies.html) +**Next:** [Cookies](cookies.md) diff --git a/docs/contributing.md b/docs/contributing.md index a8fabc69..dde57270 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -32,4 +32,4 @@ Sanic without significant gains in usability, security, or features may not be merged. Please don't let this intimidate you! If you have any concerns about an idea, open an issue for discussion and help. -**Previous:** [Sanic extensions](extensions.html) +**Previous:** [Sanic extensions](extensions.md) diff --git a/docs/cookies.md b/docs/cookies.md index 5a933de0..c29a1f32 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -47,6 +47,6 @@ parameters available: - `httponly` (boolean): Specifies whether the cookie cannot be read by Javascript. -**Previous:** [Class-based views](class_based_views.html) +**Previous:** [Class-based views](class_based_views.md) -**Next:** [Custom protocols](custom_protocol.html) +**Next:** [Custom protocols](custom_protocol.md) diff --git a/docs/custom_protocol.md b/docs/custom_protocol.md index 42e0135a..67b732a2 100644 --- a/docs/custom_protocol.md +++ b/docs/custom_protocol.md @@ -5,7 +5,7 @@ You can change the behavior of Sanic's protocol by specifying a custom protocol, which should be a subclass of -[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes). +[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.md#protocol-classes). This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method. The constructor of the custom protocol class receives the following keyword @@ -71,6 +71,6 @@ async def response(request): app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) ``` -**Previous:** [Cookies](cookies.html) +**Previous:** [Cookies](cookies.md) -**Next:** [Testing](testing.html) +**Next:** [Testing](testing.md) diff --git a/docs/deploying.md b/docs/deploying.md index 8d0dcde6..88aedfe8 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -23,7 +23,7 @@ keyword arguments: specified, Sanic creates its own event loop. - `protocol` *(default `HttpProtocol`)*: Subclass of - [asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes). + [asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.md#protocol-classes). ## Workers @@ -54,6 +54,6 @@ if __name__ == '__main__': app.run(host='0.0.0.0', port=1337, workers=4) ``` -**Previous:** [Request Data](request_data.html) +**Previous:** [Request Data](request_data.md) -**Next:** [Static Files](static_files.html) +**Next:** [Static Files](static_files.md) diff --git a/docs/exceptions.md b/docs/exceptions.md index add0a3e4..8a294492 100644 --- a/docs/exceptions.md +++ b/docs/exceptions.md @@ -44,6 +44,6 @@ Some of the most useful exceptions are presented below: See the `sanic.exceptions` module for the full list of exceptions to throw. -**Previous:** [Middleware](middleware.html) +**Previous:** [Middleware](middleware.md) -**Next:** [Blueprints](blueprints.html) +**Next:** [Blueprints](blueprints.md) diff --git a/docs/extensions.md b/docs/extensions.md index b775f4d2..09c71d42 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -6,6 +6,6 @@ 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. -**Previous:** [Testing](testing.html) +**Previous:** [Testing](testing.md) -**Next:** [Contributing](contributing.html) +**Next:** [Contributing](contributing.md) diff --git a/docs/getting_started.md b/docs/getting_started.md index 4a062df3..e3a2a31b 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -26,4 +26,4 @@ syntax, so earlier versions of python won't work. You now have a working Sanic server! -**Next:** [Routing](routing.html) +**Next:** [Routing](routing.md) diff --git a/docs/middleware.md b/docs/middleware.md index aef9e093..6adb9328 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -65,6 +65,6 @@ async def halt_response(request, response): return text('I halted the response') ``` -**Previous:** [Static Files](static_files.html) +**Previous:** [Static Files](static_files.md) -**Next:** [Exceptions](exceptions.html) +**Next:** [Exceptions](exceptions.md) diff --git a/docs/request_data.md b/docs/request_data.md index 0826a1c7..555cf765 100644 --- a/docs/request_data.md +++ b/docs/request_data.md @@ -90,6 +90,6 @@ args.get('titles') # => 'Post 1' args.getlist('titles') # => ['Post 1', 'Post 2'] ``` -**Previous:** [Routing](routing.html) +**Previous:** [Routing](routing.md) -**Next:** [Deploying](deploying.html) +**Next:** [Deploying](deploying.md) diff --git a/docs/routing.md b/docs/routing.md index 6d5982a1..d88bcf26 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -106,6 +106,6 @@ app.add_route(handler2, '/folder/') app.add_route(person_handler2, '/person/', methods=['GET']) ``` -**Previous:** [Getting Started](getting_started.html) +**Previous:** [Getting Started](getting_started.md) -**Next:** [Request Data](request_data.html) +**Next:** [Request Data](request_data.md) diff --git a/docs/static_files.md b/docs/static_files.md index 126b2ed1..5daf7818 100644 --- a/docs/static_files.md +++ b/docs/static_files.md @@ -18,6 +18,6 @@ app.static('/the_best.png', '/home/ubuntu/test.png') app.run(host="0.0.0.0", port=8000) ``` -**Previous:** [Deploying](deploying.html) +**Previous:** [Deploying](deploying.md) -**Next:** [Middleware](middleware.html) +**Next:** [Middleware](middleware.md) diff --git a/docs/testing.md b/docs/testing.md index 47f6564a..bdb85efb 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -50,6 +50,6 @@ def test_endpoint_challenge(): assert response.text == request_data['challenge'] ``` -**Previous:** [Custom protocols](custom_protocol.html) +**Previous:** [Custom protocols](custom_protocol.md) -**Next:** [Sanic extensions](extensions.html) +**Next:** [Sanic extensions](extensions.md) From 596bb54ee37da58bb2fad34db1a912bd7cfe7ff0 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Thu, 19 Jan 2017 21:26:37 -0600 Subject: [PATCH 248/977] Oops on 2 of the non-relative links --- docs/custom_protocol.md | 2 +- docs/deploying.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/custom_protocol.md b/docs/custom_protocol.md index 67b732a2..73d1f1d3 100644 --- a/docs/custom_protocol.md +++ b/docs/custom_protocol.md @@ -5,7 +5,7 @@ You can change the behavior of Sanic's protocol by specifying a custom protocol, which should be a subclass of -[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.md#protocol-classes). +[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes). This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method. The constructor of the custom protocol class receives the following keyword diff --git a/docs/deploying.md b/docs/deploying.md index 88aedfe8..f4d63163 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -23,7 +23,7 @@ keyword arguments: specified, Sanic creates its own event loop. - `protocol` *(default `HttpProtocol`)*: Subclass of - [asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.md#protocol-classes). + [asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes). ## Workers From 96424b6b0a6e293ab08a48e7dd69110ede57ee2d Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 19 Jan 2017 23:47:07 -0800 Subject: [PATCH 249/977] add method shorthands --- sanic/sanic.py | 19 +++++++++++++ tests/test_routes.py | 67 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/sanic/sanic.py b/sanic/sanic.py index 94fcd983..15217de9 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -66,6 +66,25 @@ class Sanic: return response + # Shorthand method decorators + def get(self, uri, host=None): + return self.route(uri, methods=["GET"], host=host) + + def post(self, uri, host=None): + return self.route(uri, methods=["POST"], host=host) + + def put(self, uri, host=None): + return self.route(uri, methods=["PUT"], host=host) + + def head(self, uri, host=None): + return self.route(uri, methods=["HEAD"], host=host) + + def options(self, uri, host=None): + return self.route(uri, methods=["OPTIONS"], host=host) + + def patch(self, uri, host=None): + return self.route(uri, methods=["PATCH"], host=host) + def add_route(self, handler, uri, methods=None, host=None): """ A helper method to register class instance or diff --git a/tests/test_routes.py b/tests/test_routes.py index 9c671829..43023ed0 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -10,6 +10,73 @@ from sanic.utils import sanic_endpoint_test # UTF-8 # ------------------------------------------------------------ # +def test_shorthand_routes(): + app = Sanic('test_shorhand_routes') + + @app.get('') + def handler(request): + return text('OK') + + @app.post('/post') + def handler(request): + return text('OK') + + @app.put('/put') + def handler(request): + return text('OK') + + @app.patch('/patch') + def handler(request): + return text('OK') + + @app.head('/head') + def handler(request): + return text('OK') + + @app.options('/options') + def handler(request): + return text('OK') + + request, response = sanic_endpoint_test(app, uri='/') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/', method='post') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/post', method='post') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/post', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/put', method='put') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/put', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/patch', + method='patch') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/patch', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/head', method='head') + assert response.status== 200 + + request, response = sanic_endpoint_test(app, uri='/head', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/options', + method='options') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/options', + method='get') + assert response.status == 405 + + def test_static_routes(): app = Sanic('test_dynamic_route') From 6fd69b628425c1fc0aac33c48e69f36a72a2c4f5 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Fri, 20 Jan 2017 10:00:51 -0800 Subject: [PATCH 250/977] separate tests --- tests/test_routes.py | 79 +++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/tests/test_routes.py b/tests/test_routes.py index 43023ed0..16a1c767 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -10,73 +10,84 @@ from sanic.utils import sanic_endpoint_test # UTF-8 # ------------------------------------------------------------ # -def test_shorthand_routes(): - app = Sanic('test_shorhand_routes') +def test_shorthand_routes_get(): + app = Sanic('test_shorhand_routes_get') - @app.get('') + @app.get('/get') def handler(request): return text('OK') + request, response = sanic_endpoint_test(app, uri='/get', method='get') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/get', method='post') + assert response.status == 405 + +def test_shorthand_routes_post(): + app = Sanic('test_shorhand_routes_post') + @app.post('/post') def handler(request): return text('OK') - @app.put('/put') - def handler(request): - return text('OK') - - @app.patch('/patch') - def handler(request): - return text('OK') - - @app.head('/head') - def handler(request): - return text('OK') - - @app.options('/options') - def handler(request): - return text('OK') - - request, response = sanic_endpoint_test(app, uri='/') - assert response.text == 'OK' - - request, response = sanic_endpoint_test(app, uri='/', method='post') - assert response.status == 405 - request, response = sanic_endpoint_test(app, uri='/post', method='post') assert response.text == 'OK' request, response = sanic_endpoint_test(app, uri='/post', method='get') assert response.status == 405 +def test_shorthand_routes_put(): + app = Sanic('test_shorhand_routes_put') + + @app.put('/put') + def handler(request): + return text('OK') + request, response = sanic_endpoint_test(app, uri='/put', method='put') assert response.text == 'OK' request, response = sanic_endpoint_test(app, uri='/put', method='get') assert response.status == 405 - request, response = sanic_endpoint_test(app, uri='/patch', - method='patch') +def test_shorthand_routes_patch(): + app = Sanic('test_shorhand_routes_patch') + + @app.patch('/patch') + def handler(request): + return text('OK') + + request, response = sanic_endpoint_test(app, uri='/patch', method='patch') assert response.text == 'OK' request, response = sanic_endpoint_test(app, uri='/patch', method='get') assert response.status == 405 +def test_shorthand_routes_head(): + app = Sanic('test_shorhand_routes_head') + + @app.head('/head') + def handler(request): + return text('OK') + request, response = sanic_endpoint_test(app, uri='/head', method='head') - assert response.status== 200 + assert response.status == 200 request, response = sanic_endpoint_test(app, uri='/head', method='get') assert response.status == 405 - request, response = sanic_endpoint_test(app, uri='/options', - method='options') - assert response.text == 'OK' +def test_shorthand_routes_options(): + app = Sanic('test_shorhand_routes_options') - request, response = sanic_endpoint_test(app, uri='/options', - method='get') + @app.options('/options') + def handler(request): + return text('OK') + + request, response = sanic_endpoint_test(app, uri='/options', method='options') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/options', method='get') assert response.status == 405 - def test_static_routes(): app = Sanic('test_dynamic_route') From a7cd4ccd09d43a69ad1d4f39df7746c17766ebdd Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 20 Jan 2017 14:31:24 -0600 Subject: [PATCH 251/977] Simplify RequestParameters Simplifies request parameters, it defined a bit more than it had too, added some docstrings and made the code simpler as well. Should now raise a KeyError on __getitem__ as @amsb had noted on commit 9dd954b --- sanic/request.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index b7a95bc4..6f02d09b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -21,19 +21,13 @@ class RequestParameters(dict): value of the list and getlist returns the whole shebang """ - def __init__(self, *args, **kwargs): - self.super = super() - self.super.__init__(*args, **kwargs) - - def __getitem__(self, name): - return self.get(name) - def get(self, name, default=None): - values = self.super.get(name) - return values[0] if values else default + """Return the first value, either the default or actual""" + return super().get(name, [default])[0] def getlist(self, name, default=None): - return self.super.get(name, default) + """Return the entire list""" + return super().get(name, default) class Request(dict): From 40f1e14bb7e6b034bcdd982721adc9a1152beddb Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 20 Jan 2017 14:36:15 -0600 Subject: [PATCH 252/977] Fix exception_monitoring example to actually work Closes #324 Super was used incorrectly in the example, also fixed some formatting here and there. --- examples/exception_monitoring.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py index 34b46a14..26c6d92b 100644 --- a/examples/exception_monitoring.py +++ b/examples/exception_monitoring.py @@ -9,17 +9,15 @@ and pass in an instance of it when we create our Sanic instance. Inside this class' default handler, we can do anything including sending exceptions to an external service. """ - - - +from sanic.exceptions import Handler, SanicException """ Imports and code relevant for our CustomHandler class (Ordinarily this would be in a separate file) """ -from sanic.response import text -from sanic.exceptions import Handler, SanicException + class CustomHandler(Handler): + def default(self, request, exception): # Here, we have access to the exception object # and can do anything with it (log, send to external service, etc) @@ -31,9 +29,7 @@ class CustomHandler(Handler): # Then, we must finish handling the exception by returning # our response to the client # For this we can just call the super class' default handler - return super.default(self, request, exception) - - + return super().default(request, exception) """ @@ -49,11 +45,12 @@ app = Sanic(__name__) handler = CustomHandler(sanic=app) app.error_handler = handler + @app.route("/") async def test(request): # Here, something occurs which causes an unexpected exception # This exception will flow to our custom handler. - x = 1 / 0 + 1 / 0 return json({"test": True}) From 0c7275da1a31835d69d3f26b79980934112504c1 Mon Sep 17 00:00:00 2001 From: Andrew Widdersheim Date: Fri, 20 Jan 2017 17:20:39 -0500 Subject: [PATCH 253/977] Remove multidict requirement This is no longer necessary after #302. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index e6e9b4cc..60606ad4 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,6 @@ setup( 'httptools>=0.0.9', 'ujson>=1.35', 'aiofiles>=0.3.0', - 'multidict>=2.0', ], classifiers=[ 'Development Status :: 2 - Pre-Alpha', From 72fba62e090bdfc1b3468b251351bade667f4e9f Mon Sep 17 00:00:00 2001 From: Andrew Widdersheim Date: Fri, 20 Jan 2017 17:09:27 -0500 Subject: [PATCH 254/977] Make it easier to override logging Take influence from how Werkzeug configures logging by only configuring a handler if no root handlers were previously configured by the end user. --- sanic/log.py | 2 +- sanic/sanic.py | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/sanic/log.py b/sanic/log.py index 3988bf12..1b4d7334 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,3 +1,3 @@ import logging -log = logging.getLogger(__name__) +log = logging.getLogger('sanic') diff --git a/sanic/sanic.py b/sanic/sanic.py index 018ed720..22ca2dd9 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -22,6 +22,15 @@ from os import set_inheritable class Sanic: def __init__(self, name=None, router=None, error_handler=None): + # Only set up a default log handler if the + # end-user application didn't set anything up. + if not logging.root.handlers and log.level == logging.NOTSET: + formatter = logging.Formatter( + "%(asctime)s: %(levelname)s: %(message)s") + handler = logging.StreamHandler() + handler.setFormatter(formatter) + log.addHandler(handler) + log.setLevel(logging.INFO) if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) @@ -273,10 +282,6 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s: %(levelname)s: %(message)s" - ) self.error_handler.debug = True self.debug = debug self.loop = loop @@ -364,12 +369,6 @@ class Sanic: :param stop_event: if provided, is used as a stop signal :return: """ - # In case this is called directly, we configure logging here too. - # This won't interfere with the same call from run() - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s: %(levelname)s: %(message)s" - ) server_settings['reuse_port'] = True # Create a stop event to be triggered by a signal From a811c84e9947a731bb54f55c90f5d0a10cb5886b Mon Sep 17 00:00:00 2001 From: ctlaltdefeat Date: Sat, 21 Jan 2017 22:40:34 +0200 Subject: [PATCH 255/977] allowed passing arguments to json response encoder --- sanic/response.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 244bc1b3..9fc1b598 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -142,14 +142,15 @@ class HTTPResponse: return self._cookies -def json(body, status=200, headers=None): +def json(body, status=200, headers=None, **kwargs): """ Returns response object with body in json format. :param body: Response data to be serialized. :param status: Response code. :param headers: Custom Headers. + :param \**kwargs: Remaining arguments that are passed to the json encoder. """ - return HTTPResponse(json_dumps(body), headers=headers, status=status, + return HTTPResponse(json_dumps(body, **kwargs), headers=headers, status=status, content_type="application/json") From 592ee5f8394b27318573fd17fd084d4b39bb6421 Mon Sep 17 00:00:00 2001 From: ctlaltdefeat Date: Sat, 21 Jan 2017 23:02:02 +0200 Subject: [PATCH 256/977] fixed line length to satisfy travis --- sanic/response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 9fc1b598..c29a473e 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -150,8 +150,8 @@ def json(body, status=200, headers=None, **kwargs): :param headers: Custom Headers. :param \**kwargs: Remaining arguments that are passed to the json encoder. """ - return HTTPResponse(json_dumps(body, **kwargs), headers=headers, status=status, - content_type="application/json") + return HTTPResponse(json_dumps(body, **kwargs), headers=headers, + status=status, content_type="application/json") def text(body, status=200, headers=None): From 1f89b1579262a8a1c8dbc5cd0354b350bd276086 Mon Sep 17 00:00:00 2001 From: Manuel Miranda Date: Mon, 23 Jan 2017 17:19:42 +0100 Subject: [PATCH 257/977] Caching example (#334) * Caching example using aiocache * Caching example using aiocache * Added aiocache to requirements * Fixed example with newest aiocache * Fixed bug in cache example --- examples/cache_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cache_example.py b/examples/cache_example.py index 60823366..50a6fe85 100644 --- a/examples/cache_example.py +++ b/examples/cache_example.py @@ -21,7 +21,7 @@ from aiocache.serializers import JsonSerializer app = Sanic(__name__) aiocache.settings.set_defaults( - cache="aiocache.RedisCache" + class_="aiocache.RedisCache" ) From a9ee01c7ec841c111a595079d49640a3718bc578 Mon Sep 17 00:00:00 2001 From: Lix Xu Date: Tue, 24 Jan 2017 15:34:32 +0800 Subject: [PATCH 258/977] add sanic-jinja2 extension --- docs/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/extensions.md b/docs/extensions.md index 09c71d42..1cf6121b 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -5,6 +5,7 @@ A list of Sanic extensions created by the community. - [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. 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. **Previous:** [Testing](testing.md) From 28f7abd1f8da9d27ec716ea8ab8062871c21a222 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 24 Jan 2017 17:18:58 -0800 Subject: [PATCH 259/977] set error handler debug from run debug arg --- sanic/exceptions.py | 8 ++++---- sanic/sanic.py | 4 ++-- tests/test_exceptions.py | 21 ++++++++++++++++++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 1e8ec639..2596a97a 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -140,9 +140,9 @@ class PayloadTooLarge(SanicException): class Handler: handlers = None - def __init__(self, sanic): + def __init__(self): self.handlers = {} - self.sanic = sanic + self.debug = False def _render_traceback_html(self, exception, request): exc_type, exc_value, tb = sys.exc_info() @@ -175,7 +175,7 @@ class Handler: response = handler(request=request, exception=exception) except: log.error(format_exc()) - if self.sanic.debug: + if self.debug: response_message = ( 'Exception raised in exception handler "{}" ' 'for uri: "{}"\n{}').format( @@ -192,7 +192,7 @@ class Handler: return text( 'Error: {}'.format(exception), status=getattr(exception, 'status_code', 500)) - elif self.sanic.debug: + elif self.debug: html_output = self._render_traceback_html(exception, request) response_message = ( diff --git a/sanic/sanic.py b/sanic/sanic.py index e21fc556..cea09470 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -36,7 +36,7 @@ class Sanic: name = getmodulename(frame_records[1]) self.name = name self.router = router or Router() - self.error_handler = error_handler or Handler(self) + self.error_handler = error_handler or Handler() self.config = Config() self.request_middleware = deque() self.response_middleware = deque() @@ -300,7 +300,7 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ - self.error_handler.debug = True + self.error_handler.debug = debug self.debug = debug self.loop = loop diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index a81e0d09..819d39f2 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -84,10 +84,29 @@ def test_handled_unhandled_exception(exception_app): "The server encountered an internal error and " "cannot complete your request.") - def test_exception_in_exception_handler(exception_app): """Test that an exception thrown in an error handler is handled""" request, response = sanic_endpoint_test( exception_app, uri='/error_in_error_handler_handler') assert response.status == 500 assert response.body == b'An error occurred while handling an error' + + +def test_exception_in_exception_handler_debug_off(exception_app): + """Test that an exception thrown in an error handler is handled""" + request, response = sanic_endpoint_test( + exception_app, + uri='/error_in_error_handler_handler', + debug=False) + assert response.status == 500 + assert response.body == b'An error occurred while handling an error' + + +def test_exception_in_exception_handler_debug_off(exception_app): + """Test that an exception thrown in an error handler is handled""" + request, response = sanic_endpoint_test( + exception_app, + uri='/error_in_error_handler_handler', + debug=True) + assert response.status == 500 + assert response.body.startswith(b'Exception raised in exception ') From 85a28d3c3027a1cf85cb3b9040788d91c3a21ff5 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Wed, 25 Jan 2017 01:19:21 -0800 Subject: [PATCH 260/977] Fix slow upload --- sanic/server.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index a984854e..00711e91 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -49,7 +49,7 @@ class HttpProtocol(asyncio.Protocol): # event loop, connection 'loop', 'transport', 'connections', 'signal', # request params - 'parser', 'request', 'url', 'headers', + 'parser', 'request', 'url', 'headers', 'body', # request config 'request_handler', 'request_timeout', 'request_max_size', # connection management @@ -64,6 +64,7 @@ class HttpProtocol(asyncio.Protocol): self.parser = None self.url = None self.headers = None + self.body = [] self.signal = signal self.connections = connections self.request_handler = request_handler @@ -148,12 +149,11 @@ class HttpProtocol(asyncio.Protocol): ) def on_body(self, body): - if self.request.body: - self.request.body += body - else: - self.request.body = body + self.body.append(body) def on_message_complete(self): + if self.body: + self.request.body = b''.join(self.body) self._request_handler_task = self.loop.create_task( self.request_handler(self.request, self.write_response)) @@ -217,6 +217,7 @@ class HttpProtocol(asyncio.Protocol): self.request = None self.url = None self.headers = None + self.body = [] self._request_handler_task = None self._total_request_size = 0 From d0a121ad064f46e0499607d58f213b586885dad8 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Wed, 25 Jan 2017 01:53:39 -0800 Subject: [PATCH 261/977] Added del cookie and default path --- docs/cookies.md | 29 ++++++++++++++++++++++++++++- sanic/cookies.py | 14 +++++++++++--- tests/test_cookies.py | 22 +++++++++++++++++++++- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/docs/cookies.md b/docs/cookies.md index c29a1f32..5ec54a03 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -32,12 +32,39 @@ async def test(request): return response ``` +## Deleting cookies + +Cookies can be removed semantically or explicitly. + +```python +from sanic.response import text + +@app.route("/cookie") +async def test(request): + response = text("Time to eat some cookies muahaha") + + # This cookie will be set to expire in 0 seconds + del response.cookies['kill_me'] + + # This cookie will self destruct in 5 seconds + response.cookies['short_life'] = 'Glad to be here' + response.cookies['short_life']['max-age'] = 5 + del response.cookies['favorite_color'] + + # This cookie will remain unchanged + response.cookies['favorite_color'] = 'blue' + response.cookies['favorite_color'] = 'pink' + del response.cookies['favorite_color'] + + return response +``` + Response cookies can be set like dictionary values and have the following parameters available: - `expires` (datetime): The time for the cookie to expire on the client's browser. -- `path` (string): The subset of URLs to which this cookie applies. +- `path` (string): The subset of URLs to which this cookie applies. Defaults to 0. - `comment` (string): A comment (metadata). - `domain` (string): Specifies the domain for which the cookie is valid. An explicitly specified domain must always start with a dot. diff --git a/sanic/cookies.py b/sanic/cookies.py index b7669e76..27b85bc9 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -42,8 +42,9 @@ class CookieJar(dict): """ CookieJar dynamically writes headers as cookies are added and removed It gets around the limitation of one header per name by using the - MultiHeader class to provide a unique key that encodes to Set-Cookie + MultiHeader class to provide a unique key that encodes to Set-Cookie. """ + def __init__(self, headers): super().__init__() self.headers = headers @@ -54,6 +55,7 @@ class CookieJar(dict): cookie_header = self.cookie_headers.get(key) if not cookie_header: cookie = Cookie(key, value) + cookie['path'] = '/' cookie_header = MultiHeader("Set-Cookie") self.cookie_headers[key] = cookie_header self.headers[cookie_header] = cookie @@ -62,8 +64,14 @@ class CookieJar(dict): self[key].value = value def __delitem__(self, key): - del self.cookie_headers[key] - return super().__delitem__(key) + if key not in self.cookie_headers: + self[key] = '' + self[key]['max-age'] = 0 + else: + cookie_header = self.cookie_headers[key] + del self.headers[cookie_header] + del self.cookie_headers[key] + return super().__delitem__(key) class Cookie(dict): diff --git a/tests/test_cookies.py b/tests/test_cookies.py index cf6a4259..571c01a3 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -3,6 +3,7 @@ from http.cookies import SimpleCookie from sanic import Sanic from sanic.response import json, text from sanic.utils import sanic_endpoint_test +import pytest # ------------------------------------------------------------ # @@ -54,4 +55,23 @@ def test_cookie_options(): response_cookies.load(response.headers.get('Set-Cookie', {})) assert response_cookies['test'].value == 'at you' - assert response_cookies['test']['httponly'] == True \ No newline at end of file + assert response_cookies['test']['httponly'] == True + +def test_cookie_deletion(): + app = Sanic('test_text') + + @app.route('/') + def handler(request): + response = text("OK") + del response.cookies['i_want_to_die'] + response.cookies['i_never_existed'] = 'testing' + del response.cookies['i_never_existed'] + return response + + request, response = sanic_endpoint_test(app) + response_cookies = SimpleCookie() + response_cookies.load(response.headers.get('Set-Cookie', {})) + + assert int(response_cookies['i_want_to_die']['max-age']) == 0 + with pytest.raises(KeyError): + hold_my_beer = response.cookies['i_never_existed'] \ No newline at end of file From ecd7e5bdd39dde0ef611ef8e1598f824d7e8e4a0 Mon Sep 17 00:00:00 2001 From: Andrew Bentley Date: Wed, 25 Jan 2017 23:01:57 +1100 Subject: [PATCH 262/977] Update blueprints docs with correct listener example --- docs/blueprints.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/blueprints.md b/docs/blueprints.md index 65907750..be95c5cc 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -107,13 +107,13 @@ Available events are: ```python bp = Blueprint('my_blueprint') -@bp.listen('before_server_start') -async def setup_connection(): +@bp.listener('before_server_start') +async def setup_connection(app, loop): global database database = mysql.connect(host='127.0.0.1'...) -@bp.listen('after_server_stop') -async def close_connection(): +@bp.listener('after_server_stop') +async def close_connection(app, loop): await database.close() ``` From 4efcb6d5add33151f481186a2a0376702b1cf2ff Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 25 Jan 2017 16:25:16 -0800 Subject: [PATCH 263/977] fix before/after event docstrings --- sanic/server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index a984854e..74ffb832 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -271,15 +271,15 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param request_handler: Sanic request handler with middleware :param error_handler: Sanic error handler with middleware :param before_start: Function to be executed before the server starts - listening. Takes single argument `loop` + listening. Takes arguments `app` instance and `loop` :param after_start: Function to be executed after the server starts - listening. Takes single argument `loop` + listening. Takes arguments `app` instance and `loop` :param before_stop: Function to be executed when a stop signal is - received before it is respected. Takes single - argument `loop` + received before it is respected. Takes arguments + `app` instance and `loop` :param after_stop: Function to be executed when a stop signal is - received after it is respected. Takes single - argument `loop` + received after it is respected. Takes arguments + `app` instance and `loop` :param debug: Enables debug output (slows server) :param request_timeout: time in seconds :param ssl: SSLContext From 3c355f19eb6efcc97be7dcaaba9f3f0358b64a1c Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 25 Jan 2017 16:47:14 -0800 Subject: [PATCH 264/977] false cookie attributes should not be set --- sanic/cookies.py | 3 ++- tests/test_cookies.py | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/sanic/cookies.py b/sanic/cookies.py index b7669e76..d54174f8 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -107,7 +107,8 @@ class Cookie(dict): value.strftime("%a, %d-%b-%Y %T GMT") )) elif key in self._flags: - output.append(self._keys[key]) + if self[key]: + output.append(self._keys[key]) else: output.append('%s=%s' % (self._keys[key], value)) diff --git a/tests/test_cookies.py b/tests/test_cookies.py index cf6a4259..ac3f72d9 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -3,6 +3,7 @@ from http.cookies import SimpleCookie from sanic import Sanic from sanic.response import json, text from sanic.utils import sanic_endpoint_test +import pytest # ------------------------------------------------------------ # @@ -25,6 +26,26 @@ def test_cookies(): assert response.text == 'Cookies are: working!' assert response_cookies['right_back'].value == 'at you' +@pytest.mark.parametrize("httponly,expected", [ + (False, False), + (True, True), +]) +def test_false_cookies(httponly, expected): + app = Sanic('test_text') + + @app.route('/') + def handler(request): + response = text('Cookies are: {}'.format(request.cookies['test'])) + response.cookies['right_back'] = 'at you' + response.cookies['right_back']['httponly'] = httponly + return response + + request, response = sanic_endpoint_test(app) + response_cookies = SimpleCookie() + response_cookies.load(response.headers.get('Set-Cookie', {})) + + 'HttpOnly' in response_cookies == expected + def test_http2_cookies(): app = Sanic('test_http2_cookies') @@ -54,4 +75,4 @@ def test_cookie_options(): response_cookies.load(response.headers.get('Set-Cookie', {})) assert response_cookies['test'].value == 'at you' - assert response_cookies['test']['httponly'] == True \ No newline at end of file + assert response_cookies['test']['httponly'] == True From b0f71c5304f77231757c5daf8b963f471836aeb8 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Wed, 25 Jan 2017 21:48:34 -0800 Subject: [PATCH 265/977] Fixed default path in documentation --- docs/cookies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cookies.md b/docs/cookies.md index 5ec54a03..d593ed09 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -64,7 +64,7 @@ parameters available: - `expires` (datetime): The time for the cookie to expire on the client's browser. -- `path` (string): The subset of URLs to which this cookie applies. Defaults to 0. +- `path` (string): The subset of URLs to which this cookie applies. Defaults to /. - `comment` (string): A comment (metadata). - `domain` (string): Specifies the domain for which the cookie is valid. An explicitly specified domain must always start with a dot. From fd118a41fd871bb621ee33f409f8b8511eeb3bb3 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Wed, 25 Jan 2017 21:56:49 -0800 Subject: [PATCH 266/977] Only use request.body --- sanic/request.py | 2 +- sanic/server.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 6f02d09b..8fd2c0e9 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -54,7 +54,7 @@ class Request(dict): self.query_string = url_parsed.query.decode('utf-8') # Init but do not inhale - self.body = None + self.body = [] self.parsed_json = None self.parsed_form = None self.parsed_files = None diff --git a/sanic/server.py b/sanic/server.py index 00711e91..d34c6db2 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -49,7 +49,7 @@ class HttpProtocol(asyncio.Protocol): # event loop, connection 'loop', 'transport', 'connections', 'signal', # request params - 'parser', 'request', 'url', 'headers', 'body', + 'parser', 'request', 'url', 'headers', # request config 'request_handler', 'request_timeout', 'request_max_size', # connection management @@ -64,7 +64,6 @@ class HttpProtocol(asyncio.Protocol): self.parser = None self.url = None self.headers = None - self.body = [] self.signal = signal self.connections = connections self.request_handler = request_handler @@ -149,11 +148,11 @@ class HttpProtocol(asyncio.Protocol): ) def on_body(self, body): - self.body.append(body) + self.request.body.append(body) def on_message_complete(self): - if self.body: - self.request.body = b''.join(self.body) + if self.request.body: + self.request.body = b''.join(self.request.body) self._request_handler_task = self.loop.create_task( self.request_handler(self.request, self.write_response)) @@ -217,7 +216,6 @@ class HttpProtocol(asyncio.Protocol): self.request = None self.url = None self.headers = None - self.body = [] self._request_handler_task = None self._total_request_size = 0 From 54ca8c787b60b71efe1b93930c6ef9204a935932 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 23 Jan 2017 19:58:37 -0800 Subject: [PATCH 267/977] add async run --- examples/run_async.py | 20 ++++++++++++++ sanic/sanic.py | 62 ++++++++++++++++++++++++++++++++++++++++--- sanic/server.py | 6 +++-- 3 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 examples/run_async.py diff --git a/examples/run_async.py b/examples/run_async.py new file mode 100644 index 00000000..71912e5a --- /dev/null +++ b/examples/run_async.py @@ -0,0 +1,20 @@ +from sanic import Sanic +from sanic.response import json +from multiprocessing import Event +from signal import signal, SIGINT +import asyncio + +app = Sanic(__name__) + +@app.route("/") +async def test(request): + return json({"answer": "42"}) + +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()) +try: + loop.run_forever() +except: + loop.stop() diff --git a/sanic/sanic.py b/sanic/sanic.py index cea09470..c56e2420 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -348,9 +348,8 @@ class Sanic: log.debug(self.config.LOGO) # Serve - if ssl is None: - proto = "http" - else: + proto = "http" + if ssl is not None: proto = "https" log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) @@ -378,6 +377,63 @@ class Sanic: self.sock.close() get_event_loop().stop() + async def create_server(self, host="127.0.0.1", port=8000, debug=False, + before_start=None, after_start=None, + before_stop=None, after_stop=None, ssl=None, + sock=None, loop=None, protocol=HttpProtocol, + backlog=100, stop_event=None): + ''' + Asynchronous version of `run`. + ''' + loop = get_event_loop() + server_settings = { + 'protocol': protocol, + 'host': host, + 'port': port, + 'sock': sock, + 'ssl': ssl, + 'debug': debug, + 'request_handler': self.handle_request, + 'error_handler': self.error_handler, + 'request_timeout': self.config.REQUEST_TIMEOUT, + 'request_max_size': self.config.REQUEST_MAX_SIZE, + 'loop': loop, + 'backlog': backlog + } + + # -------------------------------------------- # + # Register start/stop events + # -------------------------------------------- # + + for event_name, settings_name, args, reverse in ( + ("before_server_start", "before_start", before_start, False), + ("after_server_start", "after_start", after_start, False), + ("before_server_stop", "before_stop", before_stop, True), + ("after_server_stop", "after_stop", after_stop, True), + ): + listeners = [] + for blueprint in self.blueprints.values(): + listeners += blueprint.listeners[event_name] + if args: + if callable(args): + args = [args] + listeners += args + if reverse: + listeners.reverse() + # Prepend sanic to the arguments when listeners are triggered + listeners = [partial(listener, self) for listener in listeners] + server_settings[settings_name] = listeners + + server_settings['run_async'] = True + + # Serve + proto = "http" + if ssl is not None: + proto = "https" + log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + + return await serve(**server_settings) + def serve_multiple(self, server_settings, workers, stop_event=None): """ Starts multiple server processes simultaneously. Stops on interrupt diff --git a/sanic/server.py b/sanic/server.py index 74ffb832..cf04df16 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -262,7 +262,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, - register_sys_signals=True): + register_sys_signals=True, run_async=False): """ Starts asynchronous HTTP Server on an individual process. @@ -320,11 +320,13 @@ def serve(host, port, request_handler, error_handler, before_start=None, sock=sock, backlog=backlog ) - # Instead of pulling time at the end of every request, # pull it once per minute loop.call_soon(partial(update_current_time, loop)) + if run_async: + return server_coroutine + try: http_server = loop.run_until_complete(server_coroutine) except Exception: From 14697c7ea9126c60a133b00450b9609c18ffcc1f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 26 Jan 2017 08:36:00 -0800 Subject: [PATCH 268/977] review changes --- sanic/sanic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index c56e2420..4953ef58 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -351,7 +351,7 @@ class Sanic: proto = "http" if ssl is not None: proto = "https" - log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + log.info('Goin\' Fast @ {}://{}:{}', proto, host, port) try: if workers == 1: @@ -382,9 +382,9 @@ class Sanic: before_stop=None, after_stop=None, ssl=None, sock=None, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None): - ''' + """ Asynchronous version of `run`. - ''' + """ loop = get_event_loop() server_settings = { 'protocol': protocol, @@ -430,7 +430,7 @@ class Sanic: proto = "http" if ssl is not None: proto = "https" - log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + log.info('Goin\' Fast @ {}://{}:{}', proto, host, port) return await serve(**server_settings) From 011be512322bf25eb4012fc3c4475dbf3ab4e255 Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Fri, 27 Jan 2017 09:52:41 +1100 Subject: [PATCH 269/977] Add documentation for the CompositionView class --- docs/class_based_views.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index 0cf7f770..6ba1d567 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -77,6 +77,36 @@ class ViewWithDecorator(HTTPMethodView): app.add_route(ViewWithDecorator.as_view(), '/url') ``` +## Using CompositionView + +As an alternative to the `HTTPMethodView`, you can use `CompositionView` to +move handler functions outside of the view class. + +Handler functions for each supported HTTP method are defined elsewhere in the +source, and then added to the view using the `CompositionView.add` method. The +first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`), +and the second is the handler function. The following example shows +`CompositionView` usage with both an external handler function and an inline +lambda: + +```python +from sanic import Sanic +from sanic.views import CompositionView +from sanic.response import text + +app = Sanic(__name__) + +def get_handler(request): + return text('I am a get method') + +view = CompositionView() +view.add(['GET'], get_handler) +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, '/') +``` + **Previous:** [Blueprints](blueprints.md) **Next:** [Cookies](cookies.md) From 2a1b63c93cee525bba408c452ee55cd90cb5a6e2 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 26 Jan 2017 15:03:25 -0800 Subject: [PATCH 270/977] add deprecate loop message --- sanic/sanic.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sanic/sanic.py b/sanic/sanic.py index 4953ef58..bc532849 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -302,6 +302,10 @@ class Sanic: """ self.error_handler.debug = debug self.debug = debug + if loop is not None: + log.warning("Passing a loop will be deprecated in version 0.4.0" + " https://github.com/channelcat/sanic/pull/335" + " has more information.", DeprecationWarning) self.loop = loop server_settings = { @@ -385,6 +389,10 @@ class Sanic: """ Asynchronous version of `run`. """ + if loop is not None: + log.warning("Passing a loop will be deprecated in version 0.4.0" + " https://github.com/channelcat/sanic/pull/335" + " has more information.", DeprecationWarning) loop = get_event_loop() server_settings = { 'protocol': protocol, @@ -444,6 +452,10 @@ class Sanic: :param stop_event: if provided, is used as a stop signal :return: """ + if server_settings.get('loop', None) is not None: + log.warning("Passing a loop will be deprecated in version 0.4.0" + " https://github.com/channelcat/sanic/pull/335" + " has more information.", DeprecationWarning) server_settings['reuse_port'] = True # Create a stop event to be triggered by a signal From 965fbc917d1938f73f722da2dd1f2a7d9e2820fa Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 26 Jan 2017 15:11:53 -0800 Subject: [PATCH 271/977] use string formatting in start message --- sanic/sanic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 4953ef58..6f8acc2e 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -351,7 +351,7 @@ class Sanic: proto = "http" if ssl is not None: proto = "https" - log.info('Goin\' Fast @ {}://{}:{}', proto, host, port) + log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) try: if workers == 1: @@ -430,7 +430,7 @@ class Sanic: proto = "http" if ssl is not None: proto = "https" - log.info('Goin\' Fast @ {}://{}:{}', proto, host, port) + log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) return await serve(**server_settings) From d52f5f0b094088bc372cb01ab90f43e828da9195 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 21 Jan 2017 18:26:32 -0800 Subject: [PATCH 272/977] remove loop as argument and update examples --- examples/aiohttp_example.py | 9 +-- examples/sanic_aiopg_example.py | 60 +++++++++---------- examples/sanic_aiopg_sqlalchemy_example.py | 49 +++++++-------- examples/sanic_asyncpg_example.py | 53 +++++++--------- examples/sanic_peewee.py | 16 +++-- sanic/sanic.py | 7 +-- sanic/server.py | 2 +- sanic/utils.py | 4 +- ...{test_sanic.py => test_signal_handlers.py} | 33 ++++++---- 9 files changed, 109 insertions(+), 124 deletions(-) rename tests/{test_sanic.py => test_signal_handlers.py} (56%) diff --git a/examples/aiohttp_example.py b/examples/aiohttp_example.py index 8e7892a7..a8fb20f0 100644 --- a/examples/aiohttp_example.py +++ b/examples/aiohttp_example.py @@ -1,12 +1,8 @@ from sanic import Sanic from sanic.response import json -import uvloop import aiohttp -#Create an event loop manually so that we can use it for both sanic & aiohttp -loop = uvloop.new_event_loop() - app = Sanic(__name__) async def fetch(session, url): @@ -24,10 +20,9 @@ async def test(request): """ url = "https://api.github.com/repos/channelcat/sanic" - async with aiohttp.ClientSession(loop=loop) as session: + async with aiohttp.ClientSession() as session: response = await fetch(session, url) return json(response) -app.run(host="0.0.0.0", port=8000, loop=loop) - +app.run(host="0.0.0.0", port=8000, workers=2) diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index 73ef6c64..fb1ea100 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -10,8 +10,6 @@ import aiopg from sanic import Sanic from sanic.response import json -asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - database_name = os.environ['DATABASE_NAME'] database_host = os.environ['DATABASE_HOST'] database_user = os.environ['DATABASE_USER'] @@ -21,45 +19,47 @@ connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, database_password, database_host, database_name) -loop = asyncio.get_event_loop() async def get_pool(): return await aiopg.create_pool(connection) app = Sanic(name=__name__) -pool = loop.run_until_complete(get_pool()) - - -async def prepare_db(): - """ Let's create some table and add some data +async def prepare_db(app, loop): """ - async with pool.acquire() as conn: - async with conn.cursor() as cur: - await cur.execute('DROP TABLE IF EXISTS sanic_polls') - await cur.execute("""CREATE TABLE sanic_polls ( - id serial primary key, - question varchar(50), - pub_date timestamp - );""") - for i in range(0, 100): - await cur.execute("""INSERT INTO sanic_polls - (id, question, pub_date) VALUES ({}, {}, now()) - """.format(i, i)) + Let's create some table and add some data + """ + async with aiopg.create_pool(connection) as pool: + async with pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute('DROP TABLE IF EXISTS sanic_polls') + await cur.execute("""CREATE TABLE sanic_polls ( + id serial primary key, + question varchar(50), + pub_date timestamp + );""") + for i in range(0, 100): + await cur.execute("""INSERT INTO sanic_polls + (id, question, pub_date) VALUES ({}, {}, now()) + """.format(i, i)) @app.route("/") async def handle(request): - async with pool.acquire() as conn: - async with conn.cursor() as cur: - result = [] - await cur.execute("SELECT question, pub_date FROM sanic_polls") - async for row in cur: - result.append({"question": row[0], "pub_date": row[1]}) - return json({"polls": result}) - + result = [] + async def test_select(): + async with aiopg.create_pool(connection) as pool: + async with pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute("SELECT question, pub_date FROM sanic_polls") + async for row in cur: + result.append({"question": row[0], "pub_date": row[1]}) + res = await test_select() + return json({'polls': result}) if __name__ == '__main__': - loop.run_until_complete(prepare_db()) - app.run(host='0.0.0.0', port=8000, loop=loop) + app.run(host='0.0.0.0', + port=8000, + debug=True, + before_start=prepare_db) diff --git a/examples/sanic_aiopg_sqlalchemy_example.py b/examples/sanic_aiopg_sqlalchemy_example.py index cb9f6c57..802d4fbe 100644 --- a/examples/sanic_aiopg_sqlalchemy_example.py +++ b/examples/sanic_aiopg_sqlalchemy_example.py @@ -12,8 +12,6 @@ import sqlalchemy as sa from sanic import Sanic from sanic.response import json -asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - database_name = os.environ['DATABASE_NAME'] database_host = os.environ['DATABASE_HOST'] database_user = os.environ['DATABASE_USER'] @@ -23,8 +21,6 @@ connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, database_password, database_host, database_name) -loop = asyncio.get_event_loop() - metadata = sa.MetaData() @@ -34,40 +30,37 @@ polls = sa.Table('sanic_polls', metadata, sa.Column("pub_date", sa.DateTime)) -async def get_engine(): - return await create_engine(connection) - app = Sanic(name=__name__) -engine = loop.run_until_complete(get_engine()) -async def prepare_db(): +async def prepare_db(app, loop): """ Let's add some data """ - async with engine.acquire() as conn: - await conn.execute('DROP TABLE IF EXISTS sanic_polls') - await conn.execute("""CREATE TABLE sanic_polls ( - id serial primary key, - question varchar(50), - pub_date timestamp - );""") - for i in range(0, 100): - await conn.execute( - polls.insert().values(question=i, - pub_date=datetime.datetime.now()) - ) + async with create_engine(connection) as engine: + async with engine.acquire() as conn: + await conn.execute('DROP TABLE IF EXISTS sanic_polls') + await conn.execute("""CREATE TABLE sanic_polls ( + id serial primary key, + question varchar(50), + pub_date timestamp + );""") + for i in range(0, 100): + await conn.execute( + polls.insert().values(question=i, + pub_date=datetime.datetime.now()) + ) @app.route("/") async def handle(request): - async with engine.acquire() as conn: - result = [] - async for row in conn.execute(polls.select()): - result.append({"question": row.question, "pub_date": row.pub_date}) - return json({"polls": result}) + async with create_engine(connection) as engine: + async with engine.acquire() as conn: + result = [] + async for row in conn.execute(polls.select()): + result.append({"question": row.question, "pub_date": row.pub_date}) + return json({"polls": result}) if __name__ == '__main__': - loop.run_until_complete(prepare_db()) - app.run(host='0.0.0.0', port=8000, loop=loop) + app.run(host='0.0.0.0', port=8000, before_start=prepare_db) diff --git a/examples/sanic_asyncpg_example.py b/examples/sanic_asyncpg_example.py index 142480e1..9bb7d9c1 100644 --- a/examples/sanic_asyncpg_example.py +++ b/examples/sanic_asyncpg_example.py @@ -10,8 +10,6 @@ from asyncpg import create_pool from sanic import Sanic from sanic.response import json -asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - DB_CONFIG = { 'host': '', 'user': '', @@ -21,45 +19,40 @@ DB_CONFIG = { } def jsonify(records): - """ Parse asyncpg record response into JSON format - + """ + Parse asyncpg record response into JSON format """ return [{key: value for key, value in zip(r.keys(), r.values())} for r in records] -loop = asyncio.get_event_loop() - -async def make_pool(): - return await create_pool(**DB_CONFIG) - app = Sanic(__name__) -pool = loop.run_until_complete(make_pool()) - -async def create_db(): - """ Create some table and add some data +async def create_db(app, loop): """ - async with pool.acquire() as connection: - async with connection.transaction(): - await connection.execute('DROP TABLE IF EXISTS sanic_post') - await connection.execute("""CREATE TABLE sanic_post ( - id serial primary key, - content varchar(50), - post_date timestamp - );""") - for i in range(0, 100): - await connection.execute(f"""INSERT INTO sanic_post - (id, content, post_date) VALUES ({i}, {i}, now())""") + Create some table and add some data + """ + async with create_pool(**DB_CONFIG) as pool: + async with pool.acquire() as connection: + async with connection.transaction(): + await connection.execute('DROP TABLE IF EXISTS sanic_post') + await connection.execute("""CREATE TABLE sanic_post ( + id serial primary key, + content varchar(50), + post_date timestamp + );""") + for i in range(0, 100): + await connection.execute(f"""INSERT INTO sanic_post + (id, content, post_date) VALUES ({i}, {i}, now())""") @app.route("/") async def handler(request): - async with pool.acquire() as connection: - async with connection.transaction(): - results = await connection.fetch('SELECT * FROM sanic_post') - return json({'posts': jsonify(results)}) + async with create_pool(**DB_CONFIG) as pool: + async with pool.acquire() as connection: + async with connection.transaction(): + results = await connection.fetch('SELECT * FROM sanic_post') + return json({'posts': jsonify(results)}) if __name__ == '__main__': - loop.run_until_complete(create_db()) - app.run(host='0.0.0.0', port=8000, loop=loop) + app.run(host='0.0.0.0', port=8000, before_start=create_db) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index d5479193..f3981b36 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -9,19 +9,18 @@ from sanic import Sanic from sanic.response import json ## peewee_async related imports -import uvloop import peewee from peewee_async import Manager, PostgresqlDatabase # we instantiate a custom loop so we can pass it to our db manager -loop = uvloop.new_event_loop() -database = PostgresqlDatabase(database='test', - host='127.0.0.1', - user='postgres', - password='mysecretpassword') +def setup(app, loop): + database = PostgresqlDatabase(database='test', + host='127.0.0.1', + user='postgres', + password='mysecretpassword') -objects = Manager(database, loop=loop) + objects = Manager(database, loop=loop) ## from peewee_async docs: # Also there’s no need to connect and re-connect before executing async queries @@ -76,5 +75,4 @@ async def get(request): if __name__ == "__main__": - app.run(host='0.0.0.0', port=8000, loop=loop) - + app.run(host='0.0.0.0', port=8000, before_start=setup) diff --git a/sanic/sanic.py b/sanic/sanic.py index 0f882d01..b50922e9 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -42,7 +42,6 @@ class Sanic: self.response_middleware = deque() self.blueprints = {} self._blueprint_order = [] - self.loop = None self.debug = None self.sock = None self.processes = None @@ -275,8 +274,8 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, ssl=None, - sock=None, workers=1, loop=None, protocol=HttpProtocol, - backlog=100, stop_event=None, register_sys_signals=True): + sock=None, workers=1, protocol=HttpProtocol, backlog=100, + stop_event=None, register_sys_signals=True): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -296,7 +295,6 @@ class Sanic: :param sock: Socket for the server to accept connections from :param workers: Number of processes received before it is respected - :param loop: asyncio compatible event loop :param protocol: Subclass of asyncio protocol class :return: Nothing """ @@ -319,7 +317,6 @@ class Sanic: 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, - 'loop': loop, 'register_sys_signals': register_sys_signals, 'backlog': backlog } diff --git a/sanic/server.py b/sanic/server.py index aaf90727..1b19c784 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -289,7 +289,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() + loop = async_loop.new_event_loop() asyncio.set_event_loop(loop) if debug: diff --git a/sanic/utils.py b/sanic/utils.py index 1eaa0493..1943652c 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,7 +16,7 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, - loop=None, debug=False, server_kwargs={}, + debug=False, server_kwargs={}, *request_args, **request_kwargs): results = [] exceptions = [] @@ -36,7 +36,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, app.stop() app.run(host=HOST, debug=debug, port=PORT, - after_start=_collect_response, loop=loop, **server_kwargs) + after_start=_collect_response, **server_kwargs) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/tests/test_sanic.py b/tests/test_signal_handlers.py similarity index 56% rename from tests/test_sanic.py rename to tests/test_signal_handlers.py index 589253f7..756df828 100644 --- a/tests/test_sanic.py +++ b/tests/test_signal_handlers.py @@ -4,12 +4,20 @@ from sanic.utils import HOST, PORT from unittest.mock import MagicMock import pytest import asyncio +from queue import Queue -async def stop(app): - await asyncio.sleep(0.2) +async def stop(app, loop): + await asyncio.sleep(0.1) app.stop() +calledq = Queue() + +def set_loop(app, loop): + loop.add_signal_handler = MagicMock() + +def after(app, loop): + calledq.put(loop.add_signal_handler.called) def test_register_system_signals(): """Test if sanic register system signals""" @@ -19,11 +27,11 @@ def test_register_system_signals(): async def hello_route(request): return HTTPResponse() - loop = asyncio.new_event_loop() - loop.add_signal_handler = MagicMock() - asyncio.ensure_future(stop(app), loop=loop) - app.run(HOST, PORT, loop=loop) - assert loop.add_signal_handler.called == True + app.run(HOST, PORT, + before_start=set_loop, + after_start=stop, + after_stop=after) + assert calledq.get() == True def test_dont_register_system_signals(): @@ -34,8 +42,9 @@ def test_dont_register_system_signals(): async def hello_route(request): return HTTPResponse() - loop = asyncio.new_event_loop() - loop.add_signal_handler = MagicMock() - asyncio.ensure_future(stop(app), loop=loop) - app.run(HOST, PORT, loop=loop, register_sys_signals=False) - assert loop.add_signal_handler.called == False + app.run(HOST, PORT, + before_start=set_loop, + after_start=stop, + after_stop=after, + register_sys_signals=False) + assert calledq.get() == False From c72bcc136c8dd417efb49517dcb9c36a5b99d05d Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 25 Jan 2017 13:11:04 -0800 Subject: [PATCH 273/977] add semaphore concurrency limit example --- examples/limit_concurrency.py | 36 +++++++++++++++++++++++++++++++++++ sanic/sanic.py | 4 ++-- 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 examples/limit_concurrency.py diff --git a/examples/limit_concurrency.py b/examples/limit_concurrency.py new file mode 100644 index 00000000..16307462 --- /dev/null +++ b/examples/limit_concurrency.py @@ -0,0 +1,36 @@ +from sanic import Sanic +from sanic.response import json + +import asyncio +import aiohttp + +app = Sanic(__name__) + +sem = None + +def init(sanic, loop): + global sem + CONCURRENCY_PER_WORKER = 4 + sem = asyncio.Semaphore(CONCURRENCY_PER_WORKER, loop=loop) + +async def bounded_fetch(session, url): + """ + Use session object to perform 'get' request on url + """ + async with sem, session.get(url) as response: + return await response.json() + + +@app.route("/") +async def test(request): + """ + Download and serve example JSON + """ + url = "https://api.github.com/repos/channelcat/sanic" + + async with aiohttp.ClientSession() as session: + response = await bounded_fetch(session, url) + return json(response) + + +app.run(host="0.0.0.0", port=8000, workers=2, before_start=init) diff --git a/sanic/sanic.py b/sanic/sanic.py index b50922e9..e02f8780 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -274,8 +274,8 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, ssl=None, - sock=None, workers=1, protocol=HttpProtocol, backlog=100, - stop_event=None, register_sys_signals=True): + sock=None, workers=1, loop=None, protocol=HttpProtocol, + backlog=100, stop_event=None, register_sys_signals=True): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. From fa36dcbe098ad6f491507213223dc278ccc0d24f Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Fri, 27 Jan 2017 11:11:29 +0100 Subject: [PATCH 274/977] Use ``isinstance(`` instead of ``issubclass(type(`` When we already have an `instance` it's less typing and faster to use `isinstance`. --- examples/exception_monitoring.py | 2 +- sanic/exceptions.py | 2 +- tests/test_middleware.py | 2 +- tests/test_views.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py index 26c6d92b..ef510589 100644 --- a/examples/exception_monitoring.py +++ b/examples/exception_monitoring.py @@ -23,7 +23,7 @@ class CustomHandler(Handler): # and can do anything with it (log, send to external service, etc) # Some exceptions are trivial and built into Sanic (404s, etc) - if not issubclass(type(exception), SanicException): + if not isinstance(exception, SanicException): print(exception) # Then, we must finish handling the exception by returning diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 2596a97a..d986cd08 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -188,7 +188,7 @@ class Handler: def default(self, request, exception): log.error(format_exc()) - if issubclass(type(exception), SanicException): + if isinstance(exception, SanicException): return text( 'Error: {}'.format(exception), status=getattr(exception, 'status_code', 500)) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 5ff9e9b5..95580e1d 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -51,7 +51,7 @@ def test_middleware_response(): assert response.text == 'OK' assert type(results[0]) is Request assert type(results[1]) is Request - assert issubclass(type(results[2]), HTTPResponse) + assert isinstance(results[2], HTTPResponse) def test_middleware_override_request(): diff --git a/tests/test_views.py b/tests/test_views.py index 592893a4..24647cf6 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -152,7 +152,7 @@ def test_with_middleware_response(): assert response.text == 'I am get method' assert type(results[0]) is Request assert type(results[1]) is Request - assert issubclass(type(results[2]), HTTPResponse) + assert isinstance(results[2], HTTPResponse) def test_with_custom_class_methods(): From 59242df7d62d42e95217c5913dbd077d36678f82 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 27 Jan 2017 19:34:21 -0600 Subject: [PATCH 275/977] Move serve_multiple, fix tests (#357) * Move serve_multiple, remove stop_events, fix tests Moves serve_multiple out of the app, removes stop_event (adds a deprecation warning, but it also wasn't doing anything) fixes multiprocessing tests so that they don't freeze pytest's runner. Other notes: Also moves around some imports so that they are better optimized as well. * Re-add in stop_event, maybe it wasn't so bad! * Get rid of unused warnings import --- sanic/sanic.py | 68 +++----------------------- sanic/server.py | 67 ++++++++++++++++++++++++-- tests/test_logging.py | 14 ++++-- tests/test_multiprocessing.py | 89 +++++++---------------------------- 4 files changed, 94 insertions(+), 144 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index e02f8780..1ed14b7f 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,22 +1,18 @@ +import logging from asyncio import get_event_loop from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename -from multiprocessing import Process, Event -from signal import signal, SIGTERM, SIGINT from traceback import format_exc -import logging from .config import Config from .exceptions import Handler +from .exceptions import ServerError from .log import log from .response import HTTPResponse from .router import Router -from .server import serve, HttpProtocol +from .server import serve, serve_multiple, HttpProtocol from .static import register as static_register -from .exceptions import ServerError -from socket import socket, SOL_SOCKET, SO_REUSEADDR -from os import set_inheritable class Sanic: @@ -358,9 +354,7 @@ class Sanic: if workers == 1: serve(**server_settings) else: - log.info('Spinning up {} workers...'.format(workers)) - - self.serve_multiple(server_settings, workers, stop_event) + serve_multiple(server_settings, workers, stop_event) except Exception as e: log.exception( @@ -369,13 +363,7 @@ class Sanic: log.info("Server Stopped") def stop(self): - """ - This kills the Sanic - """ - if self.processes is not None: - for process in self.processes: - process.terminate() - self.sock.close() + """This kills the Sanic""" get_event_loop().stop() async def create_server(self, host="127.0.0.1", port=8000, debug=False, @@ -414,8 +402,7 @@ class Sanic: ("before_server_start", "before_start", before_start, False), ("after_server_start", "after_start", after_start, False), ("before_server_stop", "before_stop", before_stop, True), - ("after_server_stop", "after_stop", after_stop, True), - ): + ("after_server_stop", "after_stop", after_stop, True)): listeners = [] for blueprint in self.blueprints.values(): listeners += blueprint.listeners[event_name] @@ -438,46 +425,3 @@ class Sanic: log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) return await serve(**server_settings) - - def serve_multiple(self, server_settings, workers, stop_event=None): - """ - Starts multiple server processes simultaneously. Stops on interrupt - and terminate signals, and drains connections when complete. - - :param server_settings: kw arguments to be passed to the serve function - :param workers: number of workers to launch - :param stop_event: if provided, is used as a stop signal - :return: - """ - if server_settings.get('loop', None) is not None: - log.warning("Passing a loop will be deprecated in version 0.4.0" - " https://github.com/channelcat/sanic/pull/335" - " has more information.", DeprecationWarning) - server_settings['reuse_port'] = True - - # Create a stop event to be triggered by a signal - if stop_event is None: - stop_event = Event() - signal(SIGINT, lambda s, f: stop_event.set()) - signal(SIGTERM, lambda s, f: stop_event.set()) - - self.sock = socket() - self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) - self.sock.bind((server_settings['host'], server_settings['port'])) - set_inheritable(self.sock.fileno(), True) - server_settings['sock'] = self.sock - server_settings['host'] = None - server_settings['port'] = None - - self.processes = [] - for _ in range(workers): - process = Process(target=serve, kwargs=server_settings) - process.daemon = True - process.start() - self.processes.append(process) - - for process in self.processes: - process.join() - - # the above processes will block this until they're stopped - self.stop() diff --git a/sanic/server.py b/sanic/server.py index 1b19c784..dc3acc42 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,11 +1,18 @@ import asyncio +import os import traceback from functools import partial from inspect import isawaitable -from signal import SIGINT, SIGTERM +from multiprocessing import Process, Event +from os import set_inheritable +from signal import SIGTERM, SIGINT +from signal import signal as signal_func +from socket import socket, SOL_SOCKET, SO_REUSEADDR from time import time + from httptools import HttpRequestParser from httptools.parser.errors import HttpParserError + from .exceptions import ServerError try: @@ -17,7 +24,6 @@ from .log import log from .request import Request from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage - current_time = None @@ -31,6 +37,7 @@ class CIDict(dict): This does not maintain the inputted case when calling items() or keys() in favor of speed, since headers are case insensitive """ + def get(self, key, default=None): return super().get(key.casefold(), default) @@ -56,7 +63,7 @@ class HttpProtocol(asyncio.Protocol): '_total_request_size', '_timeout_handler', '_last_communication_time') def __init__(self, *, loop, request_handler, error_handler, - signal=Signal(), connections={}, request_timeout=60, + signal=Signal(), connections=set(), request_timeout=60, request_max_size=None): self.loop = loop self.transport = None @@ -328,7 +335,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, try: http_server = loop.run_until_complete(server_coroutine) - except Exception: + except: log.exception("Unable to start server") return @@ -339,10 +346,12 @@ def serve(host, port, request_handler, error_handler, before_start=None, for _signal in (SIGINT, SIGTERM): loop.add_signal_handler(_signal, loop.stop) + pid = os.getpid() try: + log.info('Starting worker [{}]'.format(pid)) loop.run_forever() finally: - log.info("Stop requested, draining connections...") + log.info("Stopping worker [{}]".format(pid)) # Run the on_stop function if provided trigger_events(before_stop, loop) @@ -362,3 +371,51 @@ def serve(host, port, request_handler, error_handler, before_start=None, trigger_events(after_stop, loop) loop.close() + + +def serve_multiple(server_settings, workers, stop_event=None): + """ + Starts multiple server processes simultaneously. Stops on interrupt + and terminate signals, and drains connections when complete. + + :param server_settings: kw arguments to be passed to the serve function + :param workers: number of workers to launch + :param stop_event: if provided, is used as a stop signal + :return: + """ + if server_settings.get('loop', None) is not None: + log.warning("Passing a loop will be deprecated in version 0.4.0" + " https://github.com/channelcat/sanic/pull/335" + " has more information.", DeprecationWarning) + server_settings['reuse_port'] = True + + sock = socket() + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + sock.bind((server_settings['host'], server_settings['port'])) + set_inheritable(sock.fileno(), True) + server_settings['sock'] = sock + server_settings['host'] = None + server_settings['port'] = None + + if stop_event is None: + stop_event = Event() + + signal_func(SIGINT, lambda s, f: stop_event.set()) + signal_func(SIGTERM, lambda s, f: stop_event.set()) + + processes = [] + for _ in range(workers): + process = Process(target=serve, kwargs=server_settings) + process.daemon = True + process.start() + processes.append(process) + + for process in processes: + process.join() + + # the above processes will block this until they're stopped + for process in processes: + process.terminate() + sock.close() + + asyncio.get_event_loop().stop() diff --git a/tests/test_logging.py b/tests/test_logging.py index b3e3c1fc..ec326276 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,4 +1,5 @@ import asyncio +import uuid from sanic.response import text from sanic import Sanic from io import StringIO @@ -9,10 +10,11 @@ logging_format = '''module: %(module)s; \ function: %(funcName)s(); \ message: %(message)s''' + def test_log(): log_stream = StringIO() for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) + logging.root.removeHandler(handler) logging.basicConfig( format=logging_format, level=logging.DEBUG, @@ -20,14 +22,16 @@ def test_log(): ) log = logging.getLogger() app = Sanic('test_logging') + rand_string = str(uuid.uuid4()) + @app.route('/') def handler(request): - log.info('hello world') + log.info(rand_string) return text('hello') request, response = sanic_endpoint_test(app) - log_text = log_stream.getvalue().strip().split('\n')[-3] - assert log_text == "module: test_logging; function: handler(); message: hello world" + log_text = log_stream.getvalue() + assert rand_string in log_text -if __name__ =="__main__": +if __name__ == "__main__": test_log() diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index e39c3d24..7f68c1b6 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,81 +1,26 @@ -from multiprocessing import Array, Event, Process -from time import sleep, time -from ujson import loads as json_loads - -import pytest +import multiprocessing +import random +import signal from sanic import Sanic -from sanic.response import json -from sanic.utils import local_request, HOST, PORT +from sanic.utils import HOST, PORT -# ------------------------------------------------------------ # -# GET -# ------------------------------------------------------------ # - -# TODO: Figure out why this freezes on pytest but not when -# executed via interpreter -@pytest.mark.skip( - reason="Freezes with pytest not on interpreter") def test_multiprocessing(): - app = Sanic('test_json') + """Tests that the number of children we produce is correct""" + # Selects a number at random so we can spot check + num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1)) + app = Sanic('test_multiprocessing') + process_list = set() - response = Array('c', 50) - @app.route('/') - async def handler(request): - return json({"test": True}) + def stop_on_alarm(*args): + for process in multiprocessing.active_children(): + process_list.add(process.pid) + process.terminate() - stop_event = Event() - async def after_start(*args, **kwargs): - http_response = await local_request('get', '/') - response.value = http_response.text.encode() - stop_event.set() + signal.signal(signal.SIGALRM, stop_on_alarm) + signal.alarm(1) + app.run(HOST, PORT, workers=num_workers) - def rescue_crew(): - sleep(5) - stop_event.set() + assert len(process_list) == num_workers - rescue_process = Process(target=rescue_crew) - rescue_process.start() - - app.serve_multiple({ - 'host': HOST, - 'port': PORT, - 'after_start': after_start, - 'request_handler': app.handle_request, - 'request_max_size': 100000, - }, workers=2, stop_event=stop_event) - - rescue_process.terminate() - - try: - results = json_loads(response.value) - except: - raise ValueError("Expected JSON response but got '{}'".format(response)) - - assert results.get('test') == True - -@pytest.mark.skip( - reason="Freezes with pytest not on interpreter") -def test_drain_connections(): - app = Sanic('test_json') - - @app.route('/') - async def handler(request): - return json({"test": True}) - - stop_event = Event() - async def after_start(*args, **kwargs): - http_response = await local_request('get', '/') - stop_event.set() - - start = time() - app.serve_multiple({ - 'host': HOST, - 'port': PORT, - 'after_start': after_start, - 'request_handler': app.handle_request, - }, workers=2, stop_event=stop_event) - end = time() - - assert end - start < 0.05 From cfc32d940a0a2b78386a00cb4bc71386a7ab7f97 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 27 Jan 2017 19:36:03 -0600 Subject: [PATCH 276/977] Increment version to 0.3.0 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 6f529eea..a0d0552f 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.2.0' +__version__ = '0.3.0' __all__ = ['Sanic', 'Blueprint'] From 0eb779185db3cb38999e3811df77c2c64aa585ac Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Fri, 27 Jan 2017 17:59:08 -0800 Subject: [PATCH 277/977] fix deprecation warnings --- sanic/sanic.py | 32 ++++++++++++++++++++++---------- sanic/server.py | 9 ++++++--- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 1ed14b7f..ea2e8bef 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -4,6 +4,7 @@ from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename from traceback import format_exc +import warnings from .config import Config from .exceptions import Handler @@ -175,9 +176,12 @@ class Sanic: def register_blueprint(self, *args, **kwargs): # TODO: deprecate 1.0 - log.warning("Use of register_blueprint will be deprecated in " - "version 1.0. Please use the blueprint method instead", - DeprecationWarning) + if self.debug: + warnings.simplefilter('default') + warnings.warn("Use of register_blueprint will be deprecated in " + "version 1.0. Please use the blueprint method" + " instead", + DeprecationWarning) return self.blueprint(*args, **kwargs) # -------------------------------------------------------------------- # @@ -296,12 +300,16 @@ class Sanic: """ self.error_handler.debug = debug self.debug = debug - if loop is not None: - log.warning("Passing a loop will be deprecated in version 0.4.0" - " https://github.com/channelcat/sanic/pull/335" - " has more information.", DeprecationWarning) self.loop = loop + if loop is not None: + if self.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) + server_settings = { 'protocol': protocol, 'host': host, @@ -375,9 +383,13 @@ class Sanic: Asynchronous version of `run`. """ if loop is not None: - log.warning("Passing a loop will be deprecated in version 0.4.0" - " https://github.com/channelcat/sanic/pull/335" - " has more information.", DeprecationWarning) + if self.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) + loop = get_event_loop() server_settings = { 'protocol': protocol, diff --git a/sanic/server.py b/sanic/server.py index dc3acc42..48c3827e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -9,6 +9,7 @@ from signal import SIGTERM, SIGINT from signal import signal as signal_func from socket import socket, SOL_SOCKET, SO_REUSEADDR from time import time +import warnings from httptools import HttpRequestParser from httptools.parser.errors import HttpParserError @@ -384,9 +385,11 @@ def serve_multiple(server_settings, workers, stop_event=None): :return: """ if server_settings.get('loop', None) is not None: - log.warning("Passing a loop will be deprecated in version 0.4.0" - " https://github.com/channelcat/sanic/pull/335" - " has more information.", DeprecationWarning) + if server_settings.get('debug', False): + 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) server_settings['reuse_port'] = True sock = socket() From 41c52487eeec920bfe3caae4eddb9c020d472d81 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 27 Jan 2017 21:00:33 -0600 Subject: [PATCH 278/977] Fixes route overloading for dynamic routes Addresses #353, now dynamic routes work alongside our newly minted overloaded routes! Also fixed an unintended side effect where methods were still being passed in as None for `Sanic.add_route`. --- sanic/router.py | 27 +++++++++++++++++++-------- sanic/sanic.py | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index ec67f690..e25572fb 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -210,29 +210,40 @@ class Router: url = host + url # Check against known static routes route = self.routes_static.get(url) + method_not_supported = InvalidUsage( + 'Method {} not allowed for URL {}'.format( + method, url), status_code=405) if route: + if route.methods and method not in route.methods: + raise method_not_supported match = route.pattern.match(url) else: + route_found = False # Move on to testing all regex routes for route in self.routes_dynamic[url_hash(url)]: match = route.pattern.match(url) - if match: + route_found |= match is not None + # Do early method checking + if match and method in route.methods: break else: # Lastly, check against all regex routes that cannot be hashed for route in self.routes_always_check: match = route.pattern.match(url) - if match: + route_found |= match is not None + # Do early method checking + if match and method in route.methods: break else: + # Route was found but the methods didn't match + if route_found: + raise method_not_supported raise NotFound('Requested URL {} not found'.format(url)) - if route.methods and method not in route.methods: - raise InvalidUsage( - 'Method {} not allowed for URL {}'.format( - method, url), status_code=405) - kwargs = {p.name: p.cast(value) for value, p in zip(match.groups(1), route.parameters)} - return route.handler, [], kwargs + route_handler = route.handler + if hasattr(route_handler, 'handlers'): + route_handler = route_handler.handlers[method] + return route_handler, [], kwargs diff --git a/sanic/sanic.py b/sanic/sanic.py index 1ed14b7f..00b1961b 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -90,7 +90,7 @@ class Sanic: def patch(self, uri, host=None): return self.route(uri, methods=["PATCH"], host=host) - def add_route(self, handler, uri, methods=None, host=None): + def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None): """ A helper method to register class instance or functions as a handler to the application url From 13803bdb304e78355a8b45078ca8c36adedc6775 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 27 Jan 2017 22:05:46 -0600 Subject: [PATCH 279/977] Update for HTTPMethodView compatibility --- sanic/constants.py | 1 + sanic/sanic.py | 10 +++++++++- tests/test_views.py | 34 ++++++++++++++++++---------------- 3 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 sanic/constants.py diff --git a/sanic/constants.py b/sanic/constants.py new file mode 100644 index 00000000..8bb97ed3 --- /dev/null +++ b/sanic/constants.py @@ -0,0 +1 @@ +HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE') \ No newline at end of file diff --git a/sanic/sanic.py b/sanic/sanic.py index 00b1961b..1e4de2fb 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -6,6 +6,7 @@ from inspect import isawaitable, stack, getmodulename from traceback import format_exc from .config import Config +from .constants import HTTP_METHODS from .exceptions import Handler from .exceptions import ServerError from .log import log @@ -90,6 +91,9 @@ class Sanic: def patch(self, uri, host=None): return self.route(uri, methods=["PATCH"], host=host) + def delete(self, uri, host=None): + return self.route(uri, methods=["DELETE"], host=host) + def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None): """ A helper method to register class instance or @@ -98,9 +102,13 @@ class Sanic: :param handler: function or class instance :param uri: path of the URL - :param methods: list or tuple of methods allowed + :param methods: list or tuple of methods allowed, these are overridden + if using a HTTPMethodView :return: function or class instance """ + # Handle HTTPMethodView differently + if hasattr(handler, 'view_class'): + methods = frozenset(HTTP_METHODS) self.route(uri=uri, methods=methods, host=host)(handler) return handler diff --git a/tests/test_views.py b/tests/test_views.py index 24647cf6..3c695500 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,43 +1,45 @@ +import pytest as pytest + from sanic import Sanic from sanic.response import text, HTTPResponse from sanic.views import HTTPMethodView from sanic.blueprints import Blueprint from sanic.request import Request from sanic.utils import sanic_endpoint_test +from sanic.constants import HTTP_METHODS -def test_methods(): +@pytest.mark.parametrize('method', HTTP_METHODS) +def test_methods(method): app = Sanic('test_methods') class DummyView(HTTPMethodView): def get(self, request): - return text('I am get method') + return text('I am GET method') def post(self, request): - return text('I am post method') + return text('I am POST method') def put(self, request): - return text('I am put method') + return text('I am PUT method') + + def head(self, request): + return text('I am HEAD method') + + def options(self, request): + return text('I am OPTIONS method') def patch(self, request): - return text('I am patch method') + return text('I am PATCH method') def delete(self, request): - return text('I am delete method') + return text('I am DELETE method') app.add_route(DummyView.as_view(), '/') - request, response = sanic_endpoint_test(app, method="get") - assert response.text == 'I am get method' - request, response = sanic_endpoint_test(app, method="post") - assert response.text == 'I am post method' - request, response = sanic_endpoint_test(app, method="put") - assert response.text == 'I am put method' - request, response = sanic_endpoint_test(app, method="patch") - assert response.text == 'I am patch method' - request, response = sanic_endpoint_test(app, method="delete") - assert response.text == 'I am delete method' + request, response = sanic_endpoint_test(app, method=method) + assert response.text == 'I am {} method'.format(method) def test_unexisting_methods(): From dea8e16f49223886b50032fe36f49e836b62ef23 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 27 Jan 2017 22:07:31 -0600 Subject: [PATCH 280/977] Force method to lower --- sanic/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/utils.py b/sanic/utils.py index 1943652c..644a2a22 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -9,7 +9,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) log.info(url) async with aiohttp.ClientSession(cookies=cookies) as session: - async with getattr(session, method)(url, *args, **kwargs) as response: + async with getattr( + session, method.lower())(url, *args, **kwargs) as response: response.text = await response.text() response.body = await response.read() return response From ae0876876ee36a2a632415f77d2cec49309cae47 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 27 Jan 2017 22:13:16 -0600 Subject: [PATCH 281/977] Switch them to verifying headers instead --- tests/test_views.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index 3c695500..4e5b17f0 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -16,30 +16,30 @@ def test_methods(method): class DummyView(HTTPMethodView): def get(self, request): - return text('I am GET method') + return text('', headers={'method': 'GET'}) def post(self, request): - return text('I am POST method') + return text('', headers={'method': 'POST'}) def put(self, request): - return text('I am PUT method') + return text('', headers={'method': 'PUT'}) def head(self, request): - return text('I am HEAD method') + return text('', headers={'method': 'HEAD'}) def options(self, request): - return text('I am OPTIONS method') + return text('', headers={'method': 'OPTIONS'}) def patch(self, request): - return text('I am PATCH method') + return text('', headers={'method': 'PATCH'}) def delete(self, request): - return text('I am DELETE method') + return text('', headers={'method': 'DELETE'}) app.add_route(DummyView.as_view(), '/') request, response = sanic_endpoint_test(app, method=method) - assert response.text == 'I am {} method'.format(method) + assert response.headers['method'] == method def test_unexisting_methods(): From d3344da9c59314e5260ae4f8c8cc1f30e5d69e3f Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 27 Jan 2017 22:15:34 -0600 Subject: [PATCH 282/977] Add a pesky newline --- sanic/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/constants.py b/sanic/constants.py index 8bb97ed3..0d659f0c 100644 --- a/sanic/constants.py +++ b/sanic/constants.py @@ -1 +1 @@ -HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE') \ No newline at end of file +HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE') From 753d2da6dbd0deb6f8cb2ceab7c93c8fba56acac Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 28 Jan 2017 15:26:44 -0800 Subject: [PATCH 283/977] fix async run --- sanic/server.py | 4 ++-- tests/test_loop_policy.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 tests/test_loop_policy.py diff --git a/sanic/server.py b/sanic/server.py index 48c3827e..be9bc31c 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -297,8 +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() - asyncio.set_event_loop(loop) + loop = asyncio.get_event_loop() + asyncio.set_event_loop_policy(async_loop.EventLoopPolicy()) if debug: loop.set_debug(debug) diff --git a/tests/test_loop_policy.py b/tests/test_loop_policy.py new file mode 100644 index 00000000..f764548c --- /dev/null +++ b/tests/test_loop_policy.py @@ -0,0 +1,27 @@ +from sanic import Sanic +import asyncio +from signal import signal, SIGINT +import uvloop + + +def test_loop_policy(): + app = Sanic('test_loop_policy') + + server = app.create_server(host="0.0.0.0", port=8000) + + loop = asyncio.get_event_loop() + task = asyncio.ensure_future(server) + signal(SIGINT, lambda s, f: loop.close()) + + # serve() sets the event loop policy to uvloop but + # doesn't get called until we run the server task + assert isinstance(asyncio.get_event_loop_policy(), + asyncio.unix_events._UnixDefaultEventLoopPolicy) + + try: + loop.run_until_complete(task) + except: + loop.stop() + + assert isinstance(asyncio.get_event_loop_policy(), + uvloop.EventLoopPolicy) From de32c389d03e20069600a7f94081d58df54a34ac Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 12:47:00 -0800 Subject: [PATCH 284/977] Added basic readthedocs support --- README.rst | 21 +++------------- docs/Makefile | 20 +++++++++++++++ docs/conf.py | 6 ++++- docs/index.rst | 31 +++++++++++------------ docs/make.bat | 36 +++++++++++++++++++++++++++ docs/{ => sanic}/blueprints.md | 4 --- docs/{ => sanic}/class_based_views.md | 4 --- docs/{ => sanic}/contributing.md | 2 -- docs/{ => sanic}/cookies.md | 4 --- docs/{ => sanic}/custom_protocol.md | 4 --- docs/{ => sanic}/deploying.md | 4 --- docs/{ => sanic}/exceptions.md | 4 --- docs/{ => sanic}/extensions.md | 6 +---- docs/{ => sanic}/getting_started.md | 2 -- docs/{ => sanic}/middleware.md | 4 --- docs/{ => sanic}/request_data.md | 4 --- docs/{ => sanic}/routing.md | 4 --- docs/sanic/ssl.rst | 12 +++++++++ docs/{ => sanic}/static_files.md | 4 --- docs/{ => sanic}/testing.md | 4 --- requirements-dev.txt | 1 + 21 files changed, 93 insertions(+), 88 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/make.bat rename docs/{ => sanic}/blueprints.md (97%) rename docs/{ => sanic}/class_based_views.md (97%) rename docs/{ => sanic}/contributing.md (95%) rename docs/{ => sanic}/cookies.md (95%) rename docs/{ => sanic}/custom_protocol.md (97%) rename docs/{ => sanic}/deploying.md (96%) rename docs/{ => sanic}/exceptions.md (94%) rename docs/{ => sanic}/extensions.md (77%) rename docs/{ => sanic}/getting_started.md (95%) rename docs/{ => sanic}/middleware.md (96%) rename docs/{ => sanic}/request_data.md (97%) rename docs/{ => sanic}/routing.md (96%) create mode 100644 docs/sanic/ssl.rst rename docs/{ => sanic}/static_files.md (87%) rename docs/{ => sanic}/testing.md (95%) diff --git a/README.rst b/README.rst index ab25debf..b9414b0d 100644 --- a/README.rst +++ b/README.rst @@ -45,10 +45,8 @@ Hello World Example from sanic import Sanic from sanic.response import json - app = Sanic() - @app.route("/") async def test(request): return json({"hello": "world"}) @@ -56,21 +54,6 @@ Hello World Example if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) -SSL Example ------------ - -Optionally pass in an SSLContext: - -.. code:: python - - import ssl - certificate = "/path/to/certificate" - keyfile = "/path/to/keyfile" - context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) - context.load_cert_chain(certificate, keyfile=keyfile) - - app.run(host="0.0.0.0", port=8443, ssl=context) - Installation ------------ @@ -79,12 +62,14 @@ Installation Documentation ------------- -Documentation can be found in the ``docs`` directory. +`Documentation on Readthedocs `_. .. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge .. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master :target: https://travis-ci.org/channelcat/sanic +.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest + :target: http://sanic.readthedocs.io/en/latest/?badge=latest .. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg :target: https://pypi.python.org/pypi/sanic/ .. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..72df772a --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = Moon +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index caf79901..c929e9e7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -75,7 +75,7 @@ todo_include_todos = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -153,3 +153,7 @@ epub_copyright = copyright epub_exclude_files = ['search.html'] + +# -- Custom Settings ------------------------------------------------------- + +suppress_warnings = ['image.nonlocal_uri'] diff --git a/docs/index.rst b/docs/index.rst index c0b565c0..cce40f69 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,20 +6,21 @@ Guides .. toctree:: :maxdepth: 2 - getting_started - routing - request_data - deploying - static_files - middleware - exceptions - blueprints - class_based_views - cookies - custom_protocol - testing - extensions - contributing + sanic/getting_started + sanic/routing + sanic/request_data + sanic/static_files + sanic/exceptions + sanic/middleware + sanic/blueprints + sanic/cookies + sanic/class_based_views + sanic/custom_protocol + sanic/ssl + sanic/testing + sanic/deploying + sanic/extensions + sanic/contributing Module Documentation @@ -27,7 +28,5 @@ Module Documentation .. toctree:: - Module Reference <_api/sanic> - * :ref:`genindex` * :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..edb4a680 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=Moon + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/blueprints.md b/docs/sanic/blueprints.md similarity index 97% rename from docs/blueprints.md rename to docs/sanic/blueprints.md index be95c5cc..71849ac1 100644 --- a/docs/blueprints.md +++ b/docs/sanic/blueprints.md @@ -158,7 +158,3 @@ app.blueprint(blueprint_v2) app.run(host='0.0.0.0', port=8000, debug=True) ``` - -**Previous:** [Exceptions](exceptions.md) - -**Next:** [Class-based views](class_based_views.md) diff --git a/docs/class_based_views.md b/docs/sanic/class_based_views.md similarity index 97% rename from docs/class_based_views.md rename to docs/sanic/class_based_views.md index 6ba1d567..5f99a0b5 100644 --- a/docs/class_based_views.md +++ b/docs/sanic/class_based_views.md @@ -106,7 +106,3 @@ 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, '/') ``` - -**Previous:** [Blueprints](blueprints.md) - -**Next:** [Cookies](cookies.md) diff --git a/docs/contributing.md b/docs/sanic/contributing.md similarity index 95% rename from docs/contributing.md rename to docs/sanic/contributing.md index dde57270..dbc64a02 100644 --- a/docs/contributing.md +++ b/docs/sanic/contributing.md @@ -31,5 +31,3 @@ One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged. Please don't let this intimidate you! If you have any concerns about an idea, open an issue for discussion and help. - -**Previous:** [Sanic extensions](extensions.md) diff --git a/docs/cookies.md b/docs/sanic/cookies.md similarity index 95% rename from docs/cookies.md rename to docs/sanic/cookies.md index d593ed09..0a1042a2 100644 --- a/docs/cookies.md +++ b/docs/sanic/cookies.md @@ -73,7 +73,3 @@ parameters available: HTTPS. - `httponly` (boolean): Specifies whether the cookie cannot be read by Javascript. - -**Previous:** [Class-based views](class_based_views.md) - -**Next:** [Custom protocols](custom_protocol.md) diff --git a/docs/custom_protocol.md b/docs/sanic/custom_protocol.md similarity index 97% rename from docs/custom_protocol.md rename to docs/sanic/custom_protocol.md index 73d1f1d3..8355e8e9 100644 --- a/docs/custom_protocol.md +++ b/docs/sanic/custom_protocol.md @@ -70,7 +70,3 @@ async def response(request): app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) ``` - -**Previous:** [Cookies](cookies.md) - -**Next:** [Testing](testing.md) diff --git a/docs/deploying.md b/docs/sanic/deploying.md similarity index 96% rename from docs/deploying.md rename to docs/sanic/deploying.md index f4d63163..0c526efe 100644 --- a/docs/deploying.md +++ b/docs/sanic/deploying.md @@ -53,7 +53,3 @@ directly run by the interpreter. if __name__ == '__main__': app.run(host='0.0.0.0', port=1337, workers=4) ``` - -**Previous:** [Request Data](request_data.md) - -**Next:** [Static Files](static_files.md) diff --git a/docs/exceptions.md b/docs/sanic/exceptions.md similarity index 94% rename from docs/exceptions.md rename to docs/sanic/exceptions.md index 8a294492..cffa219c 100644 --- a/docs/exceptions.md +++ b/docs/sanic/exceptions.md @@ -43,7 +43,3 @@ Some of the most useful exceptions are presented below: usually occurs if there is an exception raised in user code. See the `sanic.exceptions` module for the full list of exceptions to throw. - -**Previous:** [Middleware](middleware.md) - -**Next:** [Blueprints](blueprints.md) diff --git a/docs/extensions.md b/docs/sanic/extensions.md similarity index 77% rename from docs/extensions.md rename to docs/sanic/extensions.md index 1cf6121b..8303311a 100644 --- a/docs/extensions.md +++ b/docs/sanic/extensions.md @@ -1,4 +1,4 @@ -# Sanic Extensions +# Extensions A list of Sanic extensions created by the community. @@ -6,7 +6,3 @@ 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. - -**Previous:** [Testing](testing.md) - -**Next:** [Contributing](contributing.md) diff --git a/docs/getting_started.md b/docs/sanic/getting_started.md similarity index 95% rename from docs/getting_started.md rename to docs/sanic/getting_started.md index e3a2a31b..04d22248 100644 --- a/docs/getting_started.md +++ b/docs/sanic/getting_started.md @@ -25,5 +25,3 @@ syntax, so earlier versions of python won't work. the message *Hello world!*. You now have a working Sanic server! - -**Next:** [Routing](routing.md) diff --git a/docs/middleware.md b/docs/sanic/middleware.md similarity index 96% rename from docs/middleware.md rename to docs/sanic/middleware.md index 6adb9328..58ff8feb 100644 --- a/docs/middleware.md +++ b/docs/sanic/middleware.md @@ -64,7 +64,3 @@ async def halt_request(request): async def halt_response(request, response): return text('I halted the response') ``` - -**Previous:** [Static Files](static_files.md) - -**Next:** [Exceptions](exceptions.md) diff --git a/docs/request_data.md b/docs/sanic/request_data.md similarity index 97% rename from docs/request_data.md rename to docs/sanic/request_data.md index 555cf765..c1eea5c1 100644 --- a/docs/request_data.md +++ b/docs/sanic/request_data.md @@ -89,7 +89,3 @@ args.get('titles') # => 'Post 1' args.getlist('titles') # => ['Post 1', 'Post 2'] ``` - -**Previous:** [Routing](routing.md) - -**Next:** [Deploying](deploying.md) diff --git a/docs/routing.md b/docs/sanic/routing.md similarity index 96% rename from docs/routing.md rename to docs/sanic/routing.md index d88bcf26..9d5856d6 100644 --- a/docs/routing.md +++ b/docs/sanic/routing.md @@ -105,7 +105,3 @@ app.add_route(handler1, '/test') app.add_route(handler2, '/folder/') app.add_route(person_handler2, '/person/', methods=['GET']) ``` - -**Previous:** [Getting Started](getting_started.md) - -**Next:** [Request Data](request_data.md) diff --git a/docs/sanic/ssl.rst b/docs/sanic/ssl.rst new file mode 100644 index 00000000..ef2cfd34 --- /dev/null +++ b/docs/sanic/ssl.rst @@ -0,0 +1,12 @@ +SSL Example +----------- + +Optionally pass in an SSLContext: + +.. code:: python + + import ssl + context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile") + + app.run(host="0.0.0.0", port=8443, ssl=context) \ No newline at end of file diff --git a/docs/static_files.md b/docs/sanic/static_files.md similarity index 87% rename from docs/static_files.md rename to docs/sanic/static_files.md index 5daf7818..d7e4866d 100644 --- a/docs/static_files.md +++ b/docs/sanic/static_files.md @@ -17,7 +17,3 @@ app.static('/the_best.png', '/home/ubuntu/test.png') app.run(host="0.0.0.0", port=8000) ``` - -**Previous:** [Deploying](deploying.md) - -**Next:** [Middleware](middleware.md) diff --git a/docs/testing.md b/docs/sanic/testing.md similarity index 95% rename from docs/testing.md rename to docs/sanic/testing.md index bdb85efb..79c719e8 100644 --- a/docs/testing.md +++ b/docs/sanic/testing.md @@ -49,7 +49,3 @@ def test_endpoint_challenge(): # Assert that the server responds with the challenge string assert response.text == request_data['challenge'] ``` - -**Previous:** [Custom protocols](custom_protocol.md) - -**Next:** [Sanic extensions](extensions.md) diff --git a/requirements-dev.txt b/requirements-dev.txt index b41bb274..64542931 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,5 +13,6 @@ falcon tornado aiofiles sphinx +sphinx_rtd_theme recommonmark beautifulsoup4 From 0eaccea38f2b25e4e2d582d60523e743bc1a5142 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 12:49:59 -0800 Subject: [PATCH 285/977] updated project name in docs build --- docs/Makefile | 2 +- docs/make.bat | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 72df772a..ef166d7d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -SPHINXPROJ = Moon +SPHINXPROJ = Sanic SOURCEDIR = . BUILDDIR = _build diff --git a/docs/make.bat b/docs/make.bat index edb4a680..54191087 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -9,7 +9,7 @@ if "%SPHINXBUILD%" == "" ( ) set SOURCEDIR=. set BUILDDIR=_build -set SPHINXPROJ=Moon +set SPHINXPROJ=Sanic if "%1" == "" goto help From 0a5fa72099dfebbe3b05db9fd881acb386e3c805 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 29 Jan 2017 15:16:07 -0600 Subject: [PATCH 286/977] Add logic to make dynamic route merging work This is by no means the final solution but it's a start in the right direction. Eventually what needs to happen is we need to reduce the complexity of the routing. CompsitionView can probably be removed later on in favor of better Route objects. Also in the next version of sanic we need to move merge_route and add_parameter out of the add_route logic and just have them as standalone methods. The tests should cover everything that we need so that if any changes are made we can identify regression. --- sanic/router.py | 25 +++++++++++++++++++- tests/test_dynamic_routes.py | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tests/test_dynamic_routes.py diff --git a/sanic/router.py b/sanic/router.py index e25572fb..1f26171e 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -149,7 +149,22 @@ class Router: handler=view, methods=methods.union(route.methods)) return route - route = self.routes_all.get(uri) + if parameters: + # TODO: This is too complex, we need to reduce the complexity + if properties['unhashable']: + routes_to_check = self.routes_always_check + ndx, route = self.check_dynamic_route_exists( + pattern, routes_to_check) + else: + routes_to_check = self.routes_dynamic[url_hash(uri)] + ndx, route = self.check_dynamic_route_exists( + pattern, routes_to_check) + if ndx != -1: + # Pop the ndx of the route, no dups of the same route + routes_to_check.pop(ndx) + else: + route = self.routes_all.get(uri) + if route: route = merge_route(route, methods, handler) else: @@ -165,6 +180,14 @@ class Router: else: self.routes_static[uri] = route + @staticmethod + def check_dynamic_route_exists(pattern, routes_to_check): + for ndx, route in enumerate(routes_to_check): + if route.pattern == pattern: + return ndx, route + else: + return -1, None + def remove(self, uri, clean_cache=True, host=None): if host is not None: uri = host + uri diff --git a/tests/test_dynamic_routes.py b/tests/test_dynamic_routes.py new file mode 100644 index 00000000..24ba79b5 --- /dev/null +++ b/tests/test_dynamic_routes.py @@ -0,0 +1,46 @@ +from sanic import Sanic +from sanic.response import text +from sanic.utils import sanic_endpoint_test +from sanic.router import RouteExists +import pytest + + +@pytest.mark.parametrize("method,attr, expected", [ + ("get", "text", "OK1 test"), + ("post", "text", "OK2 test"), + ("put", "text", "OK2 test"), + ("delete", "status", 405), +]) +def test_overload_dynamic_routes(method, attr, expected): + app = Sanic('test_dynamic_route') + + @app.route('/overload/', methods=['GET']) + async def handler1(request, param): + return text('OK1 ' + param) + + @app.route('/overload/', methods=['POST', 'PUT']) + async def handler2(request, param): + return text('OK2 ' + param) + + request, response = sanic_endpoint_test( + app, method, uri='/overload/test') + assert getattr(response, attr) == expected + + +def test_overload_dynamic_routes_exist(): + app = Sanic('test_dynamic_route') + + @app.route('/overload/', methods=['GET']) + async def handler1(request, param): + return text('OK1 ' + param) + + @app.route('/overload/', methods=['POST', 'PUT']) + async def handler2(request, param): + return text('OK2 ' + param) + + # if this doesn't raise an error, than at least the below should happen: + # assert response.text == 'Duplicated' + with pytest.raises(RouteExists): + @app.route('/overload/', methods=['PUT', 'DELETE']) + async def handler3(request): + return text('Duplicated') From 10dbb9186d3ba0f23728f5974bc12262be67d7c3 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 28 Jan 2017 14:54:15 -0800 Subject: [PATCH 287/977] combine logic from create_server() and run() --- sanic/sanic.py | 159 +++++++++++++++++++------------------- sanic/server.py | 4 +- tests/test_loop_policy.py | 27 ------- 3 files changed, 81 insertions(+), 109 deletions(-) delete mode 100644 tests/test_loop_policy.py diff --git a/sanic/sanic.py b/sanic/sanic.py index ea2e8bef..e468ad7c 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -298,6 +298,84 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ + server_settings = \ + self._helper(host=host, port=port, debug=debug, + before_start=before_start, after_start=after_start, + before_stop=before_stop, after_stop=after_stop, + ssl=ssl, sock=sock, workers=workers, loop=loop, + protocol=protocol, backlog=backlog, + stop_event=stop_event, + register_sys_signals=register_sys_signals) + try: + if workers == 1: + serve(**server_settings) + else: + serve_multiple(server_settings, workers, stop_event) + + except Exception as e: + log.exception( + 'Experienced exception while trying to serve') + + log.info("Server Stopped") + + def stop(self): + """This kills the Sanic""" + get_event_loop().stop() + + async def create_server(self, host="127.0.0.1", port=8000, debug=False, + before_start=None, after_start=None, + before_stop=None, after_stop=None, ssl=None, + sock=None, loop=None, protocol=HttpProtocol, + backlog=100, stop_event=None): + """ + Asynchronous version of `run`. + """ + server_settings = \ + self._helper(host=host, port=port, debug=debug, + before_start=before_start, 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) + + server_settings['run_async'] = True + + # Serve + proto = "http" + if ssl is not None: + proto = "https" + log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + + return await serve(**server_settings) + + def _helper(self, host="127.0.0.1", port=8000, debug=False, + before_start=None, after_start=None, before_stop=None, + after_stop=None, ssl=None, sock=None, workers=1, loop=None, + protocol=HttpProtocol, backlog=100, stop_event=None, + register_sys_signals=True): + """ + Runs the HTTP Server and listens until keyboard interrupt or term + signal. On termination, drains connections before closing. + + :param host: Address to host on + :param port: Port to host on + :param debug: Enables debug output (slows server) + :param before_start: Functions to be executed before the server starts + accepting connections + :param after_start: Functions to be executed after the server starts + accepting connections + :param before_stop: Functions to be executed when a stop signal is + received before it is respected + :param after_stop: Functions to be executed when all requests are + complete + :param ssl: SSLContext for SSL encryption of worker(s) + :param sock: Socket for the server to accept connections from + :param workers: Number of processes + received before it is respected + :param protocol: Subclass of asyncio protocol class + :return: Nothing + """ + self.error_handler.debug = debug self.debug = debug self.loop = loop @@ -357,83 +435,4 @@ class Sanic: if ssl is not None: proto = "https" log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) - - try: - if workers == 1: - serve(**server_settings) - else: - serve_multiple(server_settings, workers, stop_event) - - except Exception as e: - log.exception( - 'Experienced exception while trying to serve') - - log.info("Server Stopped") - - def stop(self): - """This kills the Sanic""" - get_event_loop().stop() - - async def create_server(self, host="127.0.0.1", port=8000, debug=False, - before_start=None, after_start=None, - before_stop=None, after_stop=None, ssl=None, - sock=None, loop=None, protocol=HttpProtocol, - backlog=100, stop_event=None): - """ - Asynchronous version of `run`. - """ - if loop is not None: - if self.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) - - loop = get_event_loop() - server_settings = { - 'protocol': protocol, - 'host': host, - 'port': port, - 'sock': sock, - 'ssl': ssl, - 'debug': debug, - 'request_handler': self.handle_request, - 'error_handler': self.error_handler, - 'request_timeout': self.config.REQUEST_TIMEOUT, - 'request_max_size': self.config.REQUEST_MAX_SIZE, - 'loop': loop, - 'backlog': backlog - } - - # -------------------------------------------- # - # Register start/stop events - # -------------------------------------------- # - - for event_name, settings_name, args, reverse in ( - ("before_server_start", "before_start", before_start, False), - ("after_server_start", "after_start", after_start, False), - ("before_server_stop", "before_stop", before_stop, True), - ("after_server_stop", "after_stop", after_stop, True)): - listeners = [] - for blueprint in self.blueprints.values(): - listeners += blueprint.listeners[event_name] - if args: - if callable(args): - args = [args] - listeners += args - if reverse: - listeners.reverse() - # Prepend sanic to the arguments when listeners are triggered - listeners = [partial(listener, self) for listener in listeners] - server_settings[settings_name] = listeners - - server_settings['run_async'] = True - - # Serve - proto = "http" - if ssl is not None: - proto = "https" - log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) - - return await serve(**server_settings) + return server_settings diff --git a/sanic/server.py b/sanic/server.py index be9bc31c..48c3827e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -297,8 +297,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param protocol: Subclass of asyncio protocol class :return: Nothing """ - loop = asyncio.get_event_loop() - asyncio.set_event_loop_policy(async_loop.EventLoopPolicy()) + loop = async_loop.new_event_loop() + asyncio.set_event_loop(loop) if debug: loop.set_debug(debug) diff --git a/tests/test_loop_policy.py b/tests/test_loop_policy.py deleted file mode 100644 index f764548c..00000000 --- a/tests/test_loop_policy.py +++ /dev/null @@ -1,27 +0,0 @@ -from sanic import Sanic -import asyncio -from signal import signal, SIGINT -import uvloop - - -def test_loop_policy(): - app = Sanic('test_loop_policy') - - server = app.create_server(host="0.0.0.0", port=8000) - - loop = asyncio.get_event_loop() - task = asyncio.ensure_future(server) - signal(SIGINT, lambda s, f: loop.close()) - - # serve() sets the event loop policy to uvloop but - # doesn't get called until we run the server task - assert isinstance(asyncio.get_event_loop_policy(), - asyncio.unix_events._UnixDefaultEventLoopPolicy) - - try: - loop.run_until_complete(task) - except: - loop.stop() - - assert isinstance(asyncio.get_event_loop_policy(), - uvloop.EventLoopPolicy) From 884d3a03163c329a424fb75ee1b72b90eb58a697 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 13:45:44 -0800 Subject: [PATCH 288/977] Fix RTD build --- requirements-rtd.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements-rtd.txt diff --git a/requirements-rtd.txt b/requirements-rtd.txt new file mode 100644 index 00000000..856b3f4c --- /dev/null +++ b/requirements-rtd.txt @@ -0,0 +1,2 @@ +sphinx +sphinx_rtd_theme \ No newline at end of file From 3fd6ecaedbd60fa7369ba814f2394073b43ff410 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 13:52:17 -0800 Subject: [PATCH 289/977] testing --- environment.yml | 7 +++++++ readthedocs.yml | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 environment.yml create mode 100644 readthedocs.yml diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..c74a5137 --- /dev/null +++ b/environment.yml @@ -0,0 +1,7 @@ +name: py35 +dependencies: +- pip: + - uvloop>=0.5.3 + - httptools>=0.0.9 + - ujson>=1.35 + - aiofiles>=0.3.0 \ No newline at end of file diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 00000000..40c09542 --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,2 @@ +conda: + file: environment.yml \ No newline at end of file From 85639d0806154d4aa43283d29451ec8a983a7739 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 29 Jan 2017 15:55:47 -0600 Subject: [PATCH 290/977] Revert "testing" This reverts commit 3fd6ecaedbd60fa7369ba814f2394073b43ff410. --- environment.yml | 7 ------- readthedocs.yml | 2 -- 2 files changed, 9 deletions(-) delete mode 100644 environment.yml delete mode 100644 readthedocs.yml diff --git a/environment.yml b/environment.yml deleted file mode 100644 index c74a5137..00000000 --- a/environment.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: py35 -dependencies: -- pip: - - uvloop>=0.5.3 - - httptools>=0.0.9 - - ujson>=1.35 - - aiofiles>=0.3.0 \ No newline at end of file diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index 40c09542..00000000 --- a/readthedocs.yml +++ /dev/null @@ -1,2 +0,0 @@ -conda: - file: environment.yml \ No newline at end of file From 82d1d30a41ea61f23884bdc862dd6b090ff0bda4 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 29 Jan 2017 14:01:00 -0800 Subject: [PATCH 291/977] review updates --- sanic/sanic.py | 59 +++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index e468ad7c..a81a336a 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -298,14 +298,12 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ - server_settings = \ - self._helper(host=host, port=port, debug=debug, - before_start=before_start, after_start=after_start, - before_stop=before_stop, after_stop=after_stop, - ssl=ssl, sock=sock, workers=workers, loop=loop, - protocol=protocol, backlog=backlog, - stop_event=stop_event, - register_sys_signals=register_sys_signals) + server_settings = self._helper( + host=host, port=port, debug=debug, before_start=before_start, + after_start=after_start, before_stop=before_stop, + after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, + loop=loop, protocol=protocol, backlog=backlog, + stop_event=stop_event, register_sys_signals=register_sys_signals) try: if workers == 1: serve(**server_settings) @@ -330,15 +328,12 @@ class Sanic: """ Asynchronous version of `run`. """ - server_settings = \ - self._helper(host=host, port=port, debug=debug, - before_start=before_start, 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) - - server_settings['run_async'] = True + server_settings = self._helper( + host=host, port=port, debug=debug, before_start=before_start, + 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) # Serve proto = "http" @@ -352,33 +347,14 @@ class Sanic: before_start=None, after_start=None, before_stop=None, after_stop=None, ssl=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None, - register_sys_signals=True): + register_sys_signals=True, run_async=False): """ - Runs the HTTP Server and listens until keyboard interrupt or term - signal. On termination, drains connections before closing. - - :param host: Address to host on - :param port: Port to host on - :param debug: Enables debug output (slows server) - :param before_start: Functions to be executed before the server starts - accepting connections - :param after_start: Functions to be executed after the server starts - accepting connections - :param before_stop: Functions to be executed when a stop signal is - received before it is respected - :param after_stop: Functions to be executed when all requests are - complete - :param ssl: SSLContext for SSL encryption of worker(s) - :param sock: Socket for the server to accept connections from - :param workers: Number of processes - received before it is respected - :param protocol: Subclass of asyncio protocol class - :return: Nothing + Helper function used by `run` and `create_server`. """ self.error_handler.debug = debug self.debug = debug - self.loop = loop + self.loop = loop = get_event_loop() if loop is not None: if self.debug: @@ -399,6 +375,7 @@ class Sanic: 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, + 'loop': loop, 'register_sys_signals': register_sys_signals, 'backlog': backlog } @@ -430,9 +407,13 @@ class Sanic: log.setLevel(logging.DEBUG) log.debug(self.config.LOGO) + if run_async: + server_settings['run_async'] = True + # Serve proto = "http" if ssl is not None: proto = "https" log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + return server_settings From 2a9496fcdabc800df5522de8d4df68edfeaba07f Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 16:25:22 -0800 Subject: [PATCH 292/977] Fix readthedocs build --- docs/index.rst | 3 +-- docs/sanic/index.rst | 25 +++++++++++++++++++++++++ environment.yml | 13 ++++++++++++- requirements-rtd.txt | 2 -- 4 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 docs/sanic/index.rst delete mode 100644 requirements-rtd.txt diff --git a/docs/index.rst b/docs/index.rst index 62cad021..44d0299c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,4 @@ -.. include:: ../README.rst +.. include:: sanic/index.rst Guides ====== @@ -22,7 +22,6 @@ Guides sanic/deploying sanic/extensions sanic/contributing - getting_started Module Documentation diff --git a/docs/sanic/index.rst b/docs/sanic/index.rst new file mode 100644 index 00000000..eb9eb286 --- /dev/null +++ b/docs/sanic/index.rst @@ -0,0 +1,25 @@ +Sanic +================================= + +Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article `_. + +On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. + +Sanic is developed `on GitHub `_. Contributions are welcome! + +Sanic aspires to be simple: +------------------- + +.. code:: python + + from sanic import Sanic + from sanic.response import json + + app = Sanic() + + @app.route("/") + async def test(request): + return json({"hello": "world"}) + + if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/environment.yml b/environment.yml index c74a5137..7eee43a5 100644 --- a/environment.yml +++ b/environment.yml @@ -1,7 +1,18 @@ name: py35 dependencies: +- openssl=1.0.2g=0 +- pip=8.1.1=py35_0 +- python=3.5.1=0 +- readline=6.2=2 +- setuptools=20.3=py35_0 +- sqlite=3.9.2=0 +- tk=8.5.18=0 +- wheel=0.29.0=py35_0 +- xz=5.0.5=1 +- zlib=1.2.8=0 - pip: - uvloop>=0.5.3 - httptools>=0.0.9 - ujson>=1.35 - - aiofiles>=0.3.0 \ No newline at end of file + - aiofiles>=0.3.0 + - https://github.com/channelcat/docutils-fork/zipball/master \ No newline at end of file diff --git a/requirements-rtd.txt b/requirements-rtd.txt deleted file mode 100644 index 856b3f4c..00000000 --- a/requirements-rtd.txt +++ /dev/null @@ -1,2 +0,0 @@ -sphinx -sphinx_rtd_theme \ No newline at end of file From c132c4e673acfac18d998dc6e50df9e52e4ab96c Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 16:32:57 -0800 Subject: [PATCH 293/977] fix conflict part 2 --- environment.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 environment.yml diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..7eee43a5 --- /dev/null +++ b/environment.yml @@ -0,0 +1,18 @@ +name: py35 +dependencies: +- openssl=1.0.2g=0 +- pip=8.1.1=py35_0 +- python=3.5.1=0 +- readline=6.2=2 +- setuptools=20.3=py35_0 +- sqlite=3.9.2=0 +- tk=8.5.18=0 +- wheel=0.29.0=py35_0 +- xz=5.0.5=1 +- zlib=1.2.8=0 +- pip: + - uvloop>=0.5.3 + - httptools>=0.0.9 + - ujson>=1.35 + - aiofiles>=0.3.0 + - https://github.com/channelcat/docutils-fork/zipball/master \ No newline at end of file From 0de6bb00637b2a32b72817d030fb735ab7541b16 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 16:40:36 -0800 Subject: [PATCH 294/977] Adding readthedocs file --- readthedocs.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 readthedocs.yml diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 00000000..40c09542 --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,2 @@ +conda: + file: environment.yml \ No newline at end of file From 52e485cce9ca830f442c71a6a5235cde7bb25ae4 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 16:46:16 -0800 Subject: [PATCH 295/977] Fix readthedocs includes --- docs/conf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index c929e9e7..21b9b9cf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,9 +22,7 @@ import sanic # -- General configuration ------------------------------------------------ -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages'] +extensions = [] templates_path = ['_templates'] From 629524af0428f2b4d9397675261bdf6663d59c34 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 17:39:55 -0800 Subject: [PATCH 296/977] Restructured blueprint class Blueprints currently queue functions to be called, which are simple, yet hard to inspect. These changes allow tools to be built that analyze blueprints more easily. --- sanic/blueprints.py | 132 +++++++++++++++++--------------------------- 1 file changed, 52 insertions(+), 80 deletions(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 4527fa67..faa7c541 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -1,59 +1,11 @@ -from collections import defaultdict +from collections import defaultdict, namedtuple -class BlueprintSetup: - """ - Creates a blueprint state like object. - """ - - def __init__(self, blueprint, app, options): - self.app = app - self.blueprint = blueprint - self.options = options - - url_prefix = self.options.get('url_prefix') - if url_prefix is None: - url_prefix = self.blueprint.url_prefix - - #: The prefix that should be used for all URLs defined on the - #: blueprint. - self.url_prefix = url_prefix - - def add_route(self, handler, uri, methods, host=None): - """ - A helper method to register a handler to the application url routes. - """ - if self.url_prefix: - uri = self.url_prefix + uri - - if host is None: - host = self.blueprint.host - - self.app.route(uri=uri, methods=methods, host=host)(handler) - - def add_exception(self, handler, *args, **kwargs): - """ - Registers exceptions to sanic. - """ - self.app.exception(*args, **kwargs)(handler) - - def add_static(self, uri, file_or_directory, *args, **kwargs): - """ - Registers static files to sanic. - """ - if self.url_prefix: - uri = self.url_prefix + uri - - self.app.static(uri, file_or_directory, *args, **kwargs) - - def add_middleware(self, middleware, *args, **kwargs): - """ - Registers middleware to sanic. - """ - if args or kwargs: - self.app.middleware(*args, **kwargs)(middleware) - else: - self.app.middleware(middleware) +FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host']) +FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) +FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) +FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) +FutureStatic = namedtuple('Route', ['uri', 'file_or_directory', 'args', 'kwargs']) class Blueprint: @@ -65,30 +17,47 @@ class Blueprint: """ self.name = name self.url_prefix = url_prefix - self.deferred_functions = [] - self.listeners = defaultdict(list) self.host = host - def record(self, func): - """ - Registers a callback function that is invoked when the blueprint is - registered on the application. - """ - self.deferred_functions.append(func) - - def make_setup_state(self, app, options): - """ - Returns a new BlueprintSetup object - """ - return BlueprintSetup(self, app, options) + self.routes = [] + self.exceptions = [] + self.listeners = defaultdict(list) + self.middlewares = [] + self.statics = [] def register(self, app, options): """ Registers the blueprint to the sanic app. """ - state = self.make_setup_state(app, options) - for deferred in self.deferred_functions: - deferred(state) + + url_prefix = options.get('url_prefix', self.url_prefix) + + # Routes + for future in self.routes: + # Prepend the blueprint URI prefix if available + uri = url_prefix + future.uri if url_prefix else future.uri + app.route( + uri=uri, + methods=future.methods, + host=future.host or self.host + )(future.handler) + + # Middleware + for future in self.middlewares: + if future.args or future.kwargs: + app.middleware(*future.args, **future.kwargs)(future.middleware) + else: + app.middleware(future.middleware) + + # Exceptions + for future in self.exceptions: + app.exception(*future.args, **future.kwargs)(future.handler) + + # Static Files + for future in self.statics: + # Prepend the blueprint URI prefix if available + uri = url_prefix + future.uri if url_prefix else future.uri + app.static(uri, future.file_or_directory, *future.args, **future.kwargs) def route(self, uri, methods=frozenset({'GET'}), host=None): """ @@ -97,7 +66,8 @@ class Blueprint: :param methods: List of acceptable HTTP methods. """ def decorator(handler): - self.record(lambda s: s.add_route(handler, uri, methods, host)) + route = FutureRoute(handler, uri, methods, host) + self.routes.append(route) return handler return decorator @@ -108,7 +78,8 @@ class Blueprint: :param uri: Endpoint at which the route will be accessible. :param methods: List of acceptable HTTP methods. """ - self.record(lambda s: s.add_route(handler, uri, methods, host)) + route = FutureRoute(handler, uri, methods, host) + self.routes.append(route) return handler def listener(self, event): @@ -125,10 +96,10 @@ class Blueprint: """ Creates a blueprint middleware from a decorated function. """ - def register_middleware(middleware): - self.record( - lambda s: s.add_middleware(middleware, *args, **kwargs)) - return middleware + def register_middleware(_middleware): + future_middleware = FutureMiddleware(_middleware, args, kwargs) + self.middlewares.append(future_middleware) + return _middleware # Detect which way this was called, @middleware or @middleware('AT') if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): @@ -143,7 +114,8 @@ class Blueprint: Creates a blueprint exception from a decorated function. """ def decorator(handler): - self.record(lambda s: s.add_exception(handler, *args, **kwargs)) + exception = FutureException(handler, args, kwargs) + self.exceptions.append(exception) return handler return decorator @@ -153,5 +125,5 @@ class Blueprint: :param uri: Endpoint at which the route will be accessible. :param file_or_directory: Static asset. """ - self.record( - lambda s: s.add_static(uri, file_or_directory, *args, **kwargs)) + static = FutureStatic(uri, file_or_directory, args, kwargs) + self.statics.append(static) From 4c80cd185ffe7b8372949010a4327659f5b58bb8 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 17:44:46 -0800 Subject: [PATCH 297/977] Fix flake8 --- sanic/blueprints.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index faa7c541..3755e8b1 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -5,7 +5,8 @@ FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) -FutureStatic = namedtuple('Route', ['uri', 'file_or_directory', 'args', 'kwargs']) +FutureStatic = namedtuple('Route', + ['uri', 'file_or_directory', 'args', 'kwargs']) class Blueprint: @@ -45,7 +46,8 @@ class Blueprint: # Middleware for future in self.middlewares: if future.args or future.kwargs: - app.middleware(*future.args, **future.kwargs)(future.middleware) + app.middleware(*future.args, + **future.kwargs)(future.middleware) else: app.middleware(future.middleware) @@ -57,7 +59,8 @@ class Blueprint: for future in self.statics: # Prepend the blueprint URI prefix if available uri = url_prefix + future.uri if url_prefix else future.uri - app.static(uri, future.file_or_directory, *future.args, **future.kwargs) + app.static(uri, future.file_or_directory, + *future.args, **future.kwargs) def route(self, uri, methods=frozenset({'GET'}), host=None): """ From 0ef39f35aead3474acd78111ca202f5e60dc601d Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 23:20:38 -0800 Subject: [PATCH 298/977] Added route shorthands to blueprints --- sanic/blueprints.py | 22 +++++++++++++ tests/test_blueprints.py | 68 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 3755e8b1..c9a4b8ac 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -130,3 +130,25 @@ class Blueprint: """ static = FutureStatic(uri, file_or_directory, args, kwargs) self.statics.append(static) + + # Shorthand method decorators + def get(self, uri, host=None): + return self.route(uri, methods=["GET"], host=host) + + def post(self, uri, host=None): + return self.route(uri, methods=["POST"], host=host) + + def put(self, uri, host=None): + return self.route(uri, methods=["PUT"], host=host) + + def head(self, uri, host=None): + return self.route(uri, methods=["HEAD"], host=host) + + def options(self, uri, host=None): + return self.route(uri, methods=["OPTIONS"], host=host) + + def patch(self, uri, host=None): + return self.route(uri, methods=["PATCH"], host=host) + + def delete(self, uri, host=None): + return self.route(uri, methods=["DELETE"], host=host) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 75109e2c..d48c9ea9 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -228,3 +228,71 @@ def test_bp_static(): request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 assert response.body == current_file_contents + +def test_bp_shorthand(): + app = Sanic('test_shorhand_routes') + blueprint = Blueprint('test_shorhand_routes') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + app.blueprint(blueprint) + + request, response = sanic_endpoint_test(app, uri='/get', method='get') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/get', method='post') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/put', method='put') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/put', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/post', method='post') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/post', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/head', method='head') + + request, response = sanic_endpoint_test(app, uri='/head', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/options', method='options') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/options', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/patch', method='patch') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/patch', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/delete', method='delete') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/delete', method='get') + assert response.status == 405 From b72d841619843df57df117003f43323f343e5d4d Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 23:21:00 -0800 Subject: [PATCH 299/977] . --- tests/test_blueprints.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index d48c9ea9..aebb7429 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -233,24 +233,31 @@ def test_bp_shorthand(): app = Sanic('test_shorhand_routes') blueprint = Blueprint('test_shorhand_routes') + @blueprint.get('/get') def handler(request): return text('OK') + @blueprint.put('/put') def handler(request): return text('OK') + @blueprint.post('/post') def handler(request): return text('OK') + @blueprint.head('/head') def handler(request): return text('OK') + @blueprint.options('/options') def handler(request): return text('OK') + @blueprint.patch('/patch') def handler(request): return text('OK') + @blueprint.delete('/delete') def handler(request): return text('OK') @@ -275,6 +282,7 @@ def test_bp_shorthand(): assert response.status == 405 request, response = sanic_endpoint_test(app, uri='/head', method='head') + assert response.status == 200 request, response = sanic_endpoint_test(app, uri='/head', method='get') assert response.status == 405 From 41da793b5ac91e0118f359c08ad88beea0da756d Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 28 Jan 2017 15:26:44 -0800 Subject: [PATCH 300/977] 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 82680bf43f4a1e5eb24b2534c29cbe8bf1cd27bb Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Mon, 30 Jan 2017 10:39:02 +0100 Subject: [PATCH 301/977] Fix docs/config.md: the MYAPP_SETTINGS is not exported If we don"t `export` the variable, it's not available in subcommand: MYAPP_SETTINGS=/path/to/config_file; python3 -c "import os; os.environ['MYAPP_SETTINGS']" Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.5/os.py", line 725, in __getitem__ raise KeyError(key) from None KeyError: 'MYAPP_SETTINGS' The ';' is the culprit here. --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 6dd67cf2..f5d56467 100644 --- a/docs/config.md +++ b/docs/config.md @@ -54,7 +54,7 @@ app.config.from_envvar('MYAPP_SETTINGS') Then you can run your application with the `MYAPP_SETTINGS` environment variable set: ``` -$ MYAPP_SETTINGS=/path/to/config_file; python3 myapp.py +$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py INFO: Goin' Fast @ http://0.0.0.0:8000 ``` From 1649f30808fe80a5c596cd233ccecdc1ca935347 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 30 Jan 2017 02:22:12 -0800 Subject: [PATCH 302/977] Updated password --- .travis.yml | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b31c4f3..71796003 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,14 @@ sudo: false language: python python: - - '3.5' - - '3.6' +- '3.5' +- '3.6' install: pip install tox-travis script: tox deploy: provider: pypi user: channelcat password: - secure: jH4+Di2/qcBwWVhI5/3NYd/JuDDgf5/cF85h+oQnAjgwP6me3th9RS0PHL2gjKJrmyRgwrW7a3eSAityo5sQSlBloQCNrtCE30rkDiwtgoIxDW72NR/nE8nUkS9Utgy87eS+3B4NrO7ag4GTqO5ET8SQ4/MCiQwyUQATLXj2s2eTpQvqJeZG6YgoeFAOYvlR580yznXoOwldWlkiymJiWSdR/01lthtWCi40sYC/QoU7psODJ/tPcsqgQtQKyUVsci7mKvp3Y8ImkoO/POM01jYNsS9qLh5pKTNCEYxtyzC77whenCNHn7WReVidd56g1ADosbNo4yY/1D3VAvwjUnkQ0SzdBQfT7IIzccEuC0j1NXKPN97OX0a6XzyUMYJ1XiU3juTJOPxdYBPbsDM3imQiwrOh1faIf0HCgNTN+Lxe5l8obCH7kffNcVUhs2zI0+2t4MS5tjb/OVuYD/TFn+bM33DqzLctTOK/pGn6xefzZcdzb191LPo99Lof+4fo6jNUpb0UmcBu5ZJzxh0lGe8FPIK3UAG/hrYDDgjx8s8RtUJjcEUQz0659XffYx7DLlgHO7cWyfjrHD3yrLzDbYr5mAS4FR+4D917V7UL+on4SsKHN00UuMGPguqSYo/xYyPLnJU5XK0du4MIpsNMB8TtrJOIewOOfD32+AisPQ8= + secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k= on: tags: true diff --git a/setup.py b/setup.py index 60606ad4..e7899e6f 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname( raise RuntimeError('Unable to determine version.') setup( - name='Sanic', + name='sanic', version=version, url='http://github.com/channelcat/sanic/', license='MIT', From 1501c56bbc464edab4f2397d049bb33754d556c0 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 30 Jan 2017 16:42:43 -0800 Subject: [PATCH 303/977] 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 d193a1eb709d7dc562ac4c5ed960391b4dac2310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20Bl=C3=B6m?= Date: Mon, 30 Jan 2017 17:04:51 -0800 Subject: [PATCH 304/977] Added the tests, code formatting changes, and the Range Request feature. --- sanic/exceptions.py | 78 +++++-------------------- sanic/handlers.py | 128 +++++++++++++++++++++++++++++++++++++++++ sanic/response.py | 59 ++++++++++--------- sanic/sanic.py | 38 ++++++------ sanic/static.py | 51 ++++++++++++---- tests/test_requests.py | 16 ++++-- tests/test_static.py | 91 +++++++++++++++++++++++++++++ 7 files changed, 335 insertions(+), 126 deletions(-) create mode 100644 sanic/handlers.py diff --git a/sanic/exceptions.py b/sanic/exceptions.py index d986cd08..370882be 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -1,8 +1,3 @@ -from .response import text, html -from .log import log -from traceback import format_exc, extract_tb -import sys - TRACEBACK_STYLE = '''