Compare commits
4 Commits
v21.12.1
...
pre-regist
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8fa52e2d2 | ||
|
|
266af1e279 | ||
|
|
6d3f1e9982 | ||
|
|
a0a3840094 |
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -2,13 +2,9 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [ main ]
|
||||||
- main
|
|
||||||
- "*LTS"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches: [ main ]
|
||||||
- main
|
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '25 16 * * 0'
|
- cron: '25 16 * * 0'
|
||||||
|
|||||||
1
.github/workflows/coverage.yml
vendored
1
.github/workflows/coverage.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
tags:
|
tags:
|
||||||
- "!*" # Do not execute on tags
|
- "!*" # Do not execute on tags
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|||||||
1
.github/workflows/pr-bandit.yml
vendored
1
.github/workflows/pr-bandit.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
1
.github/workflows/pr-docs.yml
vendored
1
.github/workflows/pr-docs.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
1
.github/workflows/pr-linter.yml
vendored
1
.github/workflows/pr-linter.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
1
.github/workflows/pr-python310.yml
vendored
1
.github/workflows/pr-python310.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
1
.github/workflows/pr-python37.yml
vendored
1
.github/workflows/pr-python37.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
1
.github/workflows/pr-python38.yml
vendored
1
.github/workflows/pr-python38.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
1
.github/workflows/pr-python39.yml
vendored
1
.github/workflows/pr-python39.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
3
.github/workflows/pr-type-check.yml
vendored
3
.github/workflows/pr-type-check.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -16,7 +15,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
config:
|
config:
|
||||||
# - { python-version: 3.7, tox-env: type-checking}
|
- { python-version: 3.7, tox-env: type-checking}
|
||||||
- { python-version: 3.8, tox-env: type-checking}
|
- { python-version: 3.8, tox-env: type-checking}
|
||||||
- { python-version: 3.9, tox-env: type-checking}
|
- { python-version: 3.9, tox-env: type-checking}
|
||||||
- { python-version: "3.10", tox-env: type-checking}
|
- { python-version: "3.10", tox-env: type-checking}
|
||||||
|
|||||||
1
.github/workflows/pr-windows.yml
vendored
1
.github/workflows/pr-windows.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
192
CHANGELOG.rst
192
CHANGELOG.rst
@@ -1,12 +1,12 @@
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
CHANGELOG files are maintained in ``./docs/sanic/releases``. To view the full CHANGELOG, please visit https://sanic.readthedocs.io/en/stable/sanic/changelog.html.
|
From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases``
|
||||||
|
|
||||||
|
|
||||||
Version 21.6.1
|
Version 21.6.1
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
|
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
|
||||||
Update sanic-routing to allow for better splitting of complex URI templates
|
Update sanic-routing to allow for better splitting of complex URI templates
|
||||||
@@ -20,7 +20,8 @@ Version 21.6.1
|
|||||||
Version 21.6.0
|
Version 21.6.0
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
|
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
|
||||||
Add ``response.eof()`` method for closing a stream in a handler
|
Add ``response.eof()`` method for closing a stream in a handler
|
||||||
@@ -67,7 +68,8 @@ Version 21.6.0
|
|||||||
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
|
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
|
||||||
Additional methods for attaching ``HTTPMethodView``
|
Additional methods for attaching ``HTTPMethodView``
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
|
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
|
||||||
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
|
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
|
||||||
@@ -83,7 +85,8 @@ Version 21.6.0
|
|||||||
Fix issue where Blueprint exception handlers do not consistently route to proper handler
|
Fix issue where Blueprint exception handlers do not consistently route to proper handler
|
||||||
|
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
|
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
|
||||||
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
|
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
|
||||||
@@ -92,12 +95,14 @@ Version 21.6.0
|
|||||||
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
|
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
|
||||||
Deprecate StreamingHTTPResponse
|
Deprecate StreamingHTTPResponse
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
|
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
|
||||||
Remove Travis CI in favor of GitHub Actions
|
Remove Travis CI in favor of GitHub Actions
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
|
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
|
||||||
Fix typo in documentation
|
Fix typo in documentation
|
||||||
@@ -107,7 +112,8 @@ Version 21.6.0
|
|||||||
Version 21.3.2
|
Version 21.3.2
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
|
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
|
||||||
Disable response timeout on websocket connections
|
Disable response timeout on websocket connections
|
||||||
@@ -118,7 +124,8 @@ Version 21.3.2
|
|||||||
Version 21.3.1
|
Version 21.3.1
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
|
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
|
||||||
Static files inside subfolders are not accessible (404)
|
Static files inside subfolders are not accessible (404)
|
||||||
@@ -128,7 +135,8 @@ Version 21.3.0
|
|||||||
|
|
||||||
`Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_
|
`Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1876 <https://github.com/sanic-org/sanic/pull/1876>`_
|
`#1876 <https://github.com/sanic-org/sanic/pull/1876>`_
|
||||||
@@ -181,7 +189,8 @@ Version 21.3.0
|
|||||||
`#2063 <https://github.com/sanic-org/sanic/pull/2063>`_
|
`#2063 <https://github.com/sanic-org/sanic/pull/2063>`_
|
||||||
App and connection level context objects
|
App and connection level context objects
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes and issues resolved
|
||||||
|
****************************
|
||||||
|
|
||||||
* Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_
|
* Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_
|
||||||
``url_for`` where ``strict_slashes`` are on for a path ending in ``/``
|
``url_for`` where ``strict_slashes`` are on for a path ending in ``/``
|
||||||
@@ -211,7 +220,8 @@ Version 21.3.0
|
|||||||
`#2001 <https://github.com/sanic-org/sanic/pull/2001>`_
|
`#2001 <https://github.com/sanic-org/sanic/pull/2001>`_
|
||||||
Raise ValueError when cookie max-age is not an integer
|
Raise ValueError when cookie max-age is not an integer
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#2007 <https://github.com/sanic-org/sanic/pull/2007>`_
|
`#2007 <https://github.com/sanic-org/sanic/pull/2007>`_
|
||||||
@@ -230,7 +240,8 @@ Version 21.3.0
|
|||||||
* ``Request.endpoint`` deprecated in favor of ``Request.name``
|
* ``Request.endpoint`` deprecated in favor of ``Request.name``
|
||||||
* handler type name prefixes removed (static, websocket, etc)
|
* handler type name prefixes removed (static, websocket, etc)
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1995 <https://github.com/sanic-org/sanic/pull/1995>`_
|
`#1995 <https://github.com/sanic-org/sanic/pull/1995>`_
|
||||||
@@ -248,7 +259,8 @@ Version 21.3.0
|
|||||||
`#2049 <https://github.com/sanic-org/sanic/pull/2049>`_
|
`#2049 <https://github.com/sanic-org/sanic/pull/2049>`_
|
||||||
Updated setup.py to use ``find_packages``
|
Updated setup.py to use ``find_packages``
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1218 <https://github.com/sanic-org/sanic/pull/1218>`_
|
`#1218 <https://github.com/sanic-org/sanic/pull/1218>`_
|
||||||
@@ -270,7 +282,8 @@ Version 21.3.0
|
|||||||
`#2052 <https://github.com/sanic-org/sanic/pull/2052>`_
|
`#2052 <https://github.com/sanic-org/sanic/pull/2052>`_
|
||||||
Fix some examples and docs
|
Fix some examples and docs
|
||||||
|
|
||||||
**Miscellaneous**
|
Miscellaneous
|
||||||
|
*************
|
||||||
|
|
||||||
* ``Request.route`` property
|
* ``Request.route`` property
|
||||||
* Better websocket subprotocols support
|
* Better websocket subprotocols support
|
||||||
@@ -316,7 +329,8 @@ Version 21.3.0
|
|||||||
Version 20.12.3
|
Version 20.12.3
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
|
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
|
||||||
@@ -325,7 +339,8 @@ Version 20.12.3
|
|||||||
Version 20.12.2
|
Version 20.12.2
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Dependencies**
|
Dependencies
|
||||||
|
************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#2026 <https://github.com/sanic-org/sanic/pull/2026>`_
|
`#2026 <https://github.com/sanic-org/sanic/pull/2026>`_
|
||||||
@@ -338,7 +353,8 @@ Version 20.12.2
|
|||||||
Version 19.12.5
|
Version 19.12.5
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Dependencies**
|
Dependencies
|
||||||
|
************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#2025 <https://github.com/sanic-org/sanic/pull/2025>`_
|
`#2025 <https://github.com/sanic-org/sanic/pull/2025>`_
|
||||||
@@ -351,7 +367,8 @@ Version 19.12.5
|
|||||||
Version 20.12.0
|
Version 20.12.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
|
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
|
||||||
@@ -360,7 +377,8 @@ Version 20.12.0
|
|||||||
Version 20.12.0
|
Version 20.12.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
|
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
|
||||||
@@ -398,19 +416,22 @@ Version 20.12.0
|
|||||||
`#1979 <https://github.com/sanic-org/sanic/pull/1979>`_
|
`#1979 <https://github.com/sanic-org/sanic/pull/1979>`_
|
||||||
Add app registry and Sanic class level app retrieval
|
Add app registry and Sanic class level app retrieval
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1965 <https://github.com/sanic-org/sanic/pull/1965>`_
|
`#1965 <https://github.com/sanic-org/sanic/pull/1965>`_
|
||||||
Fix Chunked Transport-Encoding in ASGI streaming response
|
Fix Chunked Transport-Encoding in ASGI streaming response
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1981 <https://github.com/sanic-org/sanic/pull/1981>`_
|
`#1981 <https://github.com/sanic-org/sanic/pull/1981>`_
|
||||||
Cleanup and remove deprecated code
|
Cleanup and remove deprecated code
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1956 <https://github.com/sanic-org/sanic/pull/1956>`_
|
`#1956 <https://github.com/sanic-org/sanic/pull/1956>`_
|
||||||
@@ -424,7 +445,8 @@ Version 20.12.0
|
|||||||
`#1986 <https://github.com/sanic-org/sanic/pull/1986>`_
|
`#1986 <https://github.com/sanic-org/sanic/pull/1986>`_
|
||||||
Update tox requirements
|
Update tox requirements
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1951 <https://github.com/sanic-org/sanic/pull/1951>`_
|
`#1951 <https://github.com/sanic-org/sanic/pull/1951>`_
|
||||||
@@ -442,7 +464,8 @@ Version 20.12.0
|
|||||||
Version 20.9.1
|
Version 20.9.1
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1954 <https://github.com/sanic-org/sanic/pull/1954>`_
|
`#1954 <https://github.com/sanic-org/sanic/pull/1954>`_
|
||||||
@@ -455,7 +478,8 @@ Version 20.9.1
|
|||||||
Version 19.12.3
|
Version 19.12.3
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1959 <https://github.com/sanic-org/sanic/pull/1959>`_
|
`#1959 <https://github.com/sanic-org/sanic/pull/1959>`_
|
||||||
@@ -466,7 +490,8 @@ Version 20.9.0
|
|||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1887 <https://github.com/sanic-org/sanic/pull/1887>`_
|
`#1887 <https://github.com/sanic-org/sanic/pull/1887>`_
|
||||||
@@ -493,19 +518,22 @@ Version 20.9.0
|
|||||||
`#1937 <https://github.com/sanic-org/sanic/pull/1937>`_
|
`#1937 <https://github.com/sanic-org/sanic/pull/1937>`_
|
||||||
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
|
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1897 <https://github.com/sanic-org/sanic/pull/1897>`_
|
`#1897 <https://github.com/sanic-org/sanic/pull/1897>`_
|
||||||
Resolves exception from unread bytes in stream
|
Resolves exception from unread bytes in stream
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1903 <https://github.com/sanic-org/sanic/pull/1903>`_
|
`#1903 <https://github.com/sanic-org/sanic/pull/1903>`_
|
||||||
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
|
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1890 <https://github.com/sanic-org/sanic/pull/1890>`_,
|
`#1890 <https://github.com/sanic-org/sanic/pull/1890>`_,
|
||||||
@@ -520,7 +548,8 @@ Version 20.9.0
|
|||||||
`#1924 <https://github.com/sanic-org/sanic/pull/1924>`_
|
`#1924 <https://github.com/sanic-org/sanic/pull/1924>`_
|
||||||
Adding --strict-markers for pytest
|
Adding --strict-markers for pytest
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1922 <https://github.com/sanic-org/sanic/pull/1922>`_
|
`#1922 <https://github.com/sanic-org/sanic/pull/1922>`_
|
||||||
@@ -530,7 +559,8 @@ Version 20.9.0
|
|||||||
Version 20.6.3
|
Version 20.6.3
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1884 <https://github.com/sanic-org/sanic/pull/1884>`_
|
`#1884 <https://github.com/sanic-org/sanic/pull/1884>`_
|
||||||
@@ -540,7 +570,8 @@ Version 20.6.3
|
|||||||
Version 20.6.2
|
Version 20.6.2
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1641 <https://github.com/sanic-org/sanic/pull/1641>`_
|
`#1641 <https://github.com/sanic-org/sanic/pull/1641>`_
|
||||||
@@ -550,7 +581,8 @@ Version 20.6.2
|
|||||||
Version 20.6.1
|
Version 20.6.1
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1760 <https://github.com/sanic-org/sanic/pull/1760>`_
|
`#1760 <https://github.com/sanic-org/sanic/pull/1760>`_
|
||||||
@@ -564,7 +596,8 @@ Version 20.6.1
|
|||||||
`#1880 <https://github.com/sanic-org/sanic/pull/1880>`_
|
`#1880 <https://github.com/sanic-org/sanic/pull/1880>`_
|
||||||
Add handler names for websockets for url_for usage
|
Add handler names for websockets for url_for usage
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1776 <https://github.com/sanic-org/sanic/pull/1776>`_
|
`#1776 <https://github.com/sanic-org/sanic/pull/1776>`_
|
||||||
@@ -586,13 +619,15 @@ Version 20.6.1
|
|||||||
`#1853 <https://github.com/sanic-org/sanic/pull/1853>`_
|
`#1853 <https://github.com/sanic-org/sanic/pull/1853>`_
|
||||||
Fix pickle error when attempting to pickle an application which contains websocket routes
|
Fix pickle error when attempting to pickle an application which contains websocket routes
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1739 <https://github.com/sanic-org/sanic/pull/1739>`_
|
`#1739 <https://github.com/sanic-org/sanic/pull/1739>`_
|
||||||
Deprecate body_bytes to merge into body
|
Deprecate body_bytes to merge into body
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1852 <https://github.com/sanic-org/sanic/pull/1852>`_
|
`#1852 <https://github.com/sanic-org/sanic/pull/1852>`_
|
||||||
@@ -607,7 +642,8 @@ Version 20.6.1
|
|||||||
Wrap run()'s "protocol" type annotation in Optional[]
|
Wrap run()'s "protocol" type annotation in Optional[]
|
||||||
|
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1846 <https://github.com/sanic-org/sanic/pull/1846>`_
|
`#1846 <https://github.com/sanic-org/sanic/pull/1846>`_
|
||||||
@@ -627,7 +663,8 @@ Version 20.6.0
|
|||||||
Version 20.3.0
|
Version 20.3.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
|
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
|
||||||
@@ -658,7 +695,8 @@ Version 20.3.0
|
|||||||
`#1820 <https://github.com/sanic-org/sanic/pull/1820>`_
|
`#1820 <https://github.com/sanic-org/sanic/pull/1820>`_
|
||||||
Do not set content-type and content-length headers in exceptions
|
Do not set content-type and content-length headers in exceptions
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1748 <https://github.com/sanic-org/sanic/pull/1748>`_
|
`#1748 <https://github.com/sanic-org/sanic/pull/1748>`_
|
||||||
@@ -676,7 +714,8 @@ Version 20.3.0
|
|||||||
`#1808 <https://github.com/sanic-org/sanic/pull/1808>`_
|
`#1808 <https://github.com/sanic-org/sanic/pull/1808>`_
|
||||||
Fix Ctrl+C and tests on Windows
|
Fix Ctrl+C and tests on Windows
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1800 <https://github.com/sanic-org/sanic/pull/1800>`_
|
`#1800 <https://github.com/sanic-org/sanic/pull/1800>`_
|
||||||
@@ -694,7 +733,8 @@ Version 20.3.0
|
|||||||
`#1818 <https://github.com/sanic-org/sanic/pull/1818>`_
|
`#1818 <https://github.com/sanic-org/sanic/pull/1818>`_
|
||||||
Complete deprecation of ``app.remove_route`` and ``request.raw_args``
|
Complete deprecation of ``app.remove_route`` and ``request.raw_args``
|
||||||
|
|
||||||
**Dependencies**
|
Dependencies
|
||||||
|
************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1794 <https://github.com/sanic-org/sanic/pull/1794>`_
|
`#1794 <https://github.com/sanic-org/sanic/pull/1794>`_
|
||||||
@@ -704,13 +744,15 @@ Version 20.3.0
|
|||||||
`#1806 <https://github.com/sanic-org/sanic/pull/1806>`_
|
`#1806 <https://github.com/sanic-org/sanic/pull/1806>`_
|
||||||
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
|
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1833 <https://github.com/sanic-org/sanic/pull/1833>`_
|
`#1833 <https://github.com/sanic-org/sanic/pull/1833>`_
|
||||||
Resolve broken documentation builds
|
Resolve broken documentation builds
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1755 <https://github.com/sanic-org/sanic/pull/1755>`_
|
`#1755 <https://github.com/sanic-org/sanic/pull/1755>`_
|
||||||
@@ -752,7 +794,8 @@ Version 20.3.0
|
|||||||
Version 19.12.0
|
Version 19.12.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
- Fix blueprint middleware application
|
- Fix blueprint middleware application
|
||||||
|
|
||||||
@@ -771,7 +814,8 @@ Version 19.12.0
|
|||||||
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__)
|
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__)
|
||||||
|
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
- Move docs from MD to RST
|
- Move docs from MD to RST
|
||||||
|
|
||||||
@@ -785,7 +829,8 @@ Version 19.12.0
|
|||||||
Version 19.6.3
|
Version 19.6.3
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
- Enable Towncrier Support
|
- Enable Towncrier Support
|
||||||
|
|
||||||
@@ -793,7 +838,8 @@ Version 19.6.3
|
|||||||
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__)
|
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__)
|
||||||
|
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
- Documentation infrastructure changes
|
- Documentation infrastructure changes
|
||||||
|
|
||||||
@@ -806,7 +852,8 @@ Version 19.6.3
|
|||||||
Version 19.6.2
|
Version 19.6.2
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1562 <https://github.com/sanic-org/sanic/pull/1562>`_
|
`#1562 <https://github.com/sanic-org/sanic/pull/1562>`_
|
||||||
@@ -822,7 +869,8 @@ Version 19.6.2
|
|||||||
Add Configure support from object string
|
Add Configure support from object string
|
||||||
|
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1587 <https://github.com/sanic-org/sanic/pull/1587>`_
|
`#1587 <https://github.com/sanic-org/sanic/pull/1587>`_
|
||||||
@@ -840,7 +888,8 @@ Version 19.6.2
|
|||||||
`#1594 <https://github.com/sanic-org/sanic/pull/1594>`_
|
`#1594 <https://github.com/sanic-org/sanic/pull/1594>`_
|
||||||
Strict Slashes behavior fix
|
Strict Slashes behavior fix
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1544 <https://github.com/sanic-org/sanic/pull/1544>`_
|
`#1544 <https://github.com/sanic-org/sanic/pull/1544>`_
|
||||||
@@ -864,7 +913,8 @@ Version 19.6.2
|
|||||||
Version 19.3
|
Version 19.3
|
||||||
------------
|
------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1497 <https://github.com/sanic-org/sanic/pull/1497>`_
|
`#1497 <https://github.com/sanic-org/sanic/pull/1497>`_
|
||||||
@@ -932,7 +982,8 @@ Version 19.3
|
|||||||
|
|
||||||
This is a breaking change.
|
This is a breaking change.
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
|
||||||
*
|
*
|
||||||
@@ -968,7 +1019,8 @@ Version 19.3
|
|||||||
This allows the access log to be disabled for example when running via
|
This allows the access log to be disabled for example when running via
|
||||||
gunicorn.
|
gunicorn.
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
* `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials
|
* `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials
|
||||||
* `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
|
* `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
|
||||||
@@ -976,7 +1028,8 @@ Version 19.3
|
|||||||
* `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
|
* `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
|
||||||
* `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
|
* `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
* `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation
|
* `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation
|
||||||
* `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example
|
* `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example
|
||||||
@@ -1043,13 +1096,15 @@ Version 18.12
|
|||||||
Version 0.8
|
Version 0.8
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**0.8.3**
|
0.8.3
|
||||||
|
*****
|
||||||
|
|
||||||
* Changes:
|
* Changes:
|
||||||
|
|
||||||
* Ownership changed to org 'sanic-org'
|
* Ownership changed to org 'sanic-org'
|
||||||
|
|
||||||
**0.8.0**
|
0.8.0
|
||||||
|
*****
|
||||||
|
|
||||||
* Changes:
|
* Changes:
|
||||||
|
|
||||||
@@ -1129,16 +1184,19 @@ Version 0.1
|
|||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|
||||||
**0.1.7**
|
0.1.7
|
||||||
|
*****
|
||||||
|
|
||||||
* Reversed static url and directory arguments to meet spec
|
* Reversed static url and directory arguments to meet spec
|
||||||
|
|
||||||
**0.1.6**
|
0.1.6
|
||||||
|
*****
|
||||||
|
|
||||||
* Static files
|
* Static files
|
||||||
* Lazy Cookie Loading
|
* Lazy Cookie Loading
|
||||||
|
|
||||||
**0.1.5**
|
0.1.5
|
||||||
|
*****
|
||||||
|
|
||||||
* Cookies
|
* Cookies
|
||||||
* Blueprint listeners and ordering
|
* Blueprint listeners and ordering
|
||||||
@@ -1146,19 +1204,23 @@ Version 0.1
|
|||||||
* Fix: Incomplete file reads on medium+ sized post requests
|
* Fix: Incomplete file reads on medium+ sized post requests
|
||||||
* Breaking: after_start and before_stop now pass sanic as their first argument
|
* Breaking: after_start and before_stop now pass sanic as their first argument
|
||||||
|
|
||||||
**0.1.4**
|
0.1.4
|
||||||
|
*****
|
||||||
|
|
||||||
* Multiprocessing
|
* Multiprocessing
|
||||||
|
|
||||||
**0.1.3**
|
0.1.3
|
||||||
|
*****
|
||||||
|
|
||||||
* Blueprint support
|
* Blueprint support
|
||||||
* Faster Response processing
|
* Faster Response processing
|
||||||
|
|
||||||
**0.1.1 - 0.1.2**
|
0.1.1 - 0.1.2
|
||||||
|
*************
|
||||||
|
|
||||||
* Struggling to update pypi via CI
|
* Struggling to update pypi via CI
|
||||||
|
|
||||||
**0.1.0**
|
0.1.0
|
||||||
|
*****
|
||||||
|
|
||||||
* Released to public
|
* Released to public
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
|
|||||||
:stub-columns: 1
|
:stub-columns: 1
|
||||||
|
|
||||||
* - Build
|
* - Build
|
||||||
- | |Py310Test| |Py39Test| |Py38Test| |Py37Test|
|
- | |Py39Test| |Py38Test| |Py37Test|
|
||||||
* - Docs
|
* - Docs
|
||||||
- | |UserGuide| |Documentation|
|
- | |UserGuide| |Documentation|
|
||||||
* - Package
|
* - Package
|
||||||
@@ -27,8 +27,6 @@ Sanic | Build fast. Run fast.
|
|||||||
:target: https://community.sanicframework.org/
|
:target: https://community.sanicframework.org/
|
||||||
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
|
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
|
||||||
:target: https://discord.gg/FARQzAEMAA
|
:target: https://discord.gg/FARQzAEMAA
|
||||||
.. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main
|
|
||||||
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml
|
|
||||||
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
|
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
|
||||||
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
|
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
|
||||||
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
|
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
|
||||||
|
|||||||
@@ -38,3 +38,10 @@ sanic.views
|
|||||||
.. automodule:: sanic.views
|
.. automodule:: sanic.views
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.websocket
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.websocket
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
📜 Changelog
|
📜 Changelog
|
||||||
============
|
============
|
||||||
|
|
||||||
.. mdinclude:: ./releases/21/21.12.md
|
.. mdinclude:: ./releases/21.9.md
|
||||||
.. mdinclude:: ./releases/21/21.9.md
|
|
||||||
.. include:: ../../CHANGELOG.rst
|
.. include:: ../../CHANGELOG.rst
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
## Version 21.9.3
|
## Version 21.9
|
||||||
*Rerelease of v21.9.2 with some cleanup*
|
|
||||||
|
|
||||||
## Version 21.9.2
|
|
||||||
- [#2268](https://github.com/sanic-org/sanic/pull/2268) Make HTTP connections start in IDLE stage, avoiding delays and error messages
|
|
||||||
- [#2310](https://github.com/sanic-org/sanic/pull/2310) More consistent config setting with post-FALLBACK_ERROR_FORMAT apply
|
|
||||||
|
|
||||||
## Version 21.9.1
|
|
||||||
- [#2259](https://github.com/sanic-org/sanic/pull/2259) Allow non-conforming ErrorHandlers
|
|
||||||
|
|
||||||
## Version 21.9.0
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets
|
- [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
## Version 21.12.1
|
|
||||||
|
|
||||||
- [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup
|
|
||||||
- [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7
|
|
||||||
- [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -5,7 +5,7 @@ import asyncio
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def notify_server_started_after_five_seconds():
|
async def notify_server_started_after_five_seconds():
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from sanic import Sanic
|
|||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("request")
|
@app.middleware("request")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sanic import Sanic
|
|||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
def check_request_for_authorization_status(request):
|
def check_request_for_authorization_status(request):
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ are added. And blueprint response middleware are executed in _reverse_ order.
|
|||||||
On a valid request, it should print "1 2 3 6 5 4" to terminal
|
On a valid request, it should print "1 2 3 6 5 4" to terminal
|
||||||
"""
|
"""
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
bp = Blueprint("bp_example")
|
bp = Blueprint("bp_" + __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.on_request
|
@bp.on_request
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ from sanic import Blueprint, Sanic
|
|||||||
from sanic.response import file, json
|
from sanic.response import file, json
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
|
blueprint = Blueprint("name", url_prefix="/my_blueprint")
|
||||||
blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")
|
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2")
|
||||||
blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3")
|
blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3")
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/foo")
|
@blueprint.route("/foo")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from asyncio import sleep
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("DelayedResponseApp", strict_slashes=True)
|
app = Sanic(__name__, strict_slashes=True)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ from sanic import Sanic
|
|||||||
|
|
||||||
|
|
||||||
handler = CustomHandler()
|
handler = CustomHandler()
|
||||||
app = Sanic("Example", error_handler=handler)
|
app = Sanic(__name__, error_handler=handler)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@@ -9,5 +9,5 @@ async def test(request):
|
|||||||
return response.json({"test": True})
|
return response.json({"test": True})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sanic import Sanic
|
|||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
sem = None
|
sem = None
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ LOG_SETTINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example", log_config=LOG_SETTINGS)
|
app = Sanic(__name__, log_config=LOG_SETTINGS)
|
||||||
|
|
||||||
|
|
||||||
@app.on_request
|
@app.on_request
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ logdna = logging.getLogger(__name__)
|
|||||||
logdna.setLevel(logging.INFO)
|
logdna.setLevel(logging.INFO)
|
||||||
logdna.addHandler(logdna_handler)
|
logdna.addHandler(logdna_handler)
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.middleware
|
@app.middleware
|
||||||
|
|||||||
@@ -2,29 +2,27 @@
|
|||||||
Modify header or status in response
|
Modify header or status in response
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
@app.route('/')
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def handle_request(request):
|
def handle_request(request):
|
||||||
return response.json(
|
return response.json(
|
||||||
{"message": "Hello world!"},
|
{'message': 'Hello world!'},
|
||||||
headers={"X-Served-By": "sanic"},
|
headers={'X-Served-By': 'sanic'},
|
||||||
status=200,
|
status=200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/unauthorized")
|
@app.route('/unauthorized')
|
||||||
def handle_request(request):
|
def handle_request(request):
|
||||||
return response.json(
|
return response.json(
|
||||||
{"message": "You are not authorized"},
|
{'message': 'You are not authorized'},
|
||||||
headers={"X-Served-By": "sanic"},
|
headers={'X-Served-By': 'sanic'},
|
||||||
status=404,
|
status=404
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def test_port(worker_id):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def app():
|
def app():
|
||||||
app = Sanic("Example")
|
app = Sanic()
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def index(request):
|
async def index(request):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from sanic.handlers import ErrorHandler
|
|||||||
|
|
||||||
|
|
||||||
class RaygunExceptionReporter(ErrorHandler):
|
class RaygunExceptionReporter(ErrorHandler):
|
||||||
|
|
||||||
def __init__(self, raygun_api_key=None):
|
def __init__(self, raygun_api_key=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if raygun_api_key is None:
|
if raygun_api_key is None:
|
||||||
@@ -21,13 +22,16 @@ class RaygunExceptionReporter(ErrorHandler):
|
|||||||
|
|
||||||
|
|
||||||
raygun_error_reporter = RaygunExceptionReporter()
|
raygun_error_reporter = RaygunExceptionReporter()
|
||||||
app = Sanic("Example", error_handler=raygun_error_reporter)
|
app = Sanic(__name__, error_handler=raygun_error_reporter)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/raise")
|
@app.route("/raise")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
raise SanicException("You Broke It!")
|
raise SanicException('You Broke It!')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=getenv("PORT", 8080)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
app = Sanic("Example")
|
|
||||||
|
@app.route('/')
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def handle_request(request):
|
def handle_request(request):
|
||||||
return response.redirect("/redirect")
|
return response.redirect('/redirect')
|
||||||
|
|
||||||
|
|
||||||
@app.route("/redirect")
|
@app.route('/redirect')
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return response.json({"Redirected": True})
|
return response.json({"Redirected": True})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
@@ -1,63 +1,65 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.views import CompositionView
|
||||||
from sanic.response import stream, text
|
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView
|
||||||
from sanic.views import stream as stream_decorator
|
from sanic.views import stream as stream_decorator
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
from sanic.response import stream, text
|
||||||
|
|
||||||
|
bp = Blueprint('blueprint_request_stream')
|
||||||
bp = Blueprint("bp_example")
|
app = Sanic('request_stream')
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleView(HTTPMethodView):
|
class SimpleView(HTTPMethodView):
|
||||||
|
|
||||||
@stream_decorator
|
@stream_decorator
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
result = ""
|
result = ''
|
||||||
while True:
|
while True:
|
||||||
body = await request.stream.get()
|
body = await request.stream.get()
|
||||||
if body is None:
|
if body is None:
|
||||||
break
|
break
|
||||||
result += body.decode("utf-8")
|
result += body.decode('utf-8')
|
||||||
return text(result)
|
return text(result)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/stream", stream=True)
|
@app.post('/stream', stream=True)
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
async def streaming(response):
|
async def streaming(response):
|
||||||
while True:
|
while True:
|
||||||
body = await request.stream.get()
|
body = await request.stream.get()
|
||||||
if body is None:
|
if body is None:
|
||||||
break
|
break
|
||||||
body = body.decode("utf-8").replace("1", "A")
|
body = body.decode('utf-8').replace('1', 'A')
|
||||||
await response.write(body)
|
await response.write(body)
|
||||||
|
|
||||||
return stream(streaming)
|
return stream(streaming)
|
||||||
|
|
||||||
|
|
||||||
@bp.put("/bp_stream", stream=True)
|
@bp.put('/bp_stream', stream=True)
|
||||||
async def bp_handler(request):
|
async def bp_handler(request):
|
||||||
result = ""
|
result = ''
|
||||||
while True:
|
while True:
|
||||||
body = await request.stream.get()
|
body = await request.stream.get()
|
||||||
if body is None:
|
if body is None:
|
||||||
break
|
break
|
||||||
result += body.decode("utf-8").replace("1", "A")
|
result += body.decode('utf-8').replace('1', 'A')
|
||||||
return text(result)
|
return text(result)
|
||||||
|
|
||||||
|
|
||||||
async def post_handler(request):
|
async def post_handler(request):
|
||||||
result = ""
|
result = ''
|
||||||
while True:
|
while True:
|
||||||
body = await request.stream.get()
|
body = await request.stream.get()
|
||||||
if body is None:
|
if body is None:
|
||||||
break
|
break
|
||||||
result += body.decode("utf-8")
|
result += body.decode('utf-8')
|
||||||
return text(result)
|
return text(result)
|
||||||
|
|
||||||
|
|
||||||
app.blueprint(bp)
|
app.blueprint(bp)
|
||||||
app.add_route(SimpleView.as_view(), "/method_view")
|
app.add_route(SimpleView.as_view(), '/method_view')
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['POST'], post_handler, stream=True)
|
||||||
|
app.add_route(view, '/composition_view')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host='0.0.0.0', port=8000)
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
from sanic import Sanic
|
||||||
from sanic import Sanic, response
|
from sanic import response
|
||||||
from sanic.config import Config
|
from sanic.config import Config
|
||||||
from sanic.exceptions import RequestTimeout
|
from sanic.exceptions import RequestTimeout
|
||||||
|
|
||||||
|
|
||||||
Config.REQUEST_TIMEOUT = 1
|
Config.REQUEST_TIMEOUT = 1
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route('/')
|
||||||
async def test(request):
|
async def test(request):
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
return response.text("Hello, world!")
|
return response.text('Hello, world!')
|
||||||
|
|
||||||
|
|
||||||
@app.exception(RequestTimeout)
|
@app.exception(RequestTimeout)
|
||||||
def timeout(request, exception):
|
def timeout(request, exception):
|
||||||
return response.text("RequestTimeout from error_handler.", 408)
|
return response.text('RequestTimeout from error_handler.', 408)
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000)
|
||||||
app.run(host="0.0.0.0", port=8000)
|
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
from os import getenv
|
|
||||||
|
|
||||||
import rollbar
|
import rollbar
|
||||||
|
|
||||||
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.handlers import ErrorHandler
|
from os import getenv
|
||||||
|
|
||||||
|
|
||||||
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
||||||
|
|
||||||
|
|
||||||
class RollbarExceptionHandler(ErrorHandler):
|
class RollbarExceptionHandler(ErrorHandler):
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
rollbar.report_message(str(exception))
|
rollbar.report_message(str(exception))
|
||||||
return super().default(request, exception)
|
return super().default(request, exception)
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example", error_handler=RollbarExceptionHandler())
|
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
|
||||||
|
|
||||||
|
|
||||||
@app.route("/raise")
|
@app.route("/raise")
|
||||||
@@ -25,4 +24,7 @@ def create_error(request):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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)
|
||||||
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/text")
|
@app.route("/text")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import uvloop
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sanic import Sanic, response
|
|||||||
from sanic.server import AsyncioServer
|
from sanic.server import AsyncioServer
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_start
|
@app.before_server_start
|
||||||
|
|||||||
@@ -6,19 +6,20 @@ from sentry_sdk.integrations.sanic import SanicIntegration
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
sentry_init(
|
sentry_init(
|
||||||
dsn=getenv("SENTRY_DSN"),
|
dsn=getenv("SENTRY_DSN"),
|
||||||
integrations=[SanicIntegration()],
|
integrations=[SanicIntegration()],
|
||||||
)
|
)
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
@app.route("/working")
|
@app.route("/working")
|
||||||
async def working_path(request):
|
async def working_path(request):
|
||||||
return json({"response": "Working API Response"})
|
return json({
|
||||||
|
"response": "Working API Response"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
@@ -27,5 +28,8 @@ async def raise_error(request):
|
|||||||
raise Exception("Testing Sentry Integration")
|
raise Exception("Testing Sentry Integration")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
app.static("/", "./static")
|
app.static("/", "./static")
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic import response as res
|
from sanic import response as res
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(req):
|
async def test(req):
|
||||||
return res.text("I'm a teapot", status=418)
|
return res.text("I\'m a teapot", status=418)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from sanic.exceptions import ServerError
|
|||||||
from sanic.log import logger as log
|
from sanic.log import logger as log
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import socket
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/test")
|
@app.route("/test")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sanic.blueprints import Blueprint
|
|||||||
# curl -H "Host: bp.example.com" localhost:8000/question
|
# curl -H "Host: bp.example.com" localhost:8000/question
|
||||||
# curl -H "Host: bp.example.com" localhost:8000/answer
|
# curl -H "Host: bp.example.com" localhost:8000/answer
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
bp = Blueprint("bp", host="bp.example.com")
|
bp = Blueprint("bp", host="bp.example.com")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from sanic import Sanic
|
|||||||
from sanic.response import redirect
|
from sanic.response import redirect
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
app.static("index.html", "websocket.html")
|
app.static("index.html", "websocket.html")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "21.12.1"
|
__version__ = "21.12.0dev"
|
||||||
|
|||||||
525
sanic/app.py
525
sanic/app.py
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import os
|
import os
|
||||||
@@ -12,7 +11,6 @@ from asyncio import (
|
|||||||
AbstractEventLoop,
|
AbstractEventLoop,
|
||||||
CancelledError,
|
CancelledError,
|
||||||
Protocol,
|
Protocol,
|
||||||
Task,
|
|
||||||
ensure_future,
|
ensure_future,
|
||||||
get_event_loop,
|
get_event_loop,
|
||||||
wait_for,
|
wait_for,
|
||||||
@@ -28,7 +26,6 @@ from ssl import SSLContext
|
|||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
Any,
|
||||||
AnyStr,
|
AnyStr,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
@@ -42,11 +39,10 @@ from typing import (
|
|||||||
Set,
|
Set,
|
||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
from warnings import filterwarnings
|
from warnings import filterwarnings, warn
|
||||||
|
|
||||||
from sanic_routing.exceptions import ( # type: ignore
|
from sanic_routing.exceptions import ( # type: ignore
|
||||||
FinalizationError,
|
FinalizationError,
|
||||||
@@ -55,12 +51,11 @@ from sanic_routing.exceptions import ( # type: ignore
|
|||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
from sanic import reloader_helpers
|
from sanic import reloader_helpers
|
||||||
from sanic.application.ext import setup_ext
|
|
||||||
from sanic.application.logo import get_logo
|
from sanic.application.logo import get_logo
|
||||||
from sanic.application.motd import MOTD
|
from sanic.application.motd import MOTD
|
||||||
from sanic.application.state import ApplicationState, Mode
|
from sanic.application.state import ApplicationState, Mode
|
||||||
from sanic.asgi import ASGIApp
|
from sanic.asgi import ASGIApp
|
||||||
from sanic.base.root import BaseSanic
|
from sanic.base import BaseSanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
||||||
@@ -72,15 +67,9 @@ from sanic.exceptions import (
|
|||||||
URLBuildError,
|
URLBuildError,
|
||||||
)
|
)
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.helpers import _default
|
from sanic.helpers import Default, _default
|
||||||
from sanic.http import Stage
|
from sanic.http import Stage
|
||||||
from sanic.log import (
|
from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger
|
||||||
LOGGING_CONFIG_DEFAULTS,
|
|
||||||
Colors,
|
|
||||||
deprecation,
|
|
||||||
error_logger,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
from sanic.mixins.listeners import ListenerEvent
|
from sanic.mixins.listeners import ListenerEvent
|
||||||
from sanic.models.futures import (
|
from sanic.models.futures import (
|
||||||
FutureException,
|
FutureException,
|
||||||
@@ -94,11 +83,11 @@ from sanic.models.futures import (
|
|||||||
from sanic.models.handler_types import ListenerType, MiddlewareType
|
from sanic.models.handler_types import ListenerType, MiddlewareType
|
||||||
from sanic.models.handler_types import Sanic as SanicVar
|
from sanic.models.handler_types import Sanic as SanicVar
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream
|
from sanic.response import BaseHTTPResponse, HTTPResponse
|
||||||
from sanic.router import Router
|
from sanic.router import Router
|
||||||
from sanic.server import AsyncioServer, HttpProtocol
|
from sanic.server import AsyncioServer, HttpProtocol
|
||||||
from sanic.server import Signal as ServerSignal
|
from sanic.server import Signal as ServerSignal
|
||||||
from sanic.server import serve, serve_multiple, serve_single, try_use_uvloop
|
from sanic.server import serve, serve_multiple, serve_single
|
||||||
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
||||||
from sanic.server.websockets.impl import ConnectionClosed
|
from sanic.server.websockets.impl import ConnectionClosed
|
||||||
from sanic.signals import Signal, SignalRouter
|
from sanic.signals import Signal, SignalRouter
|
||||||
@@ -106,21 +95,11 @@ from sanic.tls import process_to_context
|
|||||||
from sanic.touchup import TouchUp, TouchUpMeta
|
from sanic.touchup import TouchUp, TouchUpMeta
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
|
||||||
try:
|
|
||||||
from sanic_ext import Extend # type: ignore
|
|
||||||
from sanic_ext.extensions.base import Extension # type: ignore
|
|
||||||
except ImportError:
|
|
||||||
Extend = TypeVar("Extend") # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
if OS_IS_WINDOWS:
|
if OS_IS_WINDOWS:
|
||||||
enable_windows_color_support()
|
enable_windows_color_support()
|
||||||
|
|
||||||
filterwarnings("once", category=DeprecationWarning)
|
filterwarnings("once", category=DeprecationWarning)
|
||||||
|
|
||||||
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
|
|
||||||
|
|
||||||
|
|
||||||
class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
"""
|
"""
|
||||||
@@ -133,12 +112,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"_run_response_middleware",
|
"_run_response_middleware",
|
||||||
"_run_request_middleware",
|
"_run_request_middleware",
|
||||||
)
|
)
|
||||||
__slots__ = (
|
__fake_slots__ = (
|
||||||
|
"_app_registry",
|
||||||
"_asgi_app",
|
"_asgi_app",
|
||||||
"_asgi_client",
|
"_asgi_client",
|
||||||
"_blueprint_order",
|
"_blueprint_order",
|
||||||
"_delayed_tasks",
|
"_delayed_tasks",
|
||||||
"_ext",
|
|
||||||
"_future_exceptions",
|
"_future_exceptions",
|
||||||
"_future_listeners",
|
"_future_listeners",
|
||||||
"_future_middleware",
|
"_future_middleware",
|
||||||
@@ -147,15 +126,20 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"_future_signals",
|
"_future_signals",
|
||||||
"_future_statics",
|
"_future_statics",
|
||||||
"_state",
|
"_state",
|
||||||
"_task_registry",
|
|
||||||
"_test_client",
|
"_test_client",
|
||||||
"_test_manager",
|
"_test_manager",
|
||||||
|
"asgi",
|
||||||
|
"auto_reload",
|
||||||
|
"auto_reload",
|
||||||
"blueprints",
|
"blueprints",
|
||||||
"config",
|
"config",
|
||||||
"configure_logging",
|
"configure_logging",
|
||||||
"ctx",
|
"ctx",
|
||||||
|
"debug",
|
||||||
"error_handler",
|
"error_handler",
|
||||||
"go_fast",
|
"go_fast",
|
||||||
|
"is_running",
|
||||||
|
"is_stopping",
|
||||||
"listeners",
|
"listeners",
|
||||||
"name",
|
"name",
|
||||||
"named_request_middleware",
|
"named_request_middleware",
|
||||||
@@ -167,12 +151,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"signal_router",
|
"signal_router",
|
||||||
"sock",
|
"sock",
|
||||||
"strict_slashes",
|
"strict_slashes",
|
||||||
|
"test_mode",
|
||||||
"websocket_enabled",
|
"websocket_enabled",
|
||||||
"websocket_tasks",
|
"websocket_tasks",
|
||||||
)
|
)
|
||||||
|
|
||||||
_app_registry: Dict[str, "Sanic"] = {}
|
_app_registry: Dict[str, "Sanic"] = {}
|
||||||
_uvloop_setting = None # TODO: Remove in v22.6
|
|
||||||
test_mode = False
|
test_mode = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -183,6 +167,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
router: Optional[Router] = None,
|
router: Optional[Router] = None,
|
||||||
signal_router: Optional[SignalRouter] = None,
|
signal_router: Optional[SignalRouter] = None,
|
||||||
error_handler: Optional[ErrorHandler] = None,
|
error_handler: Optional[ErrorHandler] = None,
|
||||||
|
load_env: Union[bool, str] = True,
|
||||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
request_class: Optional[Type[Request]] = None,
|
request_class: Optional[Type[Request]] = None,
|
||||||
strict_slashes: bool = False,
|
strict_slashes: bool = False,
|
||||||
@@ -198,27 +183,25 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
dict_config = log_config or LOGGING_CONFIG_DEFAULTS
|
dict_config = log_config or LOGGING_CONFIG_DEFAULTS
|
||||||
logging.config.dictConfig(dict_config) # type: ignore
|
logging.config.dictConfig(dict_config) # type: ignore
|
||||||
|
|
||||||
if config and env_prefix != SANIC_PREFIX:
|
if config and (load_env is not True or env_prefix != SANIC_PREFIX):
|
||||||
raise SanicException(
|
raise SanicException(
|
||||||
"When instantiating Sanic with config, you cannot also pass "
|
"When instantiating Sanic with config, you cannot also pass "
|
||||||
"env_prefix"
|
"load_env or env_prefix"
|
||||||
)
|
)
|
||||||
|
|
||||||
# First setup config
|
|
||||||
self.config: Config = config or Config(env_prefix=env_prefix)
|
|
||||||
|
|
||||||
# Then we can do the rest
|
|
||||||
self._asgi_client: Any = None
|
self._asgi_client: Any = None
|
||||||
|
self._test_client: Any = None
|
||||||
|
self._test_manager: Any = None
|
||||||
self._blueprint_order: List[Blueprint] = []
|
self._blueprint_order: List[Blueprint] = []
|
||||||
self._delayed_tasks: List[str] = []
|
self._delayed_tasks: List[str] = []
|
||||||
self._future_registry: FutureRegistry = FutureRegistry()
|
self._future_registry: FutureRegistry = FutureRegistry()
|
||||||
self._state: ApplicationState = ApplicationState(app=self)
|
self._state: ApplicationState = ApplicationState(app=self)
|
||||||
self._task_registry: Dict[str, Task] = {}
|
|
||||||
self._test_client: Any = None
|
|
||||||
self._test_manager: Any = None
|
|
||||||
self.asgi = False
|
|
||||||
self.auto_reload = False
|
|
||||||
self.blueprints: Dict[str, Blueprint] = {}
|
self.blueprints: Dict[str, Blueprint] = {}
|
||||||
|
self.config: Config = config or Config(
|
||||||
|
load_env=load_env,
|
||||||
|
env_prefix=env_prefix,
|
||||||
|
app=self,
|
||||||
|
)
|
||||||
self.configure_logging: bool = configure_logging
|
self.configure_logging: bool = configure_logging
|
||||||
self.ctx: Any = ctx or SimpleNamespace()
|
self.ctx: Any = ctx or SimpleNamespace()
|
||||||
self.debug = False
|
self.debug = False
|
||||||
@@ -240,12 +223,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
|
|
||||||
if register is not None:
|
if register is not None:
|
||||||
deprecation(
|
|
||||||
"The register argument is deprecated and will stop working "
|
|
||||||
"in v22.6. After v22.6 all apps will be added to the Sanic "
|
|
||||||
"app registry.",
|
|
||||||
22.6,
|
|
||||||
)
|
|
||||||
self.config.REGISTER = register
|
self.config.REGISTER = register
|
||||||
if self.config.REGISTER:
|
if self.config.REGISTER:
|
||||||
self.__class__.register_app(self)
|
self.__class__.register_app(self)
|
||||||
@@ -276,6 +253,32 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
# Registration
|
# Registration
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def add_task(
|
||||||
|
self,
|
||||||
|
task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Schedule a task to run later, after the loop has started.
|
||||||
|
Different from asyncio.ensure_future in that it does not
|
||||||
|
also return a future, and the actual ensure_future call
|
||||||
|
is delayed until before server start.
|
||||||
|
|
||||||
|
`See user guide re: background tasks
|
||||||
|
<https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__
|
||||||
|
|
||||||
|
:param task: future, couroutine or awaitable
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
loop = self.loop # Will raise SanicError if loop is not started
|
||||||
|
self._loop_add_task(task, self, loop)
|
||||||
|
except SanicException:
|
||||||
|
task_name = f"sanic.delayed_task.{hash(task)}"
|
||||||
|
if not self._delayed_tasks:
|
||||||
|
self.after_server_start(partial(self.dispatch_delayed_tasks))
|
||||||
|
|
||||||
|
self.signal(task_name)(partial(self.run_delayed_task, task=task))
|
||||||
|
self._delayed_tasks.append(task_name)
|
||||||
|
|
||||||
def register_listener(
|
def register_listener(
|
||||||
self, listener: ListenerType[SanicVar], event: str
|
self, listener: ListenerType[SanicVar], event: str
|
||||||
) -> ListenerType[SanicVar]:
|
) -> ListenerType[SanicVar]:
|
||||||
@@ -400,16 +403,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
websocket_handler.is_websocket = True # type: ignore
|
websocket_handler.is_websocket = True # type: ignore
|
||||||
params["handler"] = websocket_handler
|
params["handler"] = websocket_handler
|
||||||
|
|
||||||
ctx = params.pop("route_context")
|
|
||||||
|
|
||||||
routes = self.router.add(**params)
|
routes = self.router.add(**params)
|
||||||
if isinstance(routes, Route):
|
if isinstance(routes, Route):
|
||||||
routes = [routes]
|
routes = [routes]
|
||||||
|
|
||||||
for r in routes:
|
for r in routes:
|
||||||
r.ctx.websocket = websocket
|
r.ctx.websocket = websocket
|
||||||
r.ctx.static = params.get("static", False)
|
r.ctx.static = params.get("static", False)
|
||||||
r.ctx.__dict__.update(ctx)
|
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
@@ -538,6 +537,30 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
blueprint.strict_slashes = self.strict_slashes
|
blueprint.strict_slashes = self.strict_slashes
|
||||||
blueprint.register(self, options)
|
blueprint.register(self, options)
|
||||||
|
|
||||||
|
def _register_lazy_blueprints(self):
|
||||||
|
registry = {**Blueprint.__pre_registry__}
|
||||||
|
if _default in Blueprint.__pre_registry__:
|
||||||
|
if len(Sanic._app_registry) > 1:
|
||||||
|
raise SanicException(
|
||||||
|
"Ambiguous Blueprint pre-registration detected. When "
|
||||||
|
"there are multiple Sanic application instances, all "
|
||||||
|
"pre-registrations must use an application name."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.name in registry and _default in registry:
|
||||||
|
registry[_default].extend(registry.pop(self.name))
|
||||||
|
|
||||||
|
registry = {
|
||||||
|
self.name if k is _default else k: v
|
||||||
|
for k, v in registry.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, registrants in registry.items():
|
||||||
|
for reg_info in registrants:
|
||||||
|
blueprint = reg_info.pop("bp")
|
||||||
|
if name == self.name and blueprint.name not in self.blueprints:
|
||||||
|
self.blueprint(blueprint, **reg_info)
|
||||||
|
|
||||||
def url_for(self, view_name: str, **kwargs):
|
def url_for(self, view_name: str, **kwargs):
|
||||||
"""Build a URL based on a view name and the values provided.
|
"""Build a URL based on a view name and the values provided.
|
||||||
|
|
||||||
@@ -755,7 +778,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
exception, request.name if request else None
|
exception, request.name if request else None
|
||||||
)
|
)
|
||||||
if handler:
|
if handler:
|
||||||
deprecation(
|
warn(
|
||||||
"An error occurred while handling the request after at "
|
"An error occurred while handling the request after at "
|
||||||
"least some part of the response was sent to the client. "
|
"least some part of the response was sent to the client. "
|
||||||
"Therefore, the response from your custom exception "
|
"Therefore, the response from your custom exception "
|
||||||
@@ -770,7 +793,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"For further information, please see the docs: "
|
"For further information, please see the docs: "
|
||||||
"https://sanicframework.org/en/guide/advanced/"
|
"https://sanicframework.org/en/guide/advanced/"
|
||||||
"signals.html",
|
"signals.html",
|
||||||
22.6,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
response = self.error_handler.response(request, exception)
|
response = self.error_handler.response(request, exception)
|
||||||
@@ -823,9 +846,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
else:
|
else:
|
||||||
if request.stream:
|
if request.stream:
|
||||||
response = request.stream.response
|
response = request.stream.response
|
||||||
|
|
||||||
# Marked for cleanup and DRY with handle_request/handle_exception
|
|
||||||
# when ResponseStream is no longer supporder
|
|
||||||
if isinstance(response, BaseHTTPResponse):
|
if isinstance(response, BaseHTTPResponse):
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.lifecycle.response",
|
"http.lifecycle.response",
|
||||||
@@ -836,17 +856,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
await response.send(end_stream=True)
|
await response.send(end_stream=True)
|
||||||
elif isinstance(response, ResponseStream):
|
|
||||||
resp = await response(request)
|
|
||||||
await self.dispatch(
|
|
||||||
"http.lifecycle.response",
|
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": resp,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await response.eof()
|
|
||||||
else:
|
else:
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
f"Invalid response type {response!r} (need HTTPResponse)"
|
f"Invalid response type {response!r} (need HTTPResponse)"
|
||||||
@@ -950,8 +959,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
elif not hasattr(handler, "is_websocket"):
|
elif not hasattr(handler, "is_websocket"):
|
||||||
response = request.stream.response # type: ignore
|
response = request.stream.response # type: ignore
|
||||||
|
|
||||||
# Marked for cleanup and DRY with handle_request/handle_exception
|
# Make sure that response is finished / run StreamingHTTP callback
|
||||||
# when ResponseStream is no longer supporder
|
|
||||||
if isinstance(response, BaseHTTPResponse):
|
if isinstance(response, BaseHTTPResponse):
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.lifecycle.response",
|
"http.lifecycle.response",
|
||||||
@@ -962,17 +970,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
await response.send(end_stream=True)
|
await response.send(end_stream=True)
|
||||||
elif isinstance(response, ResponseStream):
|
|
||||||
resp = await response(request)
|
|
||||||
await self.dispatch(
|
|
||||||
"http.lifecycle.response",
|
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": resp,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await response.eof()
|
|
||||||
else:
|
else:
|
||||||
if not hasattr(handler, "is_websocket"):
|
if not hasattr(handler, "is_websocket"):
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
@@ -1186,11 +1183,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
register_sys_signals=register_sys_signals,
|
register_sys_signals=register_sys_signals,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.config.USE_UVLOOP is True or (
|
|
||||||
self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
|
|
||||||
):
|
|
||||||
try_use_uvloop()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
self.is_stopping = False
|
self.is_stopping = False
|
||||||
@@ -1218,7 +1210,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
This kills the Sanic
|
This kills the Sanic
|
||||||
"""
|
"""
|
||||||
if not self.is_stopping:
|
if not self.is_stopping:
|
||||||
self.shutdown_tasks(timeout=0)
|
|
||||||
self.is_stopping = True
|
self.is_stopping = True
|
||||||
get_event_loop().stop()
|
get_event_loop().stop()
|
||||||
|
|
||||||
@@ -1288,13 +1279,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set explicitly passed configuration values
|
# if access_log is passed explicitly change config.ACCESS_LOG
|
||||||
for attribute, value in {
|
if access_log is not None:
|
||||||
"ACCESS_LOG": access_log,
|
self.config.ACCESS_LOG = access_log
|
||||||
"NOISY_EXCEPTIONS": noisy_exceptions,
|
|
||||||
}.items():
|
if noisy_exceptions is not None:
|
||||||
if value is not None:
|
self.config.NOISY_EXCEPTIONS = noisy_exceptions
|
||||||
setattr(self.config, attribute, value)
|
|
||||||
|
|
||||||
server_settings = self._helper(
|
server_settings = self._helper(
|
||||||
host=host,
|
host=host,
|
||||||
@@ -1309,14 +1299,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
run_async=return_asyncio_server,
|
run_async=return_asyncio_server,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.config.USE_UVLOOP is not _default:
|
|
||||||
error_logger.warning(
|
|
||||||
"You are trying to change the uvloop configuration, but "
|
|
||||||
"this is only effective when using the run(...) method. "
|
|
||||||
"When using the create_server(...) method Sanic will use "
|
|
||||||
"the already existing loop."
|
|
||||||
)
|
|
||||||
|
|
||||||
main_start = server_settings.pop("main_start", None)
|
main_start = server_settings.pop("main_start", None)
|
||||||
main_stop = server_settings.pop("main_stop", None)
|
main_stop = server_settings.pop("main_stop", None)
|
||||||
if main_start or main_stop:
|
if main_start or main_stop:
|
||||||
@@ -1435,15 +1417,26 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"#proxy-configuration"
|
"#proxy-configuration"
|
||||||
)
|
)
|
||||||
|
|
||||||
ssl = process_to_context(ssl)
|
|
||||||
|
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.state.host = host
|
self.state.host = host
|
||||||
self.state.port = port
|
self.state.port = port
|
||||||
self.state.workers = workers
|
self.state.workers = workers
|
||||||
self.state.ssl = ssl
|
|
||||||
self.state.unix = unix
|
# Serve
|
||||||
self.state.sock = sock
|
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)
|
||||||
|
|
||||||
server_settings = {
|
server_settings = {
|
||||||
"protocol": protocol,
|
"protocol": protocol,
|
||||||
@@ -1459,7 +1452,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"backlog": backlog,
|
"backlog": backlog,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.motd(self.serve_location)
|
self.motd(serve_location)
|
||||||
|
|
||||||
if sys.stdout.isatty() and not self.state.is_debug:
|
if sys.stdout.isatty() and not self.state.is_debug:
|
||||||
error_logger.warning(
|
error_logger.warning(
|
||||||
@@ -1485,55 +1478,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
return server_settings
|
return server_settings
|
||||||
|
|
||||||
@property
|
|
||||||
def serve_location(self) -> str:
|
|
||||||
serve_location = ""
|
|
||||||
proto = "http"
|
|
||||||
if self.state.ssl is not None:
|
|
||||||
proto = "https"
|
|
||||||
if self.state.unix:
|
|
||||||
serve_location = f"{self.state.unix} {proto}://..."
|
|
||||||
elif self.state.sock:
|
|
||||||
serve_location = f"{self.state.sock.getsockname()} {proto}://..."
|
|
||||||
elif self.state.host and self.state.port:
|
|
||||||
# colon(:) is legal for a host only in an ipv6 address
|
|
||||||
display_host = (
|
|
||||||
f"[{self.state.host}]"
|
|
||||||
if ":" in self.state.host
|
|
||||||
else self.state.host
|
|
||||||
)
|
|
||||||
serve_location = f"{proto}://{display_host}:{self.state.port}"
|
|
||||||
|
|
||||||
return serve_location
|
|
||||||
|
|
||||||
def _build_endpoint_name(self, *parts):
|
def _build_endpoint_name(self, *parts):
|
||||||
parts = [self.name, *parts]
|
parts = [self.name, *parts]
|
||||||
return ".".join(parts)
|
return ".".join(parts)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _cancel_websocket_tasks(cls, app, loop):
|
def _prep_task(cls, task, app, loop):
|
||||||
for task in app.websocket_tasks:
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _listener(
|
|
||||||
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
|
|
||||||
):
|
|
||||||
maybe_coro = listener(app, loop)
|
|
||||||
if maybe_coro and isawaitable(maybe_coro):
|
|
||||||
await maybe_coro
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
|
||||||
# Task management
|
|
||||||
# -------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _prep_task(
|
|
||||||
cls,
|
|
||||||
task,
|
|
||||||
app,
|
|
||||||
loop,
|
|
||||||
):
|
|
||||||
if callable(task):
|
if callable(task):
|
||||||
try:
|
try:
|
||||||
task = task(app)
|
task = task(app)
|
||||||
@@ -1543,31 +1493,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
return task
|
return task
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _loop_add_task(
|
def _loop_add_task(cls, task, app, loop):
|
||||||
cls,
|
prepped = cls._prep_task(task, app, loop)
|
||||||
task,
|
loop.create_task(prepped)
|
||||||
app,
|
|
||||||
loop,
|
|
||||||
*,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
register: bool = True,
|
|
||||||
) -> Task:
|
|
||||||
if not isinstance(task, Future):
|
|
||||||
prepped = cls._prep_task(task, app, loop)
|
|
||||||
if sys.version_info < (3, 8):
|
|
||||||
if name:
|
|
||||||
error_logger.warning(
|
|
||||||
"Cannot set a name for a task when using Python 3.7. "
|
|
||||||
"Your task will be created without a name."
|
|
||||||
)
|
|
||||||
task = loop.create_task(prepped)
|
|
||||||
else:
|
|
||||||
task = loop.create_task(prepped, name=name)
|
|
||||||
|
|
||||||
if name and register and sys.version_info > (3, 7):
|
@classmethod
|
||||||
app._task_registry[name] = task
|
def _cancel_websocket_tasks(cls, app, loop):
|
||||||
|
for task in app.websocket_tasks:
|
||||||
return task
|
task.cancel()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def dispatch_delayed_tasks(app, loop):
|
async def dispatch_delayed_tasks(app, loop):
|
||||||
@@ -1580,142 +1513,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
prepped = app._prep_task(task, app, loop)
|
prepped = app._prep_task(task, app, loop)
|
||||||
await prepped
|
await prepped
|
||||||
|
|
||||||
def add_task(
|
@staticmethod
|
||||||
self,
|
async def _listener(
|
||||||
task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]],
|
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
|
||||||
*,
|
|
||||||
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, 8):
|
|
||||||
error_logger.warning(
|
|
||||||
"This feature (get_task) is only supported on using "
|
|
||||||
"Python 3.8+."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
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, 8):
|
|
||||||
error_logger.warning(
|
|
||||||
"This feature (cancel_task) is only supported on using "
|
|
||||||
"Python 3.8+."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
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, 8):
|
|
||||||
error_logger.warning(
|
|
||||||
"This feature (purge_tasks) is only supported on using "
|
|
||||||
"Python 3.8+."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
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
|
|
||||||
):
|
):
|
||||||
if sys.version_info < (3, 8):
|
maybe_coro = listener(app, loop)
|
||||||
error_logger.warning(
|
if maybe_coro and isawaitable(maybe_coro):
|
||||||
"This feature (shutdown_tasks) is only supported on using "
|
await maybe_coro
|
||||||
"Python 3.8+."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
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, 8):
|
|
||||||
error_logger.warning(
|
|
||||||
"This feature (tasks) is only supported on using "
|
|
||||||
"Python 3.8+."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
return iter(self._task_registry.values())
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# ASGI
|
# ASGI
|
||||||
@@ -1728,8 +1532,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
details: https://asgi.readthedocs.io/en/latest
|
details: https://asgi.readthedocs.io/en/latest
|
||||||
"""
|
"""
|
||||||
self.asgi = True
|
self.asgi = True
|
||||||
if scope["type"] == "lifespan":
|
self.motd("")
|
||||||
self.motd("")
|
|
||||||
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
|
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||||
asgi_app = self._asgi_app
|
asgi_app = self._asgi_app
|
||||||
await asgi_app()
|
await asgi_app()
|
||||||
@@ -1834,8 +1637,11 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
display["auto-reload"] = reload_display
|
display["auto-reload"] = reload_display
|
||||||
|
|
||||||
packages = []
|
packages = []
|
||||||
for package_name in SANIC_PACKAGES:
|
for package_name, module_name in {
|
||||||
module_name = package_name.replace("-", "_")
|
"sanic-routing": "sanic_routing",
|
||||||
|
"sanic-testing": "sanic_testing",
|
||||||
|
"sanic-ext": "sanic_ext",
|
||||||
|
}.items():
|
||||||
try:
|
try:
|
||||||
module = import_module(module_name)
|
module = import_module(module_name)
|
||||||
packages.append(f"{package_name}=={module.__version__}")
|
packages.append(f"{package_name}=={module.__version__}")
|
||||||
@@ -1855,41 +1661,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
MOTD.output(logo, serve_location, display, extra)
|
MOTD.output(logo, serve_location, display, extra)
|
||||||
|
|
||||||
@property
|
|
||||||
def ext(self) -> Extend:
|
|
||||||
if not hasattr(self, "_ext"):
|
|
||||||
setup_ext(self, fail=True)
|
|
||||||
|
|
||||||
if not hasattr(self, "_ext"):
|
|
||||||
raise RuntimeError(
|
|
||||||
"Sanic Extensions is not installed. You can add it to your "
|
|
||||||
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
|
|
||||||
"install sanic-ext"
|
|
||||||
)
|
|
||||||
return self._ext # type: ignore
|
|
||||||
|
|
||||||
def extend(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
extensions: Optional[List[Type[Extension]]] = None,
|
|
||||||
built_in_extensions: bool = True,
|
|
||||||
config: Optional[Union[Config, Dict[str, Any]]] = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Extend:
|
|
||||||
if hasattr(self, "_ext"):
|
|
||||||
raise RuntimeError(
|
|
||||||
"Cannot extend Sanic after Sanic Extensions has been setup."
|
|
||||||
)
|
|
||||||
setup_ext(
|
|
||||||
self,
|
|
||||||
extensions=extensions,
|
|
||||||
built_in_extensions=built_in_extensions,
|
|
||||||
config=config,
|
|
||||||
fail=True,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
return self.ext
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Class methods
|
# Class methods
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
@@ -1931,6 +1702,32 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
return cls(name)
|
return cls(name)
|
||||||
raise SanicException(f'Sanic app name "{name}" not found.')
|
raise SanicException(f'Sanic app name "{name}" not found.')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def lazy(
|
||||||
|
cls,
|
||||||
|
app_name: Union[str, Default] = _default,
|
||||||
|
*,
|
||||||
|
name: str = None,
|
||||||
|
url_prefix: Optional[str] = None,
|
||||||
|
host: Optional[Union[List[str], str]] = None,
|
||||||
|
version: Optional[Union[int, str, float]] = None,
|
||||||
|
strict_slashes: Optional[bool] = None,
|
||||||
|
version_prefix: str = "/v",
|
||||||
|
) -> Blueprint:
|
||||||
|
if not name:
|
||||||
|
flat = [1 for x in Blueprint.__pre_registry__.values() for _ in x]
|
||||||
|
name = f"bp{len(flat)}"
|
||||||
|
bp = Blueprint(
|
||||||
|
name=name,
|
||||||
|
url_prefix=url_prefix,
|
||||||
|
host=host,
|
||||||
|
version=version,
|
||||||
|
strict_slashes=strict_slashes,
|
||||||
|
version_prefix=version_prefix,
|
||||||
|
)
|
||||||
|
bp.pre_register(app_name)
|
||||||
|
return bp
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Lifecycle
|
# Lifecycle
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
@@ -1951,33 +1748,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
async def _startup(self):
|
async def _startup(self):
|
||||||
self._future_registry.clear()
|
self._future_registry.clear()
|
||||||
|
self._register_lazy_blueprints()
|
||||||
# Startup Sanic Extensions
|
|
||||||
if not hasattr(self, "_ext"):
|
|
||||||
setup_ext(self)
|
|
||||||
if hasattr(self, "_ext"):
|
|
||||||
self.ext._display()
|
|
||||||
|
|
||||||
# Setup routers
|
|
||||||
self.signalize()
|
self.signalize()
|
||||||
self.finalize()
|
self.finalize()
|
||||||
|
ErrorHandler.finalize(
|
||||||
# TODO: Replace in v22.6 to check against apps in app registry
|
self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT
|
||||||
if (
|
)
|
||||||
self.__class__._uvloop_setting is not None
|
|
||||||
and self.__class__._uvloop_setting != self.config.USE_UVLOOP
|
|
||||||
):
|
|
||||||
error_logger.warning(
|
|
||||||
"It looks like you're running several apps with different "
|
|
||||||
"uvloop settings. This is not supported and may lead to "
|
|
||||||
"unintended behaviour."
|
|
||||||
)
|
|
||||||
self.__class__._uvloop_setting = self.config.USE_UVLOOP
|
|
||||||
|
|
||||||
# Startup time optimizations
|
|
||||||
ErrorHandler.finalize(self.error_handler, config=self.config)
|
|
||||||
TouchUp.run(self)
|
TouchUp.run(self)
|
||||||
|
|
||||||
self.state.is_started = True
|
self.state.is_started = True
|
||||||
|
|
||||||
async def _server_event(
|
async def _server_event(
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -41,6 +41,9 @@ class MOTD(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class MOTDBasic(MOTD):
|
class MOTDBasic(MOTD):
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def display(self):
|
def display(self):
|
||||||
if self.logo:
|
if self.logo:
|
||||||
logger.debug(self.logo)
|
logger.debug(self.logo)
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import logging
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from socket import socket
|
from typing import TYPE_CHECKING, Any, Set, Union
|
||||||
from ssl import SSLContext
|
|
||||||
from typing import TYPE_CHECKING, Any, Optional, Set, Union
|
|
||||||
|
|
||||||
from sanic.log import logger
|
from sanic.log import logger
|
||||||
|
|
||||||
@@ -39,11 +37,8 @@ class ApplicationState:
|
|||||||
coffee: bool = field(default=False)
|
coffee: bool = field(default=False)
|
||||||
fast: bool = field(default=False)
|
fast: bool = field(default=False)
|
||||||
host: str = field(default="")
|
host: str = field(default="")
|
||||||
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)
|
mode: Mode = field(default=Mode.PRODUCTION)
|
||||||
|
port: int = field(default=0)
|
||||||
reload_dirs: Set[Path] = field(default_factory=set)
|
reload_dirs: Set[Path] = field(default_factory=set)
|
||||||
server: Server = field(default=Server.SANIC)
|
server: Server = field(default=Server.SANIC)
|
||||||
is_running: bool = field(default=False)
|
is_running: bool = field(default=False)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import sanic.app # noqa
|
|||||||
|
|
||||||
from sanic.compat import Header
|
from sanic.compat import Header
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.helpers import _default
|
|
||||||
from sanic.http import Stage
|
from sanic.http import Stage
|
||||||
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
|
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
@@ -54,13 +53,6 @@ class Lifespan:
|
|||||||
await self.asgi_app.sanic_app._server_event("init", "before")
|
await self.asgi_app.sanic_app._server_event("init", "before")
|
||||||
await self.asgi_app.sanic_app._server_event("init", "after")
|
await self.asgi_app.sanic_app._server_event("init", "after")
|
||||||
|
|
||||||
if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
|
|
||||||
warnings.warn(
|
|
||||||
"You have set the USE_UVLOOP configuration option, but Sanic "
|
|
||||||
"cannot control the event loop when running in ASGI mode."
|
|
||||||
"This option will be ignored."
|
|
||||||
)
|
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
"""
|
"""
|
||||||
Gather the listeners to fire on server stop.
|
Gather the listeners to fire on server stop.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Tuple
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.mixins.exceptions import ExceptionMixin
|
from sanic.mixins.exceptions import ExceptionMixin
|
||||||
from sanic.mixins.listeners import ListenerMixin
|
from sanic.mixins.listeners import ListenerMixin
|
||||||
@@ -20,9 +20,8 @@ class BaseSanic(
|
|||||||
ListenerMixin,
|
ListenerMixin,
|
||||||
ExceptionMixin,
|
ExceptionMixin,
|
||||||
SignalMixin,
|
SignalMixin,
|
||||||
metaclass=SanicMeta,
|
|
||||||
):
|
):
|
||||||
__slots__ = ("name",)
|
__fake_slots__: Tuple[str, ...]
|
||||||
|
|
||||||
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
|
||||||
class_name = self.__class__.__name__
|
class_name = self.__class__.__name__
|
||||||
@@ -34,10 +33,11 @@ class BaseSanic(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not VALID_NAME.match(name):
|
if not VALID_NAME.match(name):
|
||||||
raise SanicException(
|
warn(
|
||||||
f"{class_name} instance named '{name}' uses an invalid "
|
f"{class_name} instance named '{name}' uses a format that is"
|
||||||
"format. Names must begin with a character and may only "
|
f"deprecated. Starting in version 21.12, {class_name} objects "
|
||||||
"contain alphanumeric characters, _, or -."
|
"must be named only using alphanumeric characters, _, or -.",
|
||||||
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -52,12 +52,15 @@ class BaseSanic(
|
|||||||
return f'{self.__class__.__name__}(name="{self.name}")'
|
return f'{self.__class__.__name__}(name="{self.name}")'
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: Any) -> None:
|
def __setattr__(self, name: str, value: Any) -> None:
|
||||||
try:
|
# This is a temporary compat layer so we can raise a warning until
|
||||||
super().__setattr__(name, value)
|
# setting attributes on the app instance can be removed and deprecated
|
||||||
except AttributeError as e:
|
# with a proper implementation of __slots__
|
||||||
raise AttributeError(
|
if name not in self.__fake_slots__:
|
||||||
|
warn(
|
||||||
f"Setting variables on {self.__class__.__name__} instances is "
|
f"Setting variables on {self.__class__.__name__} instances is "
|
||||||
"not allowed. You should change your "
|
"deprecated and will be removed in version 21.12. You should "
|
||||||
f"{self.__class__.__name__} instance to use "
|
f"change your {self.__class__.__name__} instance to use "
|
||||||
f"instance.ctx.{name} instead.",
|
f"instance.ctx.{name} instead.",
|
||||||
) from e
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
super().__setattr__(name, value)
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
class SanicMeta(type):
|
|
||||||
@classmethod
|
|
||||||
def __prepare__(metaclass, name, bases, **kwds):
|
|
||||||
cls = super().__prepare__(metaclass, name, bases, **kwds)
|
|
||||||
cls["__slots__"] = ()
|
|
||||||
return cls
|
|
||||||
@@ -5,7 +5,7 @@ from functools import partial
|
|||||||
from typing import TYPE_CHECKING, List, Optional, Union
|
from typing import TYPE_CHECKING, List, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING:
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from typing import (
|
|||||||
from sanic_routing.exceptions import NotFound # type: ignore
|
from sanic_routing.exceptions import NotFound # type: ignore
|
||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
from sanic.base.root import BaseSanic
|
from sanic.base import BaseSanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.helpers import Default, _default
|
from sanic.helpers import Default, _default
|
||||||
@@ -36,8 +36,8 @@ from sanic.models.handler_types import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic # noqa
|
||||||
|
|
||||||
|
|
||||||
def lazy(func, as_decorator=True):
|
def lazy(func, as_decorator=True):
|
||||||
@@ -85,7 +85,7 @@ class Blueprint(BaseSanic):
|
|||||||
trailing */*
|
trailing */*
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__fake_slots__ = (
|
||||||
"_apps",
|
"_apps",
|
||||||
"_future_routes",
|
"_future_routes",
|
||||||
"_future_statics",
|
"_future_statics",
|
||||||
@@ -98,6 +98,7 @@ class Blueprint(BaseSanic):
|
|||||||
"host",
|
"host",
|
||||||
"listeners",
|
"listeners",
|
||||||
"middlewares",
|
"middlewares",
|
||||||
|
"name",
|
||||||
"routes",
|
"routes",
|
||||||
"statics",
|
"statics",
|
||||||
"strict_slashes",
|
"strict_slashes",
|
||||||
@@ -106,6 +107,7 @@ class Blueprint(BaseSanic):
|
|||||||
"version_prefix",
|
"version_prefix",
|
||||||
"websocket_routes",
|
"websocket_routes",
|
||||||
)
|
)
|
||||||
|
__pre_registry__: Dict[Union[str, Default], Any] = defaultdict(list)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -347,7 +349,6 @@ class Blueprint(BaseSanic):
|
|||||||
future.static,
|
future.static,
|
||||||
version_prefix,
|
version_prefix,
|
||||||
error_format,
|
error_format,
|
||||||
future.route_context,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (self, apply_route) in app._future_registry:
|
if (self, apply_route) in app._future_registry:
|
||||||
@@ -400,9 +401,8 @@ class Blueprint(BaseSanic):
|
|||||||
for future in self._future_signals:
|
for future in self._future_signals:
|
||||||
if (self, future) in app._future_registry:
|
if (self, future) in app._future_registry:
|
||||||
continue
|
continue
|
||||||
future.condition.update({"__blueprint__": self.name})
|
future.condition.update({"blueprint": self.name})
|
||||||
# Force exclusive to be False
|
app._apply_signal(future)
|
||||||
app._apply_signal(tuple((*future[:-1], False)))
|
|
||||||
|
|
||||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
self.routes += [route for route in routes if isinstance(route, Route)]
|
||||||
self.websocket_routes += [
|
self.websocket_routes += [
|
||||||
@@ -427,7 +427,7 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
async def dispatch(self, *args, **kwargs):
|
async def dispatch(self, *args, **kwargs):
|
||||||
condition = kwargs.pop("condition", {})
|
condition = kwargs.pop("condition", {})
|
||||||
condition.update({"__blueprint__": self.name})
|
condition.update({"blueprint": self.name})
|
||||||
kwargs["condition"] = condition
|
kwargs["condition"] = condition
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[app.dispatch(*args, **kwargs) for app in self.apps]
|
*[app.dispatch(*args, **kwargs) for app in self.apps]
|
||||||
@@ -462,3 +462,30 @@ class Blueprint(BaseSanic):
|
|||||||
):
|
):
|
||||||
for app in apps:
|
for app in apps:
|
||||||
app._future_registry.update(set((bp, item) for item in futures))
|
app._future_registry.update(set((bp, item) for item in futures))
|
||||||
|
|
||||||
|
def pre_register(
|
||||||
|
self,
|
||||||
|
name: Union[str, Default],
|
||||||
|
url_prefix: Optional[str] = None,
|
||||||
|
host: Optional[Union[List[str], str]] = None,
|
||||||
|
version: Optional[Union[int, str, float]] = None,
|
||||||
|
strict_slashes: Optional[bool] = None,
|
||||||
|
version_prefix: Union[str, Default] = _default,
|
||||||
|
) -> None:
|
||||||
|
if not hasattr(self.ctx, "_prereg"):
|
||||||
|
self.ctx._prereg = []
|
||||||
|
self.ctx._prereg.append(name)
|
||||||
|
self.__class__.__pre_registry__[name].append(
|
||||||
|
{
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"bp": self,
|
||||||
|
"url_prefix": url_prefix,
|
||||||
|
"host": host,
|
||||||
|
"version": version,
|
||||||
|
"strict_slashes": strict_slashes,
|
||||||
|
"version_prefix": version_prefix,
|
||||||
|
}.items()
|
||||||
|
if v is not None and v is not _default
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ from typing import Any, List, Union
|
|||||||
|
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.application.logo import get_logo
|
from sanic.application.logo import get_logo
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.cli.arguments import Group
|
from sanic.cli.arguments import Group
|
||||||
|
from sanic.helpers import _default
|
||||||
from sanic.log import error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.simple import create_simple_server
|
from sanic.simple import create_simple_server
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ class SanicArgumentParser(ArgumentParser):
|
|||||||
|
|
||||||
|
|
||||||
class SanicCLI:
|
class SanicCLI:
|
||||||
|
DEFAULT_APP_NAME = "SANIC"
|
||||||
DESCRIPTION = indent(
|
DESCRIPTION = indent(
|
||||||
f"""
|
f"""
|
||||||
{get_logo(True)}
|
{get_logo(True)}
|
||||||
@@ -131,7 +134,17 @@ Or, a path to a directory to run as a simple HTTP server:
|
|||||||
|
|
||||||
app_type_name = type(app).__name__
|
app_type_name = type(app).__name__
|
||||||
|
|
||||||
if not isinstance(app, Sanic):
|
if isinstance(app, Blueprint):
|
||||||
|
bp = app
|
||||||
|
name = (
|
||||||
|
bp.ctx._prereg[0]
|
||||||
|
if hasattr(bp.ctx, "_prereg")
|
||||||
|
else _default
|
||||||
|
)
|
||||||
|
if name is _default:
|
||||||
|
name = self.DEFAULT_APP_NAME
|
||||||
|
app = Sanic(name)
|
||||||
|
elif not isinstance(app, Sanic):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Module is not a Sanic app, it is a {app_type_name}\n"
|
f"Module is not a Sanic app, it is a {app_type_name}\n"
|
||||||
f" Perhaps you meant {self.args.module}.app?"
|
f" Perhaps you meant {self.args.module}.app?"
|
||||||
|
|||||||
@@ -8,14 +8,6 @@ from multidict import CIMultiDict # type: ignore
|
|||||||
|
|
||||||
|
|
||||||
OS_IS_WINDOWS = os.name == "nt"
|
OS_IS_WINDOWS = os.name == "nt"
|
||||||
UVLOOP_INSTALLED = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
import uvloop # type: ignore # noqa
|
|
||||||
|
|
||||||
UVLOOP_INSTALLED = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def enable_windows_color_support():
|
def enable_windows_color_support():
|
||||||
|
|||||||
146
sanic/config.py
146
sanic/config.py
@@ -1,26 +1,28 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from inspect import getmembers, isclass, isdatadescriptor
|
from inspect import isclass
|
||||||
from os import environ
|
from os import environ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
from sanic.errorpages import check_error_format
|
||||||
from sanic.helpers import Default, _default
|
|
||||||
from sanic.http import Http
|
from sanic.http import Http
|
||||||
from sanic.log import deprecation, error_logger
|
|
||||||
from sanic.utils import load_module_from_file_location, str_to_bool
|
from sanic.utils import load_module_from_file_location, str_to_bool
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # no cov
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
SANIC_PREFIX = "SANIC_"
|
SANIC_PREFIX = "SANIC_"
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"_FALLBACK_ERROR_FORMAT": _default,
|
|
||||||
"ACCESS_LOG": True,
|
"ACCESS_LOG": True,
|
||||||
"AUTO_EXTEND": True,
|
|
||||||
"AUTO_RELOAD": False,
|
"AUTO_RELOAD": False,
|
||||||
"EVENT_AUTOREGISTER": False,
|
"EVENT_AUTOREGISTER": False,
|
||||||
|
"FALLBACK_ERROR_FORMAT": "auto",
|
||||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||||
"FORWARDED_SECRET": None,
|
"FORWARDED_SECRET": None,
|
||||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||||
@@ -38,31 +40,17 @@ DEFAULT_CONFIG = {
|
|||||||
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||||
"REQUEST_TIMEOUT": 60, # 60 seconds
|
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||||
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||||
"USE_UVLOOP": _default,
|
|
||||||
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
||||||
"WEBSOCKET_PING_INTERVAL": 20,
|
"WEBSOCKET_PING_INTERVAL": 20,
|
||||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
# These values will be removed from the Config object in v22.6 and moved
|
|
||||||
# to the application state
|
|
||||||
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
|
|
||||||
|
|
||||||
|
class Config(dict):
|
||||||
class DescriptorMeta(type):
|
|
||||||
def __init__(cls, *_):
|
|
||||||
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_setter(member: object):
|
|
||||||
return isdatadescriptor(member) and hasattr(member, "setter")
|
|
||||||
|
|
||||||
|
|
||||||
class Config(dict, metaclass=DescriptorMeta):
|
|
||||||
ACCESS_LOG: bool
|
ACCESS_LOG: bool
|
||||||
AUTO_EXTEND: bool
|
|
||||||
AUTO_RELOAD: bool
|
AUTO_RELOAD: bool
|
||||||
EVENT_AUTOREGISTER: bool
|
EVENT_AUTOREGISTER: bool
|
||||||
|
FALLBACK_ERROR_FORMAT: str
|
||||||
FORWARDED_FOR_HEADER: str
|
FORWARDED_FOR_HEADER: str
|
||||||
FORWARDED_SECRET: Optional[str]
|
FORWARDED_SECRET: Optional[str]
|
||||||
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
||||||
@@ -81,7 +69,6 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
REQUEST_TIMEOUT: int
|
REQUEST_TIMEOUT: int
|
||||||
RESPONSE_TIMEOUT: int
|
RESPONSE_TIMEOUT: int
|
||||||
SERVER_NAME: str
|
SERVER_NAME: str
|
||||||
USE_UVLOOP: Union[Default, bool]
|
|
||||||
WEBSOCKET_MAX_SIZE: int
|
WEBSOCKET_MAX_SIZE: int
|
||||||
WEBSOCKET_PING_INTERVAL: int
|
WEBSOCKET_PING_INTERVAL: int
|
||||||
WEBSOCKET_PING_TIMEOUT: int
|
WEBSOCKET_PING_TIMEOUT: int
|
||||||
@@ -89,27 +76,33 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
|
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
|
||||||
|
load_env: Optional[Union[bool, str]] = True,
|
||||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
keep_alive: Optional[bool] = None,
|
keep_alive: Optional[bool] = None,
|
||||||
*,
|
*,
|
||||||
converters: Optional[Sequence[Callable[[str], Any]]] = None,
|
app: Optional[Sanic] = None,
|
||||||
):
|
):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
|
|
||||||
self._converters = [str, str_to_bool, float, int]
|
self._app = app
|
||||||
self._LOGO = ""
|
self._LOGO = ""
|
||||||
|
|
||||||
if converters:
|
|
||||||
for converter in converters:
|
|
||||||
self.register_type(converter)
|
|
||||||
|
|
||||||
if keep_alive is not None:
|
if keep_alive is not None:
|
||||||
self.KEEP_ALIVE = keep_alive
|
self.KEEP_ALIVE = keep_alive
|
||||||
|
|
||||||
if env_prefix != SANIC_PREFIX:
|
if env_prefix != SANIC_PREFIX:
|
||||||
if env_prefix:
|
if env_prefix:
|
||||||
self.load_environment_vars(env_prefix)
|
self.load_environment_vars(env_prefix)
|
||||||
|
elif load_env is not True:
|
||||||
|
if load_env:
|
||||||
|
self.load_environment_vars(prefix=load_env)
|
||||||
|
warn(
|
||||||
|
"Use of load_env is deprecated and will be removed in "
|
||||||
|
"21.12. Modify the configuration prefix by passing "
|
||||||
|
"env_prefix instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.load_environment_vars(SANIC_PREFIX)
|
self.load_environment_vars(SANIC_PREFIX)
|
||||||
|
|
||||||
@@ -130,21 +123,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.update({attr: value})
|
self.update({attr: value})
|
||||||
|
|
||||||
def update(self, *other, **kwargs) -> None:
|
def update(self, *other, **kwargs) -> None:
|
||||||
kwargs.update({k: v for item in other for k, v in dict(item).items()})
|
other_mapping = {k: v for item in other for k, v in dict(item).items()}
|
||||||
setters: Dict[str, Any] = {
|
super().update(*other, **kwargs)
|
||||||
k: kwargs.pop(k)
|
for attr, value in {**other_mapping, **kwargs}.items():
|
||||||
for k in {**kwargs}.keys()
|
|
||||||
if k in self.__class__.__setters__
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value in setters.items():
|
|
||||||
try:
|
|
||||||
super().__setattr__(key, value)
|
|
||||||
except AttributeError:
|
|
||||||
...
|
|
||||||
|
|
||||||
super().update(**kwargs)
|
|
||||||
for attr, value in {**setters, **kwargs}.items():
|
|
||||||
self._post_set(attr, value)
|
self._post_set(attr, value)
|
||||||
|
|
||||||
def _post_set(self, attr, value) -> None:
|
def _post_set(self, attr, value) -> None:
|
||||||
@@ -155,37 +136,32 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
"REQUEST_MAX_SIZE",
|
"REQUEST_MAX_SIZE",
|
||||||
):
|
):
|
||||||
self._configure_header_size()
|
self._configure_header_size()
|
||||||
|
elif attr == "FALLBACK_ERROR_FORMAT":
|
||||||
|
self._check_error_format()
|
||||||
|
if self.app and value != self.app.error_handler.fallback:
|
||||||
|
if self.app.error_handler.fallback != "auto":
|
||||||
|
warn(
|
||||||
|
"Overriding non-default ErrorHandler fallback "
|
||||||
|
"value. Changing from "
|
||||||
|
f"{self.app.error_handler.fallback} to {value}."
|
||||||
|
)
|
||||||
|
self.app.error_handler.fallback = value
|
||||||
elif attr == "LOGO":
|
elif attr == "LOGO":
|
||||||
self._LOGO = value
|
self._LOGO = value
|
||||||
deprecation(
|
warn(
|
||||||
"Setting the config.LOGO is deprecated and will no longer "
|
"Setting the config.LOGO is deprecated and will no longer "
|
||||||
"be supported starting in v22.6.",
|
"be supported starting in v22.6.",
|
||||||
22.6,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app(self):
|
||||||
|
return self._app
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def LOGO(self):
|
def LOGO(self):
|
||||||
return self._LOGO
|
return self._LOGO
|
||||||
|
|
||||||
@property
|
|
||||||
def FALLBACK_ERROR_FORMAT(self) -> str:
|
|
||||||
if self._FALLBACK_ERROR_FORMAT is _default:
|
|
||||||
return DEFAULT_FORMAT
|
|
||||||
return self._FALLBACK_ERROR_FORMAT
|
|
||||||
|
|
||||||
@FALLBACK_ERROR_FORMAT.setter
|
|
||||||
def FALLBACK_ERROR_FORMAT(self, value):
|
|
||||||
self._check_error_format(value)
|
|
||||||
if (
|
|
||||||
self._FALLBACK_ERROR_FORMAT is not _default
|
|
||||||
and value != self._FALLBACK_ERROR_FORMAT
|
|
||||||
):
|
|
||||||
error_logger.warning(
|
|
||||||
"Setting config.FALLBACK_ERROR_FORMAT on an already "
|
|
||||||
"configured value may have unintended consequences."
|
|
||||||
)
|
|
||||||
self._FALLBACK_ERROR_FORMAT = value
|
|
||||||
|
|
||||||
def _configure_header_size(self):
|
def _configure_header_size(self):
|
||||||
Http.set_header_max_size(
|
Http.set_header_max_size(
|
||||||
self.REQUEST_MAX_HEADER_SIZE,
|
self.REQUEST_MAX_HEADER_SIZE,
|
||||||
@@ -193,8 +169,8 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.REQUEST_MAX_SIZE,
|
self.REQUEST_MAX_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _check_error_format(self, format: Optional[str] = None):
|
def _check_error_format(self):
|
||||||
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
|
check_error_format(self.FALLBACK_ERROR_FORMAT)
|
||||||
|
|
||||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||||
"""
|
"""
|
||||||
@@ -208,23 +184,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
- ``float``
|
- ``float``
|
||||||
- ``bool``
|
- ``bool``
|
||||||
|
|
||||||
Anything else will be imported as a ``str``. If you would like to add
|
Anything else will be imported as a ``str``.
|
||||||
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>`__
|
|
||||||
"""
|
"""
|
||||||
for key, value in environ.items():
|
for key, value in environ.items():
|
||||||
if not key.startswith(prefix):
|
if not key.startswith(prefix):
|
||||||
@@ -232,7 +192,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
|
|
||||||
_, config_key = key.split(prefix, 1)
|
_, config_key = key.split(prefix, 1)
|
||||||
|
|
||||||
for converter in reversed(self._converters):
|
for converter in (int, float, str_to_bool, str):
|
||||||
try:
|
try:
|
||||||
self[config_key] = converter(value)
|
self[config_key] = converter(value)
|
||||||
break
|
break
|
||||||
@@ -307,17 +267,3 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.update(config)
|
self.update(config)
|
||||||
|
|
||||||
load = update_config
|
load = update_config
|
||||||
|
|
||||||
def register_type(self, converter: Callable[[str], Any]) -> None:
|
|
||||||
"""
|
|
||||||
Allows for adding custom function to cast from a string value to any
|
|
||||||
other type. The function should raise ValueError if it is not the
|
|
||||||
correct type.
|
|
||||||
"""
|
|
||||||
if converter in self._converters:
|
|
||||||
error_logger.warning(
|
|
||||||
f"Configuration value converter '{converter.__name__}' has "
|
|
||||||
"already been registered"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
self._converters.append(converter)
|
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ except ImportError: # noqa
|
|||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_FORMAT = "auto"
|
|
||||||
FALLBACK_TEXT = (
|
FALLBACK_TEXT = (
|
||||||
"The server encountered an internal error and "
|
"The server encountered an internal error and "
|
||||||
"cannot complete your request."
|
"cannot complete your request."
|
||||||
|
|||||||
@@ -244,3 +244,25 @@ class InvalidSignal(SanicException):
|
|||||||
class WebsocketClosed(SanicException):
|
class WebsocketClosed(SanicException):
|
||||||
quiet = True
|
quiet = True
|
||||||
message = "Client has closed the websocket connection"
|
message = "Client has closed the websocket connection"
|
||||||
|
|
||||||
|
|
||||||
|
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
||||||
|
"""
|
||||||
|
Raise an exception based on SanicException. Returns the HTTP response
|
||||||
|
message appropriate for the given status code, unless provided.
|
||||||
|
|
||||||
|
STATUS_CODES from sanic.helpers for the given status code.
|
||||||
|
|
||||||
|
:param status_code: The HTTP status code to return.
|
||||||
|
:param message: The HTTP response body. Defaults to the messages in
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"sanic.exceptions.abort has been marked as deprecated, and will be "
|
||||||
|
"removed in release 21.12.\n To migrate your code, simply replace "
|
||||||
|
"abort(status_code, msg) with raise SanicException(msg, status_code), "
|
||||||
|
"or even better, raise an appropriate SanicException subclass."
|
||||||
|
)
|
||||||
|
|
||||||
|
raise SanicException(message=message, status_code=status_code)
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
from typing import Dict, List, Optional, Tuple, Type, Union
|
from typing import Dict, List, Optional, Tuple, Type
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.config import Config
|
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
|
||||||
from sanic.errorpages import (
|
|
||||||
DEFAULT_FORMAT,
|
|
||||||
BaseRenderer,
|
|
||||||
HTMLRenderer,
|
|
||||||
exception_response,
|
|
||||||
)
|
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
ContentRangeError,
|
ContentRangeError,
|
||||||
HeaderNotFound,
|
HeaderNotFound,
|
||||||
InvalidRangeType,
|
InvalidRangeType,
|
||||||
SanicException,
|
|
||||||
)
|
)
|
||||||
from sanic.helpers import Default, _default
|
from sanic.log import error_logger
|
||||||
from sanic.log import deprecation, error_logger
|
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@@ -37,91 +28,24 @@ class ErrorHandler:
|
|||||||
|
|
||||||
# Beginning in v22.3, the base renderer will be TextRenderer
|
# Beginning in v22.3, the base renderer will be TextRenderer
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer
|
||||||
fallback: Union[str, Default] = _default,
|
|
||||||
base: Type[BaseRenderer] = HTMLRenderer,
|
|
||||||
):
|
):
|
||||||
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
|
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
|
||||||
self.cached_handlers: Dict[
|
self.cached_handlers: Dict[
|
||||||
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
|
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
|
||||||
] = {}
|
] = {}
|
||||||
self.debug = False
|
self.debug = False
|
||||||
self._fallback = fallback
|
self.fallback = fallback
|
||||||
self.base = base
|
self.base = base
|
||||||
|
|
||||||
if fallback is not _default:
|
|
||||||
self._warn_fallback_deprecation()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fallback(self):
|
|
||||||
# This is for backwards compat and can be removed in v22.6
|
|
||||||
if self._fallback is _default:
|
|
||||||
return DEFAULT_FORMAT
|
|
||||||
return self._fallback
|
|
||||||
|
|
||||||
@fallback.setter
|
|
||||||
def fallback(self, value: str):
|
|
||||||
self._warn_fallback_deprecation()
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise SanicException(
|
|
||||||
f"Cannot set error handler fallback to: value={value}"
|
|
||||||
)
|
|
||||||
self._fallback = value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _warn_fallback_deprecation():
|
|
||||||
deprecation(
|
|
||||||
"Setting the ErrorHandler fallback value directly is "
|
|
||||||
"deprecated and no longer supported. This feature will "
|
|
||||||
"be removed in v22.6. Instead, use "
|
|
||||||
"app.config.FALLBACK_ERROR_FORMAT.",
|
|
||||||
22.6,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
|
def finalize(cls, error_handler, fallback: Optional[str] = None):
|
||||||
if error_handler._fallback is not _default:
|
if (
|
||||||
if config._FALLBACK_ERROR_FORMAT is _default:
|
fallback
|
||||||
return error_handler.fallback
|
and fallback != "auto"
|
||||||
|
and error_handler.fallback == "auto"
|
||||||
error_logger.warning(
|
):
|
||||||
"Conflicting error fallback values were found in the "
|
error_handler.fallback = fallback
|
||||||
"error handler and in the app.config while handling an "
|
|
||||||
"exception. Using the value from app.config."
|
|
||||||
)
|
|
||||||
return config.FALLBACK_ERROR_FORMAT
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def finalize(
|
|
||||||
cls,
|
|
||||||
error_handler: ErrorHandler,
|
|
||||||
fallback: Optional[str] = None,
|
|
||||||
config: Optional[Config] = None,
|
|
||||||
):
|
|
||||||
if fallback:
|
|
||||||
deprecation(
|
|
||||||
"Setting the ErrorHandler fallback value via finalize() "
|
|
||||||
"is deprecated and no longer supported. This feature will "
|
|
||||||
"be removed in v22.6. Instead, use "
|
|
||||||
"app.config.FALLBACK_ERROR_FORMAT.",
|
|
||||||
22.6,
|
|
||||||
)
|
|
||||||
|
|
||||||
if config is None:
|
|
||||||
deprecation(
|
|
||||||
"Starting in v22.3, config will be a required argument "
|
|
||||||
"for ErrorHandler.finalize().",
|
|
||||||
22.3,
|
|
||||||
)
|
|
||||||
|
|
||||||
if fallback and fallback != DEFAULT_FORMAT:
|
|
||||||
if error_handler._fallback is not _default:
|
|
||||||
error_logger.warning(
|
|
||||||
f"Setting the fallback value to {fallback}. This changes "
|
|
||||||
"the current non-default value "
|
|
||||||
f"'{error_handler._fallback}'."
|
|
||||||
)
|
|
||||||
error_handler._fallback = fallback
|
|
||||||
|
|
||||||
if not isinstance(error_handler, cls):
|
if not isinstance(error_handler, cls):
|
||||||
error_logger.warning(
|
error_logger.warning(
|
||||||
@@ -130,7 +54,7 @@ class ErrorHandler:
|
|||||||
|
|
||||||
sig = signature(error_handler.lookup)
|
sig = signature(error_handler.lookup)
|
||||||
if len(sig.parameters) == 1:
|
if len(sig.parameters) == 1:
|
||||||
deprecation(
|
warn(
|
||||||
"You are using a deprecated error handler. The lookup "
|
"You are using a deprecated error handler. The lookup "
|
||||||
"method should accept two positional parameters: "
|
"method should accept two positional parameters: "
|
||||||
"(exception, route_name: Optional[str]). "
|
"(exception, route_name: Optional[str]). "
|
||||||
@@ -138,10 +62,9 @@ class ErrorHandler:
|
|||||||
"specific exceptions will not work properly. Beginning "
|
"specific exceptions will not work properly. Beginning "
|
||||||
"in v22.3, the legacy style lookup method will not "
|
"in v22.3, the legacy style lookup method will not "
|
||||||
"work at all.",
|
"work at all.",
|
||||||
22.3,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
legacy_lookup = error_handler._legacy_lookup
|
error_handler._lookup = error_handler._legacy_lookup
|
||||||
error_handler._lookup = legacy_lookup # type: ignore
|
|
||||||
|
|
||||||
def _full_lookup(self, exception, route_name: Optional[str] = None):
|
def _full_lookup(self, exception, route_name: Optional[str] = None):
|
||||||
return self.lookup(exception, route_name)
|
return self.lookup(exception, route_name)
|
||||||
@@ -265,13 +188,12 @@ class ErrorHandler:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
self.log(request, exception)
|
self.log(request, exception)
|
||||||
fallback = ErrorHandler._get_fallback_value(self, request.app.config)
|
|
||||||
return exception_response(
|
return exception_response(
|
||||||
request,
|
request,
|
||||||
exception,
|
exception,
|
||||||
debug=self.debug,
|
debug=self.debug,
|
||||||
base=self.base,
|
base=self.base,
|
||||||
fallback=fallback,
|
fallback=self.fallback,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING:
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import BaseHTTPResponse
|
from sanic.response import BaseHTTPResponse
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import sys
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
|
|
||||||
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
|
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
|
||||||
@@ -79,11 +78,3 @@ access_logger = logging.getLogger("sanic.access")
|
|||||||
"""
|
"""
|
||||||
Logger used by Sanic for access logging
|
Logger used by Sanic for access logging
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def deprecation(message: str, version: float):
|
|
||||||
version_info = f"[DEPRECATION v{version}] "
|
|
||||||
if sys.stdout.isatty():
|
|
||||||
version_info = f"{Colors.RED}{version_info}"
|
|
||||||
message = f"{Colors.YELLOW}{message}{Colors.END}"
|
|
||||||
warn(version_info + message, DeprecationWarning)
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.models.futures import FutureException
|
from sanic.models.futures import FutureException
|
||||||
|
|
||||||
|
|
||||||
class ExceptionMixin(metaclass=SanicMeta):
|
class ExceptionMixin:
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_exceptions: Set[FutureException] = set()
|
self._future_exceptions: Set[FutureException] = set()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from enum import Enum, auto
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.models.futures import FutureListener
|
from sanic.models.futures import FutureListener
|
||||||
from sanic.models.handler_types import ListenerType, Sanic
|
from sanic.models.handler_types import ListenerType, Sanic
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ class ListenerEvent(str, Enum):
|
|||||||
MAIN_PROCESS_STOP = auto()
|
MAIN_PROCESS_STOP = auto()
|
||||||
|
|
||||||
|
|
||||||
class ListenerMixin(metaclass=SanicMeta):
|
class ListenerMixin:
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_listeners: List[FutureListener] = []
|
self._future_listeners: List[FutureListener] = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.models.futures import FutureMiddleware
|
from sanic.models.futures import FutureMiddleware
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareMixin(metaclass=SanicMeta):
|
class MiddlewareMixin:
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_middleware: List[FutureMiddleware] = []
|
self._future_middleware: List[FutureMiddleware] = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from ast import NodeVisitor, Return, parse
|
from ast import NodeVisitor, Return, parse
|
||||||
from contextlib import suppress
|
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
from inspect import getsource, signature
|
from inspect import getsource, signature
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
@@ -13,7 +12,6 @@ from urllib.parse import unquote
|
|||||||
|
|
||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.compat import stat_async
|
from sanic.compat import stat_async
|
||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
||||||
from sanic.errorpages import RESPONSE_MAPPING
|
from sanic.errorpages import RESPONSE_MAPPING
|
||||||
@@ -24,27 +22,19 @@ from sanic.exceptions import (
|
|||||||
InvalidUsage,
|
InvalidUsage,
|
||||||
)
|
)
|
||||||
from sanic.handlers import ContentRangeHandler
|
from sanic.handlers import ContentRangeHandler
|
||||||
from sanic.log import deprecation, error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.models.futures import FutureRoute, FutureStatic
|
from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
from sanic.response import HTTPResponse, file, file_stream
|
from sanic.response import HTTPResponse, file, file_stream
|
||||||
from sanic.types import HashableDict
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
|
|
||||||
RouteWrapper = Callable[
|
RouteWrapper = Callable[
|
||||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||||
]
|
]
|
||||||
RESTRICTED_ROUTE_CONTEXT = (
|
|
||||||
"ignore_body",
|
|
||||||
"stream",
|
|
||||||
"hosts",
|
|
||||||
"static",
|
|
||||||
"error_format",
|
|
||||||
"websocket",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RouteMixin(metaclass=SanicMeta):
|
class RouteMixin:
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
@@ -75,20 +65,10 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
static: bool = False,
|
static: bool = False,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs: Any,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a route
|
Decorate a function to be registered as a route
|
||||||
|
|
||||||
|
|
||||||
**Example using context kwargs**
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@app.route(..., ctx_foo="foobar")
|
|
||||||
async def route_handler(request: Request):
|
|
||||||
assert request.route.ctx.foo == "foobar"
|
|
||||||
|
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
:param methods: list or tuple of methods allowed
|
:param methods: list or tuple of methods allowed
|
||||||
:param host: the host, if required
|
:param host: the host, if required
|
||||||
@@ -100,8 +80,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
body (eg. GET requests)
|
body (eg. GET requests)
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: tuple of routes, decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -116,8 +94,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
if not methods and not websocket:
|
if not methods and not websocket:
|
||||||
methods = frozenset({"GET"})
|
methods = frozenset({"GET"})
|
||||||
|
|
||||||
route_context = self._build_route_context(ctx_kwargs)
|
|
||||||
|
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
nonlocal uri
|
nonlocal uri
|
||||||
nonlocal methods
|
nonlocal methods
|
||||||
@@ -176,7 +152,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
static,
|
static,
|
||||||
version_prefix,
|
version_prefix,
|
||||||
error_format,
|
error_format,
|
||||||
route_context,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._future_routes.add(route)
|
self._future_routes.add(route)
|
||||||
@@ -221,7 +196,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""A helper method to register class instance or
|
"""A helper method to register class instance or
|
||||||
functions as a handler to the application url
|
functions as a handler to the application url
|
||||||
@@ -238,8 +212,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:param stream: boolean specifying if the handler is a stream handler
|
:param stream: boolean specifying if the handler is a stream handler
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: function or class instance
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
# Handle HTTPMethodView differently
|
# Handle HTTPMethodView differently
|
||||||
@@ -254,6 +226,14 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
if hasattr(_handler, "is_stream"):
|
if hasattr(_handler, "is_stream"):
|
||||||
stream = True
|
stream = True
|
||||||
|
|
||||||
|
# handle composition view differently
|
||||||
|
if isinstance(handler, CompositionView):
|
||||||
|
methods = handler.handlers.keys()
|
||||||
|
for _handler in handler.handlers.values():
|
||||||
|
if hasattr(_handler, "is_stream"):
|
||||||
|
stream = True
|
||||||
|
break
|
||||||
|
|
||||||
if strict_slashes is None:
|
if strict_slashes is None:
|
||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
|
|
||||||
@@ -267,7 +247,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)(handler)
|
)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
@@ -282,7 +261,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **GET** *HTTP* method
|
Add an API URL under the **GET** *HTTP* method
|
||||||
@@ -295,8 +273,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:param name: Unique name that can be used to identify the Route
|
:param name: Unique name that can be used to identify the Route
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -309,7 +285,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def post(
|
def post(
|
||||||
@@ -322,7 +297,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **POST** *HTTP* method
|
Add an API URL under the **POST** *HTTP* method
|
||||||
@@ -335,8 +309,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:param name: Unique name that can be used to identify the Route
|
:param name: Unique name that can be used to identify the Route
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -349,7 +321,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def put(
|
def put(
|
||||||
@@ -362,7 +333,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PUT** *HTTP* method
|
Add an API URL under the **PUT** *HTTP* method
|
||||||
@@ -375,8 +345,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:param name: Unique name that can be used to identify the Route
|
:param name: Unique name that can be used to identify the Route
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -389,7 +357,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def head(
|
def head(
|
||||||
@@ -402,7 +369,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **HEAD** *HTTP* method
|
Add an API URL under the **HEAD** *HTTP* method
|
||||||
@@ -423,8 +389,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:type ignore_body: bool, optional
|
:type ignore_body: bool, optional
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -437,7 +401,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def options(
|
def options(
|
||||||
@@ -450,7 +413,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **OPTIONS** *HTTP* method
|
Add an API URL under the **OPTIONS** *HTTP* method
|
||||||
@@ -471,8 +433,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:type ignore_body: bool, optional
|
:type ignore_body: bool, optional
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -485,7 +445,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def patch(
|
def patch(
|
||||||
@@ -498,7 +457,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PATCH** *HTTP* method
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
@@ -521,8 +479,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:type ignore_body: bool, optional
|
:type ignore_body: bool, optional
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -535,7 +491,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
@@ -548,7 +503,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **DELETE** *HTTP* method
|
Add an API URL under the **DELETE** *HTTP* method
|
||||||
@@ -561,8 +515,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:param name: Unique name that can be used to identify the Route
|
:param name: Unique name that can be used to identify the Route
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -575,7 +527,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def websocket(
|
def websocket(
|
||||||
@@ -589,7 +540,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a websocket route
|
Decorate a function to be registered as a websocket route
|
||||||
@@ -603,8 +553,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
be used with :func:`url_for`
|
be used with :func:`url_for`
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: tuple of routes, decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -619,7 +567,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
websocket=True,
|
websocket=True,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_websocket_route(
|
def add_websocket_route(
|
||||||
@@ -633,7 +580,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
A helper method to register a function as a websocket route.
|
A helper method to register a function as a websocket route.
|
||||||
@@ -652,8 +598,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
be used with :func:`url_for`
|
be used with :func:`url_for`
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Objected decorated by :func:`websocket`
|
:return: Objected decorated by :func:`websocket`
|
||||||
"""
|
"""
|
||||||
return self.websocket(
|
return self.websocket(
|
||||||
@@ -665,7 +609,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)(handler)
|
)(handler)
|
||||||
|
|
||||||
def static(
|
def static(
|
||||||
@@ -975,16 +918,19 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
return route
|
return route
|
||||||
|
|
||||||
def _determine_error_format(self, handler) -> str:
|
def _determine_error_format(self, handler) -> Optional[str]:
|
||||||
with suppress(OSError, TypeError):
|
if not isinstance(handler, CompositionView):
|
||||||
src = dedent(getsource(handler))
|
try:
|
||||||
tree = parse(src)
|
src = dedent(getsource(handler))
|
||||||
http_response_types = self._get_response_types(tree)
|
tree = parse(src)
|
||||||
|
http_response_types = self._get_response_types(tree)
|
||||||
|
|
||||||
if len(http_response_types) == 1:
|
if len(http_response_types) == 1:
|
||||||
return next(iter(http_response_types))
|
return next(iter(http_response_types))
|
||||||
|
except (OSError, TypeError):
|
||||||
|
...
|
||||||
|
|
||||||
return ""
|
return None
|
||||||
|
|
||||||
def _get_response_types(self, node):
|
def _get_response_types(self, node):
|
||||||
types = set()
|
types = set()
|
||||||
@@ -993,18 +939,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
def visit_Return(self, node: Return) -> Any:
|
def visit_Return(self, node: Return) -> Any:
|
||||||
nonlocal types
|
nonlocal types
|
||||||
|
|
||||||
with suppress(AttributeError):
|
try:
|
||||||
if node.value.func.id == "stream": # type: ignore
|
|
||||||
deprecation(
|
|
||||||
"The sanic.response.stream method has been "
|
|
||||||
"deprecated and will be removed in v22.6. Please "
|
|
||||||
"upgrade your application to use the new style "
|
|
||||||
"streaming pattern. See "
|
|
||||||
"https://sanicframework.org/en/guide/advanced/"
|
|
||||||
"streaming.html#response-streaming for more "
|
|
||||||
"information.",
|
|
||||||
22.6,
|
|
||||||
)
|
|
||||||
checks = [node.value.func.id] # type: ignore
|
checks = [node.value.func.id] # type: ignore
|
||||||
if node.value.keywords: # type: ignore
|
if node.value.keywords: # type: ignore
|
||||||
checks += [
|
checks += [
|
||||||
@@ -1016,32 +951,9 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
for check in checks:
|
for check in checks:
|
||||||
if check in RESPONSE_MAPPING:
|
if check in RESPONSE_MAPPING:
|
||||||
types.add(RESPONSE_MAPPING[check])
|
types.add(RESPONSE_MAPPING[check])
|
||||||
|
except AttributeError:
|
||||||
|
...
|
||||||
|
|
||||||
HttpResponseVisitor().visit(node)
|
HttpResponseVisitor().visit(node)
|
||||||
|
|
||||||
return types
|
return types
|
||||||
|
|
||||||
def _build_route_context(self, raw):
|
|
||||||
ctx_kwargs = {
|
|
||||||
key.replace("ctx_", ""): raw.pop(key)
|
|
||||||
for key in {**raw}.keys()
|
|
||||||
if key.startswith("ctx_")
|
|
||||||
}
|
|
||||||
restricted = [
|
|
||||||
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
|
|
||||||
]
|
|
||||||
if restricted:
|
|
||||||
restricted_arguments = ", ".join(restricted)
|
|
||||||
raise AttributeError(
|
|
||||||
"Cannot use restricted route context: "
|
|
||||||
f"{restricted_arguments}. This limitation is only in place "
|
|
||||||
"until v22.3 when the restricted names will no longer be in"
|
|
||||||
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
|
|
||||||
"for more information."
|
|
||||||
)
|
|
||||||
if raw:
|
|
||||||
unexpected_arguments = ", ".join(raw.keys())
|
|
||||||
raise TypeError(
|
|
||||||
f"Unexpected keyword arguments: {unexpected_arguments}"
|
|
||||||
)
|
|
||||||
return HashableDict(ctx_kwargs)
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Callable, Dict, Optional, Set, Union
|
from typing import Any, Callable, Dict, Optional, Set, Union
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.models.futures import FutureSignal
|
from sanic.models.futures import FutureSignal
|
||||||
from sanic.models.handler_types import SignalHandler
|
from sanic.models.handler_types import SignalHandler
|
||||||
from sanic.signals import Signal
|
from sanic.signals import Signal
|
||||||
from sanic.types import HashableDict
|
|
||||||
|
|
||||||
|
|
||||||
class SignalMixin(metaclass=SanicMeta):
|
class HashableDict(dict):
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(tuple(sorted(self.items())))
|
||||||
|
|
||||||
|
|
||||||
|
class SignalMixin:
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_signals: Set[FutureSignal] = set()
|
self._future_signals: Set[FutureSignal] = set()
|
||||||
|
|
||||||
@@ -21,7 +24,6 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
*,
|
*,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Dict[str, Any] = None,
|
||||||
exclusive: bool = True,
|
|
||||||
) -> Callable[[SignalHandler], SignalHandler]:
|
) -> Callable[[SignalHandler], SignalHandler]:
|
||||||
"""
|
"""
|
||||||
For creating a signal handler, used similar to a route handler:
|
For creating a signal handler, used similar to a route handler:
|
||||||
@@ -34,22 +36,17 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
:param event: Representation of the event in ``one.two.three`` form
|
:param event: Representation of the event in ``one.two.three`` form
|
||||||
:type event: str
|
:type event: str
|
||||||
:param apply: For lazy evaluation, defaults to ``True``
|
:param apply: For lazy evaluation, defaults to True
|
||||||
:type apply: bool, optional
|
:type apply: bool, optional
|
||||||
:param condition: For use with the ``condition`` argument in dispatch
|
:param condition: For use with the ``condition`` argument in dispatch
|
||||||
filtering, defaults to ``None``
|
filtering, defaults to None
|
||||||
:param exclusive: When ``True``, the signal can only be dispatched
|
|
||||||
when the condition has been met. When ``False``, the signal can
|
|
||||||
be dispatched either with or without it. *THIS IS INAPPLICABLE TO
|
|
||||||
BLUEPRINT SIGNALS. THEY ARE ALWAYS NON-EXCLUSIVE*, defaults
|
|
||||||
to ``True``
|
|
||||||
:type condition: Dict[str, Any], optional
|
:type condition: Dict[str, Any], optional
|
||||||
"""
|
"""
|
||||||
event_value = str(event.value) if isinstance(event, Enum) else event
|
event_value = str(event.value) if isinstance(event, Enum) else event
|
||||||
|
|
||||||
def decorator(handler: SignalHandler):
|
def decorator(handler: SignalHandler):
|
||||||
future_signal = FutureSignal(
|
future_signal = FutureSignal(
|
||||||
handler, event_value, HashableDict(condition or {}), exclusive
|
handler, event_value, HashableDict(condition or {})
|
||||||
)
|
)
|
||||||
self._future_signals.add(future_signal)
|
self._future_signals.add(future_signal)
|
||||||
|
|
||||||
@@ -65,7 +62,6 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
handler: Optional[Callable[..., Any]],
|
handler: Optional[Callable[..., Any]],
|
||||||
event: str,
|
event: str,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Dict[str, Any] = None,
|
||||||
exclusive: bool = True,
|
|
||||||
):
|
):
|
||||||
if not handler:
|
if not handler:
|
||||||
|
|
||||||
@@ -73,9 +69,7 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
...
|
...
|
||||||
|
|
||||||
handler = noop
|
handler = noop
|
||||||
self.signal(event=event, condition=condition, exclusive=exclusive)(
|
self.signal(event=event, condition=condition)(handler)
|
||||||
handler
|
|
||||||
)
|
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def event(self, event: str):
|
def event(self, event: str):
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from sanic.models.handler_types import (
|
|||||||
MiddlewareType,
|
MiddlewareType,
|
||||||
SignalHandler,
|
SignalHandler,
|
||||||
)
|
)
|
||||||
from sanic.types import HashableDict
|
|
||||||
|
|
||||||
|
|
||||||
class FutureRoute(NamedTuple):
|
class FutureRoute(NamedTuple):
|
||||||
@@ -26,7 +25,6 @@ class FutureRoute(NamedTuple):
|
|||||||
static: bool
|
static: bool
|
||||||
version_prefix: str
|
version_prefix: str
|
||||||
error_format: Optional[str]
|
error_format: Optional[str]
|
||||||
route_context: HashableDict
|
|
||||||
|
|
||||||
|
|
||||||
class FutureListener(NamedTuple):
|
class FutureListener(NamedTuple):
|
||||||
@@ -62,7 +60,6 @@ class FutureSignal(NamedTuple):
|
|||||||
handler: SignalHandler
|
handler: SignalHandler
|
||||||
event: str
|
event: str
|
||||||
condition: Optional[Dict[str, str]]
|
condition: Optional[Dict[str, str]]
|
||||||
exclusive: bool
|
|
||||||
|
|
||||||
|
|
||||||
class FutureRegistry(set):
|
class FutureRegistry(set):
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from typing import (
|
|||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING:
|
||||||
from sanic.server import ConnInfo
|
from sanic.server import ConnInfo
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
@@ -14,10 +12,10 @@ from typing import (
|
|||||||
Iterator,
|
Iterator,
|
||||||
Optional,
|
Optional,
|
||||||
Tuple,
|
Tuple,
|
||||||
TypeVar,
|
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.compat import Header, open_async
|
from sanic.compat import Header, open_async
|
||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||||
@@ -30,10 +28,6 @@ from sanic.models.protocol_types import HTMLProtocol, Range
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic.asgi import ASGIApp
|
from sanic.asgi import ASGIApp
|
||||||
from sanic.request import Request
|
|
||||||
else:
|
|
||||||
Request = TypeVar("Request")
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ujson import dumps as json_dumps
|
from ujson import dumps as json_dumps
|
||||||
@@ -142,6 +136,95 @@ class BaseHTTPResponse:
|
|||||||
await self.stream.send(data, end_stream=end_stream)
|
await self.stream.send(data, end_stream=end_stream)
|
||||||
|
|
||||||
|
|
||||||
|
StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]]
|
||||||
|
|
||||||
|
|
||||||
|
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
|
"""
|
||||||
|
Old style streaming response where you pass a streaming function:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def sample_streaming_fn(response):
|
||||||
|
await response.write("foo")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await response.write("bar")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
@app.post("/")
|
||||||
|
async def test(request):
|
||||||
|
return stream(sample_streaming_fn)
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
**Deprecated** and set for removal in v21.12. You can now achieve the
|
||||||
|
same functionality without a callback.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.post("/")
|
||||||
|
async def test(request):
|
||||||
|
response = await request.respond()
|
||||||
|
await response.send("foo", False)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await response.send("bar", False)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await response.send("", True)
|
||||||
|
return response
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"streaming_fn",
|
||||||
|
"status",
|
||||||
|
"content_type",
|
||||||
|
"headers",
|
||||||
|
"_cookies",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
streaming_fn: StreamingFunction,
|
||||||
|
status: int = 200,
|
||||||
|
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||||
|
content_type: str = "text/plain; charset=utf-8",
|
||||||
|
ignore_deprecation_notice: bool = False,
|
||||||
|
):
|
||||||
|
if not ignore_deprecation_notice:
|
||||||
|
warn(
|
||||||
|
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
|
||||||
|
"will be removed in v21.12. Please upgrade your streaming "
|
||||||
|
"response implementation. You can learn more here: "
|
||||||
|
"https://sanicframework.org/en/guide/advanced/streaming.html"
|
||||||
|
"#response-streaming. If you use the builtin stream() or "
|
||||||
|
"file_stream() methods, this upgrade will be be done for you."
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.content_type = content_type
|
||||||
|
self.streaming_fn = streaming_fn
|
||||||
|
self.status = status
|
||||||
|
self.headers = Header(headers or {})
|
||||||
|
self._cookies = None
|
||||||
|
|
||||||
|
async def write(self, data):
|
||||||
|
"""Writes a chunk of data to the streaming response.
|
||||||
|
|
||||||
|
:param data: str or bytes-ish data to be written.
|
||||||
|
"""
|
||||||
|
await super().send(self._encode_body(data))
|
||||||
|
|
||||||
|
async def send(self, *args, **kwargs):
|
||||||
|
if self.streaming_fn is not None:
|
||||||
|
await self.streaming_fn(self)
|
||||||
|
self.streaming_fn = None
|
||||||
|
await super().send(*args, **kwargs)
|
||||||
|
|
||||||
|
async def eof(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class HTTPResponse(BaseHTTPResponse):
|
class HTTPResponse(BaseHTTPResponse):
|
||||||
"""
|
"""
|
||||||
HTTP response to be sent back to the client.
|
HTTP response to be sent back to the client.
|
||||||
@@ -336,109 +419,6 @@ async def file(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def redirect(
|
|
||||||
to: str,
|
|
||||||
headers: Optional[Dict[str, str]] = None,
|
|
||||||
status: int = 302,
|
|
||||||
content_type: str = "text/html; charset=utf-8",
|
|
||||||
) -> HTTPResponse:
|
|
||||||
"""
|
|
||||||
Abort execution and cause a 302 redirect (by default) by setting a
|
|
||||||
Location header.
|
|
||||||
|
|
||||||
:param to: path or fully qualified URL to redirect to
|
|
||||||
:param headers: optional dict of headers to include in the new request
|
|
||||||
:param status: status code (int) of the new request, defaults to 302
|
|
||||||
:param content_type: the content type (string) of the response
|
|
||||||
"""
|
|
||||||
headers = headers or {}
|
|
||||||
|
|
||||||
# URL Quote the URL before redirecting
|
|
||||||
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
|
|
||||||
|
|
||||||
# According to RFC 7231, a relative URI is now permitted.
|
|
||||||
headers["Location"] = safe_to
|
|
||||||
|
|
||||||
return HTTPResponse(
|
|
||||||
status=status, headers=headers, content_type=content_type
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseStream:
|
|
||||||
"""
|
|
||||||
ResponseStream is a compat layer to bridge the gap after the deprecation
|
|
||||||
of StreamingHTTPResponse. In v22.6 it will be removed when:
|
|
||||||
- stream is removed
|
|
||||||
- file_stream is moved to new style streaming
|
|
||||||
- file and file_stream are combined into a single API
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"_cookies",
|
|
||||||
"content_type",
|
|
||||||
"headers",
|
|
||||||
"request",
|
|
||||||
"response",
|
|
||||||
"status",
|
|
||||||
"streaming_fn",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
streaming_fn: Callable[
|
|
||||||
[Union[BaseHTTPResponse, ResponseStream]],
|
|
||||||
Coroutine[Any, Any, None],
|
|
||||||
],
|
|
||||||
status: int = 200,
|
|
||||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
|
||||||
content_type: Optional[str] = None,
|
|
||||||
):
|
|
||||||
self.streaming_fn = streaming_fn
|
|
||||||
self.status = status
|
|
||||||
self.headers = headers or Header()
|
|
||||||
self.content_type = content_type
|
|
||||||
self.request: Optional[Request] = None
|
|
||||||
self._cookies: Optional[CookieJar] = None
|
|
||||||
|
|
||||||
async def write(self, message: str):
|
|
||||||
await self.response.send(message)
|
|
||||||
|
|
||||||
async def stream(self) -> HTTPResponse:
|
|
||||||
if not self.request:
|
|
||||||
raise ServerError("Attempted response to unknown request")
|
|
||||||
self.response = await self.request.respond(
|
|
||||||
headers=self.headers,
|
|
||||||
status=self.status,
|
|
||||||
content_type=self.content_type,
|
|
||||||
)
|
|
||||||
await self.streaming_fn(self)
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
async def eof(self) -> None:
|
|
||||||
await self.response.eof()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cookies(self) -> CookieJar:
|
|
||||||
if self._cookies is None:
|
|
||||||
self._cookies = CookieJar(self.headers)
|
|
||||||
return self._cookies
|
|
||||||
|
|
||||||
@property
|
|
||||||
def processed_headers(self):
|
|
||||||
return self.response.processed_headers
|
|
||||||
|
|
||||||
@property
|
|
||||||
def body(self):
|
|
||||||
return self.response.body
|
|
||||||
|
|
||||||
def __call__(self, request: Request) -> ResponseStream:
|
|
||||||
self.request = request
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __await__(self):
|
|
||||||
return self.stream().__await__()
|
|
||||||
|
|
||||||
|
|
||||||
async def file_stream(
|
async def file_stream(
|
||||||
location: Union[str, PurePath],
|
location: Union[str, PurePath],
|
||||||
status: int = 200,
|
status: int = 200,
|
||||||
@@ -447,7 +427,7 @@ async def file_stream(
|
|||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
filename: Optional[str] = None,
|
filename: Optional[str] = None,
|
||||||
_range: Optional[Range] = None,
|
_range: Optional[Range] = None,
|
||||||
) -> ResponseStream:
|
) -> StreamingHTTPResponse:
|
||||||
"""Return a streaming response object with file data.
|
"""Return a streaming response object with file data.
|
||||||
|
|
||||||
:param location: Location of file on system.
|
:param location: Location of file on system.
|
||||||
@@ -455,6 +435,7 @@ async def file_stream(
|
|||||||
:param mime_type: Specific mime_type.
|
:param mime_type: Specific mime_type.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param filename: Override filename.
|
:param filename: Override filename.
|
||||||
|
:param chunked: Deprecated
|
||||||
:param _range:
|
:param _range:
|
||||||
"""
|
"""
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
@@ -490,24 +471,23 @@ async def file_stream(
|
|||||||
break
|
break
|
||||||
await response.write(content)
|
await response.write(content)
|
||||||
|
|
||||||
return ResponseStream(
|
return StreamingHTTPResponse(
|
||||||
streaming_fn=_streaming_fn,
|
streaming_fn=_streaming_fn,
|
||||||
status=status,
|
status=status,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=mime_type,
|
content_type=mime_type,
|
||||||
|
ignore_deprecation_notice=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def stream(
|
def stream(
|
||||||
streaming_fn: Callable[
|
streaming_fn: StreamingFunction,
|
||||||
[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None]
|
|
||||||
],
|
|
||||||
status: int = 200,
|
status: int = 200,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
content_type: str = "text/plain; charset=utf-8",
|
content_type: str = "text/plain; charset=utf-8",
|
||||||
) -> ResponseStream:
|
):
|
||||||
"""Accepts a coroutine `streaming_fn` which can be used to
|
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||||
write chunks to a streaming response. Returns a `ResponseStream`.
|
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||||
|
|
||||||
Example usage::
|
Example usage::
|
||||||
|
|
||||||
@@ -521,13 +501,42 @@ def stream(
|
|||||||
|
|
||||||
:param streaming_fn: A coroutine accepts a response and
|
:param streaming_fn: A coroutine accepts a response and
|
||||||
writes content to that response.
|
writes content to that response.
|
||||||
:param status: HTTP status.
|
:param mime_type: Specific mime_type.
|
||||||
:param content_type: Specific content_type.
|
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
|
:param chunked: Deprecated
|
||||||
"""
|
"""
|
||||||
return ResponseStream(
|
return StreamingHTTPResponse(
|
||||||
streaming_fn,
|
streaming_fn,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
status=status,
|
status=status,
|
||||||
|
ignore_deprecation_notice=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def redirect(
|
||||||
|
to: str,
|
||||||
|
headers: Optional[Dict[str, str]] = None,
|
||||||
|
status: int = 302,
|
||||||
|
content_type: str = "text/html; charset=utf-8",
|
||||||
|
) -> HTTPResponse:
|
||||||
|
"""
|
||||||
|
Abort execution and cause a 302 redirect (by default) by setting a
|
||||||
|
Location header.
|
||||||
|
|
||||||
|
:param to: path or fully qualified URL to redirect to
|
||||||
|
:param headers: optional dict of headers to include in the new request
|
||||||
|
:param status: status code (int) of the new request, defaults to 302
|
||||||
|
:param content_type: the content type (string) of the response
|
||||||
|
"""
|
||||||
|
headers = headers or {}
|
||||||
|
|
||||||
|
# URL Quote the URL before redirecting
|
||||||
|
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
|
||||||
|
|
||||||
|
# According to RFC 7231, a relative URI is now permitted.
|
||||||
|
headers["Location"] = safe_to
|
||||||
|
|
||||||
|
return HTTPResponse(
|
||||||
|
status=status, headers=headers, content_type=content_type
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
from sanic.models.server_types import ConnInfo, Signal
|
from sanic.models.server_types import ConnInfo, Signal
|
||||||
from sanic.server.async_server import AsyncioServer
|
from sanic.server.async_server import AsyncioServer
|
||||||
from sanic.server.loop import try_use_uvloop
|
|
||||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
from sanic.server.protocols.http_protocol import HttpProtocol
|
||||||
from sanic.server.runners import serve, serve_multiple, serve_single
|
from sanic.server.runners import serve, serve_multiple, serve_single
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import uvloop # type: ignore
|
||||||
|
|
||||||
|
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"AsyncioServer",
|
"AsyncioServer",
|
||||||
"ConnInfo",
|
"ConnInfo",
|
||||||
@@ -13,5 +23,4 @@ __all__ = (
|
|||||||
"serve",
|
"serve",
|
||||||
"serve_multiple",
|
"serve_multiple",
|
||||||
"serve_single",
|
"serve_single",
|
||||||
"try_use_uvloop",
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.log import deprecation
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -37,10 +37,10 @@ class AsyncioServer:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def init(self):
|
def init(self):
|
||||||
deprecation(
|
warn(
|
||||||
"AsyncioServer.init has been deprecated and will be removed "
|
"AsyncioServer.init has been deprecated and will be removed "
|
||||||
"in v22.6. Use Sanic.state.is_started instead.",
|
"in v22.6. Use Sanic.state.is_started instead.",
|
||||||
22.6,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
return self.app.state.is_started
|
return self.app.state.is_started
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
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())
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
from typing import TYPE_CHECKING, Optional, Sequence, cast
|
from typing import TYPE_CHECKING, Optional, Sequence, cast
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from websockets.connection import CLOSED, CLOSING, OPEN
|
from websockets.connection import CLOSED, CLOSING, OPEN
|
||||||
from websockets.server import ServerConnection
|
from websockets.server import ServerConnection
|
||||||
from websockets.typing import Subprotocol
|
from websockets.typing import Subprotocol
|
||||||
|
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.log import deprecation, error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.server import HttpProtocol
|
from sanic.server import HttpProtocol
|
||||||
|
|
||||||
from ..websockets.impl import WebsocketImplProtocol
|
from ..websockets.impl import WebsocketImplProtocol
|
||||||
@@ -16,14 +17,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class WebSocketProtocol(HttpProtocol):
|
class WebSocketProtocol(HttpProtocol):
|
||||||
__slots__ = (
|
|
||||||
"websocket",
|
|
||||||
"websocket_timeout",
|
|
||||||
"websocket_max_size",
|
|
||||||
"websocket_ping_interval",
|
|
||||||
"websocket_ping_timeout",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args,
|
*args,
|
||||||
@@ -42,24 +35,24 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
self.websocket_max_size = websocket_max_size
|
self.websocket_max_size = websocket_max_size
|
||||||
if websocket_max_queue is not None and websocket_max_queue > 0:
|
if websocket_max_queue is not None and websocket_max_queue > 0:
|
||||||
# TODO: Reminder remove this warning in v22.3
|
# TODO: Reminder remove this warning in v22.3
|
||||||
deprecation(
|
warn(
|
||||||
"Websocket no longer uses queueing, so websocket_max_queue"
|
"Websocket no longer uses queueing, so websocket_max_queue"
|
||||||
" is no longer required.",
|
" is no longer required.",
|
||||||
22.3,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
if websocket_read_limit is not None and websocket_read_limit > 0:
|
if websocket_read_limit is not None and websocket_read_limit > 0:
|
||||||
# TODO: Reminder remove this warning in v22.3
|
# TODO: Reminder remove this warning in v22.3
|
||||||
deprecation(
|
warn(
|
||||||
"Websocket no longer uses read buffers, so "
|
"Websocket no longer uses read buffers, so "
|
||||||
"websocket_read_limit is not required.",
|
"websocket_read_limit is not required.",
|
||||||
22.3,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
if websocket_write_limit is not None and websocket_write_limit > 0:
|
if websocket_write_limit is not None and websocket_write_limit > 0:
|
||||||
# TODO: Reminder remove this warning in v22.3
|
# TODO: Reminder remove this warning in v22.3
|
||||||
deprecation(
|
warn(
|
||||||
"Websocket no longer uses write buffers, so "
|
"Websocket no longer uses write buffers, so "
|
||||||
"websocket_write_limit is not required.",
|
"websocket_write_limit is not required.",
|
||||||
22.3,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
self.websocket_ping_interval = websocket_ping_interval
|
self.websocket_ping_interval = websocket_ping_interval
|
||||||
self.websocket_ping_timeout = websocket_ping_timeout
|
self.websocket_ping_timeout = websocket_ping_timeout
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from ssl import SSLContext
|
from ssl import SSLContext
|
||||||
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
||||||
|
|
||||||
@@ -21,7 +19,6 @@ from functools import partial
|
|||||||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||||
from signal import signal as signal_func
|
from signal import signal as signal_func
|
||||||
|
|
||||||
from sanic.application.ext import setup_ext
|
|
||||||
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import error_logger, logger
|
||||||
from sanic.models.server_types import Signal
|
from sanic.models.server_types import Signal
|
||||||
@@ -117,7 +114,6 @@ def serve(
|
|||||||
**asyncio_server_kwargs,
|
**asyncio_server_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
setup_ext(app)
|
|
||||||
if run_async:
|
if run_async:
|
||||||
return AsyncioServer(
|
return AsyncioServer(
|
||||||
app=app,
|
app=app,
|
||||||
@@ -178,9 +174,6 @@ def serve(
|
|||||||
loop.run_until_complete(asyncio.sleep(0.1))
|
loop.run_until_complete(asyncio.sleep(0.1))
|
||||||
start_shutdown = start_shutdown + 0.1
|
start_shutdown = start_shutdown + 0.1
|
||||||
|
|
||||||
if sys.version_info > (3, 7):
|
|
||||||
app.shutdown_tasks(graceful - start_shutdown)
|
|
||||||
|
|
||||||
# Force close non-idle connection after waiting for
|
# Force close non-idle connection after waiting for
|
||||||
# graceful_shutdown_timeout
|
# graceful_shutdown_timeout
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union, cast
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
|
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
|
||||||
from sanic_routing.exceptions import NotFound # type: ignore
|
from sanic_routing.exceptions import NotFound # type: ignore
|
||||||
@@ -142,21 +142,12 @@ class SignalRouter(BaseRouter):
|
|||||||
if context:
|
if context:
|
||||||
params.update(context)
|
params.update(context)
|
||||||
|
|
||||||
signals = group.routes
|
|
||||||
if not reverse:
|
if not reverse:
|
||||||
signals = signals[::-1]
|
handlers = handlers[::-1]
|
||||||
try:
|
try:
|
||||||
for signal in signals:
|
for handler in handlers:
|
||||||
params.pop("__trigger__", None)
|
if condition is None or condition == handler.__requirements__:
|
||||||
if (
|
maybe_coroutine = handler(**params)
|
||||||
(condition is None and signal.ctx.exclusive is False)
|
|
||||||
or (
|
|
||||||
condition is None
|
|
||||||
and not signal.handler.__requirements__
|
|
||||||
)
|
|
||||||
or (condition == signal.handler.__requirements__)
|
|
||||||
) and (signal.ctx.trigger or event == signal.ctx.definition):
|
|
||||||
maybe_coroutine = signal.handler(**params)
|
|
||||||
if isawaitable(maybe_coroutine):
|
if isawaitable(maybe_coroutine):
|
||||||
retval = await maybe_coroutine
|
retval = await maybe_coroutine
|
||||||
if retval:
|
if retval:
|
||||||
@@ -199,36 +190,23 @@ class SignalRouter(BaseRouter):
|
|||||||
handler: SignalHandler,
|
handler: SignalHandler,
|
||||||
event: str,
|
event: str,
|
||||||
condition: Optional[Dict[str, Any]] = None,
|
condition: Optional[Dict[str, Any]] = None,
|
||||||
exclusive: bool = True,
|
|
||||||
) -> Signal:
|
) -> Signal:
|
||||||
event_definition = event
|
|
||||||
parts = self._build_event_parts(event)
|
parts = self._build_event_parts(event)
|
||||||
if parts[2].startswith("<"):
|
if parts[2].startswith("<"):
|
||||||
name = ".".join([*parts[:-1], "*"])
|
name = ".".join([*parts[:-1], "*"])
|
||||||
trigger = self._clean_trigger(parts[2])
|
|
||||||
else:
|
else:
|
||||||
name = event
|
name = event
|
||||||
trigger = ""
|
|
||||||
|
|
||||||
if not trigger:
|
|
||||||
event = ".".join([*parts[:2], "<__trigger__>"])
|
|
||||||
|
|
||||||
handler.__requirements__ = condition # type: ignore
|
handler.__requirements__ = condition # type: ignore
|
||||||
handler.__trigger__ = trigger # type: ignore
|
|
||||||
|
|
||||||
signal = super().add(
|
return super().add(
|
||||||
event,
|
event,
|
||||||
handler,
|
handler,
|
||||||
|
requirements=condition,
|
||||||
name=name,
|
name=name,
|
||||||
append=True,
|
append=True,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
signal.ctx.exclusive = exclusive
|
|
||||||
signal.ctx.trigger = trigger
|
|
||||||
signal.ctx.definition = event_definition
|
|
||||||
|
|
||||||
return cast(Signal, signal)
|
|
||||||
|
|
||||||
def finalize(self, do_compile: bool = True, do_optimize: bool = False):
|
def finalize(self, do_compile: bool = True, do_optimize: bool = False):
|
||||||
self.add(_blank, "sanic.__signal__.__init__")
|
self.add(_blank, "sanic.__signal__.__init__")
|
||||||
|
|
||||||
@@ -260,9 +238,3 @@ class SignalRouter(BaseRouter):
|
|||||||
"Cannot declare reserved signal event: %s" % event
|
"Cannot declare reserved signal event: %s" % event
|
||||||
)
|
)
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
def _clean_trigger(self, trigger: str) -> str:
|
|
||||||
trigger = trigger[1:-1]
|
|
||||||
if ":" in trigger:
|
|
||||||
trigger, _ = trigger.split(":")
|
|
||||||
return trigger
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
|
|
||||||
from .service import TouchUp
|
from .service import TouchUp
|
||||||
|
|
||||||
|
|
||||||
class TouchUpMeta(SanicMeta):
|
class TouchUpMeta(type):
|
||||||
def __new__(cls, name, bases, attrs, **kwargs):
|
def __new__(cls, name, bases, attrs, **kwargs):
|
||||||
gen_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
gen_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
from .hashable_dict import HashableDict
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("HashableDict",)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
class HashableDict(dict):
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(tuple(sorted(self.items())))
|
|
||||||
@@ -9,11 +9,14 @@ from typing import (
|
|||||||
Optional,
|
Optional,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from sanic.constants import HTTP_METHODS
|
||||||
|
from sanic.exceptions import InvalidUsage
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
@@ -81,8 +84,6 @@ class HTTPMethodView:
|
|||||||
|
|
||||||
def dispatch_request(self, request, *args, **kwargs):
|
def dispatch_request(self, request, *args, **kwargs):
|
||||||
handler = getattr(self, request.method.lower(), None)
|
handler = getattr(self, request.method.lower(), None)
|
||||||
if not handler and request.method == "HEAD":
|
|
||||||
handler = self.get
|
|
||||||
return handler(request, *args, **kwargs)
|
return handler(request, *args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -135,3 +136,48 @@ class HTTPMethodView:
|
|||||||
def stream(func):
|
def stream(func):
|
||||||
func.is_stream = True
|
func.is_stream = True
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
class CompositionView:
|
||||||
|
"""Simple method-function mapped view for the sanic.
|
||||||
|
You can add handler functions to methods (get, post, put, patch, delete)
|
||||||
|
for every HTTP method you want to support.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['GET'], lambda request: text('I am get method'))
|
||||||
|
view.add(['POST', 'PUT'], lambda request: text('I am post/put method'))
|
||||||
|
|
||||||
|
If someone tries to use a non-implemented method, there will be a
|
||||||
|
405 response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.handlers = {}
|
||||||
|
self.name = self.__class__.__name__
|
||||||
|
warn(
|
||||||
|
"CompositionView has been deprecated and will be removed in "
|
||||||
|
"v21.12. Please update your view to HTTPMethodView.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __name__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def add(self, methods, handler, stream=False):
|
||||||
|
if stream:
|
||||||
|
handler.is_stream = stream
|
||||||
|
for method in methods:
|
||||||
|
if method not in HTTP_METHODS:
|
||||||
|
raise InvalidUsage(f"{method} is not a valid HTTP method.")
|
||||||
|
|
||||||
|
if method in self.handlers:
|
||||||
|
raise InvalidUsage(f"Method {method} is already registered.")
|
||||||
|
self.handlers[method] = handler
|
||||||
|
|
||||||
|
def __call__(self, request, *args, **kwargs):
|
||||||
|
handler = self.handlers[request.method.upper()]
|
||||||
|
return handler(request, *args, **kwargs)
|
||||||
|
|||||||
@@ -7,19 +7,22 @@ import traceback
|
|||||||
|
|
||||||
from gunicorn.workers import base # type: ignore
|
from gunicorn.workers import base # type: ignore
|
||||||
|
|
||||||
from sanic.compat import UVLOOP_INSTALLED
|
|
||||||
from sanic.log import logger
|
from sanic.log import logger
|
||||||
from sanic.server import HttpProtocol, Signal, serve, try_use_uvloop
|
from sanic.server import HttpProtocol, Signal, serve
|
||||||
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ssl # type: ignore
|
import ssl # type: ignore
|
||||||
except ImportError: # no cov
|
except ImportError:
|
||||||
ssl = None # type: ignore
|
ssl = None # type: ignore
|
||||||
|
|
||||||
if UVLOOP_INSTALLED: # no cov
|
try:
|
||||||
try_use_uvloop()
|
import uvloop # type: ignore
|
||||||
|
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GunicornWorker(base.Worker):
|
class GunicornWorker(base.Worker):
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -147,7 +147,6 @@ extras_require = {
|
|||||||
"dev": dev_require,
|
"dev": dev_require,
|
||||||
"docs": docs_require,
|
"docs": docs_require,
|
||||||
"all": all_require,
|
"all": all_require,
|
||||||
"ext": ["sanic-ext"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_kwargs["install_requires"] = requirements
|
setup_kwargs["install_requires"] = requirements
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ import string
|
|||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from contextlib import suppress
|
|
||||||
from logging import LogRecord
|
from logging import LogRecord
|
||||||
from typing import List, Tuple
|
from typing import Callable, List, Tuple
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -186,21 +184,3 @@ def message_in_records():
|
|||||||
return error_captured
|
return error_captured
|
||||||
|
|
||||||
return msg_in_log
|
return msg_in_log
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def ext_instance():
|
|
||||||
ext_instance = MagicMock()
|
|
||||||
ext_instance.injection = MagicMock()
|
|
||||||
return ext_instance
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True) # type: ignore
|
|
||||||
def sanic_ext(ext_instance): # noqa
|
|
||||||
sanic_ext = MagicMock(__version__="1.2.3")
|
|
||||||
sanic_ext.Extend = MagicMock()
|
|
||||||
sanic_ext.Extend.return_value = ext_instance
|
|
||||||
sys.modules["sanic_ext"] = sanic_ext
|
|
||||||
yield sanic_ext
|
|
||||||
with suppress(KeyError):
|
|
||||||
del sys.modules["sanic_ext"]
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from sanic import Sanic, text
|
from sanic import Sanic, text
|
||||||
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
|
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
|
||||||
@@ -8,7 +9,7 @@ LOGGING_CONFIG = {**LOGGING_CONFIG_DEFAULTS}
|
|||||||
LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s"
|
LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s"
|
||||||
LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG"
|
LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG"
|
||||||
|
|
||||||
app = Sanic("FakeServer", log_config=LOGGING_CONFIG)
|
app = Sanic(__name__, log_config=LOGGING_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|||||||
@@ -2,20 +2,15 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from collections import Counter
|
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from os import environ
|
from os import environ
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import sanic
|
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.compat import OS_IS_WINDOWS
|
|
||||||
from sanic.config import Config
|
from sanic.config import Config
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.helpers import _default
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +19,15 @@ def clear_app_registry():
|
|||||||
Sanic._app_registry = {}
|
Sanic._app_registry = {}
|
||||||
|
|
||||||
|
|
||||||
|
def uvloop_installed():
|
||||||
|
try:
|
||||||
|
import uvloop # noqa
|
||||||
|
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_app_loop_running(app):
|
def test_app_loop_running(app):
|
||||||
@app.get("/test")
|
@app.get("/test")
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
@@ -393,22 +397,6 @@ def test_app_no_registry():
|
|||||||
Sanic.get_app("no-register")
|
Sanic.get_app("no-register")
|
||||||
|
|
||||||
|
|
||||||
def test_app_no_registry_deprecation_message():
|
|
||||||
with pytest.warns(DeprecationWarning) as records:
|
|
||||||
Sanic("no-register", register=False)
|
|
||||||
Sanic("yes-register", register=True)
|
|
||||||
|
|
||||||
message = (
|
|
||||||
"[DEPRECATION v22.6] The register argument is deprecated and will "
|
|
||||||
"stop working in v22.6. After v22.6 all apps will be added to the "
|
|
||||||
"Sanic app registry."
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(records) == 2
|
|
||||||
for record in records:
|
|
||||||
assert record.message.args[0] == message
|
|
||||||
|
|
||||||
|
|
||||||
def test_app_no_registry_env():
|
def test_app_no_registry_env():
|
||||||
environ["SANIC_REGISTER"] = "False"
|
environ["SANIC_REGISTER"] = "False"
|
||||||
Sanic("no-register")
|
Sanic("no-register")
|
||||||
@@ -420,13 +408,16 @@ def test_app_no_registry_env():
|
|||||||
|
|
||||||
|
|
||||||
def test_app_set_attribute_warning(app):
|
def test_app_set_attribute_warning(app):
|
||||||
message = (
|
with pytest.warns(DeprecationWarning) as record:
|
||||||
"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
|
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."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_app_set_context(app):
|
def test_app_set_context(app):
|
||||||
app.ctx.foo = 1
|
app.ctx.foo = 1
|
||||||
@@ -447,7 +438,15 @@ def test_bad_custom_config():
|
|||||||
SanicException,
|
SanicException,
|
||||||
match=(
|
match=(
|
||||||
"When instantiating Sanic with config, you cannot also pass "
|
"When instantiating Sanic with config, you cannot also pass "
|
||||||
"env_prefix"
|
"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"
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
Sanic("test", config=1, env_prefix=1)
|
Sanic("test", config=1, env_prefix=1)
|
||||||
@@ -473,98 +472,6 @@ def test_custom_context():
|
|||||||
assert app.ctx == ctx
|
assert app.ctx == ctx
|
||||||
|
|
||||||
|
|
||||||
def test_uvloop_config(app, monkeypatch):
|
|
||||||
@app.get("/test")
|
|
||||||
def handler(request):
|
|
||||||
return text("ok")
|
|
||||||
|
|
||||||
try_use_uvloop = Mock()
|
|
||||||
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
|
|
||||||
|
|
||||||
# Default config
|
|
||||||
app.test_client.get("/test")
|
|
||||||
if OS_IS_WINDOWS:
|
|
||||||
try_use_uvloop.assert_not_called()
|
|
||||||
else:
|
|
||||||
try_use_uvloop.assert_called_once()
|
|
||||||
|
|
||||||
try_use_uvloop.reset_mock()
|
|
||||||
app.config["USE_UVLOOP"] = False
|
|
||||||
app.test_client.get("/test")
|
|
||||||
try_use_uvloop.assert_not_called()
|
|
||||||
|
|
||||||
try_use_uvloop.reset_mock()
|
|
||||||
app.config["USE_UVLOOP"] = True
|
|
||||||
app.test_client.get("/test")
|
|
||||||
try_use_uvloop.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch):
|
|
||||||
apps = (Sanic("default-uvloop"), Sanic("no-uvloop"), Sanic("yes-uvloop"))
|
|
||||||
|
|
||||||
apps[1].config.USE_UVLOOP = False
|
|
||||||
apps[2].config.USE_UVLOOP = True
|
|
||||||
|
|
||||||
try_use_uvloop = Mock()
|
|
||||||
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
with caplog.at_level(logging.WARNING):
|
|
||||||
for app in apps:
|
|
||||||
srv_coro = app.create_server(
|
|
||||||
return_asyncio_server=True,
|
|
||||||
asyncio_server_kwargs=dict(start_serving=False),
|
|
||||||
)
|
|
||||||
loop.run_until_complete(srv_coro)
|
|
||||||
|
|
||||||
try_use_uvloop.assert_not_called() # Check it didn't try to change policy
|
|
||||||
|
|
||||||
message = (
|
|
||||||
"You are trying to change the uvloop configuration, but "
|
|
||||||
"this is only effective when using the run(...) method. "
|
|
||||||
"When using the create_server(...) method Sanic will use "
|
|
||||||
"the already existing loop."
|
|
||||||
)
|
|
||||||
|
|
||||||
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
|
|
||||||
modified = sum(1 for app in apps if app.config.USE_UVLOOP is not _default)
|
|
||||||
|
|
||||||
assert counter[(logging.WARNING, message)] == modified
|
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_uvloop_configs_display_warning(caplog):
|
|
||||||
Sanic._uvloop_setting = None # Reset the setting (changed in prev tests)
|
|
||||||
|
|
||||||
default_uvloop = Sanic("default-uvloop")
|
|
||||||
no_uvloop = Sanic("no-uvloop")
|
|
||||||
yes_uvloop = Sanic("yes-uvloop")
|
|
||||||
|
|
||||||
no_uvloop.config.USE_UVLOOP = False
|
|
||||||
yes_uvloop.config.USE_UVLOOP = True
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
with caplog.at_level(logging.WARNING):
|
|
||||||
for app in (default_uvloop, no_uvloop, yes_uvloop):
|
|
||||||
srv_coro = app.create_server(
|
|
||||||
return_asyncio_server=True,
|
|
||||||
asyncio_server_kwargs=dict(start_serving=False),
|
|
||||||
)
|
|
||||||
srv = loop.run_until_complete(srv_coro)
|
|
||||||
loop.run_until_complete(srv.startup())
|
|
||||||
|
|
||||||
message = (
|
|
||||||
"It looks like you're running several apps with different "
|
|
||||||
"uvloop settings. This is not supported and may lead to "
|
|
||||||
"unintended behaviour."
|
|
||||||
)
|
|
||||||
|
|
||||||
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
|
|
||||||
|
|
||||||
assert counter[(logging.WARNING, message)] == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_cannot_run_fast_and_workers(app):
|
def test_cannot_run_fast_and_workers(app):
|
||||||
message = "You cannot use both fast=True and workers=X"
|
message = "You cannot use both fast=True and workers=X"
|
||||||
with pytest.raises(RuntimeError, match=message):
|
with pytest.raises(RuntimeError, match=message):
|
||||||
|
|||||||
@@ -145,37 +145,6 @@ def test_listeners_triggered_async(app):
|
|||||||
assert after_server_stop
|
assert after_server_stop
|
||||||
|
|
||||||
|
|
||||||
def test_non_default_uvloop_config_raises_warning(app):
|
|
||||||
app.config.USE_UVLOOP = True
|
|
||||||
|
|
||||||
class CustomServer(uvicorn.Server):
|
|
||||||
def install_signal_handlers(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
|
|
||||||
server = CustomServer(config=config)
|
|
||||||
|
|
||||||
with pytest.warns(UserWarning) as records:
|
|
||||||
server.run()
|
|
||||||
|
|
||||||
all_tasks = asyncio.all_tasks(asyncio.get_event_loop())
|
|
||||||
for task in all_tasks:
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
msg = ""
|
|
||||||
for record in records:
|
|
||||||
_msg = str(record.message)
|
|
||||||
if _msg.startswith("You have set the USE_UVLOOP configuration"):
|
|
||||||
msg = _msg
|
|
||||||
break
|
|
||||||
|
|
||||||
assert msg == (
|
|
||||||
"You have set the USE_UVLOOP configuration option, but Sanic "
|
|
||||||
"cannot control the event loop when running in ASGI mode."
|
|
||||||
"This option will be ignored."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_mockprotocol_events(protocol):
|
async def test_mockprotocol_events(protocol):
|
||||||
assert protocol._not_paused.is_set()
|
assert protocol._not_paused.is_set()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Blueprint, Sanic
|
from sanic import Blueprint, Sanic
|
||||||
from sanic.exceptions import SanicException
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -80,18 +79,24 @@ def test_names_okay(name):
|
|||||||
)
|
)
|
||||||
def test_names_not_okay(name):
|
def test_names_not_okay(name):
|
||||||
app_message = (
|
app_message = (
|
||||||
f"Sanic instance named '{name}' uses an invalid format. Names must "
|
f"Sanic instance named '{name}' uses a format that isdeprecated. "
|
||||||
"begin with a character and may only contain alphanumeric "
|
"Starting in version 21.12, Sanic objects must be named only using "
|
||||||
"characters, _, or -."
|
"alphanumeric characters, _, or -."
|
||||||
)
|
)
|
||||||
bp_message = (
|
bp_message = (
|
||||||
f"Blueprint instance named '{name}' uses an invalid format. Names "
|
f"Blueprint instance named '{name}' uses a format that isdeprecated. "
|
||||||
"must begin with a character and may only contain alphanumeric "
|
"Starting in version 21.12, Blueprint objects must be named only using "
|
||||||
"characters, _, or -."
|
"alphanumeric characters, _, or -."
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(SanicException, match=app_message):
|
with pytest.warns(DeprecationWarning) as app_e:
|
||||||
Sanic(name)
|
app = Sanic(name)
|
||||||
|
|
||||||
with pytest.raises(SanicException, match=bp_message):
|
with pytest.warns(DeprecationWarning) as bp_e:
|
||||||
Blueprint(name)
|
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
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from sanic.exceptions import (
|
|||||||
)
|
)
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
@@ -832,7 +833,7 @@ def test_static_blueprint_name(static_file_directory, file_name):
|
|||||||
|
|
||||||
@pytest.mark.parametrize("file_name", ["test.file"])
|
@pytest.mark.parametrize("file_name", ["test.file"])
|
||||||
def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
|
def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
|
||||||
current_file = inspect.getfile(inspect.currentframe()) # type: ignore
|
current_file = inspect.getfile(inspect.currentframe())
|
||||||
with open(current_file, "rb") as file:
|
with open(current_file, "rb") as file:
|
||||||
file.read()
|
file.read()
|
||||||
|
|
||||||
@@ -861,6 +862,31 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
|
|||||||
assert triggered is True
|
assert triggered is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_handler_add(app: Sanic):
|
||||||
|
view = CompositionView()
|
||||||
|
|
||||||
|
async def get_handler(request):
|
||||||
|
return json({"response": "OK"})
|
||||||
|
|
||||||
|
view.add(["GET"], get_handler, stream=False)
|
||||||
|
|
||||||
|
async def default_handler(request):
|
||||||
|
return text("OK")
|
||||||
|
|
||||||
|
bp = Blueprint(name="handler", url_prefix="/handler")
|
||||||
|
bp.add_route(default_handler, uri="/default/", strict_slashes=True)
|
||||||
|
|
||||||
|
bp.add_route(view, uri="/view", name="test")
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/handler/default/")
|
||||||
|
assert response.text == "OK"
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/handler/view")
|
||||||
|
assert response.json["response"] == "OK"
|
||||||
|
|
||||||
|
|
||||||
def test_websocket_route(app: Sanic):
|
def test_websocket_route(app: Sanic):
|
||||||
event = asyncio.Event()
|
event = asyncio.Event()
|
||||||
|
|
||||||
@@ -1053,13 +1079,16 @@ def test_blueprint_registered_multiple_apps():
|
|||||||
|
|
||||||
def test_bp_set_attribute_warning():
|
def test_bp_set_attribute_warning():
|
||||||
bp = Blueprint("bp")
|
bp = Blueprint("bp")
|
||||||
message = (
|
with pytest.warns(DeprecationWarning) as record:
|
||||||
"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
|
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."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_early_registration(app):
|
def test_early_registration(app):
|
||||||
assert len(app.router.routes) == 0
|
assert len(app.router.routes) == 0
|
||||||
|
|||||||
@@ -32,12 +32,6 @@ def starting_line(lines):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def read_app_info(lines):
|
|
||||||
for line in lines:
|
|
||||||
if line.startswith(b"{") and line.endswith(b"}"):
|
|
||||||
return json.loads(line)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"appname",
|
"appname",
|
||||||
(
|
(
|
||||||
@@ -205,7 +199,9 @@ def test_debug(cmd):
|
|||||||
command = ["sanic", "fake.server.app", cmd]
|
command = ["sanic", "fake.server.app", cmd]
|
||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
info = read_app_info(lines)
|
|
||||||
|
app_info = lines[starting_line(lines) + 9]
|
||||||
|
info = json.loads(app_info)
|
||||||
|
|
||||||
assert info["debug"] is True
|
assert info["debug"] is True
|
||||||
assert info["auto_reload"] is True
|
assert info["auto_reload"] is True
|
||||||
@@ -216,7 +212,9 @@ def test_auto_reload(cmd):
|
|||||||
command = ["sanic", "fake.server.app", cmd]
|
command = ["sanic", "fake.server.app", cmd]
|
||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
info = read_app_info(lines)
|
|
||||||
|
app_info = lines[starting_line(lines) + 9]
|
||||||
|
info = json.loads(app_info)
|
||||||
|
|
||||||
assert info["debug"] is False
|
assert info["debug"] is False
|
||||||
assert info["auto_reload"] is True
|
assert info["auto_reload"] is True
|
||||||
@@ -229,7 +227,9 @@ def test_access_logs(cmd, expected):
|
|||||||
command = ["sanic", "fake.server.app", cmd]
|
command = ["sanic", "fake.server.app", cmd]
|
||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
info = read_app_info(lines)
|
|
||||||
|
app_info = lines[starting_line(lines) + 8]
|
||||||
|
info = json.loads(app_info)
|
||||||
|
|
||||||
assert info["access_log"] is expected
|
assert info["access_log"] is expected
|
||||||
|
|
||||||
@@ -254,6 +254,8 @@ def test_noisy_exceptions(cmd, expected):
|
|||||||
command = ["sanic", "fake.server.app", cmd]
|
command = ["sanic", "fake.server.app", cmd]
|
||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
info = read_app_info(lines)
|
|
||||||
|
app_info = lines[starting_line(lines) + 8]
|
||||||
|
info = json.loads(app_info)
|
||||||
|
|
||||||
assert info["noisy_exceptions"] is expected
|
assert info["noisy_exceptions"] is expected
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from os import environ
|
from os import environ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from unittest.mock import Mock, call
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -34,11 +32,6 @@ class ConfigTest:
|
|||||||
return self.not_for_config
|
return self.not_for_config
|
||||||
|
|
||||||
|
|
||||||
class UltimateAnswer:
|
|
||||||
def __init__(self, answer):
|
|
||||||
self.answer = int(answer)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_from_object(app):
|
def test_load_from_object(app):
|
||||||
app.config.load(ConfigTest)
|
app.config.load(ConfigTest)
|
||||||
assert "CONFIG_VALUE" in app.config
|
assert "CONFIG_VALUE" in app.config
|
||||||
@@ -81,6 +74,26 @@ def test_auto_bool_env_prefix():
|
|||||||
del environ["SANIC_TEST_ANSWER"]
|
del environ["SANIC_TEST_ANSWER"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dont_load_env():
|
||||||
|
environ["SANIC_TEST_ANSWER"] = "42"
|
||||||
|
app = Sanic(name=__name__, load_env=False)
|
||||||
|
assert getattr(app.config, "TEST_ANSWER", None) is None
|
||||||
|
del environ["SANIC_TEST_ANSWER"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("load_env", [None, False, "", "MYAPP_"])
|
||||||
|
def test_load_env_deprecation(load_env):
|
||||||
|
with pytest.warns(DeprecationWarning, match=r"21\.12"):
|
||||||
|
_ = Sanic(name=__name__, load_env=load_env)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_env_prefix():
|
||||||
|
environ["MYAPP_TEST_ANSWER"] = "42"
|
||||||
|
app = Sanic(name=__name__, load_env="MYAPP_")
|
||||||
|
assert app.config.TEST_ANSWER == 42
|
||||||
|
del environ["MYAPP_TEST_ANSWER"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("env_prefix", [None, ""])
|
@pytest.mark.parametrize("env_prefix", [None, ""])
|
||||||
def test_empty_load_env_prefix(env_prefix):
|
def test_empty_load_env_prefix(env_prefix):
|
||||||
environ["SANIC_TEST_ANSWER"] = "42"
|
environ["SANIC_TEST_ANSWER"] = "42"
|
||||||
@@ -89,6 +102,20 @@ def test_empty_load_env_prefix(env_prefix):
|
|||||||
del environ["SANIC_TEST_ANSWER"]
|
del environ["SANIC_TEST_ANSWER"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_env_prefix_float_values():
|
||||||
|
environ["MYAPP_TEST_ROI"] = "2.3"
|
||||||
|
app = Sanic(name=__name__, load_env="MYAPP_")
|
||||||
|
assert app.config.TEST_ROI == 2.3
|
||||||
|
del environ["MYAPP_TEST_ROI"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_env_prefix_string_value():
|
||||||
|
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
|
||||||
|
app = Sanic(name=__name__, load_env="MYAPP_")
|
||||||
|
assert app.config.TEST_TOKEN == "somerandomtesttoken"
|
||||||
|
del environ["MYAPP_TEST_TOKEN"]
|
||||||
|
|
||||||
|
|
||||||
def test_env_prefix():
|
def test_env_prefix():
|
||||||
environ["MYAPP_TEST_ANSWER"] = "42"
|
environ["MYAPP_TEST_ANSWER"] = "42"
|
||||||
app = Sanic(name=__name__, env_prefix="MYAPP_")
|
app = Sanic(name=__name__, env_prefix="MYAPP_")
|
||||||
@@ -110,32 +137,6 @@ def test_env_prefix_string_value():
|
|||||||
del environ["MYAPP_TEST_TOKEN"]
|
del environ["MYAPP_TEST_TOKEN"]
|
||||||
|
|
||||||
|
|
||||||
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_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):
|
def test_load_from_file(app):
|
||||||
config = dedent(
|
config = dedent(
|
||||||
"""
|
"""
|
||||||
@@ -385,24 +386,5 @@ def test_config_set_methods(app, monkeypatch):
|
|||||||
post_set.assert_called_once_with("FOO", 5)
|
post_set.assert_called_once_with("FOO", 5)
|
||||||
post_set.reset_mock()
|
post_set.reset_mock()
|
||||||
|
|
||||||
app.config.update({"FOO": 6}, {"BAR": 7})
|
app.config.update_config({"FOO": 6})
|
||||||
post_set.assert_has_calls(
|
post_set.assert_called_once_with("FOO", 6)
|
||||||
calls=[
|
|
||||||
call("FOO", 6),
|
|
||||||
call("BAR", 7),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
post_set.reset_mock()
|
|
||||||
|
|
||||||
app.config.update({"FOO": 8}, BAR=9)
|
|
||||||
post_set.assert_has_calls(
|
|
||||||
calls=[
|
|
||||||
call("FOO", 8),
|
|
||||||
call("BAR", 9),
|
|
||||||
],
|
|
||||||
any_order=True,
|
|
||||||
)
|
|
||||||
post_set.reset_mock()
|
|
||||||
|
|
||||||
app.config.update_config({"FOO": 10})
|
|
||||||
post_set.assert_called_once_with("FOO", 10)
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
|
|
||||||
from threading import Event
|
from threading import Event
|
||||||
from unittest.mock import Mock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sanic.exceptions import SanicException
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
@@ -53,60 +48,3 @@ def test_create_task_with_app_arg(app):
|
|||||||
|
|
||||||
_, response = app.test_client.get("/")
|
_, response = app.test_client.get("/")
|
||||||
assert response.text == "test_create_task_with_app_arg"
|
assert response.text == "test_create_task_with_app_arg"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
|
|
||||||
def test_create_named_task(app):
|
|
||||||
async def dummy():
|
|
||||||
...
|
|
||||||
|
|
||||||
@app.before_server_start
|
|
||||||
async def setup(app, _):
|
|
||||||
app.add_task(dummy, name="dummy_task")
|
|
||||||
|
|
||||||
@app.after_server_start
|
|
||||||
async def stop(app, _):
|
|
||||||
task = app.get_task("dummy_task")
|
|
||||||
|
|
||||||
assert app._task_registry
|
|
||||||
assert isinstance(task, asyncio.Task)
|
|
||||||
|
|
||||||
assert task.get_name() == "dummy_task"
|
|
||||||
|
|
||||||
app.stop()
|
|
||||||
|
|
||||||
app.run()
|
|
||||||
|
|
||||||
|
|
||||||
def test_named_task_called(app):
|
|
||||||
e = Event()
|
|
||||||
|
|
||||||
async def coro():
|
|
||||||
e.set()
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
async def isset(request):
|
|
||||||
await asyncio.sleep(0.05)
|
|
||||||
return text(str(e.is_set()))
|
|
||||||
|
|
||||||
@app.before_server_start
|
|
||||||
async def setup(app, _):
|
|
||||||
app.add_task(coro, name="dummy_task")
|
|
||||||
|
|
||||||
request, response = app.test_client.get("/")
|
|
||||||
assert response.body == b"True"
|
|
||||||
|
|
||||||
|
|
||||||
@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")
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -280,20 +280,40 @@ def test_allow_fallback_error_format_set_main_process_start(app):
|
|||||||
async def start(app, _):
|
async def start(app, _):
|
||||||
app.config.FALLBACK_ERROR_FORMAT = "text"
|
app.config.FALLBACK_ERROR_FORMAT = "text"
|
||||||
|
|
||||||
_, response = app.test_client.get("/error")
|
request, response = app.test_client.get("/error")
|
||||||
|
assert request.app.error_handler.fallback == "text"
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
assert response.content_type == "text/plain; charset=utf-8"
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
def test_setting_fallback_on_config_changes_as_expected(app):
|
def test_setting_fallback_to_non_default_raise_warning(app):
|
||||||
app.error_handler = ErrorHandler()
|
app.error_handler = ErrorHandler(fallback="text")
|
||||||
|
|
||||||
_, response = app.test_client.get("/error")
|
assert app.error_handler.fallback == "text"
|
||||||
assert response.content_type == "text/html; charset=utf-8"
|
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match=(
|
||||||
|
"Overriding non-default ErrorHandler fallback value. "
|
||||||
|
"Changing from text to auto."
|
||||||
|
),
|
||||||
|
):
|
||||||
|
app.config.FALLBACK_ERROR_FORMAT = "auto"
|
||||||
|
|
||||||
|
assert app.error_handler.fallback == "auto"
|
||||||
|
|
||||||
app.config.FALLBACK_ERROR_FORMAT = "text"
|
app.config.FALLBACK_ERROR_FORMAT = "text"
|
||||||
_, response = app.test_client.get("/error")
|
|
||||||
assert response.content_type == "text/plain; charset=utf-8"
|
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"
|
||||||
|
|
||||||
|
|
||||||
def test_allow_fallback_error_format_in_config_injection():
|
def test_allow_fallback_error_format_in_config_injection():
|
||||||
@@ -307,6 +327,7 @@ def test_allow_fallback_error_format_in_config_injection():
|
|||||||
raise Exception("something went wrong")
|
raise Exception("something went wrong")
|
||||||
|
|
||||||
request, response = app.test_client.get("/error")
|
request, response = app.test_client.get("/error")
|
||||||
|
assert request.app.error_handler.fallback == "text"
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
assert response.content_type == "text/plain; charset=utf-8"
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
|
||||||
@@ -318,39 +339,6 @@ def test_allow_fallback_error_format_in_config_replacement(app):
|
|||||||
app.config = MyConfig()
|
app.config = MyConfig()
|
||||||
|
|
||||||
request, response = app.test_client.get("/error")
|
request, response = app.test_client.get("/error")
|
||||||
|
assert request.app.error_handler.fallback == "text"
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
assert response.content_type == "text/plain; charset=utf-8"
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
def test_config_fallback_before_and_after_startup(app):
|
|
||||||
app.config.FALLBACK_ERROR_FORMAT = "json"
|
|
||||||
|
|
||||||
@app.main_process_start
|
|
||||||
async def start(app, _):
|
|
||||||
app.config.FALLBACK_ERROR_FORMAT = "text"
|
|
||||||
|
|
||||||
_, response = app.test_client.get("/error")
|
|
||||||
assert response.status == 500
|
|
||||||
assert response.content_type == "text/plain; charset=utf-8"
|
|
||||||
|
|
||||||
|
|
||||||
def test_config_fallback_using_update_dict(app):
|
|
||||||
app.config.update({"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_using_update_kwarg(app):
|
|
||||||
app.config.update(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"
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from sanic.exceptions import (
|
|||||||
SanicException,
|
SanicException,
|
||||||
ServerError,
|
ServerError,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
|
abort,
|
||||||
)
|
)
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@@ -87,6 +88,10 @@ def exception_app():
|
|||||||
def handler_500_error(request):
|
def handler_500_error(request):
|
||||||
raise SanicException(status_code=500)
|
raise SanicException(status_code=500)
|
||||||
|
|
||||||
|
@app.route("/old_abort")
|
||||||
|
def handler_old_abort_error(request):
|
||||||
|
abort(500)
|
||||||
|
|
||||||
@app.route("/abort/message")
|
@app.route("/abort/message")
|
||||||
def handler_abort_message(request):
|
def handler_abort_message(request):
|
||||||
raise SanicException(message="Custom Message", status_code=500)
|
raise SanicException(message="Custom Message", status_code=500)
|
||||||
@@ -234,6 +239,11 @@ def test_sanic_exception(exception_app):
|
|||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
assert "Custom Message" in response.text
|
assert "Custom Message" in response.text
|
||||||
|
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
request, response = exception_app.test_client.get("/old_abort")
|
||||||
|
assert response.status == 500
|
||||||
|
assert len(w) == 1 and "deprecated" in w[0].message.args[0]
|
||||||
|
|
||||||
|
|
||||||
def test_custom_exception_default_message(exception_app):
|
def test_custom_exception_default_message(exception_app):
|
||||||
class TeaError(SanicException):
|
class TeaError(SanicException):
|
||||||
@@ -252,7 +262,7 @@ def test_custom_exception_default_message(exception_app):
|
|||||||
|
|
||||||
|
|
||||||
def test_exception_in_ws_logged(caplog):
|
def test_exception_in_ws_logged(caplog):
|
||||||
app = Sanic(__name__)
|
app = Sanic(__file__)
|
||||||
|
|
||||||
@app.websocket("/feed")
|
@app.websocket("/feed")
|
||||||
async def feed(request, ws):
|
async def feed(request, ws):
|
||||||
@@ -261,13 +271,9 @@ def test_exception_in_ws_logged(caplog):
|
|||||||
with caplog.at_level(logging.INFO):
|
with caplog.at_level(logging.INFO):
|
||||||
app.test_client.websocket("/feed")
|
app.test_client.websocket("/feed")
|
||||||
|
|
||||||
for record in caplog.record_tuples:
|
error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"]
|
||||||
if record[2].startswith("Exception occurred"):
|
assert error_logs[1][1] == logging.ERROR
|
||||||
break
|
assert "Exception occurred while handling uri:" in error_logs[1][2]
|
||||||
|
|
||||||
assert record[0] == "sanic.error"
|
|
||||||
assert record[1] == logging.ERROR
|
|
||||||
assert "Exception occurred while handling uri:" in record[2]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("debug", (True, False))
|
@pytest.mark.parametrize("debug", (True, False))
|
||||||
|
|||||||
@@ -226,12 +226,11 @@ def test_single_arg_exception_handler_notice(
|
|||||||
exception_handler_app.error_handler = CustomErrorHandler()
|
exception_handler_app.error_handler = CustomErrorHandler()
|
||||||
|
|
||||||
message = (
|
message = (
|
||||||
"[DEPRECATION v22.3] You are using a deprecated error handler. The "
|
"You are using a deprecated error handler. The lookup method should "
|
||||||
"lookup method should accept two positional parameters: (exception, "
|
"accept two positional parameters: (exception, route_name: "
|
||||||
"route_name: Optional[str]). Until you upgrade your "
|
"Optional[str]). Until you upgrade your ErrorHandler.lookup, "
|
||||||
"ErrorHandler.lookup, Blueprint specific exceptions will not work "
|
"Blueprint specific exceptions will not work properly. Beginning in "
|
||||||
"properly. Beginning in v22.3, the legacy style lookup method will "
|
"v22.3, the legacy style lookup method will not work at all."
|
||||||
"not work at all."
|
|
||||||
)
|
)
|
||||||
with pytest.warns(DeprecationWarning) as record:
|
with pytest.warns(DeprecationWarning) as record:
|
||||||
_, response = exception_handler_app.test_client.get("/1")
|
_, response = exception_handler_app.test_client.get("/1")
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -35,14 +36,11 @@ def test_no_exceptions_when_cancel_pending_request(app, caplog):
|
|||||||
|
|
||||||
p.kill()
|
p.kill()
|
||||||
|
|
||||||
info = 0
|
counter = Counter([r[1] for r in caplog.record_tuples])
|
||||||
for record in caplog.record_tuples:
|
|
||||||
assert record[1] != logging.ERROR
|
assert counter[logging.INFO] == 11
|
||||||
if record[1] == logging.INFO:
|
assert logging.ERROR not in counter
|
||||||
info += 1
|
assert (
|
||||||
if record[2].startswith("Request:"):
|
caplog.record_tuples[9][2]
|
||||||
assert record[2] == (
|
== "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed."
|
||||||
"Request: GET http://127.0.0.1:8000/ stopped. "
|
)
|
||||||
"Transport is closed."
|
|
||||||
)
|
|
||||||
assert info == 11
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user