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
tags:
- "!*" # Do not execute on tags
paths:
- sanic/*
- tests/*
pull_request:
paths:
- "!*.MD"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
test:

View File

@ -1,12 +1,12 @@
.. 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
--------------
Bugfixes
********
**Bugfixes**
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
Update sanic-routing to allow for better splitting of complex URI templates
@ -20,8 +20,7 @@ Bugfixes
Version 21.6.0
--------------
Features
********
**Features**
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
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>`_
Additional methods for attaching ``HTTPMethodView``
Bugfixes
********
**Bugfixes**
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
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
Deprecations and Removals
*************************
**Deprecations and Removals**
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
@ -95,14 +92,12 @@ Deprecations and Removals
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
Deprecate StreamingHTTPResponse
Developer infrastructure
************************
**Developer infrastructure**
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
Remove Travis CI in favor of GitHub Actions
Improved Documentation
**********************
**Improved Documentation**
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
Fix typo in documentation
@ -112,8 +107,7 @@ Improved Documentation
Version 21.3.2
--------------
Bugfixes
********
**Bugfixes**
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
Disable response timeout on websocket connections
@ -124,8 +118,7 @@ Bugfixes
Version 21.3.1
--------------
Bugfixes
********
**Bugfixes**
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
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>`_
Features
********
**Features**
*
`#1876 <https://github.com/sanic-org/sanic/pull/1876>`_
@ -189,8 +181,7 @@ Features
`#2063 <https://github.com/sanic-org/sanic/pull/2063>`_
App and connection level context objects
Bugfixes and issues resolved
****************************
**Bugfixes**
* Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_
``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>`_
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>`_
@ -240,8 +230,7 @@ Deprecations and Removals
* ``Request.endpoint`` deprecated in favor of ``Request.name``
* handler type name prefixes removed (static, websocket, etc)
Developer infrastructure
************************
**Developer infrastructure**
*
`#1995 <https://github.com/sanic-org/sanic/pull/1995>`_
@ -259,8 +248,7 @@ Developer infrastructure
`#2049 <https://github.com/sanic-org/sanic/pull/2049>`_
Updated setup.py to use ``find_packages``
Improved Documentation
**********************
**Improved Documentation**
*
`#1218 <https://github.com/sanic-org/sanic/pull/1218>`_
@ -282,8 +270,7 @@ Improved Documentation
`#2052 <https://github.com/sanic-org/sanic/pull/2052>`_
Fix some examples and docs
Miscellaneous
*************
**Miscellaneous**
* ``Request.route`` property
* Better websocket subprotocols support
@ -329,8 +316,7 @@ Miscellaneous
Version 20.12.3
---------------
Bugfixes
********
**Bugfixes**
*
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
@ -339,8 +325,7 @@ Bugfixes
Version 20.12.2
---------------
Dependencies
************
**Dependencies**
*
`#2026 <https://github.com/sanic-org/sanic/pull/2026>`_
@ -353,8 +338,7 @@ Dependencies
Version 19.12.5
---------------
Dependencies
************
**Dependencies**
*
`#2025 <https://github.com/sanic-org/sanic/pull/2025>`_
@ -367,8 +351,7 @@ Dependencies
Version 20.12.0
---------------
Features
********
**Features**
*
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
@ -377,8 +360,7 @@ Features
Version 20.12.0
---------------
Features
********
**Features**
*
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
@ -416,22 +398,19 @@ Features
`#1979 <https://github.com/sanic-org/sanic/pull/1979>`_
Add app registry and Sanic class level app retrieval
Bugfixes
********
**Bugfixes**
*
`#1965 <https://github.com/sanic-org/sanic/pull/1965>`_
Fix Chunked Transport-Encoding in ASGI streaming response
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#1981 <https://github.com/sanic-org/sanic/pull/1981>`_
Cleanup and remove deprecated code
Developer infrastructure
************************
**Developer infrastructure**
*
`#1956 <https://github.com/sanic-org/sanic/pull/1956>`_
@ -445,8 +424,7 @@ Developer infrastructure
`#1986 <https://github.com/sanic-org/sanic/pull/1986>`_
Update tox requirements
Improved Documentation
**********************
**Improved Documentation**
*
`#1951 <https://github.com/sanic-org/sanic/pull/1951>`_
@ -464,8 +442,7 @@ Improved Documentation
Version 20.9.1
---------------
Bugfixes
********
**Bugfixes**
*
`#1954 <https://github.com/sanic-org/sanic/pull/1954>`_
@ -478,8 +455,7 @@ Bugfixes
Version 19.12.3
---------------
Bugfixes
********
**Bugfixes**
*
`#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>`_
@ -518,22 +493,19 @@ Features
`#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)
Bugfixes
********
**Bugfixes**
*
`#1897 <https://github.com/sanic-org/sanic/pull/1897>`_
Resolves exception from unread bytes in stream
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#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
Developer infrastructure
************************
**Developer infrastructure**
*
`#1890 <https://github.com/sanic-org/sanic/pull/1890>`_,
@ -548,8 +520,7 @@ Developer infrastructure
`#1924 <https://github.com/sanic-org/sanic/pull/1924>`_
Adding --strict-markers for pytest
Improved Documentation
**********************
**Improved Documentation**
*
`#1922 <https://github.com/sanic-org/sanic/pull/1922>`_
@ -559,8 +530,7 @@ Improved Documentation
Version 20.6.3
---------------
Bugfixes
********
**Bugfixes**
*
`#1884 <https://github.com/sanic-org/sanic/pull/1884>`_
@ -570,8 +540,7 @@ Bugfixes
Version 20.6.2
---------------
Features
********
**Features**
*
`#1641 <https://github.com/sanic-org/sanic/pull/1641>`_
@ -581,8 +550,7 @@ Features
Version 20.6.1
---------------
Features
********
**Features**
*
`#1760 <https://github.com/sanic-org/sanic/pull/1760>`_
@ -596,8 +564,7 @@ Features
`#1880 <https://github.com/sanic-org/sanic/pull/1880>`_
Add handler names for websockets for url_for usage
Bugfixes
********
**Bugfixes**
*
`#1776 <https://github.com/sanic-org/sanic/pull/1776>`_
@ -619,15 +586,13 @@ Bugfixes
`#1853 <https://github.com/sanic-org/sanic/pull/1853>`_
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>`_
Deprecate body_bytes to merge into body
Developer infrastructure
************************
**Developer infrastructure**
*
`#1852 <https://github.com/sanic-org/sanic/pull/1852>`_
@ -642,8 +607,7 @@ Developer infrastructure
Wrap run()'s "protocol" type annotation in Optional[]
Improved Documentation
**********************
**Improved Documentation**
*
`#1846 <https://github.com/sanic-org/sanic/pull/1846>`_
@ -663,8 +627,7 @@ Version 20.6.0
Version 20.3.0
---------------
Features
********
**Features**
*
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
@ -695,8 +658,7 @@ Features
`#1820 <https://github.com/sanic-org/sanic/pull/1820>`_
Do not set content-type and content-length headers in exceptions
Bugfixes
********
**Bugfixes**
*
`#1748 <https://github.com/sanic-org/sanic/pull/1748>`_
@ -714,8 +676,7 @@ Bugfixes
`#1808 <https://github.com/sanic-org/sanic/pull/1808>`_
Fix Ctrl+C and tests on Windows
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#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>`_
Complete deprecation of ``app.remove_route`` and ``request.raw_args``
Dependencies
************
**Dependencies**
*
`#1794 <https://github.com/sanic-org/sanic/pull/1794>`_
@ -744,15 +704,13 @@ Dependencies
`#1806 <https://github.com/sanic-org/sanic/pull/1806>`_
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
Developer infrastructure
************************
**Developer infrastructure**
*
`#1833 <https://github.com/sanic-org/sanic/pull/1833>`_
Resolve broken documentation builds
Improved Documentation
**********************
**Improved Documentation**
*
`#1755 <https://github.com/sanic-org/sanic/pull/1755>`_
@ -794,8 +752,7 @@ Improved Documentation
Version 19.12.0
---------------
Bugfixes
********
**Bugfixes**
- 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>`__)
Improved Documentation
**********************
**Improved Documentation**
- Move docs from MD to RST
@ -829,8 +785,7 @@ Improved Documentation
Version 19.6.3
--------------
Features
********
**Features**
- 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>`__)
Improved Documentation
**********************
**Improved Documentation**
- Documentation infrastructure changes
@ -852,8 +806,7 @@ Improved Documentation
Version 19.6.2
--------------
Features
********
**Features**
*
`#1562 <https://github.com/sanic-org/sanic/pull/1562>`_
@ -869,8 +822,7 @@ Features
Add Configure support from object string
Bugfixes
********
**Bugfixes**
*
`#1587 <https://github.com/sanic-org/sanic/pull/1587>`_
@ -888,8 +840,7 @@ Bugfixes
`#1594 <https://github.com/sanic-org/sanic/pull/1594>`_
Strict Slashes behavior fix
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#1544 <https://github.com/sanic-org/sanic/pull/1544>`_
@ -913,8 +864,7 @@ Deprecations and Removals
Version 19.3
------------
Features
********
**Features**
*
`#1497 <https://github.com/sanic-org/sanic/pull/1497>`_
@ -982,8 +932,7 @@ Features
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
gunicorn.
Developer infrastructure
************************
**Developer infrastructure**
* `#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)
@ -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
* `#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
* `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example
@ -1096,15 +1043,13 @@ Version 18.12
Version 0.8
-----------
0.8.3
*****
**0.8.3**
* Changes:
* Ownership changed to org 'sanic-org'
0.8.0
*****
**0.8.0**
* Changes:
@ -1184,19 +1129,16 @@ Version 0.1
-----------
0.1.7
*****
**0.1.7**
* Reversed static url and directory arguments to meet spec
0.1.6
*****
**0.1.6**
* Static files
* Lazy Cookie Loading
0.1.5
*****
**0.1.5**
* Cookies
* Blueprint listeners and ordering
@ -1204,23 +1146,19 @@ Version 0.1
* Fix: Incomplete file reads on medium+ sized post requests
* Breaking: after_start and before_stop now pass sanic as their first argument
0.1.4
*****
**0.1.4**
* Multiprocessing
0.1.3
*****
**0.1.3**
* Blueprint support
* Faster Response processing
0.1.1 - 0.1.2
*************
**0.1.1 - 0.1.2**
* Struggling to update pypi via CI
0.1.0
*****
**0.1.0**
* Released to public

View File

@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
:stub-columns: 1
* - Build
- | |Py39Test| |Py38Test| |Py37Test|
- | |Py310Test| |Py39Test| |Py38Test| |Py37Test|
* - Docs
- | |UserGuide| |Documentation|
* - Package
@ -27,6 +27,8 @@ Sanic | Build fast. Run fast.
:target: https://community.sanicframework.org/
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
: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
: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
@ -75,7 +77,11 @@ The goal of the project is to provide a simple way to get up and running a highl
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
------------
@ -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>`_.
.. |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
:members:
:show-inheritance:
sanic.websocket
---------------
.. automodule:: sanic.websocket
:members:
:show-inheritance:

View File

@ -1,6 +1,6 @@
📜 Changelog
============
.. mdinclude:: ./releases/21.9.md
.. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md
.. 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
- [#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
app = Sanic(__name__)
app = Sanic("Example")
async def notify_server_started_after_five_seconds():

View File

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

View File

@ -6,7 +6,7 @@ from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
app = Sanic("Example")
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
"""
app = Sanic(__name__)
app = Sanic("Example")
bp = Blueprint("bp_" + __name__)
bp = Blueprint("bp_example")
@bp.on_request

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
app = Sanic("Example")
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

View File

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

View File

@ -2,27 +2,29 @@
Modify header or status in response
"""
from sanic import Sanic
from sanic import response
app = Sanic(__name__)
from sanic import Sanic, response
@app.route('/')
app = Sanic("Example")
@app.route("/")
def handle_request(request):
return response.json(
{'message': 'Hello world!'},
headers={'X-Served-By': 'sanic'},
status=200
{"message": "Hello world!"},
headers={"X-Served-By": "sanic"},
status=200,
)
@app.route('/unauthorized')
@app.route("/unauthorized")
def handle_request(request):
return response.json(
{'message': 'You are not authorized'},
headers={'X-Served-By': 'sanic'},
status=404
{"message": "You are not authorized"},
headers={"X-Served-By": "sanic"},
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")
def app():
app = Sanic()
app = Sanic("Example")
@app.route("/")
async def index(request):

View File

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

View File

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

View File

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

View File

@ -1,21 +1,23 @@
import asyncio
from sanic import Sanic
from sanic import response
from sanic import Sanic, response
from sanic.config import Config
from sanic.exceptions import RequestTimeout
Config.REQUEST_TIMEOUT = 1
app = Sanic(__name__)
app = Sanic("Example")
@app.route('/')
@app.route("/")
async def test(request):
await asyncio.sleep(3)
return response.text('Hello, world!')
return response.text("Hello, world!")
@app.exception(RequestTimeout)
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
from sanic.handlers import ErrorHandler
from sanic import Sanic
from sanic.exceptions import SanicException
from os import getenv
from sanic.handlers import ErrorHandler
rollbar.init(getenv("ROLLBAR_API_KEY"))
class RollbarExceptionHandler(ErrorHandler):
def default(self, request, exception):
rollbar.report_message(str(exception))
return super().default(request, exception)
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
app = Sanic("Example", error_handler=RollbarExceptionHandler())
@app.route("/raise")
@ -24,7 +25,4 @@ def create_error(request):
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)
app.run(host="0.0.0.0", port=getenv("PORT", 8080))

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,14 @@
from sanic import Sanic
from sanic import response as res
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/")
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)

View File

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

View File

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

View File

@ -1,7 +1,7 @@
from sanic import Sanic, response
app = Sanic(__name__)
app = Sanic("Example")
@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/answer
app = Sanic(__name__)
app = Sanic("Example")
bp = Blueprint("bp", host="bp.example.com")

View File

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

View File

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

View File

@ -6,4 +6,4 @@ python:
path: .
extra_requirements:
- 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
import asyncio
import logging
import logging.config
import os
@ -11,6 +12,7 @@ from asyncio import (
AbstractEventLoop,
CancelledError,
Protocol,
Task,
ensure_future,
get_event_loop,
wait_for,
@ -26,6 +28,7 @@ from ssl import SSLContext
from traceback import format_exc
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Awaitable,
@ -40,10 +43,11 @@ from typing import (
Set,
Tuple,
Type,
TypeVar,
Union,
)
from urllib.parse import urlencode, urlunparse
from warnings import filterwarnings, warn
from warnings import filterwarnings
from sanic_routing.exceptions import ( # type: ignore
FinalizationError,
@ -52,11 +56,12 @@ from sanic_routing.exceptions import ( # type: ignore
from sanic_routing.route import Route # type: ignore
from sanic import reloader_helpers
from sanic.application.ext import setup_ext
from sanic.application.logo import get_logo
from sanic.application.motd import MOTD
from sanic.application.state import ApplicationState, Mode
from sanic.asgi import ASGIApp
from sanic.base import BaseSanic
from sanic.base.root import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
@ -68,9 +73,16 @@ from sanic.exceptions import (
URLBuildError,
)
from sanic.handlers import ErrorHandler
from sanic.helpers import _default
from sanic.http import Stage
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.models.futures import (
FutureException,
@ -84,11 +96,11 @@ from sanic.models.futures import (
from sanic.models.handler_types import ListenerType, MiddlewareType
from sanic.models.handler_types import Sanic as SanicVar
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.server import AsyncioServer, HttpProtocol
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.websockets.impl import ConnectionClosed
from sanic.signals import Signal, SignalRouter
@ -96,11 +108,21 @@ from sanic.tls import process_to_context
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:
enable_windows_color_support()
filterwarnings("once", category=DeprecationWarning)
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
class Sanic(BaseSanic, metaclass=TouchUpMeta):
"""
@ -113,12 +135,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_run_response_middleware",
"_run_request_middleware",
)
__fake_slots__ = (
"_app_registry",
__slots__ = (
"_asgi_app",
"_asgi_client",
"_blueprint_order",
"_delayed_tasks",
"_ext",
"_future_exceptions",
"_future_listeners",
"_future_middleware",
@ -127,20 +149,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_future_signals",
"_future_statics",
"_state",
"_task_registry",
"_test_client",
"_test_manager",
"asgi",
"auto_reload",
"auto_reload",
"blueprints",
"config",
"configure_logging",
"ctx",
"debug",
"error_handler",
"go_fast",
"is_running",
"is_stopping",
"listeners",
"name",
"named_request_middleware",
@ -152,12 +169,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"signal_router",
"sock",
"strict_slashes",
"test_mode",
"websocket_enabled",
"websocket_tasks",
)
_app_registry: Dict[str, "Sanic"] = {}
_uvloop_setting = None # TODO: Remove in v22.6
test_mode = False
def __init__(
@ -168,7 +185,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
router: Optional[Router] = None,
signal_router: Optional[SignalRouter] = None,
error_handler: Optional[ErrorHandler] = None,
load_env: Union[bool, str] = True,
env_prefix: Optional[str] = SANIC_PREFIX,
request_class: Optional[Type[Request]] = None,
strict_slashes: bool = False,
@ -184,25 +200,27 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
dict_config = log_config or LOGGING_CONFIG_DEFAULTS
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(
"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._test_client: Any = None
self._test_manager: Any = None
self._blueprint_order: List[Blueprint] = []
self._delayed_tasks: List[str] = []
self._future_registry: FutureRegistry = FutureRegistry()
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.config: Config = config or Config(
load_env=load_env,
env_prefix=env_prefix,
app=self,
)
self.configure_logging: bool = configure_logging
self.ctx: Any = ctx or SimpleNamespace()
self.debug = False
@ -224,6 +242,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
self.go_fast = self.run
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
if self.config.REGISTER:
self.__class__.register_app(self)
@ -254,32 +278,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
# 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(
self, listener: ListenerType[SanicVar], event: str
) -> ListenerType[SanicVar]:
@ -404,12 +402,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
websocket_handler.is_websocket = True # type: ignore
params["handler"] = websocket_handler
ctx = params.pop("route_context")
routes = self.router.add(**params)
if isinstance(routes, Route):
routes = [routes]
for r in routes:
r.ctx.websocket = websocket
r.ctx.static = params.get("static", False)
r.ctx.__dict__.update(ctx)
return routes
@ -755,7 +757,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
exception, request.name if request else None
)
if handler:
warn(
deprecation(
"An error occurred while handling the request after at "
"least some part of the response was sent to the client. "
"Therefore, the response from your custom exception "
@ -770,7 +772,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"For further information, please see the docs: "
"https://sanicframework.org/en/guide/advanced/"
"signals.html",
DeprecationWarning,
22.6,
)
try:
response = self.error_handler.response(request, exception)
@ -823,6 +825,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
else:
if request.stream:
response = request.stream.response
# Marked for cleanup and DRY with handle_request/handle_exception
# when ResponseStream is no longer supporder
if isinstance(response, BaseHTTPResponse):
await self.dispatch(
"http.lifecycle.response",
@ -833,6 +838,17 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
},
)
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:
raise ServerError(
f"Invalid response type {response!r} (need HTTPResponse)"
@ -936,7 +952,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
elif not hasattr(handler, "is_websocket"):
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):
await self.dispatch(
"http.lifecycle.response",
@ -947,6 +964,17 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
},
)
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:
if not hasattr(handler, "is_websocket"):
raise ServerError(
@ -1162,6 +1190,11 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
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:
self.is_running = True
self.is_stopping = False
@ -1189,6 +1222,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
This kills the Sanic
"""
if not self.is_stopping:
self.shutdown_tasks(timeout=0)
self.is_stopping = True
get_event_loop().stop()
@ -1258,12 +1292,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log
if noisy_exceptions is not None:
self.config.NOISY_EXCEPTIONS = noisy_exceptions
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
server_settings = self._helper(
host=host,
@ -1278,6 +1313,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
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_stop = server_settings.pop("main_stop", None)
if main_start or main_stop:
@ -1399,27 +1442,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
if isinstance(version, int):
version = HTTP(version)
ssl = process_to_context(ssl)
self.debug = debug
self.state.host = host
self.state.port = port
self.state.workers = workers
# Serve
serve_location = ""
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)
self.state.ssl = ssl
self.state.unix = unix
self.state.sock = sock
server_settings = {
"protocol": protocol,
@ -1436,7 +1467,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"version": version,
}
self.motd(serve_location)
self.motd(self.serve_location)
if sys.stdout.isatty() and not self.state.is_debug:
error_logger.warning(
@ -1462,12 +1493,55 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
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):
parts = [self.name, *parts]
return ".".join(parts)
@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):
try:
task = task(app)
@ -1477,14 +1551,22 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
return task
@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)
loop.create_task(prepped)
task = loop.create_task(prepped, name=name)
@classmethod
def _cancel_websocket_tasks(cls, app, loop):
for task in app.websocket_tasks:
task.cancel()
if name and register:
app._task_registry[name] = task
return task
@staticmethod
async def dispatch_delayed_tasks(app, loop):
@ -1497,13 +1579,132 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
prepped = app._prep_task(task, app, loop)
await prepped
@staticmethod
async def _listener(
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
def add_task(
self,
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 maybe_coro and isawaitable(maybe_coro):
await maybe_coro
if sys.version_info == (3, 7):
raise RuntimeError(
"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
@ -1621,11 +1822,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
display["auto-reload"] = reload_display
packages = []
for package_name, module_name in {
"sanic-routing": "sanic_routing",
"sanic-testing": "sanic_testing",
"sanic-ext": "sanic_ext",
}.items():
for package_name in SANIC_PACKAGES:
module_name = package_name.replace("-", "_")
try:
module = import_module(module_name)
packages.append(f"{package_name}=={module.__version__}")
@ -1645,6 +1843,41 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
)
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
# -------------------------------------------------------------------- #
@ -1706,13 +1939,35 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
async def _startup(self):
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.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)
self.state.is_started = True
async def _server_event(
self,
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):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
def display(self):
if self.logo:
logger.debug(self.logo)

View File

@ -5,7 +5,9 @@ import logging
from dataclasses import dataclass, field
from enum import Enum, auto
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
@ -37,11 +39,15 @@ class ApplicationState:
coffee: bool = field(default=False)
fast: bool = field(default=False)
host: str = field(default="")
mode: Mode = field(default=Mode.PRODUCTION)
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)
server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False)
is_started: bool = field(default=False)
is_stopping: bool = field(default=False)
verbosity: 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.exceptions import ServerError
from sanic.helpers import _default
from sanic.http import Stage
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
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", "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:
"""
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
from typing import Any, Tuple
from warnings import warn
from typing import Any
from sanic.base.meta import SanicMeta
from sanic.exceptions import SanicException
from sanic.mixins.exceptions import ExceptionMixin
from sanic.mixins.listeners import ListenerMixin
@ -11,7 +11,7 @@ from sanic.mixins.routes import RouteMixin
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(
@ -20,8 +20,9 @@ class BaseSanic(
ListenerMixin,
ExceptionMixin,
SignalMixin,
metaclass=SanicMeta,
):
__fake_slots__: Tuple[str, ...]
__slots__ = ("name",)
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
class_name = self.__class__.__name__
@ -33,11 +34,10 @@ class BaseSanic(
)
if not VALID_NAME.match(name):
warn(
f"{class_name} instance named '{name}' uses a format that is"
f"deprecated. Starting in version 21.12, {class_name} objects "
"must be named only using alphanumeric characters, _, or -.",
DeprecationWarning,
raise SanicException(
f"{class_name} instance named '{name}' uses an invalid "
"format. Names must begin with a character and may only "
"contain alphanumeric characters, _, or -."
)
self.name = name
@ -52,15 +52,12 @@ class BaseSanic(
return f'{self.__class__.__name__}(name="{self.name}")'
def __setattr__(self, name: str, value: Any) -> None:
# This is a temporary compat layer so we can raise a warning until
# setting attributes on the app instance can be removed and deprecated
# with a proper implementation of __slots__
if name not in self.__fake_slots__:
warn(
try:
super().__setattr__(name, value)
except AttributeError as e:
raise AttributeError(
f"Setting variables on {self.__class__.__name__} instances is "
"deprecated and will be removed in version 21.12. You should "
f"change your {self.__class__.__name__} instance to use "
"not allowed. You should change your "
f"{self.__class__.__name__} instance to use "
f"instance.ctx.{name} instead.",
DeprecationWarning,
)
super().__setattr__(name, value)
) from e

View File

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

View File

@ -1,28 +1,26 @@
from __future__ import annotations
from inspect import isclass
from inspect import getmembers, isclass, isdatadescriptor
from os import environ
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from warnings import warn
from typing import Any, Callable, Dict, Optional, Sequence, Union
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.log import deprecation, error_logger
from sanic.utils import load_module_from_file_location, str_to_bool
if TYPE_CHECKING: # no cov
from sanic import Sanic
SANIC_PREFIX = "SANIC_"
DEFAULT_CONFIG = {
"_FALLBACK_ERROR_FORMAT": _default,
"ACCESS_LOG": True,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False,
"FALLBACK_ERROR_FORMAT": "auto",
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FORWARDED_SECRET": None,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
@ -40,17 +38,31 @@ DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 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
AUTO_EXTEND: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool
FALLBACK_ERROR_FORMAT: str
FORWARDED_FOR_HEADER: str
FORWARDED_SECRET: Optional[str]
GRACEFUL_SHUTDOWN_TIMEOUT: float
@ -69,6 +81,7 @@ class Config(dict):
REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int
SERVER_NAME: str
USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: int
@ -76,33 +89,27 @@ class Config(dict):
def __init__(
self,
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
load_env: Optional[Union[bool, str]] = True,
env_prefix: Optional[str] = SANIC_PREFIX,
keep_alive: Optional[bool] = None,
*,
app: Optional[Sanic] = None,
converters: Optional[Sequence[Callable[[str], Any]]] = None,
):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self._app = app
self._converters = [str, str_to_bool, float, int]
self._LOGO = ""
if converters:
for converter in converters:
self.register_type(converter)
if keep_alive is not None:
self.KEEP_ALIVE = keep_alive
if env_prefix != SANIC_PREFIX:
if 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:
self.load_environment_vars(SANIC_PREFIX)
@ -117,6 +124,13 @@ class Config(dict):
raise AttributeError(f"Config has no '{ke.args[0]}'")
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})
def __setitem__(self, attr, value) -> None:
@ -136,32 +150,37 @@ class Config(dict):
"REQUEST_MAX_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":
self._LOGO = value
warn(
deprecation(
"Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.",
DeprecationWarning,
22.6,
)
@property
def app(self):
return self._app
@property
def LOGO(self):
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):
Http.set_header_max_size(
self.REQUEST_MAX_HEADER_SIZE,
@ -169,8 +188,8 @@ class Config(dict):
self.REQUEST_MAX_SIZE,
)
def _check_error_format(self):
check_error_format(self.FALLBACK_ERROR_FORMAT)
def _check_error_format(self, format: Optional[str] = None):
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
def load_environment_vars(self, prefix=SANIC_PREFIX):
"""
@ -184,20 +203,45 @@ class Config(dict):
- ``float``
- ``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():
if not key.startswith(prefix):
continue
if not key.isupper():
lower_case_var_found = True
_, config_key = key.split(prefix, 1)
for converter in (int, float, str_to_bool, str):
for converter in reversed(self._converters):
try:
self[config_key] = converter(value)
break
except ValueError:
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]):
"""
@ -267,3 +311,17 @@ class Config(dict):
self.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
DEFAULT_FORMAT = "auto"
FALLBACK_TEXT = (
"The server encountered an internal error and "
"cannot complete your request."

View File

@ -244,25 +244,3 @@ class InvalidSignal(SanicException):
class WebsocketClosed(SanicException):
quiet = True
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 typing import Dict, List, Optional, Tuple, Type
from warnings import warn
from __future__ import annotations
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 (
ContentRangeError,
HeaderNotFound,
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.response import text
@ -28,24 +37,91 @@ class ErrorHandler:
# Beginning in v22.3, the base renderer will be TextRenderer
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.cached_handlers: Dict[
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
] = {}
self.debug = False
self.fallback = fallback
self._fallback = fallback
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
def finalize(cls, error_handler, fallback: Optional[str] = None):
if (
fallback
and fallback != "auto"
and error_handler.fallback == "auto"
):
error_handler.fallback = fallback
def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
if error_handler._fallback is not _default:
if config._FALLBACK_ERROR_FORMAT is _default:
return error_handler.fallback
error_logger.warning(
"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):
error_logger.warning(
@ -54,7 +130,7 @@ class ErrorHandler:
sig = signature(error_handler.lookup)
if len(sig.parameters) == 1:
warn(
deprecation(
"You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: "
"(exception, route_name: Optional[str]). "
@ -62,9 +138,10 @@ class ErrorHandler:
"specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not "
"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):
return self.lookup(exception, route_name)
@ -188,12 +265,13 @@ class ErrorHandler:
:return:
"""
self.log(request, exception)
fallback = ErrorHandler._get_fallback_value(self, request.app.config)
return exception_response(
request,
exception,
debug=self.debug,
base=self.base,
fallback=self.fallback,
fallback=fallback,
)
@staticmethod

View File

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

View File

@ -3,6 +3,7 @@ import sys
from enum import Enum
from typing import Any, Dict
from warnings import warn
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
@ -78,3 +79,11 @@ access_logger = logging.getLogger("sanic.access")
"""
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 sanic.base.meta import SanicMeta
from sanic.models.futures import FutureException
class ExceptionMixin:
class ExceptionMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None:
self._future_exceptions: Set[FutureException] = set()

View File

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

View File

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

View File

@ -1,4 +1,5 @@
from ast import NodeVisitor, Return, parse
from contextlib import suppress
from functools import partial, wraps
from inspect import getsource, signature
from mimetypes import guess_type
@ -12,6 +13,7 @@ from urllib.parse import unquote
from sanic_routing.route import Route # type: ignore
from sanic.base.meta import SanicMeta
from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING
@ -22,19 +24,27 @@ from sanic.exceptions import (
InvalidUsage,
)
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.handler_types import RouteHandler
from sanic.response import HTTPResponse, file, file_stream
from sanic.views import CompositionView
from sanic.types import HashableDict
RouteWrapper = Callable[
[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
def __init__(self, *args, **kwargs) -> None:
@ -65,10 +75,20 @@ class RouteMixin:
static: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteWrapper:
"""
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 methods: list or tuple of methods allowed
:param host: the host, if required
@ -80,6 +100,8 @@ class RouteMixin:
body (eg. GET requests)
:param version_prefix: URL path that should be before the version
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
"""
@ -94,6 +116,8 @@ class RouteMixin:
if not methods and not websocket:
methods = frozenset({"GET"})
route_context = self._build_route_context(ctx_kwargs)
def decorator(handler):
nonlocal uri
nonlocal methods
@ -152,6 +176,7 @@ class RouteMixin:
static,
version_prefix,
error_format,
route_context,
)
self._future_routes.add(route)
@ -196,6 +221,7 @@ class RouteMixin:
stream: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteHandler:
"""A helper method to register class instance or
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 version_prefix: URL path that should be before the version
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
"""
# Handle HTTPMethodView differently
@ -226,14 +254,6 @@ class RouteMixin:
if hasattr(_handler, "is_stream"):
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:
strict_slashes = self.strict_slashes
@ -247,6 +267,7 @@ class RouteMixin:
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)(handler)
return handler
@ -261,6 +282,7 @@ class RouteMixin:
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
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 version_prefix: URL path that should be before the version
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 self.route(
@ -285,6 +309,7 @@ class RouteMixin:
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def post(
@ -297,6 +322,7 @@ class RouteMixin:
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
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 version_prefix: URL path that should be before the version
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 self.route(
@ -321,6 +349,7 @@ class RouteMixin:
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def put(
@ -333,6 +362,7 @@ class RouteMixin:
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
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 version_prefix: URL path that should be before the version
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 self.route(
@ -357,6 +389,7 @@ class RouteMixin:
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def head(
@ -369,6 +402,7 @@ class RouteMixin:
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **HEAD** *HTTP* method
@ -389,6 +423,8 @@ class RouteMixin:
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
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 self.route(
@ -401,6 +437,7 @@ class RouteMixin:
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def options(
@ -413,6 +450,7 @@ class RouteMixin:
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **OPTIONS** *HTTP* method
@ -433,6 +471,8 @@ class RouteMixin:
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
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 self.route(
@ -445,6 +485,7 @@ class RouteMixin:
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def patch(
@ -457,6 +498,7 @@ class RouteMixin:
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **PATCH** *HTTP* method
@ -479,6 +521,8 @@ class RouteMixin:
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
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 self.route(
@ -491,6 +535,7 @@ class RouteMixin:
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def delete(
@ -503,6 +548,7 @@ class RouteMixin:
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
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 version_prefix: URL path that should be before the version
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 self.route(
@ -527,6 +575,7 @@ class RouteMixin:
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def websocket(
@ -540,6 +589,7 @@ class RouteMixin:
apply: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
):
"""
Decorate a function to be registered as a websocket route
@ -553,6 +603,8 @@ class RouteMixin:
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
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 self.route(
@ -567,6 +619,7 @@ class RouteMixin:
websocket=True,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def add_websocket_route(
@ -580,6 +633,7 @@ class RouteMixin:
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
):
"""
A helper method to register a function as a websocket route.
@ -598,6 +652,8 @@ class RouteMixin:
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
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 self.websocket(
@ -609,6 +665,7 @@ class RouteMixin:
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)(handler)
def static(
@ -918,19 +975,16 @@ class RouteMixin:
return route
def _determine_error_format(self, handler) -> Optional[str]:
if not isinstance(handler, CompositionView):
try:
src = dedent(getsource(handler))
tree = parse(src)
http_response_types = self._get_response_types(tree)
def _determine_error_format(self, handler) -> str:
with suppress(OSError, TypeError):
src = dedent(getsource(handler))
tree = parse(src)
http_response_types = self._get_response_types(tree)
if len(http_response_types) == 1:
return next(iter(http_response_types))
except (OSError, TypeError):
...
if len(http_response_types) == 1:
return next(iter(http_response_types))
return None
return ""
def _get_response_types(self, node):
types = set()
@ -939,7 +993,18 @@ class RouteMixin:
def visit_Return(self, node: Return) -> Any:
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
if node.value.keywords: # type: ignore
checks += [
@ -951,9 +1016,32 @@ class RouteMixin:
for check in checks:
if check in RESPONSE_MAPPING:
types.add(RESPONSE_MAPPING[check])
except AttributeError:
...
HttpResponseVisitor().visit(node)
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 typing import Any, Callable, Dict, Optional, Set, Union
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureSignal
from sanic.models.handler_types import SignalHandler
from sanic.signals import Signal
from sanic.types import HashableDict
class HashableDict(dict):
def __hash__(self):
return hash(tuple(sorted(self.items())))
class SignalMixin:
class SignalMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None:
self._future_signals: Set[FutureSignal] = set()
@ -24,6 +21,7 @@ class SignalMixin:
*,
apply: bool = True,
condition: Dict[str, Any] = None,
exclusive: bool = True,
) -> Callable[[SignalHandler], SignalHandler]:
"""
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
:type event: str
:param apply: For lazy evaluation, defaults to True
:param apply: For lazy evaluation, defaults to ``True``
:type apply: bool, optional
: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
"""
event_value = str(event.value) if isinstance(event, Enum) else event
def decorator(handler: SignalHandler):
future_signal = FutureSignal(
handler, event_value, HashableDict(condition or {})
handler, event_value, HashableDict(condition or {}), exclusive
)
self._future_signals.add(future_signal)
@ -62,6 +65,7 @@ class SignalMixin:
handler: Optional[Callable[..., Any]],
event: str,
condition: Dict[str, Any] = None,
exclusive: bool = True,
):
if not handler:
@ -69,7 +73,9 @@ class SignalMixin:
...
handler = noop
self.signal(event=event, condition=condition)(handler)
self.signal(event=event, condition=condition, exclusive=exclusive)(
handler
)
return handler
def event(self, event: str):

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from functools import partial
from mimetypes import guess_type
from os import path
@ -12,10 +14,10 @@ from typing import (
Iterator,
Optional,
Tuple,
TypeVar,
Union,
)
from urllib.parse import quote_plus
from warnings import warn
from sanic.compat import Header, open_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
@ -28,6 +30,10 @@ from sanic.models.protocol_types import HTMLProtocol, Range
if TYPE_CHECKING:
from sanic.asgi import ASGIApp
from sanic.request import Request
else:
Request = TypeVar("Request")
try:
from ujson import dumps as json_dumps
@ -136,95 +142,6 @@ class BaseHTTPResponse:
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):
"""
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(
location: Union[str, PurePath],
status: int = 200,
@ -427,7 +447,7 @@ async def file_stream(
headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None,
_range: Optional[Range] = None,
) -> StreamingHTTPResponse:
) -> ResponseStream:
"""Return a streaming response object with file data.
:param location: Location of file on system.
@ -435,7 +455,6 @@ async def file_stream(
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param filename: Override filename.
:param chunked: Deprecated
:param _range:
"""
headers = headers or {}
@ -471,23 +490,24 @@ async def file_stream(
break
await response.write(content)
return StreamingHTTPResponse(
return ResponseStream(
streaming_fn=_streaming_fn,
status=status,
headers=headers,
content_type=mime_type,
ignore_deprecation_notice=True,
)
def stream(
streaming_fn: StreamingFunction,
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None]
],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
) -> ResponseStream:
"""Accepts a coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `ResponseStream`.
Example usage::
@ -501,42 +521,13 @@ def stream(
:param streaming_fn: A coroutine accepts a response and
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 chunked: Deprecated
"""
return StreamingHTTPResponse(
return ResponseStream(
streaming_fn,
headers=headers,
content_type=content_type,
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.server.async_server import AsyncioServer
from sanic.server.loop import try_use_uvloop
from sanic.server.protocols.http_protocol import HttpProtocol
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__ = (
"AsyncioServer",
"ConnInfo",
@ -23,4 +13,5 @@ __all__ = (
"serve",
"serve_multiple",
"serve_single",
"try_use_uvloop",
)

View File

@ -2,7 +2,14 @@ from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING
from sanic.exceptions import SanicException
from sanic.log import deprecation
if TYPE_CHECKING:
from sanic import Sanic
class AsyncioServer:
@ -11,11 +18,11 @@ class AsyncioServer:
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__(
self,
app,
app: Sanic,
loop,
serve_coro,
connections,
@ -27,13 +34,20 @@ class AsyncioServer:
self.loop = loop
self.serve_coro = serve_coro
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):
"""
Trigger "before_server_start" events
"""
self.init = True
return self.app._startup()
def before_start(self):
@ -77,30 +91,33 @@ class AsyncioServer:
return task
def start_serving(self):
if self.server:
try:
return self.server.start_serving()
except AttributeError:
raise NotImplementedError(
"server.start_serving not available in this version "
"of asyncio or uvloop."
)
return self._serve(self.server.start_serving)
def serve_forever(self):
return self._serve(self.server.serve_forever)
def _serve(self, serve_func):
if self.server:
if not self.app.state.is_started:
raise SanicException(
"Cannot run Sanic server without first running "
"await server.startup()"
)
try:
return self.server.serve_forever()
return serve_func()
except AttributeError:
name = serve_func.__name__
raise NotImplementedError(
"server.serve_forever not available in this version "
f"server.{name} not available in this version "
"of asyncio or uvloop."
)
def _server_event(self, concern: str, action: str):
if not self.init:
if not self.app.state.is_started:
raise SanicException(
"Cannot dispatch server event without "
"first running server.startup()"
"first running await server.startup()"
)
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 warnings import warn
from websockets.connection import CLOSED, CLOSING, OPEN
from websockets.server import ServerConnection
from websockets.typing import Subprotocol
from sanic.exceptions import ServerError
from sanic.log import error_logger
from sanic.log import deprecation, error_logger
from sanic.server import HttpProtocol
from ..websockets.impl import WebsocketImplProtocol
@ -17,6 +16,14 @@ if TYPE_CHECKING:
class WebSocketProtocol(HttpProtocol):
__slots__ = (
"websocket",
"websocket_timeout",
"websocket_max_size",
"websocket_ping_interval",
"websocket_ping_timeout",
)
def __init__(
self,
*args,
@ -35,24 +42,24 @@ class WebSocketProtocol(HttpProtocol):
self.websocket_max_size = websocket_max_size
if websocket_max_queue is not None and websocket_max_queue > 0:
# TODO: Reminder remove this warning in v22.3
warn(
deprecation(
"Websocket no longer uses queueing, so websocket_max_queue"
" is no longer required.",
DeprecationWarning,
22.3,
)
if websocket_read_limit is not None and websocket_read_limit > 0:
# TODO: Reminder remove this warning in v22.3
warn(
deprecation(
"Websocket no longer uses read buffers, so "
"websocket_read_limit is not required.",
DeprecationWarning,
22.3,
)
if websocket_write_limit is not None and websocket_write_limit > 0:
# TODO: Reminder remove this warning in v22.3
warn(
deprecation(
"Websocket no longer uses write buffers, so "
"websocket_write_limit is not required.",
DeprecationWarning,
22.3,
)
self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import sys
from ssl import SSLContext
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 sanic.application.ext import setup_ext
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.http.http3 import get_config, get_ticket_store
from sanic.log import error_logger, logger
@ -122,6 +125,7 @@ def serve(
**asyncio_server_kwargs,
)
setup_ext(app)
if run_async:
return AsyncioServer(
app=app,
@ -182,6 +186,9 @@ def serve(
loop.run_until_complete(asyncio.sleep(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
# graceful_shutdown_timeout
for conn in connections:

View File

@ -4,7 +4,7 @@ import asyncio
from enum import Enum
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.exceptions import NotFound # type: ignore
@ -142,12 +142,21 @@ class SignalRouter(BaseRouter):
if context:
params.update(context)
signals = group.routes
if not reverse:
handlers = handlers[::-1]
signals = signals[::-1]
try:
for handler in handlers:
if condition is None or condition == handler.__requirements__:
maybe_coroutine = handler(**params)
for signal in signals:
params.pop("__trigger__", None)
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):
retval = await maybe_coroutine
if retval:
@ -190,23 +199,36 @@ class SignalRouter(BaseRouter):
handler: SignalHandler,
event: str,
condition: Optional[Dict[str, Any]] = None,
exclusive: bool = True,
) -> Signal:
event_definition = event
parts = self._build_event_parts(event)
if parts[2].startswith("<"):
name = ".".join([*parts[:-1], "*"])
trigger = self._clean_trigger(parts[2])
else:
name = event
trigger = ""
if not trigger:
event = ".".join([*parts[:2], "<__trigger__>"])
handler.__requirements__ = condition # type: ignore
handler.__trigger__ = trigger # type: ignore
return super().add(
signal = super().add(
event,
handler,
requirements=condition,
name=name,
append=True,
) # 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):
self.add(_blank, "sanic.__signal__.__init__")
@ -238,3 +260,9 @@ class SignalRouter(BaseRouter):
"Cannot declare reserved signal event: %s" % event
)
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 .service import TouchUp
class TouchUpMeta(type):
class TouchUpMeta(SanicMeta):
def __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,
Union,
)
from warnings import warn
from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage
from sanic.models.handler_types import RouteHandler
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic import Sanic
from sanic.blueprints import Blueprint
@ -84,6 +81,8 @@ class HTTPMethodView:
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
if not handler and request.method == "HEAD":
handler = self.get
return handler(request, *args, **kwargs)
@classmethod
@ -136,48 +135,3 @@ class HTTPMethodView:
def stream(func):
func.is_stream = True
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 sanic.compat import UVLOOP_INSTALLED
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
try:
import ssl # type: ignore
except ImportError:
except ImportError: # no cov
ssl = None # type: ignore
try:
import uvloop # type: ignore
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
if UVLOOP_INSTALLED: # no cov
try_use_uvloop()
class GunicornWorker(base.Worker):

View File

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

View File

@ -6,8 +6,10 @@ import string
import sys
import uuid
from contextlib import suppress
from logging import LogRecord
from typing import Callable, List, Tuple
from typing import List, Tuple
from unittest.mock import MagicMock
import pytest
@ -184,3 +186,21 @@ def message_in_records():
return error_captured
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 logging
from sanic import Sanic, text
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["loggers"]["sanic.root"]["level"] = "DEBUG"
app = Sanic(__name__, log_config=LOGGING_CONFIG)
app = Sanic("FakeServer", log_config=LOGGING_CONFIG)
@app.get("/")

View File

@ -2,17 +2,20 @@ import asyncio
import logging
import re
from email import message
from collections import Counter
from inspect import isawaitable
from os import environ
from unittest.mock import Mock, patch
import py
import pytest
import sanic
from sanic import Sanic
from sanic.compat import OS_IS_WINDOWS
from sanic.config import Config
from sanic.exceptions import SanicException
from sanic.helpers import _default
from sanic.response import text
@ -21,15 +24,6 @@ def clear_app_registry():
Sanic._app_registry = {}
def uvloop_installed():
try:
import uvloop # noqa
return True
except ImportError:
return False
def test_app_loop_running(app):
@app.get("/test")
async def handler(request):
@ -41,41 +35,39 @@ def test_app_loop_running(app):
def test_create_asyncio_server(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
assert isawaitable(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is True
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
assert isawaitable(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is True
def test_asyncio_server_no_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43123,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43123,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
def test_asyncio_server_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43124,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
wait_close = srv.close()
loop.run_until_complete(wait_close)
# Looks like we can't easily test `serve_forever()`
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43124,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
loop.run_until_complete(srv.startup())
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
wait_close = srv.close()
loop.run_until_complete(wait_close)
# Looks like we can't easily test `serve_forever()`
def test_create_server_main(app, caplog):
@ -92,6 +84,21 @@ def test_create_server_main(app, caplog):
) 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):
app.main_process_start(lambda *_: ...)
loop = asyncio.get_event_loop()
@ -106,6 +113,19 @@ def test_create_server_main_convenience(app, caplog):
) 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):
with pytest.raises(SanicException) as excinfo:
app.loop
@ -373,6 +393,22 @@ def test_app_no_registry():
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():
environ["SANIC_REGISTER"] = "False"
Sanic("no-register")
@ -384,15 +420,12 @@ def test_app_no_registry_env():
def test_app_set_attribute_warning(app):
with pytest.warns(DeprecationWarning) as record:
app.foo = 1
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."
message = (
"Setting variables on Sanic instances is not allowed. 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):
@ -414,15 +447,7 @@ def test_bad_custom_config():
SanicException,
match=(
"When instantiating Sanic with config, you cannot also pass "
"load_env or 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"
"env_prefix"
),
):
Sanic("test", config=1, env_prefix=1)
@ -448,6 +473,98 @@ def test_custom_context():
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):
message = "You cannot use both fast=True and workers=X"
with pytest.raises(RuntimeError, match=message):

View File

@ -145,6 +145,37 @@ def test_listeners_triggered_async(app):
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
async def test_mockprotocol_events(protocol):
assert protocol._not_paused.is_set()

View File

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

View File

@ -15,7 +15,6 @@ from sanic.exceptions import (
)
from sanic.request import Request
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"])
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:
file.read()
@ -862,31 +861,6 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
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):
event = asyncio.Event()
@ -1079,15 +1053,12 @@ def test_blueprint_registered_multiple_apps():
def test_bp_set_attribute_warning():
bp = Blueprint("bp")
with pytest.warns(DeprecationWarning) as record:
bp.foo = 1
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."
message = (
"Setting variables on Blueprint instances is not allowed. 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):

View File

@ -32,6 +32,12 @@ def starting_line(lines):
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(
"appname",
(
@ -199,9 +205,7 @@ def test_debug(cmd):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[starting_line(lines) + 9]
info = json.loads(app_info)
info = read_app_info(lines)
assert info["debug"] is True
assert info["auto_reload"] is True
@ -212,9 +216,7 @@ def test_auto_reload(cmd):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[starting_line(lines) + 9]
info = json.loads(app_info)
info = read_app_info(lines)
assert info["debug"] is False
assert info["auto_reload"] is True
@ -227,9 +229,7 @@ def test_access_logs(cmd, expected):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[starting_line(lines) + 8]
info = json.loads(app_info)
info = read_app_info(lines)
assert info["access_log"] is expected
@ -254,8 +254,6 @@ def test_noisy_exceptions(cmd, expected):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[starting_line(lines) + 8]
info = json.loads(app_info)
info = read_app_info(lines)
assert info["noisy_exceptions"] is expected

View File

@ -1,3 +1,5 @@
import logging
from contextlib import contextmanager
from os import environ
from pathlib import Path
@ -7,6 +9,8 @@ from unittest.mock import Mock
import pytest
from pytest import MonkeyPatch
from sanic import Sanic
from sanic.config import DEFAULT_CONFIG, Config
from sanic.exceptions import PyFileError
@ -32,21 +36,26 @@ class ConfigTest:
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)
assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config
def test_load_from_object_string(app):
def test_load_from_object_string(app: Sanic):
app.config.load("test_config.ConfigTest")
assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config
def test_load_from_instance(app):
def test_load_from_instance(app: Sanic):
app.config.load(ConfigTest())
assert "CONFIG_VALUE" in app.config
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
def test_load_from_object_string_exception(app):
def test_load_from_object_string_exception(app: Sanic):
with pytest.raises(ImportError):
app.config.load("test_config.Config.test")
@ -74,26 +83,6 @@ def test_auto_bool_env_prefix():
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, ""])
def test_empty_load_env_prefix(env_prefix):
environ["SANIC_TEST_ANSWER"] = "42"
@ -102,20 +91,6 @@ def test_empty_load_env_prefix(env_prefix):
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():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(name=__name__, env_prefix="MYAPP_")
@ -137,7 +112,45 @@ def test_env_prefix_string_value():
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(
"""
VALUE = 'some value'
@ -156,12 +169,12 @@ def test_load_from_file(app):
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):
app.config.load("non-existent file")
def test_load_from_envvar(app):
def test_load_from_envvar(app: Sanic):
config = "VALUE = 'some value'"
with temp_path() as config_path:
config_path.write_text(config)
@ -171,7 +184,7 @@ def test_load_from_envvar(app):
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:
app.config.load("non-existent variable")
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"
with temp_path() as config_path:
config_path.write_text(config)
@ -190,7 +203,7 @@ def test_load_config_from_file_invalid_syntax(app):
app.config.load(config_path)
def test_overwrite_exisiting_config(app):
def test_overwrite_exisiting_config(app: Sanic):
app.config.DEFAULT = 1
class Config:
@ -200,7 +213,7 @@ def test_overwrite_exisiting_config(app):
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
class Config:
@ -210,7 +223,7 @@ def test_overwrite_exisiting_config_ignore_lowercase(app):
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'"):
_ = app.config.NON_EXISTENT
@ -278,7 +291,7 @@ def test_config_custom_defaults_with_env():
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
@app.listener("after_server_start")
@ -293,7 +306,7 @@ def test_config_access_log_passing_in_run(app):
@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
@app.listener("after_server_start")
@ -342,18 +355,18 @@ _test_setting_as_module = str(
],
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)
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}
app.update_config(d)
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 = (
"Setting the config.LOGO is deprecated and will no longer be "
"supported starting in v22.6."
@ -362,7 +375,7 @@ def test_deprecation_notice_when_setting_logo(app):
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()
monkeypatch.setattr(Config, "_post_set", post_set)

View File

@ -1,7 +1,11 @@
import asyncio
import sys
from threading import Event
import pytest
from sanic.exceptions import SanicException
from sanic.response import text
@ -48,3 +52,41 @@ def test_create_task_with_app_arg(app):
_, response = app.test_client.get("/")
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, _):
app.config.FALLBACK_ERROR_FORMAT = "text"
request, response = app.test_client.get("/error")
assert request.app.error_handler.fallback == "text"
_, response = app.test_client.get("/error")
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
def test_setting_fallback_to_non_default_raise_warning(app):
app.error_handler = ErrorHandler(fallback="text")
def test_setting_fallback_on_config_changes_as_expected(app):
app.error_handler = ErrorHandler()
assert app.error_handler.fallback == "text"
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"
_, response = app.test_client.get("/error")
assert response.content_type == "text/html; charset=utf-8"
app.config.FALLBACK_ERROR_FORMAT = "text"
with pytest.warns(
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"
_, response = app.test_client.get("/error")
assert response.content_type == "text/plain; charset=utf-8"
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")
request, response = app.test_client.get("/error")
assert request.app.error_handler.fallback == "text"
assert response.status == 500
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()
request, response = app.test_client.get("/error")
assert request.app.error_handler.fallback == "text"
assert response.status == 500
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,
ServerError,
Unauthorized,
abort,
)
from sanic.response import text
@ -88,10 +87,6 @@ def exception_app():
def handler_500_error(request):
raise SanicException(status_code=500)
@app.route("/old_abort")
def handler_old_abort_error(request):
abort(500)
@app.route("/abort/message")
def handler_abort_message(request):
raise SanicException(message="Custom Message", status_code=500)
@ -239,11 +234,6 @@ def test_sanic_exception(exception_app):
assert response.status == 500
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):
class TeaError(SanicException):
@ -262,7 +252,7 @@ def test_custom_exception_default_message(exception_app):
def test_exception_in_ws_logged(caplog):
app = Sanic(__file__)
app = Sanic(__name__)
@app.websocket("/feed")
async def feed(request, ws):
@ -271,9 +261,13 @@ def test_exception_in_ws_logged(caplog):
with caplog.at_level(logging.INFO):
app.test_client.websocket("/feed")
error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"]
assert error_logs[1][1] == logging.ERROR
assert "Exception occurred while handling uri:" in error_logs[1][2]
for record in caplog.record_tuples:
if record[2].startswith("Exception occurred"):
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))

View File

@ -226,11 +226,12 @@ def test_single_arg_exception_handler_notice(
exception_handler_app.error_handler = CustomErrorHandler()
message = (
"You are using a deprecated error handler. The lookup method should "
"accept two positional parameters: (exception, route_name: "
"Optional[str]). Until you upgrade your ErrorHandler.lookup, "
"Blueprint specific exceptions will not work properly. Beginning in "
"v22.3, the legacy style lookup method will not work at all."
"[DEPRECATION v22.3] You are using a deprecated error handler. The "
"lookup method should accept two positional parameters: (exception, "
"route_name: Optional[str]). Until you upgrade your "
"ErrorHandler.lookup, Blueprint specific exceptions will not work "
"properly. Beginning in v22.3, the legacy style lookup method will "
"not work at all."
)
with pytest.warns(DeprecationWarning) as record:
_, 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 time
from collections import Counter
from multiprocessing import Process
import httpx
@ -36,11 +35,14 @@ def test_no_exceptions_when_cancel_pending_request(app, caplog):
p.kill()
counter = Counter([r[1] for r in caplog.record_tuples])
assert counter[logging.INFO] == 11
assert logging.ERROR not in counter
assert (
caplog.record_tuples[9][2]
== "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed."
)
info = 0
for record in caplog.record_tuples:
assert record[1] != logging.ERROR
if record[1] == logging.INFO:
info += 1
if record[2].startswith("Request:"):
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():
Sanic("...", dumps=sdumps)
Sanic("Test", dumps=sdumps)
assert BaseHTTPResponse._dumps == sdumps
@ -53,7 +53,7 @@ def test_change_encoder_to_some_custom():
def my_custom_encoder():
return "foo"
Sanic("...", dumps=my_custom_encoder)
Sanic("Test", dumps=my_custom_encoder)
assert BaseHTTPResponse._dumps == my_custom_encoder
@ -68,7 +68,7 @@ def test_json_response_ujson(payload):
):
json(payload, dumps=sdumps)
Sanic("...", dumps=sdumps)
Sanic("Test", dumps=sdumps)
with pytest.raises(
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)
assert sys.getsizeof(response.body) == 54
Sanic("...", dumps=sdumps)
Sanic("Test", dumps=sdumps)
response = json(too_big_for_ujson)
assert sys.getsizeof(response.body) == 54

View File

@ -2,6 +2,7 @@ import asyncio
import platform
from asyncio import sleep as aio_sleep
from itertools import count
from os import environ
import pytest
@ -15,7 +16,12 @@ from sanic.response import text
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")
@ -63,9 +69,10 @@ def test_keep_alive_timeout_reuse():
"""If the server keep-alive timeout and client keep-alive timeout are
both longer than the delay, the client _and_ server will successfully
reuse the existing connection."""
port = get_port()
loop = asyncio.new_event_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:
headers = {"Connection": "keep-alive"}
request, response = client.get("/1", headers=headers)
@ -90,10 +97,11 @@ def test_keep_alive_timeout_reuse():
def test_keep_alive_client_timeout():
"""If the server keep-alive timeout is longer than the client
keep-alive timeout, client will try to create a new connection here."""
port = get_port()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReusableClient(
keep_alive_app_client_timeout, loop=loop, port=PORT
keep_alive_app_client_timeout, loop=loop, port=port
)
with client:
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
_or_ a new connection. Depending on how the event-loop handles the
broken server connection."""
port = get_port()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReusableClient(
keep_alive_app_server_timeout, loop=loop, port=PORT
keep_alive_app_server_timeout, loop=loop, port=port
)
with client:
headers = {"Connection": "keep-alive"}
@ -141,9 +150,10 @@ def test_keep_alive_server_timeout():
reason="Not testable with current client",
)
def test_keep_alive_connection_context():
port = get_port()
loop = asyncio.new_event_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:
headers = {"Connection": "keep-alive"}
request1, _ = client.post("/ctx", headers=headers)

View File

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

View File

@ -8,13 +8,13 @@ from sanic.response import stream, text
@pytest.mark.asyncio
async def test_request_cancel_when_connection_lost(app):
app.still_serving_cancelled_request = False
app.ctx.still_serving_cancelled_request = False
@app.get("/")
async def handler(request):
await asyncio.sleep(1.0)
# at this point client is already disconnected
app.still_serving_cancelled_request = True
app.ctx.still_serving_cancelled_request = True
return text("OK")
# 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
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
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)
async def post(request, id):
@ -52,7 +52,7 @@ async def test_stream_request_cancel_when_conn_lost(app):
await asyncio.sleep(1.0)
# at this point client is already disconnected
app.still_serving_cancelled_request = True
app.ctx.still_serving_cancelled_request = True
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
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")
async def inject_data(app, loop):
app.injected = expected
app.ctx.injected = expected
@app.get("/")
async def handler(request):
return json({"injected": request.app.injected})
return json({"injected": request.app.ctx.injected})
request, response = app.test_client.get("/")

View File

@ -8,7 +8,7 @@ import pytest
from sanic import Sanic
from sanic.blueprints import Blueprint
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
@ -423,33 +423,6 @@ def test_request_stream_blueprint(app):
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):
"""test for complex application"""
bp = Blueprint("test_blueprint_request_stream")
@ -510,14 +483,8 @@ def test_request_stream(app):
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.add_route(view, "/composition_view")
request, response = app.test_client.get("/method_view")
assert response.status == 200
assert response.text == "OK"
@ -526,14 +493,6 @@ def test_request_stream(app):
assert response.status == 200
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")
assert response.status == 200
assert response.text == "OK"

View File

@ -15,6 +15,8 @@ from aiofiles import os as async_os
from pytest import LogCaptureFixture
from sanic import Request, Sanic
from sanic.compat import Header
from sanic.cookies import CookieJar
from sanic.response import (
HTTPResponse,
empty,
@ -277,7 +279,7 @@ def test_non_chunked_streaming_returns_correct_content(
assert response.text == "foo,bar"
def test_stream_response_with_cookies(app):
def test_stream_response_with_cookies_legacy(app):
@app.route("/")
async def test(request: Request):
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"
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):
@app.route("/")
async def test(request: Request):
@ -561,37 +582,37 @@ def test_multiple_responses(
message_in_records: Callable[[List[LogRecord], str], bool],
):
@app.route("/1")
async def handler(request: Request):
async def handler1(request: Request):
response = await request.respond()
await response.send("foo")
response = await request.respond()
@app.route("/2")
async def handler(request: Request):
async def handler2(request: Request):
response = await request.respond()
response = await request.respond()
await response.send("foo")
@app.get("/3")
async def handler(request: Request):
async def handler3(request: Request):
response = await request.respond()
await response.send("foo,")
response = await request.respond()
await response.send("bar")
@app.get("/4")
async def handler(request: Request):
async def handler4(request: Request):
response = await request.respond(headers={"one": "one"})
return json({"foo": "bar"}, headers={"one": "two"})
@app.get("/5")
async def handler(request: Request):
async def handler5(request: Request):
response = await request.respond(headers={"one": "one"})
await response.send("foo")
return json({"foo": "bar"}, headers={"one": "two"})
@app.get("/6")
async def handler(request: Request):
async def handler6(request: Request):
response = await request.respond(headers={"one": "one"})
await response.send("foo, ")
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.exceptions import NotFound, SanicException
from sanic.request import Request
from sanic.response import json, text
from sanic.response import empty, json, text
@pytest.mark.parametrize(
@ -1230,3 +1230,41 @@ def test_routes_with_and_without_slash_definitions(app):
_, response = app.test_client.post(f"/{term}/")
assert response.status == 200
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")
async def init_db(app, loop):
app.db = MySanicDb()
app.ctx.db = MySanicDb()
srv = await app.create_server(
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.before_start()
assert hasattr(app, "db")
assert isinstance(app.db, MySanicDb)
assert hasattr(app.ctx, "db")
assert isinstance(app.ctx.db, MySanicDb)
@pytest.mark.asyncio
@ -122,9 +122,9 @@ async def test_trigger_before_events_create_server_missing_event(app):
@app.listener
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):

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