Compare commits

..

4 Commits

Author SHA1 Message Date
Adam Hopkins
af1d289a45 Add error format from config replacement objects 2021-11-17 09:05:24 +02:00
Adam Hopkins
b20b3cb417 Bump version 2021-11-16 23:13:59 +02:00
L. Kärkkäinen
45c22f9af2 Make HTTP connections start in IDLE stage, avoiding delays and error messages (#2268)
* Make all new connections start in IDLE stage, and switch to REQUEST stage only once any bytes are received from client. This makes new connections without any request obey keepalive timeout rather than request timeout like they currently do.

* Revert typo

* Remove request timeout endpoint test which is no longer working (still tested by mocking). Fix mock timeout test setup.

Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com>
2021-11-16 23:09:29 +02:00
Adam Hopkins
71d845786d Add error format commit and merge conflicts 2021-11-16 23:08:55 +02:00
159 changed files with 2176 additions and 5831 deletions

View File

@@ -1,7 +1,5 @@
exclude_patterns: exclude_patterns:
- "sanic/__main__.py" - "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py" - "sanic/reloader_helpers.py"
- "sanic/simple.py" - "sanic/simple.py"
- "sanic/utils.py" - "sanic/utils.py"
@@ -10,6 +8,7 @@ exclude_patterns:
- "docker/" - "docker/"
- "docs/" - "docs/"
- "examples/" - "examples/"
- "hack/"
- "scripts/" - "scripts/"
- "tests/" - "tests/"
checks: checks:
@@ -23,6 +22,3 @@ checks:
threshold: 40 threshold: 40
complex-logic: complex-logic:
enabled: false enabled: false
method-complexity:
config:
threshold: 10

View File

@@ -3,9 +3,6 @@ branch = True
source = sanic source = sanic
omit = omit =
site-packages site-packages
sanic/application/logo.py
sanic/application/motd.py
sanic/cli
sanic/__main__.py sanic/__main__.py
sanic/reloader_helpers.py sanic/reloader_helpers.py
sanic/simple.py sanic/simple.py

View File

@@ -2,20 +2,14 @@ name: "CodeQL"
on: on:
push: push:
branches: branches: [ main ]
- main
- "*LTS"
pull_request: pull_request:
branches: branches: [ main ]
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
schedule: schedule:
- cron: '25 16 * * 0' - cron: '25 16 * * 0'
jobs: jobs:
analyze: analyze:
if: github.event.pull_request.draft == false
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -1,16 +1,19 @@
name: Coverage check name: Coverage check
on: # on:
push: # push:
branches: # branches:
- main # - main
- "*LTS" # tags:
tags: # - "!*" # Do not execute on tags
- "!*" # Do not execute on tags # paths:
pull_request: # - sanic/*
types: [opened, synchronize, reopened, ready_for_review] # - tests/*
# pull_request:
# paths:
# - "!*.MD"
on: [push, pull_request]
jobs: jobs:
test: test:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:

View File

@@ -3,12 +3,9 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
bandit: bandit:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }} name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@@ -19,7 +16,6 @@ jobs:
- { python-version: 3.7, tox-env: security} - { python-version: 3.7, tox-env: security}
- { python-version: 3.8, tox-env: security} - { python-version: 3.8, tox-env: security}
- { python-version: 3.9, tox-env: security} - { python-version: 3.9, tox-env: security}
- { python-version: "3.10", tox-env: security}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -3,12 +3,9 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
docsLinter: docsLinter:
if: github.event.pull_request.draft == false
name: Lint Documentation name: Lint Documentation
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:

View File

@@ -3,12 +3,9 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
linter: linter:
if: github.event.pull_request.draft == false
name: lint name: lint
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:

View File

@@ -1,47 +0,0 @@
name: Python 3.10 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy310:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: "3.10",
tox-env: py310,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: "3.10",
tox-env: py310-no-ext,
ignore-error-flake: "true",
command-timeout: "600000",
}
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
command-timeout: "${{ matrix.config.command-timeout }}"
test-failure-retry: "3"

View File

