From c657c531b482039c18bcf926052fcc1f6c8377d0 Mon Sep 17 00:00:00 2001 From: 38elements Date: Fri, 23 Dec 2016 00:13:38 +0900 Subject: [PATCH 01/97] 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 39211f8fbddb997f001f8ffc2059a0a0ad729125 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 24 Dec 2016 11:40:07 +0900 Subject: [PATCH 02/97] 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 2d05243c4a2a6d10a868cacddc103bbd25921d0b Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 24 Dec 2016 22:49:48 +0900 Subject: [PATCH 03/97] 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 7e6c92dc52525cdf30babc9f40125cddafab8b43 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 13 Dec 2016 21:24:26 -0800 Subject: [PATCH 04/97] 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 05/97] 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 06/97] 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 07/97] 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 08/97] 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 09/97] 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 10/97] 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 11/97] 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 12/97] 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 13/97] 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 01b42fb39946d6ad93ce622fba14ef81a5d9492d Mon Sep 17 00:00:00 2001 From: Hyunjun Kim Date: Mon, 26 Dec 2016 20:37:16 +0900 Subject: [PATCH 14/97] 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 15/97] 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 16/97] 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 17/97] 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 18/97] 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 19/97] 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 20/97] 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 21/97] 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 22/97] 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 23/97] 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 24/97] 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 0f6ed642daa5294e40bda11024c7c48950f5c4c8 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 30 Dec 2016 07:36:57 -0200 Subject: [PATCH 25/97] 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 26/97] 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 27/97] 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 28/97] 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 29/97] 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 30/97] 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 31/97] 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 32/97] 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 33/97] 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 34/97] 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 35/97] 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 36/97] 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 37/97] 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 38/97] 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 39/97] 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 40/97] 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 41/97] 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 42/97] 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 43/97] 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 44/97] 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 45/97] 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 46/97] 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 47/97] 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 48/97] 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 49/97] 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 50/97] 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 51/97] 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 52/97] 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 53/97] 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 54/97] 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 55/97] 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 56/97] 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 57/97] 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 58/97] 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 59/97] 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 60/97] 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 61/97] 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 62/97] 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 63/97] 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 64/97] 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 65/97] 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 66/97] 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 67/97] 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 68/97] 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 69/97] 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 70/97] 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 71/97] 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 72/97] 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 73/97] 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 74/97] 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 75/97] 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 76/97] 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 77/97] 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 78/97] 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 79/97] 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 80/97] 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 81/97] 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 82/97] 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 83/97] 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 84/97] 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 85/97] 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 86/97] 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 87/97] 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 88/97] 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 89/97] 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 90/97] 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 91/97] 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 92/97] 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 93/97] 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 94/97] 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 95/97] 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 96/97] 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 97/97] 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