Compare commits

...

63 Commits

Author SHA1 Message Date
Ashley Sommer
7c04c9a227 Merge pull request #1848 from ashleysommer/fix_named_response_middleware
Reverse named_response_middlware execution order, to match normal response middleware execution order.
2020-05-14 20:45:29 +10:00
Ashley Sommer
44973125c1 Reverse named_response_middlware execution order, to match normal response middleware execution order.
Fixes #1847
Adds a test to ensure fix is correct
Adds an example which demonstrates correct blueprint-middlware execution order behavior.
2020-05-14 09:54:47 +10:00
7
e7001b0074 release 20.3.0 (#1844) 2020-05-12 16:58:42 -07:00
WH-2099
ae1874ce34 Delete unnecessary isolated blanks and letters. (#1838) 2020-04-30 10:07:06 -07:00
L. Kärkkäinen
8abba597a8 Do not set content-type and content-length headers in exceptions. (#1820)
Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-04-25 20:18:59 -07:00
Prasanna Walimbe
9987893963 Update docs for order of listeners #1805 (#1834) 2020-04-25 17:03:23 -07:00
Jacob
638322d905 docs: Fix doc build (#1833)
* docs: Fix doc build

* docs: Use python-3.8 instead

* test: Remove pytest-asyncio form tox.ini
2020-04-24 14:13:35 -07:00
wangqr
ae40f960ff Import ASGIDispatch from top-level httpx (#1806)
As importing from submodules of httpx is deprecated and removed in 0.12.0
2020-04-10 12:03:51 -07:00
koug44
d969fdc19f [Doc] Update getting_started.rst (#1814)
* Update getting_started.rst

Replacing command to install Sanic without uvloop as the provided one is not working (at least in my case)

* Same thing as oneliner

* Update getting_started.rst

Dummy commit for Travis
2020-04-09 10:07:07 -07:00
L. Kärkkäinen
710024125e Remove server config args that can be read directly from app. (#1807)
* Remove server config args that can be read directly from app.

* Linter

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-04-08 22:10:58 -07:00
Mykhailo Yusko
9a39aff803 Replaced str.format() method in core functionality (#1819)
* Replaced str.format() method in core functionality

* Fixed linter checks
2020-04-06 12:45:25 -07:00
L. Kärkkäinen
78e912ea45 Update docs with changes done in 20.3 (#1822)
* Remove raw_args from docs (deprecated feature removed in Sanic 20.3).

* Add missing Sanic(name) arguments in docs. Merge async/non-async class view examples.

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-03-31 10:57:09 -07:00
L. Kärkkäinen
aa6ea5b5a0 Updated deployment docs (#1821)
* Updated deployment docs

* Wording and formatting.

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-03-28 11:43:42 -07:00
L. Kärkkäinen
48800e657f Deprecation and test cleanup (#1818)
* Remove remove_route, deprecated in 19.6.

* No need for py35 compat anymore.

* Rewrite asyncio.coroutines with async/await.

* Remove deprecated request.raw_args.

* response.text() takes str only: avoid deprecation warning in all but one test.

* Remove unused import.

* Revert unnecessary deprecation warning.

* Remove apparently unnecessary py38 compat.

* Avoid asyncio.Task.all_tasks deprecation warning.

* Avoid warning on a test that tests deprecated response.text(int).

* Add pytest-asyncio to tox deps.

* Run the coroutine returned by AsyncioServer.close.

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-03-28 11:43:14 -07:00
L. Kärkkäinen
120f0262f7 Fix Ctrl+C and tests on Windows. (#1808)
* Fix Ctrl+C on Windows.

* Disable testing of a function N/A on Windows.

* Add test for coverage, avoid crash on missing _stopping.

* Initialise StreamingHTTPResponse.protocol = None

* Improved comments.

* Reduce amount of data in test_request_stream to avoid failures on Windows.

* The Windows test doesn't work on Windows :(

* Use port numbers more likely to be free than 8000.

* Disable the other signal tests on Windows as well.

* Windows doesn't properly support SO_REUSEADDR, so that's disabled in Python, and thus rebinding fails. For successful testing, reuse port instead.

* app.run argument handling: added server kwargs (alike create_server), added warning on extra kwargs, made auto_reload explicit argument. Another go at Windows tests

* Revert "app.run argument handling: added server kwargs (alike create_server), added warning on extra kwargs, made auto_reload explicit argument. Another go at Windows tests"

This reverts commit dc5d682448.

* Use random test server port on most tests. Should avoid port/addr reuse issues.

* Another test to random port instead of 8000.

* Fix deprecation warnings about missing name on Sanic() in tests.

* Linter and typing

* Increase test coverage

* Rewrite test for ctrlc_windows_workaround

* py36 compat

* py36 compat

* py36 compat

* Don't rely on loop internals but add a stopping flag to app.

* App may be restarted.

* py36 compat

* Linter

* Add a constant for OS checking.

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-03-25 21:42:46 -07:00
L. Kärkkäinen
4db075ffc1 Streaming migration for 20.3 release (#1800)
* Compatibility and deprecations for Sanic 20.3 in preparation of the streaming branch.

* Add test for new API.

* isort tests

* More coverage

* json takes str, not bytes

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-03-24 10:11:09 -07:00
Kevin Guillaumond
60b4efad67 Update config docs to match DEFAULT_CONFIG (#1803)
* Set REAL_IP_HEADER's default value to "X-Real-IP"

* Update config instead
2020-03-14 08:57:39 -07:00
L. Kärkkäinen
319388d78b Remove the old request context API deprecated in 19.9. Use request.ctx instead. (#1801)
Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-03-05 21:40:46 -08:00
Subham Roy
ce71514d71 bump httpx dependency version to 0.11.1 (#1794) 2020-03-01 11:42:11 -08:00
L. Kärkkäinen
7833d70d9e Allow multiple workers on MacOS with Python 3.8. Fallback to single worker on Windows until pickling can be fixed. (#1798) 2020-03-01 11:41:09 -08:00
Mykhailo Yusko
16961fab9d Use f-strings instead of str.format() (#1793) 2020-02-25 14:01:13 -06:00
L. Kärkkäinen
861e87347a Fix #1788 incorrect url_for for routes with hosts, added tests. (#1789)
* Fix #1788 incorrect url_for for routes with hosts, added tests.

* Linter

* Remove debug print
2020-02-21 09:10:22 -08:00
Tim Gates
91f6abaa81 Fix simple typo: viewes -> views (#1783)
Closes #1782
2020-02-17 10:16:58 -06:00
Eli Uriegas
d380b52f9a Merge pull request #1784 from gdub/changelog_correction
Corrected changelog for docs move of MD to RST (#1691).
2020-02-15 17:09:41 -08:00
Gary Wilson Jr
d656a06a19 Corrected changelog for docs move of MD to RST (#1691). 2020-02-11 11:45:56 -06:00
Adam Hopkins
258dbee3b9 Py38 tox env (#1752)
* Set version

Set version

* Add Python 3.8 to tests and package classifiers

Add Python3.8 to Appveyor config
2020-02-05 13:17:55 -06:00
Sudeep Mandal
6b9287b076 Update README re: experimental support for Windows (#1778)
As mentioned in #1517 , Windows support is "experimental" and does not currently support multiple workers.
2020-02-03 10:27:56 -06:00
L. Kärkkäinen
dac0514441 Code cleanup in file responses (#1769)
* Code cleanup in file responses.

* Lint
2020-01-26 22:08:34 -08:00
L. Kärkkäinen
bffdb3b5c2 More robust response datatype handling (#1674)
* HTTP1 header formatting moved to headers.format_headers and rewritten.

- New implementation is one line of code and twice faster than the old one.
- Whole header block encoded to UTF-8 in one pass.
- No longer supports custom encode method on header values.
- Cookie objects now have __str__ in addition to encode, to work with this.

* Linter

* format_http1_response

* Replace encode_body with faster implementation based on f-string.

Benchmarks:

def encode_body(data):
    try:
        # Try to encode it regularly
        return data.encode()
    except AttributeError:
        # Convert it to a str if you can't
        return str(data).encode()

def encode_body2(data):
    return f"{data}".encode()

def encode_body3(data):
    return str(data).encode()

data_str, data_int = "foo", 123

%timeit encode_body(data_int)
928 ns ± 2.96 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit encode_body2(data_int)
280 ns ± 2.09 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit encode_body3(data_int)
387 ns ± 1.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit encode_body(data_str)
202 ns ± 1.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit encode_body2(data_str)
197 ns ± 0.507 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

%timeit encode_body3(data_str)
313 ns ± 1.28 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

* Wtf linter

* Content-type fixes.

* Body encoding sanitation, first pass.
- body/data type autodetection fixed.
- do not repr(body).encode() bytes-ish values.
- support __html__ and _repr_html_ in sanic.response.html().

* <any type>-to-str response autoconversion limited to sanic.response.text() only.

* Workaround MyPy issue.

* Add an empty line to make isort happy.

* Add html test for __html__ and _repr_html_.

* Remove StreamingHTTPResponse.get_headers helper function.

* Add back HTTPResponse Keep-Alive removed by earlier merge or something.

* Revert "Remove StreamingHTTPResponse.get_headers helper function."

Tests depend on this otherwise useless function.

This reverts commit 9651e6ae01.

* Add deprecation warnings; instead of assert for wrong HTTP version, and for non-string response.text.

* Add back missing import.

* Avoid duplicate response header tweaking code.

* Linter errors
2020-01-20 10:34:32 -06:00
L. Kärkkäinen
e908ca8cef [Trio] Quick fixes to make Sanic usable on hypercorn -k trio myweb.app (#1767)
* Quick fixes to make Sanic usable on hypercorn -k trio myweb.app

* Quick'n dirty compatibility and autodetection of hypercorn trio mode.

* mypy ignore for aiofiles/trio.

* lint
2020-01-20 10:29:06 -06:00
Ashley Sommer
801595e24a Add server.start_serving and server.serve_forever to AsyncioServer proxy object, to match asyncio-python3.7 example doc, fixes #1754 (#1762) 2020-01-20 09:00:48 -06:00
L. Kärkkäinen
ba9b432993 No tracebacks on normal errors and prettier error pages (#1768)
* Default error handler now only logs traceback on 500 errors and all responses are HTML formatted.

* Tests passing.

* Ability to flag any exception object with self.quiet = True following @ashleysommer suggestion.

* Refactor HTML formatting into errorpages.py. String escapes for debug tracebacks.

* Remove extra includes

* Auto-set quiet flag also when decorator is used.

* Cleanup, make error pages (probably) HTML5-compliant and similar for debug and non-debug modes.

* Fix lookup of non-existant status codes

* No logging of 503 errors after all.

* lint
2020-01-20 08:58:14 -06:00
Ashley Sommer
b565072ed9 Allow route decorators to stack up again (#1764)
* Allow route decorators to stack up without causing a function signature inspection crash
Fix #1742

* Apply fix to @websocket routes docorator too
Add test for double-stacked websocket decorator
remove introduction of new variable in route wrapper, extend routes in-place.
Add explanation of why a handler will be a tuple in the case of a double-stacked route decorator
2020-01-10 21:50:16 -08:00
Ashley Sommer
caa1b4d69b Fix dangling comma in arguments list for HTTPResponse in response.empty() (#1761)
* Fix dangling comma arguments list for HTTPResponse in response.empty()

* Found another black error, including another dangling comma
2020-01-10 19:58:01 -08:00
Liran Nuna
865536c5c4 Simplify status code to text lookup (#1738) 2020-01-10 08:43:44 -06:00
Eli Uriegas
784d5cce52 Merge pull request #1755 from Lagicrus/empty-response
Update docs
2020-01-04 19:15:24 -08:00
Lagicrus
0fd08c6114 Update response.rst 2020-01-04 21:26:03 +00:00
Lagicrus
cd779b6e4f Update response.rst 2020-01-04 19:51:50 +00:00
好风
3430907046 fix 1748 : Drop DeprecationWarning in python 3.8 (#1750)
https://github.com/huge-success/sanic/issues/1748
2020-01-03 20:20:42 -08:00
Eli Uriegas
2f776eba85 Release v19.12.0 (#1747)
Release v19.12.0
2020-01-03 11:50:33 -08:00
Adam Hopkins
b9cd2ed1f1 Merge pull request #1751 from huge-success/master
Move Release into LTS Branch
2020-01-02 23:45:08 +02:00
Adam Hopkins
850b63f642 Merge pull request #1743 from eric-nieuwland/master
Forgotten slot
2020-01-02 23:17:35 +02:00
Eric Nieuwland
a9c669f17b Forgotten slot
Crashes the server at __init__() time
2019-12-28 15:21:27 +01:00
Stephen Sadowski
075affec23 Release v19.12.0 (#1740)
* Bumping up version from 19.9.0 to 19.12.0

* Pipfile crud removed
2019-12-27 07:10:46 -06:00
Stephen Sadowski
3411a12c40 Pipfile crud removed 2019-12-26 18:50:52 -06:00
Stephen Sadowski
28899356c8 Bumping up version from 19.12.0 to 19.12.0 2019-12-26 18:47:56 -06:00
Eli Uriegas
2b5f8d20de ci: Add python nightlies to test matrix (#1710)
Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
2019-12-25 16:50:31 -08:00
Adam Hopkins
243f240e5f Add RFC labels to stale exclusion list (#1737) 2019-12-23 17:31:33 -06:00
L. Kärkkäinen
0a25868a86 HTTP1 header formatting moved to headers.format_headers and rewritten. (#1669)
* HTTP1 header formatting moved to headers.format_headers and rewritten.

- New implementation is one line of code and twice faster than the old one.
- Whole header block encoded to UTF-8 in one pass.
- No longer supports custom encode method on header values.
- Cookie objects now have __str__ in addition to encode, to work with this.

* Add an import missed in merge.
2019-12-23 17:30:45 -06:00
Liran Nuna
fccbc1adc4 Allow empty body without Content-Type; Introduce response.empty() (#1736) 2019-12-23 14:16:53 -06:00
Adam Hopkins
3f6a978328 Swap out requests-async for httpx (#1728)
* Begin swap of requests-async for httpx

* Finalize httpx adoption and resolve tests

Resolve linting and formatting

* Remove documentation references to requests-async in favor of httpx
2019-12-20 19:23:52 -08:00
Harsha Narayana
a6077a1790 GIT-37: fix blueprint middleware application (#1690)
* GIT-37: fix blueprint middleware application

1. If you register a middleware via `@blueprint.middleware` then it will apply only to the routes defined by the blueprint.
2. If you register a middleware via `@blueprint_group.middleware` then it will apply to all blueprint based routes that are part of the group.
3. If you define a middleware via `@app.middleware` then it will be applied on all available routes

Fixes #37

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>

* GIT-37: add changelog

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-12-20 10:01:04 -06:00
Eli Uriegas
179a07942e Merge pull request #1734 from seemethere/testing_host
testing: Add host argument to SanicTestClient
2019-12-18 16:50:59 -08:00
Eli Uriegas
c3aed01096 testing: Add host argument to SanicTestClient
Adds the ability to specify a host argument when using the
SanicTestClient.

Signed-off-by: Eli Uriegas <eliuriegas@fb.com>
2019-12-18 16:31:38 -08:00
7
028778ed56 Fix #1714 (#1716)
* fix abort call errors out when calling inside stream handler

* handle pending task properly after cleanup
2019-12-16 09:46:18 -06:00
Adam Bannister
2d72874b0b Add return type to Sanic.create_server for type hinting and docs (#1724)
* add type hint and doc when create_server returns AsyncioServer

* fix linting
2019-12-12 10:25:13 -06:00
Seonghyeon Kim
4c45d30400 FIX: invalid rst syntax (#1727) 2019-12-12 10:24:11 -06:00
Junyeong Jeong
ecbe5c839f pass request_buffer_queue_size argument to HttpProtocol (#1717)
* pass request_buffer_queue_size argument to HttpProtocol

* fix to use simultaneously only one task to put body to stream buffer

* add a test code for REQUEST_BUFFER_QUEUE_SIZE
2019-11-21 09:33:50 -06:00
Vinícius Dantas
ed1f367a8a Reduce nesting for the sample authentication decorator (#1715)
* Reduce nesting for the sample authentication decorator

* Add missing decorator argument
2019-11-14 14:57:41 -06:00
Lagicrus
a4185a0ba7 Doc rework (#1698)
* blueprints

* class_based_views

* config

* decorators

* deploying

* exceptions

* extensions

* getting_started

* middleware

* request_data

* response

* routing

* static_files

* streaming

* testing

* versioning

* Fix bug and links

* spelling mistakes

* Bug fixes and minor tweaks

* Create 1691.doc.rst

* Bug fixes and tweaks

Co-Authored-By: Harsha Narayana <harsha2k4@gmail.com>
2019-11-14 14:11:38 -06:00
Harsha Narayana
e81a8ce073 fix SERVER_NAME enforcement in url_for and request.args documentation (#1708)
* 🐛 fix SERVER_NAME enforcement in url_for

fixes #1707

* 💡 add additional documentation for using request.args

fixes #1704

*  add additional test to check url_for without SERVER_NAME

* 📝 add changelog for fixes
2019-11-01 10:32:49 -07:00
Harsha Narayana
e506c89304 deprecate None value support for app name (#1705)
*  deprecate None value support for app name

* 🚨 cleanup linter issues across the codebase
2019-10-23 09:12:20 -07:00
Bruno Oliveira
fcdc9c83c5 Add 'python_requires' key to setup.py (#1701)
This key is important so that `pip` doesn't try to install `sanic` in unsupported Python versions:

https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
2019-10-14 21:17:05 -07:00
112 changed files with 4718 additions and 3762 deletions

View File

@@ -12,6 +12,11 @@ environment:
PYTHON_VERSION: "3.7.x"
PYTHON_ARCH: "64"
- TOXENV: py38-no-ext
PYTHON: "C:\\Python38-x64"
PYTHON_VERSION: "3.8.x"
PYTHON_ARCH: "64"
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
install:

1
.github/stale.yml vendored
View File

@@ -8,6 +8,7 @@ exemptLabels:
- urgent
- necessary
- help wanted
- RFC
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable

View File

@@ -21,28 +21,64 @@ matrix:
dist: xenial
sudo: true
name: "Python 3.7 without Extensions"
- env: TOX_ENV=py38
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 with Extensions"
- env: TOX_ENV=py38-no-ext
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 without Extensions"
- env: TOX_ENV=type-checking
python: 3.6
name: "Python 3.6 Type checks"
- env: TOX_ENV=type-checking
python: 3.7
name: "Python 3.7 Type checks"
- env: TOX_ENV=type-checking
python: 3.8
name: "Python 3.8 Type checks"
- env: TOX_ENV=lint
python: 3.6
name: "Python 3.6 Linter checks"
- env: TOX_ENV=check
python: 3.6
name: "Python 3.6 Package checks"
- env: TOX_ENV=security
python: 3.6
dist: xenial
sudo: true
name: "Python 3.6 Bandit security scan"
- env: TOX_ENV=security
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Bandit security scan"
- env: TOX_ENV=security
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 Bandit security scan"
- env: TOX_ENV=docs
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Documentation tests"
- env: TOX_ENV=pyNightly
python: 'nightly'
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: 'nightly'
name: "Python nightly Extensions"
allow_failures:
- env: TOX_ENV=pyNightly
python: 'nightly'
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: 'nightly'
name: "Python nightly Extensions"
install:
- pip install -U tox
- pip install codecov

View File

@@ -1,3 +1,38 @@
Version 19.12.0
===============
Bugfixes
********
- Fix blueprint middleware application
Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was
being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike.
As part of this change, the blueprint based middleware application is enforced based on where they are
registered.
- If you register a middleware via :code:`@blueprint.middleware` then it will apply only to the routes defined by the blueprint.
- If you register a middleware via :code:`@blueprint_group.middleware` then it will apply to all blueprint based routes that are part of the group.
- If you define a middleware via :code:`@app.middleware` then it will be applied on all available routes (`#37 <https://github.com/huge-success/sanic/issues/37>`__)
- Fix `url_for` behavior with missing SERVER_NAME
If the `SERVER_NAME` was missing in the `app.config` entity, the `url_for` on the `request` and `app` were failing
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/huge-success/sanic/issues/1707>`__)
Improved Documentation
**********************
- Move docs from MD to RST
Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in
the future to update documentation. (`#1691 <https://github.com/huge-success/sanic/issues/1691>`__)
- Fix documentation for `get` and `getlist` of the `request.args`
Add additional example for showing the usage of `getlist` and fix the documentation string for `request.args` behavior (`#1704 <https://github.com/huge-success/sanic/issues/1704>`__)
Version 19.6.3
==============

View File

@@ -68,7 +68,7 @@ run all unittests, perform lint and other checks.
Run unittests
-------------
``tox`` environment -> ``[testenv]`
``tox`` environment -> ``[testenv]``
To execute only unittests, run ``tox`` with environment like so:

View File

@@ -83,6 +83,9 @@ Installation
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
use ``sanic`` with ``ujson`` dependency.
.. note::
Windows support is currently "experimental" and on a best-effort basis. Multiple workers are also not currently supported on Windows (see `Issue #1517 <https://github.com/huge-success/sanic/issues/1517>`_), but setting ``workers=1`` should launch the server successfully.
Hello World Example
-------------------

468
docs/index.html Normal file
View File

@@ -0,0 +1,468 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.2: http://docutils.sourceforge.net/" />
<title>index.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<div class="section" id="sanic">
<h1>Sanic</h1>
<p>Sanic is a Python 3.6+ web server and web framework that's written to go fast. It allows the usage of the async/await syntax added in Python 3.5, which makes your code non-blocking and speedy.</p>
<p>The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.</p>
<p>Sanic is developed <a class="reference external" href="https://github.com/channelcat/sanic/">on GitHub</a>. Contributions are welcome!</p>
<div class="section" id="sanic-aspires-to-be-simple">
<h2>Sanic aspires to be simple</h2>
<pre class="code python literal-block">
<span class="keyword namespace">from</span> <span class="name namespace">sanic</span> <span class="keyword namespace">import</span> <span class="name">Sanic</span>
<span class="keyword namespace">from</span> <span class="name namespace">sanic.response</span> <span class="keyword namespace">import</span> <span class="name">json</span>
<span class="name">app</span> <span class="operator">=</span> <span class="name">Sanic</span><span class="punctuation">()</span>
<span class="name decorator">&#64;app.route</span><span class="punctuation">(</span><span class="literal string double">&quot;/&quot;</span><span class="punctuation">)</span>
<span class="name">async</span> <span class="keyword">def</span> <span class="name function">test</span><span class="punctuation">(</span><span class="name">request</span><span class="punctuation">):</span>
<span class="keyword">return</span> <span class="name">json</span><span class="punctuation">({</span><span class="literal string double">&quot;hello&quot;</span><span class="punctuation">:</span> <span class="literal string double">&quot;world&quot;</span><span class="punctuation">})</span>
<span class="keyword">if</span> <span class="name variable magic">__name__</span> <span class="operator">==</span> <span class="literal string double">&quot;__main__&quot;</span><span class="punctuation">:</span>
<span class="name">app</span><span class="operator">.</span><span class="name">run</span><span class="punctuation">(</span><span class="name">host</span><span class="operator">=</span><span class="literal string double">&quot;0.0.0.0&quot;</span><span class="punctuation">,</span> <span class="name">port</span><span class="operator">=</span><span class="literal number integer">8000</span><span class="punctuation">)</span>
</pre>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru
December 2020. Official Python support for version 3.5 is set to expire in September 2020.</p>
</div>
</div>
</div>
<div class="section" id="guides">
<h1>Guides</h1>
<div class="system-message">
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 6)</p>
<p>Unknown directive type &quot;toctree&quot;.</p>
<pre class="literal-block">
.. toctree::
:maxdepth: 2
sanic/getting_started
sanic/config
sanic/logging
sanic/request_data
sanic/response
sanic/cookies
sanic/routing
sanic/blueprints
sanic/static_files
sanic/versioning
sanic/exceptions
sanic/middleware
sanic/websocket
sanic/decorators
sanic/streaming
sanic/class_based_views
sanic/custom_protocol
sanic/sockets
sanic/ssl
sanic/debug_mode
sanic/testing
sanic/deploying
sanic/extensions
sanic/examples
sanic/changelog
sanic/contributing
sanic/api_reference
sanic/asyncio_python37
</pre>
</div>
</div>
<div class="section" id="module-documentation">
<h1>Module Documentation</h1>
<div class="system-message">
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 42)</p>
<p>Unknown directive type &quot;toctree&quot;.</p>
<pre class="literal-block">
.. toctree::
</pre>
</div>
<ul>
<li><p class="first"><a href="#id1"><span class="problematic" id="id2">:ref:`genindex`</span></a></p>
<div class="system-message" id="id1">
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 44); <em><a href="#id2">backlink</a></em></p>
<p>Unknown interpreted text role &quot;ref&quot;.</p>
</div>
</li>
<li><p class="first"><a href="#id3"><span class="problematic" id="id4">:ref:`modindex`</span></a></p>
<div class="system-message" id="id3">
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 45); <em><a href="#id4">backlink</a></em></p>
<p>Unknown interpreted text role &quot;ref&quot;.</p>
</div>
</li>
<li><p class="first"><a href="#id5"><span class="problematic" id="id6">:ref:`search`</span></a></p>
<div class="system-message" id="id5">
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 46); <em><a href="#id6">backlink</a></em></p>
<p>Unknown interpreted text role &quot;ref&quot;.</p>
</div>
</li>
</ul>
</div>
</div>
</body>
</html>

View File

@@ -28,6 +28,7 @@ Guides
sanic/debug_mode
sanic/testing
sanic/deploying
sanic/nginx
sanic/extensions
sanic/examples
sanic/changelog

View File

@@ -1,286 +0,0 @@
# Blueprints
Blueprints are objects that can be used for sub-routing within an application.
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.
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`, which can be imported into your
main application later.
```python
from sanic.response import json
from sanic import Blueprint
bp = Blueprint('my_blueprint')
@bp.route('/')
async def bp_root(request):
return json({'my': 'blueprint'})
```
## Registering blueprints
Blueprints must be registered with the application.
```python
from sanic import Sanic
from my_blueprint import bp
app = Sanic(__name__)
app.blueprint(bp)
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:
```python
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
```
## Blueprint groups and nesting
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
```
api/
├──content/
│ ├──authors.py
│ ├──static.py
│ └──__init__.py
├──info.py
└──__init__.py
app.py
```
Initialization of this app's blueprint hierarchy could go as follows:
```python
# api/content/authors.py
from sanic import Blueprint
authors = Blueprint('content_authors', url_prefix='/authors')
```
```python
# api/content/static.py
from sanic import Blueprint
static = Blueprint('content_static', url_prefix='/static')
```
```python
# api/content/__init__.py
from sanic import Blueprint
from .static import static
from .authors import authors
content = Blueprint.group(static, authors, url_prefix='/content')
```
```python
# api/info.py
from sanic import Blueprint
info = Blueprint('info', url_prefix='/info')
```
```python
# api/__init__.py
from sanic import Blueprint
from .content import content
from .info import info
api = Blueprint.group(content, info, url_prefix='/api')
```
And registering these blueprints in `app.py` can now be done like so:
```python
# app.py
from sanic import Sanic
from .api import api
app = Sanic(__name__)
app.blueprint(api)
```
## Using Blueprints
Blueprints have almost the same functionality as an application instance.
### WebSocket routes
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
decorator or `bp.add_websocket_route` method.
### Blueprint Middleware
Using blueprints allows you to also register middleware globally.
```python
@bp.middleware
async def print_on_request(request):
print("I am a spy")
@bp.middleware('request')
async def halt_request(request):
return text('I halted the request')
@bp.middleware('response')
async def halt_response(request, response):
return text('I halted the response')
```
### Blueprint Group Middleware
Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the
current blueprint group under consideration.
```python
bp1 = Blueprint('bp1', url_prefix='/bp1')
bp2 = Blueprint('bp2', url_prefix='/bp2')
@bp1.middleware('request')
async def bp1_only_middleware(request):
print('applied on Blueprint : bp1 Only')
@bp1.route('/')
async def bp1_route(request):
return text('bp1')
@bp2.route('/<param>')
async def bp2_route(request, param):
return text(param)
group = Blueprint.group(bp1, bp2)
@group.middleware('request')
async def group_middleware(request):
print('common middleware applied for both bp1 and bp2')
# Register Blueprint group under the app
app.blueprint(group)
```
### 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 be served globally, under the blueprint prefix.
```python
# suppose bp.name == 'bp'
bp.static('/web/path', '/folder/to/serve')
# also you can pass name parameter to it for url_for
bp.static('/web/path', '/folder/to/server', name='uploads')
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
```
## 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
```python
bp = Blueprint('my_blueprint')
@bp.listener('before_server_start')
async def setup_connection(app, loop):
global database
database = mysql.connect(host='127.0.0.1'...)
@bp.listener('after_server_stop')
async def close_connection(app, loop):
await database.close()
```
## Use-case: API versioning
Blueprints can be very useful for API versioning, where one blueprint may point
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
When a blueprint is initialised, it can take an optional `version` 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', url_prefix='/api', version="v1")
blueprint_v2 = Blueprint('v2', url_prefix='/api', version="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/api` and `/v2/api` 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)
```
## URL Building with `url_for`
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
takes the format `<blueprint_name>.<handler_name>`. For example:
```python
@blueprint_v1.route('/')
async def root(request):
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/api/post/5'
return redirect(url)
@blueprint_v1.route('/post/<post_id>')
async def post_handler(request, post_id):
return text('Post {} in Blueprint V1'.format(post_id))
```

301
docs/sanic/blueprints.rst Normal file
View File

@@ -0,0 +1,301 @@
Blueprints
==========
Blueprints are objects that can be used for sub-routing within an application.
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.
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`, which can be imported into your
main application later.
.. code-block:: python
from sanic.response import json
from sanic import Blueprint
bp = Blueprint('my_blueprint')
@bp.route('/')
async def bp_root(request):
return json({'my': 'blueprint'})
Registering blueprints
----------------------
Blueprints must be registered with the application.
.. code-block:: python
from sanic import Sanic
from my_blueprint import bp
app = Sanic(__name__)
app.blueprint(bp)
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:
.. code-block:: python
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
Blueprint groups and nesting
----------------------------
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
| api/
| ├──content/
| │ ├──authors.py
| │ ├──static.py
| │ └──__init__.py
| ├──info.py
| └──__init__.py
| app.py
Initialization of this app's blueprint hierarchy could go as follows:
.. code-block:: python
# api/content/authors.py
from sanic import Blueprint
authors = Blueprint('content_authors', url_prefix='/authors')
.. code-block:: python
# api/content/static.py
from sanic import Blueprint
static = Blueprint('content_static', url_prefix='/static')
.. code-block:: python
# api/content/__init__.py
from sanic import Blueprint
from .static import static
from .authors import authors
content = Blueprint.group(static, authors, url_prefix='/content')
.. code-block:: python
# api/info.py
from sanic import Blueprint
info = Blueprint('info', url_prefix='/info')
.. code-block:: python
# api/__init__.py
from sanic import Blueprint
from .content import content
from .info import info
api = Blueprint.group(content, info, url_prefix='/api')
And registering these blueprints in `app.py` can now be done like so:
.. code-block:: python
# app.py
from sanic import Sanic
from .api import api
app = Sanic(__name__)
app.blueprint(api)
Using Blueprints
----------------
Blueprints have almost the same functionality as an application instance.
WebSocket routes
~~~~~~~~~~~~~~~~
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
decorator or `bp.add_websocket_route` method.
Blueprint Middleware
~~~~~~~~~~~~~~~~~~~~
Using blueprints allows you to also register middleware globally.
.. code-block:: python
@bp.middleware
async def print_on_request(request):
print("I am a spy")
@bp.middleware('request')
async def halt_request(request):
return text('I halted the request')
@bp.middleware('response')
async def halt_response(request, response):
return text('I halted the response')
Blueprint Group Middleware
~~~~~~~~~~~~~~~~~~~~~~~~~~
Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the
current blueprint group under consideration.
.. code-block:: python
bp1 = Blueprint('bp1', url_prefix='/bp1')
bp2 = Blueprint('bp2', url_prefix='/bp2')
@bp1.middleware('request')
async def bp1_only_middleware(request):
print('applied on Blueprint : bp1 Only')
@bp1.route('/')
async def bp1_route(request):
return text('bp1')
@bp2.route('/<param>')
async def bp2_route(request, param):
return text(param)
group = Blueprint.group(bp1, bp2)
@group.middleware('request')
async def group_middleware(request):
print('common middleware applied for both bp1 and bp2')
# Register Blueprint group under the app
app.blueprint(group)
Exceptions
~~~~~~~~~~
Exceptions can be applied exclusively to blueprints globally.
.. code-block:: 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 be served globally, under the blueprint prefix.
.. code-block:: python
# suppose bp.name == 'bp'
bp.static('/web/path', '/folder/to/serve')
# also you can pass name parameter to it for url_for
bp.static('/web/path', '/folder/to/server', name='uploads')
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
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
.. code-block:: python
bp = Blueprint('my_blueprint')
@bp.listener('before_server_start')
async def setup_connection(app, loop):
global database
database = mysql.connect(host='127.0.0.1'...)
@bp.listener('after_server_stop')
async def close_connection(app, loop):
await database.close()
Use-case: API versioning
------------------------
Blueprints can be very useful for API versioning, where one blueprint may point
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
When a blueprint is initialised, it can take an optional `version` argument,
which will be prepended to all routes defined on the blueprint. This feature
can be used to implement our API versioning scheme.
.. code-block:: python
# blueprints.py
from sanic.response import text
from sanic import Blueprint
blueprint_v1 = Blueprint('v1', url_prefix='/api', version="v1")
blueprint_v2 = Blueprint('v2', url_prefix='/api', version="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/api` and `/v2/api` will now
point to the individual blueprints, which allows the creation of *sub-sites*
for each API version.
.. code-block:: 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)
URL Building with `url_for`
---------------------------
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
takes the format `<blueprint_name>.<handler_name>`. For example:
.. code-block:: python
@blueprint_v1.route('/')
async def root(request):
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/api/post/5'
return redirect(url)
@blueprint_v1.route('/post/<post_id>')
async def post_handler(request, post_id):
return text('Post {} in Blueprint V1'.format(post_id))

View File

@@ -1,166 +0,0 @@
# Class-Based Views
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.
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
class SimpleView(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(SimpleView.as_view(), '/')
```
You can also use `async` syntax.
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
class SimpleAsyncView(HTTPMethodView):
async def get(self, request):
return text('I am async get method')
app.add_route(SimpleAsyncView.as_view(), '/')
```
## URL parameters
If you need any URL parameters, as discussed in the routing guide, include them
in the method definition.
```python
class NameView(HTTPMethodView):
def get(self, request, name):
return text('Hello {}'.format(name))
app.add_route(NameView.as_view(), '/<name>')
```
## 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.
```python
class ViewWithDecorator(HTTPMethodView):
decorators = [some_decorator_here]
def get(self, request, name):
return text('Hello I have a decorator')
def post(self, request, name):
return text("Hello I also have a decorator")
app.add_route(ViewWithDecorator.as_view(), '/url')
```
But if you just want to decorate some functions and not all functions, you can do as follows:
```python
class ViewWithSomeDecorator(HTTPMethodView):
@staticmethod
@some_decorator_here
def get(request, name):
return text("Hello I have a decorator")
def post(self, request, name):
return text("Hello I don't have any decorators")
```
## URL Building
If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint
that you will pass into `url_for`. For example:
```python
@app.route('/')
def index(request):
url = app.url_for('SpecialClassView')
return redirect(url)
class SpecialClassView(HTTPMethodView):
def get(self, request):
return text('Hello from the Special Class View!')
app.add_route(SpecialClassView.as_view(), '/special_class_view')
```
## Using CompositionView
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
move handler functions outside of the view class.
Handler functions for each supported HTTP method are defined elsewhere in the
source, and then added to the view using the `CompositionView.add` method. The
first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`),
and the second is the handler function. The following example shows
`CompositionView` usage with both an external handler function and an inline
lambda:
```python
from sanic import Sanic
from sanic.views import CompositionView
from sanic.response import text
app = Sanic(__name__)
def get_handler(request):
return text('I am a get method')
view = CompositionView()
view.add(['GET'], get_handler)
view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
# Use the new view to handle requests to the base URL
app.add_route(view, '/')
```
Note: currently you cannot build a URL for a CompositionView using `url_for`.

View File

@@ -0,0 +1,154 @@
Class-Based Views
=================
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.
.. code-block:: python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic("class_views_example")
class SimpleView(HTTPMethodView):
def get(self, request):
return text('I am get method')
# You can also use async syntax
async def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(SimpleView.as_view(), '/')
URL parameters
--------------
If you need any URL parameters, as discussed in the routing guide, include them
in the method definition.
.. code-block:: python
class NameView(HTTPMethodView):
def get(self, request, name):
return text('Hello {}'.format(name))
app.add_route(NameView.as_view(), '/<name>')
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.
.. code-block:: python
class ViewWithDecorator(HTTPMethodView):
decorators = [some_decorator_here]
def get(self, request, name):
return text('Hello I have a decorator')
def post(self, request, name):
return text("Hello I also have a decorator")
app.add_route(ViewWithDecorator.as_view(), '/url')
But if you just want to decorate some functions and not all functions, you can do as follows:
.. code-block:: python
class ViewWithSomeDecorator(HTTPMethodView):
@staticmethod
@some_decorator_here
def get(request, name):
return text("Hello I have a decorator")
def post(self, request, name):
return text("Hello I don't have any decorators")
URL Building
------------
If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint
that you will pass into `url_for`. For example:
.. code-block:: python
@app.route('/')
def index(request):
url = app.url_for('SpecialClassView')
return redirect(url)
class SpecialClassView(HTTPMethodView):
def get(self, request):
return text('Hello from the Special Class View!')
app.add_route(SpecialClassView.as_view(), '/special_class_view')
Using CompositionView
---------------------
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
move handler functions outside of the view class.
Handler functions for each supported HTTP method are defined elsewhere in the
source, and then added to the view using the `CompositionView.add` method. The
first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`),
and the second is the handler function. The following example shows
`CompositionView` usage with both an external handler function and an inline
lambda:
.. code-block:: python
from sanic import Sanic
from sanic.views import CompositionView
from sanic.response import text
app = Sanic("composition_example")
def get_handler(request):
return text('I am a get method')
view = CompositionView()
view.add(['GET'], get_handler)
view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
# Use the new view to handle requests to the base URL
app.add_route(view, '/')
Note: currently you cannot build a URL for a CompositionView using `url_for`.

View File

@@ -1,211 +0,0 @@
# Configuration
Any reasonably complex application will need configuration that is not baked into the actual code. Settings might be different for different environments or installations.
## Basics
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
```
app = Sanic('myapp')
app.config.DB_NAME = 'appdb'
app.config.DB_USER = 'appuser'
```
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
```
db_settings = {
'DB_HOST': 'localhost',
'DB_NAME': 'appdb',
'DB_USER': 'appuser'
}
app.config.update(db_settings)
```
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
## Loading Configuration
There are several ways how to load configuration.
### From Environment Variables
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic:
```python
app = Sanic(load_env='MYAPP_')
```
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
```python
app = Sanic(load_env=False)
```
### From an Object
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
```
import myapp.default_settings
app = Sanic('myapp')
app.config.from_object(myapp.default_settings)
```
or also by path to config:
```
app = Sanic('myapp')
app.config.from_object('config.path.config.Class')
```
You could use a class or any other object as well.
### From a File
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
```
app = Sanic('myapp')
app.config.from_envvar('MYAPP_SETTINGS')
```
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
```
$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py
INFO: Goin' Fast @ http://0.0.0.0:8000
```
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase variables are added to the configuration. Most commonly the configuration consists of simple key value pairs:
```
# config_file
DB_HOST = 'localhost'
DB_NAME = 'appdb'
DB_USER = 'appuser'
```
## Builtin Configuration Values
Out of the box there are just a few predefined values which can be overwritten when creating the application.
| Variable | Default | Description |
| ------------------------- | ----------------- | --------------------------------------------------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
| ACCESS_LOG | True | Disable or enable access log |
| PROXIES_COUNT | -1 | The number of proxy servers in front of the app (e.g. nginx; see below) |
| FORWARDED_FOR_HEADER | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip |
| REAL_IP_HEADER | "X-Real-IP" | The name of "X-Real-IP" HTTP header that contains real client ip |
### The different Timeout variables:
#### `REQUEST_TIMEOUT`
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the
Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the
`REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates an `HTTP 408` response
and sends that to the client. Set this parameter's value higher if your clients routinely pass very large request payloads
or upload requests very slowly.
#### `RESPONSE_TIMEOUT`
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the
Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT`
value (in seconds), this is considered a Server Error so Sanic generates an `HTTP 503` response and sends that to the
client. Set this parameter's value higher if your application is likely to have long-running process that delay the
generation of a response.
#### `KEEP_ALIVE_TIMEOUT`
##### What is Keep Alive? And what does the Keep Alive Timeout value do?
`Keep-Alive` is a HTTP feature introduced in `HTTP 1.1`. When sending a HTTP request, the client (usually a web browser application)
can set a `Keep-Alive` header to indicate the http server (Sanic) to not close the TCP connection after it has send the response.
This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient
network traffic for both the client and the server.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application,
set it to `False` to cause all client connections to close immediately after a response is sent, regardless of
the `Keep-Alive` header on the request.
The amount of time the server holds the TCP connection open is decided by the server itself.
In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds.
This is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for
the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless
you know your clients are using a browser which supports TCP connections held open for that long.
For reference:
```
Apache httpd server default keepalive timeout = 5 seconds
Nginx server default keepalive timeout = 75 seconds
Nginx performance tuning guidelines uses keepalive = 15 seconds
IE (5-9) client hard keepalive limit = 60 seconds
Firefox client hard keepalive limit = 115 seconds
Opera 11 client hard keepalive limit = 120 seconds
Chrome 13+ client keepalive limit > 300+ seconds
```
### Proxy configuration
When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain ip of a proxy, typically `127.0.0.1`. Sanic may be configured to use proxy headers for determining the true client IP, available as `request.remote_addr`. The full external URL is also constructed from header fields if available.
Without proper precautions, a malicious client may use proxy headers to spoof its own IP. To avoid such issues, Sanic does not use any proxy headers unless explicitly enabled.
Services behind reverse proxies must configure `FORWARDED_SECRET`, `REAL_IP_HEADER` and/or `PROXIES_COUNT`.
#### Forwarded header
```
Forwarded: for="1.2.3.4"; proto="https"; host="yoursite.com"; secret="Pr0xy",
for="10.0.0.1"; proto="http"; host="proxy.internal"; by="_1234proxy"
```
* Set `FORWARDED_SECRET` to an identifier used by the proxy of interest.
The secret is used to securely identify a specific proxy server. Given the above header, secret `Pr0xy` would use the information on the first line and secret `_1234proxy` would use the second line. The secret must exactly match the value of `secret` or `by`. A secret in `by` must begin with an underscore and use only characters specified in [RFC 7239 section 6.3](https://tools.ietf.org/html/rfc7239#section-6.3), while `secret` has no such restrictions.
Sanic ignores any elements without the secret key, and will not even parse the header if no secret is set.
All other proxy headers are ignored once a trusted forwarded element is found, as it already carries complete information about the client.
#### Traditional proxy headers
```
X-Real-IP: 1.2.3.4
X-Forwarded-For: 1.2.3.4, 10.0.0.1
X-Forwarded-Proto: https
X-Forwarded-Host: yoursite.com
```
* Set `REAL_IP_HEADER` to `x-real-ip`, `true-client-ip`, `cf-connecting-ip` or other name of such header.
* Set `PROXIES_COUNT` to the number of entries expected in `x-forwarded-for` (name configurable via `FORWARDED_FOR_HEADER`).
If client IP is found by one of these methods, Sanic uses the following headers for URL parts:
* `x-forwarded-proto`, `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-path` and if necessary, `x-scheme`.
#### Proxy config if using ...
* a proxy that supports `forwarded`: set `FORWARDED_SECRET` to the value that the proxy inserts in the header
* Apache Traffic Server: `CONFIG proxy.config.http.insert_forwarded STRING for|proto|host|by=_secret`
* NGHTTPX: `nghttpx --add-forwarded=for,proto,host,by --forwarded-for=ip --forwarded-by=_secret`
* NGINX: after [the official instructions](https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/), add anywhere in your config:
proxy_set_header Forwarded "$proxy_add_forwarded;by=\"_$server_name\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\";secret=_secret";
* a custom header with client IP: set `REAL_IP_HEADER` to the name of that header
* `x-forwarded-for`: set `PROXIES_COUNT` to `1` for a single proxy, or a greater number to allow Sanic to select the correct IP
* no proxies: no configuration required!
#### Changes in Sanic 19.9
Earlier Sanic versions had unsafe default settings. From 19.9 onwards proxy settings must be set manually, and support for negative PROXIES_COUNT has been removed.

250
docs/sanic/config.rst Normal file
View File

@@ -0,0 +1,250 @@
Configuration
=============
Any reasonably complex application will need configuration that is not baked into the actual code. Settings might be different for different environments or installations.
Basics
------
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
.. code-block:: python
app = Sanic('myapp')
app.config.DB_NAME = 'appdb'
app.config.DB_USER = 'appuser'
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
.. code-block:: python
db_settings = {
'DB_HOST': 'localhost',
'DB_NAME': 'appdb',
'DB_USER': 'appuser'
}
app.config.update(db_settings)
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
Loading Configuration
---------------------
There are several ways how to load configuration.
From Environment Variables
~~~~~~~~~~~~~~~~~~~~~~~~~~
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic:
.. code-block:: python
app = Sanic(__name__, load_env='MYAPP_')
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
.. code-block:: python
app = Sanic(__name__, load_env=False)
From an Object
~~~~~~~~~~~~~~
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
.. code-block:: python
import myapp.default_settings
app = Sanic('myapp')
app.config.from_object(myapp.default_settings)
or also by path to config:
.. code-block:: python
app = Sanic('myapp')
app.config.from_object('config.path.config.Class')
You could use a class or any other object as well.
From a File
~~~~~~~~~~~
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
.. code-block:: python
app = Sanic('myapp')
app.config.from_envvar('MYAPP_SETTINGS')
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
.. code-block:: python
#$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py
#INFO: Goin' Fast @ http://0.0.0.0:8000
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase variables are added to the configuration. Most commonly the configuration consists of simple key value pairs:
.. code-block:: python
# config_file
DB_HOST = 'localhost'
DB_NAME = 'appdb'
DB_USER = 'appuser'
Builtin Configuration Values
----------------------------
Out of the box there are just a few predefined values which can be overwritten when creating the application.
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| Variable | Default | Description |
+===========================+===================+=============================================================================+
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| KEEP_ALIVE | True | Disables keep-alive when False |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_MAX_SIZE | 2^20 | Maximum size for incoming messages (bytes) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_MAX_QUEUE | 32 | Maximum length of the queue that holds incoming messages |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_READ_LIMIT | 2^16 | High-water limit of the buffer for incoming bytes |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_WRITE_LIMIT | 2^16 | High-water limit of the buffer for outgoing bytes |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| ACCESS_LOG | True | Disable or enable access log |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| FORWARDED_SECRET | None | Used to securely identify a specific proxy server (see below) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| PROXIES_COUNT | None | The number of proxy servers in front of the app (e.g. nginx; see below) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| FORWARDED_FOR_HEADER | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| REAL_IP_HEADER | None | The name of "X-Real-IP" HTTP header that contains real client ip |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
The different Timeout variables:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`REQUEST_TIMEOUT`
#################
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the
Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the
`REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates an `HTTP 408` response
and sends that to the client. Set this parameter's value higher if your clients routinely pass very large request payloads
or upload requests very slowly.
`RESPONSE_TIMEOUT`
##################
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the
Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT`
value (in seconds), this is considered a Server Error so Sanic generates an `HTTP 503` response and sends that to the
client. Set this parameter's value higher if your application is likely to have long-running process that delay the
generation of a response.
`KEEP_ALIVE_TIMEOUT`
####################
What is Keep Alive? And what does the Keep Alive Timeout value do?
******************************************************************
`Keep-Alive` is a HTTP feature introduced in `HTTP 1.1`. When sending a HTTP request, the client (usually a web browser application)
can set a `Keep-Alive` header to indicate the http server (Sanic) to not close the TCP connection after it has send the response.
This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient
network traffic for both the client and the server.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application,
set it to `False` to cause all client connections to close immediately after a response is sent, regardless of
the `Keep-Alive` header on the request.
The amount of time the server holds the TCP connection open is decided by the server itself.
In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds.
This is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for
the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless
you know your clients are using a browser which supports TCP connections held open for that long.
For reference:
* Apache httpd server default keepalive timeout = 5 seconds
* Nginx server default keepalive timeout = 75 seconds
* Nginx performance tuning guidelines uses keepalive = 15 seconds
* IE (5-9) client hard keepalive limit = 60 seconds
* Firefox client hard keepalive limit = 115 seconds
* Opera 11 client hard keepalive limit = 120 seconds
* Chrome 13+ client keepalive limit > 300+ seconds
Proxy configuration
~~~~~~~~~~~~~~~~~~~
When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain ip of a proxy,
typically `127.0.0.1`. Sanic may be configured to use proxy headers for determining the true client IP,
available as `request.remote_addr`. The full external URL is also constructed from header fields if available.
Without proper precautions, a malicious client may use proxy headers to spoof its own IP. To avoid such issues, Sanic does not use any proxy headers unless explicitly enabled.
Services behind reverse proxies must configure `FORWARDED_SECRET`, `REAL_IP_HEADER` and/or `PROXIES_COUNT`.
Forwarded header
################
.. Forwarded: for="1.2.3.4"; proto="https"; host="yoursite.com"; secret="Pr0xy", for="10.0.0.1"; proto="http"; host="proxy.internal"; by="_1234proxy"
* Set `FORWARDED_SECRET` to an identifier used by the proxy of interest.
The secret is used to securely identify a specific proxy server. Given the above header, secret `Pr0xy` would use the
information on the first line and secret `_1234proxy` would use the second line. The secret must exactly match the value
of `secret` or `by`. A secret in `by` must begin with an underscore and use only characters specified in
`RFC 7239 section 6.3 <https://tools.ietf.org/html/rfc7239#section-6.3>`_, while `secret` has no such restrictions.
Sanic ignores any elements without the secret key, and will not even parse the header if no secret is set.
All other proxy headers are ignored once a trusted forwarded element is found, as it already carries complete information about the client.
Traditional proxy headers
#########################
.. X-Real-IP: 1.2.3.4
X-Forwarded-For: 1.2.3.4, 10.0.0.1
X-Forwarded-Proto: https
X-Forwarded-Host: yoursite.com
* Set `REAL_IP_HEADER` to `x-real-ip`, `true-client-ip`, `cf-connecting-ip` or other name of such header.
* Set `PROXIES_COUNT` to the number of entries expected in `x-forwarded-for` (name configurable via `FORWARDED_FOR_HEADER`).
If client IP is found by one of these methods, Sanic uses the following headers for URL parts:
* `x-forwarded-proto`, `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-path` and if necessary, `x-scheme`.
Proxy config if using ...
#########################
* a proxy that supports `forwarded`: set `FORWARDED_SECRET` to the value that the proxy inserts in the header
* Apache Traffic Server: `CONFIG proxy.config.http.insert_forwarded STRING for|proto|host|by=_secret`
* NGHTTPX: `nghttpx --add-forwarded=for,proto,host,by --forwarded-for=ip --forwarded-by=_secret`
* NGINX: :ref:`nginx`.
* a custom header with client IP: set `REAL_IP_HEADER` to the name of that header
* `x-forwarded-for`: set `PROXIES_COUNT` to `1` for a single proxy, or a greater number to allow Sanic to select the correct IP
* no proxies: no configuration required!
Changes in Sanic 19.9
#####################
Earlier Sanic versions had unsafe default settings. From 19.9 onwards proxy settings must be set manually, and support for negative PROXIES_COUNT has been removed.

View File

@@ -13,7 +13,7 @@ and by default will enable the Auto Reload feature.
Setting the debug mode
----------------------
By setting the ``debug`` mode a more verbose output from Sanic will be outputed
By setting the ``debug`` mode a more verbose output from Sanic will be output
and the Automatic Reloader will be activated.
.. code-block:: python
@@ -21,7 +21,7 @@ and the Automatic Reloader will be activated.
from sanic import Sanic
from sanic.response import json
app = Sanic()
app = Sanic(__name__)
@app.route('/')
async def hello_world(request):
@@ -43,11 +43,11 @@ the ``auto_reload`` argument will activate or deactivate the Automatic Reloader.
from sanic import Sanic
from sanic.response import json
app = Sanic()
app = Sanic(__name__)
@app.route('/')
async def hello_world(request):
return json({"hello": "world"})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, auto_reload=True)
app.run(host="0.0.0.0", port=8000, auto_reload=True)

View File

@@ -1,39 +0,0 @@
# Handler Decorators
Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.
## Authorization Decorator
Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response.
```python
from functools import wraps
from sanic.response import json
def authorized():
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
# run some method that checks the request
# for the client's authorization status
is_authorized = check_request_for_authorization_status(request)
if is_authorized:
# the user is authorized.
# run the handler method and return the response
response = await f(request, *args, **kwargs)
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return decorated_function
return decorator
@app.route("/")
@authorized()
async def test(request):
return json({'status': 'authorized'})
```

40
docs/sanic/decorators.rst Normal file
View File

@@ -0,0 +1,40 @@
Handler Decorators
==================
Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.
Authorization Decorator
-----------------------
Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response.
.. code-block:: python
from functools import wraps
from sanic.response import json
def authorized():
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
# run some method that checks the request
# for the client's authorization status
is_authorized = check_request_for_authorization_status(request)
if is_authorized:
# the user is authorized.
# run the handler method and return the response
response = await f(request, *args, **kwargs)
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return decorated_function
return decorator
@app.route("/")
@authorized()
async def test(request):
return json({'status': 'authorized'})

View File

@@ -1,10 +1,15 @@
# Deploying
Deploying
=========
Deploying Sanic is very simple using one of three options: the inbuilt webserver,
an [ASGI webserver](https://asgi.readthedocs.io/en/latest/implementations.html), or `gunicorn`.
It is also very common to place Sanic behind a reverse proxy, like `nginx`.
Sanic has three serving options: the inbuilt webserver,
an `ASGI webserver <https://asgi.readthedocs.io/en/latest/implementations.html>`_, or `gunicorn`.
## Running via Sanic webserver
Sanic's own webserver is the fastest option, and it can be securely run on
the Internet. Still, it is also very common to place Sanic behind a reverse
proxy, as shown in :ref:`nginx`.
Running via Sanic webserver
---------------------------
After defining an instance of `sanic.Sanic`, we can call the `run` method with the following
keyword arguments:
@@ -15,136 +20,126 @@ keyword arguments:
- `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).
- `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>`_.
- `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server).
```python
app.run(host='0.0.0.0', port=1337, access_log=False)
```
.. code-block:: python
app.run(host='0.0.0.0', port=1337, access_log=False)
In the above example, we decided to turn off the access log in order to increase performance.
### Workers
Workers
~~~~~~~
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)
```
.. code-block:: python
app.run(host='0.0.0.0', port=1337, workers=4)
Sanic will automatically spin up multiple processes and route traffic between
them. We recommend as many workers as you have available cores.
### Running via command
Running via command
~~~~~~~~~~~~~~~~~~~
If you like using command line arguments, you can launch a Sanic webserver 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`
.. 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 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)
```
.. code-block:: python
## Running via ASGI
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
Running via ASGI
----------------
Sanic is also ASGI-compliant. This means you can use your preferred ASGI webserver
to run Sanic. The three main implementations of ASGI are
[Daphne](http://github.com/django/daphne), [Uvicorn](https://www.uvicorn.org/),
and [Hypercorn](https://pgjones.gitlab.io/hypercorn/index.html).
`Daphne <http://github.com/django/daphne>`_, `Uvicorn <https://www.uvicorn.org/>`_,
and `Hypercorn <https://pgjones.gitlab.io/hypercorn/index.html>`_.
Follow their documentation for the proper way to run them, but it should look
something like:
```
daphne myapp:app
uvicorn myapp:app
hypercorn myapp:app
```
::
daphne myapp:app
uvicorn myapp:app
hypercorn myapp:app
A couple things to note when using ASGI:
1. When using the Sanic webserver, websockets will run using the [`websockets`](https://websockets.readthedocs.io/) package. In ASGI mode, there is no need for this package since websockets are managed in the ASGI server.
1. The ASGI [lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) supports
only two server events: startup and shutdown. Sanic has four: before startup, after startup,
before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and shutdown events will
run consecutively and not actually around the server process beginning and ending (since that
is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and
1. When using the Sanic webserver, websockets will run using the `websockets <https://websockets.readthedocs.io/>`_ package.
In ASGI mode, there is no need for this package since websockets are managed in the ASGI server.
2. The ASGI `lifespan protocol <https://asgi.readthedocs.io/en/latest/specs/lifespan.html>`, supports
only two server events: startup and shutdown. Sanic has four: before startup, after startup,
before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and shutdown events will
run consecutively and not actually around the server process beginning and ending (since that
is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and
`before_server_stop`.
1. ASGI mode is still in "beta" as of Sanic v19.6.
## Running via Gunicorn
Sanic has experimental support for running on `Trio <https://trio.readthedocs.io/en/stable/>`_ with::
[Gunicorn](http://gunicorn.org/) Green Unicorn is a WSGI HTTP Server for UNIX.
hypercorn -k trio myapp:app
Running via Gunicorn
--------------------
`Gunicorn <http://gunicorn.org/>`_ Green Unicorn is a WSGI HTTP Server for UNIX.
Its a pre-fork worker model ported from Rubys Unicorn project.
In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker`
for Gunicorn `worker-class` argument:
```
gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
```
::
gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker
after it has processed a given number of requests. This can be a convenient way to help limit the effects
of the memory leak.
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
See the `Gunicorn Docs <http://docs.gunicorn.org/en/latest/settings.html#max-requests>`_ for more information.
## Other deployment considerations
Other deployment considerations
-------------------------------
### Running behind a reverse proxy
Sanic can be used with a reverse proxy (e.g. nginx). There's a simple example of nginx configuration:
```
server {
listen 80;
server_name example.org;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
```
If you want to get real client ip, you should configure `X-Real-IP` and `X-Forwarded-For` HTTP headers and set `app.config.PROXIES_COUNT` to `1`; see the configuration page for more information.
### Disable debug logging for performance
Disable debug logging for performance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
```python
app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False)
```
.. code-block:: python
app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False)
Running via Gunicorn you can set Environment variable `SANIC_ACCESS_LOG="False"`
```
env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning
```
::
env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning
Or you can rewrite app config directly
```python
app.config.ACCESS_LOG = False
```
.. code-block:: python
### Asynchronous support and sharing the loop
app.config.ACCESS_LOG = False
Asynchronous support and sharing the loop
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is suitable if you *need* to share the Sanic process with other applications, in particular the `loop`.
However, be advised that this method does not support using multiple processes, and is not the preferred way
@@ -152,12 +147,12 @@ to run the app in general.
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
```python
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
loop.run_forever()
```
.. code-block:: python
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
loop.run_forever()
Caveat: using this method, calling `app.create_server()` will trigger "before_server_start" server events, but not
"after_server_start", "before_server_stop", or "after_server_stop" server events.
@@ -167,25 +162,25 @@ the server task.
Here is an incomplete example (please see `run_async_advanced.py` in examples for something more complete):
```python
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
server = loop.run_until_complete(serv_task)
server.after_start()
try:
loop.run_forever()
except KeyboardInterrupt as e:
loop.stop()
finally:
server.before_stop()
.. code-block:: python
# Wait for server to close
close_task = server.close()
loop.run_until_complete(close_task)
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
server = loop.run_until_complete(serv_task)
server.after_start()
try:
loop.run_forever()
except KeyboardInterrupt as e:
loop.stop()
finally:
server.before_stop()
# Complete all tasks on the loop
for connection in server.connections:
connection.close_if_idle()
server.after_stop()
```
# Wait for server to close
close_task = server.close()
loop.run_until_complete(close_task)
# Complete all tasks on the loop
for connection in server.connections:
connection.close_if_idle()
server.after_stop()

View File

@@ -25,7 +25,7 @@ A simple sanic application with a single ``async`` method with ``text`` and ``js
Simple App with ``Sanic Views``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Showcasing the simple mechanism of using :class:`sanic.viewes.HTTPMethodView` as well as a way to extend the same
Showcasing the simple mechanism of using :class:`sanic.views.HTTPMethodView` as well as a way to extend the same
into providing a custom ``async`` behavior for ``view``.
.. literalinclude:: ../../examples/simple_async_view.py

View File

@@ -1,88 +0,0 @@
# 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.
## Throwing an exception
To throw an exception, simply `raise` the relevant exception from the
`sanic.exceptions` module.
```python
from sanic.exceptions import ServerError
@app.route('/killme')
async def i_am_ready_to_die(request):
raise ServerError("Something bad happened", status_code=500)
```
You can also use the `abort` function with the appropriate status code:
```python
from sanic.exceptions import abort
from sanic.response import text
@app.route('/youshallnotpass')
async def no_no(request):
abort(401)
# this won't happen
text("OK")
```
## Handling exceptions
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.response import text
from sanic.exceptions import NotFound
@app.exception(NotFound)
async def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
```
You can also add an exception handler as such:
```python
from sanic import Sanic
async def server_error_handler(request, exception):
return text("Oops, server error", status=500)
app = Sanic()
app.error_handler.add(Exception, server_error_handler)
```
In some cases, you might want to add some more error handling
functionality to what is provided by default. In that case, you
can subclass Sanic's default error handler as such:
```python
from sanic import Sanic
from sanic.handlers import ErrorHandler
class CustomErrorHandler(ErrorHandler):
def default(self, request, exception):
''' handles errors that have no error handlers assigned '''
# You custom error handling logic...
return super().default(request, exception)
app = Sanic()
app.error_handler = CustomErrorHandler()
```
## 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.

92
docs/sanic/exceptions.rst Normal file
View File

@@ -0,0 +1,92 @@
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.
Throwing an exception
---------------------
To throw an exception, simply `raise` the relevant exception from the
`sanic.exceptions` module.
.. code-block:: python
from sanic.exceptions import ServerError
@app.route('/killme')
async def i_am_ready_to_die(request):
raise ServerError("Something bad happened", status_code=500)
You can also use the `abort` function with the appropriate status code:
.. code-block:: python
from sanic.exceptions import abort
from sanic.response import text
@app.route('/youshallnotpass')
async def no_no(request):
abort(401)
# this won't happen
text("OK")
Handling exceptions
-------------------
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.
.. code-block:: python
from sanic.response import text
from sanic.exceptions import NotFound
@app.exception(NotFound)
async def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
You can also add an exception handler as such:
.. code-block:: python
from sanic import Sanic
async def server_error_handler(request, exception):
return text("Oops, server error", status=500)
app = Sanic("error_handler_example")
app.error_handler.add(Exception, server_error_handler)
In some cases, you might want to add some more error handling
functionality to what is provided by default. In that case, you
can subclass Sanic's default error handler as such:
.. code-block:: python
from sanic import Sanic
from sanic.handlers import ErrorHandler
class CustomErrorHandler(ErrorHandler):
def default(self, request, exception):
''' handles errors that have no error handlers assigned '''
# You custom error handling logic...
return super().default(request, exception)
app = Sanic("custom_error_handler_example")
app.error_handler = CustomErrorHandler()
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.

View File

@@ -1,3 +0,0 @@
# Extensions
Moved to the [awesome-sanic](https://github.com/mekicha/awesome-sanic) list.

View File

@@ -0,0 +1,4 @@
Extensions
==========
Moved to the `awesome-sanic <https://github.com/mekicha/awesome-sanic>`_ list.

View File

@@ -1,57 +0,0 @@
# Getting Started
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
least version 3.6 of Python before starting. Sanic uses the new `async`/`await`
syntax, so earlier versions of python won't work.
## 1. Install Sanic
> If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency.
```bash
pip3 install sanic
```
To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`)
to true will stop that features installation.
```bash
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic
```
You can also install Sanic from [`conda-forge`](https://anaconda.org/conda-forge/sanic)
```bash
conda config --add channels conda-forge
conda install sanic
```
## 2. Create a file called `main.py`
```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)
```
## 3. Run the server
```
python3 main.py
```
## 4. Check your browser
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!

View File

@@ -0,0 +1,62 @@
Getting Started
===============
Make sure you have both `pip <https://pip.pypa.io/en/stable/installing/>`_ and at
least version 3.6 of Python before starting. Sanic uses the new `async`/`await`
syntax, so earlier versions of python won't work.
1. Install Sanic
----------------
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency.
.. code-block:: bash
pip3 install sanic
To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` ( with`X` = `UVLOOP`/`UJSON`)
to true will stop that features installation.
.. code-block:: bash
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install --no-binary :all: sanic
You can also install Sanic from `conda-forge <https://anaconda.org/conda-forge/sanic>`_
.. code-block:: bash
conda config --add channels conda-forge
conda install sanic
2. Create a file called `main.py`
---------------------------------
.. code-block:: python
from sanic import Sanic
from sanic.response import json
app = Sanic("hello_example")
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
3. Run the server
-----------------
.. code-block:: bash
python3 main.py
4. Check your browser
---------------------
Open the address `http://0.0.0.0:8000 <http://0.0.0.0:8000>`_ in your web browser. You should see
the message *Hello world!*.
You now have a working Sanic server!

View File

@@ -15,7 +15,7 @@ Sanic aspires to be simple
from sanic import Sanic
from sanic.response import json
app = Sanic()
app = Sanic("App Name")
@app.route("/")
async def test(request):
@@ -26,4 +26,5 @@ Sanic aspires to be simple
.. note::
Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru December 2020. Official Python support for version 3.5 is set to expire in September 2020.
Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru
December 2020. Official Python support for version 3.5 is set to expire in September 2020.

View File

@@ -17,7 +17,7 @@ A simple example using default settings would be like this:
from sanic.log import logger
from sanic.response import text
app = Sanic('test')
app = Sanic('logging_example')
@app.route('/')
async def test(request):
@@ -47,7 +47,7 @@ initialize ``Sanic`` app:
.. code:: python
app = Sanic('test', log_config=LOGGING_CONFIG)
app = Sanic('logging_example', log_config=LOGGING_CONFIG)
And to close logging, simply assign access_log=False:
@@ -100,4 +100,4 @@ Log Context Parameter Parameter Value Datatype
The default access log format is ``%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d``
.. _python3 logging API: https://docs.python.org/3/howto/logging.html
.. _python3 logging API: https://docs.python.org/3/howto/logging.html

View File

@@ -1,173 +0,0 @@
# Middleware And Listeners
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.
Additionally, Sanic provides listeners which allow you to run code at various points of your application's lifecycle.
## Middleware
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'`.
* Request middleware receives only the `request` as argument.
* Response middleware receives both the `request` and `response`.
The simplest middleware doesn't modify the request or response at all:
```
@app.middleware('request')
async def print_on_request(request):
print("I print when a request is received by the server")
@app.middleware('response')
async def print_on_response(request, response):
print("I print when a response is returned by the server")
```
## Modifying the request or response
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.
```
app = Sanic(__name__)
@app.middleware('request')
async def add_key(request):
# Arbitrary data may be stored in request context:
request.ctx.foo = 'bar'
@app.middleware('response')
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
@app.get("/")
async def index(request):
return sanic.response.text(request.ctx.foo)
app.run(host="0.0.0.0", port=8000)
```
The three middlewares are executed in order:
1. The first request middleware **add_key** adds a new key `foo` into request context.
2. Request is routed to handler **index**, which gets the key from context and returns a text response.
3. The first response middleware **custom_banner** changes the HTTP response header *Server* to
say *Fake-Server*
4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks.
## 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.
```
@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')
```
## Custom context
Arbitrary data may be stored in `request.ctx`. A typical use case
would be to store the user object acquired from database in an authentication
middleware. Keys added are accessible to all later middleware as well as
the handler over the duration of the request.
Custom context is reserved for applications and extensions. Sanic itself makes
no use of it.
## Listeners
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
- `before_server_start`
- `after_server_start`
- `before_server_stop`
- `after_server_stop`
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
For example:
```
@app.listener('before_server_start')
async def setup_db(app, loop):
app.db = await db_setup()
@app.listener('after_server_start')
async def notify_server_started(app, loop):
print('Server successfully started!')
@app.listener('before_server_stop')
async def notify_server_stopping(app, loop):
print('Server shutting down!')
@app.listener('after_server_stop')
async def close_db(app, loop):
await app.db.close()
```
It's also possible to register a listener using the `register_listener` method.
This may be useful if you define your listeners in another module besides
the one you instantiate your app in.
```
app = Sanic()
async def setup_db(app, loop):
app.db = await db_setup()
app.register_listener(setup_db, 'before_server_start')
```
If you want to schedule a background task to run after the loop has started,
Sanic provides the `add_task` method to easily do so.
```
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
app.add_task(notify_server_started_after_five_seconds())
```
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
```
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(notify_server_started_after_five_seconds)
```
Or you can pass the app explicitly for the same effect:
```
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(notify_server_started_after_five_seconds(app))
`

188
docs/sanic/middleware.rst Normal file
View File

@@ -0,0 +1,188 @@
Middleware And Listeners
========================
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.
Additionally, Sanic provides listeners which allow you to run code at various points of your application's lifecycle.
Middleware
----------
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'`.
* Request middleware receives only the `request` as argument.
* Response middleware receives both the `request` and `response`.
The simplest middleware doesn't modify the request or response at all:
.. code-block:: python
@app.middleware('request')
async def print_on_request(request):
print("I print when a request is received by the server")
@app.middleware('response')
async def print_on_response(request, response):
print("I print when a response is returned by the server")
Modifying the request or response
---------------------------------
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.
.. code-block:: python
app = Sanic(__name__)
@app.middleware('request')
async def add_key(request):
# Arbitrary data may be stored in request context:
request.ctx.foo = 'bar'
@app.middleware('response')
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
@app.get("/")
async def index(request):
return sanic.response.text(request.ctx.foo)
app.run(host="0.0.0.0", port=8000)
The three middlewares are executed in order:
1. The first request middleware **add_key** adds a new key `foo` into request context.
2. Request is routed to handler **index**, which gets the key from context and returns a text response.
3. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server*
4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks.
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.
.. code-block:: 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')
Custom context
--------------
Arbitrary data may be stored in `request.ctx`. A typical use case
would be to store the user object acquired from database in an authentication
middleware. Keys added are accessible to all later middleware as well as
the handler over the duration of the request.
Custom context is reserved for applications and extensions. Sanic itself makes
no use of it.
Listeners
---------
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
- `before_server_start`
- `after_server_start`
- `before_server_stop`
- `after_server_stop`
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
For example:
.. code-block:: python
@app.listener('before_server_start')
async def setup_db(app, loop):
app.db = await db_setup()
@app.listener('after_server_start')
async def notify_server_started(app, loop):
print('Server successfully started!')
@app.listener('before_server_stop')
async def notify_server_stopping(app, loop):
print('Server shutting down!')
@app.listener('after_server_stop')
async def close_db(app, loop):
await app.db.close()
Note:
The listeners are deconstructed in the reverse order of being constructed.
For example:
If the first listener in before_server_start handler setups a database connection,
ones registered after it can rely on that connection being alive both when they are started
and stopped, because stopping is done in reverse order, and the database connection is
torn down last.
It's also possible to register a listener using the `register_listener` method.
This may be useful if you define your listeners in another module besides
the one you instantiate your app in.
.. code-block:: python
app = Sanic(__name__)
async def setup_db(app, loop):
app.db = await db_setup()
app.register_listener(setup_db, 'before_server_start')
If you want to schedule a background task to run after the loop has started,
Sanic provides the `add_task` method to easily do so.
.. code-block:: python
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
app.add_task(notify_server_started_after_five_seconds())
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
.. code-block:: python
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(notify_server_started_after_five_seconds)
Or you can pass the app explicitly for the same effect:
.. code-block:: python
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(notify_server_started_after_five_seconds(app))

222
docs/sanic/nginx.rst Normal file
View File

@@ -0,0 +1,222 @@
.. _nginx:
Nginx Deployment
================
Introduction
~~~~~~~~~~~~
Although Sanic can be run directly on Internet, it may be useful to use a proxy
server such as Nginx in front of it. This is particularly useful for running
multiple virtual hosts on the same IP, serving NodeJS or other services beside
a single Sanic app, and it also allows for efficient serving of static files.
SSL and HTTP/2 are also easily implemented on such proxy.
We are setting the Sanic app to serve only locally at `127.0.0.1:8000`, while the
Nginx installation is responsible for providing the service to public Internet
on domain `example.com`. Static files will be served from `/var/www/`.
Proxied Sanic app
~~~~~~~~~~~~~~~~~
The app needs to be setup with a secret key used to identify a trusted proxy,
so that real client IP and other information can be identified. This protects
against anyone on the Internet sending fake headers to spoof their IP addresses
and other details. Choose any random string and configure it both on the app
and in Nginx config.
.. code-block:: python
from sanic import Sanic
from sanic.response import text
app = Sanic("proxied_example")
app.config.FORWARDED_SECRET = "YOUR SECRET"
@app.get("/")
def index(request):
# This should display external (public) addresses:
return text(
f"{request.remote_addr} connected to {request.url_for('index')}\n"
f"Forwarded: {request.forwarded}\n"
)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000, workers=8, access_log=False)
Since this is going to be a system service, save your code to
`/srv/sanicexample/sanicexample.py`.
For testing, run your app in a terminal.
Nginx configuration
~~~~~~~~~~~~~~~~~~~
Quite much configuration is required to allow fast transparent proxying, but
for the most part these don't need to be modified, so bear with me.
Upstream servers need to be configured in a separate `upstream` block to enable
HTTP keep-alive, which can drastically improve performance, so we use this
instead of directly providing an upstream address in `proxy_pass` directive. In
this example, the upstream section is named by `server_name`, i.e. the public
domain name, which then also gets passed to Sanic in the `Host` header. You may
change the naming as you see fit. Multiple servers may also be provided for
load balancing and failover.
Change the two occurrences of `example.com` to your true domain name, and
instead of `YOUR SECRET` use the secret you chose for your app.
::
upstream example.com {
keepalive 100;
server 127.0.0.1:8000;
#server unix:/tmp/sanic.sock;
}
server {
server_name example.com;
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
# Serve static files if found, otherwise proxy to Sanic
location / {
root /var/www;
try_files $uri @sanic;
}
location @sanic {
proxy_pass http://$server_name;
# Allow fast streaming HTTP/1.1 pipes (keep-alive, unbuffered)
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_buffering off;
# Proxy forwarding (password configured in app.config.FORWARDED_SECRET)
proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\"";
# Allow websockets
proxy_set_header connection "upgrade";
proxy_set_header upgrade $http_upgrade;
}
}
To avoid cookie visibility issues and inconsistent addresses on search engines,
it is a good idea to redirect all visitors to one true domain, always using
HTTPS:
::
# Redirect all HTTP to HTTPS with no-WWW
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name ~^(?:www\.)?(.*)$;
return 301 https://$1$request_uri;
}
# Redirect WWW to no-WWW
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ~^www\.(.*)$;
return 301 $scheme://$1$request_uri;
}
The above config sections may be placed in `/etc/nginx/sites-available/default`
or in other site configs (be sure to symlink them to `sites-enabled` if you
create new ones).
Make sure that your SSL certificates are configured in the main config, or
add the `ssl_certificate` and `ssl_certificate_key` directives to each
`server` section that listens on SSL.
Additionally, copy&paste all of this into `nginx/conf.d/forwarded.conf`:
::
# RFC 7239 Forwarded header for Nginx proxy_pass
# Add within your server or location block:
# proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\"";
# Configure your upstream web server to identify this proxy by that password
# because otherwise anyone on the Internet could spoof these headers and fake
# their real IP address and other information to your service.
# Provide the full proxy chain in $proxy_forwarded
map $proxy_add_forwarded $proxy_forwarded {
default "$proxy_add_forwarded;by=\"_$hostname\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\"";
}
# The following mappings are based on
# https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
map $remote_addr $proxy_forwarded_elem {
# IPv4 addresses can be sent as-is
~^[0-9.]+$ "for=$remote_addr";
# IPv6 addresses need to be bracketed and quoted
~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
# Unix domain socket names cannot be represented in RFC 7239 syntax
default "for=unknown";
}
map $http_forwarded $proxy_add_forwarded {
# If the incoming Forwarded header is syntactically valid, append to it
"~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";
# Otherwise, replace it
default "$proxy_forwarded_elem";
}
For installs that don't use `conf.d` and `sites-available`, all of the above
configs may also be placed inside the `http` section of the main `nginx.conf`.
Reload Nginx config after changes:
::
sudo nginx -s reload
Now you should be able to connect your app on `https://example.com/`. Any 404
errors and such will be handled by Sanic's error pages, and whenever a static
file is present at a given path, it will be served by Nginx.
SSL certificates
~~~~~~~~~~~~~~~~
If you haven't already configured valid certificates on your server, now is a
good time to do so. Install `certbot` and `python3-certbot-nginx`, then run
::
certbot --nginx -d example.com -d www.example.com
`<https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/>`_
Running as a service
~~~~~~~~~~~~~~~~~~~~
This part is for Linux distributions based on `systemd`. Create a unit file
`/etc/systemd/system/sanicexample.service`::
[Unit]
Description=Sanic Example
[Service]
User=nobody
WorkingDirectory=/srv/sanicexample
ExecStart=/usr/bin/env python3 sanicexample.py
Restart=always
[Install]
WantedBy=multi-user.target
Then reload service files, start your service and enable it on boot::
sudo systemctl daemon-reload
sudo systemctl start sanicexample
sudo systemctl enable sanicexample

View File

@@ -1,4 +1,5 @@
# Request Data
Request Data
============
When an endpoint receives a HTTP request, the route function is passed a
`Request` object.
@@ -7,85 +8,80 @@ The following variables are accessible as properties on `Request` objects:
- `json` (any) - JSON body
```python
from sanic.response import json
.. code-block:: python
@app.route("/json")
def post_json(request):
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.
Property is providing the default parsing strategy. If you would like to change it look to the section below
(`Changing the default parsing rules of the queryset`).
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. Property is providing the default parsing
strategy. If you would like to change it look to the section below (`Changing the default parsing rules of the queryset`).
```python
from sanic.response import json
.. code-block:: python
@app.route("/query_string")
def query_string(request):
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 })
```
- `query_args` (list) - On many cases you would need to access the url arguments in
a less packed form. `query_args` is the list of `(key, value)` tuples.
Property is providing the default parsing strategy. If you would like to change it look to the section below
(`Changing the default parsing rules of the queryset`).
For the same previous URL queryset `?key1=value1&key2=value2`, the
`query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`.
And in case of the multiple params with the same key like `?key1=value1&key2=value2&key1=value3`
the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`.
Property is providing the default parsing strategy. If you would like to change it look to the section below
(`Changing the default parsing rules of the queryset`). For the same previous URL queryset `?key1=value1&key2=value2`,
the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`. And in case of the multiple params
with the same key like `?key1=value1&key2=value2&key1=value3` the `query_args` list would look like
`[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`.
The difference between Request.args and Request.query_args
for the queryset `?key1=value1&key2=value2&key1=value3`
The difference between Request.args and Request.query_args for the queryset `?key1=value1&key2=value2&key1=value3`
```python
from sanic import Sanic
from sanic.response import json
.. code-block:: python
app = Sanic(__name__)
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/test_request_args")
async def test_request_args(request):
@app.route("/test_request_args")
async def test_request_args(request):
return json({
"parsed": True,
"url": request.url,
"query_string": request.query_string,
"args": request.args,
"raw_args": request.raw_args,
"query_args": request.query_args,
})
if __name__ == '__main__':
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
```
Output
```
{
"parsed":true,
"url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3",
"query_string":"key1=value1&key2=value2&key1=value3",
"args":{"key1":["value1","value3"],"key2":["value2"]},
"raw_args":{"key1":"value1","key2":"value2"},
"query_args":[["key1","value1"],["key2","value2"],["key1","value3"]]
}
```
.. code-block:: json
`raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions.
{
"parsed":true,
"url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3",
"query_string":"key1=value1&key2=value2&key1=value3",
"args":{"key1":["value1","value3"],"key2":["value2"]},
"query_args":[["key1","value1"],["key2","value2"],["key1","value3"]]
}
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
```python
from sanic.response import json
.. code-block:: python
@app.route("/files")
def post_json(request):
from sanic.response import json
@app.route("/files")
def post_json(request):
test_file = request.files.get('test')
file_parameters = {
@@ -95,28 +91,28 @@ The following variables are accessible as properties on `Request` objects:
}
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
.. code-block:: python
@app.route("/form")
def post_json(request):
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
.. code-block:: 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)
```
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
@@ -130,7 +126,8 @@ The following variables are accessible as properties on `Request` objects:
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
```python
.. code-block:: python
from sanic.response import json
from sanic import Blueprint
@@ -143,7 +140,6 @@ The following variables are accessible as properties on `Request` objects:
else:
return json({'status': 'production'})
```
- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar`
- `scheme`: The URL scheme associated with the request: 'http|https|ws|wss' or arbitrary value given by the headers.
- `host`: The host associated with the request(which in the `Host` header): `localhost:8080`
@@ -157,7 +153,8 @@ The following variables are accessible as properties on `Request` objects:
- `url_for`: Just like `sanic.Sanic.url_for`, but automatically determine `scheme` and `netloc` base on the request. Since this method is aiming to generate correct schema & netloc, `_external` is implied.
## Changing the default parsing rules of the queryset
Changing the default parsing rules of the queryset
--------------------------------------------------
The default parameters that are using internally in `args` and `query_args` properties to parse queryset:
@@ -177,85 +174,97 @@ with the new values.
For the queryset `/?test1=value1&test2=&test3=value3`:
```python
from sanic.response import json
.. code-block:: python
@app.route("/query_string")
def query_string(request):
args_with_blank_values = request.get_args(keep_blank_values=True)
return json({
"parsed": True,
"url": request.url,
"args_with_blank_values": args_with_blank_values,
"query_string": request.query_string
})
```
from sanic.response import json
@app.route("/query_string")
def query_string(request):
args_with_blank_values = request.get_args(keep_blank_values=True)
return json({
"parsed": True,
"url": request.url,
"args_with_blank_values": args_with_blank_values,
"query_string": request.query_string
})
The output will be:
```
{
"parsed": true,
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3",
"args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]},
"query_string": "test1=value1&test2=&test3=value3"
}
```
.. code-block:: JSON
## Accessing values using `get` and `getlist`
{
"parsed": true,
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3",
"args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]},
"query_string": "test1=value1&test2=&test3=value3"
}
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.
Accessing values using `get` and `getlist`
------------------------------------------
The `request.args` returns 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.request import RequestParameters
.. code-block:: python
args = RequestParameters()
args['titles'] = ['Post 1', 'Post 2']
from sanic.request import RequestParameters
args.get('titles') # => 'Post 1'
args = RequestParameters()
args['titles'] = ['Post 1', 'Post 2']
args.getlist('titles') # => ['Post 1', 'Post 2']
```
args.get('titles') # => 'Post 1'
## Accessing the handler name with the request.endpoint attribute
args.getlist('titles') # => ['Post 1', 'Post 2']
.. code-block:: python
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/")
def get_handler(request):
return json({
"p1": request.args.getlist("p1")
})
Accessing the handler name with the request.endpoint attribute
--------------------------------------------------------------
The `request.endpoint` attribute holds the handler's name. For instance, the below
route will return "hello".
```python
from sanic.response import text
from sanic import Sanic
.. code-block:: python
app = Sanic()
from sanic.response import text
from sanic import Sanic
@app.get("/")
def hello(request):
return text(request.endpoint)
```
app = Sanic(__name__)
Or, with a blueprint it will be include both, separated by a period. For example,
the below route would return foo.bar:
@app.get("/")
def hello(request):
return text(request.endpoint)
```python
from sanic import Sanic
from sanic import Blueprint
from sanic.response import text
Or, with a blueprint it will be include both, separated by a period. For example, the below route would return foo.bar:
.. code-block:: python
from sanic import Sanic
from sanic import Blueprint
from sanic.response import text
app = Sanic(__name__)
blueprint = Blueprint('foo')
app = Sanic(__name__)
blueprint = Blueprint('foo')
@blueprint.get('/')
async def bar(request):
return text(request.endpoint)
@blueprint.get('/')
async def bar(request):
return text(request.endpoint)
app.blueprint(blueprint)
app.blueprint(blueprint)
app.run(host="0.0.0.0", port=8000, debug=True)
```
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,114 +0,0 @@
# Response
Use functions in `sanic.response` module to create responses.
## Plain Text
```python
from sanic import response
@app.route('/text')
def handle_request(request):
return response.text('Hello world!')
```
## HTML
```python
from sanic import response
@app.route('/html')
def handle_request(request):
return response.html('<p>Hello world!</p>')
```
## JSON
```python
from sanic import response
@app.route('/json')
def handle_request(request):
return response.json({'message': 'Hello world!'})
```
## File
```python
from sanic import response
@app.route('/file')
async def handle_request(request):
return await response.file('/srv/www/whatever.png')
```
## Streaming
```python
from sanic import response
@app.route("/streaming")
async def index(request):
async def streaming_fn(response):
await response.write('foo')
await response.write('bar')
return response.stream(streaming_fn, content_type='text/plain')
```
See [Streaming](streaming.md) for more information.
## File Streaming
For large files, a combination of File and Streaming above
```python
from sanic import response
@app.route('/big_file.png')
async def handle_request(request):
return await response.file_stream('/srv/www/whatever.png')
```
## Redirect
```python
from sanic import response
@app.route('/redirect')
def handle_request(request):
return response.redirect('/json')
```
## Raw
Response without encoding the body
```python
from sanic import response
@app.route('/raw')
def handle_request(request):
return response.raw(b'raw data')
```
## Modify headers or status
To modify headers or status code, pass the `headers` or `status` argument to those functions:
```python
from sanic import response
@app.route('/json')
def handle_request(request):
return response.json(
{'message': 'Hello world!'},
headers={'X-Served-By': 'sanic'},
status=200
)
```

139
docs/sanic/response.rst Normal file
View File

@@ -0,0 +1,139 @@
Response
========
Use functions in `sanic.response` module to create responses.
Plain Text
----------
.. code-block:: python
from sanic import response
@app.route('/text')
def handle_request(request):
return response.text('Hello world!')
HTML
----
.. code-block:: python
from sanic import response
@app.route('/html')
def handle_request(request):
return response.html('<p>Hello world!</p>')
JSON
----
.. code-block:: python
from sanic import response
@app.route('/json')
def handle_request(request):
return response.json({'message': 'Hello world!'})
File
----
.. code-block:: python
from sanic import response
@app.route('/file')
async def handle_request(request):
return await response.file('/srv/www/whatever.png')
Streaming
---------
.. code-block:: python
from sanic import response
@app.route("/streaming")
async def index(request):
async def streaming_fn(response):
await response.write('foo')
await response.write('bar')
return response.stream(streaming_fn, content_type='text/plain')
See `Streaming <streaming.html>`_ for more information.
File Streaming
--------------
For large files, a combination of File and Streaming above
.. code-block:: python
from sanic import response
@app.route('/big_file.png')
async def handle_request(request):
return await response.file_stream('/srv/www/whatever.png')
Redirect
--------
.. code-block:: python
from sanic import response
@app.route('/redirect')
def handle_request(request):
return response.redirect('/json')
Raw
---
Response without encoding the body
.. code-block:: python
from sanic import response
@app.route('/raw')
def handle_request(request):
return response.raw(b'raw data')
Empty
--------------
For responding with an empty message as defined by `RFC 2616 <https://tools.ietf.org/search/rfc2616#section-7.2.1>`_
.. code-block:: python
from sanic import response
@app.route('/empty')
async def handle_request(request):
return response.empty()
Modify headers or status
------------------------
To modify headers or status code, pass the `headers` or `status` argument to those functions:
.. code-block:: python
from sanic import response
@app.route('/json')
def handle_request(request):
return response.json(
{'message': 'Hello world!'},
headers={'X-Served-By': 'sanic'},
status=200
)

View File

@@ -1,429 +0,0 @@
# Routing
Routing allows the user to specify handler functions for different URL endpoints.
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: `<PARAM>`.
Request parameters will be passed to the route handler functions as keyword
arguments.
```python
from sanic.response import text
@app.route('/tag/<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.
### Supported types
* `string`
* "Bob"
* "Python 3"
* `int`
* 10
* 20
* 30
* -10
* (No floats work here)
* `number`
* 1
* 1.5
* 10
* -10
* `alpha`
* "Bob"
* "Python"
* (If it contains a symbol or a non alphanumeric character it will fail)
* `path`
* "hello"
* "hello.text"
* "hello world"
* `uuid`
* 123a123a-a12a-1a1a-a1a1-1a12a1a12345 (UUIDv4 Support)
* `regex expression`
If no type is set then a string is expected. The argument given to the function will always be a string, independent of the type.
```python
from sanic.response import text
@app.route('/string/<string_arg:string>')
async def string_handler(request, string_arg):
return text('String - {}'.format(string_arg))
@app.route('/int/<integer_arg:int>')
async def integer_handler(request, integer_arg):
return text('Integer - {}'.format(integer_arg))
@app.route('/number/<number_arg:number>')
async def number_handler(request, number_arg):
return text('Number - {}'.format(number_arg))
@app.route('/alpha/<alpha_arg:alpha>')
async def number_handler(request, alpha_arg):
return text('Alpha - {}'.format(alpha_arg))
@app.route('/path/<path_arg:path>')
async def number_handler(request, path_arg):
return text('Path - {}'.format(path_arg))
@app.route('/uuid/<uuid_arg:uuid>')
async def number_handler(request, uuid_arg):
return text('Uuid - {}'.format(uuid_arg))
@app.route('/person/<name:[A-z]+>')
async def person_handler(request, name):
return text('Person - {}'.format(name))
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id))
```
**Warning** `str` is not a valid type tag. If you want `str` recognition then you must use `string`
## HTTP request types
By default, a route defined on a URL will be available for only GET requests to that URL.
However, the `@app.route` decorator accepts an optional parameter, `methods`,
which allows the handler function to work with any of the HTTP methods in the list.
```python
from sanic.response import text
@app.route('/post', methods=['POST'])
async def post_handler(request):
return text('POST request - {}'.format(request.json))
@app.route('/get', methods=['GET'])
async def get_handler(request):
return text('GET request - {}'.format(request.args))
```
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
```python
@app.route('/get', methods=['GET'], host='example.com')
async def get_handler(request):
return text('GET request - {}'.format(request.args))
# if the host header doesn't match example.com, this route will be used
@app.route('/get', methods=['GET'])
async def get_handler(request):
return text('GET request in default - {}'.format(request.args))
```
There are also shorthand method decorators:
```python
from sanic.response import text
@app.post('/post')
async def post_handler(request):
return text('POST request - {}'.format(request.json))
@app.get('/get')
async def get_handler(request):
return text('GET request - {}'.format(request.args))
```
## The `add_route` method
As we have seen, routes are often specified using the `@app.route` decorator.
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')
async def handler2(request, name):
return text('Folder - {}'.format(name))
async def person_handler2(request, name):
return text('Person - {}'.format(name))
# Add each handler function as a route
app.add_route(handler1, '/test')
app.add_route(handler2, '/folder/<name>')
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
```
## URL building with `url_for`
Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example:
```python
from sanic.response import redirect
@app.route('/')
async def index(request):
# generate a URL for the endpoint `post_handler`
url = app.url_for('post_handler', post_id=5)
# the URL is `/posts/5`, redirect to it
return redirect(url)
@app.route('/posts/<post_id>')
async def post_handler(request, post_id):
return text('Post - {}'.format(post_id))
```
Other things to keep in mind when using `url_for`:
- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example:
```python
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
# /posts/5?arg_one=one&arg_two=two
```
- Multivalue argument can be passed to `url_for`. For example:
```python
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
# /posts/5?arg_one=one&arg_one=two
```
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not supported now and will be ignored). For example:
```python
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
# /posts/5?arg_one=one#anchor
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
# //server/posts/5?arg_one=one
# _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
# http://server/posts/5?arg_one=one
# when specifying _scheme, _external must be True
# you can pass all special arguments at once
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
```
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be raised.
## WebSocket routes
Routes for the WebSocket protocol can be defined with the `@app.websocket`
decorator:
```python
@app.websocket('/feed')
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
```
Alternatively, the `app.add_websocket_route` method can be used instead of the
decorator:
```python
async def feed(request, ws):
pass
app.add_websocket_route(my_websocket_handler, '/feed')
```
Handlers to a WebSocket route are invoked with the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has `send`
and `recv` methods to send and receive data respectively.
WebSocket support requires the [websockets](https://github.com/aaugustin/websockets)
package by Aymeric Augustin.
## About `strict_slashes`
You can make `routes` strict to trailing slash or not, it's configurable.
```python
# provide default strict_slashes value for all routes
app = Sanic('test_route_strict_slash', strict_slashes=True)
# you can also overwrite strict_slashes value for specific route
@app.get('/get', strict_slashes=False)
def handler(request):
return text('OK')
# It also works for blueprints
bp = Blueprint('test_bp_strict_slash', strict_slashes=True)
@bp.get('/bp/get', strict_slashes=False)
def handler(request):
return text('OK')
app.blueprint(bp)
```
The behavior of how the `strict_slashes` flag follows a defined hierarchy which decides if a specific route
falls under the `strict_slashes` behavior.
```bash
|___ Route
|___ Blueprint
|___ Application
```
Above hierarchy defines how the `strict_slashes` flag will behave. The first non `None` value of the `strict_slashes`
found in the above order will be applied to the route in question.
```python
from sanic import Sanic, Blueprint
from sanic.response import text
app = Sanic("sample_strict_slashes", strict_slashes=True)
@app.get("/r1")
def r1(request):
return text("strict_slashes is applicable from App level")
@app.get("/r2", strict_slashes=False)
def r2(request):
return text("strict_slashes is not applicable due to False value set in route level")
bp = Blueprint("bp", strict_slashes=False)
@bp.get("/r3", strict_slashes=True)
def r3(request):
return text("strict_slashes applicable from blueprint route level")
bp1 = Blueprint("bp1", strict_slashes=True)
@bp.get("/r4")
def r3(request):
return text("strict_slashes applicable from blueprint level")
```
## User defined route name
A custom route name can be used by passing a `name` argument while registering the route which will
override the default route name generated using the `handler.__name__` attribute.
```python
app = Sanic('test_named_route')
@app.get('/get', name='get_handler')
def handler(request):
return text('OK')
# then you need use `app.url_for('get_handler')`
# instead of # `app.url_for('handler')`
# It also works for blueprints
bp = Blueprint('test_named_bp')
@bp.get('/bp/get', name='get_handler')
def handler(request):
return text('OK')
app.blueprint(bp)
# then you need use `app.url_for('test_named_bp.get_handler')`
# instead of `app.url_for('test_named_bp.handler')`
# different names can be used for same url with different methods
@app.get('/test', name='route_test')
def handler(request):
return text('OK')
@app.post('/test', name='route_post')
def handler2(request):
return text('OK POST')
@app.put('/test', name='route_put')
def handler3(request):
return text('OK PUT')
# below url are the same, you can use any of them
# '/test'
app.url_for('route_test')
# app.url_for('route_post')
# app.url_for('route_put')
# for same handler name with different methods
# you need specify the name (it's url_for issue)
@app.get('/get')
def handler(request):
return text('OK')
@app.post('/post', name='post_handler')
def handler(request):
return text('OK')
# then
# app.url_for('handler') == '/get'
# app.url_for('post_handler') == '/post'
```
## Build URL for static files
Sanic supports using `url_for` method to build static file urls. In case if the static url
is pointing to a directory, `filename` parameter to the `url_for` can be ignored. q
```python
app = Sanic('test_static')
app.static('/static', './static')
app.static('/uploads', './uploads', name='uploads')
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
bp = Blueprint('bp', url_prefix='bp')
bp.static('/static', './static')
bp.static('/uploads', './uploads', name='uploads')
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
app.blueprint(bp)
# then build the url
app.url_for('static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
app.url_for('static', name='best_png') == '/the_best.png'
# blueprint url building
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'
```

433
docs/sanic/routing.rst Normal file
View File

@@ -0,0 +1,433 @@
Routing
-------
Routing allows the user to specify handler functions for different URL endpoints.
A basic route looks like the following, where `app` is an instance of the
`Sanic` class:
.. code-block:: 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: `<PARAM>`.
Request parameters will be passed to the route handler functions as keyword
arguments.
.. code-block:: python
from sanic.response import text
@app.route('/tag/<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.
Supported types
~~~~~~~~~~~~~~~
* `string`
* "Bob"
* "Python 3"
* `int`
* 10
* 20
* 30
* -10
* (No floats work here)
* `number`
* 1
* 1.5
* 10
* -10
* `alpha`
* "Bob"
* "Python"
* (If it contains a symbol or a non alphanumeric character it will fail)
* `path`
* "hello"
* "hello.text"
* "hello world"
* `uuid`
* 123a123a-a12a-1a1a-a1a1-1a12a1a12345 (UUIDv4 Support)
* `regex expression`
If no type is set then a string is expected. The argument given to the function will always be a string, independent of the type.
.. code-block:: python
from sanic.response import text
@app.route('/string/<string_arg:string>')
async def string_handler(request, string_arg):
return text('String - {}'.format(string_arg))
@app.route('/int/<integer_arg:int>')
async def integer_handler(request, integer_arg):
return text('Integer - {}'.format(integer_arg))
@app.route('/number/<number_arg:number>')
async def number_handler(request, number_arg):
return text('Number - {}'.format(number_arg))
@app.route('/alpha/<alpha_arg:alpha>')
async def number_handler(request, alpha_arg):
return text('Alpha - {}'.format(alpha_arg))
@app.route('/path/<path_arg:path>')
async def number_handler(request, path_arg):
return text('Path - {}'.format(path_arg))
@app.route('/uuid/<uuid_arg:uuid>')
async def number_handler(request, uuid_arg):
return text('Uuid - {}'.format(uuid_arg))
@app.route('/person/<name:[A-z]+>')
async def person_handler(request, name):
return text('Person - {}'.format(name))
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id))
.. warning::
`str` is not a valid type tag. If you want `str` recognition then you must use `string`
HTTP request types
==================
By default, a route defined on a URL will be available for only GET requests to that URL.
However, the `@app.route` decorator accepts an optional parameter, `methods`,
which allows the handler function to work with any of the HTTP methods in the list.
.. code-block:: python
from sanic.response import text
@app.route('/post', methods=['POST'])
async def post_handler(request):
return text('POST request - {}'.format(request.json))
@app.route('/get', methods=['GET'])
async def get_handler(request):
return text('GET request - {}'.format(request.args))
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
.. code-block:: python
@app.route('/get', methods=['GET'], host='example.com')
async def get_handler(request):
return text('GET request - {}'.format(request.args))
# if the host header doesn't match example.com, this route will be used
@app.route('/get', methods=['GET'])
async def get_handler(request):
return text('GET request in default - {}'.format(request.args))
There are also shorthand method decorators:
.. code-block:: python
from sanic.response import text
@app.post('/post')
async def post_handler(request):
return text('POST request - {}'.format(request.json))
@app.get('/get')
async def get_handler(request):
return text('GET request - {}'.format(request.args))
The `add_route` method
======================
As we have seen, routes are often specified using the `@app.route` decorator.
However, this decorator is really just a wrapper for the `app.add_route`
method, which is used as follows:
.. code-block:: python
from sanic.response import text
# Define the handler functions
async def handler1(request):
return text('OK')
async def handler2(request, name):
return text('Folder - {}'.format(name))
async def person_handler2(request, name):
return text('Person - {}'.format(name))
# Add each handler function as a route
app.add_route(handler1, '/test')
app.add_route(handler2, '/folder/<name>')
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
URL building with `url_for`
===========================
Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example:
.. code-block:: python
from sanic.response import redirect
@app.route('/')
async def index(request):
# generate a URL for the endpoint `post_handler`
url = app.url_for('post_handler', post_id=5)
# the URL is `/posts/5`, redirect to it
return redirect(url)
@app.route('/posts/<post_id>')
async def post_handler(request, post_id):
return text('Post - {}'.format(post_id))
Other things to keep in mind when using `url_for`:
- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example:
.. code-block:: python
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
# /posts/5?arg_one=one&arg_two=two
- Multivalue argument can be passed to `url_for`. For example:
.. code-block:: python
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
# /posts/5?arg_one=one&arg_one=two
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not supported now and will be ignored). For example:
.. code-block:: python
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
# /posts/5?arg_one=one#anchor
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
# //server/posts/5?arg_one=one
# _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
# http://server/posts/5?arg_one=one
# when specifying _scheme, _external must be True
# you can pass all special arguments at once
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be raised.
WebSocket routes
================
Routes for the WebSocket protocol can be defined with the `@app.websocket`
decorator:
.. code-block:: python
@app.websocket('/feed')
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
Alternatively, the `app.add_websocket_route` method can be used instead of the
decorator:
.. code-block:: python
async def feed(request, ws):
pass
app.add_websocket_route(my_websocket_handler, '/feed')
Handlers to a WebSocket route are invoked with the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has `send`
and `recv` methods to send and receive data respectively.
WebSocket support requires the `websockets <https://github.com/aaugustin/websockets>`_
package by Aymeric Augustin.
About `strict_slashes`
======================
You can make `routes` strict to trailing slash or not, it's configurable.
.. code-block:: python
# provide default strict_slashes value for all routes
app = Sanic('test_route_strict_slash', strict_slashes=True)
# you can also overwrite strict_slashes value for specific route
@app.get('/get', strict_slashes=False)
def handler(request):
return text('OK')
# It also works for blueprints
bp = Blueprint('test_bp_strict_slash', strict_slashes=True)
@bp.get('/bp/get', strict_slashes=False)
def handler(request):
return text('OK')
app.blueprint(bp)
The behavior of how the `strict_slashes` flag follows a defined hierarchy which decides if a specific route
falls under the `strict_slashes` behavior.
| Route/
| ├──Blueprint/
| ├──Application/
Above hierarchy defines how the `strict_slashes` flag will behave. The first non `None` value of the `strict_slashes`
found in the above order will be applied to the route in question.
.. code-block:: python
from sanic import Sanic, Blueprint
from sanic.response import text
app = Sanic("sample_strict_slashes", strict_slashes=True)
@app.get("/r1")
def r1(request):
return text("strict_slashes is applicable from App level")
@app.get("/r2", strict_slashes=False)
def r2(request):
return text("strict_slashes is not applicable due to False value set in route level")
bp = Blueprint("bp", strict_slashes=False)
@bp.get("/r3", strict_slashes=True)
def r3(request):
return text("strict_slashes applicable from blueprint route level")
bp1 = Blueprint("bp1", strict_slashes=True)
@bp.get("/r4")
def r3(request):
return text("strict_slashes applicable from blueprint level")
User defined route name
=======================
A custom route name can be used by passing a `name` argument while registering the route which will
override the default route name generated using the `handler.__name__` attribute.
.. code-block:: python
app = Sanic('test_named_route')
@app.get('/get', name='get_handler')
def handler(request):
return text('OK')
# then you need use `app.url_for('get_handler')`
# instead of # `app.url_for('handler')`
# It also works for blueprints
bp = Blueprint('test_named_bp')
@bp.get('/bp/get', name='get_handler')
def handler(request):
return text('OK')
app.blueprint(bp)
# then you need use `app.url_for('test_named_bp.get_handler')`
# instead of `app.url_for('test_named_bp.handler')`
# different names can be used for same url with different methods
@app.get('/test', name='route_test')
def handler(request):
return text('OK')
@app.post('/test', name='route_post')
def handler2(request):
return text('OK POST')
@app.put('/test', name='route_put')
def handler3(request):
return text('OK PUT')
# below url are the same, you can use any of them
# '/test'
app.url_for('route_test')
# app.url_for('route_post')
# app.url_for('route_put')
# for same handler name with different methods
# you need specify the name (it's url_for issue)
@app.get('/get')
def handler(request):
return text('OK')
@app.post('/post', name='post_handler')
def handler(request):
return text('OK')
# then
# app.url_for('handler') == '/get'
# app.url_for('post_handler') == '/post'
Build URL for static files
==========================
Sanic supports using `url_for` method to build static file urls. In case if the static url
is pointing to a directory, `filename` parameter to the `url_for` can be ignored.
.. code-block:: python
app = Sanic('test_static')
app.static('/static', './static')
app.static('/uploads', './uploads', name='uploads')
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
bp = Blueprint('bp', url_prefix='bp')
bp.static('/static', './static')
bp.static('/uploads', './uploads', name='uploads')
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
app.blueprint(bp)
# then build the url
app.url_for('static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
app.url_for('static', name='best_png') == '/the_best.png'
# blueprint url building
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'

View File

@@ -16,7 +16,7 @@ IPv6 example:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('::', 7777))
app = Sanic()
app = Sanic("ipv6_example")
@app.route("/")
@@ -46,7 +46,7 @@ UNIX socket example:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_socket)
app = Sanic()
app = Sanic("unix_socket_example")
@app.route("/")

View File

@@ -1,87 +0,0 @@
# Static Files
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
from sanic.blueprints import Blueprint
app = Sanic(__name__)
# Serves files from the static folder to the URL /static
app.static('/static', './static')
# use url_for to build the url, name defaults to 'static' and can be ignored
app.url_for('static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
# is requested
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
# you can use url_for to build the static file url
# you can ignore name and filename parameters if you don't define it
app.url_for('static', name='best_png') == '/the_best.png'
app.url_for('static', name='best_png', filename='any') == '/the_best.png'
# you need define the name for other static files
app.static('/another.png', '/home/ubuntu/another.png', name='another')
app.url_for('static', name='another') == '/another.png'
app.url_for('static', name='another', filename='any') == '/another.png'
# also, you can use static for blueprint
bp = Blueprint('bp', url_prefix='/bp')
bp.static('/static', './static')
# specify a different content_type for your files
# such as adding 'charset'
app.static('/', '/public/index.html', content_type="text/html; charset=utf-8")
# servers the file directly
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
app.blueprint(bp)
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
app.run(host="0.0.0.0", port=8000)
```
> **Note:** Sanic does not provide directory index when you serve a static directory.
## Virtual Host
The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example:
```python
from sanic import Sanic
app = Sanic(__name__)
app.static('/static', './static')
app.static('/example_static', './example_static', host='www.example.com')
```
## Streaming Large File
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
Here is an example:
```python
from sanic import Sanic
app = Sanic(__name__)
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
```
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
```python
from sanic import Sanic
app = Sanic(__name__)
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
```

View File

@@ -0,0 +1,92 @@
Static Files
============
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.
.. code-block:: python
from sanic import Sanic
from sanic.blueprints import Blueprint
app = Sanic(__name__)
# Serves files from the static folder to the URL /static
app.static('/static', './static')
# use url_for to build the url, name defaults to 'static' and can be ignored
app.url_for('static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
# is requested
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
# you can use url_for to build the static file url
# you can ignore name and filename parameters if you don't define it
app.url_for('static', name='best_png') == '/the_best.png'
app.url_for('static', name='best_png', filename='any') == '/the_best.png'
# you need define the name for other static files
app.static('/another.png', '/home/ubuntu/another.png', name='another')
app.url_for('static', name='another') == '/another.png'
app.url_for('static', name='another', filename='any') == '/another.png'
# also, you can use static for blueprint
bp = Blueprint('bp', url_prefix='/bp')
bp.static('/static', './static')
# specify a different content_type for your files
# such as adding 'charset'
app.static('/', '/public/index.html', content_type="text/html; charset=utf-8")
# servers the file directly
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
app.blueprint(bp)
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
app.run(host="0.0.0.0", port=8000)
> **Note:** Sanic does not provide directory index when you serve a static directory.
Virtual Host
------------
The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example:
.. code-block:: python
from sanic import Sanic
app = Sanic(__name__)
app.static('/static', './static')
app.static('/example_static', './example_static', host='www.example.com')
Streaming Large File
--------------------
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
Here is an example:
.. code-block:: python
from sanic import Sanic
app = Sanic(__name__)
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
.. code-block:: python
from sanic import Sanic
app = Sanic(__name__)
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)

View File

@@ -1,143 +0,0 @@
# Streaming
## Request Streaming
Sanic allows you to get request data by stream, as below. When the request ends, `await request.stream.read()` returns `None`. Only post, put and patch decorator have stream argument.
```python
from sanic import Sanic
from sanic.views import CompositionView
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
from sanic.blueprints import Blueprint
from sanic.response import stream, text
bp = Blueprint('blueprint_request_stream')
app = Sanic('request_stream')
class SimpleView(HTTPMethodView):
@stream_decorator
async def post(self, request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
return text(result)
@app.post('/stream', stream=True)
async def handler(request):
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
await response.write(body)
return stream(streaming)
@bp.put('/bp_stream', stream=True)
async def bp_put_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
# You can also use `bp.add_route()` with stream argument
async def bp_post_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
bp.add_route(bp_post_handler, '/bp_stream', methods=['POST'], stream=True)
async def post_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
return text(result)
app.blueprint(bp)
app.add_route(SimpleView.as_view(), '/method_view')
view = CompositionView()
view.add(['POST'], post_handler, stream=True)
app.add_route(view, '/composition_view')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000)
```
## Response Streaming
Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows:
```python
from sanic import Sanic
from sanic.response import stream
app = Sanic(__name__)
@app.route("/")
async def test(request):
async def sample_streaming_fn(response):
await response.write('foo,')
await response.write('bar')
return stream(sample_streaming_fn, content_type='text/csv')
```
This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides:
```python
@app.route("/")
async def index(request):
async def stream_from_db(response):
conn = await asyncpg.connect(database='test')
async with conn.transaction():
async for record in conn.cursor('SELECT generate_series(0, 10)'):
await response.write(record[0])
return stream(stream_from_db)
```
If a client supports HTTP/1.1, Sanic will use [chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding); you can explicitly enable or disable it using `chunked` option of the `stream` function.
## File Streaming
Sanic provides `sanic.response.file_stream` function that is useful when you want to send a large file. It returns a `StreamingHTTPResponse` object and will use chunked transfer encoding by default; for this reason Sanic doesn't add `Content-Length` HTTP header in the response. If you want to use this header, you can disable chunked transfer encoding and add it manually:
```python
from aiofiles import os as async_os
from sanic.response import file_stream
@app.route("/")
async def index(request):
file_path = "/srv/www/whatever.png"
file_stat = await async_os.stat(file_path)
headers = {"Content-Length": str(file_stat.st_size)}
return await file_stream(
file_path,
headers=headers,
chunked=False,
)
```

147
docs/sanic/streaming.rst Normal file
View File

@@ -0,0 +1,147 @@
Streaming
=========
Request Streaming
-----------------
Sanic allows you to get request data by stream, as below. When the request ends, `await request.stream.read()` returns `None`. Only post, put and patch decorator have stream argument.
.. code-block:: python
from sanic import Sanic
from sanic.views import CompositionView
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
from sanic.blueprints import Blueprint
from sanic.response import stream, text
bp = Blueprint('blueprint_request_stream')
app = Sanic(__name__)
class SimpleView(HTTPMethodView):
@stream_decorator
async def post(self, request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
return text(result)
@app.post('/stream', stream=True)
async def handler(request):
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
await response.write(body)
return stream(streaming)
@bp.put('/bp_stream', stream=True)
async def bp_put_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
# You can also use `bp.add_route()` with stream argument
async def bp_post_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
bp.add_route(bp_post_handler, '/bp_stream', methods=['POST'], stream=True)
async def post_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
return text(result)
app.blueprint(bp)
app.add_route(SimpleView.as_view(), '/method_view')
view = CompositionView()
view.add(['POST'], post_handler, stream=True)
app.add_route(view, '/composition_view')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000)
Response Streaming
------------------
Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows:
.. code-block:: python
from sanic import Sanic
from sanic.response import stream
app = Sanic(__name__)
@app.route("/")
async def test(request):
async def sample_streaming_fn(response):
await response.write('foo,')
await response.write('bar')
return stream(sample_streaming_fn, content_type='text/csv')
This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides:
.. code-block:: python
@app.route("/")
async def index(request):
async def stream_from_db(response):
conn = await asyncpg.connect(database='test')
async with conn.transaction():
async for record in conn.cursor('SELECT generate_series(0, 10)'):
await response.write(record[0])
return stream(stream_from_db)
If a client supports HTTP/1.1, Sanic will use `chunked transfer encoding <https://en.wikipedia.org/wiki/Chunked_transfer_encoding>`_; you can explicitly enable or disable it using `chunked` option of the `stream` function.
File Streaming
--------------
Sanic provides `sanic.response.file_stream` function that is useful when you want to send a large file. It returns a `StreamingHTTPResponse` object and will use chunked transfer encoding by default; for this reason Sanic doesn't add `Content-Length` HTTP header in the response. If you want to use this header, you can disable chunked transfer encoding and add it manually:
.. code-block:: python
from aiofiles import os as async_os
from sanic.response import file_stream
@app.route("/")
async def index(request):
file_path = "/srv/www/whatever.png"
file_stat = await async_os.stat(file_path)
headers = {"Content-Length": str(file_stat.st_size)}
return await file_stream(
file_path,
headers=headers,
chunked=False,
)

View File

@@ -1,144 +0,0 @@
# Testing
Sanic endpoints can be tested locally using the `test_client` object, which
depends on the additional [`requests-async`](https://github.com/encode/requests-async)
library, which implements an API that mirrors the `requests` library.
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
for you to run against your application. A simple example (using pytest) is like follows:
```python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
def test_index_returns_200():
request, response = app.test_client.get('/')
assert response.status == 200
def test_index_put_not_allowed():
request, response = app.test_client.put('/')
assert response.status == 405
```
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
your test request is executed against your application, using `requests-async`.
The `test_client` methods accept the following arguments and keyword arguments:
- `uri` *(default `'/'`)* A string representing the URI to test.
- `gather_request` *(default `True`)* A boolean which determines whether the
original request will be returned by the function. If set to `True`, the
return value is a tuple of `(request, response)`, if `False` only the
response is returned.
- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the request.
For example, to supply data to a GET request, you would do the following:
```python
def test_get_request_includes_data():
params = {'key1': 'value1', 'key2': 'value2'}
request, response = app.test_client.get('/', params=params)
assert request.args.get('key1') == 'value1'
```
And to supply data to a JSON POST request:
```python
def test_post_json_request_includes_data():
data = {'key1': 'value1', 'key2': 'value2'}
request, response = app.test_client.post('/', data=json.dumps(data))
assert request.json.get('key1') == 'value1'
```
More information about
the available arguments to `requests-async` can be found
[in the documentation for `requests`](https://2.python-requests.org/en/master/).
## Using a random port
If you need to test using a free unpriveleged port chosen by the kernel
instead of the default with `SanicTestClient`, you can do so by specifying
`port=None`. On most systems the port will be in the range 1024 to 65535.
```python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
from sanic.testing import SanicTestClient
def test_index_returns_200():
request, response = SanicTestClient(app, port=None).get('/')
assert response.status == 200
```
## pytest-sanic
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously.
Just write tests like,
```python
async def test_sanic_db_find_by_id(app):
"""
Let's assume that, in db we have,
{
"id": "123",
"name": "Kobe Bryant",
"team": "Lakers",
}
"""
doc = await app.db["players"].find_by_id("123")
assert doc.name == "Kobe Bryant"
assert doc.team == "Lakers"
```
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) also provides some useful fixtures, like loop, unused_port,
test_server, test_client.
```python
@pytest.yield_fixture
def app():
app = Sanic("test_sanic_app")
@app.route("/test_get", methods=['GET'])
async def test_get(request):
return response.json({"GET": True})
@app.route("/test_post", methods=['POST'])
async def test_post(request):
return response.json({"POST": True})
yield app
@pytest.fixture
def test_cli(loop, app, test_client):
return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol))
#########
# Tests #
#########
async def test_fixture_test_client_get(test_cli):
"""
GET request
"""
resp = await test_cli.get('/test_get')
assert resp.status == 200
resp_json = await resp.json()
assert resp_json == {"GET": True}
async def test_fixture_test_client_post(test_cli):
"""
POST request
"""
resp = await test_cli.post('/test_post')
assert resp.status == 200
resp_json = await resp.json()
assert resp_json == {"POST": True}
```

145
docs/sanic/testing.rst Normal file
View File

@@ -0,0 +1,145 @@
Testing
=======
Sanic endpoints can be tested locally using the `test_client` object, which
depends on an additional package: `httpx <https://www.encode.io/httpx/>`_
library, which implements an API that mirrors the `requests` library.
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
for you to run against your application. A simple example (using pytest) is like follows:
.. code-block:: python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
def test_index_returns_200():
request, response = app.test_client.get('/')
assert response.status == 200
def test_index_put_not_allowed():
request, response = app.test_client.put('/')
assert response.status == 405
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
your test request is executed against your application, using `httpx`.
The `test_client` methods accept the following arguments and keyword arguments:
- `uri` *(default `'/'`)* A string representing the URI to test.
- `gather_request` *(default `True`)* A boolean which determines whether the
original request will be returned by the function. If set to `True`, the
return value is a tuple of `(request, response)`, if `False` only the
response is returned.
- `server_kwargs` *(default `{}`)* a dict of additional arguments to pass into `app.run` before the test request is run.
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the request.
For example, to supply data to a GET request, you would do the following:
.. code-block:: python
def test_get_request_includes_data():
params = {'key1': 'value1', 'key2': 'value2'}
request, response = app.test_client.get('/', params=params)
assert request.args.get('key1') == 'value1'
And to supply data to a JSON POST request:
.. code-block:: python
def test_post_json_request_includes_data():
data = {'key1': 'value1', 'key2': 'value2'}
request, response = app.test_client.post('/', data=json.dumps(data))
assert request.json.get('key1') == 'value1'
More information about
the available arguments to `httpx` can be found
[in the documentation for `httpx <https://www.encode.io/httpx/>`_.
Using a random port
-------------------
If you need to test using a free unpriveleged port chosen by the kernel
instead of the default with `SanicTestClient`, you can do so by specifying
`port=None`. On most systems the port will be in the range 1024 to 65535.
.. code-block:: python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
from sanic.testing import SanicTestClient
def test_index_returns_200():
request, response = SanicTestClient(app, port=None).get('/')
assert response.status == 200
pytest-sanic
------------
`pytest-sanic <https://github.com/yunstanford/pytest-sanic>`_ is a pytest plugin, it helps you to test your code asynchronously.
Just write tests like,
.. code-block:: python
async def test_sanic_db_find_by_id(app):
"""
Let's assume that, in db we have,
{
"id": "123",
"name": "Kobe Bryant",
"team": "Lakers",
}
"""
doc = await app.db["players"].find_by_id("123")
assert doc.name == "Kobe Bryant"
assert doc.team == "Lakers"
`pytest-sanic <https://github.com/yunstanford/pytest-sanic>`_ also provides some useful fixtures, like loop, unused_port,
test_server, test_client.
.. code-block:: python
@pytest.yield_fixture
def app():
app = Sanic("test_sanic_app")
@app.route("/test_get", methods=['GET'])
async def test_get(request):
return response.json({"GET": True})
@app.route("/test_post", methods=['POST'])
async def test_post(request):
return response.json({"POST": True})
yield app
@pytest.fixture
def test_cli(loop, app, test_client):
return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol))
#########
# Tests #
#########
async def test_fixture_test_client_get(test_cli):
"""
GET request
"""
resp = await test_cli.get('/test_get')
assert resp.status == 200
resp_json = await resp.json()
assert resp_json == {"GET": True}
async def test_fixture_test_client_post(test_cli):
"""
POST request
"""
resp = await test_cli.post('/test_post')
assert resp.status == 200
resp_json = await resp.json()
assert resp_json == {"POST": True}

View File

@@ -1,50 +0,0 @@
# Versioning
You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number.
## Per route
You can pass a version number to the routes directly.
```python
from sanic import response
@app.route('/text', version=1)
def handle_request(request):
return response.text('Hello world! Version 1')
@app.route('/text', version=2)
def handle_request(request):
return response.text('Hello world! Version 2')
app.run(port=80)
```
Then with curl:
```bash
curl localhost/v1/text
curl localhost/v2/text
```
## Global blueprint version
You can also pass a version number to the blueprint, which will apply to all routes.
```python
from sanic import response
from sanic.blueprints import Blueprint
bp = Blueprint('test', version=1)
@bp.route('/html')
def handle_request(request):
return response.html('<p>Hello world!</p>')
```
Then with curl:
```bash
curl localhost/v1/html
```

54
docs/sanic/versioning.rst Normal file
View File

@@ -0,0 +1,54 @@
Versioning
==========
You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number.
Per route
---------
You can pass a version number to the routes directly.
.. code-block:: python
from sanic import response
@app.route('/text', version=1)
def handle_request(request):
return response.text('Hello world! Version 1')
@app.route('/text', version=2)
def handle_request(request):
return response.text('Hello world! Version 2')
app.run(port=80)
Then with curl:
.. code-block:: bash
curl localhost/v1/text
curl localhost/v2/text
Global blueprint version
------------------------
You can also pass a version number to the blueprint, which will apply to all routes.
.. code-block:: python
from sanic import response
from sanic.blueprints import Blueprint
bp = Blueprint('test', version=1)
@bp.route('/html')
def handle_request(request):
return response.html('<p>Hello world!</p>')
Then with curl:
.. code-block:: bash
curl localhost/v1/html

View File

@@ -12,7 +12,7 @@ To setup a WebSocket:
from sanic.response import json
from sanic.websocket import WebSocketProtocol
app = Sanic()
app = Sanic("websocket_example")
@app.websocket('/feed')
async def feed(request, ws):

View File

@@ -1,19 +0,0 @@
name: py36
dependencies:
- pip=18.1=py36_0
- python=3.6=0
- setuptools=40.4.3=py36_0
- pip:
- httptools>=0.0.10
- uvloop>=0.5.3
- ujson>=1.35
- aiofiles>=0.3.0
- websockets>=6.0,<7.0
- multidict>=4.0,<5.0
- sphinx==1.8.3
- sphinx_rtd_theme==0.4.2
- recommonmark==0.5.0
- requests-async==0.5.0
- sphinxcontrib-asyncio>=0.2.0
- docutils==0.14
- pygments==2.3.1

View File

@@ -13,28 +13,26 @@ def check_request_for_authorization_status(request):
return flag
def authorized():
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
# run some method that checks the request
# for the client's authorization status
is_authorized = check_request_for_authorization_status(request)
def authorized(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
# run some method that checks the request
# for the client's authorization status
is_authorized = check_request_for_authorization_status(request)
if is_authorized:
# the user is authorized.
# run the handler method and return the response
response = await f(request, *args, **kwargs)
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return decorated_function
return decorator
if is_authorized:
# the user is authorized.
# run the handler method and return the response
response = await f(request, *args, **kwargs)
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return decorated_function
@app.route("/")
@authorized()
@authorized
async def test(request):
return json({'status': 'authorized'})

View File

@@ -0,0 +1,43 @@
from sanic import Sanic, Blueprint
from sanic.response import text
'''
Demonstrates that blueprint request middleware are executed in the order they
are added. And blueprint response middleware are executed in _reverse_ order.
On a valid request, it should print "1 2 3 6 5 4" to terminal
'''
app = Sanic(__name__)
bp = Blueprint("bp_"+__name__)
@bp.middleware('request')
def request_middleware_1(request):
print('1')
@bp.middleware('request')
def request_middleware_2(request):
print('2')
@bp.middleware('request')
def request_middleware_3(request):
print('3')
@bp.middleware('response')
def resp_middleware_4(request, response):
print('4')
@bp.middleware('response')
def resp_middleware_5(request, response):
print('5')
@bp.middleware('response')
def resp_middleware_6(request, response):
print('6')
@bp.route('/')
def pop_handler(request):
return text('hello world')
app.blueprint(bp, url_prefix='/bp')
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)

View File

@@ -1,2 +1,9 @@
conda:
file: environment.yml
version: 2
python:
version: 3.8
install:
- method: pip
path: .
extra_requirements:
- docs
system_packages: true

View File

@@ -28,12 +28,12 @@ if __name__ == "__main__":
module = import_module(module_name)
app = getattr(module, app_name, None)
app_name = type(app).__name__
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
)
f"Module is not a Sanic app, it is a {app_name}. "
f"Perhaps you meant {args.module}.app?"
)
if args.cert is not None or args.key is not None:
ssl = {
@@ -52,9 +52,9 @@ if __name__ == "__main__":
)
except ImportError as e:
logger.error(
"No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app".format(e.name)
f"No module named {e.name} found.\n"
f" Example File: project/sanic_server.py -> app\n"
f" Example Module: project.sanic_server.app"
)
except ValueError:
logger.exception("Failed to run app")

View File

@@ -1 +1 @@
__version__ = "19.9.0"
__version__ = "20.3.0"

View File

@@ -24,7 +24,13 @@ from sanic.handlers import ErrorHandler
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.router import Router
from sanic.server import HttpProtocol, Signal, serve, serve_multiple
from sanic.server import (
AsyncioServer,
HttpProtocol,
Signal,
serve,
serve_multiple,
)
from sanic.static import register as static_register
from sanic.testing import SanicASGITestClient, SanicTestClient
from sanic.views import CompositionView
@@ -46,6 +52,13 @@ class Sanic:
# Get name from previous stack frame
if name is None:
warnings.warn(
"Sanic(name=None) is deprecated and None value support "
"for `name` will be removed in the next release. "
"Please use Sanic(name='your_application_name') instead.",
DeprecationWarning,
stacklevel=2,
)
frame_records = stack()[1]
name = getmodulename(frame_records[1])
@@ -68,11 +81,13 @@ class Sanic:
self.sock = None
self.strict_slashes = strict_slashes
self.listeners = defaultdict(list)
self.is_stopping = False
self.is_running = False
self.is_request_stream = False
self.websocket_enabled = False
self.websocket_tasks = set()
self.named_request_middleware = {}
self.named_response_middleware = {}
# Register alternative method names
self.go_fast = self.run
@@ -165,7 +180,7 @@ class Sanic:
:param stream:
:param version:
:param name: user defined route name for url_for
:return: decorated function
:return: tuple of routes, decorated function
"""
# Fix case where the user did not prefix the URL with a /
@@ -180,27 +195,37 @@ class Sanic:
strict_slashes = self.strict_slashes
def response(handler):
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn)
routes, handler = handler
else:
routes = []
args = list(signature(handler).parameters.keys())
if not args:
handler_name = handler.__name__
raise ValueError(
"Required parameter `request` missing "
"in the {0}() route?".format(handler.__name__)
f"Required parameter `request` missing "
f"in the {handler_name}() route?"
)
if stream:
handler.is_stream = stream
self.router.add(
uri=uri,
methods=methods,
handler=handler,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
routes.extend(
self.router.add(
uri=uri,
methods=methods,
handler=handler,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
)
return handler
return routes, handler
return response
@@ -449,7 +474,7 @@ class Sanic:
:param subprotocols: optional list of str with supported subprotocols
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:return: decorated function
:return: tuple of routes, decorated function
"""
self.enable_websocket()
@@ -462,6 +487,13 @@ class Sanic:
strict_slashes = self.strict_slashes
def response(handler):
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn)
routes, handler = handler
else:
routes = []
async def websocket_handler(request, *args, **kwargs):
request.app = self
if not getattr(handler, "__blueprintname__", False):
@@ -477,12 +509,7 @@ class Sanic:
if self.asgi:
ws = request.transport.get_websocket_connection()
else:
try:
protocol = request.transport.get_protocol()
except AttributeError:
# On Python3.5 the Transport classes in asyncio do not
# have a get_protocol() method as in uvloop
protocol = request.transport._protocol
protocol = request.transport.get_protocol()
protocol.app = self
ws = await protocol.websocket_handshake(
@@ -502,15 +529,17 @@ class Sanic:
self.websocket_tasks.remove(fut)
await ws.close()
self.router.add(
uri=uri,
handler=websocket_handler,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
name=name,
routes.extend(
self.router.add(
uri=uri,
handler=websocket_handler,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
name=name,
)
)
return handler
return routes, handler
return response
@@ -531,6 +560,7 @@ class Sanic:
:param host: Host IP or FQDN details
:param uri: URL path that will be mapped to the websocket
handler
handler
:param strict_slashes: If the API endpoint needs to terminate
with a "/" or not
:param subprotocols: Subprotocols to be used with websocket
@@ -566,29 +596,6 @@ class Sanic:
self.websocket_enabled = enable
def remove_route(self, uri, clean_cache=True, host=None):
"""
This method provides the app user a mechanism by which an already
existing route can be removed from the :class:`Sanic` object
.. warning::
remove_route is deprecated in v19.06 and will be removed
from future versions.
:param uri: URL Path to be removed from the app
:param clean_cache: Instruct sanic if it needs to clean up the LRU
route cache
:param host: IP address or FQDN specific to the host
:return: None
"""
warnings.warn(
"remove_route is deprecated and will be removed "
"from future versions.",
DeprecationWarning,
stacklevel=2,
)
self.router.remove(uri, clean_cache, host)
# Decorator
def exception(self, *exceptions):
"""Decorate a function to be registered as a handler for exceptions
@@ -632,6 +639,22 @@ class Sanic:
self.response_middleware.appendleft(middleware)
return middleware
def register_named_middleware(
self, middleware, route_names, attach_to="request"
):
if attach_to == "request":
for _rn in route_names:
if _rn not in self.named_request_middleware:
self.named_request_middleware[_rn] = deque()
if middleware not in self.named_request_middleware[_rn]:
self.named_request_middleware[_rn].append(middleware)
if attach_to == "response":
for _rn in route_names:
if _rn not in self.named_response_middleware:
self.named_response_middleware[_rn] = deque()
if middleware not in self.named_response_middleware[_rn]:
self.named_response_middleware[_rn].appendleft(middleware)
# Decorator
def middleware(self, middleware_or_request):
"""
@@ -779,9 +802,17 @@ class Sanic:
uri, route = self.router.find_route_by_view_name(view_name, **kw)
if not (uri and route):
raise URLBuildError(
"Endpoint with name `{}` was not found".format(view_name)
f"Endpoint with name `{view_name}` was not found"
)
# If the route has host defined, split that off
# TODO: Retain netloc and path separately in Route objects
host = uri.find("/")
if host > 0:
host, uri = uri[:host], uri[host:]
else:
host = None
if view_name == "static" or view_name.endswith(".static"):
filename = kwargs.pop("filename", None)
# it's static folder
@@ -793,7 +824,7 @@ class Sanic:
if filename.startswith("/"):
filename = filename[1:]
uri = "{}/{}".format(folder_, filename)
uri = f"{folder_}/{filename}"
if uri != "/" and uri.endswith("/"):
uri = uri[:-1]
@@ -814,7 +845,7 @@ class Sanic:
netloc = kwargs.pop("_server", None)
if netloc is None and external:
netloc = self.config.get("SERVER_NAME", "")
netloc = host or self.config.get("SERVER_NAME", "")
if external:
if not scheme:
@@ -829,7 +860,7 @@ class Sanic:
for match in matched_params:
name, _type, pattern = self.router.parse_parameter_string(match)
# we only want to match against each individual parameter
specific_pattern = "^{}$".format(pattern)
specific_pattern = f"^{pattern}$"
supplied_param = None
if name in kwargs:
@@ -837,9 +868,7 @@ class Sanic:
del kwargs[name]
else:
raise URLBuildError(
"Required parameter `{}` was not passed to url_for".format(
name
)
f"Required parameter `{name}` was not passed to url_for"
)
supplied_param = str(supplied_param)
@@ -849,23 +878,22 @@ class Sanic:
if not passes_pattern:
if _type != str:
type_name = _type.__name__
msg = (
'Value "{}" for parameter `{}` does not '
"match pattern for type `{}`: {}".format(
supplied_param, name, _type.__name__, pattern
)
f'Value "{supplied_param}" '
f"for parameter `{name}` does not "
f"match pattern for type `{type_name}`: {pattern}"
)
else:
msg = (
'Value "{}" for parameter `{}` '
"does not satisfy pattern {}".format(
supplied_param, name, pattern
)
f'Value "{supplied_param}" for parameter `{name}` '
f"does not satisfy pattern {pattern}"
)
raise URLBuildError(msg)
# replace the parameter in the URL with the supplied value
replacement_regex = "(<{}.*?>)".format(name)
replacement_regex = f"(<{name}.*?>)"
out = re.sub(replacement_regex, supplied_param, out)
@@ -903,20 +931,23 @@ class Sanic:
# allocation before assignment below.
response = None
cancelled = False
name = None
try:
# Fetch handler from router
handler, args, kwargs, uri, name = self.router.get(request)
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
response = await self._run_request_middleware(request)
response = await self._run_request_middleware(
request, request_name=name
)
# No middleware results
if not response:
# -------------------------------------------- #
# Execute Handler
# -------------------------------------------- #
# Fetch handler from router
handler, args, kwargs, uri = self.router.get(request)
request.uri_template = uri
if handler is None:
raise ServerError(
@@ -963,9 +994,8 @@ class Sanic:
)
elif self.debug:
response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format(
e, format_exc()
),
f"Error while "
f"handling error: {e}\nStack: {format_exc()}",
status=500,
)
else:
@@ -980,7 +1010,7 @@ class Sanic:
if response is not None:
try:
response = await self._run_response_middleware(
request, response
request, response, request_name=name
)
except CancelledError:
# Response middleware can timeout too, as above.
@@ -1037,7 +1067,7 @@ class Sanic:
stop_event: Any = None,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
**kwargs: Any
**kwargs: Any,
) -> None:
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
@@ -1118,6 +1148,13 @@ class Sanic:
try:
self.is_running = True
self.is_stopping = False
if workers > 1 and os.name != "posix":
logger.warn(
f"Multiprocessing is currently not supported on {os.name},"
" using workers=1 instead"
)
workers = 1
if workers == 1:
if auto_reload and os.name != "posix":
# This condition must be removed after implementing
@@ -1144,7 +1181,9 @@ class Sanic:
def stop(self):
"""This kills the Sanic"""
get_event_loop().stop()
if not self.is_stopping:
self.is_stopping = True
get_event_loop().stop()
async def create_server(
self,
@@ -1159,7 +1198,7 @@ class Sanic:
access_log: Optional[bool] = None,
return_asyncio_server=False,
asyncio_server_kwargs=None,
) -> None:
) -> Optional[AsyncioServer]:
"""
Asynchronous version of :func:`run`.
@@ -1199,7 +1238,7 @@ class Sanic:
:param asyncio_server_kwargs: key-value arguments for
asyncio/uvloop create_server method
:type asyncio_server_kwargs: dict
:return: Nothing
:return: AsyncioServer if return_asyncio_server is true, else Nothing
"""
if sock is None:
@@ -1252,10 +1291,14 @@ class Sanic:
if isawaitable(result):
await result
async def _run_request_middleware(self, request):
async def _run_request_middleware(self, request, request_name=None):
# The if improves speed. I don't know why
if self.request_middleware:
for middleware in self.request_middleware:
named_middleware = self.named_request_middleware.get(
request_name, deque()
)
applicable_middleware = self.request_middleware + named_middleware
if applicable_middleware:
for middleware in applicable_middleware:
response = middleware(request)
if isawaitable(response):
response = await response
@@ -1263,9 +1306,15 @@ class Sanic:
return response
return None
async def _run_response_middleware(self, request, response):
if self.response_middleware:
for middleware in self.response_middleware:
async def _run_response_middleware(
self, request, response, request_name=None
):
named_middleware = self.named_response_middleware.get(
request_name, deque()
)
applicable_middleware = self.response_middleware + named_middleware
if applicable_middleware:
for middleware in applicable_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
@@ -1319,33 +1368,15 @@ class Sanic:
server_settings = {
"protocol": protocol,
"request_class": self.request_class,
"is_request_stream": self.is_request_stream,
"router": self.router,
"host": host,
"port": port,
"sock": sock,
"ssl": ssl,
"app": self,
"signal": Signal(),
"debug": debug,
"request_handler": self.handle_request,
"error_handler": self.error_handler,
"request_timeout": self.config.REQUEST_TIMEOUT,
"response_timeout": self.config.RESPONSE_TIMEOUT,
"keep_alive_timeout": self.config.KEEP_ALIVE_TIMEOUT,
"request_max_size": self.config.REQUEST_MAX_SIZE,
"request_buffer_queue_size": self.config.REQUEST_BUFFER_QUEUE_SIZE,
"keep_alive": self.config.KEEP_ALIVE,
"loop": loop,
"register_sys_signals": register_sys_signals,
"backlog": backlog,
"access_log": self.config.ACCESS_LOG,
"websocket_max_size": self.config.WEBSOCKET_MAX_SIZE,
"websocket_max_queue": self.config.WEBSOCKET_MAX_QUEUE,
"websocket_read_limit": self.config.WEBSOCKET_READ_LIMIT,
"websocket_write_limit": self.config.WEBSOCKET_WRITE_LIMIT,
"graceful_shutdown_timeout": self.config.GRACEFUL_SHUTDOWN_TIMEOUT,
}
# -------------------------------------------- #
@@ -1386,7 +1417,7 @@ class Sanic:
proto = "http"
if ssl is not None:
proto = "https"
logger.info("Goin' Fast @ {}://{}:{}".format(proto, host, port))
logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
return server_settings

View File

@@ -15,8 +15,6 @@ from typing import (
)
from urllib.parse import quote
from requests_async import ASGISession # type: ignore
import sanic.app # noqa
from sanic.compat import Header
@@ -189,7 +187,7 @@ class Lifespan:
class ASGIApp:
sanic_app: Union[ASGISession, "sanic.app.Sanic"]
sanic_app: "sanic.app.Sanic"
request: Request
transport: MockTransport
do_stream: bool
@@ -223,8 +221,13 @@ class ASGIApp:
if scope["type"] == "lifespan":
await instance.lifespan(scope, receive, send)
else:
url_bytes = scope.get("root_path", "") + quote(scope["path"])
url_bytes = url_bytes.encode("latin-1")
path = (
scope["path"][1:]
if scope["path"].startswith("/")
else scope["path"]
)
url = "/".join([scope.get("root_path", ""), quote(path)])
url_bytes = url.encode("latin-1")
url_bytes += b"?" + scope["query_string"]
if scope["type"] == "http":

View File

@@ -104,6 +104,8 @@ class Blueprint:
url_prefix = options.get("url_prefix", self.url_prefix)
routes = []
# Routes
for future in self.routes:
# attach the blueprint name to the handler so that it can be
@@ -114,7 +116,7 @@ class Blueprint:
version = future.version or self.version
app.route(
_routes, _ = app.route(
uri=uri[1:] if uri.startswith("//") else uri,
methods=future.methods,
host=future.host or self.host,
@@ -123,6 +125,8 @@ class Blueprint:
version=version,
name=future.name,
)(future.handler)
if _routes:
routes += _routes
for future in self.websocket_routes:
# attach the blueprint name to the handler so that it can be
@@ -130,21 +134,27 @@ class Blueprint:
future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
app.websocket(
_routes, _ = app.websocket(
uri=uri,
host=future.host or self.host,
strict_slashes=future.strict_slashes,
name=future.name,
)(future.handler)
if _routes:
routes += _routes
route_names = [route.name for route in routes]
# Middleware
for future in self.middlewares:
if future.args or future.kwargs:
app.register_middleware(
future.middleware, *future.args, **future.kwargs
app.register_named_middleware(
future.middleware,
route_names,
*future.args,
**future.kwargs,
)
else:
app.register_middleware(future.middleware)
app.register_named_middleware(future.middleware, route_names)
# Exceptions
for future in self.exceptions:
@@ -366,7 +376,7 @@ class Blueprint:
"""
name = kwargs.pop("name", "static")
if not name.startswith(self.name + "."):
name = "{}.{}".format(self.name, name)
name = f"{self.name}.{name}"
kwargs.update(name=name)
strict_slashes = kwargs.get("strict_slashes")

View File

@@ -1,6 +1,52 @@
import asyncio
import signal
from sys import argv
from multidict import CIMultiDict # type: ignore
class Header(CIMultiDict):
def get_all(self, key):
return self.getall(key, default=[])
use_trio = argv[0].endswith("hypercorn") and "trio" in argv
if use_trio:
from trio import open_file as open_async, Path # type: ignore
def stat_async(path):
return Path(path).stat()
else:
from aiofiles import open as aio_open # type: ignore
from aiofiles.os import stat as stat_async # type: ignore # noqa: F401
async def open_async(file, mode="r", **kwargs):
return aio_open(file, mode, **kwargs)
def ctrlc_workaround_for_windows(app):
async def stay_active(app):
"""Asyncio wakeups to allow receiving SIGINT in Python"""
while not die:
# If someone else stopped the app, just exit
if app.is_stopping:
return
# Windows Python blocks signal handlers while the event loop is
# waiting for I/O. Frequent wakeups keep interrupts flowing.
await asyncio.sleep(0.1)
# Can't be called from signal handler, so call it from here
app.stop()
def ctrlc_handler(sig, frame):
nonlocal die
if die:
raise KeyboardInterrupt("Non-graceful Ctrl+C")
die = True
die = False
signal.signal(signal.SIGINT, ctrlc_handler)
app.add_task(stay_active)

View File

@@ -20,7 +20,7 @@ DEFAULT_CONFIG = {
"RESPONSE_TIMEOUT": 60, # 60 seconds
"KEEP_ALIVE": True,
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabytes
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
@@ -51,7 +51,7 @@ class Config(dict):
try:
return self[attr]
except KeyError as ke:
raise AttributeError("Config has no '{}'".format(ke.args[0]))
raise AttributeError(f"Config has no '{ke.args[0]}'")
def __setattr__(self, attr, value):
self[attr] = value

View File

@@ -130,6 +130,10 @@ class Cookie(dict):
:return: Cookie encoded in a codec of choosing.
:except: UnicodeEncodeError
"""
return str(self).encode(encoding)
def __str__(self):
"""Format as a Set-Cookie header value."""
output = ["%s=%s" % (self.key, _quote(self.value))]
for key, value in self.items():
if key == "max-age":
@@ -147,4 +151,4 @@ class Cookie(dict):
else:
output.append("%s=%s" % (self._keys[key], value))
return "; ".join(output).encode(encoding)
return "; ".join(output)

117
sanic/errorpages.py Normal file
View File

@@ -0,0 +1,117 @@
import sys
from traceback import extract_tb
from sanic.exceptions import SanicException
from sanic.helpers import STATUS_CODES
from sanic.response import html
# Here, There Be Dragons (custom HTML formatting to follow)
def escape(text):
"""Minimal HTML escaping, not for attribute values (unlike html.escape)."""
return f"{text}".replace("&", "&amp;").replace("<", "&lt;")
def exception_response(request, exception, debug):
status = 500
text = (
"The server encountered an internal error "
"and cannot complete your request."
)
headers = {}
if isinstance(exception, SanicException):
text = f"{exception}"
status = getattr(exception, "status_code", status)
headers = getattr(exception, "headers", headers)
elif debug:
text = f"{exception}"
status_text = STATUS_CODES.get(status, b"Error Occurred").decode()
title = escape(f"{status}{status_text}")
text = escape(text)
if debug and not getattr(exception, "quiet", False):
return html(
f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>"
f"<style>{TRACEBACK_STYLE}</style>\n"
f"<h1>⚠️ {title}</h1><p>{text}\n"
f"{_render_traceback_html(request, exception)}",
status=status,
)
# Keeping it minimal with trailing newline for pretty curl/console output
return html(
f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>"
"<style>html { font-family: sans-serif }</style>\n"
f"<h1>⚠️ {title}</h1><p>{text}\n",
status=status,
headers=headers,
)
def _render_exception(exception):
frames = extract_tb(exception.__traceback__)
frame_html = "".join(TRACEBACK_LINE_HTML.format(frame) for frame in frames)
return TRACEBACK_WRAPPER_HTML.format(
exc_name=escape(exception.__class__.__name__),
exc_value=escape(exception),
frame_html=frame_html,
)
def _render_traceback_html(request, exception):
exc_type, exc_value, tb = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(_render_exception(exc_value))
exc_value = exc_value.__cause__
traceback_html = TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(request.app.name)
name = escape(exception.__class__.__name__)
value = escape(exception)
path = escape(request.path)
return (
f"<h2>Traceback of {appname} (most recent call last):</h2>"
f"{traceback_html}"
"<div class=summary><p>"
f"<b>{name}: {value}</b> while handling path <code>{path}</code>"
)
TRACEBACK_STYLE = """
html { font-family: sans-serif }
h2 { color: #888; }
.tb-wrapper p { margin: 0 }
.frame-border { margin: 1rem }
.frame-line > * { padding: 0.3rem 0.6rem }
.frame-line { margin-bottom: 0.3rem }
.frame-code { font-size: 16px; padding-left: 4ch }
.tb-wrapper { border: 1px solid #eee }
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
.frame-descriptor { background: #e2eafb; font-size: 14px }
"""
TRACEBACK_WRAPPER_HTML = (
"<div class=tb-header>{exc_name}: {exc_value}</div>"
"<div class=tb-wrapper>{frame_html}</div>"
)
TRACEBACK_BORDER = (
"<div class=frame-border>"
"The above exception was the direct cause of the following exception:"
"</div>"
)
TRACEBACK_LINE_HTML = (
"<div class=frame-line>"
"<p class=frame-descriptor>"
"File {0.filename}, line <i>{0.lineno}</i>, "
"in <code><b>{0.name}</b></code>"
"<p class=frame-code><code>{0.line}</code>"
"</div>"
)

View File

@@ -1,133 +1,18 @@
from sanic.helpers import STATUS_CODES
TRACEBACK_STYLE = """
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
}
p {
margin: 0;
}
.summary {
padding: 10px;
}
h1 {
margin-bottom: 0;
}
h3 {
margin-top: 10px;
}
h3 code {
font-size: 24px;
}
.frame-line > * {
padding: 5px 10px;
}
.frame-line {
margin-bottom: 5px;
}
.frame-code {
font-size: 16px;
padding-left: 30px;
}
.tb-wrapper {
border: 1px solid #f3f3f3;
}
.tb-header {
background-color: #f3f3f3;
padding: 5px 10px;
}
.tb-border {
padding-top: 20px;
}
.frame-descriptor {
background-color: #e2eafb;
}
.frame-descriptor {
font-size: 14px;
}
</style>
"""
TRACEBACK_WRAPPER_HTML = """
<html>
<head>
{style}
</head>
<body>
{inner_html}
<div class="summary">
<p>
<b>{exc_name}: {exc_value}</b>
while handling path <code>{path}</code>
</p>
</div>
</body>
</html>
"""
TRACEBACK_WRAPPER_INNER_HTML = """
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
</div>
"""
TRACEBACK_BORDER = """
<div class="tb-border">
<b><i>
The above exception was the direct cause of the
following exception:
</i></b>
</div>
"""
TRACEBACK_LINE_HTML = """
<div class="frame-line">
<p class="frame-descriptor">
File {0.filename}, line <i>{0.lineno}</i>,
in <code><b>{0.name}</b></code>
</p>
<p class="frame-code"><code>{0.line}</code></p>
</div>
"""
INTERNAL_SERVER_ERROR_HTML = """
<h1>Internal Server Error</h1>
<p>
The server encountered an internal error and cannot complete
your request.
</p>
"""
_sanic_exceptions = {}
def add_status_code(code):
def add_status_code(code, quiet=None):
"""
Decorator used for adding exceptions to :class:`SanicException`.
"""
def class_decorator(cls):
cls.status_code = code
if quiet or quiet is None and code != 500:
cls.quiet = True
_sanic_exceptions[code] = cls
return cls
@@ -135,12 +20,16 @@ def add_status_code(code):
class SanicException(Exception):
def __init__(self, message, status_code=None):
def __init__(self, message, status_code=None, quiet=None):
super().__init__(message)
if status_code is not None:
self.status_code = status_code
# quiet=None/False/True with None meaning choose by status
if quiet or quiet is None and status_code not in (None, 500):
self.quiet = True
@add_status_code(404)
class NotFound(SanicException):
@@ -156,10 +45,7 @@ class InvalidUsage(SanicException):
class MethodNotSupported(SanicException):
def __init__(self, message, method, allowed_methods):
super().__init__(message)
self.headers = dict()
self.headers["Allow"] = ", ".join(allowed_methods)
if method in ["HEAD", "PATCH", "PUT", "DELETE"]:
self.headers["Content-Length"] = 0
self.headers = {"Allow": ", ".join(allowed_methods)}
@add_status_code(500)
@@ -212,10 +98,7 @@ class HeaderNotFound(InvalidUsage):
class ContentRangeError(SanicException):
def __init__(self, message, content_range):
super().__init__(message)
self.headers = {
"Content-Type": "text/plain",
"Content-Range": "bytes */%s" % (content_range.total,),
}
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
@add_status_code(417)
@@ -282,7 +165,7 @@ class Unauthorized(SanicException):
challenge = ", ".join(values)
self.headers = {
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
"WWW-Authenticate": f"{scheme} {challenge}".rstrip()
}

View File

@@ -1,21 +1,13 @@
import sys
from traceback import extract_tb, format_exc
from traceback import format_exc
from sanic.errorpages import exception_response
from sanic.exceptions import (
INTERNAL_SERVER_ERROR_HTML,
TRACEBACK_BORDER,
TRACEBACK_LINE_HTML,
TRACEBACK_STYLE,
TRACEBACK_WRAPPER_HTML,
TRACEBACK_WRAPPER_INNER_HTML,
ContentRangeError,
HeaderNotFound,
InvalidRangeType,
SanicException,
)
from sanic.log import logger
from sanic.response import html, text
from sanic.response import text
class ErrorHandler:
@@ -40,35 +32,6 @@ class ErrorHandler:
self.cached_handlers = {}
self.debug = False
def _render_exception(self, exception):
frames = extract_tb(exception.__traceback__)
frame_html = []
for frame in frames:
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
return TRACEBACK_WRAPPER_INNER_HTML.format(
exc_name=exception.__class__.__name__,
exc_value=exception,
frame_html="".join(frame_html),
)
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._render_exception(exc_value))
exc_value = exc_value.__cause__
return TRACEBACK_WRAPPER_HTML.format(
style=TRACEBACK_STYLE,
exc_name=exception.__class__.__name__,
exc_value=exception,
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
path=request.path,
)
def add(self, exception, handler):
"""
Add a new exception handler to an already existing handler object.
@@ -166,27 +129,17 @@ class ErrorHandler:
:class:`Exception`
:return:
"""
self.log(format_exc())
try:
url = repr(request.url)
except AttributeError:
url = "unknown"
quiet = getattr(exception, "quiet", False)
if quiet is False:
try:
url = repr(request.url)
except AttributeError:
url = "unknown"
response_message = "Exception occurred while handling uri: %s"
logger.exception(response_message, url)
self.log(format_exc())
logger.exception("Exception occurred while handling uri: %s", url)
if issubclass(type(exception), SanicException):
return text(
"Error: {}".format(exception),
status=getattr(exception, "status_code", 500),
headers=getattr(exception, "headers", dict()),
)
elif self.debug:
html_output = self._render_traceback_html(exception, request)
return html(html_output, status=500)
else:
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
return exception_response(request, exception, self.debug)
class ContentRangeHandler:

View File

@@ -1,9 +1,12 @@
import re
from typing import Dict, Iterable, List, Optional, Tuple, Union
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from urllib.parse import unquote
from sanic.helpers import STATUS_CODES
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
Options = Dict[str, Union[int, str]] # key=value fields in various headers
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
@@ -170,3 +173,28 @@ def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]:
return None, None
host, port = m.groups()
return host.lower(), int(port) if port is not None else None
def format_http1(headers: HeaderIterable) -> bytes:
"""Convert a headers iterable into HTTP/1 header format.
- Outputs UTF-8 bytes where each header line ends with \\r\\n.
- Values are converted into strings if necessary.
"""
return "".join(f"{name}: {val}\r\n" for name, val in headers).encode()
def format_http1_response(
status: int, headers: HeaderIterable, body=b""
) -> bytes:
"""Format a full HTTP/1.1 response.
- If `body` is included, content-length must be specified in headers.
"""
headerbytes = format_http1(headers)
return b"HTTP/1.1 %d %b\r\n%b\r\n%b" % (
status,
STATUS_CODES.get(status, b"UNKNOWN"),
headerbytes,
body,
)

View File

@@ -72,7 +72,7 @@ def kill_process_children_unix(pid):
:param pid: PID of parent process (process ID)
:return: Nothing
"""
root_process_path = "/proc/{pid}/task/{pid}/children".format(pid=pid)
root_process_path = f"/proc/{pid}/task/{pid}/children"
if not os.path.isfile(root_process_path):
return
with open(root_process_path) as children_list_file:

View File

@@ -1,6 +1,5 @@
import asyncio
import email.utils
import warnings
from collections import defaultdict, namedtuple
from http.cookies import SimpleCookie
@@ -56,12 +55,24 @@ class StreamBuffer:
self._queue.task_done()
return payload
async def __aiter__(self):
"""Support `async for data in request.stream`"""
while True:
data = await self.read()
if not data:
break
yield data
async def put(self, payload):
await self._queue.put(payload)
def is_full(self):
return self._queue.full()
@property
def buffer_size(self):
return self._queue.maxsize
class Request:
"""Properties of an HTTP request such as URL, headers, etc."""
@@ -119,44 +130,37 @@ class Request:
self.endpoint = None
def __repr__(self):
return "<{0}: {1} {2}>".format(
self.__class__.__name__, self.method, self.path
)
def get(self, key, default=None):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
return self.ctx.__dict__.get(key, default)
def __contains__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
return key in self.ctx.__dict__
def __getitem__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
return self.ctx.__dict__[key]
def __delitem__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
del self.ctx.__dict__[key]
def __setitem__(self, key, value):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
setattr(self.ctx, key, value)
class_name = self.__class__.__name__
return f"<{class_name}: {self.method} {self.path}>"
def body_init(self):
""".. deprecated:: 20.3"""
self.body = []
def body_push(self, data):
""".. deprecated:: 20.3"""
self.body.append(data)
def body_finish(self):
""".. deprecated:: 20.3"""
self.body = b"".join(self.body)
async def receive_body(self):
"""Receive request.body, if not already received.
Streaming handlers may call this to receive the full body.
This is added as a compatibility shim in Sanic 20.3 because future
versions of Sanic will make all requests streaming and will use this
function instead of the non-async body_init/push/finish functions.
Please make an issue if your code depends on the old functionality and
cannot be upgraded to the new API.
"""
if not self.stream:
return
self.body = b"".join([data async for data in self.stream])
@property
def json(self):
if self.parsed_json is None:
@@ -278,18 +282,6 @@ class Request:
args = property(get_args)
@property
def raw_args(self) -> dict:
if self.app.debug: # pragma: no cover
warnings.simplefilter("default")
warnings.warn(
"Use of raw_args will be deprecated in "
"the future versions. Please use args or query_args "
"properties instead",
DeprecationWarning,
)
return {k: v[0] for k, v in self.args.items()}
def get_query_args(
self,
keep_blank_values: bool = False,
@@ -519,8 +511,11 @@ class Request:
:rtype: str
"""
# Full URL SERVER_NAME can only be handled in app.url_for
if "//" in self.app.config.SERVER_NAME:
return self.app.url_for(view_name, _external=True, **kwargs)
try:
if "//" in self.app.config.SERVER_NAME:
return self.app.url_for(view_name, _external=True, **kwargs)
except AttributeError:
pass
scheme = self.scheme
host = self.server_name
@@ -531,7 +526,7 @@ class Request:
):
netloc = host
else:
netloc = "{}:{}".format(host, port)
netloc = f"{host}:{port}"
return self.app.url_for(
view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs

View File

@@ -1,13 +1,14 @@
import warnings
from functools import partial
from mimetypes import guess_type
from os import path
from urllib.parse import quote_plus
from aiofiles import open as open_async # type: ignore
from sanic.compat import Header
from sanic.compat import Header, open_async
from sanic.cookies import CookieJar
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
from sanic.headers import format_http1, format_http1_response
from sanic.helpers import has_message_body, remove_entity_headers
try:
@@ -22,28 +23,10 @@ except ImportError:
class BaseHTTPResponse:
def _encode_body(self, data):
try:
# Try to encode it regularly
return data.encode()
except AttributeError:
# Convert it to a str if you can't
return str(data).encode()
return data.encode() if hasattr(data, "encode") else data
def _parse_headers(self):
headers = b""
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"),
)
return headers
return format_http1(self.headers.items())
@property
def cookies(self):
@@ -51,6 +34,32 @@ class BaseHTTPResponse:
self._cookies = CookieJar(self.headers)
return self._cookies
def get_headers(
self,
version="1.1",
keep_alive=False,
keep_alive_timeout=None,
body=b"",
):
""".. deprecated:: 20.3:
This function is not public API and will be removed."""
# self.headers get priority over content_type
if self.content_type and "Content-Type" not in self.headers:
self.headers["Content-Type"] = self.content_type
if keep_alive:
self.headers["Connection"] = "keep-alive"
if keep_alive_timeout is not None:
self.headers["Keep-Alive"] = keep_alive_timeout
else:
self.headers["Connection"] = "close"
if self.status in (304, 412):
self.headers = remove_entity_headers(self.headers)
return format_http1_response(self.status, self.headers.items(), body)
class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = (
@@ -68,7 +77,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
streaming_fn,
status=200,
headers=None,
content_type="text/plain",
content_type="text/plain; charset=utf-8",
chunked=True,
):
self.content_type = content_type
@@ -77,14 +86,14 @@ class StreamingHTTPResponse(BaseHTTPResponse):
self.headers = Header(headers or {})
self.chunked = chunked
self._cookies = None
self.protocol = None
async def write(self, data):
"""Writes a chunk of data to the streaming response.
:param data: bytes-ish data to be written.
:param data: str or bytes-ish data to be written.
"""
if type(data) != bytes:
data = self._encode_body(data)
data = self._encode_body(data)
if self.chunked:
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
@@ -116,33 +125,11 @@ class StreamingHTTPResponse(BaseHTTPResponse):
def get_headers(
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
timeout_header = b""
if keep_alive and keep_alive_timeout is not None:
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
if self.chunked and version == "1.1":
self.headers["Transfer-Encoding"] = "chunked"
self.headers.pop("Content-Length", None)
self.headers["Content-Type"] = self.headers.get(
"Content-Type", self.content_type
)
headers = self._parse_headers()
if self.status == 200:
status = b"OK"
else:
status = STATUS_CODES.get(self.status)
return (b"HTTP/%b %d %b\r\n" b"%b" b"%b\r\n") % (
version.encode(),
self.status,
status,
timeout_header,
headers,
)
return super().get_headers(version, keep_alive, keep_alive_timeout)
class HTTPResponse(BaseHTTPResponse):
@@ -153,27 +140,16 @@ class HTTPResponse(BaseHTTPResponse):
body=None,
status=200,
headers=None,
content_type="text/plain",
content_type=None,
body_bytes=b"",
):
self.content_type = content_type
if body is not None:
self.body = self._encode_body(body)
else:
self.body = body_bytes
self.body = body_bytes if body is None else self._encode_body(body)
self.status = status
self.headers = Header(headers or {})
self._cookies = None
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
# This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python
timeout_header = b""
if keep_alive and keep_alive_timeout is not None:
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
body = b""
if has_message_body(self.status):
body = self.body
@@ -181,31 +157,7 @@ class HTTPResponse(BaseHTTPResponse):
"Content-Length", len(self.body)
)
self.headers["Content-Type"] = self.headers.get(
"Content-Type", self.content_type
)
if self.status in (304, 412):
self.headers = remove_entity_headers(self.headers)
headers = self._parse_headers()
if self.status == 200:
status = b"OK"
else:
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
return (
b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b"
) % (
version.encode(),
self.status,
status,
b"keep-alive" if keep_alive else b"close",
timeout_header,
headers,
body,
)
return self.get_headers(version, keep_alive, keep_alive_timeout, body)
@property
def cookies(self):
@@ -214,13 +166,23 @@ class HTTPResponse(BaseHTTPResponse):
return self._cookies
def empty(status=204, headers=None):
"""
Returns an empty response to the client.
:param status Response code.
:param headers Custom Headers.
"""
return HTTPResponse(body_bytes=b"", status=status, headers=headers)
def json(
body,
status=200,
headers=None,
content_type="application/json",
dumps=json_dumps,
**kwargs
**kwargs,
):
"""
Returns response object with body in json format.
@@ -249,6 +211,21 @@ def text(
:param headers: Custom Headers.
:param content_type: the content type (string) of the response
"""
if not isinstance(body, str):
warnings.warn(
"Types other than str will be deprecated in future versions for"
f" response.text, got type {type(body).__name__})",
DeprecationWarning,
)
# Type conversions are deprecated and quite b0rked but still supported for
# text() until applications get fixed. This try-except should be removed.
try:
# Avoid repr(body).encode() b0rkage for body that is already encoded.
# memoryview used only to test bytes-ishness.
with memoryview(body):
pass
except TypeError:
body = f"{body}" # no-op if body is already str
return HTTPResponse(
body, status=status, headers=headers, content_type=content_type
)
@@ -277,10 +254,14 @@ def html(body, status=200, headers=None):
"""
Returns response object with body in html format.
:param body: Response data to be encoded.
:param body: str or bytes-ish, or an object with __html__ or _repr_html_.
:param status: Response code.
:param headers: Custom Headers.
"""
if hasattr(body, "__html__"):
body = body.__html__()
elif hasattr(body, "_repr_html_"):
body = body._repr_html_()
return HTTPResponse(
body,
status=status,
@@ -308,22 +289,20 @@ async def file(
headers = headers or {}
if filename:
headers.setdefault(
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
"Content-Disposition", f'attachment; filename="{filename}"'
)
filename = filename or path.split(location)[-1]
async with open_async(location, mode="rb") as _file:
async with await open_async(location, mode="rb") as f:
if _range:
await _file.seek(_range.start)
out_stream = await _file.read(_range.size)
headers["Content-Range"] = "bytes %s-%s/%s" % (
_range.start,
_range.end,
_range.total,
)
await f.seek(_range.start)
out_stream = await f.read(_range.size)
headers[
"Content-Range"
] = f"bytes {_range.start}-{_range.end}/{_range.total}"
status = 206
else:
out_stream = await _file.read()
out_stream = await f.read()
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
return HTTPResponse(
@@ -357,43 +336,36 @@ async def file_stream(
headers = headers or {}
if filename:
headers.setdefault(
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
"Content-Disposition", f'attachment; filename="{filename}"'
)
filename = filename or path.split(location)[-1]
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
if _range:
start = _range.start
end = _range.end
total = _range.total
_file = await open_async(location, mode="rb")
headers["Content-Range"] = f"bytes {start}-{end}/{total}"
status = 206
async def _streaming_fn(response):
nonlocal _file, chunk_size
try:
async with await open_async(location, mode="rb") as f:
if _range:
chunk_size = min((_range.size, chunk_size))
await _file.seek(_range.start)
await f.seek(_range.start)
to_send = _range.size
while to_send > 0:
content = await _file.read(chunk_size)
content = await f.read(min((_range.size, chunk_size)))
if len(content) < 1:
break
to_send -= len(content)
await response.write(content)
else:
while True:
content = await _file.read(chunk_size)
content = await f.read(chunk_size)
if len(content) < 1:
break
await response.write(content)
finally:
await _file.close()
return # Returning from this fn closes the stream
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
if _range:
headers["Content-Range"] = "bytes %s-%s/%s" % (
_range.start,
_range.end,
_range.total,
)
status = 206
return StreamingHTTPResponse(
streaming_fn=_streaming_fn,
status=status,

View File

@@ -109,7 +109,7 @@ class Router:
name, pattern = parameter_string.split(":", 1)
if not name:
raise ValueError(
"Invalid parameter syntax: {}".format(parameter_string)
f"Invalid parameter syntax: {parameter_string}"
)
default = (str, pattern)
@@ -140,21 +140,22 @@ class Router:
docs for further details.
:return: Nothing
"""
routes = []
if version is not None:
version = re.escape(str(version).strip("/").lstrip("v"))
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
uri = "/".join([f"/v{version}", uri.lstrip("/")])
# add regular version
self._add(uri, methods, handler, host, name)
routes.append(self._add(uri, methods, handler, host, name))
if strict_slashes:
return
return routes
if not isinstance(host, str) and host is not None:
# we have gotten back to the top of the recursion tree where the
# host was originally a list. By now, we've processed the strict
# slashes logic on the leaf nodes (the individual host strings in
# the list of host)
return
return routes
# Add versions with and without trailing /
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
@@ -176,10 +177,12 @@ class Router:
)
# add version with trailing slash
if slash_is_missing:
self._add(uri + "/", methods, handler, host, name)
routes.append(self._add(uri + "/", methods, handler, host, name))
# add version without trailing slash
elif without_slash_is_missing:
self._add(uri[:-1], methods, handler, host, name)
routes.append(self._add(uri[:-1], methods, handler, host, name))
return routes
def _add(self, uri, methods, handler, host=None, name=None):
"""Add a handler to the route list
@@ -200,8 +203,8 @@ class Router:
else:
if not isinstance(host, Iterable):
raise ValueError(
"Expected either string or Iterable of "
"host strings, not {!r}".format(host)
f"Expected either string or Iterable of "
f"host strings, not {host!r}"
)
for host_ in host:
@@ -222,8 +225,7 @@ class Router:
if name in parameter_names:
raise ParameterNameConflicts(
"Multiple parameter named <{name}> "
"in route uri {uri}".format(name=name, uri=uri)
f"Multiple parameter named <{name}> " f"in route uri {uri}"
)
parameter_names.add(name)
@@ -237,23 +239,23 @@ class Router:
elif re.search(r"/", pattern):
properties["unhashable"] = True
return "({})".format(pattern)
return f"({pattern})"
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
pattern = re.compile(r"^{}$".format(pattern_string))
pattern = re.compile(fr"^{pattern_string}$")
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))
raise RouteExists(f"Route already registered: {uri}")
elif route.methods.intersection(methods):
# already existing method is not overloadable.
duplicated = methods.intersection(route.methods)
duplicated_methods = ",".join(list(duplicated))
raise RouteExists(
"Route already registered: {} [{}]".format(
uri, ",".join(list(duplicated))
)
f"Route already registered: {uri} [{duplicated_methods}]"
)
if isinstance(route.handler, CompositionView):
view = route.handler
@@ -293,9 +295,9 @@ class Router:
name = name.split("_static_", 1)[-1]
if hasattr(handler, "__blueprintname__"):
handler_name = "{}.{}".format(
handler.__blueprintname__, name or handler.__name__
)
bp_name = handler.__blueprintname__
handler_name = f"{bp_name}.{name or handler.__name__}"
else:
handler_name = name or getattr(handler, "__name__", None)
@@ -328,6 +330,7 @@ class Router:
self.routes_dynamic[url_hash(uri)].append(route)
else:
self.routes_static[uri] = route
return route
@staticmethod
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
@@ -348,37 +351,6 @@ class Router:
else:
return -1, None
def remove(self, uri, clean_cache=True, host=None):
if host is not None:
uri = host + uri
try:
route = self.routes_all.pop(uri)
for handler_name, pairs in self.routes_names.items():
if pairs[0] == uri:
self.routes_names.pop(handler_name)
break
for handler_name, pairs in self.routes_static_files.items():
if pairs[0] == uri:
self.routes_static_files.pop(handler_name)
break
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()
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def find_route_by_view_name(self, view_name, name=None):
"""Find a route in the router based on the specified view name.
@@ -438,10 +410,11 @@ class Router:
# Check against known static routes
route = self.routes_static.get(url)
method_not_supported = MethodNotSupported(
"Method {} not allowed for URL {}".format(method, url),
f"Method {method} not allowed for URL {url}",
method=method,
allowed_methods=self.get_supported_methods(url),
)
if route:
if route.methods and method not in route.methods:
raise method_not_supported
@@ -467,7 +440,7 @@ class Router:
# Route was found but the methods didn't match
if route_found:
raise method_not_supported
raise NotFound("Requested URL {} not found".format(url))
raise NotFound(f"Requested URL {url} not found")
kwargs = {
p.name: p.cast(value)
@@ -476,7 +449,7 @@ class Router:
route_handler = route.handler
if hasattr(route_handler, "handlers"):
route_handler = route_handler.handlers[method]
return route_handler, [], kwargs, route.uri
return route_handler, [], kwargs, route.uri, route.name
def is_stream_handler(self, request):
""" Handler for request is stream or not.

View File

@@ -1,10 +1,12 @@
import asyncio
import multiprocessing
import os
import sys
import traceback
from collections import deque
from functools import partial
from inspect import isawaitable
from multiprocessing import Process
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from socket import SO_REUSEADDR, SOL_SOCKET, socket
@@ -13,7 +15,7 @@ from time import time
from httptools import HttpRequestParser # type: ignore
from httptools.parser.errors import HttpParserError # type: ignore
from sanic.compat import Header
from sanic.compat import Header, ctrlc_workaround_for_windows
from sanic.exceptions import (
HeaderExpectationFailed,
InvalidUsage,
@@ -35,6 +37,8 @@ try:
except ImportError:
pass
OS_IS_WINDOWS = os.name == "nt"
class Signal:
stopped = False
@@ -67,7 +71,6 @@ class HttpProtocol(asyncio.Protocol):
"request_buffer_queue_size",
"request_class",
"is_request_stream",
"router",
"error_handler",
# enable or disable access log purpose
"access_log",
@@ -85,7 +88,7 @@ class HttpProtocol(asyncio.Protocol):
"_keep_alive",
"_header_fragment",
"state",
"_debug",
"_body_chunks",
)
def __init__(
@@ -93,46 +96,36 @@ class HttpProtocol(asyncio.Protocol):
*,
loop,
app,
request_handler,
error_handler,
signal=Signal(),
connections=None,
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
request_max_size=None,
request_buffer_queue_size=100,
request_class=None,
access_log=True,
keep_alive=True,
is_request_stream=False,
router=None,
state=None,
debug=False,
**kwargs
**kwargs,
):
asyncio.set_event_loop(loop)
self.loop = loop
deprecated_loop = self.loop if sys.version_info < (3, 7) else None
self.app = app
self.transport = None
self.request = None
self.parser = None
self.url = None
self.headers = None
self.router = router
self.signal = signal
self.access_log = access_log
self.access_log = self.app.config.ACCESS_LOG
self.connections = connections if connections is not None else set()
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
self.request_buffer_queue_size = request_buffer_queue_size
self.response_timeout = response_timeout
self.keep_alive_timeout = keep_alive_timeout
self.request_max_size = request_max_size
self.request_class = request_class or Request
self.is_request_stream = is_request_stream
self.request_handler = self.app.handle_request
self.error_handler = self.app.error_handler
self.request_timeout = self.app.config.REQUEST_TIMEOUT
self.request_buffer_queue_size = (
self.app.config.REQUEST_BUFFER_QUEUE_SIZE
)
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
self.request_class = self.app.request_class or Request
self.is_request_stream = self.app.is_request_stream
self._is_stream_handler = False
self._not_paused = asyncio.Event(loop=loop)
self._not_paused = asyncio.Event(loop=deprecated_loop)
self._total_request_size = 0
self._request_timeout_handler = None
self._response_timeout_handler = None
@@ -141,13 +134,13 @@ class HttpProtocol(asyncio.Protocol):
self._last_response_time = None
self._request_handler_task = None
self._request_stream_task = None
self._keep_alive = keep_alive
self._keep_alive = self.app.config.KEEP_ALIVE
self._header_fragment = b""
self.state = state if state else {}
if "requests_count" not in self.state:
self.state["requests_count"] = 0
self._debug = debug
self._not_paused.set()
self._body_chunks = deque()
@property
def keep_alive(self):
@@ -273,7 +266,7 @@ class HttpProtocol(asyncio.Protocol):
self.parser.feed_data(data)
except HttpParserError:
message = "Bad Request"
if self._debug:
if self.app.debug:
message += "\n" + traceback.format_exc()
self.write_error(InvalidUsage(message))
@@ -321,7 +314,7 @@ class HttpProtocol(asyncio.Protocol):
self.expect_handler()
if self.is_request_stream:
self._is_stream_handler = self.router.is_stream_handler(
self._is_stream_handler = self.app.router.is_stream_handler(
self.request
)
if self._is_stream_handler:
@@ -340,20 +333,34 @@ class HttpProtocol(asyncio.Protocol):
self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
else:
self.write_error(
HeaderExpectationFailed(
"Unknown Expect: {expect}".format(expect=expect)
)
HeaderExpectationFailed(f"Unknown Expect: {expect}")
)
def on_body(self, body):
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
self.body_append(body)
)
# body chunks can be put into asyncio.Queue out of order if
# multiple tasks put concurrently and the queue is full in python
# 3.7. so we should not create more than one task putting into the
# queue simultaneously.
self._body_chunks.append(body)
if (
not self._request_stream_task
or self._request_stream_task.done()
):
self._request_stream_task = self.loop.create_task(
self.stream_append()
)
else:
self.request.body_push(body)
async def body_append(self, body):
if (
self.request is None
or self._request_stream_task is None
or self._request_stream_task.cancelled()
):
return
if self.request.stream.is_full():
self.transport.pause_reading()
await self.request.stream.put(body)
@@ -361,6 +368,16 @@ class HttpProtocol(asyncio.Protocol):
else:
await self.request.stream.put(body)
async def stream_append(self):
while self._body_chunks:
body = self._body_chunks.popleft()
if self.request.stream.is_full():
self.transport.pause_reading()
await self.request.stream.put(body)
self.transport.resume_reading()
else:
await self.request.stream.put(body)
def on_message_complete(self):
# Entire request (headers and whole body) is received.
# We can cancel and remove the request timeout handler now.
@@ -368,9 +385,14 @@ class HttpProtocol(asyncio.Protocol):
self._request_timeout_handler.cancel()
self._request_timeout_handler = None
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
self.request.stream.put(None)
)
self._body_chunks.append(None)
if (
not self._request_stream_task
or self._request_stream_task.done()
):
self._request_stream_task = self.loop.create_task(
self.stream_append()
)
return
self.request.body_finish()
self.execute_request_handler()
@@ -418,13 +440,9 @@ class HttpProtocol(asyncio.Protocol):
extra["host"] = "UNKNOWN"
if self.request is not None:
if self.request.ip:
extra["host"] = "{0}:{1}".format(
self.request.ip, self.request.port
)
extra["host"] = f"{self.request.ip}:{self.request.port}"
extra["request"] = "{0} {1}".format(
self.request.method, self.request.url
)
extra["request"] = f"{self.request.method} {self.request.url}"
else:
extra["request"] = "nil"
@@ -454,16 +472,14 @@ class HttpProtocol(asyncio.Protocol):
)
self.write_error(ServerError("Invalid response type"))
except RuntimeError:
if self._debug:
if self.app.debug:
logger.error(
"Connection lost before response written @ %s",
self.request.ip,
)
keep_alive = False
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(repr(e))
)
self.bail_out(f"Writing response failed, connection closed {e!r}")
finally:
if not keep_alive:
self.transport.close()
@@ -507,16 +523,14 @@ class HttpProtocol(asyncio.Protocol):
)
self.write_error(ServerError("Invalid response type"))
except RuntimeError:
if self._debug:
if self.app.debug:
logger.error(
"Connection lost before response written @ %s",
self.request.ip,
)
keep_alive = False
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(repr(e))
)
self.bail_out(f"Writing response failed, connection closed {e!r}")
finally:
if not keep_alive:
self.transport.close()
@@ -540,14 +554,14 @@ class HttpProtocol(asyncio.Protocol):
version = self.request.version if self.request else "1.1"
self.transport.write(response.output(version))
except RuntimeError:
if self._debug:
if self.app.debug:
logger.error(
"Connection lost before error written @ %s",
self.request.ip if self.request else "Unknown",
)
except Exception as e:
self.bail_out(
"Writing error failed, connection closed {}".format(repr(e)),
f"Writing error failed, connection closed {e!r}",
from_error=True,
)
finally:
@@ -608,7 +622,7 @@ class HttpProtocol(asyncio.Protocol):
:return: boolean - True if closed, false if staying open
"""
if not self.parser:
if not self.parser and self.transport is not None:
self.transport.close()
return True
return False
@@ -697,6 +711,26 @@ class AsyncioServer:
task = asyncio.ensure_future(coro, loop=self.loop)
return task
def start_serving(self):
if self.server:
try:
return self.server.start_serving()
except AttributeError:
raise NotImplementedError(
"server.start_serving not available in this version "
"of asyncio or uvloop."
)
def serve_forever(self):
if self.server:
try:
return self.server.serve_forever()
except AttributeError:
raise NotImplementedError(
"server.serve_forever not available in this version "
"of asyncio or uvloop."
)
def __await__(self):
"""Starts the asyncio server, returns AsyncServerCoro"""
task = asyncio.ensure_future(self.serve_coro)
@@ -710,20 +744,12 @@ def serve(
host,
port,
app,
request_handler,
error_handler,
before_start=None,
after_start=None,
before_stop=None,
after_stop=None,
debug=False,
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
ssl=None,
sock=None,
request_max_size=None,
request_buffer_queue_size=100,
reuse_port=False,
loop=None,
protocol=HttpProtocol,
@@ -733,25 +759,13 @@ def serve(
run_async=False,
connections=None,
signal=Signal(),
request_class=None,
access_log=True,
keep_alive=True,
is_request_stream=False,
router=None,
websocket_max_size=None,
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16,
state=None,
graceful_shutdown_timeout=15.0,
asyncio_server_kwargs=None,
):
"""Start 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 error_handler: Sanic error handler with middleware
:param before_start: function to be executed before the server starts
listening. Takes arguments `app` instance and `loop`
:param after_start: function to be executed after the server starts
@@ -762,35 +776,12 @@ def serve(
:param after_stop: function to be executed when a stop signal is
received after it is respected. Takes arguments
`app` instance and `loop`
:param debug: enables debug output (slows server)
:param request_timeout: time in seconds
:param response_timeout: time in seconds
:param keep_alive_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
:param loop: asyncio compatible event loop
:param protocol: subclass of asyncio protocol class
:param run_async: bool: Do not create a new event loop for the server,
and return an AsyncServer object rather than running it
:param request_class: Request class to use
:param access_log: disable/enable access log
:param websocket_max_size: enforces the maximum size for
incoming messages in bytes.
:param websocket_max_queue: sets the maximum length of the queue
that holds incoming messages.
:param websocket_read_limit: sets the high-water limit of the buffer for
incoming bytes, the low-water limit is half
the high-water limit.
:param websocket_write_limit: sets the high-water limit of the buffer for
outgoing bytes, the low-water limit is a
quarter of the high-water limit.
:param is_request_stream: disable/enable Request.stream
:param request_buffer_queue_size: streaming request buffer queue size
:param router: Router object
:param graceful_shutdown_timeout: How long take to Force close non-idle
connection
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
create_server method
:return: Nothing
@@ -800,8 +791,8 @@ def serve(
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if debug:
loop.set_debug(debug)
if app.debug:
loop.set_debug(app.debug)
app.asgi = False
@@ -812,23 +803,7 @@ def serve(
connections=connections,
signal=signal,
app=app,
request_handler=request_handler,
error_handler=error_handler,
request_timeout=request_timeout,
response_timeout=response_timeout,
keep_alive_timeout=keep_alive_timeout,
request_max_size=request_max_size,
request_class=request_class,
access_log=access_log,
keep_alive=keep_alive,
is_request_stream=is_request_stream,
router=router,
websocket_max_size=websocket_max_size,
websocket_max_queue=websocket_max_queue,
websocket_read_limit=websocket_read_limit,
websocket_write_limit=websocket_write_limit,
state=state,
debug=debug,
)
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
@@ -841,17 +816,17 @@ def serve(
reuse_port=reuse_port,
sock=sock,
backlog=backlog,
**asyncio_server_kwargs
**asyncio_server_kwargs,
)
if run_async:
return AsyncioServer(
loop,
server_coroutine,
connections,
after_start,
before_stop,
after_stop,
loop=loop,
serve_coro=server_coroutine,
connections=connections,
after_start=after_start,
before_stop=before_stop,
after_stop=after_stop,
)
trigger_events(before_start, loop)
@@ -870,15 +845,11 @@ def serve(
# Register signals for graceful termination
if register_sys_signals:
_singals = (SIGTERM,) if run_multiple else (SIGINT, SIGTERM)
for _signal in _singals:
try:
loop.add_signal_handler(_signal, loop.stop)
except NotImplementedError:
logger.warning(
"Sanic tried to use loop.add_signal_handler "
"but it is not implemented on this platform."
)
if OS_IS_WINDOWS:
ctrlc_workaround_for_windows(app)
else:
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
loop.add_signal_handler(_signal, app.stop)
pid = os.getpid()
try:
logger.info("Starting worker [%s]", pid)
@@ -902,8 +873,9 @@ def serve(
# We should provide graceful_shutdown_timeout,
# instead of letting connection hangs forever.
# Let's roughly calcucate time.
graceful = app.config.GRACEFUL_SHUTDOWN_TIMEOUT
start_shutdown = 0
while connections and (start_shutdown < graceful_shutdown_timeout):
while connections and (start_shutdown < graceful):
loop.run_until_complete(asyncio.sleep(0.1))
start_shutdown = start_shutdown + 0.1
@@ -916,7 +888,7 @@ def serve(
else:
conn.close()
_shutdown = asyncio.gather(*coros, loop=loop)
_shutdown = asyncio.gather(*coros)
loop.run_until_complete(_shutdown)
trigger_events(after_stop, loop)
@@ -955,9 +927,10 @@ def serve_multiple(server_settings, workers):
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
mp = multiprocessing.get_context("fork")
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process = mp.Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start()
processes.append(process)

View File

@@ -4,8 +4,7 @@ from re import sub
from time import gmtime, strftime
from urllib.parse import unquote
from aiofiles.os import stat # type: ignore
from sanic.compat import stat_async
from sanic.exceptions import (
ContentRangeError,
FileNotFound,
@@ -84,7 +83,7 @@ def register(
# and it has not been modified since
stats = None
if use_modified_since:
stats = await stat(file_path)
stats = await stat_async(file_path)
modified_since = strftime(
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
)
@@ -95,7 +94,7 @@ def register(
if use_content_range:
_range = None
if not stats:
stats = await stat(file_path)
stats = await stat_async(file_path)
headers["Accept-Ranges"] = "bytes"
headers["Content-Length"] = str(stats.st_size)
if request.method != "HEAD":
@@ -120,7 +119,7 @@ def register(
threshold = 1024 * 1024
if not stats:
stats = await stat(file_path)
stats = await stat_async(file_path)
if stats.st_size >= threshold:
return await file_stream(
file_path, headers=headers, _range=_range
@@ -135,7 +134,7 @@ def register(
# special prefix for static files
if not name.startswith("_static_"):
name = "_static_{}".format(name)
name = f"_static_{name}"
app.route(
uri,

View File

@@ -1,14 +1,8 @@
import asyncio
import types
import typing
from json import JSONDecodeError
from socket import socket
from urllib.parse import unquote, urlsplit
import httpcore # type: ignore
import requests_async as requests # type: ignore
import websockets # type: ignore
import httpx
import websockets
from sanic.asgi import ASGIApp
from sanic.exceptions import MethodNotSupported
@@ -18,17 +12,18 @@ from sanic.response import text
ASGI_HOST = "mockserver"
HOST = "127.0.0.1"
PORT = 42101
PORT = None
class SanicTestClient:
def __init__(self, app, port=PORT):
def __init__(self, app, port=PORT, host=HOST):
"""Use port=None to bind to a random port"""
self.app = app
self.port = port
self.host = host
def get_new_session(self):
return requests.Session()
return httpx.AsyncClient(verify=False)
async def _local_request(self, method, url, *args, **kwargs):
logger.info(url)
@@ -43,23 +38,26 @@ class SanicTestClient:
try:
response = await getattr(session, method.lower())(
url, verify=False, *args, **kwargs
url, *args, **kwargs
)
except NameError:
raise Exception(response.status_code)
response.body = await response.aread()
response.status = response.status_code
response.content_type = response.headers.get("content-type")
# response can be decoded as json after response._content
# is set by response.aread()
try:
response.json = response.json()
except (JSONDecodeError, UnicodeDecodeError):
response.json = None
response.body = await response.read()
response.status = response.status_code
response.content_type = response.headers.get("content-type")
if raw_cookies:
response.raw_cookies = {}
for cookie in response.cookies:
for cookie in response.cookies.jar:
response.raw_cookies[cookie.name] = cookie
return response
@@ -71,6 +69,7 @@ class SanicTestClient:
gather_request=True,
debug=False,
server_kwargs={"auto_reload": False},
host=None,
*request_args,
**request_kwargs,
):
@@ -95,24 +94,28 @@ class SanicTestClient:
return self.app.error_handler.default(request, exception)
if self.port:
server_kwargs = dict(host=HOST, port=self.port, **server_kwargs)
host, port = HOST, self.port
server_kwargs = dict(
host=host or self.host, port=self.port, **server_kwargs,
)
host, port = host or self.host, self.port
else:
sock = socket()
sock.bind((HOST, 0))
sock.bind((host or self.host, 0))
server_kwargs = dict(sock=sock, **server_kwargs)
host, port = sock.getsockname()
self.port = port
if uri.startswith(
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
):
url = uri
else:
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
uri = uri if uri.startswith("/") else f"/{uri}"
scheme = "ws" if method == "websocket" else "http"
url = "{scheme}://{host}:{port}{uri}".format(
scheme=scheme, host=host, port=port, uri=uri
)
url = f"{scheme}://{host}:{port}{uri}"
# Tests construct URLs using PORT = None, which means random port not
# known until this function is called, so fix that here
url = url.replace(":None/", f":{port}/")
@self.app.listener("after_server_start")
async def _collect_response(sanic, loop):
@@ -130,7 +133,7 @@ class SanicTestClient:
self.app.listeners["after_server_start"].pop()
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
raise ValueError(f"Exception during request: {exceptions}")
if gather_request:
try:
@@ -138,17 +141,13 @@ class SanicTestClient:
return request, response
except BaseException: # noqa
raise ValueError(
"Request and response object expected, got ({})".format(
results
)
f"Request and response object expected, got ({results})"
)
else:
try:
return results[-1]
except BaseException: # noqa
raise ValueError(
"Request object expected, got ({})".format(results)
)
raise ValueError(f"Request object expected, got ({results})")
def get(self, *args, **kwargs):
return self._sanic_endpoint_test("get", *args, **kwargs)
@@ -175,181 +174,6 @@ class SanicTestClient:
return self._sanic_endpoint_test("websocket", *args, **kwargs)
class SanicASGIAdapter(requests.asgi.ASGIAdapter): # noqa
async def send( # type: ignore
self,
request: requests.PreparedRequest,
gather_return: bool = False,
*args: typing.Any,
**kwargs: typing.Any,
) -> requests.Response:
"""This method is taken MOSTLY verbatim from requests-asyn. The
difference is the capturing of a response on the ASGI call and then
returning it on the response object. This is implemented to achieve:
request, response = await app.asgi_client.get("/")
You can see the original code here:
https://github.com/encode/requests-async/blob/614f40f77f19e6c6da8a212ae799107b0384dbf9/requests_async/asgi.py#L51""" # noqa
scheme, netloc, path, query, fragment = urlsplit(
request.url
) # type: ignore
default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme]
if ":" in netloc:
host, port_string = netloc.split(":", 1)
port = int(port_string)
else:
host = netloc
port = default_port
# Include the 'host' header.
if "host" in request.headers:
headers = [] # type: typing.List[typing.Tuple[bytes, bytes]]
elif port == default_port:
headers = [(b"host", host.encode())]
else:
headers = [(b"host", (f"{host}:{port}").encode())]
# Include other request headers.
headers += [
(key.lower().encode(), value.encode())
for key, value in request.headers.items()
]
no_response = False
if scheme in {"ws", "wss"}:
subprotocol = request.headers.get("sec-websocket-protocol", None)
if subprotocol is None:
subprotocols = [] # type: typing.Sequence[str]
else:
subprotocols = [
value.strip() for value in subprotocol.split(",")
]
scope = {
"type": "websocket",
"path": unquote(path),
"root_path": "",
"scheme": scheme,
"query_string": query.encode(),
"headers": headers,
"client": ["testclient", 50000],
"server": [host, port],
"subprotocols": subprotocols,
}
no_response = True
else:
scope = {
"type": "http",
"http_version": "1.1",
"method": request.method,
"path": unquote(path),
"root_path": "",
"scheme": scheme,
"query_string": query.encode(),
"headers": headers,
"client": ["testclient", 50000],
"server": [host, port],
"extensions": {"http.response.template": {}},
}
async def receive():
nonlocal request_complete, response_complete
if request_complete:
while not response_complete:
await asyncio.sleep(0.0001)
return {"type": "http.disconnect"}
body = request.body
if isinstance(body, str):
body_bytes = body.encode("utf-8") # type: bytes
elif body is None:
body_bytes = b""
elif isinstance(body, types.GeneratorType):
try:
chunk = body.send(None)
if isinstance(chunk, str):
chunk = chunk.encode("utf-8")
return {
"type": "http.request",
"body": chunk,
"more_body": True,
}
except StopIteration:
request_complete = True
return {"type": "http.request", "body": b""}
else:
body_bytes = body
request_complete = True
return {"type": "http.request", "body": body_bytes}
request_complete = False
response_started = False
response_complete = False
raw_kwargs = {"content": b""} # type: typing.Dict[str, typing.Any]
template = None
context = None
return_value = None
async def send(message) -> None:
nonlocal raw_kwargs, response_started, response_complete, template, context # noqa
if message["type"] == "http.response.start":
assert (
not response_started
), 'Received multiple "http.response.start" messages.'
raw_kwargs["status_code"] = message["status"]
raw_kwargs["headers"] = message["headers"]
response_started = True
elif message["type"] == "http.response.body":
assert response_started, (
'Received "http.response.body" '
'without "http.response.start".'
)
assert (
not response_complete
), 'Received "http.response.body" after response completed.'
body = message.get("body", b"")
more_body = message.get("more_body", False)
if request.method != "HEAD":
raw_kwargs["content"] += body
if not more_body:
response_complete = True
elif message["type"] == "http.response.template":
template = message["template"]
context = message["context"]
try:
return_value = await self.app(scope, receive, send)
except BaseException as exc:
if not self.suppress_exceptions:
raise exc from None
if no_response:
response_started = True
raw_kwargs = {"status_code": 204, "headers": []}
if not self.suppress_exceptions:
assert response_started, "TestClient did not receive any response."
elif not response_started:
raw_kwargs = {"status_code": 500, "headers": []}
raw = httpcore.Response(**raw_kwargs)
response = self.build_response(request, raw)
if template is not None:
response.template = template
response.context = context
if gather_return:
response.return_value = return_value
return response
class TestASGIApp(ASGIApp):
async def __call__(self):
await super().__call__()
@@ -361,27 +185,31 @@ async def app_call_with_return(self, scope, receive, send):
return await asgi_app()
class SanicASGITestClient(requests.ASGISession):
class SanicASGIDispatch(httpx.ASGIDispatch):
pass
class SanicASGITestClient(httpx.AsyncClient):
def __init__(
self,
app,
base_url: str = "http://{}".format(ASGI_HOST),
base_url: str = f"http://{ASGI_HOST}",
suppress_exceptions: bool = False,
) -> None:
app.__class__.__call__ = app_call_with_return
app.asgi = True
super().__init__(app)
adapter = SanicASGIAdapter(
app, suppress_exceptions=suppress_exceptions
)
self.mount("http://", adapter)
self.mount("https://", adapter)
self.mount("ws://", adapter)
self.mount("wss://", adapter)
self.headers.update({"user-agent": "testclient"})
self.app = app
self.base_url = base_url
dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT or 0))
super().__init__(dispatch=dispatch, base_url=base_url)
self.last_request = None
def _collect_request(request):
self.last_request = request
app.request_middleware.appendleft(_collect_request)
async def request(self, method, url, gather_request=True, *args, **kwargs):
@@ -391,33 +219,39 @@ class SanicASGITestClient(requests.ASGISession):
response.body = response.content
response.content_type = response.headers.get("content-type")
if hasattr(response, "return_value"):
request = response.return_value
del response.return_value
return request, response
return response
def merge_environment_settings(self, *args, **kwargs):
settings = super().merge_environment_settings(*args, **kwargs)
settings.update({"gather_return": self.gather_request})
return settings
return self.last_request, response
async def websocket(self, uri, subprotocols=None, *args, **kwargs):
if uri.startswith(("ws:", "wss:")):
url = uri
else:
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
url = "ws://testserver{uri}".format(uri=uri)
scheme = "ws"
path = uri
root_path = f"{scheme}://{ASGI_HOST}"
headers = kwargs.get("headers", {})
headers.setdefault("connection", "upgrade")
headers.setdefault("sec-websocket-key", "testserver==")
headers.setdefault("sec-websocket-version", "13")
if subprotocols is not None:
headers.setdefault(
"sec-websocket-protocol", ", ".join(subprotocols)
)
kwargs["headers"] = headers
headers = kwargs.get("headers", {})
headers.setdefault("connection", "upgrade")
headers.setdefault("sec-websocket-key", "testserver==")
headers.setdefault("sec-websocket-version", "13")
if subprotocols is not None:
headers.setdefault(
"sec-websocket-protocol", ", ".join(subprotocols)
)
return await self.request("websocket", url, **kwargs)
scope = {
"type": "websocket",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"headers": [map(lambda y: y.encode(), x) for x in headers.items()],
"scheme": scheme,
"root_path": root_path,
"path": path,
"query_string": b"",
}
async def receive():
return {}
async def send(message):
pass
await self.app(scope, receive, send)
return None, {}

View File

@@ -96,14 +96,10 @@ class CompositionView:
handler.is_stream = stream
for method in methods:
if method not in HTTP_METHODS:
raise InvalidUsage(
"{} is not a valid HTTP method.".format(method)
)
raise InvalidUsage(f"{method} is not a valid HTTP method.")
if method in self.handlers:
raise InvalidUsage(
"Method {} is already registered.".format(method)
)
raise InvalidUsage(f"Method {method} is already registered.")
self.handlers[method] = handler
def __call__(self, request, *args, **kwargs):

View File

@@ -1,6 +1,6 @@
[tool.towncrier]
package = "sanic"
package_dir = "."
package_dir = ".."
filename = "../CHANGELOG.rst"
directory = "./changelogs"
underlines = ["=", "*", "~"]

View File

@@ -14,7 +14,7 @@ multi_line_output = 3
not_skip = __init__.py
[version]
current_version = 19.9.0
current_version = 19.12.0
files = sanic/__version__.py
current_version_pattern = __version__ = "{current_version}"
new_version_pattern = __version__ = "{new_version}"

View File

@@ -5,6 +5,7 @@ import codecs
import os
import re
import sys
from distutils.util import strtobool
from setuptools import setup
@@ -60,12 +61,14 @@ setup_kwargs = {
"long_description": long_description,
"packages": ["sanic"],
"platforms": "any",
"python_requires": ">=3.6",
"classifiers": [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
],
}
@@ -82,7 +85,7 @@ requirements = [
"aiofiles>=0.3.0",
"websockets>=7.0,<9.0",
"multidict>=4.0,<5.0",
"requests-async==0.5.0",
"httpx==0.11.1",
]
tests_require = [

View File

@@ -103,7 +103,7 @@ def sanic_router():
for method, route in route_details:
try:
router._add(
uri="/{}".format(route),
uri=f"/{route}",
methods=frozenset({method}),
host="localhost",
handler=_handler,

View File

@@ -6,6 +6,7 @@ from inspect import isawaitable
import pytest
from sanic import Sanic
from sanic.exceptions import SanicException
from sanic.response import text
@@ -44,16 +45,35 @@ def test_create_asyncio_server(app):
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
def test_asyncio_server_start_serving(app):
def test_asyncio_server_no_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43123,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
def test_asyncio_server_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43124,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
wait_close = srv.close()
loop.run_until_complete(wait_close)
# Looks like we can't easily test `serve_forever()`
def test_app_loop_not_running(app):
with pytest.raises(SanicException) as excinfo:
@@ -94,7 +114,7 @@ def test_app_route_raise_value_error(app):
def test_app_handle_request_handler_is_none(app, monkeypatch):
def mockreturn(*args, **kwargs):
return None, [], {}, ""
return None, [], {}, "", ""
# Not sure how to make app.router.get() return None, so use mock here.
monkeypatch.setattr(app.router, "get", mockreturn)
@@ -105,10 +125,7 @@ def test_app_handle_request_handler_is_none(app, monkeypatch):
request, response = app.test_client.get("/test")
assert (
response.text
== "Error: 'None' was returned while requesting a handler from the router"
)
assert "'None' was returned while requesting a handler from the router" in response.text
@pytest.mark.parametrize("websocket_enabled", [True, False])
@@ -166,9 +183,7 @@ def test_handle_request_with_nested_exception_debug(app, monkeypatch):
request, response = app.test_client.get("/", debug=True)
assert response.status == 500
assert response.text.startswith(
"Error while handling error: {}\nStack: Traceback (most recent call last):\n".format(
err_msg
)
f"Error while handling error: {err_msg}\nStack: Traceback (most recent call last):\n"
)
@@ -188,10 +203,17 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
with caplog.at_level(logging.ERROR):
request, response = app.test_client.get("/")
port = request.server_port
assert port > 0
assert response.status == 500
assert response.text == "Error: Mock SanicException"
assert "Mock SanicException" in response.text
assert (
"sanic.root",
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'",
) in caplog.record_tuples
def test_app_name_required():
with pytest.deprecated_call():
Sanic()

View File

@@ -1,6 +1,7 @@
import asyncio
import sys
from collections import deque
from collections import deque, namedtuple
import pytest
import uvicorn
@@ -81,7 +82,12 @@ def test_listeners_triggered(app):
with pytest.warns(UserWarning):
server.run()
for task in asyncio.Task.all_tasks():
all_tasks = (
asyncio.Task.all_tasks()
if sys.version_info < (3, 7) else
asyncio.all_tasks(asyncio.get_event_loop())
)
for task in all_tasks:
task.cancel()
assert before_server_start
@@ -126,7 +132,12 @@ def test_listeners_triggered_async(app):
with pytest.warns(UserWarning):
server.run()
for task in asyncio.Task.all_tasks():
all_tasks = (
asyncio.Task.all_tasks()
if sys.version_info < (3, 7) else
asyncio.all_tasks(asyncio.get_event_loop())
)
for task in all_tasks:
task.cancel()
assert before_server_start
@@ -221,7 +232,7 @@ async def test_request_class_custom():
class MyCustomRequest(Request):
pass
app = Sanic(request_class=MyCustomRequest)
app = Sanic(name=__name__, request_class=MyCustomRequest)
@app.get("/custom")
def custom_request(request):
@@ -245,17 +256,26 @@ async def test_cookie_customization(app):
return response
_, response = await app.asgi_client.get("/cookie")
CookieDef = namedtuple("CookieDef", ("value", "httponly"))
Cookie = namedtuple("Cookie", ("domain", "path", "value", "httponly"))
cookie_map = {
"test": {"value": "Cookie1", "HttpOnly": True},
"c2": {"value": "Cookie2", "HttpOnly": False},
"test": CookieDef("Cookie1", True),
"c2": CookieDef("Cookie2", False),
}
for k, v in (
response.cookies._cookies.get("mockserver.local").get("/").items()
):
assert cookie_map.get(k).get("value") == v.value
if cookie_map.get(k).get("HttpOnly"):
assert "HttpOnly" in v._rest.keys()
cookies = {
c.name: Cookie(c.domain, c.path, c.value, "HttpOnly" in c._rest.keys())
for c in response.cookies.jar
}
for name, definition in cookie_map.items():
cookie = cookies.get(name)
assert cookie
assert cookie.value == definition.value
assert cookie.domain == "mockserver.local"
assert cookie.path == "/"
assert cookie.httponly == definition.httponly
@pytest.mark.asyncio

View File

@@ -18,4 +18,4 @@ def test_bad_request_response(app):
app.run(host="127.0.0.1", port=42101, debug=False)
assert lines[0] == b"HTTP/1.1 400 Bad Request\r\n"
assert lines[-1] == b"Error: Bad Request"
assert b"Bad Request" in lines[-1]

View File

@@ -40,9 +40,9 @@ def test_bp_group_with_additional_route_params(app: Sanic):
)
def blueprint_2_named_method(request: Request, param):
if request.method == "DELETE":
return text("DELETE_{}".format(param))
return text(f"DELETE_{param}")
elif request.method == "PATCH":
return text("PATCH_{}".format(param))
return text(f"PATCH_{param}")
blueprint_group = Blueprint.group(
blueprint_1, blueprint_2, url_prefix="/api"
@@ -83,7 +83,7 @@ def test_bp_group_with_additional_route_params(app: Sanic):
_, response = app.test_client.patch("/api/bp2/route/bp2", headers=header)
assert response.text == "PATCH_bp2"
_, response = app.test_client.get("/v2/api/bp1/request_path")
_, response = app.test_client.put("/v2/api/bp1/request_path")
assert response.status == 401
@@ -141,8 +141,8 @@ def test_bp_group(app: Sanic):
_, response = app.test_client.get("/api/bp3")
assert response.text == "BP3_OK"
assert MIDDLEWARE_INVOKE_COUNTER["response"] == 4
assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4
assert MIDDLEWARE_INVOKE_COUNTER["response"] == 3
assert MIDDLEWARE_INVOKE_COUNTER["request"] == 2
def test_bp_group_list_operations(app: Sanic):

View File

@@ -46,19 +46,19 @@ def test_versioned_routes_get(app, method):
func = getattr(bp, method)
if callable(func):
@func("/{}".format(method), version=1)
@func(f"/{method}", version=1)
def handler(request):
return text("OK")
else:
print(func)
raise Exception("{} is not callable".format(func))
raise Exception(f"{func} is not callable")
app.blueprint(bp)
client_method = getattr(app.test_client, method)
request, response = client_method("/v1/{}".format(method))
request, response = client_method(f"/v1/{method}")
assert response.status == 200
@@ -253,7 +253,7 @@ def test_several_bp_with_host(app):
def test_bp_middleware(app):
blueprint = Blueprint("test_middleware")
blueprint = Blueprint("test_bp_middleware")
@blueprint.middleware("response")
async def process_response(request, response):
@@ -268,8 +268,40 @@ def test_bp_middleware(app):
request, response = app.test_client.get("/")
assert response.status == 200
assert response.text == "OK"
assert response.text == "FAIL"
def test_bp_middleware_order(app):
blueprint = Blueprint("test_bp_middleware_order")
order = list()
@blueprint.middleware("request")
def mw_1(request):
order.append(1)
@blueprint.middleware("request")
def mw_2(request):
order.append(2)
@blueprint.middleware("request")
def mw_3(request):
order.append(3)
@blueprint.middleware("response")
def mw_4(request, response):
order.append(6)
@blueprint.middleware("response")
def mw_5(request, response):
order.append(5)
@blueprint.middleware("response")
def mw_6(request, response):
order.append(4)
@blueprint.route("/")
def process_response(request):
return text("OK")
app.blueprint(blueprint)
order.clear()
request, response = app.test_client.get("/")
assert response.status == 200
assert order == [1, 2, 3, 4, 5, 6]
def test_bp_exception_handler(app):
blueprint = Blueprint("test_middleware")
@@ -554,7 +586,7 @@ def test_bp_group_with_default_url_prefix(app):
resource_id = str(uuid4())
request, response = app.test_client.get(
"/api/v1/resources/{0}".format(resource_id)
f"/api/v1/resources/{resource_id}"
)
assert response.json == {"resource_id": resource_id}
@@ -669,9 +701,9 @@ def test_duplicate_blueprint(app):
app.blueprint(bp1)
assert str(excinfo.value) == (
'A blueprint with the name "{}" is already registered. '
f'A blueprint with the name "{bp_name}" is already registered. '
"Blueprint names must be unique."
).format(bp_name)
)
@pytest.mark.parametrize("debug", [True, False, None])

View File

@@ -44,42 +44,42 @@ def test_load_from_object_string_exception(app):
def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic()
app = Sanic(name=__name__)
assert app.config.TEST_ANSWER == 42
del environ["SANIC_TEST_ANSWER"]
def test_auto_load_bool_env():
environ["SANIC_TEST_ANSWER"] = "True"
app = Sanic()
app = Sanic(name=__name__)
assert app.config.TEST_ANSWER == True
del environ["SANIC_TEST_ANSWER"]
def test_dont_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(load_env=False)
app = Sanic(name=__name__, load_env=False)
assert getattr(app.config, "TEST_ANSWER", None) is None
del environ["SANIC_TEST_ANSWER"]
def test_load_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(load_env="MYAPP_")
app = Sanic(name=__name__, load_env="MYAPP_")
assert app.config.TEST_ANSWER == 42
del environ["MYAPP_TEST_ANSWER"]
def test_load_env_prefix_float_values():
environ["MYAPP_TEST_ROI"] = "2.3"
app = Sanic(load_env="MYAPP_")
app = Sanic(name=__name__, load_env="MYAPP_")
assert app.config.TEST_ROI == 2.3
del environ["MYAPP_TEST_ROI"]
def test_load_env_prefix_string_value():
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
app = Sanic(load_env="MYAPP_")
app = Sanic(name=__name__, load_env="MYAPP_")
assert app.config.TEST_TOKEN == "somerandomtesttoken"
del environ["MYAPP_TEST_TOKEN"]

View File

@@ -15,7 +15,8 @@ from sanic.response import text
def test_cookies(app):
@app.route("/")
def handler(request):
response = text("Cookies are: {}".format(request.cookies["test"]))
cookie_value = request.cookies["test"]
response = text(f"Cookies are: {cookie_value}")
response.cookies["right_back"] = "at you"
return response
@@ -31,7 +32,8 @@ def test_cookies(app):
async def test_cookies_asgi(app):
@app.route("/")
def handler(request):
response = text("Cookies are: {}".format(request.cookies["test"]))
cookie_value = request.cookies["test"]
response = text(f"Cookies are: {cookie_value}")
response.cookies["right_back"] = "at you"
return response
@@ -52,7 +54,7 @@ def test_false_cookies_encoded(app, httponly, expected):
response = text("hello cookies")
response.cookies["hello"] = "world"
response.cookies["hello"]["httponly"] = httponly
return text(response.cookies["hello"].encode("utf8"))
return text(response.cookies["hello"].encode("utf8").decode())
request, response = app.test_client.get("/")
@@ -78,7 +80,8 @@ def test_false_cookies(app, httponly, expected):
def test_http2_cookies(app):
@app.route("/")
async def handler(request):
response = text("Cookies are: {}".format(request.cookies["test"]))
cookie_value = request.cookies["test"]
response = text(f"Cookies are: {cookie_value}")
return response
headers = {"cookie": "test=working!"}

View File

@@ -17,12 +17,12 @@ def test_create_task(app):
@app.route("/early")
def not_set(request):
return text(e.is_set())
return text(str(e.is_set()))
@app.route("/late")
async def set(request):
await asyncio.sleep(0.1)
return text(e.is_set())
return text(str(e.is_set()))
request, response = app.test_client.get("/early")
assert response.body == b"False"

View File

@@ -20,7 +20,7 @@ class CustomRequest(Request):
def test_custom_request():
app = Sanic(request_class=CustomRequest)
app = Sanic(name=__name__, request_class=CustomRequest)
@app.route("/post", methods=["POST"])
async def post_handler(request):

View File

@@ -172,7 +172,7 @@ def test_handled_unhandled_exception(exception_app):
request, response = exception_app.test_client.get("/divide_by_zero")
assert response.status == 500
soup = BeautifulSoup(response.body, "html.parser")
assert soup.h1.text == "Internal Server Error"
assert "Internal Server Error" in soup.h1.text
message = " ".join(soup.p.text.split())
assert message == (
@@ -218,4 +218,4 @@ def test_abort(exception_app):
request, response = exception_app.test_client.get("/abort/message")
assert response.status == 500
assert response.text == "Error: Abort"
assert "Abort" in response.text

View File

@@ -43,7 +43,7 @@ def handler_6(request, arg):
try:
foo = 1 / arg
except Exception as e:
raise e from ValueError("{}".format(arg))
raise e from ValueError(f"{arg}")
return text(foo)
@@ -86,7 +86,7 @@ def test_html_traceback_output_in_debug_mode():
summary_text = " ".join(soup.select(".summary")[0].text.split())
assert (
"NameError: name 'bar' " "is not defined while handling path /4"
"NameError: name 'bar' is not defined while handling path /4"
) == summary_text
@@ -112,7 +112,7 @@ def test_chained_exception_handler():
summary_text = " ".join(soup.select(".summary")[0].text.split())
assert (
"ZeroDivisionError: division by zero " "while handling path /6/0"
"ZeroDivisionError: division by zero while handling path /6/0"
) == summary_text

View File

@@ -8,27 +8,33 @@ from sanic import headers
[
("text/plain", ("text/plain", {})),
("text/vnd.just.made.this.up ; ", ("text/vnd.just.made.this.up", {})),
("text/plain;charset=us-ascii", ("text/plain", {"charset": "us-ascii"})),
('text/plain ; charset="us-ascii"', ("text/plain", {"charset": "us-ascii"})),
(
"text/plain;charset=us-ascii",
("text/plain", {"charset": "us-ascii"}),
),
(
'text/plain ; charset="us-ascii"',
("text/plain", {"charset": "us-ascii"}),
),
(
'text/plain ; charset="us-ascii"; another=opt',
("text/plain", {"charset": "us-ascii", "another": "opt"})
("text/plain", {"charset": "us-ascii", "another": "opt"}),
),
(
'attachment; filename="silly.txt"',
("attachment", {"filename": "silly.txt"})
("attachment", {"filename": "silly.txt"}),
),
(
'attachment; filename="strange;name"',
("attachment", {"filename": "strange;name"})
("attachment", {"filename": "strange;name"}),
),
(
'attachment; filename="strange;name";size=123;',
("attachment", {"filename": "strange;name", "size": "123"})
("attachment", {"filename": "strange;name", "size": "123"}),
),
(
'form-data; name="files"; filename="fo\\"o;bar\\"',
('form-data', {'name': 'files', 'filename': 'fo"o;bar\\'})
("form-data", {"name": "files", "filename": 'fo"o;bar\\'})
# cgi.parse_header:
# ('form-data', {'name': 'files', 'filename': 'fo"o;bar\\'})
# werkzeug.parse_options_header:
@@ -39,7 +45,7 @@ from sanic import headers
# Chrome:
# Content-Disposition: form-data; name="foo%22;bar\"; filename="😀"
'form-data; name="foo%22;bar\\"; filename="😀"',
('form-data', {'name': 'foo";bar\\', 'filename': '😀'})
("form-data", {"name": 'foo";bar\\', "filename": "😀"})
# cgi: ('form-data', {'name': 'foo%22;bar"; filename="😀'})
# werkzeug: ('form-data', {'name': 'foo%22;bar"; filename='})
),
@@ -47,11 +53,11 @@ from sanic import headers
# Firefox:
# Content-Disposition: form-data; name="foo\";bar\"; filename="😀"
'form-data; name="foo\\";bar\\"; filename="😀"',
('form-data', {'name': 'foo";bar\\', 'filename': '😀'})
("form-data", {"name": 'foo";bar\\', "filename": "😀"})
# cgi: ('form-data', {'name': 'foo";bar"; filename="😀'})
# werkzeug: ('form-data', {'name': 'foo";bar"; filename='})
),
]
],
)
def test_parse_headers(input, expected):
assert headers.parse_content_header(input) == expected

View File

@@ -1,44 +1,61 @@
import asyncio
import functools
import socket
from asyncio import sleep as aio_sleep
from http.client import _encode
from json import JSONDecodeError
import httpcore
import requests_async as requests
from httpcore import PoolTimeout
import httpx
from sanic import Sanic, server
from sanic.response import text
from sanic.testing import HOST, PORT, SanicTestClient
from sanic.testing import HOST, SanicTestClient
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
old_conn = None
PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port
class ReusableSanicConnectionPool(httpcore.ConnectionPool):
async def acquire_connection(self, origin):
class ReusableSanicConnectionPool(
httpx.dispatch.connection_pool.ConnectionPool
):
@property
def cert(self):
return self.ssl.cert
@property
def verify(self):
return self.ssl.verify
@property
def trust_env(self):
return self.ssl.trust_env
@property
def http2(self):
return self.ssl.http2
async def acquire_connection(self, origin, timeout):
global old_conn
connection = self.active_connections.pop_by_origin(
origin, http2_only=True
)
if connection is None:
connection = self.keepalive_connections.pop_by_origin(origin)
connection = self.pop_connection(origin)
if connection is None:
await self.max_connections.acquire()
connection = httpcore.HTTPConnection(
pool_timeout = None if timeout is None else timeout.pool_timeout
await self.max_connections.acquire(timeout=pool_timeout)
ssl_config = httpx.config.SSLConfig(
cert=self.cert,
verify=self.verify,
trust_env=self.trust_env,
http2=self.http2
)
connection = httpx.dispatch.connection.HTTPConnection(
origin,
ssl=self.ssl,
timeout=self.timeout,
ssl=ssl_config,
backend=self.backend,
release_func=self.release_connection,
uds=self.uds,
)
self.active_connections.add(connection)
if old_conn is not None:
@@ -51,17 +68,10 @@ class ReusableSanicConnectionPool(httpcore.ConnectionPool):
return connection
class ReusableSanicAdapter(requests.adapters.HTTPAdapter):
def __init__(self):
self.pool = ReusableSanicConnectionPool()
class ResusableSanicSession(requests.Session):
class ResusableSanicSession(httpx.AsyncClient):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
adapter = ReusableSanicAdapter()
self.mount("http://", adapter)
self.mount("https://", adapter)
dispatch = ReusableSanicConnectionPool()
super().__init__(dispatch=dispatch, *args, **kwargs)
class ReuseableSanicTestClient(SanicTestClient):
@@ -74,6 +84,9 @@ class ReuseableSanicTestClient(SanicTestClient):
self._tcp_connector = None
self._session = None
def get_new_session(self):
return ResusableSanicSession()
# Copied from SanicTestClient, but with some changes to reuse the
# same loop for the same app.
def _sanic_endpoint_test(
@@ -103,11 +116,9 @@ class ReuseableSanicTestClient(SanicTestClient):
):
url = uri
else:
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
uri = uri if uri.startswith("/") else f"/{uri}"
scheme = "http"
url = "{scheme}://{host}:{port}{uri}".format(
scheme=scheme, host=HOST, port=PORT, uri=uri
)
url = f"{scheme}://{HOST}:{PORT}{uri}"
@self.app.listener("after_server_start")
async def _collect_response(loop):
@@ -140,7 +151,7 @@ class ReuseableSanicTestClient(SanicTestClient):
self.app.listeners["after_server_start"].pop()
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
raise ValueError(f"Exception during request: {exceptions}")
if gather_request:
self.app.request_middleware.pop()
@@ -149,16 +160,14 @@ class ReuseableSanicTestClient(SanicTestClient):
return request, response
except Exception:
raise ValueError(
"Request and response object expected, got ({})".format(
results
)
f"Request and response object expected, got ({results})"
)
else:
try:
return results[-1]
except Exception:
raise ValueError(
"Request object expected, got ({})".format(results)
f"Request object expected, got ({results})"
)
def kill_server(self):
@@ -167,10 +176,9 @@ class ReuseableSanicTestClient(SanicTestClient):
self._server.close()
self._loop.run_until_complete(self._server.wait_closed())
self._server = None
self.app.stop()
if self._session:
self._loop.run_until_complete(self._session.close())
self._loop.run_until_complete(self._session.aclose())
self._session = None
except Exception as e3:
@@ -186,10 +194,10 @@ class ReuseableSanicTestClient(SanicTestClient):
"request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"]
)
if not self._session:
self._session = ResusableSanicSession()
self._session = self.get_new_session()
try:
response = await getattr(self._session, method.lower())(
url, verify=False, timeout=request_keepalive, *args, **kwargs
url, timeout=request_keepalive, *args, **kwargs
)
except NameError:
raise Exception(response.status_code)
@@ -199,7 +207,7 @@ class ReuseableSanicTestClient(SanicTestClient):
except (JSONDecodeError, UnicodeDecodeError):
response.json = None
response.body = await response.read()
response.body = await response.aread()
response.status = response.status_code
response.content_type = response.headers.get("content-type")

View File

@@ -1,4 +1,5 @@
import logging
import os
import uuid
from importlib import reload
@@ -12,6 +13,7 @@ import sanic
from sanic import Sanic
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
from sanic.response import text
from sanic.testing import SanicTestClient
logging_format = """module: %(module)s; \
@@ -127,7 +129,7 @@ def test_log_connection_lost(app, debug, monkeypatch):
def test_logger(caplog):
rand_string = str(uuid.uuid4())
app = Sanic()
app = Sanic(name=__name__)
@app.get("/")
def log_info(request):
@@ -137,15 +139,67 @@ def test_logger(caplog):
with caplog.at_level(logging.INFO):
request, response = app.test_client.get("/")
port = request.server_port
# Note: testing with random port doesn't show the banner because it doesn't
# define host and port. This test supports both modes.
if caplog.record_tuples[0] == (
"sanic.root",
logging.INFO,
f"Goin' Fast @ http://127.0.0.1:{port}",
):
caplog.record_tuples.pop(0)
assert caplog.record_tuples[0] == (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:42101",
f"http://127.0.0.1:{port}/",
)
assert caplog.record_tuples[1] == ("sanic.root", logging.INFO, rand_string)
assert caplog.record_tuples[-1] == (
"sanic.root",
logging.INFO,
"Server Stopped",
)
def test_logger_static_and_secure(caplog):
# Same as test_logger, except for more coverage:
# - test_client initialised separately for static port
# - using ssl
rand_string = str(uuid.uuid4())
app = Sanic(name=__name__)
@app.get("/")
def log_info(request):
logger.info(rand_string)
return text("hello")
current_dir = os.path.dirname(os.path.realpath(__file__))
ssl_cert = os.path.join(current_dir, "certs/selfsigned.cert")
ssl_key = os.path.join(current_dir, "certs/selfsigned.key")
ssl_dict = {"cert": ssl_cert, "key": ssl_key}
test_client = SanicTestClient(app, port=42101)
with caplog.at_level(logging.INFO):
request, response = test_client.get(
f"https://127.0.0.1:{test_client.port}/",
server_kwargs=dict(ssl=ssl_dict),
)
port = test_client.port
assert caplog.record_tuples[0] == (
"sanic.root",
logging.INFO,
f"Goin' Fast @ https://127.0.0.1:{port}",
)
assert caplog.record_tuples[1] == (
"sanic.root",
logging.INFO,
"http://127.0.0.1:42101/",
f"https://127.0.0.1:{port}/",
)
assert caplog.record_tuples[2] == ("sanic.root", logging.INFO, rand_string)
assert caplog.record_tuples[-1] == (

View File

@@ -49,10 +49,10 @@ def test_logo_false(app, caplog):
loop.run_until_complete(_server.wait_closed())
app.stop()
banner, port = caplog.record_tuples[ROW][2].rsplit(":", 1)
assert caplog.record_tuples[ROW][1] == logging.INFO
assert caplog.record_tuples[ROW][
2
] == "Goin' Fast @ http://127.0.0.1:{}".format(PORT)
assert banner == "Goin' Fast @ http://127.0.0.1"
assert int(port) > 0
def test_logo_true(app, caplog):

View File

@@ -2,7 +2,7 @@ import logging
from asyncio import CancelledError
from sanic.exceptions import NotFound
from sanic.exceptions import NotFound, SanicException
from sanic.request import Request
from sanic.response import HTTPResponse, text
@@ -93,7 +93,7 @@ def test_middleware_response_raise_cancelled_error(app, caplog):
"sanic.root",
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) in caplog.record_tuples
) not in caplog.record_tuples
def test_middleware_response_raise_exception(app, caplog):
@@ -102,14 +102,16 @@ def test_middleware_response_raise_exception(app, caplog):
raise Exception("Exception at response middleware")
with caplog.at_level(logging.ERROR):
reqrequest, response = app.test_client.get("/")
reqrequest, response = app.test_client.get("/fail")
assert response.status == 404
# 404 errors are not logged
assert (
"sanic.root",
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) in caplog.record_tuples
) not in caplog.record_tuples
# Middleware exception ignored but logged
assert (
"sanic.error",
logging.ERROR,

View File

@@ -21,13 +21,13 @@ def test_versioned_named_routes_get(app, method):
bp = Blueprint("test_bp", url_prefix="/bp")
method = method.lower()
route_name = "route_{}".format(method)
route_name2 = "route2_{}".format(method)
route_name = f"route_{method}"
route_name2 = f"route2_{method}"
func = getattr(app, method)
if callable(func):
@func("/{}".format(method), version=1, name=route_name)
@func(f"/{method}", version=1, name=route_name)
def handler(request):
return text("OK")
@@ -38,7 +38,7 @@ def test_versioned_named_routes_get(app, method):
func = getattr(bp, method)
if callable(func):
@func("/{}".format(method), version=1, name=route_name2)
@func(f"/{method}", version=1, name=route_name2)
def handler2(request):
return text("OK")
@@ -48,14 +48,14 @@ def test_versioned_named_routes_get(app, method):
app.blueprint(bp)
assert app.router.routes_all["/v1/{}".format(method)].name == route_name
assert app.router.routes_all[f"/v1/{method}"].name == route_name
route = app.router.routes_all["/v1/bp/{}".format(method)]
assert route.name == "test_bp.{}".format(route_name2)
route = app.router.routes_all[f"/v1/bp/{method}"]
assert route.name == f"test_bp.{route_name2}"
assert app.url_for(route_name) == "/v1/{}".format(method)
url = app.url_for("test_bp.{}".format(route_name2))
assert url == "/v1/bp/{}".format(method)
assert app.url_for(route_name) == f"/v1/{method}"
url = app.url_for(f"test_bp.{route_name2}")
assert url == f"/v1/bp/{method}"
with pytest.raises(URLBuildError):
app.url_for("handler")

View File

@@ -27,7 +27,7 @@ def test_payload_too_large_at_data_received_default(app):
response = app.test_client.get("/1", gather_request=False)
assert response.status == 413
assert response.text == "Error: Payload Too Large"
assert "Payload Too Large" in response.text
def test_payload_too_large_at_on_header_default(app):
@@ -40,4 +40,4 @@ def test_payload_too_large_at_on_header_default(app):
data = "a" * 1000
response = app.test_client.post("/1", gather_request=False, data=data)
assert response.status == 413
assert response.text == "Error: Payload Too Large"
assert "Payload Too Large" in response.text

View File

@@ -115,14 +115,14 @@ def test_redirect_with_params(app, test_str):
@app.route("/api/v1/test/<test>/")
async def init_handler(request, test):
return redirect("/api/v2/test/{}/".format(use_in_uri))
return redirect(f"/api/v2/test/{use_in_uri}/")
@app.route("/api/v2/test/<test>/")
async def target_handler(request, test):
assert test == test_str
return text("OK")
_, response = app.test_client.get("/api/v1/test/{}/".format(use_in_uri))
_, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/")
assert response.status == 200
assert response.content == b"OK"

View File

@@ -0,0 +1,37 @@
import io
from sanic.response import text
data = "abc" * 10_000_000
def test_request_buffer_queue_size(app):
default_buf_qsz = app.config.get("REQUEST_BUFFER_QUEUE_SIZE")
qsz = 1
while qsz == default_buf_qsz:
qsz += 1
app.config.REQUEST_BUFFER_QUEUE_SIZE = qsz
@app.post("/post", stream=True)
async def post(request):
assert request.stream.buffer_size == qsz
print("request.stream.buffer_size =", request.stream.buffer_size)
bio = io.BytesIO()
while True:
bdata = await request.stream.read()
if not bdata:
break
bio.write(bdata)
head = bdata[:3].decode("utf-8")
tail = bdata[3:][-3:].decode("utf-8")
print(head, "...", tail)
bio.seek(0)
return text(bio.read().decode("utf-8"))
request, response = app.test_client.post("/post", data=data)
assert response.status == 200
assert response.text == data

View File

@@ -8,6 +8,7 @@ try:
except ImportError:
from json import loads
def test_custom_context(app):
@app.middleware("request")
def store(request):
@@ -21,14 +22,16 @@ def test_custom_context(app):
invalid = request.ctx.missing
except AttributeError as e:
invalid = str(e)
return json({
"user": request.ctx.user,
"session": request.ctx.session,
"has_user": hasattr(request.ctx, "user"),
"has_session": hasattr(request.ctx, "session"),
"has_missing": hasattr(request.ctx, "missing"),
"invalid": invalid
})
return json(
{
"user": request.ctx.user,
"session": request.ctx.session,
"has_user": hasattr(request.ctx, "user"),
"has_session": hasattr(request.ctx, "session"),
"has_missing": hasattr(request.ctx, "missing"),
"invalid": invalid,
}
)
request, response = app.test_client.get("/")
assert response.json == {
@@ -41,44 +44,6 @@ def test_custom_context(app):
}
# Remove this once the deprecated API is abolished.
def test_custom_context_old(app):
@app.middleware("request")
def store(request):
try:
request["foo"]
except KeyError:
pass
request["user"] = "sanic"
sidekick = request.get("sidekick", "tails") # Item missing -> default
request["sidekick"] = sidekick
request["bar"] = request["sidekick"]
del request["sidekick"]
@app.route("/")
def handler(request):
return json(
{
"user": request.get("user"),
"sidekick": request.get("sidekick"),
"has_bar": "bar" in request,
"has_sidekick": "sidekick" in request,
}
)
request, response = app.test_client.get("/")
assert response.json == {
"user": "sanic",
"sidekick": None,
"has_bar": True,
"has_sidekick": False,
}
response_json = loads(response.text)
assert response_json["user"] == "sanic"
assert response_json.get("sidekick") is None
def test_app_injection(app):
expected = random.choice(range(0, 100))

View File

@@ -3,12 +3,12 @@ import pytest
from sanic.blueprints import Blueprint
from sanic.exceptions import HeaderExpectationFailed
from sanic.request import StreamBuffer
from sanic.response import stream, text
from sanic.response import json, stream, text
from sanic.views import CompositionView, HTTPMethodView
from sanic.views import stream as stream_decorator
data = "abc" * 10000000
data = "abc" * 1_000_000
def test_request_stream_method_view(app):
@@ -329,15 +329,12 @@ def test_request_stream_handle_exception(app):
# 404
request, response = app.test_client.post("/in_valid_post", data=data)
assert response.status == 404
assert response.text == "Error: Requested URL /in_valid_post not found"
assert "Requested URL /in_valid_post not found" in response.text
# 405
request, response = app.test_client.get("/post/random_id", data=data)
request, response = app.test_client.get("/post/random_id")
assert response.status == 405
assert (
response.text == "Error: Method GET not allowed for URL"
" /post/random_id"
)
assert "Method GET not allowed for URL /post/random_id" in response.text
def test_request_stream_blueprint(app):
@@ -616,3 +613,43 @@ def test_request_stream(app):
request, response = app.test_client.post("/bp_stream", data=data)
assert response.status == 200
assert response.text == data
def test_streaming_new_api(app):
@app.post("/non-stream")
async def handler(request):
assert request.body == b"x"
await request.receive_body() # This should do nothing
assert request.body == b"x"
return text("OK")
@app.post("/1", stream=True)
async def handler(request):
assert request.stream
assert not request.body
await request.receive_body()
return text(request.body.decode().upper())
@app.post("/2", stream=True)
async def handler(request):
ret = []
async for data in request.stream:
# We should have no b"" or None, just proper chunks
assert data
assert isinstance(data, bytes)
ret.append(data.decode("ASCII"))
return json(ret)
request, response = app.test_client.post("/non-stream", data="x")
assert response.status == 200
request, response = app.test_client.post("/1", data="TEST data")
assert request.body == b"TEST data"
assert response.status == 200
assert response.text == "TEST DATA"
request, response = app.test_client.post("/2", data=data)
assert response.status == 200
res = response.json
assert isinstance(res, list)
assert len(res) > 1
assert "".join(res) == data

View File

@@ -1,49 +1,64 @@
import asyncio
import httpcore
import requests_async as requests
import httpx
from sanic import Sanic
from sanic.response import text
from sanic.testing import SanicTestClient
class DelayableSanicConnectionPool(httpcore.ConnectionPool):
class DelayableHTTPConnection(httpx.dispatch.connection.HTTPConnection):
def __init__(self, *args, **kwargs):
self._request_delay = None
if "request_delay" in kwargs:
self._request_delay = kwargs.pop("request_delay")
super().__init__(*args, **kwargs)
async def send(self, request, timeout=None):
if self.connection is None:
self.connection = (await self.connect(timeout=timeout))
if self._request_delay:
await asyncio.sleep(self._request_delay)
response = await self.connection.send(request, timeout=timeout)
return response
class DelayableSanicConnectionPool(
httpx.dispatch.connection_pool.ConnectionPool
):
def __init__(self, request_delay=None, *args, **kwargs):
self._request_delay = request_delay
super().__init__(*args, **kwargs)
async def send(self, request, stream=False, ssl=None, timeout=None):
connection = await self.acquire_connection(request.url.origin)
if (
connection.h11_connection is None
and connection.h2_connection is None
):
await connection.connect(ssl=ssl, timeout=timeout)
if self._request_delay:
await asyncio.sleep(self._request_delay)
try:
response = await connection.send(
request, stream=stream, ssl=ssl, timeout=timeout
async def acquire_connection(self, origin, timeout=None):
connection = self.pop_connection(origin)
if connection is None:
pool_timeout = None if timeout is None else timeout.pool_timeout
await self.max_connections.acquire(timeout=pool_timeout)
connection = DelayableHTTPConnection(
origin,
ssl=self.ssl,
backend=self.backend,
release_func=self.release_connection,
uds=self.uds,
request_delay=self._request_delay,
)
except BaseException as exc:
self.active_connections.remove(connection)
self.max_connections.release()
raise exc
return response
self.active_connections.add(connection)
return connection
class DelayableSanicAdapter(requests.adapters.HTTPAdapter):
def __init__(self, request_delay=None):
self.pool = DelayableSanicConnectionPool(request_delay=request_delay)
class DelayableSanicSession(requests.Session):
class DelayableSanicSession(httpx.AsyncClient):
def __init__(self, request_delay=None, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
adapter = DelayableSanicAdapter(request_delay=request_delay)
self.mount("http://", adapter)
self.mount("https://", adapter)
dispatch = DelayableSanicConnectionPool(request_delay=request_delay)
super().__init__(dispatch=dispatch, *args, **kwargs)
class DelayableSanicTestClient(SanicTestClient):
@@ -81,7 +96,7 @@ def test_default_server_error_request_timeout():
client = DelayableSanicTestClient(request_timeout_default_app, 2)
request, response = client.get("/1")
assert response.status == 408
assert response.text == "Error: Request Timeout"
assert "Request Timeout" in response.text
def test_default_server_error_request_dont_timeout():
@@ -104,4 +119,4 @@ def test_default_server_error_websocket_request_timeout():
request, response = client.get("/ws1", headers=headers)
assert response.status == 408
assert response.text == "Error: Request Timeout"
assert "Request Timeout" in response.text

View File

@@ -10,9 +10,9 @@ import pytest
from sanic import Blueprint, Sanic
from sanic.exceptions import ServerError
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters
from sanic.response import json, text
from sanic.testing import ASGI_HOST, HOST, PORT
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters
from sanic.response import html, json, text
from sanic.testing import ASGI_HOST, HOST, PORT, SanicTestClient
# ------------------------------------------------------------ #
@@ -44,7 +44,7 @@ async def test_sync_asgi(app):
def test_ip(app):
@app.route("/")
def handler(request):
return text("{}".format(request.ip))
return text(f"{request.ip}")
request, response = app.test_client.get("/")
@@ -55,11 +55,11 @@ def test_ip(app):
async def test_ip_asgi(app):
@app.route("/")
def handler(request):
return text("{}".format(request.ip))
return text(f"{request.url}")
request, response = await app.asgi_client.get("/")
assert response.text == "mockserver"
assert response.text == "http://mockserver/"
def test_text(app):
@@ -72,6 +72,41 @@ def test_text(app):
assert response.text == "Hello"
def test_html(app):
class Foo:
def __html__(self):
return "<h1>Foo</h1>"
def _repr_html_(self):
return "<h1>Foo object repr</h1>"
class Bar:
def _repr_html_(self):
return "<h1>Bar object repr</h1>"
@app.route("/")
async def handler(request):
return html("<h1>Hello</h1>")
@app.route("/foo")
async def handler(request):
return html(Foo())
@app.route("/bar")
async def handler(request):
return html(Bar())
request, response = app.test_client.get("/")
assert response.content_type == "text/html; charset=utf-8"
assert response.text == "<h1>Hello</h1>"
request, response = app.test_client.get("/foo")
assert response.text == "<h1>Foo</h1>"
request, response = app.test_client.get("/bar")
assert response.text == "<h1>Bar object repr</h1>"
@pytest.mark.asyncio
async def test_text_asgi(app):
@app.route("/")
@@ -207,24 +242,24 @@ async def test_empty_json_asgi(app):
def test_invalid_json(app):
@app.route("/")
@app.post("/")
async def handler(request):
return json(request.json)
data = "I am not json"
request, response = app.test_client.get("/", data=data)
request, response = app.test_client.post("/", data=data)
assert response.status == 400
@pytest.mark.asyncio
async def test_invalid_json_asgi(app):
@app.route("/")
@app.post("/")
async def handler(request):
return json(request.json)
data = "I am not json"
request, response = await app.asgi_client.get("/", data=data)
request, response = await app.asgi_client.post("/", data=data)
assert response.status == 400
@@ -290,7 +325,7 @@ def test_token(app):
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": "{}".format(token),
"Authorization": f"{token}",
}
request, response = app.test_client.get("/", headers=headers)
@@ -300,7 +335,7 @@ def test_token(app):
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": "Token {}".format(token),
"Authorization": f"Token {token}",
}
request, response = app.test_client.get("/", headers=headers)
@@ -310,7 +345,7 @@ def test_token(app):
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": "Bearer {}".format(token),
"Authorization": f"Bearer {token}",
}
request, response = app.test_client.get("/", headers=headers)
@@ -335,7 +370,7 @@ async def test_token_asgi(app):
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": "{}".format(token),
"Authorization": f"{token}",
}
request, response = await app.asgi_client.get("/", headers=headers)
@@ -345,7 +380,7 @@ async def test_token_asgi(app):
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": "Token {}".format(token),
"Authorization": f"Token {token}",
}
request, response = await app.asgi_client.get("/", headers=headers)
@@ -355,7 +390,7 @@ async def test_token_asgi(app):
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": "Bearer {}".format(token),
"Authorization": f"Bearer {token}",
}
request, response = await app.asgi_client.get("/", headers=headers)
@@ -413,15 +448,15 @@ def test_standard_forwarded(app):
"Forwarded": (
'for=1.1.1.1, for=injected;host="'
', for="[::2]";proto=https;host=me.tld;path="/app/";secret=mySecret'
',for=broken;;secret=b0rked'
', for=127.0.0.3;scheme=http;port=1234'
",for=broken;;secret=b0rked"
", for=127.0.0.3;scheme=http;port=1234"
),
"X-Real-IP": "127.0.0.2",
"X-Forwarded-For": "127.0.1.1",
"X-Scheme": "ws",
}
request, response = app.test_client.get("/", headers=headers)
assert response.json == { "for": "127.0.0.2", "proto": "ws" }
assert response.json == {"for": "127.0.0.2", "proto": "ws"}
assert request.remote_addr == "127.0.0.2"
assert request.scheme == "ws"
assert request.server_port == 80
@@ -433,7 +468,7 @@ def test_standard_forwarded(app):
"proto": "https",
"host": "me.tld",
"path": "/app/",
"secret": "mySecret"
"secret": "mySecret",
}
assert request.remote_addr == "[::2]"
assert request.server_name == "me.tld"
@@ -443,7 +478,7 @@ def test_standard_forwarded(app):
# Empty Forwarded header -> use X-headers
headers["Forwarded"] = ""
request, response = app.test_client.get("/", headers=headers)
assert response.json == { "for": "127.0.0.2", "proto": "ws" }
assert response.json == {"for": "127.0.0.2", "proto": "ws"}
# Header present but not matching anything
request, response = app.test_client.get("/", headers={"Forwarded": "."})
@@ -451,8 +486,8 @@ def test_standard_forwarded(app):
# Forwarded header present but no matching secret -> use X-headers
headers = {
"Forwarded": 'for=1.1.1.1;secret=x, for=127.0.0.1',
"X-Real-IP": "127.0.0.2"
"Forwarded": "for=1.1.1.1;secret=x, for=127.0.0.1",
"X-Real-IP": "127.0.0.2",
}
request, response = app.test_client.get("/", headers=headers)
assert response.json == {"for": "127.0.0.2"}
@@ -464,7 +499,7 @@ def test_standard_forwarded(app):
assert response.json == {
"for": "127.0.0.4",
"port": 1234,
"secret": "mySecret"
"secret": "mySecret",
}
# Test escapes (modify this if you see anyone implementing quoted-pairs)
@@ -472,29 +507,29 @@ def test_standard_forwarded(app):
request, response = app.test_client.get("/", headers=headers)
assert response.json == {
"for": "test",
"quoted": '\\,x=x;y=\\',
"secret": "mySecret"
"quoted": "\\,x=x;y=\\",
"secret": "mySecret",
}
# Secret insulated by malformed field #1
headers = {"Forwarded": 'for=test;secret=mySecret;b0rked;proto=wss;'}
headers = {"Forwarded": "for=test;secret=mySecret;b0rked;proto=wss;"}
request, response = app.test_client.get("/", headers=headers)
assert response.json == {"for": "test", "secret": "mySecret"}
# Secret insulated by malformed field #2
headers = {"Forwarded": 'for=test;b0rked;secret=mySecret;proto=wss'}
headers = {"Forwarded": "for=test;b0rked;secret=mySecret;proto=wss"}
request, response = app.test_client.get("/", headers=headers)
assert response.json == {"proto": "wss", "secret": "mySecret"}
# Unexpected termination should not lose existing acceptable values
headers = {"Forwarded": 'b0rked;secret=mySecret;proto=wss'}
headers = {"Forwarded": "b0rked;secret=mySecret;proto=wss"}
request, response = app.test_client.get("/", headers=headers)
assert response.json == {"proto": "wss", "secret": "mySecret"}
# Field normalization
headers = {
"Forwarded": 'PROTO=WSS;BY="CAFE::8000";FOR=unknown;PORT=X;HOST="A:2";'
'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret'
'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret'
}
request, response = app.test_client.get("/", headers=headers)
assert response.json == {
@@ -507,7 +542,7 @@ def test_standard_forwarded(app):
# Using "by" field as secret
app.config.FORWARDED_SECRET = "_proxySecret"
headers = {"Forwarded": 'for=1.2.3.4; by=_proxySecret'}
headers = {"Forwarded": "for=1.2.3.4; by=_proxySecret"}
request, response = app.test_client.get("/", headers=headers)
assert response.json == {"for": "1.2.3.4", "by": "_proxySecret"}
@@ -525,15 +560,15 @@ async def test_standard_forwarded_asgi(app):
"Forwarded": (
'for=1.1.1.1, for=injected;host="'
', for="[::2]";proto=https;host=me.tld;path="/app/";secret=mySecret'
',for=broken;;secret=b0rked'
', for=127.0.0.3;scheme=http;port=1234'
",for=broken;;secret=b0rked"
", for=127.0.0.3;scheme=http;port=1234"
),
"X-Real-IP": "127.0.0.2",
"X-Forwarded-For": "127.0.1.1",
"X-Scheme": "ws",
}
request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == { "for": "127.0.0.2", "proto": "ws" }
assert response.json() == {"for": "127.0.0.2", "proto": "ws"}
assert request.remote_addr == "127.0.0.2"
assert request.scheme == "ws"
assert request.server_port == 80
@@ -545,7 +580,7 @@ async def test_standard_forwarded_asgi(app):
"proto": "https",
"host": "me.tld",
"path": "/app/",
"secret": "mySecret"
"secret": "mySecret",
}
assert request.remote_addr == "[::2]"
assert request.server_name == "me.tld"
@@ -555,16 +590,18 @@ async def test_standard_forwarded_asgi(app):
# Empty Forwarded header -> use X-headers
headers["Forwarded"] = ""
request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == { "for": "127.0.0.2", "proto": "ws" }
assert response.json() == {"for": "127.0.0.2", "proto": "ws"}
# Header present but not matching anything
request, response = await app.asgi_client.get("/", headers={"Forwarded": "."})
request, response = await app.asgi_client.get(
"/", headers={"Forwarded": "."}
)
assert response.json() == {}
# Forwarded header present but no matching secret -> use X-headers
headers = {
"Forwarded": 'for=1.1.1.1;secret=x, for=127.0.0.1',
"X-Real-IP": "127.0.0.2"
"Forwarded": "for=1.1.1.1;secret=x, for=127.0.0.1",
"X-Real-IP": "127.0.0.2",
}
request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == {"for": "127.0.0.2"}
@@ -576,7 +613,7 @@ async def test_standard_forwarded_asgi(app):
assert response.json() == {
"for": "127.0.0.4",
"port": 1234,
"secret": "mySecret"
"secret": "mySecret",
}
# Test escapes (modify this if you see anyone implementing quoted-pairs)
@@ -584,29 +621,29 @@ async def test_standard_forwarded_asgi(app):
request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == {
"for": "test",
"quoted": '\\,x=x;y=\\',
"secret": "mySecret"
"quoted": "\\,x=x;y=\\",
"secret": "mySecret",
}
# Secret insulated by malformed field #1
headers = {"Forwarded": 'for=test;secret=mySecret;b0rked;proto=wss;'}
headers = {"Forwarded": "for=test;secret=mySecret;b0rked;proto=wss;"}
request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == {"for": "test", "secret": "mySecret"}
# Secret insulated by malformed field #2
headers = {"Forwarded": 'for=test;b0rked;secret=mySecret;proto=wss'}
headers = {"Forwarded": "for=test;b0rked;secret=mySecret;proto=wss"}
request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == {"proto": "wss", "secret": "mySecret"}
# Unexpected termination should not lose existing acceptable values
headers = {"Forwarded": 'b0rked;secret=mySecret;proto=wss'}
headers = {"Forwarded": "b0rked;secret=mySecret;proto=wss"}
request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == {"proto": "wss", "secret": "mySecret"}
# Field normalization
headers = {
"Forwarded": 'PROTO=WSS;BY="CAFE::8000";FOR=unknown;PORT=X;HOST="A:2";'
'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret'
'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret'
}
request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == {
@@ -619,7 +656,7 @@ async def test_standard_forwarded_asgi(app):
# Using "by" field as secret
app.config.FORWARDED_SECRET = "_proxySecret"
headers = {"Forwarded": 'for=1.2.3.4; by=_proxySecret'}
headers = {"Forwarded": "for=1.2.3.4; by=_proxySecret"}
request, response = await app.asgi_client.get("/", headers=headers)
assert response.json() == {"for": "1.2.3.4", "by": "_proxySecret"}
@@ -813,11 +850,14 @@ def test_forwarded_scheme(app):
assert request.scheme == "http"
request, response = app.test_client.get(
"/", headers={"X-Forwarded-For": "127.1.2.3", "X-Forwarded-Proto": "https"}
"/",
headers={"X-Forwarded-For": "127.1.2.3", "X-Forwarded-Proto": "https"},
)
assert request.scheme == "https"
request, response = app.test_client.get("/", headers={"X-Forwarded-For": "127.1.2.3", "X-Scheme": "https"})
request, response = app.test_client.get(
"/", headers={"X-Forwarded-For": "127.1.2.3", "X-Scheme": "https"}
)
assert request.scheme == "https"
@@ -988,8 +1028,8 @@ def test_url_attributes_no_ssl(app, path, query, expected_url):
app.add_route(handler, path)
request, response = app.test_client.get(path + "?{}".format(query))
assert request.url == expected_url.format(HOST, PORT)
request, response = app.test_client.get(path + f"?{query}")
assert request.url == expected_url.format(HOST, request.server_port)
parsed = urlparse(request.url)
@@ -1014,7 +1054,7 @@ async def test_url_attributes_no_ssl_asgi(app, path, query, expected_url):
app.add_route(handler, path)
request, response = await app.asgi_client.get(path + "?{}".format(query))
request, response = await app.asgi_client.get(path + f"?{query}")
assert request.url == expected_url.format(ASGI_HOST)
parsed = urlparse(request.url)
@@ -1046,11 +1086,12 @@ def test_url_attributes_with_ssl_context(app, path, query, expected_url):
app.add_route(handler, path)
port = app.test_client.port
request, response = app.test_client.get(
"https://{}:{}".format(HOST, PORT) + path + "?{}".format(query),
f"https://{HOST}:{PORT}" + path + f"?{query}",
server_kwargs={"ssl": context},
)
assert request.url == expected_url.format(HOST, PORT)
assert request.url == expected_url.format(HOST, request.server_port)
parsed = urlparse(request.url)
@@ -1082,10 +1123,10 @@ def test_url_attributes_with_ssl_dict(app, path, query, expected_url):
app.add_route(handler, path)
request, response = app.test_client.get(
"https://{}:{}".format(HOST, PORT) + path + "?{}".format(query),
f"https://{HOST}:{PORT}" + path + f"?{query}",
server_kwargs={"ssl": ssl_dict},
)
assert request.url == expected_url.format(HOST, PORT)
assert request.url == expected_url.format(HOST, request.server_port)
parsed = urlparse(request.url)
@@ -1566,33 +1607,6 @@ async def test_request_args_no_query_string_await(app):
assert request.args == {}
def test_request_raw_args(app):
params = {"test": "OK"}
@app.get("/")
def handler(request):
return text("pass")
request, response = app.test_client.get("/", params=params)
assert request.raw_args == params
@pytest.mark.asyncio
async def test_request_raw_args_asgi(app):
params = {"test": "OK"}
@app.get("/")
def handler(request):
return text("pass")
request, response = await app.asgi_client.get("/", params=params)
assert request.raw_args == params
def test_request_query_args(app):
# test multiple params with the same key
params = [("test", "value1"), ("test", "value2")]
@@ -1802,26 +1816,6 @@ def test_request_port(app):
assert hasattr(request, "_port")
@pytest.mark.asyncio
async def test_request_port_asgi(app):
@app.get("/")
def handler(request):
return text("OK")
request, response = await app.asgi_client.get("/")
port = request.port
assert isinstance(port, int)
delattr(request, "_socket")
delattr(request, "_port")
port = request.port
assert isinstance(port, int)
assert hasattr(request, "_socket")
assert hasattr(request, "_port")
def test_request_socket(app):
@app.get("/")
def handler(request):
@@ -1872,7 +1866,7 @@ def test_request_server_name_in_host_header(app):
request, response = app.test_client.get(
"/", headers={"Host": "mal_formed"}
)
assert request.server_name == None # For now (later maybe 127.0.0.1)
assert request.server_name == None # For now (later maybe 127.0.0.1)
def test_request_server_name_forwarded(app):
@@ -1883,7 +1877,11 @@ def test_request_server_name_forwarded(app):
app.config.PROXIES_COUNT = 1
request, response = app.test_client.get(
"/",
headers={"Host": "my-server:5555", "X-Forwarded-For": "127.1.2.3", "X-Forwarded-Host": "your-server"},
headers={
"Host": "my-server:5555",
"X-Forwarded-For": "127.1.2.3",
"X-Forwarded-Host": "your-server",
},
)
assert request.server_name == "your-server"
@@ -1893,8 +1891,9 @@ def test_request_server_port(app):
def handler(request):
return text("OK")
request, response = app.test_client.get("/", headers={"Host": "my-server"})
assert request.server_port == app.test_client.port
test_client = SanicTestClient(app)
request, response = test_client.get("/", headers={"Host": "my-server"})
assert request.server_port == test_client.port
def test_request_server_port_in_host_header(app):
@@ -1915,7 +1914,10 @@ def test_request_server_port_in_host_header(app):
request, response = app.test_client.get(
"/", headers={"Host": "mal_formed:5555"}
)
assert request.server_port == app.test_client.port
if PORT is None:
assert request.server_port != 5555
else:
assert request.server_port == app.test_client.port
def test_request_server_port_forwarded(app):
@@ -1925,7 +1927,12 @@ def test_request_server_port_forwarded(app):
app.config.PROXIES_COUNT = 1
request, response = app.test_client.get(
"/", headers={"Host": "my-server:5555", "X-Forwarded-For": "127.1.2.3", "X-Forwarded-Port": "4444"}
"/",
headers={
"Host": "my-server:5555",
"X-Forwarded-For": "127.1.2.3",
"X-Forwarded-Port": "4444",
},
)
assert request.server_port == 4444
@@ -1948,7 +1955,10 @@ def test_server_name_and_url_for(app):
app.config.SERVER_NAME = "my-server"
assert app.url_for("handler", _external=True) == "http://my-server/foo"
request, response = app.test_client.get("/foo")
assert request.url_for("handler") == f"http://my-server:{app.test_client.port}/foo"
assert (
request.url_for("handler")
== f"http://my-server:{request.server_port}/foo"
)
app.config.SERVER_NAME = "https://my-server/path"
request, response = app.test_client.get("/foo")
@@ -1969,7 +1979,12 @@ def test_url_for_with_forwarded_request(app):
app.config.SERVER_NAME = "my-server"
app.config.PROXIES_COUNT = 1
request, response = app.test_client.get(
"/", headers={"X-Forwarded-For": "127.1.2.3", "X-Forwarded-Proto": "https", "X-Forwarded-Port": "6789"}
"/",
headers={
"X-Forwarded-For": "127.1.2.3",
"X-Forwarded-Proto": "https",
"X-Forwarded-Port": "6789",
},
)
assert app.url_for("view_name") == "/another_view"
assert (
@@ -1981,7 +1996,12 @@ def test_url_for_with_forwarded_request(app):
)
request, response = app.test_client.get(
"/", headers={"X-Forwarded-For": "127.1.2.3", "X-Forwarded-Proto": "https", "X-Forwarded-Port": "443"}
"/",
headers={
"X-Forwarded-For": "127.1.2.3",
"X-Forwarded-Proto": "https",
"X-Forwarded-Port": "443",
},
)
assert request.url_for("view_name") == "https://my-server/another_view"
@@ -1998,7 +2018,7 @@ async def test_request_form_invalid_content_type_asgi(app):
def test_endpoint_basic():
app = Sanic()
app = Sanic(name=__name__)
@app.route("/")
def my_unique_handler(request):
@@ -2011,7 +2031,7 @@ def test_endpoint_basic():
@pytest.mark.asyncio
async def test_endpoint_basic_asgi():
app = Sanic()
app = Sanic(name=__name__)
@app.route("/")
def my_unique_handler(request):
@@ -2076,3 +2096,19 @@ async def test_endpoint_blueprint_asgi():
request, response = await app.asgi_client.get("/bp")
assert request.endpoint == "named.my_blueprint.bp_root"
def test_url_for_without_server_name(app):
@app.route("/sample")
def sample(request):
return json({"url": request.url_for("url_for")})
@app.route("/url-for")
def url_for(request):
return text("url-for")
request, response = app.test_client.get("/sample")
assert (
response.json["url"]
== f"http://127.0.0.1:{request.server_port}/url-for"
)

View File

@@ -15,11 +15,13 @@ from aiofiles import os as async_os
from sanic.response import (
HTTPResponse,
StreamingHTTPResponse,
empty,
file,
file_stream,
json,
raw,
stream,
text,
)
from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT
@@ -28,13 +30,14 @@ from sanic.testing import HOST, PORT
JSON_DATA = {"ok": True}
@pytest.mark.filterwarnings("ignore:Types other than str will be")
def test_response_body_not_a_string(app):
"""Test when a response body sent from the application is not a string"""
random_num = choice(range(1000))
@app.route("/hello")
async def hello_route(request):
return HTTPResponse(body=random_num)
return text(random_num)
request, response = app.test_client.get("/hello")
assert response.text == str(random_num)
@@ -405,7 +408,7 @@ def test_file_response(app, file_name, static_file_directory, status):
mime_type=guess_type(file_path)[0] or "text/plain",
)
request, response = app.test_client.get("/files/{}".format(file_name))
request, response = app.test_client.get(f"/files/{file_name}")
assert response.status == status
assert response.body == get_file_content(static_file_directory, file_name)
assert "Content-Disposition" not in response.headers
@@ -428,12 +431,12 @@ def test_file_response_custom_filename(
file_path = os.path.abspath(unquote(file_path))
return file(file_path, filename=dest)
request, response = app.test_client.get("/files/{}".format(source))
request, response = app.test_client.get(f"/files/{source}")
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers[
"Content-Disposition"
] == 'attachment; filename="{}"'.format(dest)
] == f'attachment; filename="{dest}"'
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
@@ -458,7 +461,7 @@ def test_file_head_response(app, file_name, static_file_directory):
mime_type=guess_type(file_path)[0] or "text/plain",
)
request, response = app.test_client.head("/files/{}".format(file_name))
request, response = app.test_client.head(f"/files/{file_name}")
assert response.status == 200
assert "Accept-Ranges" in response.headers
assert "Content-Length" in response.headers
@@ -481,7 +484,7 @@ def test_file_stream_response(app, file_name, static_file_directory):
mime_type=guess_type(file_path)[0] or "text/plain",
)
request, response = app.test_client.get("/files/{}".format(file_name))
request, response = app.test_client.get(f"/files/{file_name}")
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
assert "Content-Disposition" not in response.headers
@@ -504,12 +507,12 @@ def test_file_stream_response_custom_filename(
file_path = os.path.abspath(unquote(file_path))
return file_stream(file_path, chunk_size=32, filename=dest)
request, response = app.test_client.get("/files/{}".format(source))
request, response = app.test_client.get(f"/files/{source}")
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers[
"Content-Disposition"
] == 'attachment; filename="{}"'.format(dest)
] == f'attachment; filename="{dest}"'
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
@@ -537,7 +540,7 @@ def test_file_stream_head_response(app, file_name, static_file_directory):
mime_type=guess_type(file_path)[0] or "text/plain",
)
request, response = app.test_client.head("/files/{}".format(file_name))
request, response = app.test_client.head(f"/files/{file_name}")
assert response.status == 200
# A HEAD request should never be streamed/chunked.
if "Transfer-Encoding" in response.headers:
@@ -575,12 +578,10 @@ def test_file_stream_response_range(
_range=range,
)
request, response = app.test_client.get("/files/{}".format(file_name))
request, response = app.test_client.get(f"/files/{file_name}")
assert response.status == 206
assert "Content-Range" in response.headers
assert response.headers["Content-Range"] == "bytes {}-{}/{}".format(
range.start, range.end, range.total
)
assert response.headers["Content-Range"] == f"bytes {range.start}-{range.end}/{range.total}"
def test_raw_response(app):
@@ -591,3 +592,13 @@ def test_raw_response(app):
request, response = app.test_client.get("/test")
assert response.content_type == "application/octet-stream"
assert response.body == b"raw_response"
def test_empty_response(app):
@app.get("/test")
def handler(request):
return empty()
request, response = app.test_client.get("/test")
assert response.content_type is None
assert response.body == b""

View File

@@ -40,7 +40,7 @@ async def handler_2(request):
def test_default_server_error_response_timeout():
request, response = response_timeout_default_app.test_client.get("/1")
assert response.status == 503
assert response.text == "Error: Response Timeout"
assert "Response Timeout" in response.text
response_handler_cancelled_app.flag = False
@@ -65,5 +65,5 @@ async def handler_3(request):
def test_response_handler_cancelled():
request, response = response_handler_cancelled_app.test_client.get("/1")
assert response.status == 503
assert response.text == "Error: Response Timeout"
assert "Response Timeout" in response.text
assert response_handler_cancelled_app.flag is False

View File

@@ -6,6 +6,7 @@ from sanic import Sanic
from sanic.constants import HTTP_METHODS
from sanic.response import json, text
from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists
from sanic.testing import SanicTestClient
# ------------------------------------------------------------ #
@@ -20,17 +21,17 @@ def test_versioned_routes_get(app, method):
func = getattr(app, method)
if callable(func):
@func("/{}".format(method), version=1)
@func(f"/{method}", version=1)
def handler(request):
return text("OK")
else:
print(func)
raise Exception("Method: {} is not callable".format(method))
raise Exception(f"Method: {method} is not callable")
client_method = getattr(app.test_client, method)
request, response = client_method("/v1/{}".format(method))
request, response = client_method(f"/v1/{method}")
assert response.status == 200
@@ -167,35 +168,36 @@ def test_route_optional_slash(app):
def test_route_strict_slashes_set_to_false_and_host_is_a_list(app):
# Part of regression test for issue #1120
site1 = "127.0.0.1:{}".format(app.test_client.port)
test_client = SanicTestClient(app, port=42101)
site1 = f"127.0.0.1:{test_client.port}"
# before fix, this raises a RouteExists error
@app.get("/get", host=[site1, "site2.com"], strict_slashes=False)
def get_handler(request):
return text("OK")
request, response = app.test_client.get("http://" + site1 + "/get")
request, response = test_client.get("http://" + site1 + "/get")
assert response.text == "OK"
@app.post("/post", host=[site1, "site2.com"], strict_slashes=False)
def post_handler(request):
return text("OK")
request, response = app.test_client.post("http://" + site1 + "/post")
request, response = test_client.post("http://" + site1 + "/post")
assert response.text == "OK"
@app.put("/put", host=[site1, "site2.com"], strict_slashes=False)
def put_handler(request):
return text("OK")
request, response = app.test_client.put("http://" + site1 + "/put")
request, response = test_client.put("http://" + site1 + "/put")
assert response.text == "OK"
@app.delete("/delete", host=[site1, "site2.com"], strict_slashes=False)
def delete_handler(request):
return text("OK")
request, response = app.test_client.delete("http://" + site1 + "/delete")
request, response = test_client.delete("http://" + site1 + "/delete")
assert response.text == "OK"
@@ -412,7 +414,8 @@ def test_dynamic_route_uuid(app):
assert response.text == "OK"
assert type(results[0]) is uuid.UUID
request, response = app.test_client.get("/quirky/{}".format(uuid.uuid4()))
generated_uuid = uuid.uuid4()
request, response = app.test_client.get(f"/quirky/{generated_uuid}")
assert response.status == 200
request, response = app.test_client.get("/quirky/non-existing")
@@ -551,6 +554,35 @@ def test_route_duplicate(app):
pass
def test_double_stack_route(app):
@app.route("/test/1")
@app.route("/test/2")
async def handler1(request):
return text("OK")
request, response = app.test_client.get("/test/1")
assert response.status == 200
request, response = app.test_client.get("/test/2")
assert response.status == 200
@pytest.mark.asyncio
async def test_websocket_route_asgi(app):
ev = asyncio.Event()
@app.websocket("/test/1")
@app.websocket("/test/2")
async def handler(request, ws):
ev.set()
request, response = await app.asgi_client.websocket("/test/1")
first_set = ev.is_set()
ev.clear()
request, response = await app.asgi_client.websocket("/test/1")
second_set = ev.is_set()
assert(first_set and second_set)
def test_method_not_allowed(app):
@app.route("/test", methods=["GET"])
async def handler(request):
@@ -738,55 +770,6 @@ def test_add_route_method_not_allowed(app):
assert response.status == 405
def test_remove_static_route(app):
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 = app.test_client.get("/test")
assert response.status == 200
request, response = app.test_client.get("/test2")
assert response.status == 200
app.remove_route("/test")
app.remove_route("/test2")
request, response = app.test_client.get("/test")
assert response.status == 404
request, response = app.test_client.get("/test2")
assert response.status == 404
def test_remove_dynamic_route(app):
async def handler(request, name):
return text("OK")
app.add_route(handler, "/folder/<name>")
request, response = app.test_client.get("/folder/test123")
assert response.status == 200
app.remove_route("/folder/<name>")
request, response = app.test_client.get("/folder/test123")
assert response.status == 404
def test_remove_inexistent_route(app):
uri = "/test"
with pytest.raises(RouteDoesNotExist) as excinfo:
app.remove_route(uri)
assert str(excinfo.value) == "Route was not registered: {}".format(uri)
def test_removing_slash(app):
@app.get("/rest/<resource>")
def get(_):
@@ -799,59 +782,6 @@ def test_removing_slash(app):
assert len(app.router.routes_all.keys()) == 2
def test_remove_unhashable_route(app):
async def handler(request, unhashable):
return text("OK")
app.add_route(handler, "/folder/<unhashable:[A-Za-z0-9/]+>/end/")
request, response = app.test_client.get("/folder/test/asdf/end/")
assert response.status == 200
request, response = app.test_client.get("/folder/test///////end/")
assert response.status == 200
request, response = app.test_client.get("/folder/test/end/")
assert response.status == 200
app.remove_route("/folder/<unhashable:[A-Za-z0-9/]+>/end/")
request, response = app.test_client.get("/folder/test/asdf/end/")
assert response.status == 404
request, response = app.test_client.get("/folder/test///////end/")
assert response.status == 404
request, response = app.test_client.get("/folder/test/end/")
assert response.status == 404
def test_remove_route_without_clean_cache(app):
async def handler(request):
return text("OK")
app.add_route(handler, "/test")
request, response = app.test_client.get("/test")
assert response.status == 200
app.remove_route("/test", clean_cache=True)
app.remove_route("/test/", clean_cache=True)
request, response = app.test_client.get("/test")
assert response.status == 404
app.add_route(handler, "/test")
request, response = app.test_client.get("/test")
assert response.status == 200
app.remove_route("/test", clean_cache=False)
request, response = app.test_client.get("/test")
assert response.status == 200
def test_overload_routes(app):
@app.route("/overload", methods=["GET"])
async def handler1(request):

Some files were not shown because too many files have changed in this diff Show More