Compare commits
40 Commits
pre-regist
...
v22.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc97287f8e | ||
|
|
00218aa9f2 | ||
|
|
874718db94 | ||
|
|
bb4474897f | ||
|
|
0cb342aef4 | ||
|
|
030987480c | ||
|
|
f6fdc80b40 | ||
|
|
361c242473 | ||
|
|
32962d1e1c | ||
|
|
6e0a6871b5 | ||
|
|
0030425c8c | ||
|
|
c9dbc8ed26 | ||
|
|
44b108b564 | ||
|
|
2a8e91052f | ||
|
|
0c9df02e66 | ||
|
|
7523e87937 | ||
|
|
d4fb44e986 | ||
|
|
68b654d981 | ||
|
|
88bc6d8966 | ||
|
|
ac388d644b | ||
|
|
bb517ddcca | ||
|
|
b8d991420b | ||
|
|
4a416e177a | ||
|
|
8dfa49b648 | ||
|
|
8b0eaa097c | ||
|
|
101151b419 | ||
|
|
4669036f45 | ||
|
|
9bf9067c99 | ||
|
|
a7bc8b56ba | ||
|
|
371985d129 | ||
|
|
3eae00898d | ||
|
|
dc3ccba527 | ||
|
|
b91ffed010 | ||
|
|
8c07e388cd | ||
|
|
98ce4bdeb2 | ||
|
|
4659069350 | ||
|
|
080d41627a | ||
|
|
d799c5f03c | ||
|
|
abe062b371 | ||
|
|
b5a00ac1ca |
@@ -1,28 +0,0 @@
|
||||
exclude_patterns:
|
||||
- "sanic/__main__.py"
|
||||
- "sanic/application/logo.py"
|
||||
- "sanic/application/motd.py"
|
||||
- "sanic/reloader_helpers.py"
|
||||
- "sanic/simple.py"
|
||||
- "sanic/utils.py"
|
||||
- ".github/"
|
||||
- "changelogs/"
|
||||
- "docker/"
|
||||
- "docs/"
|
||||
- "examples/"
|
||||
- "scripts/"
|
||||
- "tests/"
|
||||
checks:
|
||||
argument-count:
|
||||
enabled: false
|
||||
file-lines:
|
||||
config:
|
||||
threshold: 1000
|
||||
method-count:
|
||||
config:
|
||||
threshold: 40
|
||||
complex-logic:
|
||||
enabled: false
|
||||
method-complexity:
|
||||
config:
|
||||
threshold: 10
|
||||
14
.coveragerc
14
.coveragerc
@@ -3,13 +3,12 @@ branch = True
|
||||
source = sanic
|
||||
omit =
|
||||
site-packages
|
||||
sanic/application/logo.py
|
||||
sanic/application/motd.py
|
||||
sanic/cli
|
||||
sanic/__main__.py
|
||||
sanic/compat.py
|
||||
sanic/reloader_helpers.py
|
||||
sanic/simple.py
|
||||
sanic/utils.py
|
||||
sanic/cli
|
||||
|
||||
[html]
|
||||
directory = coverage
|
||||
@@ -21,3 +20,12 @@ exclude_lines =
|
||||
noqa
|
||||
NOQA
|
||||
pragma: no cover
|
||||
omit =
|
||||
site-packages
|
||||
sanic/__main__.py
|
||||
sanic/compat.py
|
||||
sanic/reloader_helpers.py
|
||||
sanic/simple.py
|
||||
sanic/utils.py
|
||||
sanic/cli
|
||||
skip_empty = True
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -2,9 +2,13 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
schedule:
|
||||
- cron: '25 16 * * 0'
|
||||
|
||||
18
.github/workflows/coverage.yml
vendored
18
.github/workflows/coverage.yml
vendored
@@ -3,13 +3,15 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
tags:
|
||||
- "!*" # Do not execute on tags
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
jobs:
|
||||
test:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -19,7 +21,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -28,9 +29,10 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox
|
||||
- uses: paambaati/codeclimate-action@v2.5.3
|
||||
if: always()
|
||||
env:
|
||||
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }}
|
||||
- name: Run coverage
|
||||
run: tox -e coverage
|
||||
continue-on-error: true
|
||||
- uses: codecov/codecov-action@v2
|
||||
with:
|
||||
coverageCommand: tox -e coverage
|
||||
files: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
|
||||
1
.github/workflows/pr-bandit.yml
vendored
1
.github/workflows/pr-bandit.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
|
||||
1
.github/workflows/pr-docs.yml
vendored
1
.github/workflows/pr-docs.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
|
||||
1
.github/workflows/pr-linter.yml
vendored
1
.github/workflows/pr-linter.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
|
||||
1
.github/workflows/pr-python310.yml
vendored
1
.github/workflows/pr-python310.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
|
||||
1
.github/workflows/pr-python37.yml
vendored
1
.github/workflows/pr-python37.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
|
||||
1
.github/workflows/pr-python38.yml
vendored
1
.github/workflows/pr-python38.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
|
||||
1
.github/workflows/pr-python39.yml
vendored
1
.github/workflows/pr-python39.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
|
||||
3
.github/workflows/pr-type-check.yml
vendored
3
.github/workflows/pr-type-check.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
@@ -15,7 +16,7 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
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.9, 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,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "*LTS"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
|
||||
192
CHANGELOG.rst
192
CHANGELOG.rst
@@ -1,12 +1,12 @@
|
||||
.. note::
|
||||
|
||||
From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases``
|
||||
CHANGELOG files are maintained in ``./docs/sanic/releases``. To view the full CHANGELOG, please visit https://sanic.readthedocs.io/en/stable/sanic/changelog.html.
|
||||
|
||||
|
||||
Version 21.6.1
|
||||
--------------
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
|
||||
Update sanic-routing to allow for better splitting of complex URI templates
|
||||
@@ -20,8 +20,7 @@ Bugfixes
|
||||
Version 21.6.0
|
||||
--------------
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
|
||||
Add ``response.eof()`` method for closing a stream in a handler
|
||||
@@ -68,8 +67,7 @@ Features
|
||||
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
|
||||
Additional methods for attaching ``HTTPMethodView``
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
|
||||
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
|
||||
@@ -85,8 +83,7 @@ Bugfixes
|
||||
Fix issue where Blueprint exception handlers do not consistently route to proper handler
|
||||
|
||||
|
||||
Deprecations and Removals
|
||||
*************************
|
||||
**Deprecations and Removals**
|
||||
|
||||
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
|
||||
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
|
||||
@@ -95,14 +92,12 @@ Deprecations and Removals
|
||||
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
|
||||
Deprecate StreamingHTTPResponse
|
||||
|
||||
Developer infrastructure
|
||||
************************
|
||||
**Developer infrastructure**
|
||||
|
||||
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
|
||||
Remove Travis CI in favor of GitHub Actions
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
**Improved Documentation**
|
||||
|
||||
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
|
||||
Fix typo in documentation
|
||||
@@ -112,8 +107,7 @@ Improved Documentation
|
||||
Version 21.3.2
|
||||
--------------
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
|
||||
Disable response timeout on websocket connections
|
||||
@@ -124,8 +118,7 @@ Bugfixes
|
||||
Version 21.3.1
|
||||
--------------
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
|
||||
Static files inside subfolders are not accessible (404)
|
||||
@@ -135,8 +128,7 @@ Version 21.3.0
|
||||
|
||||
`Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
*
|
||||
`#1876 <https://github.com/sanic-org/sanic/pull/1876>`_
|
||||
@@ -189,8 +181,7 @@ Features
|
||||
`#2063 <https://github.com/sanic-org/sanic/pull/2063>`_
|
||||
App and connection level context objects
|
||||
|
||||
Bugfixes and issues resolved
|
||||
****************************
|
||||
**Bugfixes**
|
||||
|
||||
* Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_
|
||||
``url_for`` where ``strict_slashes`` are on for a path ending in ``/``
|
||||
@@ -220,8 +211,7 @@ Bugfixes and issues resolved
|
||||
`#2001 <https://github.com/sanic-org/sanic/pull/2001>`_
|
||||
Raise ValueError when cookie max-age is not an integer
|
||||
|
||||
Deprecations and Removals
|
||||
*************************
|
||||
**Deprecations and Removals**
|
||||
|
||||
*
|
||||
`#2007 <https://github.com/sanic-org/sanic/pull/2007>`_
|
||||
@@ -240,8 +230,7 @@ Deprecations and Removals
|
||||
* ``Request.endpoint`` deprecated in favor of ``Request.name``
|
||||
* handler type name prefixes removed (static, websocket, etc)
|
||||
|
||||
Developer infrastructure
|
||||
************************
|
||||
**Developer infrastructure**
|
||||
|
||||
*
|
||||
`#1995 <https://github.com/sanic-org/sanic/pull/1995>`_
|
||||
@@ -259,8 +248,7 @@ Developer infrastructure
|
||||
`#2049 <https://github.com/sanic-org/sanic/pull/2049>`_
|
||||
Updated setup.py to use ``find_packages``
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
**Improved Documentation**
|
||||
|
||||
*
|
||||
`#1218 <https://github.com/sanic-org/sanic/pull/1218>`_
|
||||
@@ -282,8 +270,7 @@ Improved Documentation
|
||||
`#2052 <https://github.com/sanic-org/sanic/pull/2052>`_
|
||||
Fix some examples and docs
|
||||
|
||||
Miscellaneous
|
||||
*************
|
||||
**Miscellaneous**
|
||||
|
||||
* ``Request.route`` property
|
||||
* Better websocket subprotocols support
|
||||
@@ -329,8 +316,7 @@ Miscellaneous
|
||||
Version 20.12.3
|
||||
---------------
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
*
|
||||
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
|
||||
@@ -339,8 +325,7 @@ Bugfixes
|
||||
Version 20.12.2
|
||||
---------------
|
||||
|
||||
Dependencies
|
||||
************
|
||||
**Dependencies**
|
||||
|
||||
*
|
||||
`#2026 <https://github.com/sanic-org/sanic/pull/2026>`_
|
||||
@@ -353,8 +338,7 @@ Dependencies
|
||||
Version 19.12.5
|
||||
---------------
|
||||
|
||||
Dependencies
|
||||
************
|
||||
**Dependencies**
|
||||
|
||||
*
|
||||
`#2025 <https://github.com/sanic-org/sanic/pull/2025>`_
|
||||
@@ -367,8 +351,7 @@ Dependencies
|
||||
Version 20.12.0
|
||||
---------------
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
*
|
||||
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
|
||||
@@ -377,8 +360,7 @@ Features
|
||||
Version 20.12.0
|
||||
---------------
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
*
|
||||
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
|
||||
@@ -416,22 +398,19 @@ Features
|
||||
`#1979 <https://github.com/sanic-org/sanic/pull/1979>`_
|
||||
Add app registry and Sanic class level app retrieval
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
*
|
||||
`#1965 <https://github.com/sanic-org/sanic/pull/1965>`_
|
||||
Fix Chunked Transport-Encoding in ASGI streaming response
|
||||
|
||||
Deprecations and Removals
|
||||
*************************
|
||||
**Deprecations and Removals**
|
||||
|
||||
*
|
||||
`#1981 <https://github.com/sanic-org/sanic/pull/1981>`_
|
||||
Cleanup and remove deprecated code
|
||||
|
||||
Developer infrastructure
|
||||
************************
|
||||
**Developer infrastructure**
|
||||
|
||||
*
|
||||
`#1956 <https://github.com/sanic-org/sanic/pull/1956>`_
|
||||
@@ -445,8 +424,7 @@ Developer infrastructure
|
||||
`#1986 <https://github.com/sanic-org/sanic/pull/1986>`_
|
||||
Update tox requirements
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
**Improved Documentation**
|
||||
|
||||
*
|
||||
`#1951 <https://github.com/sanic-org/sanic/pull/1951>`_
|
||||
@@ -464,8 +442,7 @@ Improved Documentation
|
||||
Version 20.9.1
|
||||
---------------
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
*
|
||||
`#1954 <https://github.com/sanic-org/sanic/pull/1954>`_
|
||||
@@ -478,8 +455,7 @@ Bugfixes
|
||||
Version 19.12.3
|
||||
---------------
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
*
|
||||
`#1959 <https://github.com/sanic-org/sanic/pull/1959>`_
|
||||
@@ -490,8 +466,7 @@ Version 20.9.0
|
||||
---------------
|
||||
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
*
|
||||
`#1887 <https://github.com/sanic-org/sanic/pull/1887>`_
|
||||
@@ -518,22 +493,19 @@ Features
|
||||
`#1937 <https://github.com/sanic-org/sanic/pull/1937>`_
|
||||
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
*
|
||||
`#1897 <https://github.com/sanic-org/sanic/pull/1897>`_
|
||||
Resolves exception from unread bytes in stream
|
||||
|
||||
Deprecations and Removals
|
||||
*************************
|
||||
**Deprecations and Removals**
|
||||
|
||||
*
|
||||
`#1903 <https://github.com/sanic-org/sanic/pull/1903>`_
|
||||
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
|
||||
|
||||
Developer infrastructure
|
||||
************************
|
||||
**Developer infrastructure**
|
||||
|
||||
*
|
||||
`#1890 <https://github.com/sanic-org/sanic/pull/1890>`_,
|
||||
@@ -548,8 +520,7 @@ Developer infrastructure
|
||||
`#1924 <https://github.com/sanic-org/sanic/pull/1924>`_
|
||||
Adding --strict-markers for pytest
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
**Improved Documentation**
|
||||
|
||||
*
|
||||
`#1922 <https://github.com/sanic-org/sanic/pull/1922>`_
|
||||
@@ -559,8 +530,7 @@ Improved Documentation
|
||||
Version 20.6.3
|
||||
---------------
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
*
|
||||
`#1884 <https://github.com/sanic-org/sanic/pull/1884>`_
|
||||
@@ -570,8 +540,7 @@ Bugfixes
|
||||
Version 20.6.2
|
||||
---------------
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
*
|
||||
`#1641 <https://github.com/sanic-org/sanic/pull/1641>`_
|
||||
@@ -581,8 +550,7 @@ Features
|
||||
Version 20.6.1
|
||||
---------------
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
*
|
||||
`#1760 <https://github.com/sanic-org/sanic/pull/1760>`_
|
||||
@@ -596,8 +564,7 @@ Features
|
||||
`#1880 <https://github.com/sanic-org/sanic/pull/1880>`_
|
||||
Add handler names for websockets for url_for usage
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
*
|
||||
`#1776 <https://github.com/sanic-org/sanic/pull/1776>`_
|
||||
@@ -619,15 +586,13 @@ Bugfixes
|
||||
`#1853 <https://github.com/sanic-org/sanic/pull/1853>`_
|
||||
Fix pickle error when attempting to pickle an application which contains websocket routes
|
||||
|
||||
Deprecations and Removals
|
||||
*************************
|
||||
**Deprecations and Removals**
|
||||
|
||||
*
|
||||
`#1739 <https://github.com/sanic-org/sanic/pull/1739>`_
|
||||
Deprecate body_bytes to merge into body
|
||||
|
||||
Developer infrastructure
|
||||
************************
|
||||
**Developer infrastructure**
|
||||
|
||||
*
|
||||
`#1852 <https://github.com/sanic-org/sanic/pull/1852>`_
|
||||
@@ -642,8 +607,7 @@ Developer infrastructure
|
||||
Wrap run()'s "protocol" type annotation in Optional[]
|
||||
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
**Improved Documentation**
|
||||
|
||||
*
|
||||
`#1846 <https://github.com/sanic-org/sanic/pull/1846>`_
|
||||
@@ -663,8 +627,7 @@ Version 20.6.0
|
||||
Version 20.3.0
|
||||
---------------
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
*
|
||||
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
|
||||
@@ -695,8 +658,7 @@ Features
|
||||
`#1820 <https://github.com/sanic-org/sanic/pull/1820>`_
|
||||
Do not set content-type and content-length headers in exceptions
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
*
|
||||
`#1748 <https://github.com/sanic-org/sanic/pull/1748>`_
|
||||
@@ -714,8 +676,7 @@ Bugfixes
|
||||
`#1808 <https://github.com/sanic-org/sanic/pull/1808>`_
|
||||
Fix Ctrl+C and tests on Windows
|
||||
|
||||
Deprecations and Removals
|
||||
*************************
|
||||
**Deprecations and Removals**
|
||||
|
||||
*
|
||||
`#1800 <https://github.com/sanic-org/sanic/pull/1800>`_
|
||||
@@ -733,8 +694,7 @@ Deprecations and Removals
|
||||
`#1818 <https://github.com/sanic-org/sanic/pull/1818>`_
|
||||
Complete deprecation of ``app.remove_route`` and ``request.raw_args``
|
||||
|
||||
Dependencies
|
||||
************
|
||||
**Dependencies**
|
||||
|
||||
*
|
||||
`#1794 <https://github.com/sanic-org/sanic/pull/1794>`_
|
||||
@@ -744,15 +704,13 @@ Dependencies
|
||||
`#1806 <https://github.com/sanic-org/sanic/pull/1806>`_
|
||||
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
|
||||
|
||||
Developer infrastructure
|
||||
************************
|
||||
**Developer infrastructure**
|
||||
|
||||
*
|
||||
`#1833 <https://github.com/sanic-org/sanic/pull/1833>`_
|
||||
Resolve broken documentation builds
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
**Improved Documentation**
|
||||
|
||||
*
|
||||
`#1755 <https://github.com/sanic-org/sanic/pull/1755>`_
|
||||
@@ -794,8 +752,7 @@ Improved Documentation
|
||||
Version 19.12.0
|
||||
---------------
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
- Fix blueprint middleware application
|
||||
|
||||
@@ -814,8 +771,7 @@ Bugfixes
|
||||
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
**Improved Documentation**
|
||||
|
||||
- Move docs from MD to RST
|
||||
|
||||
@@ -829,8 +785,7 @@ Improved Documentation
|
||||
Version 19.6.3
|
||||
--------------
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
- Enable Towncrier Support
|
||||
|
||||
@@ -838,8 +793,7 @@ Features
|
||||
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
**Improved Documentation**
|
||||
|
||||
- Documentation infrastructure changes
|
||||
|
||||
@@ -852,8 +806,7 @@ Improved Documentation
|
||||
Version 19.6.2
|
||||
--------------
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
*
|
||||
`#1562 <https://github.com/sanic-org/sanic/pull/1562>`_
|
||||
@@ -869,8 +822,7 @@ Features
|
||||
Add Configure support from object string
|
||||
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
*
|
||||
`#1587 <https://github.com/sanic-org/sanic/pull/1587>`_
|
||||
@@ -888,8 +840,7 @@ Bugfixes
|
||||
`#1594 <https://github.com/sanic-org/sanic/pull/1594>`_
|
||||
Strict Slashes behavior fix
|
||||
|
||||
Deprecations and Removals
|
||||
*************************
|
||||
**Deprecations and Removals**
|
||||
|
||||
*
|
||||
`#1544 <https://github.com/sanic-org/sanic/pull/1544>`_
|
||||
@@ -913,8 +864,7 @@ Deprecations and Removals
|
||||
Version 19.3
|
||||
------------
|
||||
|
||||
Features
|
||||
********
|
||||
**Features**
|
||||
|
||||
*
|
||||
`#1497 <https://github.com/sanic-org/sanic/pull/1497>`_
|
||||
@@ -982,8 +932,7 @@ Features
|
||||
|
||||
This is a breaking change.
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
**Bugfixes**
|
||||
|
||||
|
||||
*
|
||||
@@ -1019,8 +968,7 @@ Bugfixes
|
||||
This allows the access log to be disabled for example when running via
|
||||
gunicorn.
|
||||
|
||||
Developer infrastructure
|
||||
************************
|
||||
**Developer infrastructure**
|
||||
|
||||
* `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials
|
||||
* `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
|
||||
@@ -1028,8 +976,7 @@ Developer infrastructure
|
||||
* `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
|
||||
* `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
**Improved Documentation**
|
||||
|
||||
* `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation
|
||||
* `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example
|
||||
@@ -1096,15 +1043,13 @@ Version 18.12
|
||||
Version 0.8
|
||||
-----------
|
||||
|
||||
0.8.3
|
||||
*****
|
||||
**0.8.3**
|
||||
|
||||
* Changes:
|
||||
|
||||
* Ownership changed to org 'sanic-org'
|
||||
|
||||
0.8.0
|
||||
*****
|
||||
**0.8.0**
|
||||
|
||||
* Changes:
|
||||
|
||||
@@ -1184,19 +1129,16 @@ Version 0.1
|
||||
-----------
|
||||
|
||||
|
||||
0.1.7
|
||||
*****
|
||||
**0.1.7**
|
||||
|
||||
* Reversed static url and directory arguments to meet spec
|
||||
|
||||
0.1.6
|
||||
*****
|
||||
**0.1.6**
|
||||
|
||||
* Static files
|
||||
* Lazy Cookie Loading
|
||||
|
||||
0.1.5
|
||||
*****
|
||||
**0.1.5**
|
||||
|
||||
* Cookies
|
||||
* Blueprint listeners and ordering
|
||||
@@ -1204,23 +1146,19 @@ Version 0.1
|
||||
* Fix: Incomplete file reads on medium+ sized post requests
|
||||
* Breaking: after_start and before_stop now pass sanic as their first argument
|
||||
|
||||
0.1.4
|
||||
*****
|
||||
**0.1.4**
|
||||
|
||||
* Multiprocessing
|
||||
|
||||
0.1.3
|
||||
*****
|
||||
**0.1.3**
|
||||
|
||||
* Blueprint support
|
||||
* Faster Response processing
|
||||
|
||||
0.1.1 - 0.1.2
|
||||
*************
|
||||
**0.1.1 - 0.1.2**
|
||||
|
||||
* Struggling to update pypi via CI
|
||||
|
||||
0.1.0
|
||||
*****
|
||||
**0.1.0**
|
||||
|
||||
* Released to public
|
||||
|
||||
@@ -140,6 +140,7 @@ To maintain the code consistency, Sanic uses following tools.
|
||||
#. `isort <https://github.com/timothycrosley/isort>`_
|
||||
#. `black <https://github.com/python/black>`_
|
||||
#. `flake8 <https://github.com/PyCQA/flake8>`_
|
||||
#. `slotscheck <https://github.com/ariebovenberg/slotscheck>`_
|
||||
|
||||
isort
|
||||
*****
|
||||
@@ -167,7 +168,13 @@ flake8
|
||||
#. pycodestyle
|
||||
#. Ned Batchelder's McCabe script
|
||||
|
||||
``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks.
|
||||
slotscheck
|
||||
**********
|
||||
|
||||
``slotscheck`` ensures that there are no problems with ``__slots__``
|
||||
(e.g. overlaps, or missing slots in base classes).
|
||||
|
||||
``isort``\ , ``black``\ , ``flake8`` and ``slotscheck`` checks are performed during ``tox`` lint checks.
|
||||
|
||||
The **easiest** way to make your code conform is to run the following before committing.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
|
||||
:stub-columns: 1
|
||||
|
||||
* - Build
|
||||
- | |Py39Test| |Py38Test| |Py37Test|
|
||||
- | |Py310Test| |Py39Test| |Py38Test| |Py37Test|
|
||||
* - Docs
|
||||
- | |UserGuide| |Documentation|
|
||||
* - Package
|
||||
@@ -27,6 +27,8 @@ Sanic | Build fast. Run fast.
|
||||
:target: https://community.sanicframework.org/
|
||||
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
|
||||
:target: https://discord.gg/FARQzAEMAA
|
||||
.. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main
|
||||
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml
|
||||
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
|
||||
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
|
||||
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
|
||||
@@ -64,7 +66,7 @@ Sanic | Build fast. Run fast.
|
||||
|
||||
Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
||||
|
||||
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-asgi>`_.
|
||||
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanicframework.org/en/guide/deployment/running.html#asgi>`_.
|
||||
|
||||
`Source code on GitHub <https://github.com/sanic-org/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_ | `User Guide <https://sanicframework.org>`_ | `Chat on Discord <https://discord.gg/FARQzAEMAA>`_
|
||||
|
||||
|
||||
27
codecov.yml
Normal file
27
codecov.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.75
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.5
|
||||
precision: 3
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
ignore:
|
||||
- "sanic/__main__.py"
|
||||
- "sanic/compat.py"
|
||||
- "sanic/reloader_helpers.py"
|
||||
- "sanic/simple.py"
|
||||
- "sanic/utils.py"
|
||||
- "sanic/cli"
|
||||
- ".github/"
|
||||
- "changelogs/"
|
||||
- "docker/"
|
||||
- "docs/"
|
||||
- "examples/"
|
||||
- "scripts/"
|
||||
- "tests/"
|
||||
@@ -38,10 +38,3 @@ sanic.views
|
||||
.. automodule:: sanic.views
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.websocket
|
||||
---------------
|
||||
|
||||
.. automodule:: sanic.websocket
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
📜 Changelog
|
||||
============
|
||||
|
||||
.. mdinclude:: ./releases/21.9.md
|
||||
|
||||
.. mdinclude:: ./releases/22/22.3.md
|
||||
.. mdinclude:: ./releases/21/21.12.md
|
||||
.. mdinclude:: ./releases/21/21.9.md
|
||||
.. include:: ../../CHANGELOG.rst
|
||||
|
||||
64
docs/sanic/releases/21/21.12.md
Normal file
64
docs/sanic/releases/21/21.12.md
Normal file
@@ -0,0 +1,64 @@
|
||||
## 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
|
||||
@@ -1,4 +1,14 @@
|
||||
## Version 21.9
|
||||
## Version 21.9.3
|
||||
*Rerelease of v21.9.2 with some cleanup*
|
||||
|
||||
## Version 21.9.2
|
||||
- [#2268](https://github.com/sanic-org/sanic/pull/2268) Make HTTP connections start in IDLE stage, avoiding delays and error messages
|
||||
- [#2310](https://github.com/sanic-org/sanic/pull/2310) More consistent config setting with post-FALLBACK_ERROR_FORMAT apply
|
||||
|
||||
## Version 21.9.1
|
||||
- [#2259](https://github.com/sanic-org/sanic/pull/2259) Allow non-conforming ErrorHandlers
|
||||
|
||||
## Version 21.9.0
|
||||
|
||||
### Features
|
||||
- [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets
|
||||
52
docs/sanic/releases/22/22.3.md
Normal file
52
docs/sanic/releases/22/22.3.md
Normal file
@@ -0,0 +1,52 @@
|
||||
## Version 22.3.0
|
||||
|
||||
### Features
|
||||
- [#2347](https://github.com/sanic-org/sanic/pull/2347) API for multi-application server
|
||||
- 🚨 *BREAKING CHANGE*: The old `sanic.worker.GunicornWorker` has been **removed**. To run Sanic with `gunicorn`, you should use it thru `uvicorn` [as described in their docs](https://www.uvicorn.org/#running-with-gunicorn).
|
||||
- 🧁 *SIDE EFFECT*: Named background tasks are now supported, even in Python 3.7
|
||||
- [#2357](https://github.com/sanic-org/sanic/pull/2357) Parse `Authorization` header as `Request.credentials`
|
||||
- [#2361](https://github.com/sanic-org/sanic/pull/2361) Add config option to skip `Touchup` step in application startup
|
||||
- [#2372](https://github.com/sanic-org/sanic/pull/2372) Updates to CLI help messaging
|
||||
- [#2382](https://github.com/sanic-org/sanic/pull/2382) Downgrade warnings to backwater debug messages
|
||||
- [#2396](https://github.com/sanic-org/sanic/pull/2396) Allow for `multidict` v0.6
|
||||
- [#2401](https://github.com/sanic-org/sanic/pull/2401) Upgrade CLI catching for alternative application run types
|
||||
- [#2402](https://github.com/sanic-org/sanic/pull/2402) Conditionally inject CLI arguments into factory
|
||||
- [#2413](https://github.com/sanic-org/sanic/pull/2413) Add new start and stop event listeners to reloader process
|
||||
- [#2414](https://github.com/sanic-org/sanic/pull/2414) Remove loop as required listener arg
|
||||
- [#2415](https://github.com/sanic-org/sanic/pull/2415) Better exception for bad URL parsing
|
||||
- [sanic-routing#47](https://github.com/sanic-org/sanic-routing/pull/47) Add a new extention parameter type: `<file:ext>`, `<file:ext=jpg>`, `<file:ext=jpg|png|gif|svg>`, `<file=int:ext>`, `<file=int:ext=jpg|png|gif|svg>`, `<file=float:ext=tar.gz>`
|
||||
- 👶 *BETA FEATURE*: This feature will not work with `path` type matching, and is being released as a beta feature only.
|
||||
- [sanic-routing#57](https://github.com/sanic-org/sanic-routing/pull/57) Change `register_pattern` to accept a `str` or `Pattern`
|
||||
- [sanic-routing#58](https://github.com/sanic-org/sanic-routing/pull/58) Default matching on non-empty strings only, and new `strorempty` pattern type
|
||||
- 🚨 *BREAKING CHANGE*: Previously a route with a dynamic string parameter (`/<foo>` or `/<foo:str>`) would match on any string, including empty strings. It will now **only** match a non-empty string. To retain the old behavior, you should use the new parameter type: `/<foo:strorempty>`.
|
||||
|
||||
### Bugfixes
|
||||
- [#2373](https://github.com/sanic-org/sanic/pull/2373) Remove `error_logger` on websockets
|
||||
- [#2381](https://github.com/sanic-org/sanic/pull/2381) Fix newly assigned `None` in task registry
|
||||
- [sanic-routing#52](https://github.com/sanic-org/sanic-routing/pull/52) Add type casting to regex route matching
|
||||
- [sanic-routing#60](https://github.com/sanic-org/sanic-routing/pull/60) Add requirements check on regex routes (this resolves, for example, multiple static directories with differing `host` values)
|
||||
|
||||
### Deprecations and Removals
|
||||
- [#2362](https://github.com/sanic-org/sanic/pull/2362) 22.3 Deprecations and changes
|
||||
1. `debug=True` and `--debug` do _NOT_ automatically run `auto_reload`
|
||||
2. Default error render is with plain text (browsers still get HTML by default because `auto` looks at headers)
|
||||
3. `config` is required for `ErrorHandler.finalize`
|
||||
4. `ErrorHandler.lookup` requires two positional args
|
||||
5. Unused websocket protocol args removed
|
||||
- [#2344](https://github.com/sanic-org/sanic/pull/2344) Deprecate loading of lowercase environment variables
|
||||
|
||||
### Developer infrastructure
|
||||
- [#2363](https://github.com/sanic-org/sanic/pull/2363) Revert code coverage back to Codecov
|
||||
- [#2405](https://github.com/sanic-org/sanic/pull/2405) Upgrade tests for `sanic-routing` changes
|
||||
- [sanic-testing#35](https://github.com/sanic-org/sanic-testing/pull/35) Allow for httpx v0.22
|
||||
|
||||
### Improved Documentation
|
||||
- [#2350](https://github.com/sanic-org/sanic/pull/2350) Fix link in README for ASGI
|
||||
- [#2398](https://github.com/sanic-org/sanic/pull/2398) Document middleware on_request and on_response
|
||||
- [#2409](https://github.com/sanic-org/sanic/pull/2409) Add missing documentation for `Request.respond`
|
||||
|
||||
### Miscellaneous
|
||||
- [#2376](https://github.com/sanic-org/sanic/pull/2376) Fix typing for `ListenerMixin.listener`
|
||||
- [#2383](https://github.com/sanic-org/sanic/pull/2383) Clear deprecation warning in `asyncio.wait`
|
||||
- [#2387](https://github.com/sanic-org/sanic/pull/2387) Cleanup `__slots__` implementations
|
||||
- [#2390](https://github.com/sanic-org/sanic/pull/2390) Clear deprecation warning in `asyncio.get_event_loop`
|
||||
@@ -5,7 +5,7 @@ import asyncio
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
async def notify_server_started_after_five_seconds():
|
||||
|
||||
@@ -4,7 +4,7 @@ from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.middleware("request")
|
||||
|
||||
@@ -6,7 +6,7 @@ from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
def check_request_for_authorization_status(request):
|
||||
|
||||
@@ -8,9 +8,9 @@ are added. And blueprint response middleware are executed in _reverse_ order.
|
||||
On a valid request, it should print "1 2 3 6 5 4" to terminal
|
||||
"""
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
bp = Blueprint("bp_" + __name__)
|
||||
bp = Blueprint("bp_example")
|
||||
|
||||
|
||||
@bp.on_request
|
||||
|
||||
@@ -2,10 +2,10 @@ from sanic import Blueprint, Sanic
|
||||
from sanic.response import file, json
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
blueprint = Blueprint("name", url_prefix="/my_blueprint")
|
||||
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2")
|
||||
blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3")
|
||||
app = Sanic("Example")
|
||||
blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
|
||||
blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")
|
||||
blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3")
|
||||
|
||||
|
||||
@blueprint.route("/foo")
|
||||
|
||||
@@ -3,7 +3,8 @@ from asyncio import sleep
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__, strict_slashes=True)
|
||||
app = Sanic("DelayedResponseApp", strict_slashes=True)
|
||||
app.config.AUTO_EXTEND = False
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@@ -11,7 +12,7 @@ async def handler(request):
|
||||
return response.redirect("/sleep/3")
|
||||
|
||||
|
||||
@app.get("/sleep/<t:number>")
|
||||
@app.get("/sleep/<t:float>")
|
||||
async def handler2(request, t=0.3):
|
||||
await sleep(t)
|
||||
return response.text(f"Slept {t:.1f} seconds.\n")
|
||||
|
||||
@@ -41,7 +41,7 @@ from sanic import Sanic
|
||||
|
||||
|
||||
handler = CustomHandler()
|
||||
app = Sanic(__name__, error_handler=handler)
|
||||
app = Sanic("Example", error_handler=handler)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from sanic import Sanic, response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@@ -9,5 +9,5 @@ async def test(request):
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -6,7 +6,7 @@ from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
sem = None
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ LOG_SETTINGS = {
|
||||
}
|
||||
|
||||
|
||||
app = Sanic(__name__, log_config=LOG_SETTINGS)
|
||||
app = Sanic("Example", log_config=LOG_SETTINGS)
|
||||
|
||||
|
||||
@app.on_request
|
||||
|
||||
@@ -43,7 +43,7 @@ logdna = logging.getLogger(__name__)
|
||||
logdna.setLevel(logging.INFO)
|
||||
logdna.addHandler(logdna_handler)
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.middleware
|
||||
|
||||
@@ -2,27 +2,29 @@
|
||||
Modify header or status in response
|
||||
"""
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
@app.route('/')
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def handle_request(request):
|
||||
return response.json(
|
||||
{'message': 'Hello world!'},
|
||||
headers={'X-Served-By': 'sanic'},
|
||||
status=200
|
||||
{"message": "Hello world!"},
|
||||
headers={"X-Served-By": "sanic"},
|
||||
status=200,
|
||||
)
|
||||
|
||||
|
||||
@app.route('/unauthorized')
|
||||
@app.route("/unauthorized")
|
||||
def handle_request(request):
|
||||
return response.json(
|
||||
{'message': 'You are not authorized'},
|
||||
headers={'X-Served-By': 'sanic'},
|
||||
status=404
|
||||
{"message": "You are not authorized"},
|
||||
headers={"X-Served-By": "sanic"},
|
||||
status=404,
|
||||
)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_port(worker_id):
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app():
|
||||
app = Sanic()
|
||||
app = Sanic("Example")
|
||||
|
||||
@app.route("/")
|
||||
async def index(request):
|
||||
|
||||
@@ -8,7 +8,6 @@ from sanic.handlers import ErrorHandler
|
||||
|
||||
|
||||
class RaygunExceptionReporter(ErrorHandler):
|
||||
|
||||
def __init__(self, raygun_api_key=None):
|
||||
super().__init__()
|
||||
if raygun_api_key is None:
|
||||
@@ -22,16 +21,13 @@ class RaygunExceptionReporter(ErrorHandler):
|
||||
|
||||
|
||||
raygun_error_reporter = RaygunExceptionReporter()
|
||||
app = Sanic(__name__, error_handler=raygun_error_reporter)
|
||||
app = Sanic("Example", error_handler=raygun_error_reporter)
|
||||
|
||||
|
||||
@app.route("/raise")
|
||||
async def test(request):
|
||||
raise SanicException('You Broke It!')
|
||||
raise SanicException("You Broke It!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=getenv("PORT", 8080)
|
||||
)
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from sanic import Sanic, response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def handle_request(request):
|
||||
return response.redirect('/redirect')
|
||||
return response.redirect("/redirect")
|
||||
|
||||
|
||||
@app.route('/redirect')
|
||||
@app.route("/redirect")
|
||||
async def test(request):
|
||||
return response.json({"Redirected": True})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -1,65 +1,63 @@
|
||||
from sanic import Sanic
|
||||
from sanic.views import CompositionView
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.views import stream as stream_decorator
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import stream, text
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.views import stream as stream_decorator
|
||||
|
||||
bp = Blueprint('blueprint_request_stream')
|
||||
app = Sanic('request_stream')
|
||||
|
||||
bp = Blueprint("bp_example")
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
@stream_decorator
|
||||
async def post(self, request):
|
||||
result = ''
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
|
||||
@app.post('/stream', stream=True)
|
||||
@app.post("/stream", stream=True)
|
||||
async def handler(request):
|
||||
async def streaming(response):
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
body = body.decode('utf-8').replace('1', 'A')
|
||||
body = body.decode("utf-8").replace("1", "A")
|
||||
await response.write(body)
|
||||
|
||||
return stream(streaming)
|
||||
|
||||
|
||||
@bp.put('/bp_stream', stream=True)
|
||||
@bp.put("/bp_stream", stream=True)
|
||||
async def bp_handler(request):
|
||||
result = ''
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8').replace('1', 'A')
|
||||
result += body.decode("utf-8").replace("1", "A")
|
||||
return text(result)
|
||||
|
||||
|
||||
async def post_handler(request):
|
||||
result = ''
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
|
||||
app.blueprint(bp)
|
||||
app.add_route(SimpleView.as_view(), '/method_view')
|
||||
view = CompositionView()
|
||||
view.add(['POST'], post_handler, stream=True)
|
||||
app.add_route(view, '/composition_view')
|
||||
app.add_route(SimpleView.as_view(), "/method_view")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import asyncio
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
from sanic import Sanic, response
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import RequestTimeout
|
||||
|
||||
|
||||
Config.REQUEST_TIMEOUT = 1
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
await asyncio.sleep(3)
|
||||
return response.text('Hello, world!')
|
||||
return response.text("Hello, world!")
|
||||
|
||||
|
||||
@app.exception(RequestTimeout)
|
||||
def timeout(request, exception):
|
||||
return response.text('RequestTimeout from error_handler.', 408)
|
||||
return response.text("RequestTimeout from error_handler.", 408)
|
||||
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
from os import getenv
|
||||
|
||||
import rollbar
|
||||
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import SanicException
|
||||
from os import getenv
|
||||
from sanic.handlers import ErrorHandler
|
||||
|
||||
|
||||
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
||||
|
||||
|
||||
class RollbarExceptionHandler(ErrorHandler):
|
||||
|
||||
def default(self, request, exception):
|
||||
rollbar.report_message(str(exception))
|
||||
return super().default(request, exception)
|
||||
|
||||
|
||||
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
|
||||
app = Sanic("Example", error_handler=RollbarExceptionHandler())
|
||||
|
||||
|
||||
@app.route("/raise")
|
||||
@@ -24,7 +25,4 @@ def create_error(request):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=getenv("PORT", 8080)
|
||||
)
|
||||
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
||||
|
||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route("/text")
|
||||
|
||||
@@ -5,7 +5,7 @@ import uvloop
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
|
||||
@@ -8,7 +8,7 @@ from sanic import Sanic, response
|
||||
from sanic.server import AsyncioServer
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.before_server_start
|
||||
|
||||
@@ -6,20 +6,19 @@ from sentry_sdk.integrations.sanic import SanicIntegration
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
sentry_init(
|
||||
dsn=getenv("SENTRY_DSN"),
|
||||
integrations=[SanicIntegration()],
|
||||
)
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@app.route("/working")
|
||||
async def working_path(request):
|
||||
return json({
|
||||
"response": "Working API Response"
|
||||
})
|
||||
return json({"response": "Working API Response"})
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@@ -28,8 +27,5 @@ async def raise_error(request):
|
||||
raise Exception("Testing Sentry Integration")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=getenv("PORT", 8080)
|
||||
)
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
app.static("/", "./static")
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response as res
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(req):
|
||||
return res.text("I\'m a teapot", status=418)
|
||||
return res.text("I'm a teapot", status=418)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -5,7 +5,7 @@ from sanic.exceptions import ServerError
|
||||
from sanic.log import logger as log
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
|
||||
@@ -4,7 +4,7 @@ import socket
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@app.route("/test")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
@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/answer
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
bp = Blueprint("bp", host="bp.example.com")
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from sanic import Sanic
|
||||
from sanic.response import redirect
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic("Example")
|
||||
|
||||
|
||||
app.static("index.html", "websocket.html")
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
requires = ["setuptools<60.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
@@ -6,4 +6,4 @@ python:
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
system_packages: true
|
||||
system_packages: true
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "21.12.0dev"
|
||||
__version__ = "22.3.1"
|
||||
|
||||
850
sanic/app.py
850
sanic/app.py
File diff suppressed because it is too large
Load Diff
39
sanic/application/ext.py
Normal file
39
sanic/application/ext.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic import Sanic
|
||||
|
||||
try:
|
||||
from sanic_ext import Extend # type: ignore
|
||||
except ImportError:
|
||||
...
|
||||
|
||||
|
||||
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
||||
if not app.config.AUTO_EXTEND:
|
||||
return
|
||||
|
||||
sanic_ext = None
|
||||
with suppress(ModuleNotFoundError):
|
||||
sanic_ext = import_module("sanic_ext")
|
||||
|
||||
if not sanic_ext:
|
||||
if fail:
|
||||
raise RuntimeError(
|
||||
"Sanic Extensions is not installed. You can add it to your "
|
||||
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
|
||||
"install sanic-ext"
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if not getattr(app, "_ext", None):
|
||||
Ext: Extend = getattr(sanic_ext, "Extend")
|
||||
app._ext = Ext(app, **kwargs)
|
||||
|
||||
return app.ext
|
||||
@@ -41,9 +41,6 @@ class MOTD(ABC):
|
||||
|
||||
|
||||
class MOTDBasic(MOTD):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def display(self):
|
||||
if self.logo:
|
||||
logger.debug(self.logo)
|
||||
|
||||
@@ -3,14 +3,17 @@ from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from enum import Enum, IntEnum, auto
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Set, Union
|
||||
from socket import socket
|
||||
from ssl import SSLContext
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
|
||||
|
||||
from sanic.log import logger
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
@@ -30,6 +33,19 @@ class Mode(StrEnum):
|
||||
DEBUG = auto()
|
||||
|
||||
|
||||
class ServerStage(IntEnum):
|
||||
STOPPED = auto()
|
||||
PARTIAL = auto()
|
||||
SERVING = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationServerInfo:
|
||||
settings: Dict[str, Any]
|
||||
stage: ServerStage = field(default=ServerStage.STOPPED)
|
||||
server: Optional[AsyncioServer] = field(default=None)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationState:
|
||||
app: Sanic
|
||||
@@ -37,15 +53,21 @@ class ApplicationState:
|
||||
coffee: bool = field(default=False)
|
||||
fast: bool = field(default=False)
|
||||
host: str = field(default="")
|
||||
mode: Mode = field(default=Mode.PRODUCTION)
|
||||
port: int = field(default=0)
|
||||
ssl: Optional[SSLContext] = field(default=None)
|
||||
sock: Optional[socket] = field(default=None)
|
||||
unix: Optional[str] = field(default=None)
|
||||
mode: Mode = field(default=Mode.PRODUCTION)
|
||||
reload_dirs: Set[Path] = field(default_factory=set)
|
||||
auto_reload: bool = field(default=False)
|
||||
server: Server = field(default=Server.SANIC)
|
||||
is_running: bool = field(default=False)
|
||||
is_started: bool = field(default=False)
|
||||
is_stopping: bool = field(default=False)
|
||||
verbosity: int = field(default=0)
|
||||
workers: int = field(default=0)
|
||||
primary: bool = field(default=True)
|
||||
server_info: List[ApplicationServerInfo] = field(default_factory=list)
|
||||
|
||||
# This property relates to the ApplicationState instance and should
|
||||
# not be changed except in the __post_init__ method
|
||||
@@ -72,3 +94,17 @@ class ApplicationState:
|
||||
@property
|
||||
def is_debug(self):
|
||||
return self.mode is Mode.DEBUG
|
||||
|
||||
@property
|
||||
def stage(self) -> ServerStage:
|
||||
if not self.server_info:
|
||||
return ServerStage.STOPPED
|
||||
|
||||
if all(info.stage is ServerStage.SERVING for info in self.server_info):
|
||||
return ServerStage.SERVING
|
||||
elif any(
|
||||
info.stage is ServerStage.SERVING for info in self.server_info
|
||||
):
|
||||
return ServerStage.PARTIAL
|
||||
|
||||
return ServerStage.STOPPED
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
import sanic.app # noqa
|
||||
|
||||
from sanic.compat import Header
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.helpers import _default
|
||||
from sanic.http import Stage
|
||||
from sanic.log import logger
|
||||
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
|
||||
from sanic.request import Request
|
||||
from sanic.response import BaseHTTPResponse
|
||||
@@ -15,30 +17,35 @@ from sanic.server import ConnInfo
|
||||
from sanic.server.websockets.connection import WebSocketConnection
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self, asgi_app: "ASGIApp") -> None:
|
||||
def __init__(self, asgi_app: ASGIApp) -> None:
|
||||
self.asgi_app = asgi_app
|
||||
|
||||
if (
|
||||
"server.init.before"
|
||||
in self.asgi_app.sanic_app.signal_router.name_index
|
||||
):
|
||||
warnings.warn(
|
||||
'You have set a listener for "before_server_start" '
|
||||
"in ASGI mode. "
|
||||
"It will be executed as early as possible, but not before "
|
||||
"the ASGI server is started."
|
||||
)
|
||||
if (
|
||||
"server.shutdown.after"
|
||||
in self.asgi_app.sanic_app.signal_router.name_index
|
||||
):
|
||||
warnings.warn(
|
||||
'You have set a listener for "after_server_stop" '
|
||||
"in ASGI mode. "
|
||||
"It will be executed as late as possible, but not after "
|
||||
"the ASGI server is stopped."
|
||||
)
|
||||
if self.asgi_app.sanic_app.state.verbosity > 0:
|
||||
if (
|
||||
"server.init.before"
|
||||
in self.asgi_app.sanic_app.signal_router.name_index
|
||||
):
|
||||
logger.debug(
|
||||
'You have set a listener for "before_server_start" '
|
||||
"in ASGI mode. "
|
||||
"It will be executed as early as possible, but not before "
|
||||
"the ASGI server is started."
|
||||
)
|
||||
if (
|
||||
"server.shutdown.after"
|
||||
in self.asgi_app.sanic_app.signal_router.name_index
|
||||
):
|
||||
logger.debug(
|
||||
'You have set a listener for "after_server_stop" '
|
||||
"in ASGI mode. "
|
||||
"It will be executed as late as possible, but not after "
|
||||
"the ASGI server is stopped."
|
||||
)
|
||||
|
||||
async def startup(self) -> None:
|
||||
"""
|
||||
@@ -53,6 +60,13 @@ class Lifespan:
|
||||
await self.asgi_app.sanic_app._server_event("init", "before")
|
||||
await self.asgi_app.sanic_app._server_event("init", "after")
|
||||
|
||||
if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
|
||||
warnings.warn(
|
||||
"You have set the USE_UVLOOP configuration option, but Sanic "
|
||||
"cannot control the event loop when running in ASGI mode."
|
||||
"This option will be ignored."
|
||||
)
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
Gather the listeners to fire on server stop.
|
||||
@@ -80,7 +94,7 @@ class Lifespan:
|
||||
|
||||
|
||||
class ASGIApp:
|
||||
sanic_app: "sanic.app.Sanic"
|
||||
sanic_app: Sanic
|
||||
request: Request
|
||||
transport: MockTransport
|
||||
lifespan: Lifespan
|
||||
|
||||
0
sanic/base/__init__.py
Normal file
0
sanic/base/__init__.py
Normal file
6
sanic/base/meta.py
Normal file
6
sanic/base/meta.py
Normal file
@@ -0,0 +1,6 @@
|
||||
class SanicMeta(type):
|
||||
@classmethod
|
||||
def __prepare__(metaclass, name, bases, **kwds):
|
||||
cls = super().__prepare__(metaclass, name, bases, **kwds)
|
||||
cls["__slots__"] = ()
|
||||
return cls
|
||||
@@ -1,8 +1,8 @@
|
||||
import re
|
||||
|
||||
from typing import Any, Tuple
|
||||
from warnings import warn
|
||||
from typing import Any
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.mixins.exceptions import ExceptionMixin
|
||||
from sanic.mixins.listeners import ListenerMixin
|
||||
@@ -20,8 +20,9 @@ class BaseSanic(
|
||||
ListenerMixin,
|
||||
ExceptionMixin,
|
||||
SignalMixin,
|
||||
metaclass=SanicMeta,
|
||||
):
|
||||
__fake_slots__: Tuple[str, ...]
|
||||
__slots__ = ("name",)
|
||||
|
||||
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
|
||||
class_name = self.__class__.__name__
|
||||
@@ -33,11 +34,10 @@ class BaseSanic(
|
||||
)
|
||||
|
||||
if not VALID_NAME.match(name):
|
||||
warn(
|
||||
f"{class_name} instance named '{name}' uses a format that is"
|
||||
f"deprecated. Starting in version 21.12, {class_name} objects "
|
||||
"must be named only using alphanumeric characters, _, or -.",
|
||||
DeprecationWarning,
|
||||
raise SanicException(
|
||||
f"{class_name} instance named '{name}' uses an invalid "
|
||||
"format. Names must begin with a character and may only "
|
||||
"contain alphanumeric characters, _, or -."
|
||||
)
|
||||
|
||||
self.name = name
|
||||
@@ -52,15 +52,12 @@ class BaseSanic(
|
||||
return f'{self.__class__.__name__}(name="{self.name}")'
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
# This is a temporary compat layer so we can raise a warning until
|
||||
# setting attributes on the app instance can be removed and deprecated
|
||||
# with a proper implementation of __slots__
|
||||
if name not in self.__fake_slots__:
|
||||
warn(
|
||||
try:
|
||||
super().__setattr__(name, value)
|
||||
except AttributeError as e:
|
||||
raise AttributeError(
|
||||
f"Setting variables on {self.__class__.__name__} instances is "
|
||||
"deprecated and will be removed in version 21.12. You should "
|
||||
f"change your {self.__class__.__name__} instance to use "
|
||||
"not allowed. You should change your "
|
||||
f"{self.__class__.__name__} instance to use "
|
||||
f"instance.ctx.{name} instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
super().__setattr__(name, value)
|
||||
) from e
|
||||
@@ -5,7 +5,7 @@ from functools import partial
|
||||
from typing import TYPE_CHECKING, List, Optional, Union
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from typing import (
|
||||
from sanic_routing.exceptions import NotFound # type: ignore
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.base import BaseSanic
|
||||
from sanic.base.root import BaseSanic
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.helpers import Default, _default
|
||||
@@ -36,8 +36,8 @@ from sanic.models.handler_types import (
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic # noqa
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def lazy(func, as_decorator=True):
|
||||
@@ -85,7 +85,7 @@ class Blueprint(BaseSanic):
|
||||
trailing */*
|
||||
"""
|
||||
|
||||
__fake_slots__ = (
|
||||
__slots__ = (
|
||||
"_apps",
|
||||
"_future_routes",
|
||||
"_future_statics",
|
||||
@@ -98,7 +98,6 @@ class Blueprint(BaseSanic):
|
||||
"host",
|
||||
"listeners",
|
||||
"middlewares",
|
||||
"name",
|
||||
"routes",
|
||||
"statics",
|
||||
"strict_slashes",
|
||||
@@ -348,6 +347,7 @@ class Blueprint(BaseSanic):
|
||||
future.static,
|
||||
version_prefix,
|
||||
error_format,
|
||||
future.route_context,
|
||||
)
|
||||
|
||||
if (self, apply_route) in app._future_registry:
|
||||
@@ -400,8 +400,9 @@ class Blueprint(BaseSanic):
|
||||
for future in self._future_signals:
|
||||
if (self, future) in app._future_registry:
|
||||
continue
|
||||
future.condition.update({"blueprint": self.name})
|
||||
app._apply_signal(future)
|
||||
future.condition.update({"__blueprint__": self.name})
|
||||
# Force exclusive to be False
|
||||
app._apply_signal(tuple((*future[:-1], False)))
|
||||
|
||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
||||
self.websocket_routes += [
|
||||
@@ -426,7 +427,7 @@ class Blueprint(BaseSanic):
|
||||
|
||||
async def dispatch(self, *args, **kwargs):
|
||||
condition = kwargs.pop("condition", {})
|
||||
condition.update({"blueprint": self.name})
|
||||
condition.update({"__blueprint__": self.name})
|
||||
kwargs["condition"] = condition
|
||||
await asyncio.gather(
|
||||
*[app.dispatch(*args, **kwargs) for app in self.apps]
|
||||
|
||||
@@ -68,6 +68,13 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
|
||||
parse_args = ["--version"] if legacy_version else None
|
||||
|
||||
if not parse_args:
|
||||
parsed, unknown = self.parser.parse_known_args()
|
||||
if unknown and parsed.factory:
|
||||
for arg in unknown:
|
||||
if arg.startswith("--"):
|
||||
self.parser.add_argument(arg.split("=")[0])
|
||||
|
||||
self.args = self.parser.parse_args(args=parse_args)
|
||||
self._precheck()
|
||||
|
||||
@@ -79,13 +86,6 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||
error_logger.exception("Failed to run app")
|
||||
|
||||
def _precheck(self):
|
||||
if self.args.debug and self.main_process:
|
||||
error_logger.warning(
|
||||
"Starting in v22.3, --debug will no "
|
||||
"longer automatically run the auto-reloader.\n Switch to "
|
||||
"--dev to continue using that functionality."
|
||||
)
|
||||
|
||||
# # Custom TLS mismatch handling for better diagnostics
|
||||
if self.main_process and (
|
||||
# one of cert/key missing
|
||||
@@ -120,6 +120,14 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||
delimiter = ":" if ":" in self.args.module else "."
|
||||
module_name, app_name = self.args.module.rsplit(delimiter, 1)
|
||||
|
||||
if module_name == "" and os.path.isdir(self.args.module):
|
||||
raise ValueError(
|
||||
"App not found.\n"
|
||||
" Please use --simple if you are passing a "
|
||||
"directory to sanic.\n"
|
||||
f" eg. sanic {self.args.module} --simple"
|
||||
)
|
||||
|
||||
if app_name.endswith("()"):
|
||||
self.args.factory = True
|
||||
app_name = app_name[:-2]
|
||||
@@ -127,14 +135,26 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
if self.args.factory:
|
||||
app = app()
|
||||
try:
|
||||
app = app(self.args)
|
||||
except TypeError:
|
||||
app = app()
|
||||
|
||||
app_type_name = type(app).__name__
|
||||
|
||||
if not isinstance(app, Sanic):
|
||||
if callable(app):
|
||||
solution = f"sanic {self.args.module} --factory"
|
||||
raise ValueError(
|
||||
"Module is not a Sanic app, it is a"
|
||||
f"{app_type_name}\n"
|
||||
" If this callable returns a"
|
||||
f"Sanic instance try: \n{solution}"
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
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?"
|
||||
)
|
||||
except ImportError as e:
|
||||
if module_name.startswith(e.name):
|
||||
@@ -174,16 +194,11 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||
"workers": self.args.workers,
|
||||
}
|
||||
|
||||
if self.args.auto_reload:
|
||||
kwargs["auto_reload"] = True
|
||||
for maybe_arg in ("auto_reload", "dev"):
|
||||
if getattr(self.args, maybe_arg, False):
|
||||
kwargs[maybe_arg] = True
|
||||
|
||||
if self.args.path:
|
||||
if self.args.auto_reload or self.args.debug:
|
||||
kwargs["reload_dir"] = self.args.path
|
||||
else:
|
||||
error_logger.warning(
|
||||
"Ignoring '--reload-dir' since auto reloading was not "
|
||||
"enabled. If you would like to watch directories for "
|
||||
"changes, consider using --debug or --auto-reload."
|
||||
)
|
||||
kwargs["auto_reload"] = True
|
||||
kwargs["reload_dir"] = self.args.path
|
||||
return kwargs
|
||||
|
||||
@@ -180,18 +180,10 @@ class DevelopmentGroup(Group):
|
||||
"--debug",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
help="Run the server in debug mode",
|
||||
)
|
||||
self.container.add_argument(
|
||||
"-d",
|
||||
"--dev",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Currently is an alias for --debug. But starting in v22.3, \n"
|
||||
"--debug will no longer automatically trigger auto_restart. \n"
|
||||
"However, --dev will continue, effectively making it the \n"
|
||||
"same as debug + auto_reload."
|
||||
"Run the server in DEBUG mode. It includes DEBUG logging,\n"
|
||||
"additional context on exceptions, and other settings\n"
|
||||
"not-safe for PRODUCTION, but helpful for debugging problems."
|
||||
),
|
||||
)
|
||||
self.container.add_argument(
|
||||
@@ -212,6 +204,13 @@ class DevelopmentGroup(Group):
|
||||
action="append",
|
||||
help="Extra directories to watch and reload on changes",
|
||||
)
|
||||
self.container.add_argument(
|
||||
"-d",
|
||||
"--dev",
|
||||
dest="dev",
|
||||
action="store_true",
|
||||
help=("debug + auto reload."),
|
||||
)
|
||||
|
||||
|
||||
class OutputGroup(Group):
|
||||
|
||||
@@ -8,6 +8,14 @@ from multidict import CIMultiDict # type: ignore
|
||||
|
||||
|
||||
OS_IS_WINDOWS = os.name == "nt"
|
||||
UVLOOP_INSTALLED = False
|
||||
|
||||
try:
|
||||
import uvloop # type: ignore # noqa
|
||||
|
||||
UVLOOP_INSTALLED = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def enable_windows_color_support():
|
||||
@@ -64,7 +72,7 @@ def ctrlc_workaround_for_windows(app):
|
||||
"""Asyncio wakeups to allow receiving SIGINT in Python"""
|
||||
while not die:
|
||||
# If someone else stopped the app, just exit
|
||||
if app.is_stopping:
|
||||
if app.state.is_stopping:
|
||||
return
|
||||
# Windows Python blocks signal handlers while the event loop is
|
||||
# waiting for I/O. Frequent wakeups keep interrupts flowing.
|
||||
|
||||
159
sanic/config.py
159
sanic/config.py
@@ -1,28 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from inspect import isclass
|
||||
from inspect import getmembers, isclass, isdatadescriptor
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||
from warnings import warn
|
||||
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
||||
|
||||
from sanic.errorpages import check_error_format
|
||||
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
||||
from sanic.helpers import Default, _default
|
||||
from sanic.http import Http
|
||||
from sanic.log import deprecation, error_logger
|
||||
from sanic.utils import load_module_from_file_location, str_to_bool
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
SANIC_PREFIX = "SANIC_"
|
||||
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"_FALLBACK_ERROR_FORMAT": _default,
|
||||
"ACCESS_LOG": True,
|
||||
"AUTO_EXTEND": True,
|
||||
"AUTO_RELOAD": False,
|
||||
"EVENT_AUTOREGISTER": False,
|
||||
"FALLBACK_ERROR_FORMAT": "auto",
|
||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||
"FORWARDED_SECRET": None,
|
||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||
@@ -40,17 +38,32 @@ DEFAULT_CONFIG = {
|
||||
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
||||
"TOUCHUP": True,
|
||||
"USE_UVLOOP": _default,
|
||||
"WEBSOCKET_MAX_SIZE": 2**20, # 1 megabyte
|
||||
"WEBSOCKET_PING_INTERVAL": 20,
|
||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||
}
|
||||
|
||||
# These values will be removed from the Config object in v22.6 and moved
|
||||
# to the application state
|
||||
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
|
||||
|
||||
class Config(dict):
|
||||
|
||||
class DescriptorMeta(type):
|
||||
def __init__(cls, *_):
|
||||
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
|
||||
|
||||
@staticmethod
|
||||
def _is_setter(member: object):
|
||||
return isdatadescriptor(member) and hasattr(member, "setter")
|
||||
|
||||
|
||||
class Config(dict, metaclass=DescriptorMeta):
|
||||
ACCESS_LOG: bool
|
||||
AUTO_EXTEND: bool
|
||||
AUTO_RELOAD: bool
|
||||
EVENT_AUTOREGISTER: bool
|
||||
FALLBACK_ERROR_FORMAT: str
|
||||
FORWARDED_FOR_HEADER: str
|
||||
FORWARDED_SECRET: Optional[str]
|
||||
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
||||
@@ -69,6 +82,8 @@ class Config(dict):
|
||||
REQUEST_TIMEOUT: int
|
||||
RESPONSE_TIMEOUT: int
|
||||
SERVER_NAME: str
|
||||
TOUCHUP: bool
|
||||
USE_UVLOOP: Union[Default, bool]
|
||||
WEBSOCKET_MAX_SIZE: int
|
||||
WEBSOCKET_PING_INTERVAL: int
|
||||
WEBSOCKET_PING_TIMEOUT: int
|
||||
@@ -76,33 +91,27 @@ class Config(dict):
|
||||
def __init__(
|
||||
self,
|
||||
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
|
||||
load_env: Optional[Union[bool, str]] = True,
|
||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||
keep_alive: Optional[bool] = None,
|
||||
*,
|
||||
app: Optional[Sanic] = None,
|
||||
converters: Optional[Sequence[Callable[[str], Any]]] = None,
|
||||
):
|
||||
defaults = defaults or {}
|
||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||
|
||||
self._app = app
|
||||
self._converters = [str, str_to_bool, float, int]
|
||||
self._LOGO = ""
|
||||
|
||||
if converters:
|
||||
for converter in converters:
|
||||
self.register_type(converter)
|
||||
|
||||
if keep_alive is not None:
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
|
||||
if env_prefix != SANIC_PREFIX:
|
||||
if env_prefix:
|
||||
self.load_environment_vars(env_prefix)
|
||||
elif load_env is not True:
|
||||
if load_env:
|
||||
self.load_environment_vars(prefix=load_env)
|
||||
warn(
|
||||
"Use of load_env is deprecated and will be removed in "
|
||||
"21.12. Modify the configuration prefix by passing "
|
||||
"env_prefix instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
else:
|
||||
self.load_environment_vars(SANIC_PREFIX)
|
||||
|
||||
@@ -123,9 +132,21 @@ class Config(dict):
|
||||
self.update({attr: value})
|
||||
|
||||
def update(self, *other, **kwargs) -> None:
|
||||
other_mapping = {k: v for item in other for k, v in dict(item).items()}
|
||||
super().update(*other, **kwargs)
|
||||
for attr, value in {**other_mapping, **kwargs}.items():
|
||||
kwargs.update({k: v for item in other for k, v in dict(item).items()})
|
||||
setters: Dict[str, Any] = {
|
||||
k: kwargs.pop(k)
|
||||
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)
|
||||
|
||||
def _post_set(self, attr, value) -> None:
|
||||
@@ -136,32 +157,37 @@ class Config(dict):
|
||||
"REQUEST_MAX_SIZE",
|
||||
):
|
||||
self._configure_header_size()
|
||||
elif attr == "FALLBACK_ERROR_FORMAT":
|
||||
self._check_error_format()
|
||||
if self.app and value != self.app.error_handler.fallback:
|
||||
if self.app.error_handler.fallback != "auto":
|
||||
warn(
|
||||
"Overriding non-default ErrorHandler fallback "
|
||||
"value. Changing from "
|
||||
f"{self.app.error_handler.fallback} to {value}."
|
||||
)
|
||||
self.app.error_handler.fallback = value
|
||||
elif attr == "LOGO":
|
||||
self._LOGO = value
|
||||
warn(
|
||||
deprecation(
|
||||
"Setting the config.LOGO is deprecated and will no longer "
|
||||
"be supported starting in v22.6.",
|
||||
DeprecationWarning,
|
||||
22.6,
|
||||
)
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
return self._app
|
||||
|
||||
@property
|
||||
def LOGO(self):
|
||||
return self._LOGO
|
||||
|
||||
@property
|
||||
def FALLBACK_ERROR_FORMAT(self) -> str:
|
||||
if self._FALLBACK_ERROR_FORMAT is _default:
|
||||
return DEFAULT_FORMAT
|
||||
return self._FALLBACK_ERROR_FORMAT
|
||||
|
||||
@FALLBACK_ERROR_FORMAT.setter
|
||||
def FALLBACK_ERROR_FORMAT(self, value):
|
||||
self._check_error_format(value)
|
||||
if (
|
||||
self._FALLBACK_ERROR_FORMAT is not _default
|
||||
and value != self._FALLBACK_ERROR_FORMAT
|
||||
):
|
||||
error_logger.warning(
|
||||
"Setting config.FALLBACK_ERROR_FORMAT on an already "
|
||||
"configured value may have unintended consequences."
|
||||
)
|
||||
self._FALLBACK_ERROR_FORMAT = value
|
||||
|
||||
def _configure_header_size(self):
|
||||
Http.set_header_max_size(
|
||||
self.REQUEST_MAX_HEADER_SIZE,
|
||||
@@ -169,8 +195,8 @@ class Config(dict):
|
||||
self.REQUEST_MAX_SIZE,
|
||||
)
|
||||
|
||||
def _check_error_format(self):
|
||||
check_error_format(self.FALLBACK_ERROR_FORMAT)
|
||||
def _check_error_format(self, format: Optional[str] = None):
|
||||
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
|
||||
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
@@ -184,20 +210,45 @@ class Config(dict):
|
||||
- ``float``
|
||||
- ``bool``
|
||||
|
||||
Anything else will be imported as a ``str``.
|
||||
Anything else will be imported as a ``str``. If you would like to add
|
||||
additional types to this list, you can use
|
||||
:meth:`sanic.config.Config.register_type`. Just make sure that they
|
||||
are registered before you instantiate your application.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Foo:
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name
|
||||
|
||||
|
||||
config = Config(converters=[Foo])
|
||||
app = Sanic(__name__, config=config)
|
||||
|
||||
`See user guide re: config
|
||||
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
||||
"""
|
||||
lower_case_var_found = False
|
||||
for key, value in environ.items():
|
||||
if not key.startswith(prefix):
|
||||
continue
|
||||
if not key.isupper():
|
||||
lower_case_var_found = True
|
||||
|
||||
_, config_key = key.split(prefix, 1)
|
||||
|
||||
for converter in (int, float, str_to_bool, str):
|
||||
for converter in reversed(self._converters):
|
||||
try:
|
||||
self[config_key] = converter(value)
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
if lower_case_var_found:
|
||||
deprecation(
|
||||
"Lowercase environment variables will not be "
|
||||
"loaded into Sanic config beginning in v22.9.",
|
||||
22.9,
|
||||
)
|
||||
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||
"""
|
||||
@@ -267,3 +318,17 @@ class Config(dict):
|
||||
self.update(config)
|
||||
|
||||
load = update_config
|
||||
|
||||
def register_type(self, converter: Callable[[str], Any]) -> None:
|
||||
"""
|
||||
Allows for adding custom function to cast from a string value to any
|
||||
other type. The function should raise ValueError if it is not the
|
||||
correct type.
|
||||
"""
|
||||
if converter in self._converters:
|
||||
error_logger.warning(
|
||||
f"Configuration value converter '{converter.__name__}' has "
|
||||
"already been registered"
|
||||
)
|
||||
return
|
||||
self._converters.append(converter)
|
||||
|
||||
@@ -34,6 +34,7 @@ except ImportError: # noqa
|
||||
from json import dumps
|
||||
|
||||
|
||||
DEFAULT_FORMAT = "auto"
|
||||
FALLBACK_TEXT = (
|
||||
"The server encountered an internal error and "
|
||||
"cannot complete your request."
|
||||
|
||||
@@ -51,6 +51,10 @@ class InvalidUsage(SanicException):
|
||||
quiet = True
|
||||
|
||||
|
||||
class BadURL(InvalidUsage):
|
||||
...
|
||||
|
||||
|
||||
class MethodNotSupported(SanicException):
|
||||
"""
|
||||
**Status**: 405 Method Not Allowed
|
||||
@@ -244,25 +248,3 @@ class InvalidSignal(SanicException):
|
||||
class WebsocketClosed(SanicException):
|
||||
quiet = True
|
||||
message = "Client has closed the websocket connection"
|
||||
|
||||
|
||||
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
||||
"""
|
||||
Raise an exception based on SanicException. Returns the HTTP response
|
||||
message appropriate for the given status code, unless provided.
|
||||
|
||||
STATUS_CODES from sanic.helpers for the given status code.
|
||||
|
||||
:param status_code: The HTTP status code to return.
|
||||
:param message: The HTTP response body. Defaults to the messages in
|
||||
"""
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"sanic.exceptions.abort has been marked as deprecated, and will be "
|
||||
"removed in release 21.12.\n To migrate your code, simply replace "
|
||||
"abort(status_code, msg) with raise SanicException(msg, status_code), "
|
||||
"or even better, raise an appropriate SanicException subclass."
|
||||
)
|
||||
|
||||
raise SanicException(message=message, status_code=status_code)
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
from inspect import signature
|
||||
from typing import Dict, List, Optional, Tuple, Type
|
||||
from warnings import warn
|
||||
from __future__ import annotations
|
||||
|
||||
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
|
||||
from typing import Dict, List, Optional, Tuple, Type, Union
|
||||
|
||||
from sanic.config import Config
|
||||
from sanic.errorpages import (
|
||||
DEFAULT_FORMAT,
|
||||
BaseRenderer,
|
||||
TextRenderer,
|
||||
exception_response,
|
||||
)
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
)
|
||||
from sanic.log import error_logger
|
||||
from sanic.helpers import Default, _default
|
||||
from sanic.log import deprecation, error_logger
|
||||
from sanic.models.handler_types import RouteHandler
|
||||
from sanic.response import text
|
||||
|
||||
@@ -26,52 +34,96 @@ class ErrorHandler:
|
||||
|
||||
"""
|
||||
|
||||
# Beginning in v22.3, the base renderer will be TextRenderer
|
||||
def __init__(
|
||||
self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer
|
||||
self,
|
||||
fallback: Union[str, Default] = _default,
|
||||
base: Type[BaseRenderer] = TextRenderer,
|
||||
):
|
||||
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
|
||||
self.cached_handlers: Dict[
|
||||
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
|
||||
] = {}
|
||||
self.debug = False
|
||||
self.fallback = fallback
|
||||
self._fallback = fallback
|
||||
self.base = base
|
||||
|
||||
if fallback is not _default:
|
||||
self._warn_fallback_deprecation()
|
||||
|
||||
@property
|
||||
def fallback(self): # no cov
|
||||
# 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): # no cov
|
||||
self._warn_fallback_deprecation()
|
||||
if not isinstance(value, str):
|
||||
raise SanicException(
|
||||
f"Cannot set error handler fallback to: value={value}"
|
||||
)
|
||||
self._fallback = value
|
||||
|
||||
@staticmethod
|
||||
def _warn_fallback_deprecation():
|
||||
deprecation(
|
||||
"Setting the ErrorHandler fallback value directly is "
|
||||
"deprecated and no longer supported. This feature will "
|
||||
"be removed in v22.6. Instead, use "
|
||||
"app.config.FALLBACK_ERROR_FORMAT.",
|
||||
22.6,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def finalize(cls, error_handler, fallback: Optional[str] = None):
|
||||
if (
|
||||
fallback
|
||||
and fallback != "auto"
|
||||
and error_handler.fallback == "auto"
|
||||
):
|
||||
error_handler.fallback = fallback
|
||||
def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
|
||||
if error_handler._fallback is not _default:
|
||||
if config._FALLBACK_ERROR_FORMAT is _default:
|
||||
return error_handler.fallback
|
||||
|
||||
error_logger.warning(
|
||||
"Conflicting error fallback values were found in the "
|
||||
"error handler and in the app.config while handling an "
|
||||
"exception. Using the value from app.config."
|
||||
)
|
||||
return config.FALLBACK_ERROR_FORMAT
|
||||
|
||||
@classmethod
|
||||
def finalize(
|
||||
cls,
|
||||
error_handler: ErrorHandler,
|
||||
config: Config,
|
||||
fallback: Optional[str] = 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 not fallback:
|
||||
fallback = config.FALLBACK_ERROR_FORMAT
|
||||
|
||||
if fallback != DEFAULT_FORMAT:
|
||||
if error_handler._fallback is not _default:
|
||||
error_logger.warning(
|
||||
f"Setting the fallback value to {fallback}. This changes "
|
||||
"the current non-default value "
|
||||
f"'{error_handler._fallback}'."
|
||||
)
|
||||
error_handler._fallback = fallback
|
||||
|
||||
if not isinstance(error_handler, cls):
|
||||
error_logger.warning(
|
||||
f"Error handler is non-conforming: {type(error_handler)}"
|
||||
)
|
||||
|
||||
sig = signature(error_handler.lookup)
|
||||
if len(sig.parameters) == 1:
|
||||
warn(
|
||||
"You are using a deprecated error handler. The lookup "
|
||||
"method should accept two positional parameters: "
|
||||
"(exception, route_name: Optional[str]). "
|
||||
"Until you upgrade your ErrorHandler.lookup, Blueprint "
|
||||
"specific exceptions will not work properly. Beginning "
|
||||
"in v22.3, the legacy style lookup method will not "
|
||||
"work at all.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
error_handler._lookup = error_handler._legacy_lookup
|
||||
|
||||
def _full_lookup(self, exception, route_name: Optional[str] = None):
|
||||
return self.lookup(exception, route_name)
|
||||
|
||||
def _legacy_lookup(self, exception, route_name: Optional[str] = None):
|
||||
return self.lookup(exception)
|
||||
|
||||
def add(self, exception, handler, route_names: Optional[List[str]] = None):
|
||||
"""
|
||||
Add a new exception handler to an already existing handler object.
|
||||
@@ -85,9 +137,6 @@ class ErrorHandler:
|
||||
|
||||
:return: None
|
||||
"""
|
||||
# self.handlers is deprecated and will be removed in version 22.3
|
||||
self.handlers.append((exception, handler))
|
||||
|
||||
if route_names:
|
||||
for route in route_names:
|
||||
self.cached_handlers[(exception, route)] = handler
|
||||
@@ -159,7 +208,7 @@ class ErrorHandler:
|
||||
except Exception:
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
except AttributeError: # no cov
|
||||
url = "unknown"
|
||||
response_message = (
|
||||
"Exception raised in exception handler " '"%s" for uri: %s'
|
||||
@@ -188,12 +237,13 @@ class ErrorHandler:
|
||||
:return:
|
||||
"""
|
||||
self.log(request, exception)
|
||||
fallback = ErrorHandler._get_fallback_value(self, request.app.config)
|
||||
return exception_response(
|
||||
request,
|
||||
exception,
|
||||
debug=self.debug,
|
||||
base=self.base,
|
||||
fallback=self.fallback,
|
||||
fallback=fallback,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -203,7 +253,7 @@ class ErrorHandler:
|
||||
if quiet is False or noisy is True:
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
except AttributeError: # no cov
|
||||
url = "unknown"
|
||||
|
||||
error_logger.exception(
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
|
||||
from urllib.parse import unquote
|
||||
|
||||
from sanic.exceptions import InvalidHeader
|
||||
@@ -18,7 +18,7 @@ Options = Dict[str, Union[int, str]] # key=value fields in various headers
|
||||
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
|
||||
|
||||
_token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
|
||||
_param = re.compile(fr";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
|
||||
_param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
|
||||
_firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)')
|
||||
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
|
||||
_ipv6_re = re.compile(_ipv6)
|
||||
@@ -394,3 +394,17 @@ def parse_accept(accept: str) -> AcceptContainer:
|
||||
return AcceptContainer(
|
||||
sorted(accept_list, key=_sort_accept_value, reverse=True)
|
||||
)
|
||||
|
||||
|
||||
def parse_credentials(
|
||||
header: Optional[str],
|
||||
prefixes: Union[List, Tuple, Set] = None,
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Parses any header with the aim to retrieve any credentials from it."""
|
||||
if not prefixes or not isinstance(prefixes, (list, tuple, set)):
|
||||
prefixes = ("Basic", "Bearer", "Token")
|
||||
if header is not None:
|
||||
for prefix in prefixes:
|
||||
if prefix in header:
|
||||
return prefix, header.partition(prefix)[-1].strip()
|
||||
return None, header
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic.request import Request
|
||||
from sanic.response import BaseHTTPResponse
|
||||
|
||||
|
||||
19
sanic/log.py
19
sanic/log.py
@@ -3,9 +3,10 @@ import sys
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Dict
|
||||
from warnings import warn
|
||||
|
||||
|
||||
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
|
||||
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
||||
version=1,
|
||||
disable_existing_loggers=False,
|
||||
loggers={
|
||||
@@ -56,7 +57,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
|
||||
)
|
||||
|
||||
|
||||
class Colors(str, Enum):
|
||||
class Colors(str, Enum): # no cov
|
||||
END = "\033[0m"
|
||||
BLUE = "\033[01;34m"
|
||||
GREEN = "\033[01;32m"
|
||||
@@ -64,17 +65,25 @@ class Colors(str, Enum):
|
||||
RED = "\033[01;31m"
|
||||
|
||||
|
||||
logger = logging.getLogger("sanic.root")
|
||||
logger = logging.getLogger("sanic.root") # no cov
|
||||
"""
|
||||
General Sanic logger
|
||||
"""
|
||||
|
||||
error_logger = logging.getLogger("sanic.error")
|
||||
error_logger = logging.getLogger("sanic.error") # no cov
|
||||
"""
|
||||
Logger used by Sanic for error logging
|
||||
"""
|
||||
|
||||
access_logger = logging.getLogger("sanic.access")
|
||||
access_logger = logging.getLogger("sanic.access") # no cov
|
||||
"""
|
||||
Logger used by Sanic for access logging
|
||||
"""
|
||||
|
||||
|
||||
def deprecation(message: str, version: float): # no cov
|
||||
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,9 +1,10 @@
|
||||
from typing import Set
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.models.futures import FutureException
|
||||
|
||||
|
||||
class ExceptionMixin:
|
||||
class ExceptionMixin(metaclass=SanicMeta):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._future_exceptions: Set[FutureException] = set()
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from enum import Enum, auto
|
||||
from functools import partial
|
||||
from typing import List, Optional, Union
|
||||
from typing import Callable, List, Optional, Union, overload
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.models.futures import FutureListener
|
||||
from sanic.models.handler_types import ListenerType, Sanic
|
||||
|
||||
@@ -16,21 +18,44 @@ class ListenerEvent(str, Enum):
|
||||
AFTER_SERVER_STOP = "server.shutdown.after"
|
||||
MAIN_PROCESS_START = auto()
|
||||
MAIN_PROCESS_STOP = auto()
|
||||
RELOAD_PROCESS_START = auto()
|
||||
RELOAD_PROCESS_STOP = auto()
|
||||
|
||||
|
||||
class ListenerMixin:
|
||||
class ListenerMixin(metaclass=SanicMeta):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._future_listeners: List[FutureListener] = []
|
||||
|
||||
def _apply_listener(self, listener: FutureListener):
|
||||
raise NotImplementedError # noqa
|
||||
|
||||
@overload
|
||||
def listener(
|
||||
self,
|
||||
listener_or_event: ListenerType[Sanic],
|
||||
event_or_none: str,
|
||||
apply: bool = ...,
|
||||
) -> ListenerType[Sanic]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def listener(
|
||||
self,
|
||||
listener_or_event: str,
|
||||
event_or_none: None = ...,
|
||||
apply: bool = ...,
|
||||
) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]:
|
||||
...
|
||||
|
||||
def listener(
|
||||
self,
|
||||
listener_or_event: Union[ListenerType[Sanic], str],
|
||||
event_or_none: Optional[str] = None,
|
||||
apply: bool = True,
|
||||
) -> ListenerType[Sanic]:
|
||||
) -> Union[
|
||||
ListenerType[Sanic],
|
||||
Callable[[ListenerType[Sanic]], ListenerType[Sanic]],
|
||||
]:
|
||||
"""
|
||||
Create a listener from a decorated function.
|
||||
|
||||
@@ -48,7 +73,9 @@ class ListenerMixin:
|
||||
:param event: event to listen to
|
||||
"""
|
||||
|
||||
def register_listener(listener, event):
|
||||
def register_listener(
|
||||
listener: ListenerType[Sanic], event: str
|
||||
) -> ListenerType[Sanic]:
|
||||
nonlocal apply
|
||||
|
||||
future_listener = FutureListener(listener, event)
|
||||
@@ -58,6 +85,10 @@ class ListenerMixin:
|
||||
return listener
|
||||
|
||||
if callable(listener_or_event):
|
||||
if event_or_none is None:
|
||||
raise InvalidUsage(
|
||||
"Invalid event registration: Missing event name."
|
||||
)
|
||||
return register_listener(listener_or_event, event_or_none)
|
||||
else:
|
||||
return partial(register_listener, event=listener_or_event)
|
||||
@@ -72,6 +103,16 @@ class ListenerMixin:
|
||||
) -> ListenerType[Sanic]:
|
||||
return self.listener(listener, "main_process_stop")
|
||||
|
||||
def reload_process_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
return self.listener(listener, "reload_process_start")
|
||||
|
||||
def reload_process_stop(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
return self.listener(listener, "reload_process_stop")
|
||||
|
||||
def before_server_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from functools import partial
|
||||
from typing import List
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.models.futures import FutureMiddleware
|
||||
|
||||
|
||||
class MiddlewareMixin:
|
||||
class MiddlewareMixin(metaclass=SanicMeta):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._future_middleware: List[FutureMiddleware] = []
|
||||
|
||||
@@ -15,9 +16,9 @@ class MiddlewareMixin:
|
||||
self, middleware_or_request, attach_to="request", apply=True
|
||||
):
|
||||
"""
|
||||
Decorate and register middleware to be called before a request.
|
||||
Can either be called as *@app.middleware* or
|
||||
*@app.middleware('request')*
|
||||
Decorate and register middleware to be called before a request
|
||||
is handled or after a response is created. Can either be called as
|
||||
*@app.middleware* or *@app.middleware('request')*.
|
||||
|
||||
`See user guide re: middleware
|
||||
<https://sanicframework.org/guide/basics/middleware.html>`__
|
||||
@@ -46,12 +47,25 @@ class MiddlewareMixin:
|
||||
)
|
||||
|
||||
def on_request(self, middleware=None):
|
||||
"""Register a middleware to be called before a request is handled.
|
||||
|
||||
This is the same as *@app.middleware('request')*.
|
||||
|
||||
:param: middleware: A callable that takes in request.
|
||||
"""
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "request")
|
||||
else:
|
||||
return partial(self.middleware, attach_to="request")
|
||||
|
||||
def on_response(self, middleware=None):
|
||||
"""Register a middleware to be called after a response is created.
|
||||
|
||||
This is the same as *@app.middleware('response')*.
|
||||
|
||||
:param: middleware:
|
||||
A callable that takes in a request and its response.
|
||||
"""
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "response")
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from ast import NodeVisitor, Return, parse
|
||||
from contextlib import suppress
|
||||
from functools import partial, wraps
|
||||
from inspect import getsource, signature
|
||||
from mimetypes import guess_type
|
||||
@@ -12,6 +13,7 @@ from urllib.parse import unquote
|
||||
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.compat import stat_async
|
||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
||||
from sanic.errorpages import RESPONSE_MAPPING
|
||||
@@ -22,19 +24,27 @@ from sanic.exceptions import (
|
||||
InvalidUsage,
|
||||
)
|
||||
from sanic.handlers import ContentRangeHandler
|
||||
from sanic.log import error_logger
|
||||
from sanic.log import deprecation, error_logger
|
||||
from sanic.models.futures import FutureRoute, FutureStatic
|
||||
from sanic.models.handler_types import RouteHandler
|
||||
from sanic.response import HTTPResponse, file, file_stream
|
||||
from sanic.views import CompositionView
|
||||
from sanic.types import HashableDict
|
||||
|
||||
|
||||
RouteWrapper = Callable[
|
||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||
]
|
||||
RESTRICTED_ROUTE_CONTEXT = (
|
||||
"ignore_body",
|
||||
"stream",
|
||||
"hosts",
|
||||
"static",
|
||||
"error_format",
|
||||
"websocket",
|
||||
)
|
||||
|
||||
|
||||
class RouteMixin:
|
||||
class RouteMixin(metaclass=SanicMeta):
|
||||
name: str
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
@@ -65,10 +75,20 @@ class RouteMixin:
|
||||
static: bool = False,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs: Any,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Decorate a function to be registered as a route
|
||||
|
||||
|
||||
**Example using context kwargs**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@app.route(..., ctx_foo="foobar")
|
||||
async def route_handler(request: Request):
|
||||
assert request.route.ctx.foo == "foobar"
|
||||
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:param host: the host, if required
|
||||
@@ -80,6 +100,8 @@ class RouteMixin:
|
||||
body (eg. GET requests)
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: tuple of routes, decorated function
|
||||
"""
|
||||
|
||||
@@ -94,6 +116,8 @@ class RouteMixin:
|
||||
if not methods and not websocket:
|
||||
methods = frozenset({"GET"})
|
||||
|
||||
route_context = self._build_route_context(ctx_kwargs)
|
||||
|
||||
def decorator(handler):
|
||||
nonlocal uri
|
||||
nonlocal methods
|
||||
@@ -152,6 +176,7 @@ class RouteMixin:
|
||||
static,
|
||||
version_prefix,
|
||||
error_format,
|
||||
route_context,
|
||||
)
|
||||
|
||||
self._future_routes.add(route)
|
||||
@@ -196,6 +221,7 @@ class RouteMixin:
|
||||
stream: bool = False,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
) -> RouteHandler:
|
||||
"""A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
@@ -212,6 +238,8 @@ class RouteMixin:
|
||||
:param stream: boolean specifying if the handler is a stream handler
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
@@ -226,14 +254,6 @@ class RouteMixin:
|
||||
if hasattr(_handler, "is_stream"):
|
||||
stream = True
|
||||
|
||||
# handle composition view differently
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
for _handler in handler.handlers.values():
|
||||
if hasattr(_handler, "is_stream"):
|
||||
stream = True
|
||||
break
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
@@ -247,6 +267,7 @@ class RouteMixin:
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)(handler)
|
||||
return handler
|
||||
|
||||
@@ -261,6 +282,7 @@ class RouteMixin:
|
||||
ignore_body: bool = True,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **GET** *HTTP* method
|
||||
@@ -273,6 +295,8 @@ class RouteMixin:
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -285,6 +309,7 @@ class RouteMixin:
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)
|
||||
|
||||
def post(
|
||||
@@ -297,6 +322,7 @@ class RouteMixin:
|
||||
name: Optional[str] = None,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **POST** *HTTP* method
|
||||
@@ -309,6 +335,8 @@ class RouteMixin:
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -321,6 +349,7 @@ class RouteMixin:
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)
|
||||
|
||||
def put(
|
||||
@@ -333,6 +362,7 @@ class RouteMixin:
|
||||
name: Optional[str] = None,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **PUT** *HTTP* method
|
||||
@@ -345,6 +375,8 @@ class RouteMixin:
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -357,6 +389,7 @@ class RouteMixin:
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)
|
||||
|
||||
def head(
|
||||
@@ -369,6 +402,7 @@ class RouteMixin:
|
||||
ignore_body: bool = True,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **HEAD** *HTTP* method
|
||||
@@ -389,6 +423,8 @@ class RouteMixin:
|
||||
:type ignore_body: bool, optional
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -401,6 +437,7 @@ class RouteMixin:
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)
|
||||
|
||||
def options(
|
||||
@@ -413,6 +450,7 @@ class RouteMixin:
|
||||
ignore_body: bool = True,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **OPTIONS** *HTTP* method
|
||||
@@ -433,6 +471,8 @@ class RouteMixin:
|
||||
:type ignore_body: bool, optional
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -445,6 +485,7 @@ class RouteMixin:
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)
|
||||
|
||||
def patch(
|
||||
@@ -457,6 +498,7 @@ class RouteMixin:
|
||||
name: Optional[str] = None,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **PATCH** *HTTP* method
|
||||
@@ -479,6 +521,8 @@ class RouteMixin:
|
||||
:type ignore_body: bool, optional
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -491,6 +535,7 @@ class RouteMixin:
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)
|
||||
|
||||
def delete(
|
||||
@@ -503,6 +548,7 @@ class RouteMixin:
|
||||
ignore_body: bool = True,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **DELETE** *HTTP* method
|
||||
@@ -515,6 +561,8 @@ class RouteMixin:
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -527,6 +575,7 @@ class RouteMixin:
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)
|
||||
|
||||
def websocket(
|
||||
@@ -540,6 +589,7 @@ class RouteMixin:
|
||||
apply: bool = True,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
):
|
||||
"""
|
||||
Decorate a function to be registered as a websocket route
|
||||
@@ -553,6 +603,8 @@ class RouteMixin:
|
||||
be used with :func:`url_for`
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: tuple of routes, decorated function
|
||||
"""
|
||||
return self.route(
|
||||
@@ -567,6 +619,7 @@ class RouteMixin:
|
||||
websocket=True,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)
|
||||
|
||||
def add_websocket_route(
|
||||
@@ -580,6 +633,7 @@ class RouteMixin:
|
||||
name: Optional[str] = None,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
**ctx_kwargs,
|
||||
):
|
||||
"""
|
||||
A helper method to register a function as a websocket route.
|
||||
@@ -598,6 +652,8 @@ class RouteMixin:
|
||||
be used with :func:`url_for`
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||
will be appended to the route context (``route.ctx``)
|
||||
:return: Objected decorated by :func:`websocket`
|
||||
"""
|
||||
return self.websocket(
|
||||
@@ -609,6 +665,7 @@ class RouteMixin:
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
**ctx_kwargs,
|
||||
)(handler)
|
||||
|
||||
def static(
|
||||
@@ -918,19 +975,16 @@ class RouteMixin:
|
||||
|
||||
return route
|
||||
|
||||
def _determine_error_format(self, handler) -> Optional[str]:
|
||||
if not isinstance(handler, CompositionView):
|
||||
try:
|
||||
src = dedent(getsource(handler))
|
||||
tree = parse(src)
|
||||
http_response_types = self._get_response_types(tree)
|
||||
def _determine_error_format(self, handler) -> str:
|
||||
with suppress(OSError, TypeError):
|
||||
src = dedent(getsource(handler))
|
||||
tree = parse(src)
|
||||
http_response_types = self._get_response_types(tree)
|
||||
|
||||
if len(http_response_types) == 1:
|
||||
return next(iter(http_response_types))
|
||||
except (OSError, TypeError):
|
||||
...
|
||||
if len(http_response_types) == 1:
|
||||
return next(iter(http_response_types))
|
||||
|
||||
return None
|
||||
return ""
|
||||
|
||||
def _get_response_types(self, node):
|
||||
types = set()
|
||||
@@ -939,7 +993,18 @@ class RouteMixin:
|
||||
def visit_Return(self, node: Return) -> Any:
|
||||
nonlocal types
|
||||
|
||||
try:
|
||||
with suppress(AttributeError):
|
||||
if node.value.func.id == "stream": # type: ignore
|
||||
deprecation(
|
||||
"The sanic.response.stream method has been "
|
||||
"deprecated and will be removed in v22.6. Please "
|
||||
"upgrade your application to use the new style "
|
||||
"streaming pattern. See "
|
||||
"https://sanicframework.org/en/guide/advanced/"
|
||||
"streaming.html#response-streaming for more "
|
||||
"information.",
|
||||
22.6,
|
||||
)
|
||||
checks = [node.value.func.id] # type: ignore
|
||||
if node.value.keywords: # type: ignore
|
||||
checks += [
|
||||
@@ -951,9 +1016,32 @@ class RouteMixin:
|
||||
for check in checks:
|
||||
if check in RESPONSE_MAPPING:
|
||||
types.add(RESPONSE_MAPPING[check])
|
||||
except AttributeError:
|
||||
...
|
||||
|
||||
HttpResponseVisitor().visit(node)
|
||||
|
||||
return types
|
||||
|
||||
def _build_route_context(self, raw):
|
||||
ctx_kwargs = {
|
||||
key.replace("ctx_", ""): raw.pop(key)
|
||||
for key in {**raw}.keys()
|
||||
if key.startswith("ctx_")
|
||||
}
|
||||
restricted = [
|
||||
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
|
||||
]
|
||||
if restricted:
|
||||
restricted_arguments = ", ".join(restricted)
|
||||
raise AttributeError(
|
||||
"Cannot use restricted route context: "
|
||||
f"{restricted_arguments}. This limitation is only in place "
|
||||
"until v22.3 when the restricted names will no longer be in"
|
||||
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
|
||||
"for more information."
|
||||
)
|
||||
if raw:
|
||||
unexpected_arguments = ", ".join(raw.keys())
|
||||
raise TypeError(
|
||||
f"Unexpected keyword arguments: {unexpected_arguments}"
|
||||
)
|
||||
return HashableDict(ctx_kwargs)
|
||||
|
||||
703
sanic/mixins/runner.py
Normal file
703
sanic/mixins/runner.py
Normal file
@@ -0,0 +1,703 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from asyncio import (
|
||||
AbstractEventLoop,
|
||||
CancelledError,
|
||||
Protocol,
|
||||
all_tasks,
|
||||
get_event_loop,
|
||||
get_running_loop,
|
||||
new_event_loop,
|
||||
)
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from socket import socket
|
||||
from ssl import SSLContext
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Type, Union
|
||||
|
||||
from sanic import reloader_helpers
|
||||
from sanic.application.logo import get_logo
|
||||
from sanic.application.motd import MOTD
|
||||
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.compat import OS_IS_WINDOWS
|
||||
from sanic.helpers import _default
|
||||
from sanic.log import Colors, error_logger, logger
|
||||
from sanic.models.handler_types import ListenerType
|
||||
from sanic.server import Signal as ServerSignal
|
||||
from sanic.server import try_use_uvloop
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
from sanic.server.events import trigger_events
|
||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
||||
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
||||
from sanic.server.runners import serve, serve_multiple, serve_single
|
||||
from sanic.tls import process_to_context
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic import Sanic
|
||||
from sanic.application.state import ApplicationState
|
||||
from sanic.config import Config
|
||||
|
||||
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
|
||||
|
||||
|
||||
class RunnerMixin(metaclass=SanicMeta):
|
||||
_app_registry: Dict[str, Sanic]
|
||||
config: Config
|
||||
listeners: Dict[str, List[ListenerType[Any]]]
|
||||
state: ApplicationState
|
||||
websocket_enabled: bool
|
||||
|
||||
def make_coffee(self, *args, **kwargs):
|
||||
self.state.coffee = True
|
||||
self.run(*args, **kwargs)
|
||||
|
||||
def run(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
*,
|
||||
dev: bool = False,
|
||||
debug: bool = False,
|
||||
auto_reload: Optional[bool] = None,
|
||||
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||
sock: Optional[socket] = None,
|
||||
workers: int = 1,
|
||||
protocol: Optional[Type[Protocol]] = None,
|
||||
backlog: int = 100,
|
||||
register_sys_signals: bool = True,
|
||||
access_log: Optional[bool] = None,
|
||||
unix: Optional[str] = None,
|
||||
loop: AbstractEventLoop = None,
|
||||
reload_dir: Optional[Union[List[str], str]] = None,
|
||||
noisy_exceptions: Optional[bool] = None,
|
||||
motd: bool = True,
|
||||
fast: bool = False,
|
||||
verbosity: int = 0,
|
||||
motd_display: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Run the HTTP Server and listen until keyboard interrupt or term
|
||||
signal. On termination, drain connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:type host: str
|
||||
:param port: Port to host on
|
||||
:type port: int
|
||||
:param debug: Enables debug output (slows server)
|
||||
:type debug: bool
|
||||
:param auto_reload: Reload app whenever its source code is changed.
|
||||
Enabled by default in debug mode.
|
||||
:type auto_relaod: bool
|
||||
:param ssl: SSLContext, or location of certificate and key
|
||||
for SSL encryption of worker(s)
|
||||
:type ssl: str, dict, SSLContext or list
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:type sock: socket
|
||||
:param workers: Number of processes received before it is respected
|
||||
:type workers: int
|
||||
:param protocol: Subclass of asyncio Protocol class
|
||||
:type protocol: type[Protocol]
|
||||
:param backlog: a number of unaccepted connections that the system
|
||||
will allow before refusing new connections
|
||||
:type backlog: int
|
||||
:param register_sys_signals: Register SIG* events
|
||||
:type register_sys_signals: bool
|
||||
:param access_log: Enables writing access logs (slows server)
|
||||
:type access_log: bool
|
||||
:param unix: Unix socket to listen on instead of TCP port
|
||||
:type unix: str
|
||||
:param noisy_exceptions: Log exceptions that are normally considered
|
||||
to be quiet/silent
|
||||
:type noisy_exceptions: bool
|
||||
:return: Nothing
|
||||
"""
|
||||
self.prepare(
|
||||
host=host,
|
||||
port=port,
|
||||
dev=dev,
|
||||
debug=debug,
|
||||
auto_reload=auto_reload,
|
||||
ssl=ssl,
|
||||
sock=sock,
|
||||
workers=workers,
|
||||
protocol=protocol,
|
||||
backlog=backlog,
|
||||
register_sys_signals=register_sys_signals,
|
||||
access_log=access_log,
|
||||
unix=unix,
|
||||
loop=loop,
|
||||
reload_dir=reload_dir,
|
||||
noisy_exceptions=noisy_exceptions,
|
||||
motd=motd,
|
||||
fast=fast,
|
||||
verbosity=verbosity,
|
||||
motd_display=motd_display,
|
||||
)
|
||||
|
||||
self.__class__.serve(primary=self) # type: ignore
|
||||
|
||||
def prepare(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
*,
|
||||
dev: bool = False,
|
||||
debug: bool = False,
|
||||
auto_reload: Optional[bool] = None,
|
||||
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||
sock: Optional[socket] = None,
|
||||
workers: int = 1,
|
||||
protocol: Optional[Type[Protocol]] = None,
|
||||
backlog: int = 100,
|
||||
register_sys_signals: bool = True,
|
||||
access_log: Optional[bool] = None,
|
||||
unix: Optional[str] = None,
|
||||
loop: AbstractEventLoop = None,
|
||||
reload_dir: Optional[Union[List[str], str]] = None,
|
||||
noisy_exceptions: Optional[bool] = None,
|
||||
motd: bool = True,
|
||||
fast: bool = False,
|
||||
verbosity: int = 0,
|
||||
motd_display: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
if dev:
|
||||
debug = True
|
||||
auto_reload = True
|
||||
|
||||
self.state.verbosity = verbosity
|
||||
if not self.state.auto_reload:
|
||||
self.state.auto_reload = bool(auto_reload)
|
||||
|
||||
if fast and workers != 1:
|
||||
raise RuntimeError("You cannot use both fast=True and workers=X")
|
||||
|
||||
if motd_display:
|
||||
self.config.MOTD_DISPLAY.update(motd_display)
|
||||
|
||||
if reload_dir:
|
||||
if isinstance(reload_dir, str):
|
||||
reload_dir = [reload_dir]
|
||||
|
||||
for directory in reload_dir:
|
||||
direc = Path(directory)
|
||||
if not direc.is_dir():
|
||||
logger.warning(
|
||||
f"Directory {directory} could not be located"
|
||||
)
|
||||
self.state.reload_dirs.add(Path(directory))
|
||||
|
||||
if loop is not None:
|
||||
raise TypeError(
|
||||
"loop is not a valid argument. To use an existing loop, "
|
||||
"change to create_server().\nSee more: "
|
||||
"https://sanic.readthedocs.io/en/latest/sanic/deploying.html"
|
||||
"#asynchronous-support"
|
||||
)
|
||||
|
||||
if (
|
||||
self.__class__.should_auto_reload()
|
||||
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
|
||||
): # no cov
|
||||
return
|
||||
|
||||
if sock is None:
|
||||
host, port = host or "127.0.0.1", port or 8000
|
||||
|
||||
if protocol is None:
|
||||
protocol = (
|
||||
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
||||
)
|
||||
|
||||
# Set explicitly passed configuration values
|
||||
for attribute, value in {
|
||||
"ACCESS_LOG": access_log,
|
||||
"AUTO_RELOAD": auto_reload,
|
||||
"MOTD": motd,
|
||||
"NOISY_EXCEPTIONS": noisy_exceptions,
|
||||
}.items():
|
||||
if value is not None:
|
||||
setattr(self.config, attribute, value)
|
||||
|
||||
if fast:
|
||||
self.state.fast = True
|
||||
try:
|
||||
workers = len(os.sched_getaffinity(0))
|
||||
except AttributeError: # no cov
|
||||
workers = os.cpu_count() or 1
|
||||
|
||||
server_settings = self._helper(
|
||||
host=host,
|
||||
port=port,
|
||||
debug=debug,
|
||||
ssl=ssl,
|
||||
sock=sock,
|
||||
unix=unix,
|
||||
workers=workers,
|
||||
protocol=protocol,
|
||||
backlog=backlog,
|
||||
register_sys_signals=register_sys_signals,
|
||||
)
|
||||
self.state.server_info.append(
|
||||
ApplicationServerInfo(settings=server_settings)
|
||||
)
|
||||
|
||||
if self.config.USE_UVLOOP is True or (
|
||||
self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
|
||||
):
|
||||
try_use_uvloop()
|
||||
|
||||
async def create_server(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
*,
|
||||
debug: bool = False,
|
||||
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||
sock: Optional[socket] = None,
|
||||
protocol: Type[Protocol] = None,
|
||||
backlog: int = 100,
|
||||
access_log: Optional[bool] = None,
|
||||
unix: Optional[str] = None,
|
||||
return_asyncio_server: bool = False,
|
||||
asyncio_server_kwargs: Dict[str, Any] = None,
|
||||
noisy_exceptions: Optional[bool] = None,
|
||||
) -> Optional[AsyncioServer]:
|
||||
"""
|
||||
Asynchronous version of :func:`run`.
|
||||
|
||||
This method will take care of the operations necessary to invoke
|
||||
the *before_start* events via :func:`trigger_events` method invocation
|
||||
before starting the *sanic* app in Async mode.
|
||||
|
||||
.. note::
|
||||
This does not support multiprocessing and is not the preferred
|
||||
way to run a :class:`Sanic` application.
|
||||
|
||||
:param host: Address to host on
|
||||
:type host: str
|
||||
:param port: Port to host on
|
||||
:type port: int
|
||||
:param debug: Enables debug output (slows server)
|
||||
:type debug: bool
|
||||
:param ssl: SSLContext, or location of certificate and key
|
||||
for SSL encryption of worker(s)
|
||||
:type ssl: SSLContext or dict
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:type sock: socket
|
||||
:param protocol: Subclass of asyncio Protocol class
|
||||
:type protocol: type[Protocol]
|
||||
:param backlog: a number of unaccepted connections that the system
|
||||
will allow before refusing new connections
|
||||
:type backlog: int
|
||||
:param access_log: Enables writing access logs (slows server)
|
||||
:type access_log: bool
|
||||
:param return_asyncio_server: flag that defines whether there's a need
|
||||
to return asyncio.Server or
|
||||
start it serving right away
|
||||
:type return_asyncio_server: bool
|
||||
:param asyncio_server_kwargs: key-value arguments for
|
||||
asyncio/uvloop create_server method
|
||||
:type asyncio_server_kwargs: dict
|
||||
:param noisy_exceptions: Log exceptions that are normally considered
|
||||
to be quiet/silent
|
||||
:type noisy_exceptions: bool
|
||||
:return: AsyncioServer if return_asyncio_server is true, else Nothing
|
||||
"""
|
||||
|
||||
if sock is None:
|
||||
host, port = host or "127.0.0.1", port or 8000
|
||||
|
||||
if protocol is None:
|
||||
protocol = (
|
||||
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
||||
)
|
||||
|
||||
# Set explicitly passed configuration values
|
||||
for attribute, value in {
|
||||
"ACCESS_LOG": access_log,
|
||||
"NOISY_EXCEPTIONS": noisy_exceptions,
|
||||
}.items():
|
||||
if value is not None:
|
||||
setattr(self.config, attribute, value)
|
||||
|
||||
server_settings = self._helper(
|
||||
host=host,
|
||||
port=port,
|
||||
debug=debug,
|
||||
ssl=ssl,
|
||||
sock=sock,
|
||||
unix=unix,
|
||||
loop=get_event_loop(),
|
||||
protocol=protocol,
|
||||
backlog=backlog,
|
||||
run_async=return_asyncio_server,
|
||||
)
|
||||
|
||||
if self.config.USE_UVLOOP is not _default:
|
||||
error_logger.warning(
|
||||
"You are trying to change the uvloop configuration, but "
|
||||
"this is only effective when using the run(...) method. "
|
||||
"When using the create_server(...) method Sanic will use "
|
||||
"the already existing loop."
|
||||
)
|
||||
|
||||
main_start = server_settings.pop("main_start", None)
|
||||
main_stop = server_settings.pop("main_stop", None)
|
||||
if main_start or main_stop:
|
||||
logger.warning(
|
||||
"Listener events for the main process are not available "
|
||||
"with create_server()"
|
||||
)
|
||||
|
||||
return await serve(
|
||||
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
This kills the Sanic
|
||||
"""
|
||||
if self.state.stage is not ServerStage.STOPPED:
|
||||
self.shutdown_tasks(timeout=0)
|
||||
for task in all_tasks():
|
||||
with suppress(AttributeError):
|
||||
if task.get_name() == "RunServer":
|
||||
task.cancel()
|
||||
get_event_loop().stop()
|
||||
|
||||
def _helper(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
debug: bool = False,
|
||||
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||
sock: Optional[socket] = None,
|
||||
unix: Optional[str] = None,
|
||||
workers: int = 1,
|
||||
loop: AbstractEventLoop = None,
|
||||
protocol: Type[Protocol] = HttpProtocol,
|
||||
backlog: int = 100,
|
||||
register_sys_signals: bool = True,
|
||||
run_async: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Helper function used by `run` and `create_server`."""
|
||||
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
|
||||
raise ValueError(
|
||||
"PROXIES_COUNT cannot be negative. "
|
||||
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
|
||||
"#proxy-configuration"
|
||||
)
|
||||
|
||||
ssl = process_to_context(ssl)
|
||||
|
||||
if not self.state.is_debug:
|
||||
self.state.mode = Mode.DEBUG if debug else Mode.PRODUCTION
|
||||
|
||||
self.state.host = host or ""
|
||||
self.state.port = port or 0
|
||||
self.state.workers = workers
|
||||
self.state.ssl = ssl
|
||||
self.state.unix = unix
|
||||
self.state.sock = sock
|
||||
|
||||
server_settings = {
|
||||
"protocol": protocol,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"sock": sock,
|
||||
"unix": unix,
|
||||
"ssl": ssl,
|
||||
"app": self,
|
||||
"signal": ServerSignal(),
|
||||
"loop": loop,
|
||||
"register_sys_signals": register_sys_signals,
|
||||
"backlog": backlog,
|
||||
}
|
||||
|
||||
self.motd(self.serve_location)
|
||||
|
||||
if sys.stdout.isatty() and not self.state.is_debug:
|
||||
error_logger.warning(
|
||||
f"{Colors.YELLOW}Sanic is running in PRODUCTION mode. "
|
||||
"Consider using '--debug' or '--dev' while actively "
|
||||
f"developing your application.{Colors.END}"
|
||||
)
|
||||
|
||||
# Register start/stop events
|
||||
for event_name, settings_name, reverse in (
|
||||
("main_process_start", "main_start", False),
|
||||
("main_process_stop", "main_stop", True),
|
||||
):
|
||||
listeners = self.listeners[event_name].copy()
|
||||
if reverse:
|
||||
listeners.reverse()
|
||||
# Prepend sanic to the arguments when listeners are triggered
|
||||
listeners = [partial(listener, self) for listener in listeners]
|
||||
server_settings[settings_name] = listeners # type: ignore
|
||||
|
||||
if run_async:
|
||||
server_settings["run_async"] = True
|
||||
|
||||
return server_settings
|
||||
|
||||
def motd(self, serve_location):
|
||||
if self.config.MOTD:
|
||||
mode = [f"{self.state.mode},"]
|
||||
if self.state.fast:
|
||||
mode.append("goin' fast")
|
||||
if self.state.asgi:
|
||||
mode.append("ASGI")
|
||||
else:
|
||||
if self.state.workers == 1:
|
||||
mode.append("single worker")
|
||||
else:
|
||||
mode.append(f"w/ {self.state.workers} workers")
|
||||
|
||||
display = {
|
||||
"mode": " ".join(mode),
|
||||
"server": self.state.server,
|
||||
"python": platform.python_version(),
|
||||
"platform": platform.platform(),
|
||||
}
|
||||
extra = {}
|
||||
if self.config.AUTO_RELOAD:
|
||||
reload_display = "enabled"
|
||||
if self.state.reload_dirs:
|
||||
reload_display += ", ".join(
|
||||
[
|
||||
"",
|
||||
*(
|
||||
str(path.absolute())
|
||||
for path in self.state.reload_dirs
|
||||
),
|
||||
]
|
||||
)
|
||||
display["auto-reload"] = reload_display
|
||||
|
||||
packages = []
|
||||
for package_name in SANIC_PACKAGES:
|
||||
module_name = package_name.replace("-", "_")
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
packages.append(f"{package_name}=={module.__version__}")
|
||||
except ImportError:
|
||||
...
|
||||
|
||||
if packages:
|
||||
display["packages"] = ", ".join(packages)
|
||||
|
||||
if self.config.MOTD_DISPLAY:
|
||||
extra.update(self.config.MOTD_DISPLAY)
|
||||
|
||||
logo = (
|
||||
get_logo(coffee=self.state.coffee)
|
||||
if self.config.LOGO == "" or self.config.LOGO is True
|
||||
else self.config.LOGO
|
||||
)
|
||||
|
||||
MOTD.output(logo, serve_location, display, extra)
|
||||
|
||||
@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
|
||||
|
||||
@classmethod
|
||||
def should_auto_reload(cls) -> bool:
|
||||
return any(app.state.auto_reload for app in cls._app_registry.values())
|
||||
|
||||
@classmethod
|
||||
def serve(cls, primary: Optional[Sanic] = None) -> None:
|
||||
apps = list(cls._app_registry.values())
|
||||
|
||||
if not primary:
|
||||
try:
|
||||
primary = apps[0]
|
||||
except IndexError:
|
||||
raise RuntimeError("Did not find any applications.")
|
||||
|
||||
reloader_start = primary.listeners.get("reload_process_start")
|
||||
reloader_stop = primary.listeners.get("reload_process_stop")
|
||||
# We want to run auto_reload if ANY of the applications have it enabled
|
||||
if (
|
||||
cls.should_auto_reload()
|
||||
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
|
||||
): # no cov
|
||||
loop = new_event_loop()
|
||||
trigger_events(reloader_start, loop, primary)
|
||||
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
|
||||
*(app.state.reload_dirs for app in apps)
|
||||
)
|
||||
reloader_helpers.watchdog(1.0, reload_dirs)
|
||||
trigger_events(reloader_stop, loop, primary)
|
||||
return
|
||||
|
||||
# This exists primarily for unit testing
|
||||
if not primary.state.server_info: # no cov
|
||||
for app in apps:
|
||||
app.state.server_info.clear()
|
||||
return
|
||||
|
||||
primary_server_info = primary.state.server_info[0]
|
||||
primary.before_server_start(partial(primary._start_servers, apps=apps))
|
||||
|
||||
try:
|
||||
primary_server_info.stage = ServerStage.SERVING
|
||||
|
||||
if primary.state.workers > 1 and os.name != "posix": # no cov
|
||||
logger.warn(
|
||||
f"Multiprocessing is currently not supported on {os.name},"
|
||||
" using workers=1 instead"
|
||||
)
|
||||
primary.state.workers = 1
|
||||
if primary.state.workers == 1:
|
||||
serve_single(primary_server_info.settings)
|
||||
elif primary.state.workers == 0:
|
||||
raise RuntimeError("Cannot serve with no workers")
|
||||
else:
|
||||
serve_multiple(
|
||||
primary_server_info.settings, primary.state.workers
|
||||
)
|
||||
except BaseException:
|
||||
error_logger.exception(
|
||||
"Experienced exception while trying to serve"
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
primary_server_info.stage = ServerStage.STOPPED
|
||||
logger.info("Server Stopped")
|
||||
for app in apps:
|
||||
app.state.server_info.clear()
|
||||
app.router.reset()
|
||||
app.signal_router.reset()
|
||||
|
||||
async def _start_servers(
|
||||
self,
|
||||
primary: Sanic,
|
||||
_,
|
||||
apps: List[Sanic],
|
||||
) -> None:
|
||||
for app in apps:
|
||||
if (
|
||||
app.name is not primary.name
|
||||
and app.state.workers != primary.state.workers
|
||||
and app.state.server_info
|
||||
):
|
||||
message = (
|
||||
f"The primary application {repr(primary)} is running "
|
||||
f"with {primary.state.workers} worker(s). All "
|
||||
"application instances will run with the same number. "
|
||||
f"You requested {repr(app)} to run with "
|
||||
f"{app.state.workers} worker(s), which will be ignored "
|
||||
"in favor of the primary application."
|
||||
)
|
||||
if sys.stdout.isatty():
|
||||
message = "".join(
|
||||
[
|
||||
Colors.YELLOW,
|
||||
message,
|
||||
Colors.END,
|
||||
]
|
||||
)
|
||||
error_logger.warning(message, exc_info=True)
|
||||
for server_info in app.state.server_info:
|
||||
if server_info.stage is not ServerStage.SERVING:
|
||||
app.state.primary = False
|
||||
handlers = [
|
||||
*server_info.settings.pop("main_start", []),
|
||||
*server_info.settings.pop("main_stop", []),
|
||||
]
|
||||
if handlers:
|
||||
error_logger.warning(
|
||||
f"Sanic found {len(handlers)} listener(s) on "
|
||||
"secondary applications attached to the main "
|
||||
"process. These will be ignored since main "
|
||||
"process listeners can only be attached to your "
|
||||
"primary application: "
|
||||
f"{repr(primary)}"
|
||||
)
|
||||
|
||||
if not server_info.settings["loop"]:
|
||||
server_info.settings["loop"] = get_running_loop()
|
||||
|
||||
try:
|
||||
server_info.server = await serve(
|
||||
**server_info.settings,
|
||||
run_async=True,
|
||||
reuse_port=bool(primary.state.workers - 1),
|
||||
)
|
||||
except OSError as e: # no cov
|
||||
first_message = (
|
||||
"An OSError was detected on startup. "
|
||||
"The encountered error was: "
|
||||
)
|
||||
second_message = str(e)
|
||||
if sys.stdout.isatty():
|
||||
message_parts = [
|
||||
Colors.YELLOW,
|
||||
first_message,
|
||||
Colors.RED,
|
||||
second_message,
|
||||
Colors.END,
|
||||
]
|
||||
else:
|
||||
message_parts = [first_message, second_message]
|
||||
message = "".join(message_parts)
|
||||
error_logger.warning(message, exc_info=True)
|
||||
continue
|
||||
primary.add_task(
|
||||
self._run_server(app, server_info), name="RunServer"
|
||||
)
|
||||
|
||||
async def _run_server(
|
||||
self,
|
||||
app: RunnerMixin,
|
||||
server_info: ApplicationServerInfo,
|
||||
) -> None:
|
||||
|
||||
try:
|
||||
# We should never get to this point without a server
|
||||
# This is primarily to keep mypy happy
|
||||
if not server_info.server: # no cov
|
||||
raise RuntimeError("Could not locate AsyncioServer")
|
||||
if app.state.stage is ServerStage.STOPPED:
|
||||
server_info.stage = ServerStage.SERVING
|
||||
await server_info.server.startup()
|
||||
await server_info.server.before_start()
|
||||
await server_info.server.after_start()
|
||||
await server_info.server.serve_forever()
|
||||
except CancelledError:
|
||||
# We should never get to this point without a server
|
||||
# This is primarily to keep mypy happy
|
||||
if not server_info.server: # no cov
|
||||
raise RuntimeError("Could not locate AsyncioServer")
|
||||
await server_info.server.before_stop()
|
||||
await server_info.server.close()
|
||||
await server_info.server.after_stop()
|
||||
finally:
|
||||
server_info.stage = ServerStage.STOPPED
|
||||
server_info.server = None
|
||||
@@ -1,17 +1,14 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Optional, Set, Union
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.models.futures import FutureSignal
|
||||
from sanic.models.handler_types import SignalHandler
|
||||
from sanic.signals import Signal
|
||||
from sanic.types import HashableDict
|
||||
|
||||
|
||||
class HashableDict(dict):
|
||||
def __hash__(self):
|
||||
return hash(tuple(sorted(self.items())))
|
||||
|
||||
|
||||
class SignalMixin:
|
||||
class SignalMixin(metaclass=SanicMeta):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._future_signals: Set[FutureSignal] = set()
|
||||
|
||||
@@ -24,6 +21,7 @@ class SignalMixin:
|
||||
*,
|
||||
apply: bool = True,
|
||||
condition: Dict[str, Any] = None,
|
||||
exclusive: bool = True,
|
||||
) -> Callable[[SignalHandler], SignalHandler]:
|
||||
"""
|
||||
For creating a signal handler, used similar to a route handler:
|
||||
@@ -36,17 +34,22 @@ class SignalMixin:
|
||||
|
||||
:param event: Representation of the event in ``one.two.three`` form
|
||||
:type event: str
|
||||
:param apply: For lazy evaluation, defaults to True
|
||||
:param apply: For lazy evaluation, defaults to ``True``
|
||||
:type apply: bool, optional
|
||||
:param condition: For use with the ``condition`` argument in dispatch
|
||||
filtering, defaults to None
|
||||
filtering, defaults to ``None``
|
||||
:param exclusive: When ``True``, the signal can only be dispatched
|
||||
when the condition has been met. When ``False``, the signal can
|
||||
be dispatched either with or without it. *THIS IS INAPPLICABLE TO
|
||||
BLUEPRINT SIGNALS. THEY ARE ALWAYS NON-EXCLUSIVE*, defaults
|
||||
to ``True``
|
||||
:type condition: Dict[str, Any], optional
|
||||
"""
|
||||
event_value = str(event.value) if isinstance(event, Enum) else event
|
||||
|
||||
def decorator(handler: SignalHandler):
|
||||
future_signal = FutureSignal(
|
||||
handler, event_value, HashableDict(condition or {})
|
||||
handler, event_value, HashableDict(condition or {}), exclusive
|
||||
)
|
||||
self._future_signals.add(future_signal)
|
||||
|
||||
@@ -62,6 +65,7 @@ class SignalMixin:
|
||||
handler: Optional[Callable[..., Any]],
|
||||
event: str,
|
||||
condition: Dict[str, Any] = None,
|
||||
exclusive: bool = True,
|
||||
):
|
||||
if not handler:
|
||||
|
||||
@@ -69,7 +73,9 @@ class SignalMixin:
|
||||
...
|
||||
|
||||
handler = noop
|
||||
self.signal(event=event, condition=condition)(handler)
|
||||
self.signal(event=event, condition=condition, exclusive=exclusive)(
|
||||
handler
|
||||
)
|
||||
return handler
|
||||
|
||||
def event(self, event: str):
|
||||
|
||||
@@ -13,7 +13,7 @@ ASGISend = Callable[[ASGIMessage], Awaitable[None]]
|
||||
ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
|
||||
|
||||
|
||||
class MockProtocol:
|
||||
class MockProtocol: # no cov
|
||||
def __init__(self, transport: "MockTransport", loop):
|
||||
# This should be refactored when < 3.8 support is dropped
|
||||
self.transport = transport
|
||||
@@ -56,7 +56,7 @@ class MockProtocol:
|
||||
await self._not_paused.wait()
|
||||
|
||||
|
||||
class MockTransport:
|
||||
class MockTransport: # no cov
|
||||
_protocol: Optional[MockProtocol]
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -7,6 +7,7 @@ from sanic.models.handler_types import (
|
||||
MiddlewareType,
|
||||
SignalHandler,
|
||||
)
|
||||
from sanic.types import HashableDict
|
||||
|
||||
|
||||
class FutureRoute(NamedTuple):
|
||||
@@ -25,6 +26,7 @@ class FutureRoute(NamedTuple):
|
||||
static: bool
|
||||
version_prefix: str
|
||||
error_format: Optional[str]
|
||||
route_context: HashableDict
|
||||
|
||||
|
||||
class FutureListener(NamedTuple):
|
||||
@@ -60,6 +62,7 @@ class FutureSignal(NamedTuple):
|
||||
handler: SignalHandler
|
||||
event: str
|
||||
condition: Optional[Dict[str, str]]
|
||||
exclusive: bool
|
||||
|
||||
|
||||
class FutureRegistry(set):
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from asyncio.events import AbstractEventLoop
|
||||
from typing import Any, Callable, Coroutine, Optional, TypeVar, Union
|
||||
|
||||
import sanic
|
||||
|
||||
from sanic.request import Request
|
||||
from sanic.response import BaseHTTPResponse, HTTPResponse
|
||||
|
||||
|
||||
Sanic = TypeVar("Sanic")
|
||||
Sanic = TypeVar("Sanic", bound="sanic.Sanic")
|
||||
|
||||
MiddlewareResponse = Union[
|
||||
Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]]
|
||||
@@ -18,8 +20,9 @@ ErrorMiddlewareType = Callable[
|
||||
[Request, BaseException], Optional[Coroutine[Any, Any, None]]
|
||||
]
|
||||
MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
|
||||
ListenerType = Callable[
|
||||
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
|
||||
ListenerType = Union[
|
||||
Callable[[Sanic], Optional[Coroutine[Any, Any, None]]],
|
||||
Callable[[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]],
|
||||
]
|
||||
RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]]
|
||||
SignalHandler = Callable[..., Coroutine[Any, Any, None]]
|
||||
|
||||
35
sanic/models/http_types.py
Normal file
35
sanic/models/http_types.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from base64 import b64decode
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass()
|
||||
class Credentials:
|
||||
auth_type: Optional[str]
|
||||
token: Optional[str]
|
||||
_username: Optional[str] = field(default=None)
|
||||
_password: Optional[str] = field(default=None)
|
||||
|
||||
def __post_init__(self):
|
||||
if self._auth_is_basic:
|
||||
self._username, self._password = (
|
||||
b64decode(self.token.encode("utf-8")).decode().split(":")
|
||||
)
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
if not self._auth_is_basic:
|
||||
raise AttributeError("Username is available for Basic Auth only")
|
||||
return self._username
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
if not self._auth_is_basic:
|
||||
raise AttributeError("Password is available for Basic Auth only")
|
||||
return self._password
|
||||
|
||||
@property
|
||||
def _auth_is_basic(self) -> bool:
|
||||
return self.auth_type == "Basic"
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ssl import SSLObject
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
@@ -77,7 +77,7 @@ def _check_file(filename, mtimes):
|
||||
return need_reload
|
||||
|
||||
|
||||
def watchdog(sleep_interval, app):
|
||||
def watchdog(sleep_interval, reload_dirs):
|
||||
"""Watch project files, restart worker process if a change happened.
|
||||
|
||||
:param sleep_interval: interval in second.
|
||||
@@ -100,7 +100,7 @@ def watchdog(sleep_interval, app):
|
||||
changed = set()
|
||||
for filename in itertools.chain(
|
||||
_iter_module_files(),
|
||||
*(d.glob("**/*") for d in app.reload_dirs),
|
||||
*(d.glob("**/*") for d in reload_dirs),
|
||||
):
|
||||
try:
|
||||
if _check_file(filename, mtimes):
|
||||
|
||||
107
sanic/request.py
107
sanic/request.py
@@ -14,8 +14,10 @@ from typing import (
|
||||
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.models.http_types import Credentials
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic.server import ConnInfo
|
||||
from sanic.app import Sanic
|
||||
|
||||
@@ -28,15 +30,17 @@ from types import SimpleNamespace
|
||||
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
||||
|
||||
from httptools import parse_url # type: ignore
|
||||
from httptools.parser.errors import HttpParserInvalidURLError # type: ignore
|
||||
|
||||
from sanic.compat import CancelledErrors, Header
|
||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||
from sanic.exceptions import InvalidUsage, ServerError
|
||||
from sanic.exceptions import BadURL, InvalidUsage, ServerError
|
||||
from sanic.headers import (
|
||||
AcceptContainer,
|
||||
Options,
|
||||
parse_accept,
|
||||
parse_content_header,
|
||||
parse_credentials,
|
||||
parse_forwarded,
|
||||
parse_host,
|
||||
parse_xforwarded,
|
||||
@@ -98,11 +102,13 @@ class Request:
|
||||
"method",
|
||||
"parsed_accept",
|
||||
"parsed_args",
|
||||
"parsed_not_grouped_args",
|
||||
"parsed_credentials",
|
||||
"parsed_files",
|
||||
"parsed_form",
|
||||
"parsed_json",
|
||||
"parsed_forwarded",
|
||||
"parsed_json",
|
||||
"parsed_not_grouped_args",
|
||||
"parsed_token",
|
||||
"raw_url",
|
||||
"responded",
|
||||
"request_middleware_started",
|
||||
@@ -122,9 +128,12 @@ class Request:
|
||||
app: Sanic,
|
||||
head: bytes = b"",
|
||||
):
|
||||
|
||||
self.raw_url = url_bytes
|
||||
# TODO: Content-Encoding detection
|
||||
self._parsed_url = parse_url(url_bytes)
|
||||
try:
|
||||
self._parsed_url = parse_url(url_bytes)
|
||||
except HttpParserInvalidURLError:
|
||||
raise BadURL(f"Bad URL: {url_bytes.decode()}")
|
||||
self._id: Optional[Union[uuid.UUID, str, int]] = None
|
||||
self._name: Optional[str] = None
|
||||
self.app = app
|
||||
@@ -141,9 +150,11 @@ class Request:
|
||||
self.ctx = SimpleNamespace()
|
||||
self.parsed_forwarded: Optional[Options] = None
|
||||
self.parsed_accept: Optional[AcceptContainer] = None
|
||||
self.parsed_credentials: Optional[Credentials] = None
|
||||
self.parsed_json = None
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
self.parsed_token: Optional[str] = None
|
||||
self.parsed_args: DefaultDict[
|
||||
Tuple[bool, bool, str, str], RequestParameters
|
||||
] = defaultdict(RequestParameters)
|
||||
@@ -189,6 +200,53 @@ class Request:
|
||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||
content_type: Optional[str] = None,
|
||||
):
|
||||
"""Respond to the request without returning.
|
||||
|
||||
This method can only be called once, as you can only respond once.
|
||||
If no ``response`` argument is passed, one will be created from the
|
||||
``status``, ``headers`` and ``content_type`` arguments.
|
||||
|
||||
**The first typical usecase** is if you wish to respond to the
|
||||
request without returning from the handler:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request: Request):
|
||||
data = ... # Process something
|
||||
|
||||
json_response = json({"data": data})
|
||||
await request.respond(json_response)
|
||||
|
||||
# You are now free to continue executing other code
|
||||
...
|
||||
|
||||
@app.on_response
|
||||
async def add_header(_, response: HTTPResponse):
|
||||
# Middlewares still get executed as expected
|
||||
response.headers["one"] = "two"
|
||||
|
||||
**The second possible usecase** is for when you want to directly
|
||||
respond to the request:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
response = await request.respond(content_type="text/csv")
|
||||
await response.send("foo,")
|
||||
await response.send("bar")
|
||||
|
||||
# You can control the completion of the response by calling
|
||||
# the 'eof()' method:
|
||||
await response.eof()
|
||||
|
||||
:param response: response instance to send
|
||||
:param status: status code to return in the response
|
||||
:param headers: headers to return in the response
|
||||
:param content_type: Content-Type header of the response
|
||||
:return: final response being sent (may be different from the
|
||||
``response`` parameter because of middlewares) which can be
|
||||
used to manually send data
|
||||
"""
|
||||
try:
|
||||
if self.stream is not None and self.stream.response:
|
||||
raise ServerError("Second respond call is not allowed.")
|
||||
@@ -332,20 +390,41 @@ class Request:
|
||||
return self.parsed_accept
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
def token(self) -> Optional[str]:
|
||||
"""Attempt to return the auth header token.
|
||||
|
||||
:return: token related to request
|
||||
"""
|
||||
prefixes = ("Bearer", "Token")
|
||||
auth_header = self.headers.getone("authorization", None)
|
||||
if self.parsed_token is None:
|
||||
prefixes = ("Bearer", "Token")
|
||||
_, token = parse_credentials(
|
||||
self.headers.getone("authorization", None), prefixes
|
||||
)
|
||||
self.parsed_token = token
|
||||
return self.parsed_token
|
||||
|
||||
if auth_header is not None:
|
||||
for prefix in prefixes:
|
||||
if prefix in auth_header:
|
||||
return auth_header.partition(prefix)[-1].strip()
|
||||
@property
|
||||
def credentials(self) -> Optional[Credentials]:
|
||||
"""Attempt to return the auth header value.
|
||||
|
||||
return auth_header
|
||||
Covers NoAuth, Basic Auth, Bearer Token, Api Token authentication
|
||||
schemas.
|
||||
|
||||
:return: A Credentials object with token, or username and password
|
||||
related to the request
|
||||
"""
|
||||
if self.parsed_credentials is None:
|
||||
try:
|
||||
prefix, credentials = parse_credentials(
|
||||
self.headers.getone("authorization", None)
|
||||
)
|
||||
if credentials:
|
||||
self.parsed_credentials = Credentials(
|
||||
auth_type=prefix, token=credentials
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
return self.parsed_credentials
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
@@ -12,10 +14,10 @@ from typing import (
|
||||
Iterator,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
from urllib.parse import quote_plus
|
||||
from warnings import warn
|
||||
|
||||
from sanic.compat import Header, open_async
|
||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||
@@ -28,6 +30,10 @@ from sanic.models.protocol_types import HTMLProtocol, Range
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.asgi import ASGIApp
|
||||
from sanic.request import Request
|
||||
else:
|
||||
Request = TypeVar("Request")
|
||||
|
||||
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
@@ -44,6 +50,16 @@ class BaseHTTPResponse:
|
||||
The base class for all HTTP Responses
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"asgi",
|
||||
"body",
|
||||
"content_type",
|
||||
"stream",
|
||||
"status",
|
||||
"headers",
|
||||
"_cookies",
|
||||
)
|
||||
|
||||
_dumps = json_dumps
|
||||
|
||||
def __init__(self):
|
||||
@@ -136,95 +152,6 @@ class BaseHTTPResponse:
|
||||
await self.stream.send(data, end_stream=end_stream)
|
||||
|
||||
|
||||
StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
"""
|
||||
Old style streaming response where you pass a streaming function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def sample_streaming_fn(response):
|
||||
await response.write("foo")
|
||||
await asyncio.sleep(1)
|
||||
await response.write("bar")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@app.post("/")
|
||||
async def test(request):
|
||||
return stream(sample_streaming_fn)
|
||||
|
||||
.. warning::
|
||||
|
||||
**Deprecated** and set for removal in v21.12. You can now achieve the
|
||||
same functionality without a callback.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@app.post("/")
|
||||
async def test(request):
|
||||
response = await request.respond()
|
||||
await response.send("foo", False)
|
||||
await asyncio.sleep(1)
|
||||
await response.send("bar", False)
|
||||
await asyncio.sleep(1)
|
||||
await response.send("", True)
|
||||
return response
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"streaming_fn",
|
||||
"status",
|
||||
"content_type",
|
||||
"headers",
|
||||
"_cookies",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
streaming_fn: StreamingFunction,
|
||||
status: int = 200,
|
||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||
content_type: str = "text/plain; charset=utf-8",
|
||||
ignore_deprecation_notice: bool = False,
|
||||
):
|
||||
if not ignore_deprecation_notice:
|
||||
warn(
|
||||
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
|
||||
"will be removed in v21.12. Please upgrade your streaming "
|
||||
"response implementation. You can learn more here: "
|
||||
"https://sanicframework.org/en/guide/advanced/streaming.html"
|
||||
"#response-streaming. If you use the builtin stream() or "
|
||||
"file_stream() methods, this upgrade will be be done for you."
|
||||
)
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.content_type = content_type
|
||||
self.streaming_fn = streaming_fn
|
||||
self.status = status
|
||||
self.headers = Header(headers or {})
|
||||
self._cookies = None
|
||||
|
||||
async def write(self, data):
|
||||
"""Writes a chunk of data to the streaming response.
|
||||
|
||||
:param data: str or bytes-ish data to be written.
|
||||
"""
|
||||
await super().send(self._encode_body(data))
|
||||
|
||||
async def send(self, *args, **kwargs):
|
||||
if self.streaming_fn is not None:
|
||||
await self.streaming_fn(self)
|
||||
self.streaming_fn = None
|
||||
await super().send(*args, **kwargs)
|
||||
|
||||
async def eof(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HTTPResponse(BaseHTTPResponse):
|
||||
"""
|
||||
HTTP response to be sent back to the client.
|
||||
@@ -239,7 +166,7 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
:type content_type: Optional[str]
|
||||
"""
|
||||
|
||||
__slots__ = ("body", "status", "content_type", "headers", "_cookies")
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -419,6 +346,109 @@ async def file(
|
||||
)
|
||||
|
||||
|
||||
def redirect(
|
||||
to: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
status: int = 302,
|
||||
content_type: str = "text/html; charset=utf-8",
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Abort execution and cause a 302 redirect (by default) by setting a
|
||||
Location header.
|
||||
|
||||
:param to: path or fully qualified URL to redirect to
|
||||
:param headers: optional dict of headers to include in the new request
|
||||
:param status: status code (int) of the new request, defaults to 302
|
||||
:param content_type: the content type (string) of the response
|
||||
"""
|
||||
headers = headers or {}
|
||||
|
||||
# URL Quote the URL before redirecting
|
||||
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
|
||||
|
||||
# According to RFC 7231, a relative URI is now permitted.
|
||||
headers["Location"] = safe_to
|
||||
|
||||
return HTTPResponse(
|
||||
status=status, headers=headers, content_type=content_type
|
||||
)
|
||||
|
||||
|
||||
class ResponseStream:
|
||||
"""
|
||||
ResponseStream is a compat layer to bridge the gap after the deprecation
|
||||
of StreamingHTTPResponse. In v22.6 it will be removed when:
|
||||
- stream is removed
|
||||
- file_stream is moved to new style streaming
|
||||
- file and file_stream are combined into a single API
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_cookies",
|
||||
"content_type",
|
||||
"headers",
|
||||
"request",
|
||||
"response",
|
||||
"status",
|
||||
"streaming_fn",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
streaming_fn: Callable[
|
||||
[Union[BaseHTTPResponse, ResponseStream]],
|
||||
Coroutine[Any, Any, None],
|
||||
],
|
||||
status: int = 200,
|
||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||
content_type: Optional[str] = None,
|
||||
):
|
||||
self.streaming_fn = streaming_fn
|
||||
self.status = status
|
||||
self.headers = headers or Header()
|
||||
self.content_type = content_type
|
||||
self.request: Optional[Request] = None
|
||||
self._cookies: Optional[CookieJar] = None
|
||||
|
||||
async def write(self, message: str):
|
||||
await self.response.send(message)
|
||||
|
||||
async def stream(self) -> HTTPResponse:
|
||||
if not self.request:
|
||||
raise ServerError("Attempted response to unknown request")
|
||||
self.response = await self.request.respond(
|
||||
headers=self.headers,
|
||||
status=self.status,
|
||||
content_type=self.content_type,
|
||||
)
|
||||
await self.streaming_fn(self)
|
||||
return self.response
|
||||
|
||||
async def eof(self) -> None:
|
||||
await self.response.eof()
|
||||
|
||||
@property
|
||||
def cookies(self) -> CookieJar:
|
||||
if self._cookies is None:
|
||||
self._cookies = CookieJar(self.headers)
|
||||
return self._cookies
|
||||
|
||||
@property
|
||||
def processed_headers(self):
|
||||
return self.response.processed_headers
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self.response.body
|
||||
|
||||
def __call__(self, request: Request) -> ResponseStream:
|
||||
self.request = request
|
||||
return self
|
||||
|
||||
def __await__(self):
|
||||
return self.stream().__await__()
|
||||
|
||||
|
||||
async def file_stream(
|
||||
location: Union[str, PurePath],
|
||||
status: int = 200,
|
||||
@@ -427,7 +457,7 @@ async def file_stream(
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
filename: Optional[str] = None,
|
||||
_range: Optional[Range] = None,
|
||||
) -> StreamingHTTPResponse:
|
||||
) -> ResponseStream:
|
||||
"""Return a streaming response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
@@ -435,7 +465,6 @@ async def file_stream(
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
:param chunked: Deprecated
|
||||
:param _range:
|
||||
"""
|
||||
headers = headers or {}
|
||||
@@ -471,23 +500,24 @@ async def file_stream(
|
||||
break
|
||||
await response.write(content)
|
||||
|
||||
return StreamingHTTPResponse(
|
||||
return ResponseStream(
|
||||
streaming_fn=_streaming_fn,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
ignore_deprecation_notice=True,
|
||||
)
|
||||
|
||||
|
||||
def stream(
|
||||
streaming_fn: StreamingFunction,
|
||||
streaming_fn: Callable[
|
||||
[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None]
|
||||
],
|
||||
status: int = 200,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
content_type: str = "text/plain; charset=utf-8",
|
||||
):
|
||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||
) -> ResponseStream:
|
||||
"""Accepts a coroutine `streaming_fn` which can be used to
|
||||
write chunks to a streaming response. Returns a `ResponseStream`.
|
||||
|
||||
Example usage::
|
||||
|
||||
@@ -501,42 +531,13 @@ def stream(
|
||||
|
||||
:param streaming_fn: A coroutine accepts a response and
|
||||
writes content to that response.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param status: HTTP status.
|
||||
:param content_type: Specific content_type.
|
||||
:param headers: Custom Headers.
|
||||
:param chunked: Deprecated
|
||||
"""
|
||||
return StreamingHTTPResponse(
|
||||
return ResponseStream(
|
||||
streaming_fn,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
status=status,
|
||||
ignore_deprecation_notice=True,
|
||||
)
|
||||
|
||||
|
||||
def redirect(
|
||||
to: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
status: int = 302,
|
||||
content_type: str = "text/html; charset=utf-8",
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Abort execution and cause a 302 redirect (by default) by setting a
|
||||
Location header.
|
||||
|
||||
:param to: path or fully qualified URL to redirect to
|
||||
:param headers: optional dict of headers to include in the new request
|
||||
:param status: status code (int) of the new request, defaults to 302
|
||||
:param content_type: the content type (string) of the response
|
||||
"""
|
||||
headers = headers or {}
|
||||
|
||||
# URL Quote the URL before redirecting
|
||||
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
|
||||
|
||||
# According to RFC 7231, a relative URI is now permitted.
|
||||
headers["Location"] = safe_to
|
||||
|
||||
return HTTPResponse(
|
||||
status=status, headers=headers, content_type=content_type
|
||||
)
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import asyncio
|
||||
|
||||
from sanic.models.server_types import ConnInfo, Signal
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
from sanic.server.loop import try_use_uvloop
|
||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
||||
from sanic.server.runners import serve, serve_multiple, serve_single
|
||||
|
||||
|
||||
try:
|
||||
import uvloop # type: ignore
|
||||
|
||||
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AsyncioServer",
|
||||
"ConnInfo",
|
||||
@@ -23,4 +13,5 @@ __all__ = (
|
||||
"serve",
|
||||
"serve_multiple",
|
||||
"serve_single",
|
||||
"try_use_uvloop",
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from warnings import warn
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.log import deprecation
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -37,10 +37,10 @@ class AsyncioServer:
|
||||
|
||||
@property
|
||||
def init(self):
|
||||
warn(
|
||||
deprecation(
|
||||
"AsyncioServer.init has been deprecated and will be removed "
|
||||
"in v22.6. Use Sanic.state.is_started instead.",
|
||||
DeprecationWarning,
|
||||
22.6,
|
||||
)
|
||||
return self.app.state.is_started
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from inspect import isawaitable
|
||||
from typing import Any, Callable, Iterable, Optional
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
|
||||
|
||||
|
||||
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def trigger_events(
|
||||
events: Optional[Iterable[Callable[..., Any]]],
|
||||
loop,
|
||||
app: Optional[Sanic] = None,
|
||||
):
|
||||
"""
|
||||
Trigger event callbacks (functions or async)
|
||||
|
||||
@@ -11,6 +21,9 @@ def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
|
||||
"""
|
||||
if events:
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
try:
|
||||
result = event() if not app else event(app)
|
||||
except TypeError:
|
||||
result = event(loop) if not app else event(app, loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
|
||||
49
sanic/server/loop.py
Normal file
49
sanic/server/loop.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import asyncio
|
||||
|
||||
from distutils.util import strtobool
|
||||
from os import getenv
|
||||
|
||||
from sanic.compat import OS_IS_WINDOWS
|
||||
from sanic.log import error_logger
|
||||
|
||||
|
||||
def try_use_uvloop() -> None:
|
||||
"""
|
||||
Use uvloop instead of the default asyncio loop.
|
||||
"""
|
||||
if OS_IS_WINDOWS:
|
||||
error_logger.warning(
|
||||
"You are trying to use uvloop, but uvloop is not compatible "
|
||||
"with your system. You can disable uvloop completely by setting "
|
||||
"the 'USE_UVLOOP' configuration value to false, or simply not "
|
||||
"defining it and letting Sanic handle it for you. Sanic will now "
|
||||
"continue to run using the default event loop."
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
import uvloop # type: ignore
|
||||
except ImportError:
|
||||
error_logger.warning(
|
||||
"You are trying to use uvloop, but uvloop is not "
|
||||
"installed in your system. In order to use uvloop "
|
||||
"you must first install it. Otherwise, you can disable "
|
||||
"uvloop completely by setting the 'USE_UVLOOP' "
|
||||
"configuration value to false. Sanic will now continue "
|
||||
"to run with the default event loop."
|
||||
)
|
||||
return
|
||||
|
||||
uvloop_install_removed = strtobool(getenv("SANIC_NO_UVLOOP", "no"))
|
||||
if uvloop_install_removed:
|
||||
error_logger.info(
|
||||
"You are requesting to run Sanic using uvloop, but the "
|
||||
"install-time 'SANIC_NO_UVLOOP' environment variable (used to "
|
||||
"opt-out of installing uvloop with Sanic) is set to true. If "
|
||||
"you want to prevent Sanic from overriding the event loop policy "
|
||||
"during runtime, set the 'USE_UVLOOP' configuration value to "
|
||||
"false."
|
||||
)
|
||||
|
||||
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic.app import Sanic
|
||||
|
||||
import asyncio
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from sanic.touchup.meta import TouchUpMeta
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic.app import Sanic
|
||||
|
||||
from asyncio import CancelledError
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
from typing import TYPE_CHECKING, Optional, Sequence, cast
|
||||
from warnings import warn
|
||||
|
||||
from websockets.connection import CLOSED, CLOSING, OPEN
|
||||
from websockets.server import ServerConnection
|
||||
from websockets.typing import Subprotocol
|
||||
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.log import error_logger
|
||||
from sanic.log import logger
|
||||
from sanic.server import HttpProtocol
|
||||
|
||||
from ..websockets.impl import WebsocketImplProtocol
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # no cov
|
||||
from websockets import http11
|
||||
|
||||
|
||||
class WebSocketProtocol(HttpProtocol):
|
||||
__slots__ = (
|
||||
"websocket",
|
||||
"websocket_timeout",
|
||||
"websocket_max_size",
|
||||
"websocket_ping_interval",
|
||||
"websocket_ping_timeout",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
websocket_timeout: float = 10.0,
|
||||
websocket_max_size: Optional[int] = None,
|
||||
websocket_max_queue: Optional[int] = None, # max_queue is deprecated
|
||||
websocket_read_limit: Optional[int] = None, # read_limit is deprecated
|
||||
websocket_write_limit: Optional[int] = None, # write_limit deprecated
|
||||
websocket_ping_interval: Optional[float] = 20.0,
|
||||
websocket_ping_timeout: Optional[float] = 20.0,
|
||||
**kwargs,
|
||||
@@ -33,27 +37,6 @@ class WebSocketProtocol(HttpProtocol):
|
||||
self.websocket: Optional[WebsocketImplProtocol] = None
|
||||
self.websocket_timeout = websocket_timeout
|
||||
self.websocket_max_size = websocket_max_size
|
||||
if websocket_max_queue is not None and websocket_max_queue > 0:
|
||||
# TODO: Reminder remove this warning in v22.3
|
||||
warn(
|
||||
"Websocket no longer uses queueing, so websocket_max_queue"
|
||||
" is no longer required.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
if websocket_read_limit is not None and websocket_read_limit > 0:
|
||||
# TODO: Reminder remove this warning in v22.3
|
||||
warn(
|
||||
"Websocket no longer uses read buffers, so "
|
||||
"websocket_read_limit is not required.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
if websocket_write_limit is not None and websocket_write_limit > 0:
|
||||
# TODO: Reminder remove this warning in v22.3
|
||||
warn(
|
||||
"Websocket no longer uses write buffers, so "
|
||||
"websocket_write_limit is not required.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
self.websocket_ping_interval = websocket_ping_interval
|
||||
self.websocket_ping_timeout = websocket_ping_timeout
|
||||
|
||||
@@ -121,7 +104,7 @@ class WebSocketProtocol(HttpProtocol):
|
||||
max_size=self.websocket_max_size,
|
||||
subprotocols=subprotocols,
|
||||
state=OPEN,
|
||||
logger=error_logger,
|
||||
logger=logger,
|
||||
)
|
||||
resp: "http11.Response" = ws_conn.accept(request)
|
||||
except Exception:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from ssl import SSLContext
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
||||
|
||||
@@ -19,6 +21,7 @@ from functools import partial
|
||||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||
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.log import error_logger, logger
|
||||
from sanic.models.server_types import Signal
|
||||
@@ -114,6 +117,7 @@ def serve(
|
||||
**asyncio_server_kwargs,
|
||||
)
|
||||
|
||||
setup_ext(app)
|
||||
if run_async:
|
||||
return AsyncioServer(
|
||||
app=app,
|
||||
@@ -128,7 +132,7 @@ def serve(
|
||||
try:
|
||||
http_server = loop.run_until_complete(server_coroutine)
|
||||
except BaseException:
|
||||
error_logger.exception("Unable to start server")
|
||||
error_logger.exception("Unable to start server", exc_info=True)
|
||||
return
|
||||
|
||||
# Ignore SIGINT when run_multiple
|
||||
@@ -174,6 +178,9 @@ def serve(
|
||||
loop.run_until_complete(asyncio.sleep(0.1))
|
||||
start_shutdown = start_shutdown + 0.1
|
||||
|
||||
if sys.version_info > (3, 7):
|
||||
app.shutdown_tasks(graceful - start_shutdown)
|
||||
|
||||
# Force close non-idle connection after waiting for
|
||||
# graceful_shutdown_timeout
|
||||
for conn in connections:
|
||||
|
||||
@@ -9,7 +9,7 @@ from websockets.typing import Data
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # no cov
|
||||
from .impl import WebsocketImplProtocol
|
||||
|
||||
UTF8Decoder = codecs.getincrementaldecoder("utf-8")
|
||||
@@ -37,7 +37,7 @@ class WebsocketFrameAssembler:
|
||||
"get_id",
|
||||
"put_id",
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # no cov
|
||||
protocol: "WebsocketImplProtocol"
|
||||
read_mutex: asyncio.Lock
|
||||
write_mutex: asyncio.Lock
|
||||
@@ -131,7 +131,7 @@ class WebsocketFrameAssembler:
|
||||
if self.paused:
|
||||
self.protocol.resume_frames()
|
||||
self.paused = False
|
||||
if not self.get_in_progress:
|
||||
if not self.get_in_progress: # no cov
|
||||
# This should be guarded against with the read_mutex,
|
||||
# exception is here as a failsafe
|
||||
raise ServerError(
|
||||
@@ -204,7 +204,7 @@ class WebsocketFrameAssembler:
|
||||
if self.paused:
|
||||
self.protocol.resume_frames()
|
||||
self.paused = False
|
||||
if not self.get_in_progress:
|
||||
if not self.get_in_progress: # no cov
|
||||
# This should be guarded against with the read_mutex,
|
||||
# exception is here as a failsafe
|
||||
raise ServerError(
|
||||
@@ -212,7 +212,7 @@ class WebsocketFrameAssembler:
|
||||
"asynchronous get was in progress."
|
||||
)
|
||||
self.get_in_progress = False
|
||||
if not self.message_complete.is_set():
|
||||
if not self.message_complete.is_set(): # no cov
|
||||
# This should be guarded against with the read_mutex,
|
||||
# exception is here as a failsafe
|
||||
raise ServerError(
|
||||
@@ -220,7 +220,7 @@ class WebsocketFrameAssembler:
|
||||
"message was complete."
|
||||
)
|
||||
self.message_complete.clear()
|
||||
if self.message_fetched.is_set():
|
||||
if self.message_fetched.is_set(): # no cov
|
||||
# This should be guarded against with the read_mutex,
|
||||
# and get_in_progress check, this exception is
|
||||
# here as a failsafe
|
||||
|
||||
@@ -518,8 +518,12 @@ class WebsocketImplProtocol:
|
||||
)
|
||||
try:
|
||||
self.recv_cancel = asyncio.Future()
|
||||
tasks = (
|
||||
self.recv_cancel,
|
||||
asyncio.ensure_future(self.assembler.get(timeout)),
|
||||
)
|
||||
done, pending = await asyncio.wait(
|
||||
(self.recv_cancel, self.assembler.get(timeout)),
|
||||
tasks,
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
done_task = next(iter(done))
|
||||
@@ -570,8 +574,12 @@ class WebsocketImplProtocol:
|
||||
self.can_pause = False
|
||||
self.recv_cancel = asyncio.Future()
|
||||
while True:
|
||||
tasks = (
|
||||
self.recv_cancel,
|
||||
asyncio.ensure_future(self.assembler.get(timeout=0)),
|
||||
)
|
||||
done, pending = await asyncio.wait(
|
||||
(self.recv_cancel, self.assembler.get(timeout=0)),
|
||||
tasks,
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
done_task = next(iter(done))
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
|
||||
from enum import Enum
|
||||
from inspect import isawaitable
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union, cast
|
||||
|
||||
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
|
||||
from sanic_routing.exceptions import NotFound # type: ignore
|
||||
@@ -80,6 +80,7 @@ class SignalRouter(BaseRouter):
|
||||
group_class=SignalGroup,
|
||||
stacking=True,
|
||||
)
|
||||
self.allow_fail_builtin = True
|
||||
self.ctx.loop = None
|
||||
|
||||
def get( # type: ignore
|
||||
@@ -129,7 +130,8 @@ class SignalRouter(BaseRouter):
|
||||
try:
|
||||
group, handlers, params = self.get(event, condition=condition)
|
||||
except NotFound as e:
|
||||
if fail_not_found:
|
||||
is_reserved = event.split(".", 1)[0] in RESERVED_NAMESPACES
|
||||
if fail_not_found and (not is_reserved or self.allow_fail_builtin):
|
||||
raise e
|
||||
else:
|
||||
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
|
||||
@@ -142,12 +144,21 @@ class SignalRouter(BaseRouter):
|
||||
if context:
|
||||
params.update(context)
|
||||
|
||||
signals = group.routes
|
||||
if not reverse:
|
||||
handlers = handlers[::-1]
|
||||
signals = signals[::-1]
|
||||
try:
|
||||
for handler in handlers:
|
||||
if condition is None or condition == handler.__requirements__:
|
||||
maybe_coroutine = handler(**params)
|
||||
for signal in signals:
|
||||
params.pop("__trigger__", None)
|
||||
if (
|
||||
(condition is None and signal.ctx.exclusive is False)
|
||||
or (
|
||||
condition is None
|
||||
and not signal.handler.__requirements__
|
||||
)
|
||||
or (condition == signal.handler.__requirements__)
|
||||
) and (signal.ctx.trigger or event == signal.ctx.definition):
|
||||
maybe_coroutine = signal.handler(**params)
|
||||
if isawaitable(maybe_coroutine):
|
||||
retval = await maybe_coroutine
|
||||
if retval:
|
||||
@@ -190,23 +201,36 @@ class SignalRouter(BaseRouter):
|
||||
handler: SignalHandler,
|
||||
event: str,
|
||||
condition: Optional[Dict[str, Any]] = None,
|
||||
exclusive: bool = True,
|
||||
) -> Signal:
|
||||
event_definition = event
|
||||
parts = self._build_event_parts(event)
|
||||
if parts[2].startswith("<"):
|
||||
name = ".".join([*parts[:-1], "*"])
|
||||
trigger = self._clean_trigger(parts[2])
|
||||
else:
|
||||
name = event
|
||||
trigger = ""
|
||||
|
||||
if not trigger:
|
||||
event = ".".join([*parts[:2], "<__trigger__>"])
|
||||
|
||||
handler.__requirements__ = condition # type: ignore
|
||||
handler.__trigger__ = trigger # type: ignore
|
||||
|
||||
return super().add(
|
||||
signal = super().add(
|
||||
event,
|
||||
handler,
|
||||
requirements=condition,
|
||||
name=name,
|
||||
append=True,
|
||||
) # type: ignore
|
||||
|
||||
signal.ctx.exclusive = exclusive
|
||||
signal.ctx.trigger = trigger
|
||||
signal.ctx.definition = event_definition
|
||||
|
||||
return cast(Signal, signal)
|
||||
|
||||
def finalize(self, do_compile: bool = True, do_optimize: bool = False):
|
||||
self.add(_blank, "sanic.__signal__.__init__")
|
||||
|
||||
@@ -238,3 +262,9 @@ class SignalRouter(BaseRouter):
|
||||
"Cannot declare reserved signal event: %s" % event
|
||||
)
|
||||
return parts
|
||||
|
||||
def _clean_trigger(self, trigger: str) -> str:
|
||||
trigger = trigger[1:-1]
|
||||
if ":" in trigger:
|
||||
trigger, _ = trigger.split(":")
|
||||
return trigger
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.exceptions import SanicException
|
||||
|
||||
from .service import TouchUp
|
||||
|
||||
|
||||
class TouchUpMeta(type):
|
||||
class TouchUpMeta(SanicMeta):
|
||||
def __new__(cls, name, bases, attrs, **kwargs):
|
||||
gen_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user