Merge conflicts

This commit is contained in:
Adam Hopkins 2021-12-26 22:14:44 +02:00
commit 027c8e092b
No known key found for this signature in database
GPG Key ID: 9F85EE6C807303FB
106 changed files with 2225 additions and 1154 deletions

View File

@ -5,12 +5,7 @@ on:
- main - main
tags: tags:
- "!*" # Do not execute on tags - "!*" # Do not execute on tags
paths:
- sanic/*
- tests/*
pull_request: pull_request:
paths:
- "!*.MD"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
test: test:

View File

@ -1,12 +1,12 @@
.. note:: .. note::
From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases`` CHANGELOG files are maintained in ``./docs/sanic/releases``. To view the full CHANGELOG, please visit https://sanic.readthedocs.io/en/stable/sanic/changelog.html.
Version 21.6.1 Version 21.6.1
-------------- --------------
Bugfixes **Bugfixes**
********
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_ * `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
Update sanic-routing to allow for better splitting of complex URI templates Update sanic-routing to allow for better splitting of complex URI templates
@ -20,8 +20,7 @@ Bugfixes
Version 21.6.0 Version 21.6.0
-------------- --------------
Features **Features**
********
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_ * `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
Add ``response.eof()`` method for closing a stream in a handler Add ``response.eof()`` method for closing a stream in a handler
@ -68,8 +67,7 @@ Features
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_ * `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
Additional methods for attaching ``HTTPMethodView`` Additional methods for attaching ``HTTPMethodView``
Bugfixes **Bugfixes**
********
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_ * `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
Fix ``UserWarning`` in ASGI mode for missing ``__slots__`` Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
@ -85,8 +83,7 @@ Bugfixes
Fix issue where Blueprint exception handlers do not consistently route to proper handler Fix issue where Blueprint exception handlers do not consistently route to proper handler
Deprecations and Removals **Deprecations and Removals**
*************************
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_ * `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE`` Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
@ -95,14 +92,12 @@ Deprecations and Removals
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_ * `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
Deprecate StreamingHTTPResponse Deprecate StreamingHTTPResponse
Developer infrastructure **Developer infrastructure**
************************
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_ * `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
Remove Travis CI in favor of GitHub Actions Remove Travis CI in favor of GitHub Actions
Improved Documentation **Improved Documentation**
**********************
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_ * `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
Fix typo in documentation Fix typo in documentation
@ -112,8 +107,7 @@ Improved Documentation
Version 21.3.2 Version 21.3.2
-------------- --------------
Bugfixes **Bugfixes**
********
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_ * `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
Disable response timeout on websocket connections Disable response timeout on websocket connections
@ -124,8 +118,7 @@ Bugfixes
Version 21.3.1 Version 21.3.1
-------------- --------------
Bugfixes **Bugfixes**
********
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_ * `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
Static files inside subfolders are not accessible (404) Static files inside subfolders are not accessible (404)
@ -135,8 +128,7 @@ Version 21.3.0
`Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_ `Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_
Features **Features**
********
* *
`#1876 <https://github.com/sanic-org/sanic/pull/1876>`_ `#1876 <https://github.com/sanic-org/sanic/pull/1876>`_
@ -189,8 +181,7 @@ Features
`#2063 <https://github.com/sanic-org/sanic/pull/2063>`_ `#2063 <https://github.com/sanic-org/sanic/pull/2063>`_
App and connection level context objects App and connection level context objects
Bugfixes and issues resolved **Bugfixes**
****************************
* Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_ * Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_
``url_for`` where ``strict_slashes`` are on for a path ending in ``/`` ``url_for`` where ``strict_slashes`` are on for a path ending in ``/``
@ -220,8 +211,7 @@ Bugfixes and issues resolved
`#2001 <https://github.com/sanic-org/sanic/pull/2001>`_ `#2001 <https://github.com/sanic-org/sanic/pull/2001>`_
Raise ValueError when cookie max-age is not an integer Raise ValueError when cookie max-age is not an integer
Deprecations and Removals **Deprecations and Removals**
*************************
* *
`#2007 <https://github.com/sanic-org/sanic/pull/2007>`_ `#2007 <https://github.com/sanic-org/sanic/pull/2007>`_
@ -240,8 +230,7 @@ Deprecations and Removals
* ``Request.endpoint`` deprecated in favor of ``Request.name`` * ``Request.endpoint`` deprecated in favor of ``Request.name``
* handler type name prefixes removed (static, websocket, etc) * handler type name prefixes removed (static, websocket, etc)
Developer infrastructure **Developer infrastructure**
************************
* *
`#1995 <https://github.com/sanic-org/sanic/pull/1995>`_ `#1995 <https://github.com/sanic-org/sanic/pull/1995>`_
@ -259,8 +248,7 @@ Developer infrastructure
`#2049 <https://github.com/sanic-org/sanic/pull/2049>`_ `#2049 <https://github.com/sanic-org/sanic/pull/2049>`_
Updated setup.py to use ``find_packages`` Updated setup.py to use ``find_packages``
Improved Documentation **Improved Documentation**
**********************
* *
`#1218 <https://github.com/sanic-org/sanic/pull/1218>`_ `#1218 <https://github.com/sanic-org/sanic/pull/1218>`_
@ -282,8 +270,7 @@ Improved Documentation
`#2052 <https://github.com/sanic-org/sanic/pull/2052>`_ `#2052 <https://github.com/sanic-org/sanic/pull/2052>`_
Fix some examples and docs Fix some examples and docs
Miscellaneous **Miscellaneous**
*************
* ``Request.route`` property * ``Request.route`` property
* Better websocket subprotocols support * Better websocket subprotocols support
@ -329,8 +316,7 @@ Miscellaneous
Version 20.12.3 Version 20.12.3
--------------- ---------------
Bugfixes **Bugfixes**
********
* *
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_ `#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
@ -339,8 +325,7 @@ Bugfixes
Version 20.12.2 Version 20.12.2
--------------- ---------------
Dependencies **Dependencies**
************
* *
`#2026 <https://github.com/sanic-org/sanic/pull/2026>`_ `#2026 <https://github.com/sanic-org/sanic/pull/2026>`_
@ -353,8 +338,7 @@ Dependencies
Version 19.12.5 Version 19.12.5
--------------- ---------------
Dependencies **Dependencies**
************
* *
`#2025 <https://github.com/sanic-org/sanic/pull/2025>`_ `#2025 <https://github.com/sanic-org/sanic/pull/2025>`_
@ -367,8 +351,7 @@ Dependencies
Version 20.12.0 Version 20.12.0
--------------- ---------------
Features **Features**
********
* *
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_ `#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
@ -377,8 +360,7 @@ Features
Version 20.12.0 Version 20.12.0
--------------- ---------------
Features **Features**
********
* *
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_ `#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
@ -416,22 +398,19 @@ Features
`#1979 <https://github.com/sanic-org/sanic/pull/1979>`_ `#1979 <https://github.com/sanic-org/sanic/pull/1979>`_
Add app registry and Sanic class level app retrieval Add app registry and Sanic class level app retrieval
Bugfixes **Bugfixes**
********
* *
`#1965 <https://github.com/sanic-org/sanic/pull/1965>`_ `#1965 <https://github.com/sanic-org/sanic/pull/1965>`_
Fix Chunked Transport-Encoding in ASGI streaming response Fix Chunked Transport-Encoding in ASGI streaming response
Deprecations and Removals **Deprecations and Removals**
*************************
* *
`#1981 <https://github.com/sanic-org/sanic/pull/1981>`_ `#1981 <https://github.com/sanic-org/sanic/pull/1981>`_
Cleanup and remove deprecated code Cleanup and remove deprecated code
Developer infrastructure **Developer infrastructure**
************************
* *
`#1956 <https://github.com/sanic-org/sanic/pull/1956>`_ `#1956 <https://github.com/sanic-org/sanic/pull/1956>`_
@ -445,8 +424,7 @@ Developer infrastructure
`#1986 <https://github.com/sanic-org/sanic/pull/1986>`_ `#1986 <https://github.com/sanic-org/sanic/pull/1986>`_
Update tox requirements Update tox requirements
Improved Documentation **Improved Documentation**
**********************
* *
`#1951 <https://github.com/sanic-org/sanic/pull/1951>`_ `#1951 <https://github.com/sanic-org/sanic/pull/1951>`_
@ -464,8 +442,7 @@ Improved Documentation
Version 20.9.1 Version 20.9.1
--------------- ---------------
Bugfixes **Bugfixes**
********
* *
`#1954 <https://github.com/sanic-org/sanic/pull/1954>`_ `#1954 <https://github.com/sanic-org/sanic/pull/1954>`_
@ -478,8 +455,7 @@ Bugfixes
Version 19.12.3 Version 19.12.3
--------------- ---------------
Bugfixes **Bugfixes**
********
* *
`#1959 <https://github.com/sanic-org/sanic/pull/1959>`_ `#1959 <https://github.com/sanic-org/sanic/pull/1959>`_
@ -490,8 +466,7 @@ Version 20.9.0
--------------- ---------------
Features **Features**
********
* *
`#1887 <https://github.com/sanic-org/sanic/pull/1887>`_ `#1887 <https://github.com/sanic-org/sanic/pull/1887>`_
@ -518,22 +493,19 @@ Features
`#1937 <https://github.com/sanic-org/sanic/pull/1937>`_ `#1937 <https://github.com/sanic-org/sanic/pull/1937>`_
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto) Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
Bugfixes **Bugfixes**
********
* *
`#1897 <https://github.com/sanic-org/sanic/pull/1897>`_ `#1897 <https://github.com/sanic-org/sanic/pull/1897>`_
Resolves exception from unread bytes in stream Resolves exception from unread bytes in stream
Deprecations and Removals **Deprecations and Removals**
*************************
* *
`#1903 <https://github.com/sanic-org/sanic/pull/1903>`_ `#1903 <https://github.com/sanic-org/sanic/pull/1903>`_
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3 config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
Developer infrastructure **Developer infrastructure**
************************
* *
`#1890 <https://github.com/sanic-org/sanic/pull/1890>`_, `#1890 <https://github.com/sanic-org/sanic/pull/1890>`_,
@ -548,8 +520,7 @@ Developer infrastructure
`#1924 <https://github.com/sanic-org/sanic/pull/1924>`_ `#1924 <https://github.com/sanic-org/sanic/pull/1924>`_
Adding --strict-markers for pytest Adding --strict-markers for pytest
Improved Documentation **Improved Documentation**
**********************
* *
`#1922 <https://github.com/sanic-org/sanic/pull/1922>`_ `#1922 <https://github.com/sanic-org/sanic/pull/1922>`_
@ -559,8 +530,7 @@ Improved Documentation
Version 20.6.3 Version 20.6.3
--------------- ---------------
Bugfixes **Bugfixes**
********
* *
`#1884 <https://github.com/sanic-org/sanic/pull/1884>`_ `#1884 <https://github.com/sanic-org/sanic/pull/1884>`_
@ -570,8 +540,7 @@ Bugfixes
Version 20.6.2 Version 20.6.2
--------------- ---------------
Features **Features**
********
* *
`#1641 <https://github.com/sanic-org/sanic/pull/1641>`_ `#1641 <https://github.com/sanic-org/sanic/pull/1641>`_
@ -581,8 +550,7 @@ Features
Version 20.6.1 Version 20.6.1
--------------- ---------------
Features **Features**
********
* *
`#1760 <https://github.com/sanic-org/sanic/pull/1760>`_ `#1760 <https://github.com/sanic-org/sanic/pull/1760>`_
@ -596,8 +564,7 @@ Features
`#1880 <https://github.com/sanic-org/sanic/pull/1880>`_ `#1880 <https://github.com/sanic-org/sanic/pull/1880>`_
Add handler names for websockets for url_for usage Add handler names for websockets for url_for usage
Bugfixes **Bugfixes**
********
* *
`#1776 <https://github.com/sanic-org/sanic/pull/1776>`_ `#1776 <https://github.com/sanic-org/sanic/pull/1776>`_
@ -619,15 +586,13 @@ Bugfixes
`#1853 <https://github.com/sanic-org/sanic/pull/1853>`_ `#1853 <https://github.com/sanic-org/sanic/pull/1853>`_
Fix pickle error when attempting to pickle an application which contains websocket routes Fix pickle error when attempting to pickle an application which contains websocket routes
Deprecations and Removals **Deprecations and Removals**
*************************
* *
`#1739 <https://github.com/sanic-org/sanic/pull/1739>`_ `#1739 <https://github.com/sanic-org/sanic/pull/1739>`_
Deprecate body_bytes to merge into body Deprecate body_bytes to merge into body
Developer infrastructure **Developer infrastructure**
************************
* *
`#1852 <https://github.com/sanic-org/sanic/pull/1852>`_ `#1852 <https://github.com/sanic-org/sanic/pull/1852>`_
@ -642,8 +607,7 @@ Developer infrastructure
Wrap run()'s "protocol" type annotation in Optional[] Wrap run()'s "protocol" type annotation in Optional[]
Improved Documentation **Improved Documentation**
**********************
* *
`#1846 <https://github.com/sanic-org/sanic/pull/1846>`_ `#1846 <https://github.com/sanic-org/sanic/pull/1846>`_
@ -663,8 +627,7 @@ Version 20.6.0
Version 20.3.0 Version 20.3.0
--------------- ---------------
Features **Features**
********
* *
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_ `#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
@ -695,8 +658,7 @@ Features
`#1820 <https://github.com/sanic-org/sanic/pull/1820>`_ `#1820 <https://github.com/sanic-org/sanic/pull/1820>`_
Do not set content-type and content-length headers in exceptions Do not set content-type and content-length headers in exceptions
Bugfixes **Bugfixes**
********
* *
`#1748 <https://github.com/sanic-org/sanic/pull/1748>`_ `#1748 <https://github.com/sanic-org/sanic/pull/1748>`_
@ -714,8 +676,7 @@ Bugfixes
`#1808 <https://github.com/sanic-org/sanic/pull/1808>`_ `#1808 <https://github.com/sanic-org/sanic/pull/1808>`_
Fix Ctrl+C and tests on Windows Fix Ctrl+C and tests on Windows
Deprecations and Removals **Deprecations and Removals**
*************************
* *
`#1800 <https://github.com/sanic-org/sanic/pull/1800>`_ `#1800 <https://github.com/sanic-org/sanic/pull/1800>`_
@ -733,8 +694,7 @@ Deprecations and Removals
`#1818 <https://github.com/sanic-org/sanic/pull/1818>`_ `#1818 <https://github.com/sanic-org/sanic/pull/1818>`_
Complete deprecation of ``app.remove_route`` and ``request.raw_args`` Complete deprecation of ``app.remove_route`` and ``request.raw_args``
Dependencies **Dependencies**
************
* *
`#1794 <https://github.com/sanic-org/sanic/pull/1794>`_ `#1794 <https://github.com/sanic-org/sanic/pull/1794>`_
@ -744,15 +704,13 @@ Dependencies
`#1806 <https://github.com/sanic-org/sanic/pull/1806>`_ `#1806 <https://github.com/sanic-org/sanic/pull/1806>`_
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation) Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
Developer infrastructure **Developer infrastructure**
************************
* *
`#1833 <https://github.com/sanic-org/sanic/pull/1833>`_ `#1833 <https://github.com/sanic-org/sanic/pull/1833>`_
Resolve broken documentation builds Resolve broken documentation builds
Improved Documentation **Improved Documentation**
**********************
* *
`#1755 <https://github.com/sanic-org/sanic/pull/1755>`_ `#1755 <https://github.com/sanic-org/sanic/pull/1755>`_
@ -794,8 +752,7 @@ Improved Documentation
Version 19.12.0 Version 19.12.0
--------------- ---------------
Bugfixes **Bugfixes**
********
- Fix blueprint middleware application - Fix blueprint middleware application
@ -814,8 +771,7 @@ Bugfixes
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__) due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__)
Improved Documentation **Improved Documentation**
**********************
- Move docs from MD to RST - Move docs from MD to RST
@ -829,8 +785,7 @@ Improved Documentation
Version 19.6.3 Version 19.6.3
-------------- --------------
Features **Features**
********
- Enable Towncrier Support - Enable Towncrier Support
@ -838,8 +793,7 @@ Features
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__) of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__)
Improved Documentation **Improved Documentation**
**********************
- Documentation infrastructure changes - Documentation infrastructure changes
@ -852,8 +806,7 @@ Improved Documentation
Version 19.6.2 Version 19.6.2
-------------- --------------
Features **Features**
********
* *
`#1562 <https://github.com/sanic-org/sanic/pull/1562>`_ `#1562 <https://github.com/sanic-org/sanic/pull/1562>`_
@ -869,8 +822,7 @@ Features
Add Configure support from object string Add Configure support from object string
Bugfixes **Bugfixes**
********
* *
`#1587 <https://github.com/sanic-org/sanic/pull/1587>`_ `#1587 <https://github.com/sanic-org/sanic/pull/1587>`_
@ -888,8 +840,7 @@ Bugfixes
`#1594 <https://github.com/sanic-org/sanic/pull/1594>`_ `#1594 <https://github.com/sanic-org/sanic/pull/1594>`_
Strict Slashes behavior fix Strict Slashes behavior fix
Deprecations and Removals **Deprecations and Removals**
*************************
* *
`#1544 <https://github.com/sanic-org/sanic/pull/1544>`_ `#1544 <https://github.com/sanic-org/sanic/pull/1544>`_
@ -913,8 +864,7 @@ Deprecations and Removals
Version 19.3 Version 19.3
------------ ------------
Features **Features**
********
* *
`#1497 <https://github.com/sanic-org/sanic/pull/1497>`_ `#1497 <https://github.com/sanic-org/sanic/pull/1497>`_
@ -982,8 +932,7 @@ Features
This is a breaking change. This is a breaking change.
Bugfixes **Bugfixes**
********
* *
@ -1019,8 +968,7 @@ Bugfixes
This allows the access log to be disabled for example when running via This allows the access log to be disabled for example when running via
gunicorn. gunicorn.
Developer infrastructure **Developer infrastructure**
************************
* `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials * `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials
* `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514) * `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
@ -1028,8 +976,7 @@ Developer infrastructure
* `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build * `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
* `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests * `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
Improved Documentation **Improved Documentation**
**********************
* `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation * `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation
* `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example * `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example
@ -1096,15 +1043,13 @@ Version 18.12
Version 0.8 Version 0.8
----------- -----------
0.8.3 **0.8.3**
*****
* Changes: * Changes:
* Ownership changed to org 'sanic-org' * Ownership changed to org 'sanic-org'
0.8.0 **0.8.0**
*****
* Changes: * Changes:
@ -1184,19 +1129,16 @@ Version 0.1
----------- -----------
0.1.7 **0.1.7**
*****
* Reversed static url and directory arguments to meet spec * Reversed static url and directory arguments to meet spec
0.1.6 **0.1.6**
*****
* Static files * Static files
* Lazy Cookie Loading * Lazy Cookie Loading
0.1.5 **0.1.5**
*****
* Cookies * Cookies
* Blueprint listeners and ordering * Blueprint listeners and ordering
@ -1204,23 +1146,19 @@ Version 0.1
* Fix: Incomplete file reads on medium+ sized post requests * Fix: Incomplete file reads on medium+ sized post requests
* Breaking: after_start and before_stop now pass sanic as their first argument * Breaking: after_start and before_stop now pass sanic as their first argument
0.1.4 **0.1.4**
*****
* Multiprocessing * Multiprocessing
0.1.3 **0.1.3**
*****
* Blueprint support * Blueprint support
* Faster Response processing * Faster Response processing
0.1.1 - 0.1.2 **0.1.1 - 0.1.2**
*************
* Struggling to update pypi via CI * Struggling to update pypi via CI
0.1.0 **0.1.0**
*****
* Released to public * Released to public

View File

@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
:stub-columns: 1 :stub-columns: 1
* - Build * - Build
- | |Py39Test| |Py38Test| |Py37Test| - | |Py310Test| |Py39Test| |Py38Test| |Py37Test|
* - Docs * - Docs
- | |UserGuide| |Documentation| - | |UserGuide| |Documentation|
* - Package * - Package
@ -27,6 +27,8 @@ Sanic | Build fast. Run fast.
:target: https://community.sanicframework.org/ :target: https://community.sanicframework.org/
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord .. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
:target: https://discord.gg/FARQzAEMAA :target: https://discord.gg/FARQzAEMAA
.. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main .. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main .. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
@ -75,7 +77,11 @@ The goal of the project is to provide a simple way to get up and running a highl
Sponsor Sponsor
------- -------
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic. Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
Thanks to `Linode <https://www.linode.com>`_ for their contribution towards the development and community of Sanic.
|Linode|
Installation Installation
------------ ------------
@ -160,3 +166,8 @@ Contribution
------------ ------------
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_. We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_.
.. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg
:alt: Linode
:target: https://www.linode.com
:width: 200px

View File

@ -38,10 +38,3 @@ sanic.views
.. automodule:: sanic.views .. automodule:: sanic.views
:members: :members:
:show-inheritance: :show-inheritance:
sanic.websocket
---------------
.. automodule:: sanic.websocket
:members:
:show-inheritance:

View File

@ -1,6 +1,6 @@
📜 Changelog 📜 Changelog
============ ============
.. mdinclude:: ./releases/21.9.md .. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md
.. include:: ../../CHANGELOG.rst .. include:: ../../CHANGELOG.rst

View File

@ -0,0 +1,58 @@
## Version 21.12.0
### Features
- [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects
- [#2262](https://github.com/sanic-org/sanic/pull/2262) Noisy exceptions - force logging of all exceptions
- [#2264](https://github.com/sanic-org/sanic/pull/2264) Optional `uvloop` by configuration
- [#2270](https://github.com/sanic-org/sanic/pull/2270) Vhost support using multiple TLS certificates
- [#2277](https://github.com/sanic-org/sanic/pull/2277) Change signal routing for increased consistency
- *BREAKING CHANGE*: If you were manually routing signals there is a breaking change. The signal router's `get` is no longer 100% determinative. There is now an additional step to loop thru the returned signals for proper matching on the requirements. If signals are being dispatched using `app.dispatch` or `bp.dispatch`, there is no change.
- [#2290](https://github.com/sanic-org/sanic/pull/2290) Add contextual exceptions
- [#2291](https://github.com/sanic-org/sanic/pull/2291) Increase join concat performance
- [#2295](https://github.com/sanic-org/sanic/pull/2295), [#2316](https://github.com/sanic-org/sanic/pull/2316), [#2331](https://github.com/sanic-org/sanic/pull/2331) Restructure of CLI and application state with new displays and more command parity with `app.run`
- [#2302](https://github.com/sanic-org/sanic/pull/2302) Add route context at definition time
- [#2304](https://github.com/sanic-org/sanic/pull/2304) Named tasks and new API for managing background tasks
- [#2307](https://github.com/sanic-org/sanic/pull/2307) On app auto-reload, provide insight of changed files
- [#2308](https://github.com/sanic-org/sanic/pull/2308) Auto extend application with [Sanic Extensions](https://sanicframework.org/en/plugins/sanic-ext/getting-started.html) if it is installed, and provide first class support for accessing the extensions
- [#2309](https://github.com/sanic-org/sanic/pull/2309) Builtin signals changed to `Enum`
- [#2313](https://github.com/sanic-org/sanic/pull/2313) Support additional config implementation use case
- [#2321](https://github.com/sanic-org/sanic/pull/2321) Refactor environment variable hydration logic
- [#2327](https://github.com/sanic-org/sanic/pull/2327) Prevent sending multiple or mixed responses on a single request
- [#2330](https://github.com/sanic-org/sanic/pull/2330) Custom type casting on environment variables
- [#2332](https://github.com/sanic-org/sanic/pull/2332) Make all deprecation notices consistent
- [#2335](https://github.com/sanic-org/sanic/pull/2335) Allow underscore to start instance names
### Bugfixes
- [#2273](https://github.com/sanic-org/sanic/pull/2273) Replace assignation by typing for `websocket_handshake`
- [#2285](https://github.com/sanic-org/sanic/pull/2285) Fix IPv6 display in startup logs
- [#2299](https://github.com/sanic-org/sanic/pull/2299) Dispatch `http.lifecyle.response` from exception handler
### Deprecations and Removals
- [#2306](https://github.com/sanic-org/sanic/pull/2306) Removal of deprecated items
- `Sanic` and `Blueprint` may no longer have arbitrary properties attached to them
- `Sanic` and `Blueprint` forced to have compliant names
- alphanumeric + `_` + `-`
- must start with letter or `_`
- `load_env` keyword argument of `Sanic`
- `sanic.exceptions.abort`
- `sanic.views.CompositionView`
- `sanic.response.StreamingHTTPResponse`
- *NOTE:* the `stream()` response method (where you pass a callable streaming function) has been deprecated and will be removed in v22.6. You should upgrade all streaming responses to the new style: https://sanicframework.org/en/guide/advanced/streaming.html#response-streaming
- [#2320](https://github.com/sanic-org/sanic/pull/2320) Remove app instance from Config for error handler setting
### Developer infrastructure
- [#2251](https://github.com/sanic-org/sanic/pull/2251) Change dev install command
- [#2286](https://github.com/sanic-org/sanic/pull/2286) Change codeclimate complexity threshold from 5 to 10
- [#2287](https://github.com/sanic-org/sanic/pull/2287) Update host test function names so they are not overwritten
- [#2292](https://github.com/sanic-org/sanic/pull/2292) Fail CI on error
- [#2311](https://github.com/sanic-org/sanic/pull/2311), [#2324](https://github.com/sanic-org/sanic/pull/2324) Do not run tests for draft PRs
- [#2336](https://github.com/sanic-org/sanic/pull/2336) Remove paths from coverage checks
- [#2338](https://github.com/sanic-org/sanic/pull/2338) Cleanup ports on tests
### Improved Documentation
- [#2269](https://github.com/sanic-org/sanic/pull/2269), [#2329](https://github.com/sanic-org/sanic/pull/2329), [#2333](https://github.com/sanic-org/sanic/pull/2333) Cleanup typos and fix language
### Miscellaneous
- [#2257](https://github.com/sanic-org/sanic/pull/2257), [#2294](https://github.com/sanic-org/sanic/pull/2294), [#2341](https://github.com/sanic-org/sanic/pull/2341) Add Python 3.10 support
- [#2279](https://github.com/sanic-org/sanic/pull/2279), [#2317](https://github.com/sanic-org/sanic/pull/2317), [#2322](https://github.com/sanic-org/sanic/pull/2322) Add/correct missing type annotations
- [#2305](https://github.com/sanic-org/sanic/pull/2305) Fix examples to use modern implementations

View File

@ -1,4 +1,14 @@
## Version 21.9 ## Version 21.9.3
*Rerelease of v21.9.2 with some cleanup*
## Version 21.9.2
- [#2268](https://github.com/sanic-org/sanic/pull/2268) Make HTTP connections start in IDLE stage, avoiding delays and error messages
- [#2310](https://github.com/sanic-org/sanic/pull/2310) More consistent config setting with post-FALLBACK_ERROR_FORMAT apply
## Version 21.9.1
- [#2259](https://github.com/sanic-org/sanic/pull/2259) Allow non-conforming ErrorHandlers
## Version 21.9.0
### Features ### Features
- [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets - [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets

View File

@ -5,7 +5,7 @@ import asyncio
from sanic import Sanic from sanic import Sanic
app = Sanic(__name__) app = Sanic("Example")
async def notify_server_started_after_five_seconds(): async def notify_server_started_after_five_seconds():

View File

@ -4,7 +4,7 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
app = Sanic(__name__) app = Sanic("Example")
@app.middleware("request") @app.middleware("request")

View File

@ -6,7 +6,7 @@ from sanic import Sanic
from sanic.response import json from sanic.response import json
app = Sanic(__name__) app = Sanic("Example")
def check_request_for_authorization_status(request): def check_request_for_authorization_status(request):

View File

@ -8,9 +8,9 @@ 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 On a valid request, it should print "1 2 3 6 5 4" to terminal
""" """
app = Sanic(__name__) app = Sanic("Example")
bp = Blueprint("bp_" + __name__) bp = Blueprint("bp_example")
@bp.on_request @bp.on_request

View File

@ -2,10 +2,10 @@ from sanic import Blueprint, Sanic
from sanic.response import file, json from sanic.response import file, json
app = Sanic(__name__) app = Sanic("Example")
blueprint = Blueprint("name", url_prefix="/my_blueprint") blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2") blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")
blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3") blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3")
@blueprint.route("/foo") @blueprint.route("/foo")

View File

@ -3,7 +3,7 @@ from asyncio import sleep
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic(__name__, strict_slashes=True) app = Sanic("DelayedResponseApp", strict_slashes=True)
@app.get("/") @app.get("/")

View File

@ -41,7 +41,7 @@ from sanic import Sanic
handler = CustomHandler() handler = CustomHandler()
app = Sanic(__name__, error_handler=handler) app = Sanic("Example", error_handler=handler)
@app.route("/") @app.route("/")

View File

@ -1,7 +1,7 @@
from sanic import Sanic from sanic import Sanic, response
from sanic import response
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/") @app.route("/")
@ -9,5 +9,5 @@ async def test(request):
return response.json({"test": True}) return response.json({"test": True})
if __name__ == '__main__': if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)

View File

@ -6,7 +6,7 @@ from sanic import Sanic
from sanic.response import json from sanic.response import json
app = Sanic(__name__) app = Sanic("Example")
sem = None sem = None

View File

@ -44,7 +44,7 @@ LOG_SETTINGS = {
} }
app = Sanic(__name__, log_config=LOG_SETTINGS) app = Sanic("Example", log_config=LOG_SETTINGS)
@app.on_request @app.on_request

View File

@ -43,7 +43,7 @@ logdna = logging.getLogger(__name__)
logdna.setLevel(logging.INFO) logdna.setLevel(logging.INFO)
logdna.addHandler(logdna_handler) logdna.addHandler(logdna_handler)
app = Sanic(__name__) app = Sanic("Example")
@app.middleware @app.middleware

View File

@ -2,27 +2,29 @@
Modify header or status in response Modify header or status in response
""" """
from sanic import Sanic from sanic import Sanic, response
from sanic import response
app = Sanic(__name__)
@app.route('/') app = Sanic("Example")
@app.route("/")
def handle_request(request): def handle_request(request):
return response.json( return response.json(
{'message': 'Hello world!'}, {"message": "Hello world!"},
headers={'X-Served-By': 'sanic'}, headers={"X-Served-By": "sanic"},
status=200 status=200,
) )
@app.route('/unauthorized') @app.route("/unauthorized")
def handle_request(request): def handle_request(request):
return response.json( return response.json(
{'message': 'You are not authorized'}, {"message": "You are not authorized"},
headers={'X-Served-By': 'sanic'}, headers={"X-Served-By": "sanic"},
status=404 status=404,
) )
app.run(host="0.0.0.0", port=8000, debug=True)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -32,7 +32,7 @@ def test_port(worker_id):
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def app(): def app():
app = Sanic() app = Sanic("Example")
@app.route("/") @app.route("/")
async def index(request): async def index(request):

View File

@ -8,7 +8,6 @@ from sanic.handlers import ErrorHandler
class RaygunExceptionReporter(ErrorHandler): class RaygunExceptionReporter(ErrorHandler):
def __init__(self, raygun_api_key=None): def __init__(self, raygun_api_key=None):
super().__init__() super().__init__()
if raygun_api_key is None: if raygun_api_key is None:
@ -22,16 +21,13 @@ class RaygunExceptionReporter(ErrorHandler):
raygun_error_reporter = RaygunExceptionReporter() raygun_error_reporter = RaygunExceptionReporter()
app = Sanic(__name__, error_handler=raygun_error_reporter) app = Sanic("Example", error_handler=raygun_error_reporter)
@app.route("/raise") @app.route("/raise")
async def test(request): async def test(request):
raise SanicException('You Broke It!') raise SanicException("You Broke It!")
if __name__ == '__main__': if __name__ == "__main__":
app.run( app.run(host="0.0.0.0", port=getenv("PORT", 8080))
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@ -1,18 +1,18 @@
from sanic import Sanic from sanic import Sanic, response
from sanic import response
app = Sanic(__name__)
app = Sanic("Example")
@app.route('/')
@app.route("/")
def handle_request(request): def handle_request(request):
return response.redirect('/redirect') return response.redirect("/redirect")
@app.route('/redirect') @app.route("/redirect")
async def test(request): async def test(request):
return response.json({"Redirected": True}) return response.json({"Redirected": True})
if __name__ == '__main__': if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)

View File

@ -1,65 +1,63 @@
from sanic import Sanic 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.blueprints import Blueprint
from sanic.response import stream, text from sanic.response import stream, text
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
bp = Blueprint('blueprint_request_stream')
app = Sanic('request_stream') bp = Blueprint("bp_example")
app = Sanic("Example")
class SimpleView(HTTPMethodView): class SimpleView(HTTPMethodView):
@stream_decorator @stream_decorator
async def post(self, request): async def post(self, request):
result = '' result = ""
while True: while True:
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
result += body.decode('utf-8') result += body.decode("utf-8")
return text(result) return text(result)
@app.post('/stream', stream=True) @app.post("/stream", stream=True)
async def handler(request): async def handler(request):
async def streaming(response): async def streaming(response):
while True: while True:
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
body = body.decode('utf-8').replace('1', 'A') body = body.decode("utf-8").replace("1", "A")
await response.write(body) await response.write(body)
return stream(streaming) return stream(streaming)
@bp.put('/bp_stream', stream=True) @bp.put("/bp_stream", stream=True)
async def bp_handler(request): async def bp_handler(request):
result = '' result = ""
while True: while True:
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
result += body.decode('utf-8').replace('1', 'A') result += body.decode("utf-8").replace("1", "A")
return text(result) return text(result)
async def post_handler(request): async def post_handler(request):
result = '' result = ""
while True: while True:
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
result += body.decode('utf-8') result += body.decode("utf-8")
return text(result) return text(result)
app.blueprint(bp) app.blueprint(bp)
app.add_route(SimpleView.as_view(), '/method_view') 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__': if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000) app.run(host="0.0.0.0", port=8000)

View File

@ -1,21 +1,23 @@
import asyncio import asyncio
from sanic import Sanic
from sanic import response from sanic import Sanic, response
from sanic.config import Config from sanic.config import Config
from sanic.exceptions import RequestTimeout from sanic.exceptions import RequestTimeout
Config.REQUEST_TIMEOUT = 1 Config.REQUEST_TIMEOUT = 1
app = Sanic(__name__) app = Sanic("Example")
@app.route('/') @app.route("/")
async def test(request): async def test(request):
await asyncio.sleep(3) await asyncio.sleep(3)
return response.text('Hello, world!') return response.text("Hello, world!")
@app.exception(RequestTimeout) @app.exception(RequestTimeout)
def timeout(request, exception): def timeout(request, exception):
return response.text('RequestTimeout from error_handler.', 408) return response.text("RequestTimeout from error_handler.", 408)
app.run(host='0.0.0.0', port=8000)
app.run(host="0.0.0.0", port=8000)

View File

@ -1,21 +1,22 @@
from os import getenv
import rollbar import rollbar
from sanic.handlers import ErrorHandler
from sanic import Sanic from sanic import Sanic
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from os import getenv from sanic.handlers import ErrorHandler
rollbar.init(getenv("ROLLBAR_API_KEY")) rollbar.init(getenv("ROLLBAR_API_KEY"))
class RollbarExceptionHandler(ErrorHandler): class RollbarExceptionHandler(ErrorHandler):
def default(self, request, exception): def default(self, request, exception):
rollbar.report_message(str(exception)) rollbar.report_message(str(exception))
return super().default(request, exception) return super().default(request, exception)
app = Sanic(__name__, error_handler=RollbarExceptionHandler()) app = Sanic("Example", error_handler=RollbarExceptionHandler())
@app.route("/raise") @app.route("/raise")
@ -24,7 +25,4 @@ def create_error(request):
if __name__ == "__main__": if __name__ == "__main__":
app.run( app.run(host="0.0.0.0", port=getenv("PORT", 8080))
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@ -11,7 +11,7 @@ from pathlib import Path
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic(__name__) app = Sanic("Example")
@app.route("/text") @app.route("/text")

View File

@ -1,13 +1,11 @@
import asyncio import asyncio
from signal import SIGINT, signal
import uvloop import uvloop
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic(__name__) app = Sanic("Example")
@app.route("/") @app.route("/")
@ -15,17 +13,18 @@ async def test(request):
return response.json({"answer": "42"}) return response.json({"answer": "42"})
asyncio.set_event_loop(uvloop.new_event_loop()) async def main():
server = app.create_server( server = await app.create_server(
host="0.0.0.0", port=8000, return_asyncio_server=True port=8000, host="0.0.0.0", return_asyncio_server=True
) )
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
server = loop.run_until_complete(task)
loop.run_until_complete(server.startup())
signal(SIGINT, lambda s, f: loop.stop())
try: if server is None:
loop.run_forever() return
finally:
loop.stop() await server.startup()
await server.serve_forever()
if __name__ == "__main__":
asyncio.set_event_loop(uvloop.new_event_loop())
asyncio.run(main())

View File

@ -8,7 +8,7 @@ from sanic import Sanic, response
from sanic.server import AsyncioServer from sanic.server import AsyncioServer
app = Sanic(__name__) app = Sanic("Example")
@app.before_server_start @app.before_server_start

View File

@ -6,20 +6,19 @@ from sentry_sdk.integrations.sanic import SanicIntegration
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
sentry_init( sentry_init(
dsn=getenv("SENTRY_DSN"), dsn=getenv("SENTRY_DSN"),
integrations=[SanicIntegration()], integrations=[SanicIntegration()],
) )
app = Sanic(__name__) app = Sanic("Example")
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@app.route("/working") @app.route("/working")
async def working_path(request): async def working_path(request):
return json({ return json({"response": "Working API Response"})
"response": "Working API Response"
})
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@ -28,8 +27,5 @@ async def raise_error(request):
raise Exception("Testing Sentry Integration") raise Exception("Testing Sentry Integration")
if __name__ == '__main__': if __name__ == "__main__":
app.run( app.run(host="0.0.0.0", port=getenv("PORT", 8080))
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@ -1,6 +1,6 @@
from sanic import Sanic from sanic import Sanic
app = Sanic(__name__) app = Sanic("Example")
app.static("/", "./static") app.static("/", "./static")

View File

@ -1,13 +1,14 @@
from sanic import Sanic from sanic import Sanic
from sanic import response as res from sanic import response as res
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/") @app.route("/")
async def test(req): async def test(req):
return res.text("I\'m a teapot", status=418) return res.text("I'm a teapot", status=418)
if __name__ == '__main__': if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)

View File

@ -5,7 +5,7 @@ from sanic.exceptions import ServerError
from sanic.log import logger as log from sanic.log import logger as log
app = Sanic(__name__) app = Sanic("Example")
@app.route("/") @app.route("/")

View File

@ -4,7 +4,7 @@ import socket
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic(__name__) app = Sanic("Example")
@app.route("/test") @app.route("/test")

View File

@ -1,7 +1,7 @@
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic(__name__) app = Sanic("Example")
@app.route("/") @app.route("/")

View File

@ -8,7 +8,7 @@ from sanic.blueprints import Blueprint
# curl -H "Host: bp.example.com" localhost:8000/question # curl -H "Host: bp.example.com" localhost:8000/question
# curl -H "Host: bp.example.com" localhost:8000/answer # curl -H "Host: bp.example.com" localhost:8000/answer
app = Sanic(__name__) app = Sanic("Example")
bp = Blueprint("bp", host="bp.example.com") bp = Blueprint("bp", host="bp.example.com")

View File

@ -2,7 +2,7 @@ from sanic import Sanic
from sanic.response import redirect from sanic.response import redirect
app = Sanic(__name__) app = Sanic("Example")
app.static("index.html", "websocket.html") app.static("index.html", "websocket.html")

View File

@ -1,3 +1,3 @@
[build-system] [build-system]
requires = ["setuptools", "wheel"] requires = ["setuptools<60.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"

View File

@ -6,4 +6,4 @@ python:
path: . path: .
extra_requirements: extra_requirements:
- docs - docs
system_packages: true system_packages: true

View File

@ -1 +1 @@
__version__ = "21.12.0dev" __version__ = "21.12.0"

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
import logging.config import logging.config
import os import os
@ -11,6 +12,7 @@ from asyncio import (
AbstractEventLoop, AbstractEventLoop,
CancelledError, CancelledError,
Protocol, Protocol,
Task,
ensure_future, ensure_future,
get_event_loop, get_event_loop,
wait_for, wait_for,
@ -26,6 +28,7 @@ from ssl import SSLContext
from traceback import format_exc from traceback import format_exc
from types import SimpleNamespace from types import SimpleNamespace
from typing import ( from typing import (
TYPE_CHECKING,
Any, Any,
AnyStr, AnyStr,
Awaitable, Awaitable,
@ -40,10 +43,11 @@ from typing import (
Set, Set,
Tuple, Tuple,
Type, Type,
TypeVar,
Union, Union,
) )
from urllib.parse import urlencode, urlunparse from urllib.parse import urlencode, urlunparse
from warnings import filterwarnings, warn from warnings import filterwarnings
from sanic_routing.exceptions import ( # type: ignore from sanic_routing.exceptions import ( # type: ignore
FinalizationError, FinalizationError,
@ -52,11 +56,12 @@ from sanic_routing.exceptions import ( # type: ignore
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
from sanic import reloader_helpers from sanic import reloader_helpers
from sanic.application.ext import setup_ext
from sanic.application.logo import get_logo from sanic.application.logo import get_logo
from sanic.application.motd import MOTD from sanic.application.motd import MOTD
from sanic.application.state import ApplicationState, Mode from sanic.application.state import ApplicationState, Mode
from sanic.asgi import ASGIApp from sanic.asgi import ASGIApp
from sanic.base import BaseSanic from sanic.base.root import BaseSanic
from sanic.blueprint_group import BlueprintGroup from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
@ -68,9 +73,16 @@ from sanic.exceptions import (
URLBuildError, URLBuildError,
) )
from sanic.handlers import ErrorHandler from sanic.handlers import ErrorHandler
from sanic.helpers import _default
from sanic.http import Stage from sanic.http import Stage
from sanic.http.constants import HTTP from sanic.http.constants import HTTP
from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger from sanic.log import (
LOGGING_CONFIG_DEFAULTS,
Colors,
deprecation,
error_logger,
logger,
)
from sanic.mixins.listeners import ListenerEvent from sanic.mixins.listeners import ListenerEvent
from sanic.models.futures import ( from sanic.models.futures import (
FutureException, FutureException,
@ -84,11 +96,11 @@ from sanic.models.futures import (
from sanic.models.handler_types import ListenerType, MiddlewareType from sanic.models.handler_types import ListenerType, MiddlewareType
from sanic.models.handler_types import Sanic as SanicVar from sanic.models.handler_types import Sanic as SanicVar
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream
from sanic.router import Router from sanic.router import Router
from sanic.server import AsyncioServer, HttpProtocol from sanic.server import AsyncioServer, HttpProtocol
from sanic.server import Signal as ServerSignal from sanic.server import Signal as ServerSignal
from sanic.server import serve, serve_multiple, serve_single from sanic.server import serve, serve_multiple, serve_single, try_use_uvloop
from sanic.server.protocols.websocket_protocol import WebSocketProtocol from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.websockets.impl import ConnectionClosed from sanic.server.websockets.impl import ConnectionClosed
from sanic.signals import Signal, SignalRouter from sanic.signals import Signal, SignalRouter
@ -96,11 +108,21 @@ from sanic.tls import process_to_context
from sanic.touchup import TouchUp, TouchUpMeta from sanic.touchup import TouchUp, TouchUpMeta
if TYPE_CHECKING: # no cov
try:
from sanic_ext import Extend # type: ignore
from sanic_ext.extensions.base import Extension # type: ignore
except ImportError:
Extend = TypeVar("Extend") # type: ignore
if OS_IS_WINDOWS: if OS_IS_WINDOWS:
enable_windows_color_support() enable_windows_color_support()
filterwarnings("once", category=DeprecationWarning) filterwarnings("once", category=DeprecationWarning)
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
class Sanic(BaseSanic, metaclass=TouchUpMeta): class Sanic(BaseSanic, metaclass=TouchUpMeta):
""" """
@ -113,12 +135,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_run_response_middleware", "_run_response_middleware",
"_run_request_middleware", "_run_request_middleware",
) )
__fake_slots__ = ( __slots__ = (
"_app_registry",
"_asgi_app", "_asgi_app",
"_asgi_client", "_asgi_client",
"_blueprint_order", "_blueprint_order",
"_delayed_tasks", "_delayed_tasks",
"_ext",
"_future_exceptions", "_future_exceptions",
"_future_listeners", "_future_listeners",
"_future_middleware", "_future_middleware",
@ -127,20 +149,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_future_signals", "_future_signals",
"_future_statics", "_future_statics",
"_state", "_state",
"_task_registry",
"_test_client", "_test_client",
"_test_manager", "_test_manager",
"asgi",
"auto_reload",
"auto_reload",
"blueprints", "blueprints",
"config", "config",
"configure_logging", "configure_logging",
"ctx", "ctx",
"debug",
"error_handler", "error_handler",
"go_fast", "go_fast",
"is_running",
"is_stopping",
"listeners", "listeners",
"name", "name",
"named_request_middleware", "named_request_middleware",
@ -152,12 +169,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"signal_router", "signal_router",
"sock", "sock",
"strict_slashes", "strict_slashes",
"test_mode",
"websocket_enabled", "websocket_enabled",
"websocket_tasks", "websocket_tasks",
) )
_app_registry: Dict[str, "Sanic"] = {} _app_registry: Dict[str, "Sanic"] = {}
_uvloop_setting = None # TODO: Remove in v22.6
test_mode = False test_mode = False
def __init__( def __init__(
@ -168,7 +185,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
router: Optional[Router] = None, router: Optional[Router] = None,
signal_router: Optional[SignalRouter] = None, signal_router: Optional[SignalRouter] = None,
error_handler: Optional[ErrorHandler] = None, error_handler: Optional[ErrorHandler] = None,
load_env: Union[bool, str] = True,
env_prefix: Optional[str] = SANIC_PREFIX, env_prefix: Optional[str] = SANIC_PREFIX,
request_class: Optional[Type[Request]] = None, request_class: Optional[Type[Request]] = None,
strict_slashes: bool = False, strict_slashes: bool = False,
@ -184,25 +200,27 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
dict_config = log_config or LOGGING_CONFIG_DEFAULTS dict_config = log_config or LOGGING_CONFIG_DEFAULTS
logging.config.dictConfig(dict_config) # type: ignore logging.config.dictConfig(dict_config) # type: ignore
if config and (load_env is not True or env_prefix != SANIC_PREFIX): if config and env_prefix != SANIC_PREFIX:
raise SanicException( raise SanicException(
"When instantiating Sanic with config, you cannot also pass " "When instantiating Sanic with config, you cannot also pass "
"load_env or env_prefix" "env_prefix"
) )
# First setup config
self.config: Config = config or Config(env_prefix=env_prefix)
# Then we can do the rest
self._asgi_client: Any = None self._asgi_client: Any = None
self._test_client: Any = None
self._test_manager: Any = None
self._blueprint_order: List[Blueprint] = [] self._blueprint_order: List[Blueprint] = []
self._delayed_tasks: List[str] = [] self._delayed_tasks: List[str] = []
self._future_registry: FutureRegistry = FutureRegistry() self._future_registry: FutureRegistry = FutureRegistry()
self._state: ApplicationState = ApplicationState(app=self) self._state: ApplicationState = ApplicationState(app=self)
self._task_registry: Dict[str, Task] = {}
self._test_client: Any = None
self._test_manager: Any = None
self.asgi = False
self.auto_reload = False
self.blueprints: Dict[str, Blueprint] = {} self.blueprints: Dict[str, Blueprint] = {}
self.config: Config = config or Config(
load_env=load_env,
env_prefix=env_prefix,
app=self,
)
self.configure_logging: bool = configure_logging self.configure_logging: bool = configure_logging
self.ctx: Any = ctx or SimpleNamespace() self.ctx: Any = ctx or SimpleNamespace()
self.debug = False self.debug = False
@ -224,6 +242,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
self.go_fast = self.run self.go_fast = self.run
if register is not None: if register is not None:
deprecation(
"The register argument is deprecated and will stop working "
"in v22.6. After v22.6 all apps will be added to the Sanic "
"app registry.",
22.6,
)
self.config.REGISTER = register self.config.REGISTER = register
if self.config.REGISTER: if self.config.REGISTER:
self.__class__.register_app(self) self.__class__.register_app(self)
@ -254,32 +278,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
# Registration # Registration
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def add_task(
self,
task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]],
) -> None:
"""
Schedule a task to run later, after the loop has started.
Different from asyncio.ensure_future in that it does not
also return a future, and the actual ensure_future call
is delayed until before server start.
`See user guide re: background tasks
<https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__
:param task: future, couroutine or awaitable
"""
try:
loop = self.loop # Will raise SanicError if loop is not started
self._loop_add_task(task, self, loop)
except SanicException:
task_name = f"sanic.delayed_task.{hash(task)}"
if not self._delayed_tasks:
self.after_server_start(partial(self.dispatch_delayed_tasks))
self.signal(task_name)(partial(self.run_delayed_task, task=task))
self._delayed_tasks.append(task_name)
def register_listener( def register_listener(
self, listener: ListenerType[SanicVar], event: str self, listener: ListenerType[SanicVar], event: str
) -> ListenerType[SanicVar]: ) -> ListenerType[SanicVar]:
@ -404,12 +402,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
websocket_handler.is_websocket = True # type: ignore websocket_handler.is_websocket = True # type: ignore
params["handler"] = websocket_handler params["handler"] = websocket_handler
ctx = params.pop("route_context")
routes = self.router.add(**params) routes = self.router.add(**params)
if isinstance(routes, Route): if isinstance(routes, Route):
routes = [routes] routes = [routes]
for r in routes: for r in routes:
r.ctx.websocket = websocket r.ctx.websocket = websocket
r.ctx.static = params.get("static", False) r.ctx.static = params.get("static", False)
r.ctx.__dict__.update(ctx)
return routes return routes
@ -755,7 +757,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
exception, request.name if request else None exception, request.name if request else None
) )
if handler: if handler:
warn( deprecation(
"An error occurred while handling the request after at " "An error occurred while handling the request after at "
"least some part of the response was sent to the client. " "least some part of the response was sent to the client. "
"Therefore, the response from your custom exception " "Therefore, the response from your custom exception "
@ -770,7 +772,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"For further information, please see the docs: " "For further information, please see the docs: "
"https://sanicframework.org/en/guide/advanced/" "https://sanicframework.org/en/guide/advanced/"
"signals.html", "signals.html",
DeprecationWarning, 22.6,
) )
try: try:
response = self.error_handler.response(request, exception) response = self.error_handler.response(request, exception)
@ -823,6 +825,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
else: else:
if request.stream: if request.stream:
response = request.stream.response response = request.stream.response
# Marked for cleanup and DRY with handle_request/handle_exception
# when ResponseStream is no longer supporder
if isinstance(response, BaseHTTPResponse): if isinstance(response, BaseHTTPResponse):
await self.dispatch( await self.dispatch(
"http.lifecycle.response", "http.lifecycle.response",
@ -833,6 +838,17 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
}, },
) )
await response.send(end_stream=True) await response.send(end_stream=True)
elif isinstance(response, ResponseStream):
resp = await response(request)
await self.dispatch(
"http.lifecycle.response",
inline=True,
context={
"request": request,
"response": resp,
},
)
await response.eof()
else: else:
raise ServerError( raise ServerError(
f"Invalid response type {response!r} (need HTTPResponse)" f"Invalid response type {response!r} (need HTTPResponse)"
@ -936,7 +952,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
elif not hasattr(handler, "is_websocket"): elif not hasattr(handler, "is_websocket"):
response = request.stream.response # type: ignore response = request.stream.response # type: ignore
# Make sure that response is finished / run StreamingHTTP callback # Marked for cleanup and DRY with handle_request/handle_exception
# when ResponseStream is no longer supporder
if isinstance(response, BaseHTTPResponse): if isinstance(response, BaseHTTPResponse):
await self.dispatch( await self.dispatch(
"http.lifecycle.response", "http.lifecycle.response",
@ -947,6 +964,17 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
}, },
) )
await response.send(end_stream=True) await response.send(end_stream=True)
elif isinstance(response, ResponseStream):
resp = await response(request)
await self.dispatch(
"http.lifecycle.response",
inline=True,
context={
"request": request,
"response": resp,
},
)
await response.eof()
else: else:
if not hasattr(handler, "is_websocket"): if not hasattr(handler, "is_websocket"):
raise ServerError( raise ServerError(
@ -1162,6 +1190,11 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
version=version, version=version,
) )
if self.config.USE_UVLOOP is True or (
self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
):
try_use_uvloop()
try: try:
self.is_running = True self.is_running = True
self.is_stopping = False self.is_stopping = False
@ -1189,6 +1222,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
This kills the Sanic This kills the Sanic
""" """
if not self.is_stopping: if not self.is_stopping:
self.shutdown_tasks(timeout=0)
self.is_stopping = True self.is_stopping = True
get_event_loop().stop() get_event_loop().stop()
@ -1258,12 +1292,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
WebSocketProtocol if self.websocket_enabled else HttpProtocol WebSocketProtocol if self.websocket_enabled else HttpProtocol
) )
# if access_log is passed explicitly change config.ACCESS_LOG # Set explicitly passed configuration values
if access_log is not None: for attribute, value in {
self.config.ACCESS_LOG = access_log "ACCESS_LOG": access_log,
"NOISY_EXCEPTIONS": noisy_exceptions,
if noisy_exceptions is not None: }.items():
self.config.NOISY_EXCEPTIONS = noisy_exceptions if value is not None:
setattr(self.config, attribute, value)
server_settings = self._helper( server_settings = self._helper(
host=host, host=host,
@ -1278,6 +1313,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
run_async=return_asyncio_server, run_async=return_asyncio_server,
) )
if self.config.USE_UVLOOP is not _default:
error_logger.warning(
"You are trying to change the uvloop configuration, but "
"this is only effective when using the run(...) method. "
"When using the create_server(...) method Sanic will use "
"the already existing loop."
)
main_start = server_settings.pop("main_start", None) main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None) main_stop = server_settings.pop("main_stop", None)
if main_start or main_stop: if main_start or main_stop:
@ -1399,27 +1442,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
if isinstance(version, int): if isinstance(version, int):
version = HTTP(version) version = HTTP(version)
ssl = process_to_context(ssl)
self.debug = debug self.debug = debug
self.state.host = host self.state.host = host
self.state.port = port self.state.port = port
self.state.workers = workers self.state.workers = workers
self.state.ssl = ssl
# Serve self.state.unix = unix
serve_location = "" self.state.sock = sock
proto = "http"
if ssl is not None:
proto = "https"
if unix:
serve_location = f"{unix} {proto}://..."
elif sock:
serve_location = f"{sock.getsockname()} {proto}://..."
elif host and port:
# colon(:) is legal for a host only in an ipv6 address
display_host = f"[{host}]" if ":" in host else host
serve_location = f"{proto}://{display_host}:{port}"
ssl = process_to_context(ssl)
server_settings = { server_settings = {
"protocol": protocol, "protocol": protocol,
@ -1436,7 +1467,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"version": version, "version": version,
} }
self.motd(serve_location) self.motd(self.serve_location)
if sys.stdout.isatty() and not self.state.is_debug: if sys.stdout.isatty() and not self.state.is_debug:
error_logger.warning( error_logger.warning(
@ -1462,12 +1493,55 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
return server_settings return server_settings
@property
def serve_location(self) -> str:
serve_location = ""
proto = "http"
if self.state.ssl is not None:
proto = "https"
if self.state.unix:
serve_location = f"{self.state.unix} {proto}://..."
elif self.state.sock:
serve_location = f"{self.state.sock.getsockname()} {proto}://..."
elif self.state.host and self.state.port:
# colon(:) is legal for a host only in an ipv6 address
display_host = (
f"[{self.state.host}]"
if ":" in self.state.host
else self.state.host
)
serve_location = f"{proto}://{display_host}:{self.state.port}"
return serve_location
def _build_endpoint_name(self, *parts): def _build_endpoint_name(self, *parts):
parts = [self.name, *parts] parts = [self.name, *parts]
return ".".join(parts) return ".".join(parts)
@classmethod @classmethod
def _prep_task(cls, task, app, loop): def _cancel_websocket_tasks(cls, app, loop):
for task in app.websocket_tasks:
task.cancel()
@staticmethod
async def _listener(
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
):
maybe_coro = listener(app, loop)
if maybe_coro and isawaitable(maybe_coro):
await maybe_coro
# -------------------------------------------------------------------- #
# Task management
# -------------------------------------------------------------------- #
@classmethod
def _prep_task(
cls,
task,
app,
loop,
):
if callable(task): if callable(task):
try: try:
task = task(app) task = task(app)
@ -1477,14 +1551,22 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
return task return task
@classmethod @classmethod
def _loop_add_task(cls, task, app, loop): def _loop_add_task(
cls,
task,
app,
loop,
*,
name: Optional[str] = None,
register: bool = True,
) -> Task:
prepped = cls._prep_task(task, app, loop) prepped = cls._prep_task(task, app, loop)
loop.create_task(prepped) task = loop.create_task(prepped, name=name)
@classmethod if name and register:
def _cancel_websocket_tasks(cls, app, loop): app._task_registry[name] = task
for task in app.websocket_tasks:
task.cancel() return task
@staticmethod @staticmethod
async def dispatch_delayed_tasks(app, loop): async def dispatch_delayed_tasks(app, loop):
@ -1497,13 +1579,132 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
prepped = app._prep_task(task, app, loop) prepped = app._prep_task(task, app, loop)
await prepped await prepped
@staticmethod def add_task(
async def _listener( self,
app: Sanic, loop: AbstractEventLoop, listener: ListenerType task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]],
*,
name: Optional[str] = None,
register: bool = True,
) -> Optional[Task]:
"""
Schedule a task to run later, after the loop has started.
Different from asyncio.ensure_future in that it does not
also return a future, and the actual ensure_future call
is delayed until before server start.
`See user guide re: background tasks
<https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__
:param task: future, couroutine or awaitable
"""
if name and sys.version_info == (3, 7):
name = None
error_logger.warning(
"Cannot set a name for a task when using Python 3.7. Your "
"task will be created without a name."
)
try:
loop = self.loop # Will raise SanicError if loop is not started
return self._loop_add_task(
task, self, loop, name=name, register=register
)
except SanicException:
task_name = f"sanic.delayed_task.{hash(task)}"
if not self._delayed_tasks:
self.after_server_start(partial(self.dispatch_delayed_tasks))
if name:
raise RuntimeError(
"Cannot name task outside of a running application"
)
self.signal(task_name)(partial(self.run_delayed_task, task=task))
self._delayed_tasks.append(task_name)
return None
def get_task(
self, name: str, *, raise_exception: bool = True
) -> Optional[Task]:
if sys.version_info == (3, 7):
raise RuntimeError(
"This feature is only supported on using Python 3.8+."
)
try:
return self._task_registry[name]
except KeyError:
if raise_exception:
raise SanicException(
f'Registered task named "{name}" not found.'
)
return None
async def cancel_task(
self,
name: str,
msg: Optional[str] = None,
*,
raise_exception: bool = True,
) -> None:
if sys.version_info == (3, 7):
raise RuntimeError(
"This feature is only supported on using Python 3.8+."
)
task = self.get_task(name, raise_exception=raise_exception)
if task and not task.cancelled():
args: Tuple[str, ...] = ()
if msg:
if sys.version_info >= (3, 9):
args = (msg,)
else:
raise RuntimeError(
"Cancelling a task with a message is only supported "
"on Python 3.9+."
)
task.cancel(*args)
try:
await task
except CancelledError:
...
def purge_tasks(self):
if sys.version_info == (3, 7):
raise RuntimeError(
"This feature is only supported on using Python 3.8+."
)
for task in self.tasks:
if task.done() or task.cancelled():
name = task.get_name()
self._task_registry[name] = None
self._task_registry = {
k: v for k, v in self._task_registry.items() if v is not None
}
def shutdown_tasks(
self, timeout: Optional[float] = None, increment: float = 0.1
): ):
maybe_coro = listener(app, loop) if sys.version_info == (3, 7):
if maybe_coro and isawaitable(maybe_coro): raise RuntimeError(
await maybe_coro "This feature is only supported on using Python 3.8+."
)
for task in self.tasks:
task.cancel()
if timeout is None:
timeout = self.config.GRACEFUL_SHUTDOWN_TIMEOUT
while len(self._task_registry) and timeout:
self.loop.run_until_complete(asyncio.sleep(increment))
self.purge_tasks()
timeout -= increment
@property
def tasks(self):
if sys.version_info == (3, 7):
raise RuntimeError(
"This feature is only supported on using Python 3.8+."
)
return iter(self._task_registry.values())
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# ASGI # ASGI
@ -1621,11 +1822,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
display["auto-reload"] = reload_display display["auto-reload"] = reload_display
packages = [] packages = []
for package_name, module_name in { for package_name in SANIC_PACKAGES:
"sanic-routing": "sanic_routing", module_name = package_name.replace("-", "_")
"sanic-testing": "sanic_testing",
"sanic-ext": "sanic_ext",
}.items():
try: try:
module = import_module(module_name) module = import_module(module_name)
packages.append(f"{package_name}=={module.__version__}") packages.append(f"{package_name}=={module.__version__}")
@ -1645,6 +1843,41 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
) )
MOTD.output(logo, serve_location, display, extra) MOTD.output(logo, serve_location, display, extra)
@property
def ext(self) -> Extend:
if not hasattr(self, "_ext"):
setup_ext(self, fail=True)
if not hasattr(self, "_ext"):
raise RuntimeError(
"Sanic Extensions is not installed. You can add it to your "
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
"install sanic-ext"
)
return self._ext # type: ignore
def extend(
self,
*,
extensions: Optional[List[Type[Extension]]] = None,
built_in_extensions: bool = True,
config: Optional[Union[Config, Dict[str, Any]]] = None,
**kwargs,
) -> Extend:
if hasattr(self, "_ext"):
raise RuntimeError(
"Cannot extend Sanic after Sanic Extensions has been setup."
)
setup_ext(
self,
extensions=extensions,
built_in_extensions=built_in_extensions,
config=config,
fail=True,
**kwargs,
)
return self.ext
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Class methods # Class methods
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@ -1706,13 +1939,35 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
async def _startup(self): async def _startup(self):
self._future_registry.clear() self._future_registry.clear()
# Startup Sanic Extensions
if not hasattr(self, "_ext"):
setup_ext(self)
if hasattr(self, "_ext"):
self.ext._display()
# Setup routers
self.signalize() self.signalize()
self.finalize() self.finalize()
ErrorHandler.finalize(
self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT # TODO: Replace in v22.6 to check against apps in app registry
) if (
self.__class__._uvloop_setting is not None
and self.__class__._uvloop_setting != self.config.USE_UVLOOP
):
error_logger.warning(
"It looks like you're running several apps with different "
"uvloop settings. This is not supported and may lead to "
"unintended behaviour."
)
self.__class__._uvloop_setting = self.config.USE_UVLOOP
# Startup time optimizations
ErrorHandler.finalize(self.error_handler, config=self.config)
TouchUp.run(self) TouchUp.run(self)
self.state.is_started = True
async def _server_event( async def _server_event(
self, self,
concern: str, concern: str,

39
sanic/application/ext.py Normal file
View File

@ -0,0 +1,39 @@
from __future__ import annotations
from contextlib import suppress
from importlib import import_module
from typing import TYPE_CHECKING
if TYPE_CHECKING: # no cov
from sanic import Sanic
try:
from sanic_ext import Extend # type: ignore
except ImportError:
...
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
if not app.config.AUTO_EXTEND:
return
sanic_ext = None
with suppress(ModuleNotFoundError):
sanic_ext = import_module("sanic_ext")
if not sanic_ext:
if fail:
raise RuntimeError(
"Sanic Extensions is not installed. You can add it to your "
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
"install sanic-ext"
)
return
if not getattr(app, "_ext", None):
Ext: Extend = getattr(sanic_ext, "Extend")
app._ext = Ext(app, **kwargs)
return app.ext

View File

@ -41,9 +41,6 @@ class MOTD(ABC):
class MOTDBasic(MOTD): class MOTDBasic(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
def display(self): def display(self):
if self.logo: if self.logo:
logger.debug(self.logo) logger.debug(self.logo)

View File

@ -5,7 +5,9 @@ import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, auto from enum import Enum, auto
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Set, Union from socket import socket
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Optional, Set, Union
from sanic.log import logger from sanic.log import logger
@ -37,11 +39,15 @@ class ApplicationState:
coffee: bool = field(default=False) coffee: bool = field(default=False)
fast: bool = field(default=False) fast: bool = field(default=False)
host: str = field(default="") host: str = field(default="")
mode: Mode = field(default=Mode.PRODUCTION)
port: int = field(default=0) port: int = field(default=0)
ssl: Optional[SSLContext] = field(default=None)
sock: Optional[socket] = field(default=None)
unix: Optional[str] = field(default=None)
mode: Mode = field(default=Mode.PRODUCTION)
reload_dirs: Set[Path] = field(default_factory=set) reload_dirs: Set[Path] = field(default_factory=set)
server: Server = field(default=Server.SANIC) server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False) is_running: bool = field(default=False)
is_started: bool = field(default=False)
is_stopping: bool = field(default=False) is_stopping: bool = field(default=False)
verbosity: int = field(default=0) verbosity: int = field(default=0)
workers: int = field(default=0) workers: int = field(default=0)

View File

@ -7,6 +7,7 @@ import sanic.app # noqa
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.helpers import _default
from sanic.http import Stage from sanic.http import Stage
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request from sanic.request import Request
@ -53,6 +54,13 @@ class Lifespan:
await self.asgi_app.sanic_app._server_event("init", "before") await self.asgi_app.sanic_app._server_event("init", "before")
await self.asgi_app.sanic_app._server_event("init", "after") await self.asgi_app.sanic_app._server_event("init", "after")
if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
warnings.warn(
"You have set the USE_UVLOOP configuration option, but Sanic "
"cannot control the event loop when running in ASGI mode."
"This option will be ignored."
)
async def shutdown(self) -> None: async def shutdown(self) -> None:
""" """
Gather the listeners to fire on server stop. Gather the listeners to fire on server stop.

0
sanic/base/__init__.py Normal file
View File

6
sanic/base/meta.py Normal file
View File

@ -0,0 +1,6 @@
class SanicMeta(type):
@classmethod
def __prepare__(metaclass, name, bases, **kwds):
cls = super().__prepare__(metaclass, name, bases, **kwds)
cls["__slots__"] = ()
return cls

View File

@ -1,8 +1,8 @@
import re import re
from typing import Any, Tuple from typing import Any
from warnings import warn
from sanic.base.meta import SanicMeta
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from sanic.mixins.exceptions import ExceptionMixin from sanic.mixins.exceptions import ExceptionMixin
from sanic.mixins.listeners import ListenerMixin from sanic.mixins.listeners import ListenerMixin
@ -11,7 +11,7 @@ from sanic.mixins.routes import RouteMixin
from sanic.mixins.signals import SignalMixin from sanic.mixins.signals import SignalMixin
VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$") VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
class BaseSanic( class BaseSanic(
@ -20,8 +20,9 @@ class BaseSanic(
ListenerMixin, ListenerMixin,
ExceptionMixin, ExceptionMixin,
SignalMixin, SignalMixin,
metaclass=SanicMeta,
): ):
__fake_slots__: Tuple[str, ...] __slots__ = ("name",)
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None: def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
class_name = self.__class__.__name__ class_name = self.__class__.__name__
@ -33,11 +34,10 @@ class BaseSanic(
) )
if not VALID_NAME.match(name): if not VALID_NAME.match(name):
warn( raise SanicException(
f"{class_name} instance named '{name}' uses a format that is" f"{class_name} instance named '{name}' uses an invalid "
f"deprecated. Starting in version 21.12, {class_name} objects " "format. Names must begin with a character and may only "
"must be named only using alphanumeric characters, _, or -.", "contain alphanumeric characters, _, or -."
DeprecationWarning,
) )
self.name = name self.name = name
@ -52,15 +52,12 @@ class BaseSanic(
return f'{self.__class__.__name__}(name="{self.name}")' return f'{self.__class__.__name__}(name="{self.name}")'
def __setattr__(self, name: str, value: Any) -> None: def __setattr__(self, name: str, value: Any) -> None:
# This is a temporary compat layer so we can raise a warning until try:
# setting attributes on the app instance can be removed and deprecated super().__setattr__(name, value)
# with a proper implementation of __slots__ except AttributeError as e:
if name not in self.__fake_slots__: raise AttributeError(
warn(
f"Setting variables on {self.__class__.__name__} instances is " f"Setting variables on {self.__class__.__name__} instances is "
"deprecated and will be removed in version 21.12. You should " "not allowed. You should change your "
f"change your {self.__class__.__name__} instance to use " f"{self.__class__.__name__} instance to use "
f"instance.ctx.{name} instead.", f"instance.ctx.{name} instead.",
DeprecationWarning, ) from e
)
super().__setattr__(name, value)

View File

@ -5,7 +5,7 @@ from functools import partial
from typing import TYPE_CHECKING, List, Optional, Union from typing import TYPE_CHECKING, List, Optional, Union
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint

View File

@ -24,7 +24,7 @@ from typing import (
from sanic_routing.exceptions import NotFound # type: ignore from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
from sanic.base import BaseSanic from sanic.base.root import BaseSanic
from sanic.blueprint_group import BlueprintGroup from sanic.blueprint_group import BlueprintGroup
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from sanic.helpers import Default, _default from sanic.helpers import Default, _default
@ -36,8 +36,8 @@ from sanic.models.handler_types import (
) )
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from sanic import Sanic # noqa from sanic import Sanic
def lazy(func, as_decorator=True): def lazy(func, as_decorator=True):
@ -85,7 +85,7 @@ class Blueprint(BaseSanic):
trailing */* trailing */*
""" """
__fake_slots__ = ( __slots__ = (
"_apps", "_apps",
"_future_routes", "_future_routes",
"_future_statics", "_future_statics",
@ -98,7 +98,6 @@ class Blueprint(BaseSanic):
"host", "host",
"listeners", "listeners",
"middlewares", "middlewares",
"name",
"routes", "routes",
"statics", "statics",
"strict_slashes", "strict_slashes",
@ -348,6 +347,7 @@ class Blueprint(BaseSanic):
future.static, future.static,
version_prefix, version_prefix,
error_format, error_format,
future.route_context,
) )
if (self, apply_route) in app._future_registry: if (self, apply_route) in app._future_registry:
@ -400,8 +400,9 @@ class Blueprint(BaseSanic):
for future in self._future_signals: for future in self._future_signals:
if (self, future) in app._future_registry: if (self, future) in app._future_registry:
continue continue
future.condition.update({"blueprint": self.name}) future.condition.update({"__blueprint__": self.name})
app._apply_signal(future) # Force exclusive to be False
app._apply_signal(tuple((*future[:-1], False)))
self.routes += [route for route in routes if isinstance(route, Route)] self.routes += [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [ self.websocket_routes += [
@ -426,7 +427,7 @@ class Blueprint(BaseSanic):
async def dispatch(self, *args, **kwargs): async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {}) condition = kwargs.pop("condition", {})
condition.update({"blueprint": self.name}) condition.update({"__blueprint__": self.name})
kwargs["condition"] = condition kwargs["condition"] = condition
await asyncio.gather( await asyncio.gather(
*[app.dispatch(*args, **kwargs) for app in self.apps] *[app.dispatch(*args, **kwargs) for app in self.apps]

View File

@ -8,6 +8,14 @@ from multidict import CIMultiDict # type: ignore
OS_IS_WINDOWS = os.name == "nt" OS_IS_WINDOWS = os.name == "nt"
UVLOOP_INSTALLED = False
try:
import uvloop # type: ignore # noqa
UVLOOP_INSTALLED = True
except ImportError:
pass
def enable_windows_color_support(): def enable_windows_color_support():

View File

@ -1,28 +1,26 @@
from __future__ import annotations from __future__ import annotations
from inspect import isclass from inspect import getmembers, isclass, isdatadescriptor
from os import environ from os import environ
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional, Union from typing import Any, Callable, Dict, Optional, Sequence, Union
from warnings import warn
from sanic.errorpages import check_error_format from sanic.errorpages import DEFAULT_FORMAT, check_error_format
from sanic.helpers import Default, _default
from sanic.http import Http from sanic.http import Http
from sanic.log import deprecation, error_logger
from sanic.utils import load_module_from_file_location, str_to_bool from sanic.utils import load_module_from_file_location, str_to_bool
if TYPE_CHECKING: # no cov
from sanic import Sanic
SANIC_PREFIX = "SANIC_" SANIC_PREFIX = "SANIC_"
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"_FALLBACK_ERROR_FORMAT": _default,
"ACCESS_LOG": True, "ACCESS_LOG": True,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False, "AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False, "EVENT_AUTOREGISTER": False,
"FALLBACK_ERROR_FORMAT": "auto",
"FORWARDED_FOR_HEADER": "X-Forwarded-For", "FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FORWARDED_SECRET": None, "FORWARDED_SECRET": None,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
@ -40,17 +38,31 @@ DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds "REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20, "WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20, "WEBSOCKET_PING_TIMEOUT": 20,
} }
# These values will be removed from the Config object in v22.6 and moved
# to the application state
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
class Config(dict):
class DescriptorMeta(type):
def __init__(cls, *_):
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
@staticmethod
def _is_setter(member: object):
return isdatadescriptor(member) and hasattr(member, "setter")
class Config(dict, metaclass=DescriptorMeta):
ACCESS_LOG: bool ACCESS_LOG: bool
AUTO_EXTEND: bool
AUTO_RELOAD: bool AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool EVENT_AUTOREGISTER: bool
FALLBACK_ERROR_FORMAT: str
FORWARDED_FOR_HEADER: str FORWARDED_FOR_HEADER: str
FORWARDED_SECRET: Optional[str] FORWARDED_SECRET: Optional[str]
GRACEFUL_SHUTDOWN_TIMEOUT: float GRACEFUL_SHUTDOWN_TIMEOUT: float
@ -69,6 +81,7 @@ class Config(dict):
REQUEST_TIMEOUT: int REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int RESPONSE_TIMEOUT: int
SERVER_NAME: str SERVER_NAME: str
USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: int WEBSOCKET_PING_TIMEOUT: int
@ -76,33 +89,27 @@ class Config(dict):
def __init__( def __init__(
self, self,
defaults: Dict[str, Union[str, bool, int, float, None]] = None, defaults: Dict[str, Union[str, bool, int, float, None]] = None,
load_env: Optional[Union[bool, str]] = True,
env_prefix: Optional[str] = SANIC_PREFIX, env_prefix: Optional[str] = SANIC_PREFIX,
keep_alive: Optional[bool] = None, keep_alive: Optional[bool] = None,
*, *,
app: Optional[Sanic] = None, converters: Optional[Sequence[Callable[[str], Any]]] = None,
): ):
defaults = defaults or {} defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults}) super().__init__({**DEFAULT_CONFIG, **defaults})
self._app = app self._converters = [str, str_to_bool, float, int]
self._LOGO = "" self._LOGO = ""
if converters:
for converter in converters:
self.register_type(converter)
if keep_alive is not None: if keep_alive is not None:
self.KEEP_ALIVE = keep_alive self.KEEP_ALIVE = keep_alive
if env_prefix != SANIC_PREFIX: if env_prefix != SANIC_PREFIX:
if env_prefix: if env_prefix:
self.load_environment_vars(env_prefix) self.load_environment_vars(env_prefix)
elif load_env is not True:
if load_env:
self.load_environment_vars(prefix=load_env)
warn(
"Use of load_env is deprecated and will be removed in "
"21.12. Modify the configuration prefix by passing "
"env_prefix instead.",
DeprecationWarning,
)
else: else:
self.load_environment_vars(SANIC_PREFIX) self.load_environment_vars(SANIC_PREFIX)
@ -117,6 +124,13 @@ class Config(dict):
raise AttributeError(f"Config has no '{ke.args[0]}'") raise AttributeError(f"Config has no '{ke.args[0]}'")
def __setattr__(self, attr, value) -> None: def __setattr__(self, attr, value) -> None:
if attr in self.__class__.__setters__:
try:
super().__setattr__(attr, value)
except AttributeError:
...
else:
return None
self.update({attr: value}) self.update({attr: value})
def __setitem__(self, attr, value) -> None: def __setitem__(self, attr, value) -> None:
@ -136,32 +150,37 @@ class Config(dict):
"REQUEST_MAX_SIZE", "REQUEST_MAX_SIZE",
): ):
self._configure_header_size() self._configure_header_size()
elif attr == "FALLBACK_ERROR_FORMAT":
self._check_error_format()
if self.app and value != self.app.error_handler.fallback:
if self.app.error_handler.fallback != "auto":
warn(
"Overriding non-default ErrorHandler fallback "
"value. Changing from "
f"{self.app.error_handler.fallback} to {value}."
)
self.app.error_handler.fallback = value
elif attr == "LOGO": elif attr == "LOGO":
self._LOGO = value self._LOGO = value
warn( deprecation(
"Setting the config.LOGO is deprecated and will no longer " "Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.", "be supported starting in v22.6.",
DeprecationWarning, 22.6,
) )
@property
def app(self):
return self._app
@property @property
def LOGO(self): def LOGO(self):
return self._LOGO return self._LOGO
@property
def FALLBACK_ERROR_FORMAT(self) -> str:
if self._FALLBACK_ERROR_FORMAT is _default:
return DEFAULT_FORMAT
return self._FALLBACK_ERROR_FORMAT
@FALLBACK_ERROR_FORMAT.setter
def FALLBACK_ERROR_FORMAT(self, value):
self._check_error_format(value)
if (
self._FALLBACK_ERROR_FORMAT is not _default
and value != self._FALLBACK_ERROR_FORMAT
):
error_logger.warning(
"Setting config.FALLBACK_ERROR_FORMAT on an already "
"configured value may have unintended consequences."
)
self._FALLBACK_ERROR_FORMAT = value
def _configure_header_size(self): def _configure_header_size(self):
Http.set_header_max_size( Http.set_header_max_size(
self.REQUEST_MAX_HEADER_SIZE, self.REQUEST_MAX_HEADER_SIZE,
@ -169,8 +188,8 @@ class Config(dict):
self.REQUEST_MAX_SIZE, self.REQUEST_MAX_SIZE,
) )
def _check_error_format(self): def _check_error_format(self, format: Optional[str] = None):
check_error_format(self.FALLBACK_ERROR_FORMAT) check_error_format(format or self.FALLBACK_ERROR_FORMAT)
def load_environment_vars(self, prefix=SANIC_PREFIX): def load_environment_vars(self, prefix=SANIC_PREFIX):
""" """
@ -184,20 +203,45 @@ class Config(dict):
- ``float`` - ``float``
- ``bool`` - ``bool``
Anything else will be imported as a ``str``. Anything else will be imported as a ``str``. If you would like to add
additional types to this list, you can use
:meth:`sanic.config.Config.register_type`. Just make sure that they
are registered before you instantiate your application.
.. code-block:: python
class Foo:
def __init__(self, name) -> None:
self.name = name
config = Config(converters=[Foo])
app = Sanic(__name__, config=config)
`See user guide re: config
<https://sanicframework.org/guide/deployment/configuration.html>`__
""" """
lower_case_var_found = False
for key, value in environ.items(): for key, value in environ.items():
if not key.startswith(prefix): if not key.startswith(prefix):
continue continue
if not key.isupper():
lower_case_var_found = True
_, config_key = key.split(prefix, 1) _, config_key = key.split(prefix, 1)
for converter in (int, float, str_to_bool, str): for converter in reversed(self._converters):
try: try:
self[config_key] = converter(value) self[config_key] = converter(value)
break break
except ValueError: except ValueError:
pass pass
if lower_case_var_found:
deprecation(
"Lowercase environment variables will not be "
"loaded into Sanic config beginning in v22.9.",
22.9,
)
def update_config(self, config: Union[bytes, str, dict, Any]): def update_config(self, config: Union[bytes, str, dict, Any]):
""" """
@ -267,3 +311,17 @@ class Config(dict):
self.update(config) self.update(config)
load = update_config load = update_config
def register_type(self, converter: Callable[[str], Any]) -> None:
"""
Allows for adding custom function to cast from a string value to any
other type. The function should raise ValueError if it is not the
correct type.
"""
if converter in self._converters:
error_logger.warning(
f"Configuration value converter '{converter.__name__}' has "
"already been registered"
)
return
self._converters.append(converter)

View File

@ -34,6 +34,7 @@ except ImportError: # noqa
from json import dumps from json import dumps
DEFAULT_FORMAT = "auto"
FALLBACK_TEXT = ( FALLBACK_TEXT = (
"The server encountered an internal error and " "The server encountered an internal error and "
"cannot complete your request." "cannot complete your request."

View File

@ -244,25 +244,3 @@ class InvalidSignal(SanicException):
class WebsocketClosed(SanicException): class WebsocketClosed(SanicException):
quiet = True quiet = True
message = "Client has closed the websocket connection" message = "Client has closed the websocket connection"
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
"""
Raise an exception based on SanicException. Returns the HTTP response
message appropriate for the given status code, unless provided.
STATUS_CODES from sanic.helpers for the given status code.
:param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages in
"""
import warnings
warnings.warn(
"sanic.exceptions.abort has been marked as deprecated, and will be "
"removed in release 21.12.\n To migrate your code, simply replace "
"abort(status_code, msg) with raise SanicException(msg, status_code), "
"or even better, raise an appropriate SanicException subclass."
)
raise SanicException(message=message, status_code=status_code)

View File

@ -1,14 +1,23 @@
from inspect import signature from __future__ import annotations
from typing import Dict, List, Optional, Tuple, Type
from warnings import warn
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response from inspect import signature
from typing import Dict, List, Optional, Tuple, Type, Union
from sanic.config import Config
from sanic.errorpages import (
DEFAULT_FORMAT,
BaseRenderer,
HTMLRenderer,
exception_response,
)
from sanic.exceptions import ( from sanic.exceptions import (
ContentRangeError, ContentRangeError,
HeaderNotFound, HeaderNotFound,
InvalidRangeType, InvalidRangeType,
SanicException,
) )
from sanic.log import error_logger from sanic.helpers import Default, _default
from sanic.log import deprecation, error_logger
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
from sanic.response import text from sanic.response import text
@ -28,24 +37,91 @@ class ErrorHandler:
# Beginning in v22.3, the base renderer will be TextRenderer # Beginning in v22.3, the base renderer will be TextRenderer
def __init__( def __init__(
self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer self,
fallback: Union[str, Default] = _default,
base: Type[BaseRenderer] = HTMLRenderer,
): ):
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = [] self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
self.cached_handlers: Dict[ self.cached_handlers: Dict[
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler] Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
] = {} ] = {}
self.debug = False self.debug = False
self.fallback = fallback self._fallback = fallback
self.base = base self.base = base
if fallback is not _default:
self._warn_fallback_deprecation()
@property
def fallback(self):
# This is for backwards compat and can be removed in v22.6
if self._fallback is _default:
return DEFAULT_FORMAT
return self._fallback
@fallback.setter
def fallback(self, value: str):
self._warn_fallback_deprecation()
if not isinstance(value, str):
raise SanicException(
f"Cannot set error handler fallback to: value={value}"
)
self._fallback = value
@staticmethod
def _warn_fallback_deprecation():
deprecation(
"Setting the ErrorHandler fallback value directly is "
"deprecated and no longer supported. This feature will "
"be removed in v22.6. Instead, use "
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
)
@classmethod @classmethod
def finalize(cls, error_handler, fallback: Optional[str] = None): def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
if ( if error_handler._fallback is not _default:
fallback if config._FALLBACK_ERROR_FORMAT is _default:
and fallback != "auto" return error_handler.fallback
and error_handler.fallback == "auto"
): error_logger.warning(
error_handler.fallback = fallback "Conflicting error fallback values were found in the "
"error handler and in the app.config while handling an "
"exception. Using the value from app.config."
)
return config.FALLBACK_ERROR_FORMAT
@classmethod
def finalize(
cls,
error_handler: ErrorHandler,
fallback: Optional[str] = None,
config: Optional[Config] = None,
):
if fallback:
deprecation(
"Setting the ErrorHandler fallback value via finalize() "
"is deprecated and no longer supported. This feature will "
"be removed in v22.6. Instead, use "
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
)
if config is None:
deprecation(
"Starting in v22.3, config will be a required argument "
"for ErrorHandler.finalize().",
22.3,
)
if fallback and fallback != DEFAULT_FORMAT:
if error_handler._fallback is not _default:
error_logger.warning(
f"Setting the fallback value to {fallback}. This changes "
"the current non-default value "
f"'{error_handler._fallback}'."
)
error_handler._fallback = fallback
if not isinstance(error_handler, cls): if not isinstance(error_handler, cls):
error_logger.warning( error_logger.warning(
@ -54,7 +130,7 @@ class ErrorHandler:
sig = signature(error_handler.lookup) sig = signature(error_handler.lookup)
if len(sig.parameters) == 1: if len(sig.parameters) == 1:
warn( deprecation(
"You are using a deprecated error handler. The lookup " "You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: " "method should accept two positional parameters: "
"(exception, route_name: Optional[str]). " "(exception, route_name: Optional[str]). "
@ -62,9 +138,10 @@ class ErrorHandler:
"specific exceptions will not work properly. Beginning " "specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not " "in v22.3, the legacy style lookup method will not "
"work at all.", "work at all.",
DeprecationWarning, 22.3,
) )
error_handler._lookup = error_handler._legacy_lookup legacy_lookup = error_handler._legacy_lookup
error_handler._lookup = legacy_lookup # type: ignore
def _full_lookup(self, exception, route_name: Optional[str] = None): def _full_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception, route_name) return self.lookup(exception, route_name)
@ -188,12 +265,13 @@ class ErrorHandler:
:return: :return:
""" """
self.log(request, exception) self.log(request, exception)
fallback = ErrorHandler._get_fallback_value(self, request.app.config)
return exception_response( return exception_response(
request, request,
exception, exception,
debug=self.debug, debug=self.debug,
base=self.base, base=self.base,
fallback=self.fallback, fallback=fallback,
) )
@staticmethod @staticmethod

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse from sanic.response import BaseHTTPResponse

View File

@ -3,6 +3,7 @@ import sys
from enum import Enum from enum import Enum
from typing import Any, Dict from typing import Any, Dict
from warnings import warn
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
@ -78,3 +79,11 @@ access_logger = logging.getLogger("sanic.access")
""" """
Logger used by Sanic for access logging Logger used by Sanic for access logging
""" """
def deprecation(message: str, version: float):
version_info = f"[DEPRECATION v{version}] "
if sys.stdout.isatty():
version_info = f"{Colors.RED}{version_info}"
message = f"{Colors.YELLOW}{message}{Colors.END}"
warn(version_info + message, DeprecationWarning)

View File

@ -1,9 +1,10 @@
from typing import Set from typing import Set
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureException from sanic.models.futures import FutureException
class ExceptionMixin: class ExceptionMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
self._future_exceptions: Set[FutureException] = set() self._future_exceptions: Set[FutureException] = set()

View File

@ -2,6 +2,7 @@ from enum import Enum, auto
from functools import partial from functools import partial
from typing import List, Optional, Union from typing import List, Optional, Union
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureListener from sanic.models.futures import FutureListener
from sanic.models.handler_types import ListenerType, Sanic from sanic.models.handler_types import ListenerType, Sanic
@ -18,7 +19,7 @@ class ListenerEvent(str, Enum):
MAIN_PROCESS_STOP = auto() MAIN_PROCESS_STOP = auto()
class ListenerMixin: class ListenerMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
self._future_listeners: List[FutureListener] = [] self._future_listeners: List[FutureListener] = []

View File

@ -1,10 +1,11 @@
from functools import partial from functools import partial
from typing import List from typing import List
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureMiddleware from sanic.models.futures import FutureMiddleware
class MiddlewareMixin: class MiddlewareMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
self._future_middleware: List[FutureMiddleware] = [] self._future_middleware: List[FutureMiddleware] = []

View File

@ -1,4 +1,5 @@
from ast import NodeVisitor, Return, parse from ast import NodeVisitor, Return, parse
from contextlib import suppress
from functools import partial, wraps from functools import partial, wraps
from inspect import getsource, signature from inspect import getsource, signature
from mimetypes import guess_type from mimetypes import guess_type
@ -12,6 +13,7 @@ from urllib.parse import unquote
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
from sanic.base.meta import SanicMeta
from sanic.compat import stat_async from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING from sanic.errorpages import RESPONSE_MAPPING
@ -22,19 +24,27 @@ from sanic.exceptions import (
InvalidUsage, InvalidUsage,
) )
from sanic.handlers import ContentRangeHandler from sanic.handlers import ContentRangeHandler
from sanic.log import error_logger from sanic.log import deprecation, error_logger
from sanic.models.futures import FutureRoute, FutureStatic from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
from sanic.response import HTTPResponse, file, file_stream from sanic.response import HTTPResponse, file, file_stream
from sanic.views import CompositionView from sanic.types import HashableDict
RouteWrapper = Callable[ RouteWrapper = Callable[
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]] [RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
] ]
RESTRICTED_ROUTE_CONTEXT = (
"ignore_body",
"stream",
"hosts",
"static",
"error_format",
"websocket",
)
class RouteMixin: class RouteMixin(metaclass=SanicMeta):
name: str name: str
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
@ -65,10 +75,20 @@ class RouteMixin:
static: bool = False, static: bool = False,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Decorate a function to be registered as a route Decorate a function to be registered as a route
**Example using context kwargs**
.. code-block:: python
@app.route(..., ctx_foo="foobar")
async def route_handler(request: Request):
assert request.route.ctx.foo == "foobar"
:param uri: path of the URL :param uri: path of the URL
:param methods: list or tuple of methods allowed :param methods: list or tuple of methods allowed
:param host: the host, if required :param host: the host, if required
@ -80,6 +100,8 @@ class RouteMixin:
body (eg. GET requests) body (eg. GET requests)
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: tuple of routes, decorated function :return: tuple of routes, decorated function
""" """
@ -94,6 +116,8 @@ class RouteMixin:
if not methods and not websocket: if not methods and not websocket:
methods = frozenset({"GET"}) methods = frozenset({"GET"})
route_context = self._build_route_context(ctx_kwargs)
def decorator(handler): def decorator(handler):
nonlocal uri nonlocal uri
nonlocal methods nonlocal methods
@ -152,6 +176,7 @@ class RouteMixin:
static, static,
version_prefix, version_prefix,
error_format, error_format,
route_context,
) )
self._future_routes.add(route) self._future_routes.add(route)
@ -196,6 +221,7 @@ class RouteMixin:
stream: bool = False, stream: bool = False,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteHandler: ) -> RouteHandler:
"""A helper method to register class instance or """A helper method to register class instance or
functions as a handler to the application url functions as a handler to the application url
@ -212,6 +238,8 @@ class RouteMixin:
:param stream: boolean specifying if the handler is a stream handler :param stream: boolean specifying if the handler is a stream handler
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: function or class instance :return: function or class instance
""" """
# Handle HTTPMethodView differently # Handle HTTPMethodView differently
@ -226,14 +254,6 @@ class RouteMixin:
if hasattr(_handler, "is_stream"): if hasattr(_handler, "is_stream"):
stream = True stream = True
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
for _handler in handler.handlers.values():
if hasattr(_handler, "is_stream"):
stream = True
break
if strict_slashes is None: if strict_slashes is None:
strict_slashes = self.strict_slashes strict_slashes = self.strict_slashes
@ -247,6 +267,7 @@ class RouteMixin:
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
)(handler) )(handler)
return handler return handler
@ -261,6 +282,7 @@ class RouteMixin:
ignore_body: bool = True, ignore_body: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **GET** *HTTP* method Add an API URL under the **GET** *HTTP* method
@ -273,6 +295,8 @@ class RouteMixin:
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@ -285,6 +309,7 @@ class RouteMixin:
ignore_body=ignore_body, ignore_body=ignore_body,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def post( def post(
@ -297,6 +322,7 @@ class RouteMixin:
name: Optional[str] = None, name: Optional[str] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **POST** *HTTP* method Add an API URL under the **POST** *HTTP* method
@ -309,6 +335,8 @@ class RouteMixin:
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@ -321,6 +349,7 @@ class RouteMixin:
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def put( def put(
@ -333,6 +362,7 @@ class RouteMixin:
name: Optional[str] = None, name: Optional[str] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **PUT** *HTTP* method Add an API URL under the **PUT** *HTTP* method
@ -345,6 +375,8 @@ class RouteMixin:
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@ -357,6 +389,7 @@ class RouteMixin:
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def head( def head(
@ -369,6 +402,7 @@ class RouteMixin:
ignore_body: bool = True, ignore_body: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **HEAD** *HTTP* method Add an API URL under the **HEAD** *HTTP* method
@ -389,6 +423,8 @@ class RouteMixin:
:type ignore_body: bool, optional :type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@ -401,6 +437,7 @@ class RouteMixin:
ignore_body=ignore_body, ignore_body=ignore_body,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def options( def options(
@ -413,6 +450,7 @@ class RouteMixin:
ignore_body: bool = True, ignore_body: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **OPTIONS** *HTTP* method Add an API URL under the **OPTIONS** *HTTP* method
@ -433,6 +471,8 @@ class RouteMixin:
:type ignore_body: bool, optional :type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@ -445,6 +485,7 @@ class RouteMixin:
ignore_body=ignore_body, ignore_body=ignore_body,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def patch( def patch(
@ -457,6 +498,7 @@ class RouteMixin:
name: Optional[str] = None, name: Optional[str] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **PATCH** *HTTP* method Add an API URL under the **PATCH** *HTTP* method
@ -479,6 +521,8 @@ class RouteMixin:
:type ignore_body: bool, optional :type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@ -491,6 +535,7 @@ class RouteMixin:
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def delete( def delete(
@ -503,6 +548,7 @@ class RouteMixin:
ignore_body: bool = True, ignore_body: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **DELETE** *HTTP* method Add an API URL under the **DELETE** *HTTP* method
@ -515,6 +561,8 @@ class RouteMixin:
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@ -527,6 +575,7 @@ class RouteMixin:
ignore_body=ignore_body, ignore_body=ignore_body,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def websocket( def websocket(
@ -540,6 +589,7 @@ class RouteMixin:
apply: bool = True, apply: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
): ):
""" """
Decorate a function to be registered as a websocket route Decorate a function to be registered as a websocket route
@ -553,6 +603,8 @@ class RouteMixin:
be used with :func:`url_for` be used with :func:`url_for`
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: tuple of routes, decorated function :return: tuple of routes, decorated function
""" """
return self.route( return self.route(
@ -567,6 +619,7 @@ class RouteMixin:
websocket=True, websocket=True,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def add_websocket_route( def add_websocket_route(
@ -580,6 +633,7 @@ class RouteMixin:
name: Optional[str] = None, name: Optional[str] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
): ):
""" """
A helper method to register a function as a websocket route. A helper method to register a function as a websocket route.
@ -598,6 +652,8 @@ class RouteMixin:
be used with :func:`url_for` be used with :func:`url_for`
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Objected decorated by :func:`websocket` :return: Objected decorated by :func:`websocket`
""" """
return self.websocket( return self.websocket(
@ -609,6 +665,7 @@ class RouteMixin:
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
)(handler) )(handler)
def static( def static(
@ -918,19 +975,16 @@ class RouteMixin:
return route return route
def _determine_error_format(self, handler) -> Optional[str]: def _determine_error_format(self, handler) -> str:
if not isinstance(handler, CompositionView): with suppress(OSError, TypeError):
try: src = dedent(getsource(handler))
src = dedent(getsource(handler)) tree = parse(src)
tree = parse(src) http_response_types = self._get_response_types(tree)
http_response_types = self._get_response_types(tree)
if len(http_response_types) == 1: if len(http_response_types) == 1:
return next(iter(http_response_types)) return next(iter(http_response_types))
except (OSError, TypeError):
...
return None return ""
def _get_response_types(self, node): def _get_response_types(self, node):
types = set() types = set()
@ -939,7 +993,18 @@ class RouteMixin:
def visit_Return(self, node: Return) -> Any: def visit_Return(self, node: Return) -> Any:
nonlocal types nonlocal types
try: with suppress(AttributeError):
if node.value.func.id == "stream": # type: ignore
deprecation(
"The sanic.response.stream method has been "
"deprecated and will be removed in v22.6. Please "
"upgrade your application to use the new style "
"streaming pattern. See "
"https://sanicframework.org/en/guide/advanced/"
"streaming.html#response-streaming for more "
"information.",
22.6,
)
checks = [node.value.func.id] # type: ignore checks = [node.value.func.id] # type: ignore
if node.value.keywords: # type: ignore if node.value.keywords: # type: ignore
checks += [ checks += [
@ -951,9 +1016,32 @@ class RouteMixin:
for check in checks: for check in checks:
if check in RESPONSE_MAPPING: if check in RESPONSE_MAPPING:
types.add(RESPONSE_MAPPING[check]) types.add(RESPONSE_MAPPING[check])
except AttributeError:
...
HttpResponseVisitor().visit(node) HttpResponseVisitor().visit(node)
return types return types
def _build_route_context(self, raw):
ctx_kwargs = {
key.replace("ctx_", ""): raw.pop(key)
for key in {**raw}.keys()
if key.startswith("ctx_")
}
restricted = [
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
]
if restricted:
restricted_arguments = ", ".join(restricted)
raise AttributeError(
"Cannot use restricted route context: "
f"{restricted_arguments}. This limitation is only in place "
"until v22.3 when the restricted names will no longer be in"
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
"for more information."
)
if raw:
unexpected_arguments = ", ".join(raw.keys())
raise TypeError(
f"Unexpected keyword arguments: {unexpected_arguments}"
)
return HashableDict(ctx_kwargs)

View File

@ -1,17 +1,14 @@
from enum import Enum from enum import Enum
from typing import Any, Callable, Dict, Optional, Set, Union from typing import Any, Callable, Dict, Optional, Set, Union
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureSignal from sanic.models.futures import FutureSignal
from sanic.models.handler_types import SignalHandler from sanic.models.handler_types import SignalHandler
from sanic.signals import Signal from sanic.signals import Signal
from sanic.types import HashableDict
class HashableDict(dict): class SignalMixin(metaclass=SanicMeta):
def __hash__(self):
return hash(tuple(sorted(self.items())))
class SignalMixin:
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
self._future_signals: Set[FutureSignal] = set() self._future_signals: Set[FutureSignal] = set()
@ -24,6 +21,7 @@ class SignalMixin:
*, *,
apply: bool = True, apply: bool = True,
condition: Dict[str, Any] = None, condition: Dict[str, Any] = None,
exclusive: bool = True,
) -> Callable[[SignalHandler], SignalHandler]: ) -> Callable[[SignalHandler], SignalHandler]:
""" """
For creating a signal handler, used similar to a route handler: For creating a signal handler, used similar to a route handler:
@ -36,17 +34,22 @@ class SignalMixin:
:param event: Representation of the event in ``one.two.three`` form :param event: Representation of the event in ``one.two.three`` form
:type event: str :type event: str
:param apply: For lazy evaluation, defaults to True :param apply: For lazy evaluation, defaults to ``True``
:type apply: bool, optional :type apply: bool, optional
:param condition: For use with the ``condition`` argument in dispatch :param condition: For use with the ``condition`` argument in dispatch
filtering, defaults to None filtering, defaults to ``None``
:param exclusive: When ``True``, the signal can only be dispatched
when the condition has been met. When ``False``, the signal can
be dispatched either with or without it. *THIS IS INAPPLICABLE TO
BLUEPRINT SIGNALS. THEY ARE ALWAYS NON-EXCLUSIVE*, defaults
to ``True``
:type condition: Dict[str, Any], optional :type condition: Dict[str, Any], optional
""" """
event_value = str(event.value) if isinstance(event, Enum) else event event_value = str(event.value) if isinstance(event, Enum) else event
def decorator(handler: SignalHandler): def decorator(handler: SignalHandler):
future_signal = FutureSignal( future_signal = FutureSignal(
handler, event_value, HashableDict(condition or {}) handler, event_value, HashableDict(condition or {}), exclusive
) )
self._future_signals.add(future_signal) self._future_signals.add(future_signal)
@ -62,6 +65,7 @@ class SignalMixin:
handler: Optional[Callable[..., Any]], handler: Optional[Callable[..., Any]],
event: str, event: str,
condition: Dict[str, Any] = None, condition: Dict[str, Any] = None,
exclusive: bool = True,
): ):
if not handler: if not handler:
@ -69,7 +73,9 @@ class SignalMixin:
... ...
handler = noop handler = noop
self.signal(event=event, condition=condition)(handler) self.signal(event=event, condition=condition, exclusive=exclusive)(
handler
)
return handler return handler
def event(self, event: str): def event(self, event: str):

View File

@ -7,6 +7,7 @@ from sanic.models.handler_types import (
MiddlewareType, MiddlewareType,
SignalHandler, SignalHandler,
) )
from sanic.types import HashableDict
class FutureRoute(NamedTuple): class FutureRoute(NamedTuple):
@ -25,6 +26,7 @@ class FutureRoute(NamedTuple):
static: bool static: bool
version_prefix: str version_prefix: str
error_format: Optional[str] error_format: Optional[str]
route_context: HashableDict
class FutureListener(NamedTuple): class FutureListener(NamedTuple):
@ -60,6 +62,7 @@ class FutureSignal(NamedTuple):
handler: SignalHandler handler: SignalHandler
event: str event: str
condition: Optional[Dict[str, str]] condition: Optional[Dict[str, str]]
exclusive: bool
class FutureRegistry(set): class FutureRegistry(set):

View File

@ -15,7 +15,7 @@ from typing import (
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from sanic.server import ConnInfo from sanic.server import ConnInfo
from sanic.app import Sanic from sanic.app import Sanic

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from functools import partial from functools import partial
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path
@ -12,10 +14,10 @@ from typing import (
Iterator, Iterator,
Optional, Optional,
Tuple, Tuple,
TypeVar,
Union, Union,
) )
from urllib.parse import quote_plus from urllib.parse import quote_plus
from warnings import warn
from sanic.compat import Header, open_async from sanic.compat import Header, open_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
@ -28,6 +30,10 @@ from sanic.models.protocol_types import HTMLProtocol, Range
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic.asgi import ASGIApp from sanic.asgi import ASGIApp
from sanic.request import Request
else:
Request = TypeVar("Request")
try: try:
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
@ -136,95 +142,6 @@ class BaseHTTPResponse:
await self.stream.send(data, end_stream=end_stream) await self.stream.send(data, end_stream=end_stream)
StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]]
class StreamingHTTPResponse(BaseHTTPResponse):
"""
Old style streaming response where you pass a streaming function:
.. code-block:: python
async def sample_streaming_fn(response):
await response.write("foo")
await asyncio.sleep(1)
await response.write("bar")
await asyncio.sleep(1)
@app.post("/")
async def test(request):
return stream(sample_streaming_fn)
.. warning::
**Deprecated** and set for removal in v21.12. You can now achieve the
same functionality without a callback.
.. code-block:: python
@app.post("/")
async def test(request):
response = await request.respond()
await response.send("foo", False)
await asyncio.sleep(1)
await response.send("bar", False)
await asyncio.sleep(1)
await response.send("", True)
return response
"""
__slots__ = (
"streaming_fn",
"status",
"content_type",
"headers",
"_cookies",
)
def __init__(
self,
streaming_fn: StreamingFunction,
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: str = "text/plain; charset=utf-8",
ignore_deprecation_notice: bool = False,
):
if not ignore_deprecation_notice:
warn(
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
"will be removed in v21.12. Please upgrade your streaming "
"response implementation. You can learn more here: "
"https://sanicframework.org/en/guide/advanced/streaming.html"
"#response-streaming. If you use the builtin stream() or "
"file_stream() methods, this upgrade will be be done for you."
)
super().__init__()
self.content_type = content_type
self.streaming_fn = streaming_fn
self.status = status
self.headers = Header(headers or {})
self._cookies = None
async def write(self, data):
"""Writes a chunk of data to the streaming response.
:param data: str or bytes-ish data to be written.
"""
await super().send(self._encode_body(data))
async def send(self, *args, **kwargs):
if self.streaming_fn is not None:
await self.streaming_fn(self)
self.streaming_fn = None
await super().send(*args, **kwargs)
async def eof(self):
raise NotImplementedError
class HTTPResponse(BaseHTTPResponse): class HTTPResponse(BaseHTTPResponse):
""" """
HTTP response to be sent back to the client. HTTP response to be sent back to the client.
@ -419,6 +336,109 @@ async def file(
) )
def redirect(
to: str,
headers: Optional[Dict[str, str]] = None,
status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response
"""
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers["Location"] = safe_to
return HTTPResponse(
status=status, headers=headers, content_type=content_type
)
class ResponseStream:
"""
ResponseStream is a compat layer to bridge the gap after the deprecation
of StreamingHTTPResponse. In v22.6 it will be removed when:
- stream is removed
- file_stream is moved to new style streaming
- file and file_stream are combined into a single API
"""
__slots__ = (
"_cookies",
"content_type",
"headers",
"request",
"response",
"status",
"streaming_fn",
)
def __init__(
self,
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]],
Coroutine[Any, Any, None],
],
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
self.streaming_fn = streaming_fn
self.status = status
self.headers = headers or Header()
self.content_type = content_type
self.request: Optional[Request] = None
self._cookies: Optional[CookieJar] = None
async def write(self, message: str):
await self.response.send(message)
async def stream(self) -> HTTPResponse:
if not self.request:
raise ServerError("Attempted response to unknown request")
self.response = await self.request.respond(
headers=self.headers,
status=self.status,
content_type=self.content_type,
)
await self.streaming_fn(self)
return self.response
async def eof(self) -> None:
await self.response.eof()
@property
def cookies(self) -> CookieJar:
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
@property
def processed_headers(self):
return self.response.processed_headers
@property
def body(self):
return self.response.body
def __call__(self, request: Request) -> ResponseStream:
self.request = request
return self
def __await__(self):
return self.stream().__await__()
async def file_stream( async def file_stream(
location: Union[str, PurePath], location: Union[str, PurePath],
status: int = 200, status: int = 200,
@ -427,7 +447,7 @@ async def file_stream(
headers: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None, filename: Optional[str] = None,
_range: Optional[Range] = None, _range: Optional[Range] = None,
) -> StreamingHTTPResponse: ) -> ResponseStream:
"""Return a streaming response object with file data. """Return a streaming response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
@ -435,7 +455,6 @@ async def file_stream(
:param mime_type: Specific mime_type. :param mime_type: Specific mime_type.
:param headers: Custom Headers. :param headers: Custom Headers.
:param filename: Override filename. :param filename: Override filename.
:param chunked: Deprecated
:param _range: :param _range:
""" """
headers = headers or {} headers = headers or {}
@ -471,23 +490,24 @@ async def file_stream(
break break
await response.write(content) await response.write(content)
return StreamingHTTPResponse( return ResponseStream(
streaming_fn=_streaming_fn, streaming_fn=_streaming_fn,
status=status, status=status,
headers=headers, headers=headers,
content_type=mime_type, content_type=mime_type,
ignore_deprecation_notice=True,
) )
def stream( def stream(
streaming_fn: StreamingFunction, streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None]
],
status: int = 200, status: int = 200,
headers: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8", content_type: str = "text/plain; charset=utf-8",
): ) -> ResponseStream:
"""Accepts an coroutine `streaming_fn` which can be used to """Accepts a coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`. write chunks to a streaming response. Returns a `ResponseStream`.
Example usage:: Example usage::
@ -501,42 +521,13 @@ def stream(
:param streaming_fn: A coroutine accepts a response and :param streaming_fn: A coroutine accepts a response and
writes content to that response. writes content to that response.
:param mime_type: Specific mime_type. :param status: HTTP status.
:param content_type: Specific content_type.
:param headers: Custom Headers. :param headers: Custom Headers.
:param chunked: Deprecated
""" """
return StreamingHTTPResponse( return ResponseStream(
streaming_fn, streaming_fn,
headers=headers, headers=headers,
content_type=content_type, content_type=content_type,
status=status, status=status,
ignore_deprecation_notice=True,
)
def redirect(
to: str,
headers: Optional[Dict[str, str]] = None,
status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response
"""
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers["Location"] = safe_to
return HTTPResponse(
status=status, headers=headers, content_type=content_type
) )

View File

@ -1,20 +1,10 @@
import asyncio
from sanic.models.server_types import ConnInfo, Signal from sanic.models.server_types import ConnInfo, Signal
from sanic.server.async_server import AsyncioServer from sanic.server.async_server import AsyncioServer
from sanic.server.loop import try_use_uvloop
from sanic.server.protocols.http_protocol import HttpProtocol from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.runners import serve, serve_multiple, serve_single from sanic.server.runners import serve, serve_multiple, serve_single
try:
import uvloop # type: ignore
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
__all__ = ( __all__ = (
"AsyncioServer", "AsyncioServer",
"ConnInfo", "ConnInfo",
@ -23,4 +13,5 @@ __all__ = (
"serve", "serve",
"serve_multiple", "serve_multiple",
"serve_single", "serve_single",
"try_use_uvloop",
) )

View File

@ -2,7 +2,14 @@ from __future__ import annotations
import asyncio import asyncio
from typing import TYPE_CHECKING
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from sanic.log import deprecation
if TYPE_CHECKING:
from sanic import Sanic
class AsyncioServer: class AsyncioServer:
@ -11,11 +18,11 @@ class AsyncioServer:
a user who needs to manage the server lifecycle manually. a user who needs to manage the server lifecycle manually.
""" """
__slots__ = ("app", "connections", "loop", "serve_coro", "server", "init") __slots__ = ("app", "connections", "loop", "serve_coro", "server")
def __init__( def __init__(
self, self,
app, app: Sanic,
loop, loop,
serve_coro, serve_coro,
connections, connections,
@ -27,13 +34,20 @@ class AsyncioServer:
self.loop = loop self.loop = loop
self.serve_coro = serve_coro self.serve_coro = serve_coro
self.server = None self.server = None
self.init = False
@property
def init(self):
deprecation(
"AsyncioServer.init has been deprecated and will be removed "
"in v22.6. Use Sanic.state.is_started instead.",
22.6,
)
return self.app.state.is_started
def startup(self): def startup(self):
""" """
Trigger "before_server_start" events Trigger "before_server_start" events
""" """
self.init = True
return self.app._startup() return self.app._startup()
def before_start(self): def before_start(self):
@ -77,30 +91,33 @@ class AsyncioServer:
return task return task
def start_serving(self): def start_serving(self):
if self.server: return self._serve(self.server.start_serving)
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): def serve_forever(self):
return self._serve(self.server.serve_forever)
def _serve(self, serve_func):
if self.server: if self.server:
if not self.app.state.is_started:
raise SanicException(
"Cannot run Sanic server without first running "
"await server.startup()"
)
try: try:
return self.server.serve_forever() return serve_func()
except AttributeError: except AttributeError:
name = serve_func.__name__
raise NotImplementedError( raise NotImplementedError(
"server.serve_forever not available in this version " f"server.{name} not available in this version "
"of asyncio or uvloop." "of asyncio or uvloop."
) )
def _server_event(self, concern: str, action: str): def _server_event(self, concern: str, action: str):
if not self.init: if not self.app.state.is_started:
raise SanicException( raise SanicException(
"Cannot dispatch server event without " "Cannot dispatch server event without "
"first running server.startup()" "first running await server.startup()"
) )
return self.app._server_event(concern, action, loop=self.loop) return self.app._server_event(concern, action, loop=self.loop)

49
sanic/server/loop.py Normal file
View File

@ -0,0 +1,49 @@
import asyncio
from distutils.util import strtobool
from os import getenv
from sanic.compat import OS_IS_WINDOWS
from sanic.log import error_logger
def try_use_uvloop() -> None:
"""
Use uvloop instead of the default asyncio loop.
"""
if OS_IS_WINDOWS:
error_logger.warning(
"You are trying to use uvloop, but uvloop is not compatible "
"with your system. You can disable uvloop completely by setting "
"the 'USE_UVLOOP' configuration value to false, or simply not "
"defining it and letting Sanic handle it for you. Sanic will now "
"continue to run using the default event loop."
)
return
try:
import uvloop # type: ignore
except ImportError:
error_logger.warning(
"You are trying to use uvloop, but uvloop is not "
"installed in your system. In order to use uvloop "
"you must first install it. Otherwise, you can disable "
"uvloop completely by setting the 'USE_UVLOOP' "
"configuration value to false. Sanic will now continue "
"to run with the default event loop."
)
return
uvloop_install_removed = strtobool(getenv("SANIC_NO_UVLOOP", "no"))
if uvloop_install_removed:
error_logger.info(
"You are requesting to run Sanic using uvloop, but the "
"install-time 'SANIC_NO_UVLOOP' environment variable (used to "
"opt-out of installing uvloop with Sanic) is set to true. If "
"you want to prevent Sanic from overriding the event loop policy "
"during runtime, set the 'USE_UVLOOP' configuration value to "
"false."
)
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

View File

@ -1,12 +1,11 @@
from typing import TYPE_CHECKING, Optional, Sequence, cast from typing import TYPE_CHECKING, Optional, Sequence, cast
from warnings import warn
from websockets.connection import CLOSED, CLOSING, OPEN from websockets.connection import CLOSED, CLOSING, OPEN
from websockets.server import ServerConnection from websockets.server import ServerConnection
from websockets.typing import Subprotocol from websockets.typing import Subprotocol
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.log import error_logger from sanic.log import deprecation, error_logger
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
from ..websockets.impl import WebsocketImplProtocol from ..websockets.impl import WebsocketImplProtocol
@ -17,6 +16,14 @@ if TYPE_CHECKING:
class WebSocketProtocol(HttpProtocol): class WebSocketProtocol(HttpProtocol):
__slots__ = (
"websocket",
"websocket_timeout",
"websocket_max_size",
"websocket_ping_interval",
"websocket_ping_timeout",
)
def __init__( def __init__(
self, self,
*args, *args,
@ -35,24 +42,24 @@ class WebSocketProtocol(HttpProtocol):
self.websocket_max_size = websocket_max_size self.websocket_max_size = websocket_max_size
if websocket_max_queue is not None and websocket_max_queue > 0: if websocket_max_queue is not None and websocket_max_queue > 0:
# TODO: Reminder remove this warning in v22.3 # TODO: Reminder remove this warning in v22.3
warn( deprecation(
"Websocket no longer uses queueing, so websocket_max_queue" "Websocket no longer uses queueing, so websocket_max_queue"
" is no longer required.", " is no longer required.",
DeprecationWarning, 22.3,
) )
if websocket_read_limit is not None and websocket_read_limit > 0: if websocket_read_limit is not None and websocket_read_limit > 0:
# TODO: Reminder remove this warning in v22.3 # TODO: Reminder remove this warning in v22.3
warn( deprecation(
"Websocket no longer uses read buffers, so " "Websocket no longer uses read buffers, so "
"websocket_read_limit is not required.", "websocket_read_limit is not required.",
DeprecationWarning, 22.3,
) )
if websocket_write_limit is not None and websocket_write_limit > 0: if websocket_write_limit is not None and websocket_write_limit > 0:
# TODO: Reminder remove this warning in v22.3 # TODO: Reminder remove this warning in v22.3
warn( deprecation(
"Websocket no longer uses write buffers, so " "Websocket no longer uses write buffers, so "
"websocket_write_limit is not required.", "websocket_write_limit is not required.",
DeprecationWarning, 22.3,
) )
self.websocket_ping_interval = websocket_ping_interval self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout self.websocket_ping_timeout = websocket_ping_timeout

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import sys
from ssl import SSLContext from ssl import SSLContext
from typing import TYPE_CHECKING, Dict, Optional, Type, Union from typing import TYPE_CHECKING, Dict, Optional, Type, Union
@ -22,6 +24,7 @@ from signal import signal as signal_func
from aioquic.asyncio import serve as quic_serve from aioquic.asyncio import serve as quic_serve
from sanic.application.ext import setup_ext
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.http.http3 import get_config, get_ticket_store from sanic.http.http3 import get_config, get_ticket_store
from sanic.log import error_logger, logger from sanic.log import error_logger, logger
@ -122,6 +125,7 @@ def serve(
**asyncio_server_kwargs, **asyncio_server_kwargs,
) )
setup_ext(app)
if run_async: if run_async:
return AsyncioServer( return AsyncioServer(
app=app, app=app,
@ -182,6 +186,9 @@ def serve(
loop.run_until_complete(asyncio.sleep(0.1)) loop.run_until_complete(asyncio.sleep(0.1))
start_shutdown = start_shutdown + 0.1 start_shutdown = start_shutdown + 0.1
if sys.version_info > (3, 7):
app.shutdown_tasks(graceful - start_shutdown)
# Force close non-idle connection after waiting for # Force close non-idle connection after waiting for
# graceful_shutdown_timeout # graceful_shutdown_timeout
for conn in connections: for conn in connections:

View File

@ -4,7 +4,7 @@ import asyncio
from enum import Enum from enum import Enum
from inspect import isawaitable from inspect import isawaitable
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union, cast
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
from sanic_routing.exceptions import NotFound # type: ignore from sanic_routing.exceptions import NotFound # type: ignore
@ -142,12 +142,21 @@ class SignalRouter(BaseRouter):
if context: if context:
params.update(context) params.update(context)
signals = group.routes
if not reverse: if not reverse:
handlers = handlers[::-1] signals = signals[::-1]
try: try:
for handler in handlers: for signal in signals:
if condition is None or condition == handler.__requirements__: params.pop("__trigger__", None)
maybe_coroutine = handler(**params) if (
(condition is None and signal.ctx.exclusive is False)
or (
condition is None
and not signal.handler.__requirements__
)
or (condition == signal.handler.__requirements__)
) and (signal.ctx.trigger or event == signal.ctx.definition):
maybe_coroutine = signal.handler(**params)
if isawaitable(maybe_coroutine): if isawaitable(maybe_coroutine):
retval = await maybe_coroutine retval = await maybe_coroutine
if retval: if retval:
@ -190,23 +199,36 @@ class SignalRouter(BaseRouter):
handler: SignalHandler, handler: SignalHandler,
event: str, event: str,
condition: Optional[Dict[str, Any]] = None, condition: Optional[Dict[str, Any]] = None,
exclusive: bool = True,
) -> Signal: ) -> Signal:
event_definition = event
parts = self._build_event_parts(event) parts = self._build_event_parts(event)
if parts[2].startswith("<"): if parts[2].startswith("<"):
name = ".".join([*parts[:-1], "*"]) name = ".".join([*parts[:-1], "*"])
trigger = self._clean_trigger(parts[2])
else: else:
name = event name = event
trigger = ""
if not trigger:
event = ".".join([*parts[:2], "<__trigger__>"])
handler.__requirements__ = condition # type: ignore handler.__requirements__ = condition # type: ignore
handler.__trigger__ = trigger # type: ignore
return super().add( signal = super().add(
event, event,
handler, handler,
requirements=condition,
name=name, name=name,
append=True, append=True,
) # type: ignore ) # type: ignore
signal.ctx.exclusive = exclusive
signal.ctx.trigger = trigger
signal.ctx.definition = event_definition
return cast(Signal, signal)
def finalize(self, do_compile: bool = True, do_optimize: bool = False): def finalize(self, do_compile: bool = True, do_optimize: bool = False):
self.add(_blank, "sanic.__signal__.__init__") self.add(_blank, "sanic.__signal__.__init__")
@ -238,3 +260,9 @@ class SignalRouter(BaseRouter):
"Cannot declare reserved signal event: %s" % event "Cannot declare reserved signal event: %s" % event
) )
return parts return parts
def _clean_trigger(self, trigger: str) -> str:
trigger = trigger[1:-1]
if ":" in trigger:
trigger, _ = trigger.split(":")
return trigger

View File

@ -1,9 +1,10 @@
from sanic.base.meta import SanicMeta
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from .service import TouchUp from .service import TouchUp
class TouchUpMeta(type): class TouchUpMeta(SanicMeta):
def __new__(cls, name, bases, attrs, **kwargs): def __new__(cls, name, bases, attrs, **kwargs):
gen_class = super().__new__(cls, name, bases, attrs, **kwargs) gen_class = super().__new__(cls, name, bases, attrs, **kwargs)

4
sanic/types/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from .hashable_dict import HashableDict
__all__ = ("HashableDict",)

View File

@ -0,0 +1,3 @@
class HashableDict(dict):
def __hash__(self):
return hash(tuple(sorted(self.items())))

View File

@ -9,14 +9,11 @@ from typing import (
Optional, Optional,
Union, Union,
) )
from warnings import warn
from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from sanic import Sanic from sanic import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
@ -84,6 +81,8 @@ class HTTPMethodView:
def dispatch_request(self, request, *args, **kwargs): def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None) handler = getattr(self, request.method.lower(), None)
if not handler and request.method == "HEAD":
handler = self.get
return handler(request, *args, **kwargs) return handler(request, *args, **kwargs)
@classmethod @classmethod
@ -136,48 +135,3 @@ class HTTPMethodView:
def stream(func): def stream(func):
func.is_stream = True func.is_stream = True
return func return func
class CompositionView:
"""Simple method-function mapped view for the sanic.
You can add handler functions to methods (get, post, put, patch, delete)
for every HTTP method you want to support.
For example:
.. code-block:: python
view = CompositionView()
view.add(['GET'], lambda request: text('I am get method'))
view.add(['POST', 'PUT'], lambda request: text('I am post/put method'))
If someone tries to use a non-implemented method, there will be a
405 response.
"""
def __init__(self):
self.handlers = {}
self.name = self.__class__.__name__
warn(
"CompositionView has been deprecated and will be removed in "
"v21.12. Please update your view to HTTPMethodView.",
DeprecationWarning,
)
def __name__(self):
return self.name
def add(self, methods, handler, stream=False):
if stream:
handler.is_stream = stream
for method in methods:
if method not in HTTP_METHODS:
raise InvalidUsage(f"{method} is not a valid HTTP method.")
if method in self.handlers:
raise InvalidUsage(f"Method {method} is already registered.")
self.handlers[method] = handler
def __call__(self, request, *args, **kwargs):
handler = self.handlers[request.method.upper()]
return handler(request, *args, **kwargs)

View File

@ -7,22 +7,19 @@ import traceback
from gunicorn.workers import base # type: ignore from gunicorn.workers import base # type: ignore
from sanic.compat import UVLOOP_INSTALLED
from sanic.log import logger from sanic.log import logger
from sanic.server import HttpProtocol, Signal, serve from sanic.server import HttpProtocol, Signal, serve, try_use_uvloop
from sanic.server.protocols.websocket_protocol import WebSocketProtocol from sanic.server.protocols.websocket_protocol import WebSocketProtocol
try: try:
import ssl # type: ignore import ssl # type: ignore
except ImportError: except ImportError: # no cov
ssl = None # type: ignore ssl = None # type: ignore
try: if UVLOOP_INSTALLED: # no cov
import uvloop # type: ignore try_use_uvloop()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
class GunicornWorker(base.Worker): class GunicornWorker(base.Worker):

View File

@ -147,6 +147,7 @@ extras_require = {
"dev": dev_require, "dev": dev_require,
"docs": docs_require, "docs": docs_require,
"all": all_require, "all": all_require,
"ext": ["sanic-ext"],
} }
setup_kwargs["install_requires"] = requirements setup_kwargs["install_requires"] = requirements

View File

@ -6,8 +6,10 @@ import string
import sys import sys
import uuid import uuid
from contextlib import suppress
from logging import LogRecord from logging import LogRecord
from typing import Callable, List, Tuple from typing import List, Tuple
from unittest.mock import MagicMock
import pytest import pytest
@ -184,3 +186,21 @@ def message_in_records():
return error_captured return error_captured
return msg_in_log return msg_in_log
@pytest.fixture
def ext_instance():
ext_instance = MagicMock()
ext_instance.injection = MagicMock()
return ext_instance
@pytest.fixture(autouse=True) # type: ignore
def sanic_ext(ext_instance): # noqa
sanic_ext = MagicMock(__version__="1.2.3")
sanic_ext.Extend = MagicMock()
sanic_ext.Extend.return_value = ext_instance
sys.modules["sanic_ext"] = sanic_ext
yield sanic_ext
with suppress(KeyError):
del sys.modules["sanic_ext"]

View File

@ -1,5 +1,4 @@
import json import json
import logging
from sanic import Sanic, text from sanic import Sanic, text
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
@ -9,7 +8,7 @@ LOGGING_CONFIG = {**LOGGING_CONFIG_DEFAULTS}
LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s" LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s"
LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG" LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG"
app = Sanic(__name__, log_config=LOGGING_CONFIG) app = Sanic("FakeServer", log_config=LOGGING_CONFIG)
@app.get("/") @app.get("/")

View File

@ -2,17 +2,20 @@ import asyncio
import logging import logging
import re import re
from email import message from collections import Counter
from inspect import isawaitable from inspect import isawaitable
from os import environ from os import environ
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import py
import pytest import pytest
import sanic
from sanic import Sanic from sanic import Sanic
from sanic.compat import OS_IS_WINDOWS
from sanic.config import Config from sanic.config import Config
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from sanic.helpers import _default
from sanic.response import text from sanic.response import text
@ -21,15 +24,6 @@ def clear_app_registry():
Sanic._app_registry = {} Sanic._app_registry = {}
def uvloop_installed():
try:
import uvloop # noqa
return True
except ImportError:
return False
def test_app_loop_running(app): def test_app_loop_running(app):
@app.get("/test") @app.get("/test")
async def handler(request): async def handler(request):
@ -41,41 +35,39 @@ def test_app_loop_running(app):
def test_create_asyncio_server(app): def test_create_asyncio_server(app):
if not uvloop_installed(): loop = asyncio.get_event_loop()
loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server(return_asyncio_server=True)
asyncio_srv_coro = app.create_server(return_asyncio_server=True) assert isawaitable(asyncio_srv_coro)
assert isawaitable(asyncio_srv_coro) srv = loop.run_until_complete(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro) assert srv.is_serving() is True
assert srv.is_serving() is True
def test_asyncio_server_no_start_serving(app): def test_asyncio_server_no_start_serving(app):
if not uvloop_installed(): loop = asyncio.get_event_loop()
loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server(
asyncio_srv_coro = app.create_server( port=43123,
port=43123, return_asyncio_server=True,
return_asyncio_server=True, asyncio_server_kwargs=dict(start_serving=False),
asyncio_server_kwargs=dict(start_serving=False), )
) srv = loop.run_until_complete(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro) assert srv.is_serving() is False
assert srv.is_serving() is False
def test_asyncio_server_start_serving(app): def test_asyncio_server_start_serving(app):
if not uvloop_installed(): loop = asyncio.get_event_loop()
loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server(
asyncio_srv_coro = app.create_server( port=43124,
port=43124, return_asyncio_server=True,
return_asyncio_server=True, asyncio_server_kwargs=dict(start_serving=False),
asyncio_server_kwargs=dict(start_serving=False), )
) srv = loop.run_until_complete(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro) assert srv.is_serving() is False
assert srv.is_serving() is False loop.run_until_complete(srv.startup())
loop.run_until_complete(srv.start_serving()) loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True assert srv.is_serving() is True
wait_close = srv.close() wait_close = srv.close()
loop.run_until_complete(wait_close) loop.run_until_complete(wait_close)
# Looks like we can't easily test `serve_forever()` # Looks like we can't easily test `serve_forever()`
def test_create_server_main(app, caplog): def test_create_server_main(app, caplog):
@ -92,6 +84,21 @@ def test_create_server_main(app, caplog):
) in caplog.record_tuples ) in caplog.record_tuples
def test_create_server_no_startup(app):
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)
message = (
"Cannot run Sanic server without first running await server.startup()"
)
with pytest.raises(SanicException, match=message):
loop.run_until_complete(srv.start_serving())
def test_create_server_main_convenience(app, caplog): def test_create_server_main_convenience(app, caplog):
app.main_process_start(lambda *_: ...) app.main_process_start(lambda *_: ...)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@ -106,6 +113,19 @@ def test_create_server_main_convenience(app, caplog):
) in caplog.record_tuples ) in caplog.record_tuples
def test_create_server_init(app, caplog):
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
server = loop.run_until_complete(asyncio_srv_coro)
message = (
"AsyncioServer.init has been deprecated and will be removed in v22.6. "
"Use Sanic.state.is_started instead."
)
with pytest.warns(DeprecationWarning, match=message):
server.init
def test_app_loop_not_running(app): def test_app_loop_not_running(app):
with pytest.raises(SanicException) as excinfo: with pytest.raises(SanicException) as excinfo:
app.loop app.loop
@ -373,6 +393,22 @@ def test_app_no_registry():
Sanic.get_app("no-register") Sanic.get_app("no-register")
def test_app_no_registry_deprecation_message():
with pytest.warns(DeprecationWarning) as records:
Sanic("no-register", register=False)
Sanic("yes-register", register=True)
message = (
"[DEPRECATION v22.6] The register argument is deprecated and will "
"stop working in v22.6. After v22.6 all apps will be added to the "
"Sanic app registry."
)
assert len(records) == 2
for record in records:
assert record.message.args[0] == message
def test_app_no_registry_env(): def test_app_no_registry_env():
environ["SANIC_REGISTER"] = "False" environ["SANIC_REGISTER"] = "False"
Sanic("no-register") Sanic("no-register")
@ -384,15 +420,12 @@ def test_app_no_registry_env():
def test_app_set_attribute_warning(app): def test_app_set_attribute_warning(app):
with pytest.warns(DeprecationWarning) as record: message = (
app.foo = 1 "Setting variables on Sanic instances is not allowed. You should "
"change your Sanic instance to use instance.ctx.foo instead."
assert len(record) == 1
assert record[0].message.args[0] == (
"Setting variables on Sanic instances is deprecated "
"and will be removed in version 21.12. You should change your "
"Sanic instance to use instance.ctx.foo instead."
) )
with pytest.raises(AttributeError, match=message):
app.foo = 1
def test_app_set_context(app): def test_app_set_context(app):
@ -414,15 +447,7 @@ def test_bad_custom_config():
SanicException, SanicException,
match=( match=(
"When instantiating Sanic with config, you cannot also pass " "When instantiating Sanic with config, you cannot also pass "
"load_env or env_prefix" "env_prefix"
),
):
Sanic("test", config=1, load_env=1)
with pytest.raises(
SanicException,
match=(
"When instantiating Sanic with config, you cannot also pass "
"load_env or env_prefix"
), ),
): ):
Sanic("test", config=1, env_prefix=1) Sanic("test", config=1, env_prefix=1)
@ -448,6 +473,98 @@ def test_custom_context():
assert app.ctx == ctx assert app.ctx == ctx
def test_uvloop_config(app, monkeypatch):
@app.get("/test")
def handler(request):
return text("ok")
try_use_uvloop = Mock()
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
# Default config
app.test_client.get("/test")
if OS_IS_WINDOWS:
try_use_uvloop.assert_not_called()
else:
try_use_uvloop.assert_called_once()
try_use_uvloop.reset_mock()
app.config["USE_UVLOOP"] = False
app.test_client.get("/test")
try_use_uvloop.assert_not_called()
try_use_uvloop.reset_mock()
app.config["USE_UVLOOP"] = True
app.test_client.get("/test")
try_use_uvloop.assert_called_once()
def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch):
apps = (Sanic("default-uvloop"), Sanic("no-uvloop"), Sanic("yes-uvloop"))
apps[1].config.USE_UVLOOP = False
apps[2].config.USE_UVLOOP = True
try_use_uvloop = Mock()
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
loop = asyncio.get_event_loop()
with caplog.at_level(logging.WARNING):
for app in apps:
srv_coro = app.create_server(
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
loop.run_until_complete(srv_coro)
try_use_uvloop.assert_not_called() # Check it didn't try to change policy
message = (
"You are trying to change the uvloop configuration, but "
"this is only effective when using the run(...) method. "
"When using the create_server(...) method Sanic will use "
"the already existing loop."
)
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
modified = sum(1 for app in apps if app.config.USE_UVLOOP is not _default)
assert counter[(logging.WARNING, message)] == modified
def test_multiple_uvloop_configs_display_warning(caplog):
Sanic._uvloop_setting = None # Reset the setting (changed in prev tests)
default_uvloop = Sanic("default-uvloop")
no_uvloop = Sanic("no-uvloop")
yes_uvloop = Sanic("yes-uvloop")
no_uvloop.config.USE_UVLOOP = False
yes_uvloop.config.USE_UVLOOP = True
loop = asyncio.get_event_loop()
with caplog.at_level(logging.WARNING):
for app in (default_uvloop, no_uvloop, yes_uvloop):
srv_coro = app.create_server(
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(srv_coro)
loop.run_until_complete(srv.startup())
message = (
"It looks like you're running several apps with different "
"uvloop settings. This is not supported and may lead to "
"unintended behaviour."
)
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
assert counter[(logging.WARNING, message)] == 2
def test_cannot_run_fast_and_workers(app): def test_cannot_run_fast_and_workers(app):
message = "You cannot use both fast=True and workers=X" message = "You cannot use both fast=True and workers=X"
with pytest.raises(RuntimeError, match=message): with pytest.raises(RuntimeError, match=message):

View File

@ -145,6 +145,37 @@ def test_listeners_triggered_async(app):
assert after_server_stop assert after_server_stop
def test_non_default_uvloop_config_raises_warning(app):
app.config.USE_UVLOOP = True
class CustomServer(uvicorn.Server):
def install_signal_handlers(self):
pass
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
server = CustomServer(config=config)
with pytest.warns(UserWarning) as records:
server.run()
all_tasks = asyncio.all_tasks(asyncio.get_event_loop())
for task in all_tasks:
task.cancel()
msg = ""
for record in records:
_msg = str(record.message)
if _msg.startswith("You have set the USE_UVLOOP configuration"):
msg = _msg
break
assert msg == (
"You have set the USE_UVLOOP configuration option, but Sanic "
"cannot control the event loop when running in ASGI mode."
"This option will be ignored."
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mockprotocol_events(protocol): async def test_mockprotocol_events(protocol):
assert protocol._not_paused.is_set() assert protocol._not_paused.is_set()

View File

@ -1,6 +1,7 @@
import pytest import pytest
from sanic import Blueprint, Sanic from sanic import Blueprint, Sanic
from sanic.exceptions import SanicException
@pytest.fixture @pytest.fixture
@ -79,24 +80,18 @@ def test_names_okay(name):
) )
def test_names_not_okay(name): def test_names_not_okay(name):
app_message = ( app_message = (
f"Sanic instance named '{name}' uses a format that isdeprecated. " f"Sanic instance named '{name}' uses an invalid format. Names must "
"Starting in version 21.12, Sanic objects must be named only using " "begin with a character and may only contain alphanumeric "
"alphanumeric characters, _, or -." "characters, _, or -."
) )
bp_message = ( bp_message = (
f"Blueprint instance named '{name}' uses a format that isdeprecated. " f"Blueprint instance named '{name}' uses an invalid format. Names "
"Starting in version 21.12, Blueprint objects must be named only using " "must begin with a character and may only contain alphanumeric "
"alphanumeric characters, _, or -." "characters, _, or -."
) )
with pytest.warns(DeprecationWarning) as app_e: with pytest.raises(SanicException, match=app_message):
app = Sanic(name) Sanic(name)
with pytest.warns(DeprecationWarning) as bp_e: with pytest.raises(SanicException, match=bp_message):
bp = Blueprint(name) Blueprint(name)
assert app.name == name
assert bp.name == name
assert app_e[0].message.args[0] == app_message
assert bp_e[0].message.args[0] == bp_message

View File

@ -15,7 +15,6 @@ from sanic.exceptions import (
) )
from sanic.request import Request from sanic.request import Request
from sanic.response import json, text from sanic.response import json, text
from sanic.views import CompositionView
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@ -833,7 +832,7 @@ def test_static_blueprint_name(static_file_directory, file_name):
@pytest.mark.parametrize("file_name", ["test.file"]) @pytest.mark.parametrize("file_name", ["test.file"])
def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
current_file = inspect.getfile(inspect.currentframe()) current_file = inspect.getfile(inspect.currentframe()) # type: ignore
with open(current_file, "rb") as file: with open(current_file, "rb") as file:
file.read() file.read()
@ -862,31 +861,6 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
assert triggered is True assert triggered is True
def test_route_handler_add(app: Sanic):
view = CompositionView()
async def get_handler(request):
return json({"response": "OK"})
view.add(["GET"], get_handler, stream=False)
async def default_handler(request):
return text("OK")
bp = Blueprint(name="handler", url_prefix="/handler")
bp.add_route(default_handler, uri="/default/", strict_slashes=True)
bp.add_route(view, uri="/view", name="test")
app.blueprint(bp)
_, response = app.test_client.get("/handler/default/")
assert response.text == "OK"
_, response = app.test_client.get("/handler/view")
assert response.json["response"] == "OK"
def test_websocket_route(app: Sanic): def test_websocket_route(app: Sanic):
event = asyncio.Event() event = asyncio.Event()
@ -1079,15 +1053,12 @@ def test_blueprint_registered_multiple_apps():
def test_bp_set_attribute_warning(): def test_bp_set_attribute_warning():
bp = Blueprint("bp") bp = Blueprint("bp")
with pytest.warns(DeprecationWarning) as record: message = (
bp.foo = 1 "Setting variables on Blueprint instances is not allowed. You should "
"change your Blueprint instance to use instance.ctx.foo instead."
assert len(record) == 1
assert record[0].message.args[0] == (
"Setting variables on Blueprint instances is deprecated "
"and will be removed in version 21.12. You should change your "
"Blueprint instance to use instance.ctx.foo instead."
) )
with pytest.raises(AttributeError, match=message):
bp.foo = 1
def test_early_registration(app): def test_early_registration(app):

View File

@ -32,6 +32,12 @@ def starting_line(lines):
return 0 return 0
def read_app_info(lines):
for line in lines:
if line.startswith(b"{") and line.endswith(b"}"):
return json.loads(line)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"appname", "appname",
( (
@ -199,9 +205,7 @@ def test_debug(cmd):
command = ["sanic", "fake.server.app", cmd] command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines)
app_info = lines[starting_line(lines) + 9]
info = json.loads(app_info)
assert info["debug"] is True assert info["debug"] is True
assert info["auto_reload"] is True assert info["auto_reload"] is True
@ -212,9 +216,7 @@ def test_auto_reload(cmd):
command = ["sanic", "fake.server.app", cmd] command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines)
app_info = lines[starting_line(lines) + 9]
info = json.loads(app_info)
assert info["debug"] is False assert info["debug"] is False
assert info["auto_reload"] is True assert info["auto_reload"] is True
@ -227,9 +229,7 @@ def test_access_logs(cmd, expected):
command = ["sanic", "fake.server.app", cmd] command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines)
app_info = lines[starting_line(lines) + 8]
info = json.loads(app_info)
assert info["access_log"] is expected assert info["access_log"] is expected
@ -254,8 +254,6 @@ def test_noisy_exceptions(cmd, expected):
command = ["sanic", "fake.server.app", cmd] command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)
lines = out.split(b"\n") lines = out.split(b"\n")
info = read_app_info(lines)
app_info = lines[starting_line(lines) + 8]
info = json.loads(app_info)
assert info["noisy_exceptions"] is expected assert info["noisy_exceptions"] is expected

View File

@ -1,3 +1,5 @@
import logging
from contextlib import contextmanager from contextlib import contextmanager
from os import environ from os import environ
from pathlib import Path from pathlib import Path
@ -7,6 +9,8 @@ from unittest.mock import Mock
import pytest import pytest
from pytest import MonkeyPatch
from sanic import Sanic from sanic import Sanic
from sanic.config import DEFAULT_CONFIG, Config from sanic.config import DEFAULT_CONFIG, Config
from sanic.exceptions import PyFileError from sanic.exceptions import PyFileError
@ -32,21 +36,26 @@ class ConfigTest:
return self.not_for_config return self.not_for_config
def test_load_from_object(app): class UltimateAnswer:
def __init__(self, answer):
self.answer = int(answer)
def test_load_from_object(app: Sanic):
app.config.load(ConfigTest) app.config.load(ConfigTest)
assert "CONFIG_VALUE" in app.config assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used" assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config assert "not_for_config" not in app.config
def test_load_from_object_string(app): def test_load_from_object_string(app: Sanic):
app.config.load("test_config.ConfigTest") app.config.load("test_config.ConfigTest")
assert "CONFIG_VALUE" in app.config assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used" assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config assert "not_for_config" not in app.config
def test_load_from_instance(app): def test_load_from_instance(app: Sanic):
app.config.load(ConfigTest()) app.config.load(ConfigTest())
assert "CONFIG_VALUE" in app.config assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used" assert app.config.CONFIG_VALUE == "should be used"
@ -55,7 +64,7 @@ def test_load_from_instance(app):
assert "another_not_for_config" not in app.config assert "another_not_for_config" not in app.config
def test_load_from_object_string_exception(app): def test_load_from_object_string_exception(app: Sanic):
with pytest.raises(ImportError): with pytest.raises(ImportError):
app.config.load("test_config.Config.test") app.config.load("test_config.Config.test")
@ -74,26 +83,6 @@ def test_auto_bool_env_prefix():
del environ["SANIC_TEST_ANSWER"] del environ["SANIC_TEST_ANSWER"]
def test_dont_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(name=__name__, load_env=False)
assert getattr(app.config, "TEST_ANSWER", None) is None
del environ["SANIC_TEST_ANSWER"]
@pytest.mark.parametrize("load_env", [None, False, "", "MYAPP_"])
def test_load_env_deprecation(load_env):
with pytest.warns(DeprecationWarning, match=r"21\.12"):
_ = Sanic(name=__name__, load_env=load_env)
def test_load_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(name=__name__, load_env="MYAPP_")
assert app.config.TEST_ANSWER == 42
del environ["MYAPP_TEST_ANSWER"]
@pytest.mark.parametrize("env_prefix", [None, ""]) @pytest.mark.parametrize("env_prefix", [None, ""])
def test_empty_load_env_prefix(env_prefix): def test_empty_load_env_prefix(env_prefix):
environ["SANIC_TEST_ANSWER"] = "42" environ["SANIC_TEST_ANSWER"] = "42"
@ -102,20 +91,6 @@ def test_empty_load_env_prefix(env_prefix):
del environ["SANIC_TEST_ANSWER"] del environ["SANIC_TEST_ANSWER"]
def test_load_env_prefix_float_values():
environ["MYAPP_TEST_ROI"] = "2.3"
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(name=__name__, load_env="MYAPP_")
assert app.config.TEST_TOKEN == "somerandomtesttoken"
del environ["MYAPP_TEST_TOKEN"]
def test_env_prefix(): def test_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42" environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(name=__name__, env_prefix="MYAPP_") app = Sanic(name=__name__, env_prefix="MYAPP_")
@ -137,7 +112,45 @@ def test_env_prefix_string_value():
del environ["MYAPP_TEST_TOKEN"] del environ["MYAPP_TEST_TOKEN"]
def test_load_from_file(app): def test_env_w_custom_converter():
environ["SANIC_TEST_ANSWER"] = "42"
config = Config(converters=[UltimateAnswer])
app = Sanic(name=__name__, config=config)
assert isinstance(app.config.TEST_ANSWER, UltimateAnswer)
assert app.config.TEST_ANSWER.answer == 42
del environ["SANIC_TEST_ANSWER"]
def test_env_lowercase():
with pytest.warns(None) as record:
environ["SANIC_test_answer"] = "42"
app = Sanic(name=__name__)
assert app.config.test_answer == 42
assert str(record[0].message) == (
"[DEPRECATION v22.9] Lowercase environment variables will not be "
"loaded into Sanic config beginning in v22.9."
)
del environ["SANIC_test_answer"]
def test_add_converter_multiple_times(caplog):
def converter():
...
message = (
"Configuration value converter 'converter' has already been registered"
)
config = Config()
config.register_type(converter)
with caplog.at_level(logging.WARNING):
config.register_type(converter)
assert ("sanic.error", logging.WARNING, message) in caplog.record_tuples
assert len(config._converters) == 5
def test_load_from_file(app: Sanic):
config = dedent( config = dedent(
""" """
VALUE = 'some value' VALUE = 'some value'
@ -156,12 +169,12 @@ def test_load_from_file(app):
assert "condition" not in app.config assert "condition" not in app.config
def test_load_from_missing_file(app): def test_load_from_missing_file(app: Sanic):
with pytest.raises(IOError): with pytest.raises(IOError):
app.config.load("non-existent file") app.config.load("non-existent file")
def test_load_from_envvar(app): def test_load_from_envvar(app: Sanic):
config = "VALUE = 'some value'" config = "VALUE = 'some value'"
with temp_path() as config_path: with temp_path() as config_path:
config_path.write_text(config) config_path.write_text(config)
@ -171,7 +184,7 @@ def test_load_from_envvar(app):
assert app.config.VALUE == "some value" assert app.config.VALUE == "some value"
def test_load_from_missing_envvar(app): def test_load_from_missing_envvar(app: Sanic):
with pytest.raises(IOError) as e: with pytest.raises(IOError) as e:
app.config.load("non-existent variable") app.config.load("non-existent variable")
assert str(e.value) == ( assert str(e.value) == (
@ -181,7 +194,7 @@ def test_load_from_missing_envvar(app):
) )
def test_load_config_from_file_invalid_syntax(app): def test_load_config_from_file_invalid_syntax(app: Sanic):
config = "VALUE = some value" config = "VALUE = some value"
with temp_path() as config_path: with temp_path() as config_path:
config_path.write_text(config) config_path.write_text(config)
@ -190,7 +203,7 @@ def test_load_config_from_file_invalid_syntax(app):
app.config.load(config_path) app.config.load(config_path)
def test_overwrite_exisiting_config(app): def test_overwrite_exisiting_config(app: Sanic):
app.config.DEFAULT = 1 app.config.DEFAULT = 1
class Config: class Config:
@ -200,7 +213,7 @@ def test_overwrite_exisiting_config(app):
assert app.config.DEFAULT == 2 assert app.config.DEFAULT == 2
def test_overwrite_exisiting_config_ignore_lowercase(app): def test_overwrite_exisiting_config_ignore_lowercase(app: Sanic):
app.config.default = 1 app.config.default = 1
class Config: class Config:
@ -210,7 +223,7 @@ def test_overwrite_exisiting_config_ignore_lowercase(app):
assert app.config.default == 1 assert app.config.default == 1
def test_missing_config(app): def test_missing_config(app: Sanic):
with pytest.raises(AttributeError, match="Config has no 'NON_EXISTENT'"): with pytest.raises(AttributeError, match="Config has no 'NON_EXISTENT'"):
_ = app.config.NON_EXISTENT _ = app.config.NON_EXISTENT
@ -278,7 +291,7 @@ def test_config_custom_defaults_with_env():
del environ[key] del environ[key]
def test_config_access_log_passing_in_run(app): def test_config_access_log_passing_in_run(app: Sanic):
assert app.config.ACCESS_LOG is True assert app.config.ACCESS_LOG is True
@app.listener("after_server_start") @app.listener("after_server_start")
@ -293,7 +306,7 @@ def test_config_access_log_passing_in_run(app):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_config_access_log_passing_in_create_server(app): async def test_config_access_log_passing_in_create_server(app: Sanic):
assert app.config.ACCESS_LOG is True assert app.config.ACCESS_LOG is True
@app.listener("after_server_start") @app.listener("after_server_start")
@ -342,18 +355,18 @@ _test_setting_as_module = str(
], ],
ids=["from_dict", "from_class", "from_file"], ids=["from_dict", "from_class", "from_file"],
) )
def test_update(app, conf_object): def test_update(app: Sanic, conf_object):
app.update_config(conf_object) app.update_config(conf_object)
assert app.config["TEST_SETTING_VALUE"] == 1 assert app.config["TEST_SETTING_VALUE"] == 1
def test_update_from_lowercase_key(app): def test_update_from_lowercase_key(app: Sanic):
d = {"test_setting_value": 1} d = {"test_setting_value": 1}
app.update_config(d) app.update_config(d)
assert "test_setting_value" not in app.config assert "test_setting_value" not in app.config
def test_deprecation_notice_when_setting_logo(app): def test_deprecation_notice_when_setting_logo(app: Sanic):
message = ( message = (
"Setting the config.LOGO is deprecated and will no longer be " "Setting the config.LOGO is deprecated and will no longer be "
"supported starting in v22.6." "supported starting in v22.6."
@ -362,7 +375,7 @@ def test_deprecation_notice_when_setting_logo(app):
app.config.LOGO = "My Custom Logo" app.config.LOGO = "My Custom Logo"
def test_config_set_methods(app, monkeypatch): def test_config_set_methods(app: Sanic, monkeypatch: MonkeyPatch):
post_set = Mock() post_set = Mock()
monkeypatch.setattr(Config, "_post_set", post_set) monkeypatch.setattr(Config, "_post_set", post_set)

View File

@ -1,7 +1,11 @@
import asyncio import asyncio
import sys
from threading import Event from threading import Event
import pytest
from sanic.exceptions import SanicException
from sanic.response import text from sanic.response import text
@ -48,3 +52,41 @@ def test_create_task_with_app_arg(app):
_, response = app.test_client.get("/") _, response = app.test_client.get("/")
assert response.text == "test_create_task_with_app_arg" assert response.text == "test_create_task_with_app_arg"
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
def test_create_named_task(app):
async def dummy():
...
@app.before_server_start
async def setup(app, _):
app.add_task(dummy, name="dummy_task")
@app.after_server_start
async def stop(app, _):
task = app.get_task("dummy_task")
assert app._task_registry
assert isinstance(task, asyncio.Task)
assert task.get_name() == "dummy_task"
app.stop()
app.run()
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
def test_create_named_task_fails_outside_app(app):
async def dummy():
...
message = "Cannot name task outside of a running application"
with pytest.raises(RuntimeError, match=message):
app.add_task(dummy, name="dummy_task")
assert not app._task_registry
message = 'Registered task named "dummy_task" not found.'
with pytest.raises(SanicException, match=message):
app.get_task("dummy_task")

View File

@ -0,0 +1,9 @@
import pytest
from sanic.log import deprecation
def test_deprecation():
message = r"\[DEPRECATION v9\.9\] hello"
with pytest.warns(DeprecationWarning, match=message):
deprecation("hello", 9.9)

View File

@ -280,40 +280,20 @@ def test_allow_fallback_error_format_set_main_process_start(app):
async def start(app, _): async def start(app, _):
app.config.FALLBACK_ERROR_FORMAT = "text" app.config.FALLBACK_ERROR_FORMAT = "text"
request, response = app.test_client.get("/error") _, response = app.test_client.get("/error")
assert request.app.error_handler.fallback == "text"
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
def test_setting_fallback_to_non_default_raise_warning(app): def test_setting_fallback_on_config_changes_as_expected(app):
app.error_handler = ErrorHandler(fallback="text") app.error_handler = ErrorHandler()
assert app.error_handler.fallback == "text" _, response = app.test_client.get("/error")
assert response.content_type == "text/html; charset=utf-8"
with pytest.warns(
UserWarning,
match=(
"Overriding non-default ErrorHandler fallback value. "
"Changing from text to auto."
),
):
app.config.FALLBACK_ERROR_FORMAT = "auto"
assert app.error_handler.fallback == "auto"
app.config.FALLBACK_ERROR_FORMAT = "text" app.config.FALLBACK_ERROR_FORMAT = "text"
_, response = app.test_client.get("/error")
with pytest.warns( assert response.content_type == "text/plain; charset=utf-8"
UserWarning,
match=(
"Overriding non-default ErrorHandler fallback value. "
"Changing from text to json."
),
):
app.config.FALLBACK_ERROR_FORMAT = "json"
assert app.error_handler.fallback == "json"
def test_allow_fallback_error_format_in_config_injection(): def test_allow_fallback_error_format_in_config_injection():
@ -327,7 +307,6 @@ def test_allow_fallback_error_format_in_config_injection():
raise Exception("something went wrong") raise Exception("something went wrong")
request, response = app.test_client.get("/error") request, response = app.test_client.get("/error")
assert request.app.error_handler.fallback == "text"
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
@ -339,6 +318,23 @@ def test_allow_fallback_error_format_in_config_replacement(app):
app.config = MyConfig() app.config = MyConfig()
request, response = app.test_client.get("/error") request, response = app.test_client.get("/error")
assert request.app.error_handler.fallback == "text"
assert response.status == 500 assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8" assert response.content_type == "text/plain; charset=utf-8"
def test_config_fallback_before_and_after_startup(app):
app.config.FALLBACK_ERROR_FORMAT = "json"
@app.main_process_start
async def start(app, _):
app.config.FALLBACK_ERROR_FORMAT = "text"
_, response = app.test_client.get("/error")
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
def test_config_fallback_bad_value(app):
message = "Unknown format: fake"
with pytest.raises(SanicException, match=message):
app.config.FALLBACK_ERROR_FORMAT = "fake"

View File

@ -13,7 +13,6 @@ from sanic.exceptions import (
SanicException, SanicException,
ServerError, ServerError,
Unauthorized, Unauthorized,
abort,
) )
from sanic.response import text from sanic.response import text
@ -88,10 +87,6 @@ def exception_app():
def handler_500_error(request): def handler_500_error(request):
raise SanicException(status_code=500) raise SanicException(status_code=500)
@app.route("/old_abort")
def handler_old_abort_error(request):
abort(500)
@app.route("/abort/message") @app.route("/abort/message")
def handler_abort_message(request): def handler_abort_message(request):
raise SanicException(message="Custom Message", status_code=500) raise SanicException(message="Custom Message", status_code=500)
@ -239,11 +234,6 @@ def test_sanic_exception(exception_app):
assert response.status == 500 assert response.status == 500
assert "Custom Message" in response.text assert "Custom Message" in response.text
with warnings.catch_warnings(record=True) as w:
request, response = exception_app.test_client.get("/old_abort")
assert response.status == 500
assert len(w) == 1 and "deprecated" in w[0].message.args[0]
def test_custom_exception_default_message(exception_app): def test_custom_exception_default_message(exception_app):
class TeaError(SanicException): class TeaError(SanicException):
@ -262,7 +252,7 @@ def test_custom_exception_default_message(exception_app):
def test_exception_in_ws_logged(caplog): def test_exception_in_ws_logged(caplog):
app = Sanic(__file__) app = Sanic(__name__)
@app.websocket("/feed") @app.websocket("/feed")
async def feed(request, ws): async def feed(request, ws):
@ -271,9 +261,13 @@ def test_exception_in_ws_logged(caplog):
with caplog.at_level(logging.INFO): with caplog.at_level(logging.INFO):
app.test_client.websocket("/feed") app.test_client.websocket("/feed")
error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"] for record in caplog.record_tuples:
assert error_logs[1][1] == logging.ERROR if record[2].startswith("Exception occurred"):
assert "Exception occurred while handling uri:" in error_logs[1][2] break
assert record[0] == "sanic.error"
assert record[1] == logging.ERROR
assert "Exception occurred while handling uri:" in record[2]
@pytest.mark.parametrize("debug", (True, False)) @pytest.mark.parametrize("debug", (True, False))

View File

@ -226,11 +226,12 @@ def test_single_arg_exception_handler_notice(
exception_handler_app.error_handler = CustomErrorHandler() exception_handler_app.error_handler = CustomErrorHandler()
message = ( message = (
"You are using a deprecated error handler. The lookup method should " "[DEPRECATION v22.3] You are using a deprecated error handler. The "
"accept two positional parameters: (exception, route_name: " "lookup method should accept two positional parameters: (exception, "
"Optional[str]). Until you upgrade your ErrorHandler.lookup, " "route_name: Optional[str]). Until you upgrade your "
"Blueprint specific exceptions will not work properly. Beginning in " "ErrorHandler.lookup, Blueprint specific exceptions will not work "
"v22.3, the legacy style lookup method will not work at all." "properly. Beginning in v22.3, the legacy style lookup method will "
"not work at all."
) )
with pytest.warns(DeprecationWarning) as record: with pytest.warns(DeprecationWarning) as record:
_, response = exception_handler_app.test_client.get("/1") _, response = exception_handler_app.test_client.get("/1")

View File

@ -0,0 +1,84 @@
import sys
from unittest.mock import MagicMock
import pytest
from sanic import Sanic
try:
import sanic_ext
SANIC_EXT_IN_ENV = True
except ImportError:
SANIC_EXT_IN_ENV = False
@pytest.fixture
def stoppable_app(app):
@app.before_server_start
async def stop(*_):
app.stop()
return app
def test_ext_is_loaded(stoppable_app: Sanic, sanic_ext):
stoppable_app.run()
sanic_ext.Extend.assert_called_once_with(stoppable_app)
def test_ext_is_not_loaded(stoppable_app: Sanic, sanic_ext):
stoppable_app.config.AUTO_EXTEND = False
stoppable_app.run()
sanic_ext.Extend.assert_not_called()
def test_extend_with_args(stoppable_app: Sanic, sanic_ext):
stoppable_app.extend(built_in_extensions=False)
stoppable_app.run()
sanic_ext.Extend.assert_called_once_with(
stoppable_app, built_in_extensions=False, config=None, extensions=None
)
def test_access_object_sets_up_extension(app: Sanic, sanic_ext):
app.ext
sanic_ext.Extend.assert_called_once_with(app)
def test_extend_cannot_be_called_multiple_times(app: Sanic, sanic_ext):
app.extend()
message = "Cannot extend Sanic after Sanic Extensions has been setup."
with pytest.raises(RuntimeError, match=message):
app.extend()
sanic_ext.Extend.assert_called_once_with(
app, extensions=None, built_in_extensions=True, config=None
)
@pytest.mark.skipif(
SANIC_EXT_IN_ENV,
reason="Running tests with sanic_ext already in the environment",
)
def test_fail_if_not_loaded(app: Sanic):
del sys.modules["sanic_ext"]
with pytest.raises(
RuntimeError, match="Sanic Extensions is not installed.*"
):
app.extend(built_in_extensions=False)
def test_can_access_app_ext_while_running(app: Sanic, sanic_ext, ext_instance):
class IceCream:
flavor: str
@app.before_server_start
async def injections(*_):
app.ext.injection(IceCream)
app.stop()
app.run()
ext_instance.injection.assert_called_with(IceCream)

View File

@ -2,7 +2,6 @@ import asyncio
import logging import logging
import time import time
from collections import Counter
from multiprocessing import Process from multiprocessing import Process
import httpx import httpx
@ -36,11 +35,14 @@ def test_no_exceptions_when_cancel_pending_request(app, caplog):
p.kill() p.kill()
counter = Counter([r[1] for r in caplog.record_tuples]) info = 0
for record in caplog.record_tuples:
assert counter[logging.INFO] == 11 assert record[1] != logging.ERROR
assert logging.ERROR not in counter if record[1] == logging.INFO:
assert ( info += 1
caplog.record_tuples[9][2] if record[2].startswith("Request:"):
== "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed." assert record[2] == (
) "Request: GET http://127.0.0.1:8000/ stopped. "
"Transport is closed."
)
assert info == 11

View File

@ -45,7 +45,7 @@ def default_back_to_ujson():
def test_change_encoder(): def test_change_encoder():
Sanic("...", dumps=sdumps) Sanic("Test", dumps=sdumps)
assert BaseHTTPResponse._dumps == sdumps assert BaseHTTPResponse._dumps == sdumps
@ -53,7 +53,7 @@ def test_change_encoder_to_some_custom():
def my_custom_encoder(): def my_custom_encoder():
return "foo" return "foo"
Sanic("...", dumps=my_custom_encoder) Sanic("Test", dumps=my_custom_encoder)
assert BaseHTTPResponse._dumps == my_custom_encoder assert BaseHTTPResponse._dumps == my_custom_encoder
@ -68,7 +68,7 @@ def test_json_response_ujson(payload):
): ):
json(payload, dumps=sdumps) json(payload, dumps=sdumps)
Sanic("...", dumps=sdumps) Sanic("Test", dumps=sdumps)
with pytest.raises( with pytest.raises(
TypeError, match="Object of type Foo is not JSON serializable" TypeError, match="Object of type Foo is not JSON serializable"
): ):
@ -87,6 +87,6 @@ def test_json_response_json():
response = json(too_big_for_ujson, dumps=sdumps) response = json(too_big_for_ujson, dumps=sdumps)
assert sys.getsizeof(response.body) == 54 assert sys.getsizeof(response.body) == 54
Sanic("...", dumps=sdumps) Sanic("Test", dumps=sdumps)
response = json(too_big_for_ujson) response = json(too_big_for_ujson)
assert sys.getsizeof(response.body) == 54 assert sys.getsizeof(response.body) == 54

View File

@ -2,6 +2,7 @@ import asyncio
import platform import platform
from asyncio import sleep as aio_sleep from asyncio import sleep as aio_sleep
from itertools import count
from os import environ from os import environ
import pytest import pytest
@ -15,7 +16,12 @@ from sanic.response import text
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port PORT = 42001 # test_keep_alive_timeout_reuse doesn't work with random port
port_counter = count()
def get_port():
return next(port_counter) + PORT
keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse") keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse")
@ -63,9 +69,10 @@ def test_keep_alive_timeout_reuse():
"""If the server keep-alive timeout and client keep-alive timeout are """If the server keep-alive timeout and client keep-alive timeout are
both longer than the delay, the client _and_ server will successfully both longer than the delay, the client _and_ server will successfully
reuse the existing connection.""" reuse the existing connection."""
port = get_port()
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
client = ReusableClient(keep_alive_timeout_app_reuse, loop=loop, port=PORT) client = ReusableClient(keep_alive_timeout_app_reuse, loop=loop, port=port)
with client: with client:
headers = {"Connection": "keep-alive"} headers = {"Connection": "keep-alive"}
request, response = client.get("/1", headers=headers) request, response = client.get("/1", headers=headers)
@ -90,10 +97,11 @@ def test_keep_alive_timeout_reuse():
def test_keep_alive_client_timeout(): def test_keep_alive_client_timeout():
"""If the server keep-alive timeout is longer than the client """If the server keep-alive timeout is longer than the client
keep-alive timeout, client will try to create a new connection here.""" keep-alive timeout, client will try to create a new connection here."""
port = get_port()
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
client = ReusableClient( client = ReusableClient(
keep_alive_app_client_timeout, loop=loop, port=PORT keep_alive_app_client_timeout, loop=loop, port=port
) )
with client: with client:
headers = {"Connection": "keep-alive"} headers = {"Connection": "keep-alive"}
@ -117,10 +125,11 @@ def test_keep_alive_server_timeout():
keep-alive timeout, the client will either a 'Connection reset' error keep-alive timeout, the client will either a 'Connection reset' error
_or_ a new connection. Depending on how the event-loop handles the _or_ a new connection. Depending on how the event-loop handles the
broken server connection.""" broken server connection."""
port = get_port()
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
client = ReusableClient( client = ReusableClient(
keep_alive_app_server_timeout, loop=loop, port=PORT keep_alive_app_server_timeout, loop=loop, port=port
) )
with client: with client:
headers = {"Connection": "keep-alive"} headers = {"Connection": "keep-alive"}
@ -141,9 +150,10 @@ def test_keep_alive_server_timeout():
reason="Not testable with current client", reason="Not testable with current client",
) )
def test_keep_alive_connection_context(): def test_keep_alive_connection_context():
port = get_port()
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
client = ReusableClient(keep_alive_app_context, loop=loop, port=PORT) client = ReusableClient(keep_alive_app_context, loop=loop, port=port)
with client: with client:
headers = {"Connection": "keep-alive"} headers = {"Connection": "keep-alive"}
request1, _ = client.post("/ctx", headers=headers) request1, _ = client.post("/ctx", headers=headers)

View File

@ -107,7 +107,7 @@ argv = dict(
"-m", "-m",
"sanic", "sanic",
"--port", "--port",
"42104", "42204",
"--debug", "--debug",
"reloader.app", "reloader.app",
], ],
@ -117,11 +117,12 @@ argv = dict(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"runargs, mode", "runargs, mode",
[ [
(dict(port=42102, auto_reload=True), "script"), (dict(port=42202, auto_reload=True), "script"),
(dict(port=42103, debug=True), "module"), (dict(port=42203, debug=True), "module"),
({}, "sanic"), ({}, "sanic"),
], ],
) )
@pytest.mark.xfail
async def test_reloader_live(runargs, mode): async def test_reloader_live(runargs, mode):
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
filename = os.path.join(tmpdir, "reloader.py") filename = os.path.join(tmpdir, "reloader.py")
@ -149,11 +150,12 @@ async def test_reloader_live(runargs, mode):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"runargs, mode", "runargs, mode",
[ [
(dict(port=42102, auto_reload=True), "script"), (dict(port=42302, auto_reload=True), "script"),
(dict(port=42103, debug=True), "module"), (dict(port=42303, debug=True), "module"),
({}, "sanic"), ({}, "sanic"),
], ],
) )
@pytest.mark.xfail
async def test_reloader_live_with_dir(runargs, mode): async def test_reloader_live_with_dir(runargs, mode):
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
filename = os.path.join(tmpdir, "reloader.py") filename = os.path.join(tmpdir, "reloader.py")

View File

@ -8,13 +8,13 @@ from sanic.response import stream, text
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_request_cancel_when_connection_lost(app): async def test_request_cancel_when_connection_lost(app):
app.still_serving_cancelled_request = False app.ctx.still_serving_cancelled_request = False
@app.get("/") @app.get("/")
async def handler(request): async def handler(request):
await asyncio.sleep(1.0) await asyncio.sleep(1.0)
# at this point client is already disconnected # at this point client is already disconnected
app.still_serving_cancelled_request = True app.ctx.still_serving_cancelled_request = True
return text("OK") return text("OK")
# schedule client call # schedule client call
@ -32,12 +32,12 @@ async def test_request_cancel_when_connection_lost(app):
# Wait for server and check if it's still serving the cancelled request # Wait for server and check if it's still serving the cancelled request
await asyncio.sleep(1.0) await asyncio.sleep(1.0)
assert app.still_serving_cancelled_request is False assert app.ctx.still_serving_cancelled_request is False
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_stream_request_cancel_when_conn_lost(app): async def test_stream_request_cancel_when_conn_lost(app):
app.still_serving_cancelled_request = False app.ctx.still_serving_cancelled_request = False
@app.post("/post/<id>", stream=True) @app.post("/post/<id>", stream=True)
async def post(request, id): async def post(request, id):
@ -52,7 +52,7 @@ async def test_stream_request_cancel_when_conn_lost(app):
await asyncio.sleep(1.0) await asyncio.sleep(1.0)
# at this point client is already disconnected # at this point client is already disconnected
app.still_serving_cancelled_request = True app.ctx.still_serving_cancelled_request = True
return stream(streaming) return stream(streaming)
@ -71,4 +71,4 @@ async def test_stream_request_cancel_when_conn_lost(app):
# Wait for server and check if it's still serving the cancelled request # Wait for server and check if it's still serving the cancelled request
await asyncio.sleep(1.0) await asyncio.sleep(1.0)
assert app.still_serving_cancelled_request is False assert app.ctx.still_serving_cancelled_request is False

View File

@ -68,11 +68,11 @@ def test_app_injection(app):
@app.listener("after_server_start") @app.listener("after_server_start")
async def inject_data(app, loop): async def inject_data(app, loop):
app.injected = expected app.ctx.injected = expected
@app.get("/") @app.get("/")
async def handler(request): async def handler(request):
return json({"injected": request.app.injected}) return json({"injected": request.app.ctx.injected})
request, response = app.test_client.get("/") request, response = app.test_client.get("/")

View File

@ -8,7 +8,7 @@ import pytest
from sanic import Sanic from sanic import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.response import json, text from sanic.response import json, text
from sanic.views import CompositionView, HTTPMethodView from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator from sanic.views import stream as stream_decorator
@ -423,33 +423,6 @@ def test_request_stream_blueprint(app):
assert response.text == data assert response.text == data
def test_request_stream_composition_view(app):
def get_handler(request):
return text("OK")
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)
view = CompositionView()
view.add(["GET"], get_handler)
view.add(["POST"], post_handler, stream=True)
app.add_route(view, "/composition_view")
request, response = app.test_client.get("/composition_view")
assert response.status == 200
assert response.text == "OK"
request, response = app.test_client.post("/composition_view", data=data)
assert response.status == 200
assert response.text == data
def test_request_stream(app): def test_request_stream(app):
"""test for complex application""" """test for complex application"""
bp = Blueprint("test_blueprint_request_stream") bp = Blueprint("test_blueprint_request_stream")
@ -510,14 +483,8 @@ def test_request_stream(app):
app.add_route(SimpleView.as_view(), "/method_view") app.add_route(SimpleView.as_view(), "/method_view")
view = CompositionView()
view.add(["GET"], get_handler)
view.add(["POST"], post_handler, stream=True)
app.blueprint(bp) app.blueprint(bp)
app.add_route(view, "/composition_view")
request, response = app.test_client.get("/method_view") request, response = app.test_client.get("/method_view")
assert response.status == 200 assert response.status == 200
assert response.text == "OK" assert response.text == "OK"
@ -526,14 +493,6 @@ def test_request_stream(app):
assert response.status == 200 assert response.status == 200
assert response.text == data assert response.text == data
request, response = app.test_client.get("/composition_view")
assert response.status == 200
assert response.text == "OK"
request, response = app.test_client.post("/composition_view", data=data)
assert response.status == 200
assert response.text == data
request, response = app.test_client.get("/get") request, response = app.test_client.get("/get")
assert response.status == 200 assert response.status == 200
assert response.text == "OK" assert response.text == "OK"

View File

@ -15,6 +15,8 @@ from aiofiles import os as async_os
from pytest import LogCaptureFixture from pytest import LogCaptureFixture
from sanic import Request, Sanic from sanic import Request, Sanic
from sanic.compat import Header
from sanic.cookies import CookieJar
from sanic.response import ( from sanic.response import (
HTTPResponse, HTTPResponse,
empty, empty,
@ -277,7 +279,7 @@ def test_non_chunked_streaming_returns_correct_content(
assert response.text == "foo,bar" assert response.text == "foo,bar"
def test_stream_response_with_cookies(app): def test_stream_response_with_cookies_legacy(app):
@app.route("/") @app.route("/")
async def test(request: Request): async def test(request: Request):
response = stream(sample_streaming_fn, content_type="text/csv") response = stream(sample_streaming_fn, content_type="text/csv")
@ -289,6 +291,25 @@ def test_stream_response_with_cookies(app):
assert response.cookies["test"] == "pass" assert response.cookies["test"] == "pass"
def test_stream_response_with_cookies(app):
@app.route("/")
async def test(request: Request):
headers = Header()
cookies = CookieJar(headers)
cookies["test"] = "modified"
cookies["test"] = "pass"
response = await request.respond(
content_type="text/csv", headers=headers
)
await response.send("foo,")
await asyncio.sleep(0.001)
await response.send("bar")
request, response = app.test_client.get("/")
assert response.cookies["test"] == "pass"
def test_stream_response_without_cookies(app): def test_stream_response_without_cookies(app):
@app.route("/") @app.route("/")
async def test(request: Request): async def test(request: Request):
@ -561,37 +582,37 @@ def test_multiple_responses(
message_in_records: Callable[[List[LogRecord], str], bool], message_in_records: Callable[[List[LogRecord], str], bool],
): ):
@app.route("/1") @app.route("/1")
async def handler(request: Request): async def handler1(request: Request):
response = await request.respond() response = await request.respond()
await response.send("foo") await response.send("foo")
response = await request.respond() response = await request.respond()
@app.route("/2") @app.route("/2")
async def handler(request: Request): async def handler2(request: Request):
response = await request.respond() response = await request.respond()
response = await request.respond() response = await request.respond()
await response.send("foo") await response.send("foo")
@app.get("/3") @app.get("/3")
async def handler(request: Request): async def handler3(request: Request):
response = await request.respond() response = await request.respond()
await response.send("foo,") await response.send("foo,")
response = await request.respond() response = await request.respond()
await response.send("bar") await response.send("bar")
@app.get("/4") @app.get("/4")
async def handler(request: Request): async def handler4(request: Request):
response = await request.respond(headers={"one": "one"}) response = await request.respond(headers={"one": "one"})
return json({"foo": "bar"}, headers={"one": "two"}) return json({"foo": "bar"}, headers={"one": "two"})
@app.get("/5") @app.get("/5")
async def handler(request: Request): async def handler5(request: Request):
response = await request.respond(headers={"one": "one"}) response = await request.respond(headers={"one": "one"})
await response.send("foo") await response.send("foo")
return json({"foo": "bar"}, headers={"one": "two"}) return json({"foo": "bar"}, headers={"one": "two"})
@app.get("/6") @app.get("/6")
async def handler(request: Request): async def handler6(request: Request):
response = await request.respond(headers={"one": "one"}) response = await request.respond(headers={"one": "one"})
await response.send("foo, ") await response.send("foo, ")
json_response = json({"foo": "bar"}, headers={"one": "two"}) json_response = json({"foo": "bar"}, headers={"one": "two"})

View File

@ -16,7 +16,7 @@ from sanic import Blueprint, Sanic
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.exceptions import NotFound, SanicException from sanic.exceptions import NotFound, SanicException
from sanic.request import Request from sanic.request import Request
from sanic.response import json, text from sanic.response import empty, json, text
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1230,3 +1230,41 @@ def test_routes_with_and_without_slash_definitions(app):
_, response = app.test_client.post(f"/{term}/") _, response = app.test_client.post(f"/{term}/")
assert response.status == 200 assert response.status == 200
assert response.text == f"{term}_with" assert response.text == f"{term}_with"
def test_added_route_ctx_kwargs(app):
@app.route("/", ctx_foo="foo", ctx_bar=99)
async def handler(request: Request):
return empty()
request, _ = app.test_client.get("/")
assert request.route.ctx.foo == "foo"
assert request.route.ctx.bar == 99
def test_added_bad_route_kwargs(app):
message = "Unexpected keyword arguments: foo, bar"
with pytest.raises(TypeError, match=message):
@app.route("/", foo="foo", bar=99)
async def handler(request: Request):
...
@pytest.mark.asyncio
async def test_added_callable_route_ctx_kwargs(app):
def foo(*args, **kwargs):
return "foo"
async def bar(*args, **kwargs):
return 99
@app.route("/", ctx_foo=foo, ctx_bar=bar)
async def handler(request: Request):
return empty()
request, _ = await app.asgi_client.get("/")
assert request.route.ctx.foo() == "foo"
assert await request.route.ctx.bar() == 99

View File

@ -101,7 +101,7 @@ async def test_trigger_before_events_create_server(app):
@app.listener("before_server_start") @app.listener("before_server_start")
async def init_db(app, loop): async def init_db(app, loop):
app.db = MySanicDb() app.ctx.db = MySanicDb()
srv = await app.create_server( srv = await app.create_server(
debug=True, return_asyncio_server=True, port=PORT debug=True, return_asyncio_server=True, port=PORT
@ -109,8 +109,8 @@ async def test_trigger_before_events_create_server(app):
await srv.startup() await srv.startup()
await srv.before_start() await srv.before_start()
assert hasattr(app, "db") assert hasattr(app.ctx, "db")
assert isinstance(app.db, MySanicDb) assert isinstance(app.ctx.db, MySanicDb)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -122,9 +122,9 @@ async def test_trigger_before_events_create_server_missing_event(app):
@app.listener @app.listener
async def init_db(app, loop): async def init_db(app, loop):
app.db = MySanicDb() app.ctx.db = MySanicDb()
assert not hasattr(app, "db") assert not hasattr(app.ctx, "db")
def test_create_server_trigger_events(app): def test_create_server_trigger_events(app):

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