@@ -3,16 +3,19 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS" push:
types: [opened, synchronize, reopened, ready_for_review] branches:
- main
paths:
- sanic/*
- tests/*
jobs: jobs:
testPy37: testPy37:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
# os: [ubuntu-latest, macos-latest] # os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest] os: [ubuntu-latest]

View File

@@ -3,16 +3,19 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS" push:
types: [opened, synchronize, reopened, ready_for_review] branches:
- main
paths:
- sanic/*
- tests/*
jobs: jobs:
testPy38: testPy38:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
# os: [ubuntu-latest, macos-latest] # os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest] os: [ubuntu-latest]

View File

@@ -3,16 +3,19 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS" push:
types: [opened, synchronize, reopened, ready_for_review] branches:
- main
paths:
- sanic/*
- tests/*
jobs: jobs:
testPy39: testPy39:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
# os: [ubuntu-latest, macos-latest] # os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest] os: [ubuntu-latest]

View File

@@ -3,12 +3,9 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
typeChecking: typeChecking:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }} name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@@ -16,10 +13,9 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
config: config:
# - { python-version: 3.7, tox-env: type-checking} - { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.8, tox-env: type-checking} - { python-version: 3.8, tox-env: type-checking}
- { python-version: 3.9, tox-env: type-checking} - { python-version: 3.9, tox-env: type-checking}
- { python-version: "3.10", tox-env: type-checking}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -3,12 +3,9 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
testsOnWindows: testsOnWindows:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }} name: ut-${{ matrix.config.tox-env }}
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
@@ -18,7 +15,6 @@ jobs:
- { python-version: 3.7, tox-env: py37-no-ext } - { python-version: 3.7, tox-env: py37-no-ext }
- { python-version: 3.8, tox-env: py38-no-ext } - { python-version: 3.8, tox-env: py38-no-ext }
- { python-version: 3.9, tox-env: py39-no-ext } - { python-version: 3.9, tox-env: py39-no-ext }
- { python-version: "3.10", tox-env: py310-no-ext }
- { python-version: pypy-3.7, tox-env: pypy37-no-ext } - { python-version: pypy-3.7, tox-env: pypy37-no-ext }
steps: steps:

View File

@@ -14,7 +14,7 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"] python-version: ["3.7", "3.8", "3.9"]
steps: steps:
- name: Checkout repository - name: Checkout repository

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
coverage:
status:
patch:
default:
target: auto
threshold: 0.75
informational: true
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/"

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,14 +4,12 @@ import asyncio
from sanic import Sanic from sanic import Sanic
app = Sanic()
app = Sanic("Example")
async def notify_server_started_after_five_seconds(): async def notify_server_started_after_five_seconds():
await asyncio.sleep(5) await asyncio.sleep(5)
print("Server successfully started!") print('Server successfully started!')
app.add_task(notify_server_started_after_five_seconds()) app.add_task(notify_server_started_after_five_seconds())

View File

@@ -1,29 +1,30 @@
from random import randint
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from random import randint
app = Sanic()
app = Sanic("Example") @app.middleware('request')
@app.middleware("request")
def append_request(request): def append_request(request):
request.ctx.num = randint(0, 100) # Add new key with random value
request['num'] = randint(0, 100)
@app.get("/pop") @app.get('/pop')
def pop_handler(request): def pop_handler(request):
return text(request.ctx.num) # Pop key from request object
num = request.pop('num')
return text(num)
@app.get("/key_exist") @app.get('/key_exist')
def key_exist_handler(request): def key_exist_handler(request):
# Check the key is exist or not # Check the key is exist or not
if hasattr(request.ctx, "num"): if 'num' in request:
return text("num exist in request") return text('num exist in request')
return text("num does not exist in request") return text('num does not exist in reqeust')
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,12 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from functools import wraps
from sanic import Sanic from sanic import Sanic
from functools import wraps
from sanic.response import json from sanic.response import json
app = Sanic()
app = Sanic("Example")
def check_request_for_authorization_status(request): def check_request_for_authorization_status(request):
@@ -29,16 +27,14 @@ def authorized(f):
return response return response
else: else:
# the user is not authorized. # the user is not authorized.
return json({"status": "not_authorized"}, 403) return json({'status': 'not_authorized'}, 403)
return decorated_function return decorated_function
@app.route("/") @app.route("/")
@authorized @authorized
async def test(request): async def test(request):
return json({"status": "authorized"}) return json({'status': 'authorized'})
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)

View File

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

View File

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

View File

@@ -2,20 +2,17 @@ from asyncio import sleep
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic(__name__, strict_slashes=True)
app = Sanic("DelayedResponseApp", strict_slashes=True)
@app.get("/") @app.get("/")
async def handler(request): async def handler(request):
return response.redirect("/sleep/3") return response.redirect("/sleep/3")
@app.get("/sleep/<t:number>") @app.get("/sleep/<t:number>")
async def handler2(request, t=0.3): async def handler2(request, t=0.3):
await sleep(t) await sleep(t)
return response.text(f"Slept {t:.1f} seconds.\n") return response.text(f"Slept {t:.1f} seconds.\n")
if __name__ == "__main__": if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)

View File

@@ -7,10 +7,8 @@ and pass in an instance of it when we create our Sanic instance. Inside this
class' default handler, we can do anything including sending exceptions to class' default handler, we can do anything including sending exceptions to
an external service. an external service.
""" """
from sanic.exceptions import SanicException
from sanic.handlers import ErrorHandler from sanic.handlers import ErrorHandler
from sanic.exceptions import SanicException
""" """
Imports and code relevant for our CustomHandler class Imports and code relevant for our CustomHandler class
(Ordinarily this would be in a separate file) (Ordinarily this would be in a separate file)
@@ -18,6 +16,7 @@ Imports and code relevant for our CustomHandler class
class CustomHandler(ErrorHandler): class CustomHandler(ErrorHandler):
def default(self, request, exception): def default(self, request, exception):
# Here, we have access to the exception object # Here, we have access to the exception object
# and can do anything with it (log, send to external service, etc) # and can do anything with it (log, send to external service, etc)
@@ -39,17 +38,17 @@ server's error_handler to an instance of our CustomHandler
from sanic import Sanic from sanic import Sanic
app = Sanic(__name__)
handler = CustomHandler() handler = CustomHandler()
app = Sanic("Example", error_handler=handler) app.error_handler = handler
@app.route("/") @app.route("/")
async def test(request): async def test(request):
# Here, something occurs which causes an unexpected exception # Here, something occurs which causes an unexpected exception
# This exception will flow to our custom handler. # This exception will flow to our custom handler.
raise SanicException("You Broke It!") raise SanicException('You Broke It!')
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,6 +1,4 @@
from sanic import Sanic, response, text from sanic import Sanic, response, text
from sanic.handlers import ErrorHandler
from sanic.server.async_server import AsyncioServer
HTTP_PORT = 9999 HTTP_PORT = 9999
@@ -34,40 +32,20 @@ def proxy(request, path):
return response.redirect(url) return response.redirect(url)
@https.main_process_start @https.listener("main_process_start")
async def start(app, _): async def start(app, _):
http_server = await http.create_server( global http
app.http_server = await http.create_server(
port=HTTP_PORT, return_asyncio_server=True port=HTTP_PORT, return_asyncio_server=True
) )
app.add_task(runner(http, http_server)) app.http_server.after_start()
app.ctx.http_server = http_server
app.ctx.http = http
@https.main_process_stop @https.listener("main_process_stop")
async def stop(app, _): async def stop(app, _):
await app.ctx.http_server.before_stop() app.http_server.before_stop()
await app.ctx.http_server.close() await app.http_server.close()
for connection in app.ctx.http_server.connections: app.http_server.after_stop()
connection.close_if_idle()
await app.ctx.http_server.after_stop()
app.ctx.http = False
async def runner(app: Sanic, app_server: AsyncioServer):
app.is_running = True
try:
app.signalize()
app.finalize()
ErrorHandler.finalize(app.error_handler)
app_server.init = True
await app_server.before_start()
await app_server.after_start()
await app_server.serve_forever()
finally:
app.is_running = False
app.is_stopping = True
https.run(port=HTTPS_PORT, debug=True) https.run(port=HTTPS_PORT, debug=True)

View File

@@ -1,30 +1,26 @@
import asyncio
import httpx
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
import asyncio
import aiohttp
app = Sanic("Example") app = Sanic(__name__)
sem = None sem = None
@app.before_server_start @app.listener('before_server_start')
def init(sanic, _): def init(sanic, loop):
global sem global sem
concurrency_per_worker = 4 concurrency_per_worker = 4
sem = asyncio.Semaphore(concurrency_per_worker) sem = asyncio.Semaphore(concurrency_per_worker, loop=loop)
async def bounded_fetch(session, url): async def bounded_fetch(session, url):
""" """
Use session object to perform 'get' request on url Use session object to perform 'get' request on url
""" """
async with sem: async with sem, session.get(url) as response:
response = await session.get(url) return await response.json()
return response.json()
@app.route("/") @app.route("/")
@@ -32,9 +28,9 @@ async def test(request):
""" """
Download and serve example JSON Download and serve example JSON
""" """
url = "https://api.github.com/repos/sanic-org/sanic" url = "https://api.github.com/repos/channelcat/sanic"
async with httpx.AsyncClient() as session: async with aiohttp.ClientSession() as session:
response = await bounded_fetch(session, url) response = await bounded_fetch(session, url)
return json(response) return json(response)

View File

@@ -1,6 +1,6 @@
import logging import logging
from contextvars import ContextVar import aiotask_context as context
from sanic import Sanic, response from sanic import Sanic, response
@@ -11,8 +11,8 @@ log = logging.getLogger(__name__)
class RequestIdFilter(logging.Filter): class RequestIdFilter(logging.Filter):
def filter(self, record): def filter(self, record):
try: try:
record.request_id = app.ctx.request_id.get(None) or "n/a" record.request_id = context.get("X-Request-ID")
except AttributeError: except ValueError:
record.request_id = "n/a" record.request_id = "n/a"
return True return True
@@ -44,12 +44,13 @@ LOG_SETTINGS = {
} }
app = Sanic("Example", log_config=LOG_SETTINGS) app = Sanic(__name__, log_config=LOG_SETTINGS)
@app.on_request @app.on_request
async def set_request_id(request): async def set_request_id(request):
request.app.ctx.request_id.set(request.id) request_id = request.id
context.set("X-Request-ID", request_id)
log.info(f"Setting {request.id=}") log.info(f"Setting {request.id=}")
@@ -60,14 +61,14 @@ async def set_request_header(request, response):
@app.route("/") @app.route("/")
async def test(request): async def test(request):
log.debug("X-Request-ID: %s", request.id) log.debug("X-Request-ID: %s", context.get("X-Request-ID"))
log.info("Hello from test!") log.info("Hello from test!")
return response.json({"test": True}) return response.json({"test": True})
@app.before_server_start @app.before_server_start
def setup(app, loop): def setup(app, loop):
app.ctx.request_id = ContextVar("request_id") loop.set_task_factory(context.task_factory)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,6 +1,5 @@
import logging import logging
import socket import socket
from os import getenv from os import getenv
from platform import node from platform import node
from uuid import getnode as get_mac from uuid import getnode as get_mac
@@ -8,11 +7,10 @@ from uuid import getnode as get_mac
from logdna import LogDNAHandler from logdna import LogDNAHandler
from sanic import Sanic from sanic import Sanic
from sanic.request import Request
from sanic.response import json from sanic.response import json
from sanic.request import Request
log = logging.getLogger('logdna')
log = logging.getLogger("logdna")
log.setLevel(logging.INFO) log.setLevel(logging.INFO)
@@ -32,18 +30,16 @@ logdna_options = {
"index_meta": True, "index_meta": True,
"hostname": node(), "hostname": node(),
"ip": get_my_ip_address(), "ip": get_my_ip_address(),
"mac": get_mac_address(), "mac": get_mac_address()
} }
logdna_handler = LogDNAHandler( logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
getenv("LOGDNA_API_KEY"), options=logdna_options
)
logdna = logging.getLogger(__name__) logdna = logging.getLogger(__name__)
logdna.setLevel(logging.INFO) logdna.setLevel(logging.INFO)
logdna.addHandler(logdna_handler) logdna.addHandler(logdna_handler)
app = Sanic("Example") app = Sanic(__name__)
@app.middleware @app.middleware
@@ -53,8 +49,13 @@ def log_request(request: Request):
@app.route("/") @app.route("/")
def default(request): def default(request):
return json({"response": "I was here"}) return json({
"response": "I was here"
})
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=getenv("PORT", 8080)) app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ from pathlib import Path
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("Example") app = Sanic(__name__)
@app.route("/text") @app.route("/text")
@@ -59,31 +59,31 @@ async def handler_stream(request):
return response.stream(body) return response.stream(body)
@app.before_server_start @app.listener("before_server_start")
async def listener_before_server_start(*args, **kwargs): async def listener_before_server_start(*args, **kwargs):
print("before_server_start") print("before_server_start")
@app.after_server_start @app.listener("after_server_start")
async def listener_after_server_start(*args, **kwargs): async def listener_after_server_start(*args, **kwargs):
print("after_server_start") print("after_server_start")
@app.before_server_stop @app.listener("before_server_stop")
async def listener_before_server_stop(*args, **kwargs): async def listener_before_server_stop(*args, **kwargs):
print("before_server_stop") print("before_server_stop")
@app.after_server_stop @app.listener("after_server_stop")
async def listener_after_server_stop(*args, **kwargs): async def listener_after_server_stop(*args, **kwargs):
print("after_server_stop") print("after_server_stop")
@app.on_request @app.middleware("request")
async def print_on_request(request): async def print_on_request(request):
print("print_on_request") print("print_on_request")
@app.on_response @app.middleware("response")
async def print_on_response(request, response): async def print_on_response(request, response):
print("print_on_response") print("print_on_response")

View File

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

View File

@@ -8,27 +8,12 @@ from sanic import Sanic, response
from sanic.server import AsyncioServer from sanic.server import AsyncioServer
app = Sanic("Example") app = Sanic(__name__)
@app.before_server_start @app.listener("after_server_start")
async def before_server_start(app, loop): async def after_start_test(app, loop):
print("Async Server starting") print("Async Server Started!")
@app.after_server_start
async def after_server_start(app, loop):
print("Async Server started")
@app.before_server_stop
async def before_server_stop(app, loop):
print("Async Server stopping")
@app.after_server_stop
async def after_server_stop(app, loop):
print("Async Server stopped")
@app.route("/") @app.route("/")
@@ -43,20 +28,20 @@ serv_coro = app.create_server(
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
serv_task = asyncio.ensure_future(serv_coro, loop=loop) serv_task = asyncio.ensure_future(serv_coro, loop=loop)
signal(SIGINT, lambda s, f: loop.stop()) signal(SIGINT, lambda s, f: loop.stop())
server: AsyncioServer = loop.run_until_complete(serv_task) server: AsyncioServer = loop.run_until_complete(serv_task) # type: ignore
loop.run_until_complete(server.startup()) server.startup()
# When using app.run(), this actually triggers before the serv_coro. # When using app.run(), this actually triggers before the serv_coro.
# But, in this example, we are using the convenience method, even if it is # But, in this example, we are using the convenience method, even if it is
# out of order. # out of order.
loop.run_until_complete(server.before_start()) server.before_start()
loop.run_until_complete(server.after_start()) server.after_start()
try: try:
loop.run_forever() loop.run_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
loop.stop() loop.stop()
finally: finally:
loop.run_until_complete(server.before_stop()) server.before_stop()
# Wait for server to close # Wait for server to close
close_task = server.close() close_task = server.close()
@@ -65,4 +50,4 @@ finally:
# Complete all tasks on the loop # Complete all tasks on the loop
for connection in server.connections: for connection in server.connections:
connection.close_if_idle() connection.close_if_idle()
loop.run_until_complete(server.after_stop()) server.after_stop()

View File

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

View File

@@ -1,41 +1,42 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import text
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
app = Sanic("some_name")
class SimpleView(HTTPMethodView): class SimpleView(HTTPMethodView):
def get(self, request): def get(self, request):
return text("I am get method") return text('I am get method')
def post(self, request): def post(self, request):
return text("I am post method") return text('I am post method')
def put(self, request): def put(self, request):
return text("I am put method") return text('I am put method')
def patch(self, request): def patch(self, request):
return text("I am patch method") return text('I am patch method')
def delete(self, request): def delete(self, request):
return text("I am delete method") return text('I am delete method')
class SimpleAsyncView(HTTPMethodView): class SimpleAsyncView(HTTPMethodView):
async def get(self, request): async def get(self, request):
return text("I am async get method") return text('I am async get method')
async def post(self, request): async def post(self, request):
return text("I am async post method") return text('I am async post method')
async def put(self, request): async def put(self, request):
return text("I am async put method") return text('I am async put method')
app.add_route(SimpleView.as_view(), "/") app.add_route(SimpleView.as_view(), '/')
app.add_route(SimpleAsyncView.as_view(), "/async") app.add_route(SimpleAsyncView.as_view(), '/async')
if __name__ == "__main__": if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import os import os
from sanic import Sanic, response from sanic import Sanic
from sanic.exceptions import ServerError
from sanic.log import logger as log from sanic.log import logger as log
from sanic import response
from sanic.exceptions import ServerError
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/") @app.route("/")
@@ -13,7 +13,7 @@ async def test_async(request):
return response.json({"test": True}) return response.json({"test": True})
@app.route("/sync", methods=["GET", "POST"]) @app.route("/sync", methods=['GET', 'POST'])
def test_sync(request): def test_sync(request):
return response.json({"test": True}) return response.json({"test": True})
@@ -31,7 +31,6 @@ def exception(request):
@app.route("/await") @app.route("/await")
async def test_await(request): async def test_await(request):
import asyncio import asyncio
await asyncio.sleep(5) await asyncio.sleep(5)
return response.text("I'm feeling sleepy") return response.text("I'm feeling sleepy")
@@ -43,10 +42,8 @@ async def test_file(request):
@app.route("/file_stream") @app.route("/file_stream")
async def test_file_stream(request): async def test_file_stream(request):
return await response.file_stream( return await response.file_stream(os.path.abspath("setup.py"),
os.path.abspath("setup.py"), chunk_size=1024 chunk_size=1024)
)
# ----------------------------------------------- # # ----------------------------------------------- #
# Exceptions # Exceptions
@@ -55,17 +52,14 @@ async def test_file_stream(request):
@app.exception(ServerError) @app.exception(ServerError)
async def test(request, exception): async def test(request, exception):
return response.json( return response.json({"exception": "{}".format(exception), "status": exception.status_code},
{"exception": str(exception), "status": exception.status_code}, status=exception.status_code)
status=exception.status_code,
)
# ----------------------------------------------- # # ----------------------------------------------- #
# Read from request # Read from request
# ----------------------------------------------- # # ----------------------------------------------- #
@app.route("/json") @app.route("/json")
def post_json(request): def post_json(request):
return response.json({"received": True, "message": request.json}) return response.json({"received": True, "message": request.json})
@@ -73,51 +67,38 @@ def post_json(request):
@app.route("/form") @app.route("/form")
def post_form_json(request): def post_form_json(request):
return response.json( return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
{
"received": True,
"form_data": request.form,
"test": request.form.get("test"),
}
)
@app.route("/query_string") @app.route("/query_string")
def query_string(request): def query_string(request):
return response.json( return response.json({"parsed": True, "args": request.args, "url": request.url,
{ "query_string": request.query_string})
"parsed": True,
"args": request.args,
"url": request.url,
"query_string": request.query_string,
}
)
# ----------------------------------------------- # # ----------------------------------------------- #
# Run Server # Run Server
# ----------------------------------------------- # # ----------------------------------------------- #
@app.listener('before_server_start')
@app.before_server_start
def before_start(app, loop): def before_start(app, loop):
log.info("SERVER STARTING") log.info("SERVER STARTING")
@app.after_server_start @app.listener('after_server_start')
def after_start(app, loop): def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH") log.info("OH OH OH OH OHHHHHHHH")
@app.before_server_stop @app.listener('before_server_stop')
def before_stop(app, loop): def before_stop(app, loop):
log.info("SERVER STOPPING") log.info("SERVER STOPPING")
@app.after_server_stop @app.listener('after_server_stop')
def after_stop(app, loop): def after_stop(app, loop):
log.info("TRIED EVERYTHING") log.info("TRIED EVERYTHING")
if __name__ == "__main__": if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,25 +1,23 @@
import os from sanic import Sanic
from sanic import response
import socket import socket
import os
from sanic import Sanic, response app = Sanic(__name__)
app = Sanic("Example")
@app.route("/test") @app.route("/test")
async def test(request): async def test(request):
return response.text("OK") return response.text("OK")
if __name__ == '__main__':
if __name__ == "__main__": server_address = './uds_socket'
server_address = "./uds_socket"
# Make sure the socket does not already exist # Make sure the socket does not already exist
try: try:
os.unlink(server_address) os.unlink(server_address)
except OSError: except OSError:
if os.path.exists(server_address): if os.path.exists(server_address):
raise raise
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_address) sock.bind(server_address)
app.run(sock=sock) app.run(sock=sock)

View File

@@ -1,21 +1,20 @@
from sanic import Sanic, response from sanic import Sanic
from sanic import response
app = Sanic(__name__)
app = Sanic("Example") @app.route('/')
@app.route("/")
async def index(request): async def index(request):
# generate a URL for the endpoint `post_handler` # generate a URL for the endpoint `post_handler`
url = app.url_for("post_handler", post_id=5) url = app.url_for('post_handler', post_id=5)
# the URL is `/posts/5`, redirect to it # the URL is `/posts/5`, redirect to it
return response.redirect(url) return response.redirect(url)
@app.route("/posts/<post_id>") @app.route('/posts/<post_id>')
async def post_handler(request, post_id): async def post_handler(request, post_id):
return response.text("Post - {}".format(post_id)) return response.text('Post - {}'.format(post_id))
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -8,9 +8,7 @@ app = Sanic(name="blue-print-group-version-example")
bp1 = Blueprint(name="ultron", url_prefix="/ultron") bp1 = Blueprint(name="ultron", url_prefix="/ultron")
bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None) bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None)
bpg = Blueprint.group( bpg = Blueprint.group([bp1, bp2], url_prefix="/sentient/robot", version=1, strict_slashes=True)
bp1, bp2, url_prefix="/sentient/robot", version=1, strict_slashes=True
)
@bp1.get("/name") @bp1.get("/name")
@@ -33,5 +31,5 @@ async def bp2_revised_name(request):
app.blueprint(bpg) app.blueprint(bpg)
if __name__ == "__main__": if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)

View File

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

View File

@@ -1,27 +1,25 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import redirect from sanic.response import redirect
app = Sanic(__name__)
app = Sanic("Example")
app.static("index.html", "websocket.html") app.static('index.html', "websocket.html")
@app.route('/')
@app.route("/")
def index(request): def index(request):
return redirect("index.html") return redirect("index.html")
@app.websocket('/feed')
@app.websocket("/feed")
async def feed(request, ws): async def feed(request, ws):
while True: while True:
data = "hello!" data = 'hello!'
print("Sending: " + data) print('Sending: ' + data)
await ws.send(data) await ws.send(data)
data = await ws.recv() data = await ws.recv()
print("Received: " + data) print('Received: ' + data)
if __name__ == "__main__": if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)

6
hack/Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM catthehacker/ubuntu:act-latest
SHELL [ "/bin/bash", "-c" ]
ENTRYPOINT []
RUN apt-get update
RUN apt-get install gcc -y
RUN apt-get install -y --no-install-recommends g++

View File

@@ -1,15 +1,196 @@
from sanic.cli.app import SanicCLI import os
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from typing import Any, Dict, Optional
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
from sanic.app import Sanic
from sanic.config import BASE_LOGO
from sanic.log import error_logger
from sanic.simple import create_simple_server
if OS_IS_WINDOWS: class SanicArgumentParser(ArgumentParser):
enable_windows_color_support() def add_bool_arguments(self, *args, **kwargs):
group = self.add_mutually_exclusive_group()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help']}\n "
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
def main(): def main():
cli = SanicCLI() parser = SanicArgumentParser(
cli.attach() prog="sanic",
cli.run() description=BASE_LOGO,
formatter_class=lambda prog: RawTextHelpFormatter(
prog, max_help_position=33
),
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
parser.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
parser.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help="Run Sanic as a Simple Server (module arg should be a path)\n ",
)
parser.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
parser.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
parser.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket\n ",
)
parser.add_argument(
"--cert", dest="cert", type=str, help="Location of certificate for SSL"
)
parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL\n "
)
parser.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
parser.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="number of worker processes [default 1]\n ",
)
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
parser.add_argument(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help="Watch source directory for file changes and reload on changes",
)
parser.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes\n ",
)
parser.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
args = parser.parse_args()
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if args.simple:
path = Path(args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in args.module else "."
module_name, app_name = args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}. "
f"Perhaps you meant {args.module}.app?"
)
if args.cert is not None or args.key is not None:
ssl: Optional[Dict[str, Any]] = {
"cert": args.cert,
"key": args.key,
}
else:
ssl = None
kwargs = {
"host": args.host,
"port": args.port,
"unix": args.unix,
"workers": args.workers,
"debug": args.debug,
"access_log": args.access_log,
"ssl": ssl,
}
if args.auto_reload:
kwargs["auto_reload"] = True
if args.path:
if args.auto_reload or args.debug:
kwargs["reload_dir"] = 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."
)
app.run(**kwargs)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
except ValueError:
error_logger.exception("Failed to run app")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1 +1 @@
__version__ = "21.12.2" __version__ = "21.9.2"

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,57 +0,0 @@
import re
import sys
from os import environ
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
COFFEE_LOGO = """\033[48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▄████████▄ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ██ ██▀▀▄ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ███████████ █ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ███████████▄▄▀ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▀███████▀ \033[0m
\033[48;2;255;13;104m \033[0m
Dark roast. No sugar."""
COLOR_LOGO = """\033[48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▄███ █████ ██ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▀███████ ███▄ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ████ ████████▀ \033[0m
\033[48;2;255;13;104m \033[0m
Build Fast. Run Fast."""
FULL_COLOR_LOGO = """
\033[38;2;255;13;104m ▄███ █████ ██ \033[0m ▄█▄ ██ █ █ ▄██████████
\033[38;2;255;13;104m ██ \033[0m █ █ █ ██ █ █ ██
\033[38;2;255;13;104m ▀███████ ███▄ \033[0m ▀ █ █ ██ ▄ █ ██
\033[38;2;255;13;104m ██\033[0m █████████ █ ██ █ █ ▄▄
\033[38;2;255;13;104m ████ ████████▀ \033[0m █ █ █ ██ █ ▀██ ███████
""" # noqa
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
def get_logo(full=False, coffee=False):
logo = (
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
if sys.stdout.isatty()
else BASE_LOGO
)
if (
sys.platform == "darwin"
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
):
logo = ansi_pattern.sub("", logo)
return logo

View File

@@ -1,143 +0,0 @@
import sys
from abc import ABC, abstractmethod
from shutil import get_terminal_size
from textwrap import indent, wrap
from typing import Dict, Optional
from sanic import __version__
from sanic.log import logger
class MOTD(ABC):
def __init__(
self,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
self.logo = logo
self.serve_location = serve_location
self.data = data
self.extra = extra
self.key_width = 0
self.value_width = 0
@abstractmethod
def display(self):
... # noqa
@classmethod
def output(
cls,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
motd_class(logo, serve_location, data, extra).display()
class MOTDBasic(MOTD):
def display(self):
if self.logo:
logger.debug(self.logo)
lines = [f"Sanic v{__version__}"]
if self.serve_location:
lines.append(f"Goin' Fast @ {self.serve_location}")
lines += [
*(f"{key}: {value}" for key, value in self.data.items()),
*(f"{key}: {value}" for key, value in self.extra.items()),
]
for line in lines:
logger.info(line)
class MOTDTTY(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.set_variables()
def set_variables(self): # no cov
fallback = (108, 24)
terminal_width = max(
get_terminal_size(fallback=fallback).columns, fallback[0]
)
self.max_value_width = terminal_width - fallback[0] + 36
self.key_width = 4
self.value_width = self.max_value_width
if self.data:
self.key_width = max(map(len, self.data.keys()))
self.value_width = min(
max(map(len, self.data.values())), self.max_value_width
)
self.logo_lines = self.logo.split("\n") if self.logo else []
self.logo_line_length = 24
self.centering_length = (
self.key_width + self.value_width + 2 + self.logo_line_length
)
self.display_length = self.key_width + self.value_width + 2
def display(self):
version = f"Sanic v{__version__}".center(self.centering_length)
running = (
f"Goin' Fast @ {self.serve_location}"
if self.serve_location
else ""
).center(self.centering_length)
length = len(version) + 2 - self.logo_line_length
first_filler = "" * (self.logo_line_length - 1)
second_filler = "" * length
display_filler = "" * (self.display_length + 2)
lines = [
f"\n{first_filler}{second_filler}",
f"{version}",
f"{running}",
f"{first_filler}{second_filler}",
]
self._render_data(lines, self.data, 0)
if self.extra:
logo_part = self._get_logo_part(len(lines) - 4)
lines.append(f"| {logo_part}{display_filler}")
self._render_data(lines, self.extra, len(lines) - 4)
self._render_fill(lines)
lines.append(f"{first_filler}{second_filler}\n")
logger.info(indent("\n".join(lines), " "))
def _render_data(self, lines, data, start):
offset = 0
for idx, (key, value) in enumerate(data.items(), start=start):
key = key.rjust(self.key_width)
wrapped = wrap(value, self.max_value_width, break_on_hyphens=False)
for wrap_index, part in enumerate(wrapped):
part = part.ljust(self.value_width)
logo_part = self._get_logo_part(idx + offset + wrap_index)
display = (
f"{key}: {part}"
if wrap_index == 0
else (" " * len(key) + f" {part}")
)
lines.append(f"{logo_part}{display}")
if wrap_index:
offset += 1
def _render_fill(self, lines):
filler = " " * self.display_length
idx = len(lines) - 5
for i in range(1, len(self.logo_lines) - idx):
logo_part = self.logo_lines[idx + i]
lines.append(f"{logo_part}{filler}")
def _get_logo_part(self, idx):
try:
logo_part = self.logo_lines[idx]
except IndexError:
logo_part = " " * (self.logo_line_length - 3)
return logo_part

View File

@@ -1,79 +0,0 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from socket import socket
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Optional, Set, Union
from sanic.log import logger
if TYPE_CHECKING:
from sanic import Sanic
class StrEnum(str, Enum):
def _generate_next_value_(name: str, *args) -> str: # type: ignore
return name.lower()
class Server(StrEnum):
SANIC = auto()
ASGI = auto()
GUNICORN = auto()
class Mode(StrEnum):
PRODUCTION = auto()
DEBUG = auto()
@dataclass
class ApplicationState:
app: Sanic
asgi: bool = field(default=False)
coffee: bool = field(default=False)
fast: bool = field(default=False)
host: str = field(default="")
port: int = field(default=0)
ssl: Optional[SSLContext] = field(default=None)
sock: Optional[socket] = field(default=None)
unix: Optional[str] = field(default=None)
mode: Mode = field(default=Mode.PRODUCTION)
reload_dirs: Set[Path] = field(default_factory=set)
server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False)
is_started: bool = field(default=False)
is_stopping: bool = field(default=False)
verbosity: int = field(default=0)
workers: int = field(default=0)
# This property relates to the ApplicationState instance and should
# not be changed except in the __post_init__ method
_init: bool = field(default=False)
def __post_init__(self) -> None:
self._init = True
def __setattr__(self, name: str, value: Any) -> None:
if self._init and name == "_init":
raise RuntimeError(
"Cannot change the value of _init after instantiation"
)
super().__setattr__(name, value)
if self._init and hasattr(self, f"set_{name}"):
getattr(self, f"set_{name}")(value)
def set_mode(self, value: Union[str, Mode]):
if hasattr(self.app, "error_handler"):
self.app.error_handler.debug = self.app.debug
if getattr(self.app, "configure_logging", False) and self.app.debug:
logger.setLevel(logging.DEBUG)
@property
def is_debug(self):
return self.mode is Mode.DEBUG

View File

@@ -7,11 +7,8 @@ import sanic.app # noqa
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.helpers import _default
from sanic.http import Stage
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse
from sanic.server import ConnInfo from sanic.server import ConnInfo
from sanic.server.websockets.connection import WebSocketConnection from sanic.server.websockets.connection import WebSocketConnection
@@ -54,13 +51,6 @@ class Lifespan:
await self.asgi_app.sanic_app._server_event("init", "before") await self.asgi_app.sanic_app._server_event("init", "before")
await self.asgi_app.sanic_app._server_event("init", "after") await self.asgi_app.sanic_app._server_event("init", "after")
if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
warnings.warn(
"You have set the USE_UVLOOP configuration option, but Sanic "
"cannot control the event loop when running in ASGI mode."
"This option will be ignored."
)
async def shutdown(self) -> None: async def shutdown(self) -> None:
""" """
Gather the listeners to fire on server stop. Gather the listeners to fire on server stop.
@@ -93,8 +83,6 @@ class ASGIApp:
transport: MockTransport transport: MockTransport
lifespan: Lifespan lifespan: Lifespan
ws: Optional[WebSocketConnection] ws: Optional[WebSocketConnection]
stage: Stage
response: Optional[BaseHTTPResponse]
def __init__(self) -> None: def __init__(self) -> None:
self.ws = None self.ws = None
@@ -107,8 +95,6 @@ class ASGIApp:
instance.sanic_app = sanic_app instance.sanic_app = sanic_app
instance.transport = MockTransport(scope, receive, send) instance.transport = MockTransport(scope, receive, send)
instance.transport.loop = sanic_app.loop instance.transport.loop = sanic_app.loop
instance.stage = Stage.IDLE
instance.response = None
setattr(instance.transport, "add_task", sanic_app.loop.create_task) setattr(instance.transport, "add_task", sanic_app.loop.create_task)
headers = Header( headers = Header(
@@ -163,8 +149,6 @@ class ASGIApp:
""" """
Read and stream the body in chunks from an incoming ASGI message. Read and stream the body in chunks from an incoming ASGI message.
""" """
if self.stage is Stage.IDLE:
self.stage = Stage.REQUEST
message = await self.transport.receive() message = await self.transport.receive()
body = message.get("body", b"") body = message.get("body", b"")
if not message.get("more_body", False): if not message.get("more_body", False):
@@ -179,17 +163,11 @@ class ASGIApp:
if data: if data:
yield data yield data
def respond(self, response: BaseHTTPResponse): def respond(self, response):
if self.stage is not Stage.HANDLER:
self.stage = Stage.FAILED
raise RuntimeError("Response already started")
if self.response is not None:
self.response.stream = None
response.stream, self.response = self, response response.stream, self.response = self, response
return response return response
async def send(self, data, end_stream): async def send(self, data, end_stream):
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
if self.response: if self.response:
response, self.response = self.response, None response, self.response = self.response, None
await self.transport.send( await self.transport.send(
@@ -217,7 +195,6 @@ class ASGIApp:
Handle the incoming request. Handle the incoming request.
""" """
try: try:
self.stage = Stage.HANDLER
await self.sanic_app.handle_request(self.request) await self.sanic_app.handle_request(self.request)
except Exception as e: except Exception as e:
await self.sanic_app.handle_exception(self.request, e) await self.sanic_app.handle_exception(self.request, e)

View File

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

View File

View File

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

View File

@@ -5,7 +5,7 @@ from functools import partial
from typing import TYPE_CHECKING, List, Optional, Union from typing import TYPE_CHECKING, List, Optional, Union
if TYPE_CHECKING: # no cov if TYPE_CHECKING:
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
@@ -208,7 +208,7 @@ class BlueprintGroup(MutableSequence):
:param args: List of Python exceptions to be caught by the handler :param args: List of Python exceptions to be caught by the handler
:param kwargs: Additional optional arguments to be passed to the :param kwargs: Additional optional arguments to be passed to the
exception handler exception handler
:return: a decorated method to handle global exceptions for any :return a decorated method to handle global exceptions for any
blueprint registered under this group. blueprint registered under this group.
""" """

View File

@@ -4,27 +4,13 @@ import asyncio
from collections import defaultdict from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from functools import wraps
from inspect import isfunction
from itertools import chain
from types import SimpleNamespace from types import SimpleNamespace
from typing import ( from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
TYPE_CHECKING,
Any,
Dict,
Iterable,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
)
from sanic_routing.exceptions import NotFound # type: ignore from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
from sanic.base.root import BaseSanic from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup from sanic.blueprint_group import BlueprintGroup
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from sanic.helpers import Default, _default from sanic.helpers import Default, _default
@@ -36,34 +22,8 @@ from sanic.models.handler_types import (
) )
if TYPE_CHECKING: # no cov if TYPE_CHECKING:
from sanic import Sanic from sanic import Sanic # noqa
def lazy(func, as_decorator=True):
@wraps(func)
def decorator(bp, *args, **kwargs):
nonlocal as_decorator
kwargs["apply"] = False
pass_handler = None
if args and isfunction(args[0]):
as_decorator = False
def wrapper(handler):
future = func(bp, *args, **kwargs)
if as_decorator:
future = future(handler)
if bp.registered:
for app in bp.apps:
bp.register(app, {})
return future
return wrapper if as_decorator else wrapper(pass_handler)
return decorator
class Blueprint(BaseSanic): class Blueprint(BaseSanic):
@@ -79,13 +39,13 @@ class Blueprint(BaseSanic):
:param name: unique name of the blueprint :param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs :param url_prefix: URL to be prefixed before all route URLs
:param host: IP Address or FQDN for the sanic server to use. :param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version :param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a :param strict_slashes: Enforce the API urls are requested with a
trailing */* trailing */*
""" """
__slots__ = ( __fake_slots__ = (
"_apps", "_apps",
"_future_routes", "_future_routes",
"_future_statics", "_future_statics",
@@ -98,6 +58,7 @@ class Blueprint(BaseSanic):
"host", "host",
"listeners", "listeners",
"middlewares", "middlewares",
"name",
"routes", "routes",
"statics", "statics",
"strict_slashes", "strict_slashes",
@@ -111,7 +72,7 @@ class Blueprint(BaseSanic):
self, self,
name: str = None, name: str = None,
url_prefix: Optional[str] = None, url_prefix: Optional[str] = None,
host: Optional[Union[List[str], str]] = None, host: Optional[str] = None,
version: Optional[Union[int, str, float]] = None, version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
@@ -154,21 +115,34 @@ class Blueprint(BaseSanic):
) )
return self._apps return self._apps
@property def route(self, *args, **kwargs):
def registered(self) -> bool: kwargs["apply"] = False
return bool(self._apps) return super().route(*args, **kwargs)
exception = lazy(BaseSanic.exception) def static(self, *args, **kwargs):
listener = lazy(BaseSanic.listener) kwargs["apply"] = False
middleware = lazy(BaseSanic.middleware) return super().static(*args, **kwargs)
route = lazy(BaseSanic.route)
signal = lazy(BaseSanic.signal) def middleware(self, *args, **kwargs):
static = lazy(BaseSanic.static, as_decorator=False) kwargs["apply"] = False
return super().middleware(*args, **kwargs)
def listener(self, *args, **kwargs):
kwargs["apply"] = False
return super().listener(*args, **kwargs)
def exception(self, *args, **kwargs):
kwargs["apply"] = False
return super().exception(*args, **kwargs)
def signal(self, event: str, *args, **kwargs):
kwargs["apply"] = False
return super().signal(event, *args, **kwargs)
def reset(self): def reset(self):
self._apps: Set[Sanic] = set() self._apps: Set[Sanic] = set()
self.exceptions: List[RouteHandler] = [] self.exceptions: List[RouteHandler] = []
self.listeners: Dict[str, List[ListenerType[Any]]] = {} self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares: List[MiddlewareType] = [] self.middlewares: List[MiddlewareType] = []
self.routes: List[Route] = [] self.routes: List[Route] = []
self.statics: List[RouteHandler] = [] self.statics: List[RouteHandler] = []
@@ -247,7 +221,7 @@ class Blueprint(BaseSanic):
version: Optional[Union[int, str, float]] = None, version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
) -> BlueprintGroup: ):
""" """
Create a list of blueprints, optionally grouping them under a Create a list of blueprints, optionally grouping them under a
general URL prefix. general URL prefix.
@@ -300,7 +274,6 @@ class Blueprint(BaseSanic):
middleware = [] middleware = []
exception_handlers = [] exception_handlers = []
listeners = defaultdict(list) listeners = defaultdict(list)
registered = set()
# Routes # Routes
for future in self._future_routes: for future in self._future_routes:
@@ -327,15 +300,12 @@ class Blueprint(BaseSanic):
) )
name = app._generate_name(future.name) name = app._generate_name(future.name)
host = future.host or self.host
if isinstance(host, list):
host = tuple(host)
apply_route = FutureRoute( apply_route = FutureRoute(
future.handler, future.handler,
uri[1:] if uri.startswith("//") else uri, uri[1:] if uri.startswith("//") else uri,
future.methods, future.methods,
host, future.host or self.host,
strict_slashes, strict_slashes,
future.stream, future.stream,
version, version,
@@ -347,13 +317,8 @@ class Blueprint(BaseSanic):
future.static, future.static,
version_prefix, version_prefix,
error_format, error_format,
future.route_context,
) )
if (self, apply_route) in app._future_registry:
continue
registered.add(apply_route)
route = app._apply_route(apply_route) route = app._apply_route(apply_route)
operation = ( operation = (
routes.extend if isinstance(route, list) else routes.append routes.extend if isinstance(route, list) else routes.append
@@ -365,11 +330,6 @@ class Blueprint(BaseSanic):
# Prepend the blueprint URI prefix if available # Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri uri = url_prefix + future.uri if url_prefix else future.uri
apply_route = FutureStatic(uri, *future[1:]) apply_route = FutureStatic(uri, *future[1:])
if (self, apply_route) in app._future_registry:
continue
registered.add(apply_route)
route = app._apply_static(apply_route) route = app._apply_static(apply_route)
routes.append(route) routes.append(route)
@@ -378,56 +338,34 @@ class Blueprint(BaseSanic):
if route_names: if route_names:
# Middleware # Middleware
for future in self._future_middleware: for future in self._future_middleware:
if (self, future) in app._future_registry:
continue
middleware.append(app._apply_middleware(future, route_names)) middleware.append(app._apply_middleware(future, route_names))
# Exceptions # Exceptions
for future in self._future_exceptions: for future in self._future_exceptions:
if (self, future) in app._future_registry:
continue
exception_handlers.append( exception_handlers.append(
app._apply_exception_handler(future, route_names) app._apply_exception_handler(future, route_names)
) )
# Event listeners # Event listeners
for future in self._future_listeners: for listener in self._future_listeners:
if (self, future) in app._future_registry: listeners[listener.event].append(app._apply_listener(listener))
continue
listeners[future.event].append(app._apply_listener(future))
# Signals # Signals
for future in self._future_signals: for signal in self._future_signals:
if (self, future) in app._future_registry: signal.condition.update({"blueprint": self.name})
continue app._apply_signal(signal)
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.routes = [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [ self.websocket_routes = [
route for route in self.routes if route.ctx.websocket route for route in self.routes if route.ctx.websocket
] ]
self.middlewares += middleware self.middlewares = middleware
self.exceptions += exception_handlers self.exceptions = exception_handlers
self.listeners.update(dict(listeners)) self.listeners = dict(listeners)
if self.registered:
self.register_futures(
self.apps,
self,
chain(
registered,
self._future_middleware,
self._future_exceptions,
self._future_listeners,
self._future_signals,
),
)
async def dispatch(self, *args, **kwargs): async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {}) condition = kwargs.pop("condition", {})
condition.update({"__blueprint__": self.name}) condition.update({"blueprint": self.name})
kwargs["condition"] = condition kwargs["condition"] = condition
await asyncio.gather( await asyncio.gather(
*[app.dispatch(*args, **kwargs) for app in self.apps] *[app.dispatch(*args, **kwargs) for app in self.apps]
@@ -455,10 +393,3 @@ class Blueprint(BaseSanic):
value = v value = v
break break
return value return value
@staticmethod
def register_futures(
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
):
for app in apps:
app._future_registry.update(set((bp, item) for item in futures))

View File

View File

@@ -1,189 +0,0 @@
import os
import shutil
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from textwrap import indent
from typing import Any, List, Union
from sanic.app import Sanic
from sanic.application.logo import get_logo
from sanic.cli.arguments import Group
from sanic.log import error_logger
from sanic.simple import create_simple_server
class SanicArgumentParser(ArgumentParser):
...
class SanicCLI:
DESCRIPTION = indent(
f"""
{get_logo(True)}
To start running a Sanic application, provide a path to the module, where
app is a Sanic() instance:
$ sanic path.to.server:app
Or, a path to a callable that returns a Sanic() instance:
$ sanic path.to.factory:create_app --factory
Or, a path to a directory to run as a simple HTTP server:
$ sanic ./path/to/static --simple
""",
prefix=" ",
)
def __init__(self) -> None:
width = shutil.get_terminal_size().columns
self.parser = SanicArgumentParser(
prog="sanic",
description=self.DESCRIPTION,
formatter_class=lambda prog: RawTextHelpFormatter(
prog,
max_help_position=36 if width > 96 else 24,
indent_increment=4,
width=None,
),
)
self.parser._positionals.title = "Required\n========\n Positional"
self.parser._optionals.title = "Optional\n========\n General"
self.main_process = (
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
)
self.args: List[Any] = []
def attach(self):
for group in Group._registry:
group.create(self.parser).attach()
def run(self):
# This is to provide backwards compat -v to display version
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
self.args = self.parser.parse_args(args=parse_args)
self._precheck()
try:
app = self._get_app()
kwargs = self._build_run_kwargs()
app.run(**kwargs)
except ValueError:
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
bool(self.args.cert) != bool(self.args.key)
# new and old style self.args used together
or self.args.tls
and self.args.cert
# strict host checking without certs would always fail
or self.args.tlshost
and not self.args.tls
and not self.args.cert
):
self.parser.print_usage(sys.stderr)
message = (
"TLS certificates must be specified by either of:\n"
" --cert certdir/fullchain.pem --key certdir/privkey.pem\n"
" --tls certdir (equivalent to the above)"
)
error_logger.error(message)
sys.exit(1)
def _get_app(self):
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if self.args.simple:
path = Path(self.args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in self.args.module else "."
module_name, app_name = self.args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
self.args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if self.args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}.app?"
)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
return app
def _build_run_kwargs(self):
ssl: Union[None, dict, str, list] = []
if self.args.tlshost:
ssl.append(None)
if self.args.cert is not None or self.args.key is not None:
ssl.append(dict(cert=self.args.cert, key=self.args.key))
if self.args.tls:
ssl += self.args.tls
if not ssl:
ssl = None
elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector.
ssl = ssl[0]
kwargs = {
"access_log": self.args.access_log,
"debug": self.args.debug,
"fast": self.args.fast,
"host": self.args.host,
"motd": self.args.motd,
"noisy_exceptions": self.args.noisy_exceptions,
"port": self.args.port,
"ssl": ssl,
"unix": self.args.unix,
"verbosity": self.args.verbosity or 0,
"workers": self.args.workers,
}
if self.args.auto_reload:
kwargs["auto_reload"] = 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."
)
return kwargs

View File

@@ -1,237 +0,0 @@
from __future__ import annotations
from argparse import ArgumentParser, _ArgumentGroup
from typing import List, Optional, Type, Union
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
class Group:
name: Optional[str]
container: Union[ArgumentParser, _ArgumentGroup]
_registry: List[Type[Group]] = []
def __init_subclass__(cls) -> None:
Group._registry.append(cls)
def __init__(self, parser: ArgumentParser, title: Optional[str]):
self.parser = parser
if title:
self.container = self.parser.add_argument_group(title=f" {title}")
else:
self.container = self.parser
@classmethod
def create(cls, parser: ArgumentParser):
instance = cls(parser, cls.name)
return instance
def add_bool_arguments(self, *args, **kwargs):
group = self.container.add_mutually_exclusive_group()
kwargs["help"] = kwargs["help"].capitalize()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help'].lower()}".capitalize()
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
class GeneralGroup(Group):
name = None
def attach(self):
self.container.add_argument(
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
self.container.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
class ApplicationGroup(Group):
name = "Application"
def attach(self):
self.container.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
self.container.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help=(
"Run Sanic as a Simple Server, and serve the contents of "
"a directory\n(module arg should be a path)"
),
)
class SocketGroup(Group):
name = "Socket binding"
def attach(self):
self.container.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
self.container.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
self.container.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket",
)
class TLSGroup(Group):
name = "TLS certificate"
def attach(self):
self.container.add_argument(
"--cert",
dest="cert",
type=str,
help="Location of fullchain.pem, bundle.crt or equivalent",
)
self.container.add_argument(
"--key",
dest="key",
type=str,
help="Location of privkey.pem or equivalent .key file",
)
self.container.add_argument(
"--tls",
metavar="DIR",
type=str,
action="append",
help=(
"TLS certificate folder with fullchain.pem and privkey.pem\n"
"May be specified multiple times to choose multiple "
"certificates"
),
)
self.container.add_argument(
"--tls-strict-host",
dest="tlshost",
action="store_true",
help="Only allow clients that send an SNI matching server certs",
)
class WorkerGroup(Group):
name = "Worker"
def attach(self):
group = self.container.add_mutually_exclusive_group()
group.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="Number of worker processes [default 1]",
)
group.add_argument(
"--fast",
dest="fast",
action="store_true",
help="Set the number of workers to max allowed",
)
self.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
class DevelopmentGroup(Group):
name = "Development"
def attach(self):
self.container.add_argument(
"--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."
),
)
self.container.add_argument(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help=(
"Watch source directory for file changes and reload on "
"changes"
),
)
self.container.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes",
)
class OutputGroup(Group):
name = "Output"
def attach(self):
self.add_bool_arguments(
"--motd",
dest="motd",
default=True,
help="Show the startup display",
)
self.container.add_argument(
"-v",
"--verbosity",
action="count",
help="Control logging noise, eg. -vv or --verbosity=2 [default 0]",
)
self.add_bool_arguments(
"--noisy-exceptions",
dest="noisy_exceptions",
help="Output stack traces for all exceptions",
)

View File

@@ -8,21 +8,6 @@ from multidict import CIMultiDict # type: ignore
OS_IS_WINDOWS = os.name == "nt" OS_IS_WINDOWS = os.name == "nt"
UVLOOP_INSTALLED = False
try:
import uvloop # type: ignore # noqa
UVLOOP_INSTALLED = True
except ImportError:
pass
def enable_windows_color_support():
import ctypes
kernel = ctypes.windll.kernel32
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
class Header(CIMultiDict): class Header(CIMultiDict):

View File

@@ -1,34 +1,38 @@
from __future__ import annotations from __future__ import annotations
from inspect import getmembers, isclass, isdatadescriptor from inspect import isclass
from os import environ from os import environ
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, Optional, Sequence, Union from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from warnings import warn
from sanic.errorpages import DEFAULT_FORMAT, check_error_format from sanic.errorpages import check_error_format
from sanic.helpers import Default, _default
from sanic.http import Http from sanic.http import Http
from sanic.log import deprecation, error_logger
from sanic.utils import load_module_from_file_location, str_to_bool from .utils import load_module_from_file_location, str_to_bool
if TYPE_CHECKING: # no cov
from sanic import Sanic
SANIC_PREFIX = "SANIC_" SANIC_PREFIX = "SANIC_"
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"_FALLBACK_ERROR_FORMAT": _default,
"ACCESS_LOG": True, "ACCESS_LOG": True,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False, "EVENT_AUTOREGISTER": False,
"FALLBACK_ERROR_FORMAT": "auto",
"FORWARDED_FOR_HEADER": "X-Forwarded-For", "FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FORWARDED_SECRET": None, "FORWARDED_SECRET": None,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds "KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"KEEP_ALIVE": True, "KEEP_ALIVE": True,
"MOTD": True,
"MOTD_DISPLAY": {},
"NOISY_EXCEPTIONS": False,
"PROXIES_COUNT": None, "PROXIES_COUNT": None,
"REAL_IP_HEADER": None, "REAL_IP_HEADER": None,
"REGISTER": True, "REGISTER": True,
@@ -38,39 +42,21 @@ DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds "REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20, "WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20, "WEBSOCKET_PING_TIMEOUT": 20,
} }
# These values will be removed from the Config object in v22.6 and moved
# to the application state
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
class Config(dict):
class DescriptorMeta(type):
def __init__(cls, *_):
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
@staticmethod
def _is_setter(member: object):
return isdatadescriptor(member) and hasattr(member, "setter")
class Config(dict, metaclass=DescriptorMeta):
ACCESS_LOG: bool ACCESS_LOG: bool
AUTO_EXTEND: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool EVENT_AUTOREGISTER: bool
FALLBACK_ERROR_FORMAT: str
FORWARDED_FOR_HEADER: str FORWARDED_FOR_HEADER: str
FORWARDED_SECRET: Optional[str] FORWARDED_SECRET: Optional[str]
GRACEFUL_SHUTDOWN_TIMEOUT: float GRACEFUL_SHUTDOWN_TIMEOUT: float
KEEP_ALIVE_TIMEOUT: int KEEP_ALIVE_TIMEOUT: int
KEEP_ALIVE: bool KEEP_ALIVE: bool
NOISY_EXCEPTIONS: bool
MOTD: bool
MOTD_DISPLAY: Dict[str, str]
PROXIES_COUNT: Optional[int] PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: Optional[str] REAL_IP_HEADER: Optional[str]
REGISTER: bool REGISTER: bool
@@ -81,7 +67,6 @@ class Config(dict, metaclass=DescriptorMeta):
REQUEST_TIMEOUT: int REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int RESPONSE_TIMEOUT: int
SERVER_NAME: str SERVER_NAME: str
USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: int WEBSOCKET_PING_TIMEOUT: int
@@ -89,27 +74,33 @@ class Config(dict, metaclass=DescriptorMeta):
def __init__( def __init__(
self, self,
defaults: Dict[str, Union[str, bool, int, float, None]] = None, defaults: Dict[str, Union[str, bool, int, float, None]] = None,
load_env: Optional[Union[bool, str]] = True,
env_prefix: Optional[str] = SANIC_PREFIX, env_prefix: Optional[str] = SANIC_PREFIX,
keep_alive: Optional[bool] = None, keep_alive: Optional[bool] = None,
*, *,
converters: Optional[Sequence[Callable[[str], Any]]] = None, app: Optional[Sanic] = None,
): ):
defaults = defaults or {} defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults}) super().__init__({**DEFAULT_CONFIG, **defaults})
self._converters = [str, str_to_bool, float, int] self._app = app
self._LOGO = "" self._LOGO = ""
if converters:
for converter in converters:
self.register_type(converter)
if keep_alive is not None: if keep_alive is not None:
self.KEEP_ALIVE = keep_alive self.KEEP_ALIVE = keep_alive
if env_prefix != SANIC_PREFIX: if env_prefix != SANIC_PREFIX:
if env_prefix: if env_prefix:
self.load_environment_vars(env_prefix) self.load_environment_vars(env_prefix)
elif load_env is not True:
if load_env:
self.load_environment_vars(prefix=load_env)
warn(
"Use of load_env is deprecated and will be removed in "
"21.12. Modify the configuration prefix by passing "
"env_prefix instead.",
DeprecationWarning,
)
else: else:
self.load_environment_vars(SANIC_PREFIX) self.load_environment_vars(SANIC_PREFIX)
@@ -130,21 +121,9 @@ class Config(dict, metaclass=DescriptorMeta):
self.update({attr: value}) self.update({attr: value})
def update(self, *other, **kwargs) -> None: def update(self, *other, **kwargs) -> None:
kwargs.update({k: v for item in other for k, v in dict(item).items()}) other_mapping = {k: v for item in other for k, v in dict(item).items()}
setters: Dict[str, Any] = { super().update(*other, **kwargs)
k: kwargs.pop(k) for attr, value in {**other_mapping, **kwargs}.items():
for k in {**kwargs}.keys()
if k in self.__class__.__setters__
}
for key, value in setters.items():
try:
super().__setattr__(key, value)
except AttributeError:
...
super().update(**kwargs)
for attr, value in {**setters, **kwargs}.items():
self._post_set(attr, value) self._post_set(attr, value)
def _post_set(self, attr, value) -> None: def _post_set(self, attr, value) -> None:
@@ -155,37 +134,32 @@ class Config(dict, metaclass=DescriptorMeta):
"REQUEST_MAX_SIZE", "REQUEST_MAX_SIZE",
): ):
self._configure_header_size() self._configure_header_size()
elif attr == "FALLBACK_ERROR_FORMAT":
self._check_error_format()
if self.app and value != self.app.error_handler.fallback:
if self.app.error_handler.fallback != "auto":
warn(
"Overriding non-default ErrorHandler fallback "
"value. Changing from "
f"{self.app.error_handler.fallback} to {value}."
)
self.app.error_handler.fallback = value
elif attr == "LOGO": elif attr == "LOGO":
self._LOGO = value self._LOGO = value
deprecation( warn(
"Setting the config.LOGO is deprecated and will no longer " "Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.", "be supported starting in v22.6.",
22.6, DeprecationWarning,
) )
@property
def app(self):
return self._app
@property @property
def LOGO(self): def LOGO(self):
return self._LOGO return self._LOGO
@property
def FALLBACK_ERROR_FORMAT(self) -> str:
if self._FALLBACK_ERROR_FORMAT is _default:
return DEFAULT_FORMAT
return self._FALLBACK_ERROR_FORMAT
@FALLBACK_ERROR_FORMAT.setter
def FALLBACK_ERROR_FORMAT(self, value):
self._check_error_format(value)
if (
self._FALLBACK_ERROR_FORMAT is not _default
and value != self._FALLBACK_ERROR_FORMAT
):
error_logger.warning(
"Setting config.FALLBACK_ERROR_FORMAT on an already "
"configured value may have unintended consequences."
)
self._FALLBACK_ERROR_FORMAT = value
def _configure_header_size(self): def _configure_header_size(self):
Http.set_header_max_size( Http.set_header_max_size(
self.REQUEST_MAX_HEADER_SIZE, self.REQUEST_MAX_HEADER_SIZE,
@@ -193,51 +167,36 @@ class Config(dict, metaclass=DescriptorMeta):
self.REQUEST_MAX_SIZE, self.REQUEST_MAX_SIZE,
) )
def _check_error_format(self, format: Optional[str] = None): def _check_error_format(self):
check_error_format(format or self.FALLBACK_ERROR_FORMAT) check_error_format(self.FALLBACK_ERROR_FORMAT)
def load_environment_vars(self, prefix=SANIC_PREFIX): def load_environment_vars(self, prefix=SANIC_PREFIX):
""" """
Looks for prefixed environment variables and applies them to the Looks for prefixed environment variables and applies
configuration if present. This is called automatically when Sanic them to the configuration if present. This is called automatically when
starts up to load environment variables into config. Sanic starts up to load environment variables into config.
It will automatically hydrate the following types: It will automatically hyrdate the following types:
- ``int`` - ``int``
- ``float`` - ``float``
- ``bool`` - ``bool``
Anything else will be imported as a ``str``. If you would like to add Anything else will be imported as a ``str``.
additional types to this list, you can use
:meth:`sanic.config.Config.register_type`. Just make sure that they
are registered before you instantiate your application.
.. code-block:: python
class Foo:
def __init__(self, name) -> None:
self.name = name
config = Config(converters=[Foo])
app = Sanic(__name__, config=config)
`See user guide re: config
<https://sanicframework.org/guide/deployment/configuration.html>`__
""" """
for key, value in environ.items(): for k, v in environ.items():
if not key.startswith(prefix): if k.startswith(prefix):
continue _, config_key = k.split(prefix, 1)
_, config_key = key.split(prefix, 1)
for converter in reversed(self._converters):
try: try:
self[config_key] = converter(value) self[config_key] = int(v)
break
except ValueError: except ValueError:
pass try:
self[config_key] = float(v)
except ValueError:
try:
self[config_key] = str_to_bool(v)
except ValueError:
self[config_key] = v
def update_config(self, config: Union[bytes, str, dict, Any]): def update_config(self, config: Union[bytes, str, dict, Any]):
""" """
@@ -307,17 +266,3 @@ class Config(dict, metaclass=DescriptorMeta):
self.update(config) self.update(config)
load = update_config load = update_config
def register_type(self, converter: Callable[[str], Any]) -> None:
"""
Allows for adding custom function to cast from a string value to any
other type. The function should raise ValueError if it is not the
correct type.
"""
if converter in self._converters:
error_logger.warning(
f"Configuration value converter '{converter.__name__}' has "
"already been registered"
)
return
self._converters.append(converter)

View File

@@ -25,16 +25,14 @@ from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text from sanic.response import HTTPResponse, html, json, text
dumps: t.Callable[..., str]
try: try:
from ujson import dumps from ujson import dumps
dumps = partial(dumps, escape_forward_slashes=False) dumps = partial(dumps, escape_forward_slashes=False)
except ImportError: # noqa except ImportError: # noqa
from json import dumps from json import dumps # type: ignore
DEFAULT_FORMAT = "auto"
FALLBACK_TEXT = ( FALLBACK_TEXT = (
"The server encountered an internal error and " "The server encountered an internal error and "
"cannot complete your request." "cannot complete your request."
@@ -47,8 +45,6 @@ class BaseRenderer:
Base class that all renderers must inherit from. Base class that all renderers must inherit from.
""" """
dumps = staticmethod(dumps)
def __init__(self, request, exception, debug): def __init__(self, request, exception, debug):
self.request = request self.request = request
self.exception = exception self.exception = exception
@@ -116,16 +112,14 @@ class HTMLRenderer(BaseRenderer):
TRACEBACK_STYLE = """ TRACEBACK_STYLE = """
html { font-family: sans-serif } html { font-family: sans-serif }
h2 { color: #888; } h2 { color: #888; }
.tb-wrapper p, dl, dd { margin: 0 } .tb-wrapper p { margin: 0 }
.frame-border { margin: 1rem } .frame-border { margin: 1rem }
.frame-line > *, dt, dd { padding: 0.3rem 0.6rem } .frame-line > * { padding: 0.3rem 0.6rem }
.frame-line, dl { margin-bottom: 0.3rem } .frame-line { margin-bottom: 0.3rem }
.frame-code, dd { font-size: 16px; padding-left: 4ch } .frame-code { font-size: 16px; padding-left: 4ch }
.tb-wrapper, dl { border: 1px solid #eee } .tb-wrapper { border: 1px solid #eee }
.tb-header,.obj-header { .tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
background: #eee; padding: 0.3rem; font-weight: bold .frame-descriptor { background: #e2eafb; font-size: 14px }
}
.frame-descriptor, dt { background: #e2eafb; font-size: 14px }
""" """
TRACEBACK_WRAPPER_HTML = ( TRACEBACK_WRAPPER_HTML = (
"<div class=tb-header>{exc_name}: {exc_value}</div>" "<div class=tb-header>{exc_name}: {exc_value}</div>"
@@ -144,11 +138,6 @@ class HTMLRenderer(BaseRenderer):
"<p class=frame-code><code>{0.line}</code>" "<p class=frame-code><code>{0.line}</code>"
"</div>" "</div>"
) )
OBJECT_WRAPPER_HTML = (
"<div class=obj-header>{title}</div>"
"<dl class={obj_type}>{display_html}</dl>"
)
OBJECT_DISPLAY_HTML = "<dt>{key}</dt><dd><code>{value}</code></dd>"
OUTPUT_HTML = ( OUTPUT_HTML = (
"<!DOCTYPE html><html lang=en>" "<!DOCTYPE html><html lang=en>"
"<meta charset=UTF-8><title>{title}</title>\n" "<meta charset=UTF-8><title>{title}</title>\n"
@@ -163,7 +152,7 @@ class HTMLRenderer(BaseRenderer):
title=self.title, title=self.title,
text=self.text, text=self.text,
style=self.TRACEBACK_STYLE, style=self.TRACEBACK_STYLE,
body=self._generate_body(full=True), body=self._generate_body(),
), ),
status=self.status, status=self.status,
) )
@@ -174,7 +163,7 @@ class HTMLRenderer(BaseRenderer):
title=self.title, title=self.title,
text=self.text, text=self.text,
style=self.TRACEBACK_STYLE, style=self.TRACEBACK_STYLE,
body=self._generate_body(full=False), body="",
), ),
status=self.status, status=self.status,
headers=self.headers, headers=self.headers,
@@ -188,49 +177,27 @@ class HTMLRenderer(BaseRenderer):
def title(self): def title(self):
return escape(f"⚠️ {super().title}") return escape(f"⚠️ {super().title}")
def _generate_body(self, *, full): def _generate_body(self):
lines = [] _, exc_value, __ = sys.exc_info()
if full: exceptions = []
_, exc_value, __ = sys.exc_info() while exc_value:
exceptions = [] exceptions.append(self._format_exc(exc_value))
while exc_value: exc_value = exc_value.__cause__
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines += [
f"<h2>Traceback of {appname} " "(most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> "
f"while handling path <code>{path}</code>",
"</div>",
]
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
lines.append(self._generate_object_display(info, attr))
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines = [
f"<h2>Traceback of {appname} (most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
"</div>",
]
return "\n".join(lines) return "\n".join(lines)
def _generate_object_display(
self, obj: t.Dict[str, t.Any], descriptor: str
) -> str:
display = "".join(
self.OBJECT_DISPLAY_HTML.format(key=key, value=value)
for key, value in obj.items()
)
return self.OBJECT_WRAPPER_HTML.format(
title=descriptor.title(),
display_html=display,
obj_type=descriptor.lower(),
)
def _format_exc(self, exc): def _format_exc(self, exc):
frames = extract_tb(exc.__traceback__) frames = extract_tb(exc.__traceback__)
frame_html = "".join( frame_html = "".join(
@@ -257,7 +224,7 @@ class TextRenderer(BaseRenderer):
title=self.title, title=self.title,
text=self.text, text=self.text,
bar=("=" * len(self.title)), bar=("=" * len(self.title)),
body=self._generate_body(full=True), body=self._generate_body(),
), ),
status=self.status, status=self.status,
) )
@@ -268,7 +235,7 @@ class TextRenderer(BaseRenderer):
title=self.title, title=self.title,
text=self.text, text=self.text,
bar=("=" * len(self.title)), bar=("=" * len(self.title)),
body=self._generate_body(full=False), body="",
), ),
status=self.status, status=self.status,
headers=self.headers, headers=self.headers,
@@ -278,31 +245,21 @@ class TextRenderer(BaseRenderer):
def title(self): def title(self):
return f"⚠️ {super().title}" return f"⚠️ {super().title}"
def _generate_body(self, *, full): def _generate_body(self):
lines = [] _, exc_value, __ = sys.exc_info()
if full: exceptions = []
_, exc_value, __ = sys.exc_info()
exceptions = []
lines += [ lines = [
f"{self.exception.__class__.__name__}: {self.exception} while " f"{self.exception.__class__.__name__}: {self.exception} while "
f"handling path {self.request.path}", f"handling path {self.request.path}",
f"Traceback of {self.request.app.name} " f"Traceback of {self.request.app.name} (most recent call last):\n",
"(most recent call last):\n", ]
]
while exc_value: while exc_value:
exceptions.append(self._format_exc(exc_value)) exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__ exc_value = exc_value.__cause__
lines += exceptions[::-1] return "\n".join(lines + exceptions[::-1])
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
lines += self._generate_object_display_list(info, attr)
return "\n".join(lines)
def _format_exc(self, exc): def _format_exc(self, exc):
frames = "\n\n".join( frames = "\n\n".join(
@@ -315,13 +272,6 @@ class TextRenderer(BaseRenderer):
) )
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}" return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
def _generate_object_display_list(self, obj, descriptor):
lines = [f"\n{descriptor.title()}"]
for key, value in obj.items():
display = self.dumps(value)
lines.append(f"{self.SPACER * 2}{key}: {display}")
return lines
class JSONRenderer(BaseRenderer): class JSONRenderer(BaseRenderer):
""" """
@@ -330,11 +280,11 @@ class JSONRenderer(BaseRenderer):
def full(self) -> HTTPResponse: def full(self) -> HTTPResponse:
output = self._generate_output(full=True) output = self._generate_output(full=True)
return json(output, status=self.status, dumps=self.dumps) return json(output, status=self.status, dumps=dumps)
def minimal(self) -> HTTPResponse: def minimal(self) -> HTTPResponse:
output = self._generate_output(full=False) output = self._generate_output(full=False)
return json(output, status=self.status, dumps=self.dumps) return json(output, status=self.status, dumps=dumps)
def _generate_output(self, *, full): def _generate_output(self, *, full):
output = { output = {
@@ -343,11 +293,6 @@ class JSONRenderer(BaseRenderer):
"message": self.text, "message": self.text,
} }
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
output[attr] = info
if full: if full:
_, exc_value, __ = sys.exc_info() _, exc_value, __ = sys.exc_info()
exceptions = [] exceptions = []
@@ -438,6 +383,7 @@ def exception_response(
""" """
content_type = None content_type = None
print("exception_response", fallback)
if not renderer: if not renderer:
# Make sure we have something set # Make sure we have something set
renderer = base renderer = base

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ _host_re = re.compile(
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and # RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
# curl all have different escaping, that we try to handle as well as possible, # curl all have different escaping, that we try to handle as well as possible,
# even though no client escapes in a way that would allow perfect handling. # even though no client espaces in a way that would allow perfect handling.
# For more information, consult ../tests/test_requests.py # For more information, consult ../tests/test_requests.py

View File

@@ -144,7 +144,7 @@ def import_string(module_name, package=None):
import a module or class by string path. import a module or class by string path.
:module_name: str with path of module or path to import and :module_name: str with path of module or path to import and
instantiate a class instanciate a class
:returns: a module object or one instance from class if :returns: a module object or one instance from class if
module_name is a valid path to class module_name is a valid path to class

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING: # no cov if TYPE_CHECKING:
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse from sanic.response import BaseHTTPResponse
@@ -584,11 +584,6 @@ class Http(metaclass=TouchUpMeta):
self.stage = Stage.FAILED self.stage = Stage.FAILED
raise RuntimeError("Response already started") raise RuntimeError("Response already started")
# Disconnect any earlier but unused response object
if self.response is not None:
self.response.stream = None
# Connect and return the response
self.response, response.stream = response, self self.response, response.stream = response, self
return response return response

View File

@@ -1,12 +1,8 @@
import logging import logging
import sys import sys
from enum import Enum
from typing import Any, Dict
from warnings import warn
LOGGING_CONFIG_DEFAULTS = dict(
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
version=1, version=1,
disable_existing_loggers=False, disable_existing_loggers=False,
loggers={ loggers={
@@ -57,14 +53,6 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
) )
class Colors(str, Enum):
END = "\033[0m"
BLUE = "\033[01;34m"
GREEN = "\033[01;32m"
YELLOW = "\033[01;33m"
RED = "\033[01;31m"
logger = logging.getLogger("sanic.root") logger = logging.getLogger("sanic.root")
""" """
General Sanic logger General Sanic logger
@@ -79,11 +67,3 @@ access_logger = logging.getLogger("sanic.access")
""" """
Logger used by Sanic for access logging Logger used by Sanic for access logging
""" """
def deprecation(message: str, version: float):
version_info = f"[DEPRECATION v{version}] "
if sys.stdout.isatty():
version_info = f"{Colors.RED}{version_info}"
message = f"{Colors.YELLOW}{message}{Colors.END}"
warn(version_info + message, DeprecationWarning)

View File

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

View File

@@ -2,9 +2,8 @@ from enum import Enum, auto
from functools import partial from functools import partial
from typing import List, Optional, Union from typing import List, Optional, Union
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureListener from sanic.models.futures import FutureListener
from sanic.models.handler_types import ListenerType, Sanic from sanic.models.handler_types import ListenerType
class ListenerEvent(str, Enum): class ListenerEvent(str, Enum):
@@ -19,7 +18,7 @@ class ListenerEvent(str, Enum):
MAIN_PROCESS_STOP = auto() MAIN_PROCESS_STOP = auto()
class ListenerMixin(metaclass=SanicMeta): class ListenerMixin:
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
self._future_listeners: List[FutureListener] = [] self._future_listeners: List[FutureListener] = []
@@ -28,10 +27,10 @@ class ListenerMixin(metaclass=SanicMeta):
def listener( def listener(
self, self,
listener_or_event: Union[ListenerType[Sanic], str], listener_or_event: Union[ListenerType, str],
event_or_none: Optional[str] = None, event_or_none: Optional[str] = None,
apply: bool = True, apply: bool = True,
) -> ListenerType[Sanic]: ):
""" """
Create a listener from a decorated function. Create a listener from a decorated function.
@@ -63,32 +62,20 @@ class ListenerMixin(metaclass=SanicMeta):
else: else:
return partial(register_listener, event=listener_or_event) return partial(register_listener, event=listener_or_event)
def main_process_start( def main_process_start(self, listener: ListenerType) -> ListenerType:
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_start") return self.listener(listener, "main_process_start")
def main_process_stop( def main_process_stop(self, listener: ListenerType) -> ListenerType:
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_stop") return self.listener(listener, "main_process_stop")
def before_server_start( def before_server_start(self, listener: ListenerType) -> ListenerType:
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "before_server_start") return self.listener(listener, "before_server_start")
def after_server_start( def after_server_start(self, listener: ListenerType) -> ListenerType:
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "after_server_start") return self.listener(listener, "after_server_start")
def before_server_stop( def before_server_stop(self, listener: ListenerType) -> ListenerType:
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "before_server_stop") return self.listener(listener, "before_server_stop")
def after_server_stop( def after_server_stop(self, listener: ListenerType) -> ListenerType:
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "after_server_stop") return self.listener(listener, "after_server_stop")

View File

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

View File

@@ -1,10 +1,10 @@
from ast import NodeVisitor, Return, parse from ast import NodeVisitor, Return, parse
from contextlib import suppress
from functools import partial, wraps from functools import partial, wraps
from inspect import getsource, signature from inspect import getsource, signature
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path
from pathlib import Path, PurePath from pathlib import PurePath
from re import sub
from textwrap import dedent from textwrap import dedent
from time import gmtime, strftime from time import gmtime, strftime
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
@@ -12,33 +12,29 @@ from urllib.parse import unquote
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
from sanic.base.meta import SanicMeta
from sanic.compat import stat_async from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING from sanic.errorpages import RESPONSE_MAPPING
from sanic.exceptions import ContentRangeError, FileNotFound, HeaderNotFound from sanic.exceptions import (
ContentRangeError,
FileNotFound,
HeaderNotFound,
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler from sanic.handlers import ContentRangeHandler
from sanic.log import deprecation, error_logger from sanic.log import error_logger
from sanic.models.futures import FutureRoute, FutureStatic from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
from sanic.response import HTTPResponse, file, file_stream from sanic.response import HTTPResponse, file, file_stream
from sanic.types import HashableDict from sanic.views import CompositionView
RouteWrapper = Callable[ RouteWrapper = Callable[
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]] [RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
] ]
RESTRICTED_ROUTE_CONTEXT = (
"ignore_body",
"stream",
"hosts",
"static",
"error_format",
"websocket",
)
class RouteMixin(metaclass=SanicMeta): class RouteMixin:
name: str name: str
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
@@ -56,7 +52,7 @@ class RouteMixin(metaclass=SanicMeta):
self, self,
uri: str, uri: str,
methods: Optional[Iterable[str]] = None, methods: Optional[Iterable[str]] = None,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
stream: bool = False, stream: bool = False,
version: Optional[Union[int, str, float]] = None, version: Optional[Union[int, str, float]] = None,
@@ -69,20 +65,10 @@ class RouteMixin(metaclass=SanicMeta):
static: bool = False, static: bool = False,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Decorate a function to be registered as a route Decorate a function to be registered as a route
**Example using context kwargs**
.. code-block:: python
@app.route(..., ctx_foo="foobar")
async def route_handler(request: Request):
assert request.route.ctx.foo == "foobar"
:param uri: path of the URL :param uri: path of the URL
:param methods: list or tuple of methods allowed :param methods: list or tuple of methods allowed
:param host: the host, if required :param host: the host, if required
@@ -94,8 +80,6 @@ class RouteMixin(metaclass=SanicMeta):
body (eg. GET requests) body (eg. GET requests)
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: tuple of routes, decorated function :return: tuple of routes, decorated function
""" """
@@ -110,8 +94,6 @@ class RouteMixin(metaclass=SanicMeta):
if not methods and not websocket: if not methods and not websocket:
methods = frozenset({"GET"}) methods = frozenset({"GET"})
route_context = self._build_route_context(ctx_kwargs)
def decorator(handler): def decorator(handler):
nonlocal uri nonlocal uri
nonlocal methods nonlocal methods
@@ -170,7 +152,6 @@ class RouteMixin(metaclass=SanicMeta):
static, static,
version_prefix, version_prefix,
error_format, error_format,
route_context,
) )
self._future_routes.add(route) self._future_routes.add(route)
@@ -208,14 +189,13 @@ class RouteMixin(metaclass=SanicMeta):
handler: RouteHandler, handler: RouteHandler,
uri: str, uri: str,
methods: Iterable[str] = frozenset({"GET"}), methods: Iterable[str] = frozenset({"GET"}),
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
stream: bool = False, stream: bool = False,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteHandler: ) -> RouteHandler:
"""A helper method to register class instance or """A helper method to register class instance or
functions as a handler to the application url functions as a handler to the application url
@@ -232,8 +212,6 @@ class RouteMixin(metaclass=SanicMeta):
:param stream: boolean specifying if the handler is a stream handler :param stream: boolean specifying if the handler is a stream handler
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: function or class instance :return: function or class instance
""" """
# Handle HTTPMethodView differently # Handle HTTPMethodView differently
@@ -248,6 +226,14 @@ class RouteMixin(metaclass=SanicMeta):
if hasattr(_handler, "is_stream"): if hasattr(_handler, "is_stream"):
stream = True stream = True
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
for _handler in handler.handlers.values():
if hasattr(_handler, "is_stream"):
stream = True
break
if strict_slashes is None: if strict_slashes is None:
strict_slashes = self.strict_slashes strict_slashes = self.strict_slashes
@@ -261,7 +247,6 @@ class RouteMixin(metaclass=SanicMeta):
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
)(handler) )(handler)
return handler return handler
@@ -269,14 +254,13 @@ class RouteMixin(metaclass=SanicMeta):
def get( def get(
self, self,
uri: str, uri: str,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
ignore_body: bool = True, ignore_body: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **GET** *HTTP* method Add an API URL under the **GET** *HTTP* method
@@ -289,8 +273,6 @@ class RouteMixin(metaclass=SanicMeta):
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@@ -303,20 +285,18 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body=ignore_body, ignore_body=ignore_body,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def post( def post(
self, self,
uri: str, uri: str,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
stream: bool = False, stream: bool = False,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **POST** *HTTP* method Add an API URL under the **POST** *HTTP* method
@@ -329,8 +309,6 @@ class RouteMixin(metaclass=SanicMeta):
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@@ -343,20 +321,18 @@ class RouteMixin(metaclass=SanicMeta):
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def put( def put(
self, self,
uri: str, uri: str,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
stream: bool = False, stream: bool = False,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **PUT** *HTTP* method Add an API URL under the **PUT** *HTTP* method
@@ -369,8 +345,6 @@ class RouteMixin(metaclass=SanicMeta):
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@@ -383,20 +357,18 @@ class RouteMixin(metaclass=SanicMeta):
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def head( def head(
self, self,
uri: str, uri: str,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
ignore_body: bool = True, ignore_body: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **HEAD** *HTTP* method Add an API URL under the **HEAD** *HTTP* method
@@ -417,8 +389,6 @@ class RouteMixin(metaclass=SanicMeta):
:type ignore_body: bool, optional :type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@@ -431,20 +401,18 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body=ignore_body, ignore_body=ignore_body,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def options( def options(
self, self,
uri: str, uri: str,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
ignore_body: bool = True, ignore_body: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **OPTIONS** *HTTP* method Add an API URL under the **OPTIONS** *HTTP* method
@@ -465,8 +433,6 @@ class RouteMixin(metaclass=SanicMeta):
:type ignore_body: bool, optional :type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@@ -479,20 +445,18 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body=ignore_body, ignore_body=ignore_body,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def patch( def patch(
self, self,
uri: str, uri: str,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
stream=False, stream=False,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **PATCH** *HTTP* method Add an API URL under the **PATCH** *HTTP* method
@@ -515,8 +479,6 @@ class RouteMixin(metaclass=SanicMeta):
:type ignore_body: bool, optional :type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@@ -529,20 +491,18 @@ class RouteMixin(metaclass=SanicMeta):
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def delete( def delete(
self, self,
uri: str, uri: str,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
ignore_body: bool = True, ignore_body: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper: ) -> RouteWrapper:
""" """
Add an API URL under the **DELETE** *HTTP* method Add an API URL under the **DELETE** *HTTP* method
@@ -555,8 +515,6 @@ class RouteMixin(metaclass=SanicMeta):
:param name: Unique name that can be used to identify the Route :param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method :return: Object decorated with :func:`route` method
""" """
return self.route( return self.route(
@@ -569,21 +527,19 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body=ignore_body, ignore_body=ignore_body,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def websocket( def websocket(
self, self,
uri: str, uri: str,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
subprotocols: Optional[List[str]] = None, subprotocols: Optional[List[str]] = None,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
apply: bool = True, apply: bool = True,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
): ):
""" """
Decorate a function to be registered as a websocket route Decorate a function to be registered as a websocket route
@@ -597,8 +553,6 @@ class RouteMixin(metaclass=SanicMeta):
be used with :func:`url_for` be used with :func:`url_for`
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: tuple of routes, decorated function :return: tuple of routes, decorated function
""" """
return self.route( return self.route(
@@ -613,21 +567,19 @@ class RouteMixin(metaclass=SanicMeta):
websocket=True, websocket=True,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
) )
def add_websocket_route( def add_websocket_route(
self, self,
handler, handler,
uri: str, uri: str,
host: Optional[Union[str, List[str]]] = None, host: Optional[str] = None,
strict_slashes: Optional[bool] = None, strict_slashes: Optional[bool] = None,
subprotocols=None, subprotocols=None,
version: Optional[Union[int, str, float]] = None, version: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
version_prefix: str = "/v", version_prefix: str = "/v",
error_format: Optional[str] = None, error_format: Optional[str] = None,
**ctx_kwargs,
): ):
""" """
A helper method to register a function as a websocket route. A helper method to register a function as a websocket route.
@@ -646,8 +598,6 @@ class RouteMixin(metaclass=SanicMeta):
be used with :func:`url_for` be used with :func:`url_for`
:param version_prefix: URL path that should be before the version :param version_prefix: URL path that should be before the version
value; default: ``/v`` value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Objected decorated by :func:`websocket` :return: Objected decorated by :func:`websocket`
""" """
return self.websocket( return self.websocket(
@@ -659,7 +609,6 @@ class RouteMixin(metaclass=SanicMeta):
name=name, name=name,
version_prefix=version_prefix, version_prefix=version_prefix,
error_format=error_format, error_format=error_format,
**ctx_kwargs,
)(handler) )(handler)
def static( def static(
@@ -769,40 +718,32 @@ class RouteMixin(metaclass=SanicMeta):
content_type=None, content_type=None,
__file_uri__=None, __file_uri__=None,
): ):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if __file_uri__ and "../" in __file_uri__:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided # Merge served directory and requested file if provided
file_path_raw = Path(unquote(file_or_directory)) # Strip all / that in the beginning of the URL to help prevent python
root_path = file_path = file_path_raw.resolve() # from herping a derp and treating the uri as an absolute path
not_found = FileNotFound( root_path = file_path = file_or_directory
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
if __file_uri__: if __file_uri__:
# Strip all / that in the beginning of the URL to help prevent file_path = path.join(
# python from herping a derp and treating the uri as an file_or_directory, sub("^[/]*", "", __file_uri__)
# absolute path )
unquoted_file_uri = unquote(__file_uri__).lstrip("/")
file_path_raw = Path(file_or_directory, unquoted_file_uri)
file_path = file_path_raw.resolve()
if (
file_path < root_path and not file_path_raw.is_symlink()
) or ".." in file_path_raw.parts:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise not_found
try: # URL decode the path sent by the browser otherwise we won't be able to
file_path.relative_to(root_path) # match filenames which got encoded (filenames with spaces etc)
except ValueError: file_path = path.abspath(unquote(file_path))
if not file_path_raw.is_symlink(): if not file_path.startswith(path.abspath(unquote(root_path))):
error_logger.exception( error_logger.exception(
f"File not found: path={file_or_directory}, " f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}" f"relative_url={__file_uri__}"
) )
raise not_found raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
try: try:
headers = {} headers = {}
# Check if the client has been sent this file before # Check if the client has been sent this file before
@@ -870,7 +811,11 @@ class RouteMixin(metaclass=SanicMeta):
except ContentRangeError: except ContentRangeError:
raise raise
except FileNotFoundError: except FileNotFoundError:
raise not_found raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
except Exception: except Exception:
error_logger.exception( error_logger.exception(
f"Exception in static request handler: " f"Exception in static request handler: "
@@ -973,16 +918,19 @@ class RouteMixin(metaclass=SanicMeta):
return route return route
def _determine_error_format(self, handler) -> str: def _determine_error_format(self, handler) -> Optional[str]:
with suppress(OSError, TypeError): if not isinstance(handler, CompositionView):
src = dedent(getsource(handler)) try:
tree = parse(src) src = dedent(getsource(handler))
http_response_types = self._get_response_types(tree) tree = parse(src)
http_response_types = self._get_response_types(tree)
if len(http_response_types) == 1: if len(http_response_types) == 1:
return next(iter(http_response_types)) return next(iter(http_response_types))
except (OSError, TypeError):
...
return "" return None
def _get_response_types(self, node): def _get_response_types(self, node):
types = set() types = set()
@@ -991,18 +939,7 @@ class RouteMixin(metaclass=SanicMeta):
def visit_Return(self, node: Return) -> Any: def visit_Return(self, node: Return) -> Any:
nonlocal types nonlocal types
with suppress(AttributeError): try:
if node.value.func.id == "stream": # type: ignore
deprecation(
"The sanic.response.stream method has been "
"deprecated and will be removed in v22.6. Please "
"upgrade your application to use the new style "
"streaming pattern. See "
"https://sanicframework.org/en/guide/advanced/"
"streaming.html#response-streaming for more "
"information.",
22.6,
)
checks = [node.value.func.id] # type: ignore checks = [node.value.func.id] # type: ignore
if node.value.keywords: # type: ignore if node.value.keywords: # type: ignore
checks += [ checks += [
@@ -1014,32 +951,9 @@ class RouteMixin(metaclass=SanicMeta):
for check in checks: for check in checks:
if check in RESPONSE_MAPPING: if check in RESPONSE_MAPPING:
types.add(RESPONSE_MAPPING[check]) types.add(RESPONSE_MAPPING[check])
except AttributeError:
...
HttpResponseVisitor().visit(node) HttpResponseVisitor().visit(node)
return types return types
def _build_route_context(self, raw):
ctx_kwargs = {
key.replace("ctx_", ""): raw.pop(key)
for key in {**raw}.keys()
if key.startswith("ctx_")
}
restricted = [
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
]
if restricted:
restricted_arguments = ", ".join(restricted)
raise AttributeError(
"Cannot use restricted route context: "
f"{restricted_arguments}. This limitation is only in place "
"until v22.3 when the restricted names will no longer be in"
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
"for more information."
)
if raw:
unexpected_arguments = ", ".join(raw.keys())
raise TypeError(
f"Unexpected keyword arguments: {unexpected_arguments}"
)
return HashableDict(ctx_kwargs)

View File

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

View File

@@ -1,5 +1,4 @@
import asyncio import asyncio
import sys
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
@@ -15,20 +14,10 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol: class MockProtocol:
def __init__(self, transport: "MockTransport", loop): def __init__(self, transport: "MockTransport", loop):
# This should be refactored when < 3.8 support is dropped
self.transport = transport self.transport = transport
# Fixup for 3.8+; Sanic still supports 3.7 where loop is required self._not_paused = asyncio.Event(loop=loop)
loop = loop if sys.version_info[:2] < (3, 8) else None self._not_paused.set()
# Optional in 3.9, necessary in 3.10 because the parameter "loop" self._complete = asyncio.Event(loop=loop)
# was completely removed
if not loop:
self._not_paused = asyncio.Event()
self._not_paused.set()
self._complete = asyncio.Event()
else:
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
def pause_writing(self) -> None: def pause_writing(self) -> None:
self._not_paused.clear() self._not_paused.clear()

View File

@@ -7,14 +7,13 @@ from sanic.models.handler_types import (
MiddlewareType, MiddlewareType,
SignalHandler, SignalHandler,
) )
from sanic.types import HashableDict
class FutureRoute(NamedTuple): class FutureRoute(NamedTuple):
handler: str handler: str
uri: str uri: str
methods: Optional[Iterable[str]] methods: Optional[Iterable[str]]
host: Union[str, List[str]] host: str
strict_slashes: bool strict_slashes: bool
stream: bool stream: bool
version: Optional[int] version: Optional[int]
@@ -26,7 +25,6 @@ class FutureRoute(NamedTuple):
static: bool static: bool
version_prefix: str version_prefix: str
error_format: Optional[str] error_format: Optional[str]
route_context: HashableDict
class FutureListener(NamedTuple): class FutureListener(NamedTuple):
@@ -62,8 +60,3 @@ class FutureSignal(NamedTuple):
handler: SignalHandler handler: SignalHandler
event: str event: str
condition: Optional[Dict[str, str]] condition: Optional[Dict[str, str]]
exclusive: bool
class FutureRegistry(set):
...

View File

@@ -1,6 +1,4 @@
from ssl import SSLObject
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any, Dict, Optional
from sanic.models.protocol_types import TransportProtocol from sanic.models.protocol_types import TransportProtocol
@@ -22,10 +20,8 @@ class ConnInfo:
"peername", "peername",
"server_port", "server_port",
"server", "server",
"server_name",
"sockname", "sockname",
"ssl", "ssl",
"cert",
) )
def __init__(self, transport: TransportProtocol, unix=None): def __init__(self, transport: TransportProtocol, unix=None):
@@ -35,16 +31,8 @@ class ConnInfo:
self.server_port = self.client_port = 0 self.server_port = self.client_port = 0
self.client_ip = "" self.client_ip = ""
self.sockname = addr = transport.get_extra_info("sockname") self.sockname = addr = transport.get_extra_info("sockname")
self.ssl = False self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
self.server_name = ""
self.cert: Dict[str, Any] = {}
sslobj: Optional[SSLObject] = transport.get_extra_info(
"ssl_object"
) # type: ignore
if sslobj:
self.ssl = True
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
self.cert = dict(getattr(sslobj.context, "sanic", {}))
if isinstance(addr, str): # UNIX socket if isinstance(addr, str): # UNIX socket
self.server = unix or addr self.server = unix or addr
return return

View File

@@ -6,6 +6,9 @@ import sys
from time import sleep from time import sleep
from sanic.config import BASE_LOGO
from sanic.log import logger
def _iter_module_files(): def _iter_module_files():
"""This iterates over all relevant Python files. """This iterates over all relevant Python files.
@@ -47,19 +50,13 @@ def _get_args_for_reloading():
return [sys.executable] + sys.argv return [sys.executable] + sys.argv
def restart_with_reloader(changed=None): def restart_with_reloader():
"""Create a new process and a subprocess in it with the same arguments as """Create a new process and a subprocess in it with the same arguments as
this one. this one.
""" """
reloaded = ",".join(changed) if changed else ""
return subprocess.Popen( return subprocess.Popen(
_get_args_for_reloading(), _get_args_for_reloading(),
env={ env={**os.environ, "SANIC_SERVER_RUNNING": "true"},
**os.environ,
"SANIC_SERVER_RUNNING": "true",
"SANIC_RELOADER_PROCESS": "true",
"SANIC_RELOADED_FILES": reloaded,
},
) )
@@ -94,29 +91,31 @@ def watchdog(sleep_interval, app):
worker_process = restart_with_reloader() worker_process = restart_with_reloader()
if app.config.LOGO:
logger.debug(
app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO
)
try: try:
while True: while True:
need_reload = False
changed = set()
for filename in itertools.chain( for filename in itertools.chain(
_iter_module_files(), _iter_module_files(),
*(d.glob("**/*") for d in app.reload_dirs), *(d.glob("**/*") for d in app.reload_dirs),
): ):
try: try:
if _check_file(filename, mtimes): check = _check_file(filename, mtimes)
path = (
filename
if isinstance(filename, str)
else filename.resolve()
)
changed.add(str(path))
except OSError: except OSError:
continue continue
if changed: if check:
need_reload = True
if need_reload:
worker_process.terminate() worker_process.terminate()
worker_process.wait() worker_process.wait()
worker_process = restart_with_reloader(changed) worker_process = restart_with_reloader()
sleep(sleep_interval) sleep(sleep_interval)
except KeyboardInterrupt: except KeyboardInterrupt:

View File

@@ -15,9 +15,10 @@ from typing import (
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
if TYPE_CHECKING: # no cov if TYPE_CHECKING:
from sanic.server import ConnInfo from sanic.server import ConnInfo
from sanic.app import Sanic from sanic.app import Sanic
from sanic.http import Http
import email.utils import email.utils
import uuid import uuid
@@ -31,7 +32,7 @@ from httptools import parse_url # type: ignore
from sanic.compat import CancelledErrors, Header from sanic.compat import CancelledErrors, Header
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.exceptions import InvalidUsage, ServerError from sanic.exceptions import InvalidUsage
from sanic.headers import ( from sanic.headers import (
AcceptContainer, AcceptContainer,
Options, Options,
@@ -41,7 +42,6 @@ from sanic.headers import (
parse_host, parse_host,
parse_xforwarded, parse_xforwarded,
) )
from sanic.http import Http, Stage
from sanic.log import error_logger, logger from sanic.log import error_logger, logger
from sanic.models.protocol_types import TransportProtocol from sanic.models.protocol_types import TransportProtocol
from sanic.response import BaseHTTPResponse, HTTPResponse from sanic.response import BaseHTTPResponse, HTTPResponse
@@ -104,7 +104,6 @@ class Request:
"parsed_json", "parsed_json",
"parsed_forwarded", "parsed_forwarded",
"raw_url", "raw_url",
"responded",
"request_middleware_started", "request_middleware_started",
"route", "route",
"stream", "stream",
@@ -156,7 +155,6 @@ class Request:
self.stream: Optional[Http] = None self.stream: Optional[Http] = None
self.route: Optional[Route] = None self.route: Optional[Route] = None
self._protocol = None self._protocol = None
self.responded: bool = False
def __repr__(self): def __repr__(self):
class_name = self.__class__.__name__ class_name = self.__class__.__name__
@@ -166,21 +164,6 @@ class Request:
def generate_id(*_): def generate_id(*_):
return uuid.uuid4() return uuid.uuid4()
def reset_response(self):
try:
if (
self.stream is not None
and self.stream.stage is not Stage.HANDLER
):
raise ServerError(
"Cannot reset response because previous response was sent."
)
self.stream.response.stream = None
self.stream.response = None
self.responded = False
except AttributeError:
pass
async def respond( async def respond(
self, self,
response: Optional[BaseHTTPResponse] = None, response: Optional[BaseHTTPResponse] = None,
@@ -189,19 +172,13 @@ class Request:
headers: Optional[Union[Header, Dict[str, str]]] = None, headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None, content_type: Optional[str] = None,
): ):
try:
if self.stream is not None and self.stream.response:
raise ServerError("Second respond call is not allowed.")
except AttributeError:
pass
# This logic of determining which response to use is subject to change # This logic of determining which response to use is subject to change
if response is None: if response is None:
response = HTTPResponse( response = (self.stream and self.stream.response) or HTTPResponse(
status=status, status=status,
headers=headers, headers=headers,
content_type=content_type, content_type=content_type,
) )
# Connect the response # Connect the response
if isinstance(response, BaseHTTPResponse) and self.stream: if isinstance(response, BaseHTTPResponse) and self.stream:
response = self.stream.respond(response) response = self.stream.respond(response)
@@ -216,7 +193,6 @@ class Request:
error_logger.exception( error_logger.exception(
"Exception occurred in one of response middleware handlers" "Exception occurred in one of response middleware handlers"
) )
self.responded = True
return response return response
async def receive_body(self): async def receive_body(self):
@@ -784,10 +760,9 @@ def parse_multipart_form(body, boundary):
break break
colon_index = form_line.index(":") colon_index = form_line.index(":")
idx = colon_index + 2
form_header_field = form_line[0:colon_index].lower() form_header_field = form_line[0:colon_index].lower()
form_header_value, form_parameters = parse_content_header( form_header_value, form_parameters = parse_content_header(
form_line[idx:] form_line[colon_index + 2 :]
) )
if form_header_field == "content-disposition": if form_header_field == "content-disposition":

View File

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

View File

@@ -54,7 +54,7 @@ class Router(BaseRouter):
self, path: str, method: str, host: Optional[str] self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]: ) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
""" """
Retrieve a `Route` object containing the details about how to handle Retrieve a `Route` object containg the details about how to handle
a response for a given request a response for a given request
:param request: the incoming request object :param request: the incoming request object

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
from typing import TYPE_CHECKING, Optional, Sequence, cast from typing import TYPE_CHECKING, Optional, Sequence
from websockets.connection import CLOSED, CLOSING, OPEN from websockets.connection import CLOSED, CLOSING, OPEN
from websockets.server import ServerConnection from websockets.server import ServerConnection
from websockets.typing import Subprotocol
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.log import deprecation, error_logger from sanic.log import error_logger
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
from ..websockets.impl import WebsocketImplProtocol from ..websockets.impl import WebsocketImplProtocol
@@ -16,13 +15,12 @@ if TYPE_CHECKING:
class WebSocketProtocol(HttpProtocol): class WebSocketProtocol(HttpProtocol):
__slots__ = (
"websocket", websocket: Optional[WebsocketImplProtocol]
"websocket_timeout", websocket_timeout: float
"websocket_max_size", websocket_max_size = Optional[int]
"websocket_ping_interval", websocket_ping_interval = Optional[float]
"websocket_ping_timeout", websocket_ping_timeout = Optional[float]
)
def __init__( def __init__(
self, self,
@@ -37,29 +35,32 @@ class WebSocketProtocol(HttpProtocol):
**kwargs, **kwargs,
): ):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.websocket: Optional[WebsocketImplProtocol] = None self.websocket = None
self.websocket_timeout = websocket_timeout self.websocket_timeout = websocket_timeout
self.websocket_max_size = websocket_max_size self.websocket_max_size = websocket_max_size
if websocket_max_queue is not None and websocket_max_queue > 0: if websocket_max_queue is not None and websocket_max_queue > 0:
# TODO: Reminder remove this warning in v22.3 # TODO: Reminder remove this warning in v22.3
deprecation( error_logger.warning(
"Websocket no longer uses queueing, so websocket_max_queue" DeprecationWarning(
" is no longer required.", "Websocket no longer uses queueing, so websocket_max_queue"
22.3, " is no longer required."
)
) )
if websocket_read_limit is not None and websocket_read_limit > 0: if websocket_read_limit is not None and websocket_read_limit > 0:
# TODO: Reminder remove this warning in v22.3 # TODO: Reminder remove this warning in v22.3
deprecation( error_logger.warning(
"Websocket no longer uses read buffers, so " DeprecationWarning(
"websocket_read_limit is not required.", "Websocket no longer uses read buffers, so "
22.3, "websocket_read_limit is not required."
)
) )
if websocket_write_limit is not None and websocket_write_limit > 0: if websocket_write_limit is not None and websocket_write_limit > 0:
# TODO: Reminder remove this warning in v22.3 # TODO: Reminder remove this warning in v22.3
deprecation( error_logger.warning(
"Websocket no longer uses write buffers, so " DeprecationWarning(
"websocket_write_limit is not required.", "Websocket no longer uses write buffers, so "
22.3, "websocket_write_limit is not required."
)
) )
self.websocket_ping_interval = websocket_ping_interval self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout self.websocket_ping_timeout = websocket_ping_timeout
@@ -108,22 +109,14 @@ class WebSocketProtocol(HttpProtocol):
return super().close_if_idle() return super().close_if_idle()
async def websocket_handshake( async def websocket_handshake(
self, request, subprotocols: Optional[Sequence[str]] = None self, request, subprotocols=Optional[Sequence[str]]
): ):
# let the websockets package do the handshake with the client # let the websockets package do the handshake with the client
try: try:
if subprotocols is not None: if subprotocols is not None:
# subprotocols can be a set or frozenset, # subprotocols can be a set or frozenset,
# but ServerConnection needs a list # but ServerConnection needs a list
subprotocols = cast( subprotocols = list(subprotocols)
Optional[Sequence[Subprotocol]],
list(
[
Subprotocol(subprotocol)
for subprotocol in subprotocols
]
),
)
ws_conn = ServerConnection( ws_conn = ServerConnection(
max_size=self.websocket_max_size, max_size=self.websocket_max_size,
subprotocols=subprotocols, subprotocols=subprotocols,
@@ -138,18 +131,21 @@ class WebSocketProtocol(HttpProtocol):
) )
raise ServerError(msg, status_code=500) raise ServerError(msg, status_code=500)
if 100 <= resp.status_code <= 299: if 100 <= resp.status_code <= 299:
first_line = ( rbody = "".join(
f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n" [
).encode() "HTTP/1.1 ",
rbody = bytearray(first_line) str(resp.status_code),
rbody += ( " ",
"".join([f"{k}: {v}\r\n" for k, v in resp.headers.items()]) resp.reason_phrase,
).encode() "\r\n",
rbody += b"\r\n" ]
)
rbody += "".join(f"{k}: {v}\r\n" for k, v in resp.headers.items())
if resp.body is not None: if resp.body is not None:
rbody += resp.body rbody += f"\r\n{resp.body}\r\n\r\n"
rbody += b"\r\n\r\n" else:
await super().send(rbody) rbody += "\r\n"
await super().send(rbody.encode())
else: else:
raise ServerError(resp.body, resp.status_code) raise ServerError(resp.body, resp.status_code)
self.websocket = WebsocketImplProtocol( self.websocket = WebsocketImplProtocol(

View File

@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import sys
from ssl import SSLContext from ssl import SSLContext
from typing import TYPE_CHECKING, Dict, Optional, Type, Union from typing import TYPE_CHECKING, Dict, Optional, Type, Union
@@ -21,7 +19,6 @@ from functools import partial
from signal import SIG_IGN, SIGINT, SIGTERM, Signals from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func from signal import signal as signal_func
from sanic.application.ext import setup_ext
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.log import error_logger, logger from sanic.log import error_logger, logger
from sanic.models.server_types import Signal from sanic.models.server_types import Signal
@@ -117,7 +114,6 @@ def serve(
**asyncio_server_kwargs, **asyncio_server_kwargs,
) )
setup_ext(app)
if run_async: if run_async:
return AsyncioServer( return AsyncioServer(
app=app, app=app,
@@ -138,7 +134,6 @@ def serve(
# Ignore SIGINT when run_multiple # Ignore SIGINT when run_multiple
if run_multiple: if run_multiple:
signal_func(SIGINT, SIG_IGN) signal_func(SIGINT, SIG_IGN)
os.environ["SANIC_WORKER_PROCESS"] = "true"
# Register signals for graceful termination # Register signals for graceful termination
if register_sys_signals: if register_sys_signals:
@@ -178,9 +173,6 @@ def serve(
loop.run_until_complete(asyncio.sleep(0.1)) loop.run_until_complete(asyncio.sleep(0.1))
start_shutdown = start_shutdown + 0.1 start_shutdown = start_shutdown + 0.1
if sys.version_info > (3, 7):
app.shutdown_tasks(graceful - start_shutdown)
# Force close non-idle connection after waiting for # Force close non-idle connection after waiting for
# graceful_shutdown_timeout # graceful_shutdown_timeout
for conn in connections: for conn in connections:
@@ -189,6 +181,7 @@ def serve(
else: else:
conn.abort() conn.abort()
loop.run_until_complete(app._server_event("shutdown", "after")) loop.run_until_complete(app._server_event("shutdown", "after"))
remove_unix_socket(unix) remove_unix_socket(unix)
@@ -256,10 +249,7 @@ def serve_multiple(server_settings, workers):
mp = multiprocessing.get_context("fork") mp = multiprocessing.get_context("fork")
for _ in range(workers): for _ in range(workers):
process = mp.Process( process = mp.Process(target=serve, kwargs=server_settings)
target=serve,
kwargs=server_settings,
)
process.daemon = True process.daemon = True
process.start() process.start()
processes.append(process) processes.append(process)

View File

@@ -2,9 +2,8 @@ from __future__ import annotations
import asyncio import asyncio
from enum import Enum
from inspect import isawaitable from inspect import isawaitable
from typing import Any, Dict, List, Optional, Tuple, Union, cast from typing import Any, Dict, List, Optional, Tuple, Union
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
from sanic_routing.exceptions import NotFound # type: ignore from sanic_routing.exceptions import NotFound # type: ignore
@@ -15,47 +14,29 @@ from sanic.log import error_logger, logger
from sanic.models.handler_types import SignalHandler from sanic.models.handler_types import SignalHandler
class Event(Enum):
SERVER_INIT_AFTER = "server.init.after"
SERVER_INIT_BEFORE = "server.init.before"
SERVER_SHUTDOWN_AFTER = "server.shutdown.after"
SERVER_SHUTDOWN_BEFORE = "server.shutdown.before"
HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin"
HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete"
HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception"
HTTP_LIFECYCLE_HANDLE = "http.lifecycle.handle"
HTTP_LIFECYCLE_READ_BODY = "http.lifecycle.read_body"
HTTP_LIFECYCLE_READ_HEAD = "http.lifecycle.read_head"
HTTP_LIFECYCLE_REQUEST = "http.lifecycle.request"
HTTP_LIFECYCLE_RESPONSE = "http.lifecycle.response"
HTTP_ROUTING_AFTER = "http.routing.after"
HTTP_ROUTING_BEFORE = "http.routing.before"
HTTP_LIFECYCLE_SEND = "http.lifecycle.send"
HTTP_MIDDLEWARE_AFTER = "http.middleware.after"
HTTP_MIDDLEWARE_BEFORE = "http.middleware.before"
RESERVED_NAMESPACES = { RESERVED_NAMESPACES = {
"server": ( "server": (
Event.SERVER_INIT_AFTER.value, # "server.main.start",
Event.SERVER_INIT_BEFORE.value, # "server.main.stop",
Event.SERVER_SHUTDOWN_AFTER.value, "server.init.before",
Event.SERVER_SHUTDOWN_BEFORE.value, "server.init.after",
"server.shutdown.before",
"server.shutdown.after",
), ),
"http": ( "http": (
Event.HTTP_LIFECYCLE_BEGIN.value, "http.lifecycle.begin",
Event.HTTP_LIFECYCLE_COMPLETE.value, "http.lifecycle.complete",
Event.HTTP_LIFECYCLE_EXCEPTION.value, "http.lifecycle.exception",
Event.HTTP_LIFECYCLE_HANDLE.value, "http.lifecycle.handle",
Event.HTTP_LIFECYCLE_READ_BODY.value, "http.lifecycle.read_body",
Event.HTTP_LIFECYCLE_READ_HEAD.value, "http.lifecycle.read_head",
Event.HTTP_LIFECYCLE_REQUEST.value, "http.lifecycle.request",
Event.HTTP_LIFECYCLE_RESPONSE.value, "http.lifecycle.response",
Event.HTTP_ROUTING_AFTER.value, "http.routing.after",
Event.HTTP_ROUTING_BEFORE.value, "http.routing.before",
Event.HTTP_LIFECYCLE_SEND.value, "http.lifecycle.send",
Event.HTTP_MIDDLEWARE_AFTER.value, "http.middleware.after",
Event.HTTP_MIDDLEWARE_BEFORE.value, "http.middleware.before",
), ),
} }
@@ -132,7 +113,7 @@ class SignalRouter(BaseRouter):
if fail_not_found: if fail_not_found:
raise e raise e
else: else:
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: if self.ctx.app.debug:
error_logger.warning(str(e)) error_logger.warning(str(e))
return None return None
@@ -142,21 +123,12 @@ class SignalRouter(BaseRouter):
if context: if context:
params.update(context) params.update(context)
signals = group.routes
if not reverse: if not reverse:
signals = signals[::-1] handlers = handlers[::-1]
try: try:
for signal in signals: for handler in handlers:
params.pop("__trigger__", None) if condition is None or condition == handler.__requirements__:
if ( maybe_coroutine = handler(**params)
(condition is None and signal.ctx.exclusive is False)
or (
condition is None
and not signal.handler.__requirements__
)
or (condition == signal.handler.__requirements__)
) and (signal.ctx.trigger or event == signal.ctx.definition):
maybe_coroutine = signal.handler(**params)
if isawaitable(maybe_coroutine): if isawaitable(maybe_coroutine):
retval = await maybe_coroutine retval = await maybe_coroutine
if retval: if retval:
@@ -199,36 +171,23 @@ class SignalRouter(BaseRouter):
handler: SignalHandler, handler: SignalHandler,
event: str, event: str,
condition: Optional[Dict[str, Any]] = None, condition: Optional[Dict[str, Any]] = None,
exclusive: bool = True,
) -> Signal: ) -> Signal:
event_definition = event
parts = self._build_event_parts(event) parts = self._build_event_parts(event)
if parts[2].startswith("<"): if parts[2].startswith("<"):
name = ".".join([*parts[:-1], "*"]) name = ".".join([*parts[:-1], "*"])
trigger = self._clean_trigger(parts[2])
else: else:
name = event name = event
trigger = ""
if not trigger:
event = ".".join([*parts[:2], "<__trigger__>"])
handler.__requirements__ = condition # type: ignore handler.__requirements__ = condition # type: ignore
handler.__trigger__ = trigger # type: ignore
signal = super().add( return super().add(
event, event,
handler, handler,
requirements=condition,
name=name, name=name,
append=True, append=True,
) # type: ignore ) # type: ignore
signal.ctx.exclusive = exclusive
signal.ctx.trigger = trigger
signal.ctx.definition = event_definition
return cast(Signal, signal)
def finalize(self, do_compile: bool = True, do_optimize: bool = False): def finalize(self, do_compile: bool = True, do_optimize: bool = False):
self.add(_blank, "sanic.__signal__.__init__") self.add(_blank, "sanic.__signal__.__init__")
@@ -260,9 +219,3 @@ class SignalRouter(BaseRouter):
"Cannot declare reserved signal event: %s" % event "Cannot declare reserved signal event: %s" % event
) )
return parts return parts
def _clean_trigger(self, trigger: str) -> str:
trigger = trigger[1:-1]
if ":" in trigger:
trigger, _ = trigger.split(":")
return trigger

View File

@@ -1,196 +0,0 @@
import os
import ssl
from typing import Iterable, Optional, Union
from sanic.log import logger
# Only allow secure ciphers, notably leaving out AES-CBC mode
# OpenSSL chooses ECDSA or RSA depending on the cert in use
CIPHERS_TLS12 = [
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-CHACHA20-POLY1305",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES128-GCM-SHA256",
]
def create_context(
certfile: Optional[str] = None,
keyfile: Optional[str] = None,
password: Optional[str] = None,
) -> ssl.SSLContext:
"""Create a context with secure crypto and HTTP/1.1 in protocols."""
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.set_ciphers(":".join(CIPHERS_TLS12))
context.set_alpn_protocols(["http/1.1"])
context.sni_callback = server_name_callback
if certfile and keyfile:
context.load_cert_chain(certfile, keyfile, password)
return context
def shorthand_to_ctx(
ctxdef: Union[None, ssl.SSLContext, dict, str]
) -> Optional[ssl.SSLContext]:
"""Convert an ssl argument shorthand to an SSLContext object."""
if ctxdef is None or isinstance(ctxdef, ssl.SSLContext):
return ctxdef
if isinstance(ctxdef, str):
return load_cert_dir(ctxdef)
if isinstance(ctxdef, dict):
return CertSimple(**ctxdef)
raise ValueError(
f"Invalid ssl argument {type(ctxdef)}."
" Expecting a list of certdirs, a dict or an SSLContext."
)
def process_to_context(
ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple]
) -> Optional[ssl.SSLContext]:
"""Process app.run ssl argument from easy formats to full SSLContext."""
return (
CertSelector(map(shorthand_to_ctx, ssldef))
if isinstance(ssldef, (list, tuple))
else shorthand_to_ctx(ssldef)
)
def load_cert_dir(p: str) -> ssl.SSLContext:
if os.path.isfile(p):
raise ValueError(f"Certificate folder expected but {p} is a file.")
keyfile = os.path.join(p, "privkey.pem")
certfile = os.path.join(p, "fullchain.pem")
if not os.access(keyfile, os.R_OK):
raise ValueError(
f"Certificate not found or permission denied {keyfile}"
)
if not os.access(certfile, os.R_OK):
raise ValueError(
f"Certificate not found or permission denied {certfile}"
)
return CertSimple(certfile, keyfile)
class CertSimple(ssl.SSLContext):
"""A wrapper for creating SSLContext with a sanic attribute."""
def __new__(cls, cert, key, **kw):
# try common aliases, rename to cert/key
certfile = kw["cert"] = kw.pop("certificate", None) or cert
keyfile = kw["key"] = kw.pop("keyfile", None) or key
password = kw.pop("password", None)
if not certfile or not keyfile:
raise ValueError("SSL dict needs filenames for cert and key.")
subject = {}
if "names" not in kw:
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
kw["names"] = [
name
for t, name in cert["subjectAltName"]
if t in ["DNS", "IP Address"]
]
subject = {k: v for item in cert["subject"] for k, v in item}
self = create_context(certfile, keyfile, password)
self.__class__ = cls
self.sanic = {**subject, **kw}
return self
def __init__(self, cert, key, **kw):
pass # Do not call super().__init__ because it is already initialized
class CertSelector(ssl.SSLContext):
"""Automatically select SSL certificate based on the hostname that the
client is trying to access, via SSL SNI. Paths to certificate folders
with privkey.pem and fullchain.pem in them should be provided, and
will be matched in the order given whenever there is a new connection.
"""
def __new__(cls, ctxs):
return super().__new__(cls)
def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]):
super().__init__()
self.sni_callback = selector_sni_callback # type: ignore
self.sanic_select = []
self.sanic_fallback = None
all_names = []
for i, ctx in enumerate(ctxs):
if not ctx:
continue
names = dict(getattr(ctx, "sanic", {})).get("names", [])
all_names += names
self.sanic_select.append(ctx)
if i == 0:
self.sanic_fallback = ctx
if not all_names:
raise ValueError(
"No certificates with SubjectAlternativeNames found."
)
logger.info(f"Certificate vhosts: {', '.join(all_names)}")
def find_cert(self: CertSelector, server_name: str):
"""Find the first certificate that matches the given SNI.
:raises ssl.CertificateError: No matching certificate found.
:return: A matching ssl.SSLContext object if found."""
if not server_name:
if self.sanic_fallback:
return self.sanic_fallback
raise ValueError(
"The client provided no SNI to match for certificate."
)
for ctx in self.sanic_select:
if match_hostname(ctx, server_name):
return ctx
if self.sanic_fallback:
return self.sanic_fallback
raise ValueError(f"No certificate found matching hostname {server_name!r}")
def match_hostname(
ctx: Union[ssl.SSLContext, CertSelector], hostname: str
) -> bool:
"""Match names from CertSelector against a received hostname."""
# Local certs are considered trusted, so this can be less pedantic
# and thus faster than the deprecated ssl.match_hostname function is.
names = dict(getattr(ctx, "sanic", {})).get("names", [])
hostname = hostname.lower()
for name in names:
if name.startswith("*."):
if hostname.split(".", 1)[-1] == name[2:]:
return True
elif name == hostname:
return True
return False
def selector_sni_callback(
sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector
) -> Optional[int]:
"""Select a certificate matching the SNI."""
# Call server_name_callback to store the SNI on sslobj
server_name_callback(sslobj, server_name, ctx)
# Find a new context matching the hostname
try:
sslobj.context = find_cert(ctx, server_name)
except ValueError as e:
logger.warning(f"Rejecting TLS connection: {e}")
# This would show ERR_SSL_UNRECOGNIZED_NAME_ALERT on client side if
# asyncio/uvloop did proper SSL shutdown. They don't.
return ssl.ALERT_DESCRIPTION_UNRECOGNIZED_NAME
return None # mypy complains without explicit return
def server_name_callback(
sslobj: ssl.SSLObject, server_name: str, ctx: ssl.SSLContext
) -> None:
"""Store the received SNI as sslobj.sanic_server_name."""
sslobj.sanic_server_name = server_name # type: ignore

View File

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

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