Compare commits

..

4 Commits

Author SHA1 Message Date
Adam Hopkins
c8fa52e2d2 Allow application creation from Blueprint at startup 2021-12-15 09:36:40 +02:00
Adam Hopkins
266af1e279 Allow empty lazy, and multiple pre-registrations 2021-12-15 09:12:56 +02:00
Adam Hopkins
6d3f1e9982 Add lazy classmethod 2021-12-15 01:44:56 +02:00
Adam Hopkins
a0a3840094 Add pre-registry 2021-12-15 01:07:42 +02:00
265 changed files with 4882 additions and 16339 deletions

2
.black.toml Normal file
View File

@@ -0,0 +1,2 @@
[tool.black]
line-length = 79

28
.codeclimate.yml Normal file
View File

@@ -0,0 +1,28 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- ".github/"
- "changelogs/"
- "docker/"
- "docs/"
- "examples/"
- "scripts/"
- "tests/"
checks:
argument-count:
enabled: false
file-lines:
config:
threshold: 1000
method-count:
config:
threshold: 40
complex-logic:
enabled: false
method-complexity:
config:
threshold: 10

View File

@@ -3,13 +3,13 @@ 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/server/legacy.py sanic/reloader_helpers.py
sanic/compat.py
sanic/simple.py sanic/simple.py
sanic/utils.py sanic/utils.py
sanic/cli
sanic/pages
[html] [html]
directory = coverage directory = coverage
@@ -21,5 +21,3 @@ exclude_lines =
noqa noqa
NOQA NOQA
pragma: no cover pragma: no cover
TYPE_CHECKING
skip_empty = True

View File

@@ -1,66 +0,0 @@
name: 🐞 Bug report
description: Create a report to help us improve
labels: ["bug", "triage"]
body:
- type: checkboxes
id: existing
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks using markdown code-block syntax to make it easier to read.
validations:
required: true
- type: textarea
id: code
attributes:
label: Code snippet
description: Relevant source code, make sure to remove what is not necessary.
validations:
required: false
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: dropdown
id: running
attributes:
label: How do you run Sanic?
options:
- Sanic CLI
- As a module
- As a script (`app.run` or `Sanic.serve`)
- ASGI
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
description: What OS?
validations:
required: true
- type: input
id: version
attributes:
label: Sanic Version
description: Check startup logs or try `sanic --version`
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

25
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,25 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks.
**Code snippet**
Relevant source code, make sure to remove what is not necessary.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 0.8.3]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,8 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: true
contact_links: contact_links:
- name: Questions and Help - name: Questions and Help
url: https://community.sanicframework.org/c/questions-and-help url: https://community.sanicframework.org/c/questions-and-help
about: Do you need help with Sanic? Ask your questions here. about: Do you need help with Sanic? Ask your questions here.
- name: Discussion and Support
url: https://discord.gg/FARQzAEMAA
about: For live discussion and support, checkout the Sanic Discord server.

View File

@@ -1,34 +0,0 @@
name: 🌟 Feature request
description: Suggest an enhancement for Sanic
labels: ["feature request"]
body:
- type: checkboxes
id: existing
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the enhancement you are proposing.
options:
- label: I have searched the existing issues
required: true
- type: textarea
id: description
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: false
- type: textarea
id: code
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

View File

@@ -0,0 +1,16 @@
---
name: Feature request
about: Suggest an idea for Sanic
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or sample code about the feature request here.

View File

@@ -2,13 +2,9 @@ name: "CodeQL"
on: on:
push: push:
branches: branches: [ main ]
- main
- "*LTS"
pull_request: pull_request:
branches: branches: [ main ]
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
schedule: schedule:
- cron: '25 16 * * 0' - cron: '25 16 * * 0'

View File

@@ -3,15 +3,13 @@ on:
push: push:
branches: branches:
- main - main
- "*LTS"
tags: tags:
- "!*" # Do not execute on tags - "!*" # Do not execute on tags
pull_request: pull_request:
branches: types: [opened, synchronize, reopened, ready_for_review]
- main
- "*LTS"
jobs: jobs:
test: test:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
@@ -21,6 +19,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@@ -29,10 +28,9 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox pip install tox
- name: Run coverage - uses: paambaati/codeclimate-action@v2.5.3
run: tox -e coverage if: always()
continue-on-error: true env:
- uses: codecov/codecov-action@v2 CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }}
with: with:
files: ./coverage.xml coverageCommand: tox -e coverage
fail_ci_if_error: false

View File

@@ -3,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
@@ -20,7 +19,6 @@ jobs:
- { 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} - { python-version: "3.10", tox-env: security}
- { python-version: "3.11", tox-env: security}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -3,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
@@ -14,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
config: config:
- {python-version: "3.10", tox-env: "docs"} - {python-version: "3.8", tox-env: "docs"}
fail-fast: false fail-fast: false

View File

@@ -3,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
@@ -16,7 +15,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
config: config:
- { python-version: "3.10", tox-env: lint} - { python-version: 3.8, tox-env: lint}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -3,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@@ -1,47 +0,0 @@
name: Python 3.11 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy311:
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.11",
tox-env: py311,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: "3.11",
tox-env: py311-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,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@@ -3,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@@ -3,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:

View File

@@ -3,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
@@ -16,11 +15,10 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
config: config:
# - { python-version: 3.7, tox-env: type-checking} - { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.8, tox-env: type-checking} - { python-version: 3.8, tox-env: type-checking}
- { python-version: 3.9, tox-env: type-checking} - { python-version: 3.9, tox-env: type-checking}
- { python-version: "3.10", tox-env: type-checking} - { python-version: "3.10", tox-env: type-checking}
- { python-version: "3.11", 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,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
@@ -19,7 +18,6 @@ jobs:
- { 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: "3.10", tox-env: py310-no-ext }
- { python-version: "3.11", 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", "3.11"] python-version: ["3.7", "3.8", "3.9", "3.10"]
steps: steps:
- name: Checkout repository - name: Checkout repository

View File

@@ -11,7 +11,7 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
python-version: ["3.10"] python-version: ["3.8"]
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,9 +329,8 @@ Version 21.3.0
Version 20.12.3 Version 20.12.3
--------------- ---------------
`Current LTS version` Bugfixes
********
**Bugfixes**
* *
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_ `#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
@@ -327,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>`_
@@ -340,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>`_
@@ -353,12 +367,19 @@ 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>`_
Add disable app registry Add disable app registry
Version 20.12.0
---------------
Features
********
* *
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_ `#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
Static route more verbose if file not found Static route more verbose if file not found
@@ -395,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>`_
@@ -421,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>`_
@@ -439,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>`_
@@ -452,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>`_
@@ -463,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>`_
@@ -490,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>`_,
@@ -517,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>`_
@@ -527,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>`_
@@ -537,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>`_
@@ -547,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>`_
@@ -561,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>`_
@@ -583,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>`_
@@ -604,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>`_
@@ -624,7 +663,8 @@ Version 20.6.0
Version 20.3.0 Version 20.3.0
--------------- ---------------
**Features** Features
********
* *
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_ `#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
@@ -655,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>`_
@@ -673,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>`_
@@ -691,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>`_
@@ -701,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>`_
@@ -749,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
@@ -768,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
@@ -782,7 +829,8 @@ Version 19.12.0
Version 19.6.3 Version 19.6.3
-------------- --------------
**Features** Features
********
- Enable Towncrier Support - Enable Towncrier Support
@@ -790,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
@@ -803,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>`_
@@ -819,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>`_
@@ -837,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>`_
@@ -861,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>`_
@@ -929,7 +982,8 @@ Version 19.3
This is a breaking change. This is a breaking change.
**Bugfixes** Bugfixes
********
* *
@@ -965,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)
@@ -973,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
@@ -1040,13 +1096,15 @@ Version 18.12
Version 0.8 Version 0.8
----------- -----------
**0.8.3** 0.8.3
*****
* Changes: * Changes:
* Ownership changed to org 'sanic-org' * Ownership changed to org 'sanic-org'
**0.8.0** 0.8.0
*****
* Changes: * Changes:
@@ -1126,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
@@ -1143,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

@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at adam@sanicframework.org. All reported by contacting the project team at sanic-maintainers@googlegroups.com. All
complaints will be reviewed and investigated and will result in a response that complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident. obligated to maintain confidentiality with regard to the reporter of an incident.

View File

@@ -71,9 +71,9 @@ To execute only unittests, run ``tox`` with environment like so:
.. code-block:: bash .. code-block:: bash
tox -e py37 -v -- tests/test_config.py tox -e py36 -v -- tests/test_config.py
# or # or
tox -e py310 -v -- tests/test_config.py tox -e py37 -v -- tests/test_config.py
Run lint checks Run lint checks
--------------- ---------------
@@ -140,7 +140,6 @@ To maintain the code consistency, Sanic uses following tools.
#. `isort <https://github.com/timothycrosley/isort>`_ #. `isort <https://github.com/timothycrosley/isort>`_
#. `black <https://github.com/python/black>`_ #. `black <https://github.com/python/black>`_
#. `flake8 <https://github.com/PyCQA/flake8>`_ #. `flake8 <https://github.com/PyCQA/flake8>`_
#. `slotscheck <https://github.com/ariebovenberg/slotscheck>`_
isort isort
***** *****
@@ -168,13 +167,7 @@ flake8
#. pycodestyle #. pycodestyle
#. Ned Batchelder's McCabe script #. Ned Batchelder's McCabe script
slotscheck ``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks.
**********
``slotscheck`` ensures that there are no problems with ``__slots__``
(e.g. overlaps, or missing slots in base classes).
``isort``\ , ``black``\ , ``flake8`` and ``slotscheck`` checks are performed during ``tox`` lint checks.
The **easiest** way to make your code conform is to run the following before committing. The **easiest** way to make your code conform is to run the following before committing.

View File

@@ -66,15 +66,15 @@ ifdef include_tests
isort -rc sanic tests isort -rc sanic tests
else else
$(info Sorting Imports) $(info Sorting Imports)
isort -rc sanic tests isort -rc sanic tests --profile=black
endif endif
endif endif
black: black:
black sanic tests black --config ./.black.toml sanic tests
isort: isort:
isort sanic tests isort sanic tests --profile=black
pretty: black isort pretty: black isort

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|
* - Docs * - Docs
- | |UserGuide| |Documentation| - | |UserGuide| |Documentation|
* - Package * - Package
@@ -27,8 +27,6 @@ Sanic | Build fast. Run fast.
:target: https://community.sanicframework.org/ :target: https://community.sanicframework.org/
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord .. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
:target: https://discord.gg/FARQzAEMAA :target: https://discord.gg/FARQzAEMAA
.. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main .. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main .. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
@@ -66,7 +64,7 @@ Sanic | Build fast. Run fast.
Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanicframework.org/en/guide/deployment/running.html#asgi>`_. Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-asgi>`_.
`Source code on GitHub <https://github.com/sanic-org/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_ | `User Guide <https://sanicframework.org>`_ | `Chat on Discord <https://discord.gg/FARQzAEMAA>`_ `Source code on GitHub <https://github.com/sanic-org/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_ | `User Guide <https://sanicframework.org>`_ | `Chat on Discord <https://discord.gg/FARQzAEMAA>`_
@@ -102,6 +100,9 @@ Installation
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
use ``sanic`` with ``ujson`` dependency. use ``sanic`` with ``ujson`` dependency.
.. note::
Windows support is currently "experimental" and on a best-effort basis. Multiple workers are also not currently supported on Windows (see `Issue #1517 <https://github.com/sanic-org/sanic/issues/1517>`_), but setting ``workers=1`` should launch the server successfully.
Hello World Example Hello World Example
------------------- -------------------
@@ -111,7 +112,7 @@ Hello World Example
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
app = Sanic("my-hello-world-app") app = Sanic("My Hello, world app")
@app.route('/') @app.route('/')
async def test(request): async def test(request):

View File

@@ -4,42 +4,31 @@
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release. Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
| Version | LTS | Supported |
| ------- | ------------- | ------------------ |
| 20.12 | until 2022-12 | :heavy_check_mark: |
| 20.9 | | :x: |
| 20.6 | | :x: |
| 20.3 | | :x: |
| 19.12 | until 2021-12 | :white_check_mark: |
| 19.9 | | :x: |
| 19.6 | | :x: |
| 19.3 | | :x: |
| 18.12 | | :x: |
| 0.8.3 | | :x: |
| 0.7.0 | | :x: |
| 0.6.0 | | :x: |
| 0.5.4 | | :x: |
| 0.4.1 | | :x: |
| 0.3.1 | | :x: |
| 0.2.0 | | :x: |
| 0.1.9 | | :x: |
| Version | LTS | Supported | :white_check_mark: = security/bug fixes
| ------- | ------------- | ----------------------- | :heavy_check_mark: = full support
| 22.12 | until 2024-12 | :white_check_mark: |
| 22.9 | | :x: |
| 22.6 | | :x: |
| 22.3 | | :x: |
| 21.12 | until 2023-12 | :ballot_box_with_check: |
| 21.9 | | :x: |
| 21.6 | | :x: |
| 21.3 | | :x: |
| 20.12 | | :x: |
| 20.9 | | :x: |
| 20.6 | | :x: |
| 20.3 | | :x: |
| 19.12 | | :x: |
| 19.9 | | :x: |
| 19.6 | | :x: |
| 19.3 | | :x: |
| 18.12 | | :x: |
| 0.8.3 | | :x: |
| 0.7.0 | | :x: |
| 0.6.0 | | :x: |
| 0.5.4 | | :x: |
| 0.4.1 | | :x: |
| 0.3.1 | | :x: |
| 0.2.0 | | :x: |
| 0.1.9 | | :x: |
:ballot_box_with_check: = security/bug fixes
:white_check_mark: = full support
## Reporting a Vulnerability ## Reporting a Vulnerability
If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button. If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button.
Alternatively, you can send a private message to Adam Hopkins on Discord. Find him on the [Sanic discord server](https://discord.gg/FARQzAEMAA).
This will help to not publicize the issue until the team can address it and resolve it. This will help to not publicize the issue until the team can address it and resolve it.

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/simple.py"
- "sanic/utils.py"
- "sanic/cli/"
- "sanic/pages/"
- ".github/"
- "changelogs/"
- "docker/"
- "docs/"
- "examples/"
- "scripts/"
- "tests/"

View File

@@ -2,12 +2,3 @@
.wy-nav-top { .wy-nav-top {
background: #444444; background: #444444;
} }
#changelog section {
padding-left: 3rem;
}
#changelog section h2,
#changelog section h3 {
margin-left: -3rem;
}

View File

@@ -24,11 +24,7 @@ import sanic
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
extensions = [ extensions = ["sphinx.ext.autodoc", "m2r2"]
"sphinx.ext.autodoc",
"m2r2",
"enum_tools.autoenum",
]
templates_path = ["_templates"] templates_path = ["_templates"]

View File

@@ -9,7 +9,7 @@ API
=== ===
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 2
👥 User Guide <https://sanicframework.org/guide/> 👥 User Guide <https://sanicframework.org/guide/>
sanic/api_reference sanic/api_reference

View File

@@ -15,19 +15,3 @@ sanic.config
.. automodule:: sanic.config .. automodule:: sanic.config
:members: :members:
:show-inheritance: :show-inheritance:
sanic.application.constants
---------------------------
.. automodule:: sanic.application.constants
:exclude-members: StrEnum
:members:
:show-inheritance:
:inherited-members:
sanic.application.state
-----------------------
.. automodule:: sanic.application.state
:members:
:show-inheritance:

View File

@@ -17,14 +17,6 @@ sanic.handlers
:show-inheritance: :show-inheritance:
sanic.headers
--------------
.. automodule:: sanic.headers
:members:
:show-inheritance:
sanic.request sanic.request
------------- -------------
@@ -46,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

@@ -16,3 +16,10 @@ sanic.server
:members: :members:
:show-inheritance: :show-inheritance:
sanic.worker
------------
.. automodule:: sanic.worker
:members:
:show-inheritance:

View File

@@ -1,10 +1,6 @@
📜 Changelog 📜 Changelog
============ ============
.. mdinclude:: ./releases/22/22.12.md .. mdinclude:: ./releases/21.9.md
.. mdinclude:: ./releases/22/22.9.md
.. mdinclude:: ./releases/22/22.6.md
.. mdinclude:: ./releases/22/22.3.md
.. mdinclude:: ./releases/21/21.12.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,66 +0,0 @@
## Version 21.12.1 🔷
_Current LTS version_
- [#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

@@ -1,55 +0,0 @@
## Version 22.12.0 🔶
_Current version_
### Features
- [#2569](https://github.com/sanic-org/sanic/pull/2569) Add `JSONResponse` class with some convenient methods when updating a response object
- [#2598](https://github.com/sanic-org/sanic/pull/2598) Change `uvloop` requirement to `>=0.15.0`
- [#2609](https://github.com/sanic-org/sanic/pull/2609) Add compatibility with `websockets` v11.0
- [#2610](https://github.com/sanic-org/sanic/pull/2610) Kill server early on worker error
- Raise deadlock timeout to 30s
- [#2617](https://github.com/sanic-org/sanic/pull/2617) Scale number of running server workers
- [#2621](https://github.com/sanic-org/sanic/pull/2621) [#2634](https://github.com/sanic-org/sanic/pull/2634) Send `SIGKILL` on subsequent `ctrl+c` to force worker exit
- [#2622](https://github.com/sanic-org/sanic/pull/2622) Add API to restart all workers from the multiplexer
- [#2624](https://github.com/sanic-org/sanic/pull/2624) Default to `spawn` for all subprocesses unless specifically set:
```python
from sanic import Sanic
Sanic.start_method = "fork"
```
- [#2625](https://github.com/sanic-org/sanic/pull/2625) Filename normalisation of form-data/multipart file uploads
- [#2626](https://github.com/sanic-org/sanic/pull/2626) Move to HTTP Inspector:
- Remote access to inspect running Sanic instances
- TLS support for encrypted calls to Inspector
- Authentication to Inspector with API key
- Ability to extend Inspector with custom commands
- [#2632](https://github.com/sanic-org/sanic/pull/2632) Control order of restart operations
- [#2633](https://github.com/sanic-org/sanic/pull/2633) Move reload interval to class variable
- [#2636](https://github.com/sanic-org/sanic/pull/2636) Add `priority` to `register_middleware` method
- [#2639](https://github.com/sanic-org/sanic/pull/2639) Add `unquote` to `add_route` method
- [#2640](https://github.com/sanic-org/sanic/pull/2640) ASGI websockets to receive `text` or `bytes`
### Bugfixes
- [#2607](https://github.com/sanic-org/sanic/pull/2607) Force socket shutdown before close to allow rebinding
- [#2590](https://github.com/sanic-org/sanic/pull/2590) Use actual `StrEnum` in Python 3.11+
- [#2615](https://github.com/sanic-org/sanic/pull/2615) Ensure middleware executes only once per request timeout
- [#2627](https://github.com/sanic-org/sanic/pull/2627) Crash ASGI application on lifespan failure
- [#2635](https://github.com/sanic-org/sanic/pull/2635) Resolve error with low-level server creation on Windows
### Deprecations and Removals
- [#2608](https://github.com/sanic-org/sanic/pull/2608) [#2630](https://github.com/sanic-org/sanic/pull/2630) Signal conditions and triggers saved on `signal.extra`
- [#2626](https://github.com/sanic-org/sanic/pull/2626) Move to HTTP Inspector
- 🚨 *BREAKING CHANGE*: Moves the Inspector to a Sanic app from a simple TCP socket with a custom protocol
- *DEPRECATE*: The `--inspect*` commands have been deprecated in favor of `inspect ...` commands
- [#2628](https://github.com/sanic-org/sanic/pull/2628) Replace deprecated `distutils.strtobool`
### Developer infrastructure
- [#2612](https://github.com/sanic-org/sanic/pull/2612) Add CI testing for Python 3.11

View File

@@ -1,52 +0,0 @@
## Version 22.3.0
### Features
- [#2347](https://github.com/sanic-org/sanic/pull/2347) API for multi-application server
- 🚨 *BREAKING CHANGE*: The old `sanic.worker.GunicornWorker` has been **removed**. To run Sanic with `gunicorn`, you should use it thru `uvicorn` [as described in their docs](https://www.uvicorn.org/#running-with-gunicorn).
- 🧁 *SIDE EFFECT*: Named background tasks are now supported, even in Python 3.7
- [#2357](https://github.com/sanic-org/sanic/pull/2357) Parse `Authorization` header as `Request.credentials`
- [#2361](https://github.com/sanic-org/sanic/pull/2361) Add config option to skip `Touchup` step in application startup
- [#2372](https://github.com/sanic-org/sanic/pull/2372) Updates to CLI help messaging
- [#2382](https://github.com/sanic-org/sanic/pull/2382) Downgrade warnings to backwater debug messages
- [#2396](https://github.com/sanic-org/sanic/pull/2396) Allow for `multidict` v0.6
- [#2401](https://github.com/sanic-org/sanic/pull/2401) Upgrade CLI catching for alternative application run types
- [#2402](https://github.com/sanic-org/sanic/pull/2402) Conditionally inject CLI arguments into factory
- [#2413](https://github.com/sanic-org/sanic/pull/2413) Add new start and stop event listeners to reloader process
- [#2414](https://github.com/sanic-org/sanic/pull/2414) Remove loop as required listener arg
- [#2415](https://github.com/sanic-org/sanic/pull/2415) Better exception for bad URL parsing
- [sanic-routing#47](https://github.com/sanic-org/sanic-routing/pull/47) Add a new extention parameter type: `<file:ext>`, `<file:ext=jpg>`, `<file:ext=jpg|png|gif|svg>`, `<file=int:ext>`, `<file=int:ext=jpg|png|gif|svg>`, `<file=float:ext=tar.gz>`
- 👶 *BETA FEATURE*: This feature will not work with `path` type matching, and is being released as a beta feature only.
- [sanic-routing#57](https://github.com/sanic-org/sanic-routing/pull/57) Change `register_pattern` to accept a `str` or `Pattern`
- [sanic-routing#58](https://github.com/sanic-org/sanic-routing/pull/58) Default matching on non-empty strings only, and new `strorempty` pattern type
- 🚨 *BREAKING CHANGE*: Previously a route with a dynamic string parameter (`/<foo>` or `/<foo:str>`) would match on any string, including empty strings. It will now **only** match a non-empty string. To retain the old behavior, you should use the new parameter type: `/<foo:strorempty>`.
### Bugfixes
- [#2373](https://github.com/sanic-org/sanic/pull/2373) Remove `error_logger` on websockets
- [#2381](https://github.com/sanic-org/sanic/pull/2381) Fix newly assigned `None` in task registry
- [sanic-routing#52](https://github.com/sanic-org/sanic-routing/pull/52) Add type casting to regex route matching
- [sanic-routing#60](https://github.com/sanic-org/sanic-routing/pull/60) Add requirements check on regex routes (this resolves, for example, multiple static directories with differing `host` values)
### Deprecations and Removals
- [#2362](https://github.com/sanic-org/sanic/pull/2362) 22.3 Deprecations and changes
1. `debug=True` and `--debug` do _NOT_ automatically run `auto_reload`
2. Default error render is with plain text (browsers still get HTML by default because `auto` looks at headers)
3. `config` is required for `ErrorHandler.finalize`
4. `ErrorHandler.lookup` requires two positional args
5. Unused websocket protocol args removed
- [#2344](https://github.com/sanic-org/sanic/pull/2344) Deprecate loading of lowercase environment variables
### Developer infrastructure
- [#2363](https://github.com/sanic-org/sanic/pull/2363) Revert code coverage back to Codecov
- [#2405](https://github.com/sanic-org/sanic/pull/2405) Upgrade tests for `sanic-routing` changes
- [sanic-testing#35](https://github.com/sanic-org/sanic-testing/pull/35) Allow for httpx v0.22
### Improved Documentation
- [#2350](https://github.com/sanic-org/sanic/pull/2350) Fix link in README for ASGI
- [#2398](https://github.com/sanic-org/sanic/pull/2398) Document middleware on_request and on_response
- [#2409](https://github.com/sanic-org/sanic/pull/2409) Add missing documentation for `Request.respond`
### Miscellaneous
- [#2376](https://github.com/sanic-org/sanic/pull/2376) Fix typing for `ListenerMixin.listener`
- [#2383](https://github.com/sanic-org/sanic/pull/2383) Clear deprecation warning in `asyncio.wait`
- [#2387](https://github.com/sanic-org/sanic/pull/2387) Cleanup `__slots__` implementations
- [#2390](https://github.com/sanic-org/sanic/pull/2390) Clear deprecation warning in `asyncio.get_event_loop`

View File

@@ -1,54 +0,0 @@
## Version 22.6.2
### Bugfixes
- [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI
## Version 22.6.1
### Bugfixes
- [#2477](https://github.com/sanic-org/sanic/pull/2477) Sanic static directory fails when folder name ends with ".."
## Version 22.6.0
### Features
- [#2378](https://github.com/sanic-org/sanic/pull/2378) Introduce HTTP/3 and autogeneration of TLS certificates in `DEBUG` mode
- 👶 *EARLY RELEASE FEATURE*: Serving Sanic over HTTP/3 is an early release feature. It does not yet fully cover the HTTP/3 spec, but instead aims for feature parity with Sanic's existing HTTP/1.1 server. Websockets, WebTransport, push responses are examples of some features not yet implemented.
- 📦 *EXTRA REQUIREMENT*: Not all HTTP clients are capable of interfacing with HTTP/3 servers. You may need to install a [HTTP/3 capable client](https://curl.se/docs/http3.html).
- 📦 *EXTRA REQUIREMENT*: In order to use TLS autogeneration, you must install either [mkcert](https://github.com/FiloSottile/mkcert) or [trustme](https://github.com/python-trio/trustme).
- [#2416](https://github.com/sanic-org/sanic/pull/2416) Add message to `task.cancel`
- [#2420](https://github.com/sanic-org/sanic/pull/2420) Add exception aliases for more consistent naming with standard HTTP response types (`BadRequest`, `MethodNotAllowed`, `RangeNotSatisfiable`)
- [#2432](https://github.com/sanic-org/sanic/pull/2432) Expose ASGI `scope` as a property on the `Request` object
- [#2438](https://github.com/sanic-org/sanic/pull/2438) Easier access to websocket class for annotation: `from sanic import Websocket`
- [#2439](https://github.com/sanic-org/sanic/pull/2439) New API for reading form values with options: `Request.get_form`
- [#2445](https://github.com/sanic-org/sanic/pull/2445) Add custom `loads` function
- [#2447](https://github.com/sanic-org/sanic/pull/2447), [#2486](https://github.com/sanic-org/sanic/pull/2486) Improved API to support setting cache control headers
- [#2453](https://github.com/sanic-org/sanic/pull/2453) Move verbosity filtering to logger
- [#2475](https://github.com/sanic-org/sanic/pull/2475) Expose getter for current request using `Request.get_current()`
### Bugfixes
- [#2448](https://github.com/sanic-org/sanic/pull/2448) Fix to allow running with `pythonw.exe` or places where there is no `sys.stdout`
- [#2451](https://github.com/sanic-org/sanic/pull/2451) Trigger `http.lifecycle.request` signal in ASGI mode
- [#2455](https://github.com/sanic-org/sanic/pull/2455) Resolve typing of stacked route definitions
- [#2463](https://github.com/sanic-org/sanic/pull/2463) Properly catch websocket CancelledError in websocket handler in Python 3.7
### Deprecations and Removals
- [#2487](https://github.com/sanic-org/sanic/pull/2487) v22.6 deprecations and changes
1. Optional application registry
1. Execution of custom handlers after some part of response was sent
1. Configuring fallback handlers on the `ErrorHandler`
1. Custom `LOGO` setting
1. `sanic.response.stream`
1. `AsyncioServer.init`
### Developer infrastructure
- [#2449](https://github.com/sanic-org/sanic/pull/2449) Clean up `black` and `isort` config
- [#2479](https://github.com/sanic-org/sanic/pull/2479) Fix some flappy tests
### Improved Documentation
- [#2461](https://github.com/sanic-org/sanic/pull/2461) Update example to match current application naming standards
- [#2466](https://github.com/sanic-org/sanic/pull/2466) Better type annotation for `Extend`
- [#2485](https://github.com/sanic-org/sanic/pull/2485) Improved help messages in CLI

View File

@@ -1,74 +0,0 @@
## Version 22.9.1
### Features
- [#2585](https://github.com/sanic-org/sanic/pull/2585) Improved error message when no applications have been registered
### Bugfixes
- [#2578](https://github.com/sanic-org/sanic/pull/2578) Add certificate loader for in process certificate creation
- [#2591](https://github.com/sanic-org/sanic/pull/2591) Do not use sentinel identity for `spawn` compatibility
- [#2592](https://github.com/sanic-org/sanic/pull/2592) Fix properties in nested blueprint groups
- [#2595](https://github.com/sanic-org/sanic/pull/2595) Introduce sleep interval on new worker reloader
### Deprecations and Removals
### Developer infrastructure
- [#2588](https://github.com/sanic-org/sanic/pull/2588) Markdown templates on issue forms
### Improved Documentation
- [#2556](https://github.com/sanic-org/sanic/pull/2556) v22.9 documentation
- [#2582](https://github.com/sanic-org/sanic/pull/2582) Cleanup documentation on Windows support
## Version 22.9.0
### Features
- [#2445](https://github.com/sanic-org/sanic/pull/2445) Add custom loads function
- [#2490](https://github.com/sanic-org/sanic/pull/2490) Make `WebsocketImplProtocol` async iterable
- [#2499](https://github.com/sanic-org/sanic/pull/2499) Sanic Server WorkerManager refactor
- [#2506](https://github.com/sanic-org/sanic/pull/2506) Use `pathlib` for path resolution (for static file serving)
- [#2508](https://github.com/sanic-org/sanic/pull/2508) Use `path.parts` instead of `match` (for static file serving)
- [#2513](https://github.com/sanic-org/sanic/pull/2513) Better request cancel handling
- [#2516](https://github.com/sanic-org/sanic/pull/2516) Add request properties for HTTP method info:
- `request.is_safe`
- `request.is_idempotent`
- `request.is_cacheable`
- *See* [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) *for more information about when these apply*
- [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI
- [#2526](https://github.com/sanic-org/sanic/pull/2526) Cache control support for static files for returning 304 when appropriate
- [#2533](https://github.com/sanic-org/sanic/pull/2533) Refactor `_static_request_handler`
- [#2540](https://github.com/sanic-org/sanic/pull/2540) Add signals before and after handler execution
- `http.handler.before`
- `http.handler.after`
- [#2542](https://github.com/sanic-org/sanic/pull/2542) Add *[redacted]* to CLI :)
- [#2546](https://github.com/sanic-org/sanic/pull/2546) Add deprecation warning filter
- [#2550](https://github.com/sanic-org/sanic/pull/2550) Middleware priority and performance enhancements
### Bugfixes
- [#2495](https://github.com/sanic-org/sanic/pull/2495) Prevent directory traversion with static files
- [#2515](https://github.com/sanic-org/sanic/pull/2515) Do not apply double slash to paths in certain static dirs in Blueprints
### Deprecations and Removals
- [#2525](https://github.com/sanic-org/sanic/pull/2525) Warn on duplicate route names, will be prevented outright in v23.3
- [#2537](https://github.com/sanic-org/sanic/pull/2537) Raise warning and deprecation notice on duplicate exceptions, will be prevented outright in v23.3
### Developer infrastructure
- [#2504](https://github.com/sanic-org/sanic/pull/2504) Cleanup test suite
- [#2505](https://github.com/sanic-org/sanic/pull/2505) Replace Unsupported Python Version Number from the Contributing Doc
- [#2530](https://github.com/sanic-org/sanic/pull/2530) Do not include tests folder in installed package resolver
### Improved Documentation
- [#2502](https://github.com/sanic-org/sanic/pull/2502) Fix a few typos
- [#2517](https://github.com/sanic-org/sanic/pull/2517) [#2536](https://github.com/sanic-org/sanic/pull/2536) Add some type hints

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,9 @@ are added. And blueprint response middleware are executed in _reverse_ order.
On a valid request, it should print "1 2 3 6 5 4" to terminal On a valid request, it should print "1 2 3 6 5 4" to terminal
""" """
app = Sanic("Example") app = Sanic(__name__)
bp = Blueprint("bp_example") bp = Blueprint("bp_" + __name__)
@bp.on_request @bp.on_request

View File

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

View File

@@ -3,8 +3,7 @@ from asyncio import sleep
from sanic import Sanic, response from sanic import Sanic, response
app = Sanic("DelayedResponseApp", strict_slashes=True) app = Sanic(__name__, strict_slashes=True)
app.config.AUTO_EXTEND = False
@app.get("/") @app.get("/")
@@ -12,7 +11,7 @@ async def handler(request):
return response.redirect("/sleep/3") return response.redirect("/sleep/3")
@app.get("/sleep/<t:float>") @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")

View File

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

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

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

View File

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

View File

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

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")

View File

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

View File

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

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,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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,3 @@
[build-system] [build-system]
requires = ["setuptools<60.0", "wheel"] requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.black]
line-length = 79
[tool.isort]
atomic = true
default_section = "THIRDPARTY"
include_trailing_comma = true
known_first_party = "sanic"
known_third_party = "pytest"
line_length = 79
lines_after_imports = 2
lines_between_types = 1
multi_line_output = 3
profile = "black"
[[tool.mypy.overrides]]
module = [
"httptools.*",
"trustme.*",
"sanic_routing.*",
"aioquic.*",
"html5tagger.*",
]
ignore_missing_imports = true

View File

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

View File

@@ -3,16 +3,7 @@ from sanic.app import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.constants import HTTPMethod from sanic.constants import HTTPMethod
from sanic.request import Request from sanic.request import Request
from sanic.response import ( from sanic.response import HTTPResponse, html, json, text
HTTPResponse,
empty,
file,
html,
json,
redirect,
text,
)
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
__all__ = ( __all__ = (
@@ -22,11 +13,7 @@ __all__ = (
"HTTPMethod", "HTTPMethod",
"HTTPResponse", "HTTPResponse",
"Request", "Request",
"Websocket",
"empty",
"file",
"html", "html",
"json", "json",
"redirect",
"text", "text",
) )

View File

@@ -6,10 +6,10 @@ if OS_IS_WINDOWS:
enable_windows_color_support() enable_windows_color_support()
def main(args=None): def main():
cli = SanicCLI() cli = SanicCLI()
cli.attach() cli.attach()
cli.run(args) cli.run()
if __name__ == "__main__": if __name__ == "__main__":

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +0,0 @@
from enum import Enum, IntEnum, auto
class StrEnum(str, Enum): # no cov
def _generate_next_value_(name: str, *args) -> str: # type: ignore
return name.lower()
def __eq__(self, value: object) -> bool:
value = str(value).upper()
return super().__eq__(value)
def __hash__(self) -> int:
return hash(self.value)
def __str__(self) -> str:
return self.value
class Server(StrEnum):
SANIC = auto()
ASGI = auto()
class Mode(StrEnum):
PRODUCTION = auto()
DEBUG = auto()
class ServerStage(IntEnum):
STOPPED = auto()
PARTIAL = auto()
SERVING = auto()

View File

@@ -1,34 +0,0 @@
from __future__ import annotations
from contextlib import suppress
from importlib import import_module
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sanic import Sanic
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: # no cov
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 = getattr(sanic_ext, "Extend")
app._ext = Ext(app, **kwargs)
return app.ext

View File

@@ -3,8 +3,6 @@ import sys
from os import environ from os import environ
from sanic.compat import is_atty
BASE_LOGO = """ BASE_LOGO = """
@@ -40,15 +38,13 @@ FULL_COLOR_LOGO = """
""" # noqa """ # noqa
SVG_LOGO_SIMPLE = """<svg id=logo-simple viewBox="0 0 964 279"><desc>Sanic</desc><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#1f1f1f"/></svg>""" # noqa
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
def get_logo(full=False, coffee=False): def get_logo(full=False, coffee=False):
logo = ( logo = (
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO)) (FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
if is_atty() if sys.stdout.isatty()
else BASE_LOGO else BASE_LOGO
) )

View File

@@ -1,10 +1,11 @@
import sys
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from shutil import get_terminal_size from shutil import get_terminal_size
from textwrap import indent, wrap from textwrap import indent, wrap
from typing import Dict, Optional from typing import Dict, Optional
from sanic import __version__ from sanic import __version__
from sanic.compat import is_atty
from sanic.log import logger from sanic.log import logger
@@ -35,11 +36,14 @@ class MOTD(ABC):
data: Dict[str, str], data: Dict[str, str],
extra: Dict[str, str], extra: Dict[str, str],
) -> None: ) -> None:
motd_class = MOTDTTY if is_atty() else MOTDBasic motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
motd_class(logo, serve_location, data, extra).display() motd_class(logo, serve_location, data, extra).display()
class MOTDBasic(MOTD): class MOTDBasic(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
def display(self): def display(self):
if self.logo: if self.logo:
logger.debug(self.logo) logger.debug(self.logo)
@@ -80,23 +84,20 @@ class MOTDTTY(MOTD):
) )
self.display_length = self.key_width + self.value_width + 2 self.display_length = self.key_width + self.value_width + 2
def display(self, version=True, action="Goin' Fast", out=None): def display(self):
if not out: version = f"Sanic v{__version__}".center(self.centering_length)
out = logger.info
header = "Sanic"
if version:
header += f" v{__version__}"
header = header.center(self.centering_length)
running = ( running = (
f"{action} @ {self.serve_location}" if self.serve_location else "" f"Goin' Fast @ {self.serve_location}"
if self.serve_location
else ""
).center(self.centering_length) ).center(self.centering_length)
length = len(header) + 2 - self.logo_line_length length = len(version) + 2 - self.logo_line_length
first_filler = "" * (self.logo_line_length - 1) first_filler = "" * (self.logo_line_length - 1)
second_filler = "" * length second_filler = "" * length
display_filler = "" * (self.display_length + 2) display_filler = "" * (self.display_length + 2)
lines = [ lines = [
f"\n{first_filler}{second_filler}", f"\n{first_filler}{second_filler}",
f"{header}", f"{version}",
f"{running}", f"{running}",
f"{first_filler}{second_filler}", f"{first_filler}{second_filler}",
] ]
@@ -110,7 +111,7 @@ class MOTDTTY(MOTD):
self._render_fill(lines) self._render_fill(lines)
lines.append(f"{first_filler}{second_filler}\n") lines.append(f"{first_filler}{second_filler}\n")
out(indent("\n".join(lines), " ")) logger.info(indent("\n".join(lines), " "))
def _render_data(self, lines, data, start): def _render_data(self, lines, data, start):
offset = 0 offset = 0

View File

@@ -1,86 +0,0 @@
import os
import sys
import time
from contextlib import contextmanager
from queue import Queue
from threading import Thread
if os.name == "nt": # noqa
import ctypes # noqa
class _CursorInfo(ctypes.Structure):
_fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)]
class Spinner: # noqa
def __init__(self, message: str) -> None:
self.message = message
self.queue: Queue[int] = Queue()
self.spinner = self.cursor()
self.thread = Thread(target=self.run)
def start(self):
self.queue.put(1)
self.thread.start()
self.hide()
def run(self):
while self.queue.get():
output = f"\r{self.message} [{next(self.spinner)}]"
sys.stdout.write(output)
sys.stdout.flush()
time.sleep(0.1)
self.queue.put(1)
def stop(self):
self.queue.put(0)
self.thread.join()
self.show()
@staticmethod
def cursor():
while True:
for cursor in "|/-\\":
yield cursor
@staticmethod
def hide():
if os.name == "nt":
ci = _CursorInfo()
handle = ctypes.windll.kernel32.GetStdHandle(-11)
ctypes.windll.kernel32.GetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
ci.visible = False
ctypes.windll.kernel32.SetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
elif os.name == "posix":
sys.stdout.write("\033[?25l")
sys.stdout.flush()
@staticmethod
def show():
if os.name == "nt":
ci = _CursorInfo()
handle = ctypes.windll.kernel32.GetStdHandle(-11)
ctypes.windll.kernel32.GetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
ci.visible = True
ctypes.windll.kernel32.SetConsoleCursorInfo(
handle, ctypes.byref(ci)
)
elif os.name == "posix":
sys.stdout.write("\033[?25h")
sys.stdout.flush()
@contextmanager
def loading(message: str = "Loading"): # noqa
spinner = Spinner(message)
spinner.start()
yield
spinner.stop()

View File

@@ -3,25 +3,31 @@ from __future__ import annotations
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path from pathlib import Path
from socket import socket from typing import TYPE_CHECKING, Any, Set, Union
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
from sanic.application.constants import Mode, Server, ServerStage from sanic.log import logger
from sanic.log import VerbosityFilter, logger
from sanic.server.async_server import AsyncioServer
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Sanic from sanic import Sanic
@dataclass class StrEnum(str, Enum):
class ApplicationServerInfo: def _generate_next_value_(name: str, *args) -> str: # type: ignore
settings: Dict[str, Any] return name.lower()
stage: ServerStage = field(default=ServerStage.STOPPED)
server: Optional[AsyncioServer] = field(default=None)
class Server(StrEnum):
SANIC = auto()
ASGI = auto()
GUNICORN = auto()
class Mode(StrEnum):
PRODUCTION = auto()
DEBUG = auto()
@dataclass @dataclass
@@ -31,21 +37,15 @@ class ApplicationState:
coffee: bool = field(default=False) coffee: bool = field(default=False)
fast: bool = field(default=False) fast: bool = field(default=False)
host: str = field(default="") host: str = field(default="")
port: int = field(default=0)
ssl: Optional[SSLContext] = field(default=None)
sock: Optional[socket] = field(default=None)
unix: Optional[str] = field(default=None)
mode: Mode = field(default=Mode.PRODUCTION) mode: Mode = field(default=Mode.PRODUCTION)
port: int = field(default=0)
reload_dirs: Set[Path] = field(default_factory=set) reload_dirs: Set[Path] = field(default_factory=set)
auto_reload: bool = field(default=False)
server: Server = field(default=Server.SANIC) server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False) is_running: bool = field(default=False)
is_started: bool = field(default=False) is_started: bool = field(default=False)
is_stopping: bool = field(default=False) is_stopping: bool = field(default=False)
verbosity: int = field(default=0) verbosity: int = field(default=0)
workers: int = field(default=0) workers: int = field(default=0)
primary: bool = field(default=True)
server_info: List[ApplicationServerInfo] = field(default_factory=list)
# This property relates to the ApplicationState instance and should # This property relates to the ApplicationState instance and should
# not be changed except in the __post_init__ method # not be changed except in the __post_init__ method
@@ -69,23 +69,6 @@ class ApplicationState:
if getattr(self.app, "configure_logging", False) and self.app.debug: if getattr(self.app, "configure_logging", False) and self.app.debug:
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
def set_verbosity(self, value: int):
VerbosityFilter.verbosity = value
@property @property
def is_debug(self): def is_debug(self):
return self.mode is Mode.DEBUG return self.mode is Mode.DEBUG
@property
def stage(self) -> ServerStage:
if not self.server_info:
return ServerStage.STOPPED
if all(info.stage is ServerStage.SERVING for info in self.server_info):
return ServerStage.SERVING
elif any(
info.stage is ServerStage.SERVING for info in self.server_info
):
return ServerStage.PARTIAL
return ServerStage.STOPPED

View File

@@ -1,15 +1,13 @@
from __future__ import annotations
import warnings import warnings
from typing import TYPE_CHECKING, Optional from typing import Optional
from urllib.parse import quote from urllib.parse import quote
import sanic.app # noqa
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.helpers import Default
from sanic.http import Stage from sanic.http import Stage
from sanic.log import error_logger, logger
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.response import BaseHTTPResponse
@@ -17,35 +15,29 @@ from sanic.server import ConnInfo
from sanic.server.websockets.connection import WebSocketConnection from sanic.server.websockets.connection import WebSocketConnection
if TYPE_CHECKING:
from sanic import Sanic
class Lifespan: class Lifespan:
def __init__(self, asgi_app: ASGIApp) -> None: def __init__(self, asgi_app: "ASGIApp") -> None:
self.asgi_app = asgi_app self.asgi_app = asgi_app
if ( if (
"server.init.before" "server.init.before"
in self.asgi_app.sanic_app.signal_router.name_index in self.asgi_app.sanic_app.signal_router.name_index
): ):
logger.debug( warnings.warn(
'You have set a listener for "before_server_start" ' 'You have set a listener for "before_server_start" '
"in ASGI mode. " "in ASGI mode. "
"It will be executed as early as possible, but not before " "It will be executed as early as possible, but not before "
"the ASGI server is started.", "the ASGI server is started."
extra={"verbosity": 1},
) )
if ( if (
"server.shutdown.after" "server.shutdown.after"
in self.asgi_app.sanic_app.signal_router.name_index in self.asgi_app.sanic_app.signal_router.name_index
): ):
logger.debug( warnings.warn(
'You have set a listener for "after_server_stop" ' 'You have set a listener for "after_server_stop" '
"in ASGI mode. " "in ASGI mode. "
"It will be executed as late as possible, but not after " "It will be executed as late as possible, but not after "
"the ASGI server is stopped.", "the ASGI server is stopped."
extra={"verbosity": 1},
) )
async def startup(self) -> None: async def startup(self) -> None:
@@ -61,13 +53,6 @@ class Lifespan:
await self.asgi_app.sanic_app._server_event("init", "before") await self.asgi_app.sanic_app._server_event("init", "before")
await self.asgi_app.sanic_app._server_event("init", "after") await self.asgi_app.sanic_app._server_event("init", "after")
if not isinstance(self.asgi_app.sanic_app.config.USE_UVLOOP, 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.
@@ -85,31 +70,17 @@ class Lifespan:
) -> None: ) -> None:
message = await receive() message = await receive()
if message["type"] == "lifespan.startup": if message["type"] == "lifespan.startup":
try: await self.startup()
await self.startup() await send({"type": "lifespan.startup.complete"})
except Exception as e:
error_logger.exception(e)
await send(
{"type": "lifespan.startup.failed", "message": str(e)}
)
else:
await send({"type": "lifespan.startup.complete"})
message = await receive() message = await receive()
if message["type"] == "lifespan.shutdown": if message["type"] == "lifespan.shutdown":
try: await self.shutdown()
await self.shutdown() await send({"type": "lifespan.shutdown.complete"})
except Exception as e:
error_logger.exception(e)
await send(
{"type": "lifespan.shutdown.failed", "message": str(e)}
)
else:
await send({"type": "lifespan.shutdown.complete"})
class ASGIApp: class ASGIApp:
sanic_app: Sanic sanic_app: "sanic.app.Sanic"
request: Request request: Request
transport: MockTransport transport: MockTransport
lifespan: Lifespan lifespan: Lifespan
@@ -178,13 +149,6 @@ class ASGIApp:
instance.request_body = True instance.request_body = True
instance.request.conn_info = ConnInfo(instance.transport) instance.request.conn_info = ConnInfo(instance.transport)
await sanic_app.dispatch(
"http.lifecycle.request",
inline=True,
context={"request": instance.request},
fail_not_found=False,
)
return instance return instance
async def read(self) -> Optional[bytes]: async def read(self) -> Optional[bytes]:
@@ -248,7 +212,4 @@ class ASGIApp:
self.stage = Stage.HANDLER 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:
try: await self.sanic_app.handle_exception(self.request, e)
await self.sanic_app.handle_exception(self.request, e)
except Exception as exc:
await self.sanic_app.handle_exception(self.request, exc, False)

View File

@@ -1,15 +1,14 @@
import re import re
from typing import Any, Optional 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
from sanic.mixins.middleware import MiddlewareMixin from sanic.mixins.middleware import MiddlewareMixin
from sanic.mixins.routes import RouteMixin from sanic.mixins.routes import RouteMixin
from sanic.mixins.signals import SignalMixin from sanic.mixins.signals import SignalMixin
from sanic.mixins.static import StaticMixin
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$") VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
@@ -17,18 +16,14 @@ VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
class BaseSanic( class BaseSanic(
RouteMixin, RouteMixin,
StaticMixin,
MiddlewareMixin, MiddlewareMixin,
ListenerMixin, ListenerMixin,
ExceptionMixin, ExceptionMixin,
SignalMixin, SignalMixin,
metaclass=SanicMeta,
): ):
__slots__ = ("name",) __fake_slots__: Tuple[str, ...]
def __init__( def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
self, name: Optional[str] = None, *args: Any, **kwargs: Any
) -> None:
class_name = self.__class__.__name__ class_name = self.__class__.__name__
if name is None: if name is None:
@@ -38,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
@@ -56,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

@@ -21,10 +21,10 @@ from typing import (
Union, Union,
) )
from sanic_routing.exceptions import NotFound from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.route import Route 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
@@ -37,7 +37,7 @@ from sanic.models.handler_types import (
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Sanic from sanic import Sanic # noqa
def lazy(func, as_decorator=True): def lazy(func, as_decorator=True):
@@ -85,7 +85,7 @@ class Blueprint(BaseSanic):
trailing */* trailing */*
""" """
__slots__ = ( __fake_slots__ = (
"_apps", "_apps",
"_future_routes", "_future_routes",
"_future_statics", "_future_statics",
@@ -98,6 +98,7 @@ class Blueprint(BaseSanic):
"host", "host",
"listeners", "listeners",
"middlewares", "middlewares",
"name",
"routes", "routes",
"statics", "statics",
"strict_slashes", "strict_slashes",
@@ -105,8 +106,8 @@ class Blueprint(BaseSanic):
"version", "version",
"version_prefix", "version_prefix",
"websocket_routes", "websocket_routes",
"wrappers",
) )
__pre_registry__: Dict[Union[str, Default], Any] = defaultdict(list)
def __init__( def __init__(
self, self,
@@ -305,8 +306,11 @@ class Blueprint(BaseSanic):
# Routes # Routes
for future in self._future_routes: for future in self._future_routes:
# attach the blueprint name to the handler so that it can be
# prefixed properly in the router
future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available # Prepend the blueprint URI prefix if available
uri = self._setup_uri(future.uri, url_prefix) uri = url_prefix + future.uri if url_prefix else future.uri
version_prefix = self.version_prefix version_prefix = self.version_prefix
for prefix in ( for prefix in (
@@ -331,7 +335,7 @@ class Blueprint(BaseSanic):
apply_route = FutureRoute( apply_route = FutureRoute(
future.handler, future.handler,
uri, uri[1:] if uri.startswith("//") else uri,
future.methods, future.methods,
host, host,
strict_slashes, strict_slashes,
@@ -345,7 +349,6 @@ class Blueprint(BaseSanic):
future.static, future.static,
version_prefix, version_prefix,
error_format, error_format,
future.route_context,
) )
if (self, apply_route) in app._future_registry: if (self, apply_route) in app._future_registry:
@@ -361,7 +364,7 @@ class Blueprint(BaseSanic):
# Static Files # Static Files
for future in self._future_statics: for future in self._future_statics:
# Prepend the blueprint URI prefix if available # Prepend the blueprint URI prefix if available
uri = self._setup_uri(future.uri, url_prefix) 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: if (self, apply_route) in app._future_registry:
@@ -398,13 +401,12 @@ class Blueprint(BaseSanic):
for future in self._future_signals: for future in self._future_signals:
if (self, future) in app._future_registry: if (self, future) in app._future_registry:
continue continue
future.condition.update({"__blueprint__": self.name}) future.condition.update({"blueprint": self.name})
# Force exclusive to be False app._apply_signal(future)
app._apply_signal(tuple((*future[:-1], False)))
self.routes += [route for route in routes if isinstance(route, Route)] self.routes += [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [ self.websocket_routes += [
route for route in self.routes if route.extra.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
@@ -425,7 +427,7 @@ class Blueprint(BaseSanic):
async def dispatch(self, *args, **kwargs): async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {}) condition = kwargs.pop("condition", {})
condition.update({"__blueprint__": self.name}) condition.update({"blueprint": self.name})
kwargs["condition"] = condition kwargs["condition"] = condition
await asyncio.gather( await asyncio.gather(
*[app.dispatch(*args, **kwargs) for app in self.apps] *[app.dispatch(*args, **kwargs) for app in self.apps]
@@ -440,7 +442,7 @@ class Blueprint(BaseSanic):
events.add(signal.ctx.event) events.add(signal.ctx.event)
return asyncio.wait( return asyncio.wait(
[asyncio.create_task(event.wait()) for event in events], [event.wait() for event in events],
return_when=asyncio.FIRST_COMPLETED, return_when=asyncio.FIRST_COMPLETED,
timeout=timeout, timeout=timeout,
) )
@@ -454,21 +456,36 @@ class Blueprint(BaseSanic):
break break
return value return value
@staticmethod
def _setup_uri(base: str, prefix: Optional[str]):
uri = base
if prefix:
uri = prefix
if base.startswith("/") and prefix.endswith("/"):
uri += base[1:]
else:
uri += base
return uri[1:] if uri.startswith("//") else uri
@staticmethod @staticmethod
def register_futures( def register_futures(
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]] apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
): ):
for app in apps: for app in apps:
app._future_registry.update(set((bp, item) for item in futures)) app._future_registry.update(set((bp, item) for item in futures))
def pre_register(
self,
name: Union[str, Default],
url_prefix: Optional[str] = None,
host: Optional[Union[List[str], str]] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: Union[str, Default] = _default,
) -> None:
if not hasattr(self.ctx, "_prereg"):
self.ctx._prereg = []
self.ctx._prereg.append(name)
self.__class__.__pre_registry__[name].append(
{
k: v
for k, v in {
"bp": self,
"url_prefix": url_prefix,
"host": host,
"version": version,
"strict_slashes": strict_slashes,
"version_prefix": version_prefix,
}.items()
if v is not None and v is not _default
}
)

View File

@@ -1,24 +1,28 @@
import logging
import os import os
import shutil import shutil
import sys import sys
from argparse import Namespace from argparse import ArgumentParser, RawTextHelpFormatter
from functools import partial from importlib import import_module
from pathlib import Path
from textwrap import indent from textwrap import indent
from typing import List, Union, cast from typing import Any, List, Union
from sanic.app import Sanic from sanic.app import Sanic
from sanic.application.logo import get_logo from sanic.application.logo import get_logo
from sanic.blueprints import Blueprint
from sanic.cli.arguments import Group from sanic.cli.arguments import Group
from sanic.cli.base import SanicArgumentParser, SanicHelpFormatter from sanic.helpers import _default
from sanic.cli.inspector import make_inspector_parser from sanic.log import error_logger
from sanic.cli.inspector_client import InspectorClient from sanic.simple import create_simple_server
from sanic.log import Colors, error_logger
from sanic.worker.loader import AppLoader
class SanicArgumentParser(ArgumentParser):
...
class SanicCLI: class SanicCLI:
DEFAULT_APP_NAME = "SANIC"
DESCRIPTION = indent( DESCRIPTION = indent(
f""" f"""
{get_logo(True)} {get_logo(True)}
@@ -44,7 +48,7 @@ Or, a path to a directory to run as a simple HTTP server:
self.parser = SanicArgumentParser( self.parser = SanicArgumentParser(
prog="sanic", prog="sanic",
description=self.DESCRIPTION, description=self.DESCRIPTION,
formatter_class=lambda prog: SanicHelpFormatter( formatter_class=lambda prog: RawTextHelpFormatter(
prog, prog,
max_help_position=36 if width > 96 else 24, max_help_position=36 if width > 96 else 24,
indent_increment=4, indent_increment=4,
@@ -56,128 +60,36 @@ Or, a path to a directory to run as a simple HTTP server:
self.main_process = ( self.main_process = (
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true" os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
) )
self.args: Namespace = Namespace() self.args: List[Any] = []
self.groups: List[Group] = []
self.inspecting = False
def attach(self): def attach(self):
if len(sys.argv) > 1 and sys.argv[1] == "inspect":
self.inspecting = True
self.parser.description = get_logo(True)
make_inspector_parser(self.parser)
return
for group in Group._registry: for group in Group._registry:
instance = group.create(self.parser) group.create(self.parser).attach()
instance.attach()
self.groups.append(instance)
def run(self, parse_args=None): def run(self):
if self.inspecting: # This is to provide backwards compat -v to display version
self._inspector() legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
return parse_args = ["--version"] if legacy_version else None
legacy_version = False
if not parse_args:
# 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
elif parse_args == ["-v"]:
parse_args = ["--version"]
if not legacy_version:
parsed, unknown = self.parser.parse_known_args(args=parse_args)
if unknown and parsed.factory:
for arg in unknown:
if arg.startswith("--"):
self.parser.add_argument(arg.split("=")[0])
self.args = self.parser.parse_args(args=parse_args) self.args = self.parser.parse_args(args=parse_args)
self._precheck() self._precheck()
app_loader = AppLoader(
self.args.module, self.args.factory, self.args.simple, self.args
)
if self.args.inspect or self.args.inspect_raw or self.args.trigger:
self._inspector_legacy(app_loader)
return
try: try:
app = self._get_app(app_loader) app = self._get_app()
kwargs = self._build_run_kwargs() kwargs = self._build_run_kwargs()
except ValueError as e: app.run(**kwargs)
error_logger.exception(f"Failed to run app: {e}") except ValueError:
else: error_logger.exception("Failed to run app")
for http_version in self.args.http:
app.prepare(**kwargs, version=http_version)
if self.args.single:
serve = Sanic.serve_single
elif self.args.legacy:
serve = Sanic.serve_legacy
else:
serve = partial(Sanic.serve, app_loader=app_loader)
serve(app)
def _inspector_legacy(self, app_loader: AppLoader):
host = port = None
module = cast(str, self.args.module)
if ":" in module:
maybe_host, maybe_port = module.rsplit(":", 1)
if maybe_port.isnumeric():
host, port = maybe_host, int(maybe_port)
if not host:
app = self._get_app(app_loader)
host, port = app.config.INSPECTOR_HOST, app.config.INSPECTOR_PORT
action = self.args.trigger or "info"
InspectorClient(
str(host), int(port or 6457), False, self.args.inspect_raw, ""
).do(action)
sys.stdout.write(
f"\n{Colors.BOLD}{Colors.YELLOW}WARNING:{Colors.END} "
"You are using the legacy CLI command that will be removed in "
f"{Colors.RED}v23.3{Colors.END}. See "
"https://sanic.dev/en/guide/release-notes/v22.12.html"
"#deprecations-and-removals or checkout the new "
"style commands:\n\n\t"
f"{Colors.YELLOW}sanic inspect --help{Colors.END}\n"
)
def _inspector(self):
args = sys.argv[2:]
self.args, unknown = self.parser.parse_known_args(args=args)
if unknown:
for arg in unknown:
if arg.startswith("--"):
try:
key, value = arg.split("=")
key = key.lstrip("-")
except ValueError:
value = False if arg.startswith("--no-") else True
key = (
arg.replace("--no-", "")
.lstrip("-")
.replace("-", "_")
)
setattr(self.args, key, value)
kwargs = {**self.args.__dict__}
host = kwargs.pop("host")
port = kwargs.pop("port")
secure = kwargs.pop("secure")
raw = kwargs.pop("raw")
action = kwargs.pop("action") or "info"
api_key = kwargs.pop("api_key")
positional = kwargs.pop("positional", None)
if action == "<custom>" and positional:
action = positional[0]
if len(positional) > 1:
kwargs["args"] = positional[1:]
InspectorClient(host, port, secure, raw, api_key).do(action, **kwargs)
def _precheck(self): def _precheck(self):
# Custom TLS mismatch handling for better diagnostics 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 ( if self.main_process and (
# one of cert/key missing # one of cert/key missing
bool(self.args.cert) != bool(self.args.key) bool(self.args.cert) != bool(self.args.key)
@@ -197,27 +109,58 @@ Or, a path to a directory to run as a simple HTTP server:
) )
error_logger.error(message) error_logger.error(message)
sys.exit(1) sys.exit(1)
if self.args.inspect or self.args.inspect_raw:
logging.disable(logging.CRITICAL)
def _get_app(self, app_loader: AppLoader): def _get_app(self):
try: try:
app = app_loader.load() 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 isinstance(app, Blueprint):
bp = app
name = (
bp.ctx._prereg[0]
if hasattr(bp.ctx, "_prereg")
else _default
)
if name is _default:
name = self.DEFAULT_APP_NAME
app = Sanic(name)
elif not isinstance(app, Sanic):
raise ValueError(
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: except ImportError as e:
if app_loader.module_name.startswith(e.name): # type: ignore if module_name.startswith(e.name):
error_logger.error( error_logger.error(
f"No module named {e.name} found.\n" f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n" " Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app" " Example Module: project.sanic_server.app"
) )
sys.exit(1)
else: else:
raise e raise e
return app return app
def _build_run_kwargs(self): def _build_run_kwargs(self):
for group in self.groups:
group.prepare(self.args)
ssl: Union[None, dict, str, list] = [] ssl: Union[None, dict, str, list] = []
if self.args.tlshost: if self.args.tlshost:
ssl.append(None) ssl.append(None)
@@ -230,10 +173,8 @@ Or, a path to a directory to run as a simple HTTP server:
elif len(ssl) == 1 and ssl[0] is not None: elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector. # Use only one cert, no TLSSelector.
ssl = ssl[0] ssl = ssl[0]
kwargs = { kwargs = {
"access_log": self.args.access_log, "access_log": self.args.access_log,
"coffee": self.args.coffee,
"debug": self.args.debug, "debug": self.args.debug,
"fast": self.args.fast, "fast": self.args.fast,
"host": self.args.host, "host": self.args.host,
@@ -244,17 +185,18 @@ Or, a path to a directory to run as a simple HTTP server:
"unix": self.args.unix, "unix": self.args.unix,
"verbosity": self.args.verbosity or 0, "verbosity": self.args.verbosity or 0,
"workers": self.args.workers, "workers": self.args.workers,
"auto_tls": self.args.auto_tls,
"single_process": self.args.single,
"legacy": self.args.legacy,
} }
for maybe_arg in ("auto_reload", "dev"): if self.args.auto_reload:
if getattr(self.args, maybe_arg, False): kwargs["auto_reload"] = True
kwargs[maybe_arg] = True
if self.args.path: if self.args.path:
kwargs["auto_reload"] = True if self.args.auto_reload or self.args.debug:
kwargs["reload_dir"] = self.args.path 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 return kwargs

View File

@@ -3,10 +3,9 @@ from __future__ import annotations
from argparse import ArgumentParser, _ArgumentGroup from argparse import ArgumentParser, _ArgumentGroup
from typing import List, Optional, Type, Union from typing import List, Optional, Type, Union
from sanic_routing import __version__ as __routing_version__ from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__ from sanic import __version__
from sanic.http.constants import HTTP
class Group: class Group:
@@ -30,7 +29,7 @@ class Group:
instance = cls(parser, cls.name) instance = cls(parser, cls.name)
return instance return instance
def add_bool_arguments(self, *args, nullable=False, **kwargs): def add_bool_arguments(self, *args, **kwargs):
group = self.container.add_mutually_exclusive_group() group = self.container.add_mutually_exclusive_group()
kwargs["help"] = kwargs["help"].capitalize() kwargs["help"] = kwargs["help"].capitalize()
group.add_argument(*args, action="store_true", **kwargs) group.add_argument(*args, action="store_true", **kwargs)
@@ -38,12 +37,6 @@ class Group:
group.add_argument( group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs "--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
) )
if nullable:
params = {args[0][2:].replace("-", "_"): None}
group.set_defaults(**params)
def prepare(self, args) -> None:
...
class GeneralGroup(Group): class GeneralGroup(Group):
@@ -70,8 +63,7 @@ class ApplicationGroup(Group):
name = "Application" name = "Application"
def attach(self): def attach(self):
group = self.container.add_mutually_exclusive_group() self.container.add_argument(
group.add_argument(
"--factory", "--factory",
action="store_true", action="store_true",
help=( help=(
@@ -79,7 +71,7 @@ class ApplicationGroup(Group):
"i.e. a () -> <Sanic app> callable" "i.e. a () -> <Sanic app> callable"
), ),
) )
group.add_argument( self.container.add_argument(
"-s", "-s",
"--simple", "--simple",
dest="simple", dest="simple",
@@ -89,70 +81,6 @@ class ApplicationGroup(Group):
"a directory\n(module arg should be a path)" "a directory\n(module arg should be a path)"
), ),
) )
group.add_argument(
"--inspect",
dest="inspect",
action="store_true",
help=("Inspect the state of a running instance, human readable"),
)
group.add_argument(
"--inspect-raw",
dest="inspect_raw",
action="store_true",
help=("Inspect the state of a running instance, JSON output"),
)
group.add_argument(
"--trigger-reload",
dest="trigger",
action="store_const",
const="reload",
help=("Trigger worker processes to reload"),
)
group.add_argument(
"--trigger-shutdown",
dest="trigger",
action="store_const",
const="shutdown",
help=("Trigger all processes to shutdown"),
)
class HTTPVersionGroup(Group):
name = "HTTP version"
def attach(self):
http_values = [http.value for http in HTTP.__members__.values()]
self.container.add_argument(
"--http",
dest="http",
action="append",
choices=http_values,
type=int,
help=(
"Which HTTP version to use: HTTP/1.1 or HTTP/3. Value should\n"
"be either 1, or 3. [default 1]"
),
)
self.container.add_argument(
"-1",
dest="http",
action="append_const",
const=1,
help=("Run Sanic server using HTTP/1.1"),
)
self.container.add_argument(
"-3",
dest="http",
action="append_const",
const=3,
help=("Run Sanic server using HTTP/3"),
)
def prepare(self, args):
if not args.http:
args.http = [1]
args.http = tuple(sorted(set(map(HTTP, args.http)), reverse=True))
class SocketGroup(Group): class SocketGroup(Group):
@@ -164,6 +92,7 @@ class SocketGroup(Group):
"--host", "--host",
dest="host", dest="host",
type=str, type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]", help="Host address [default 127.0.0.1]",
) )
self.container.add_argument( self.container.add_argument(
@@ -171,6 +100,7 @@ class SocketGroup(Group):
"--port", "--port",
dest="port", dest="port",
type=int, type=int,
default=8000,
help="Port to serve on [default 8000]", help="Port to serve on [default 8000]",
) )
self.container.add_argument( self.container.add_argument(
@@ -237,22 +167,8 @@ class WorkerGroup(Group):
action="store_true", action="store_true",
help="Set the number of workers to max allowed", help="Set the number of workers to max allowed",
) )
group.add_argument(
"--single-process",
dest="single",
action="store_true",
help="Do not use multiprocessing, run server in a single process",
)
self.container.add_argument(
"--legacy",
action="store_true",
help="Use the legacy server manager",
)
self.add_bool_arguments( self.add_bool_arguments(
"--access-logs", "--access-logs", dest="access_log", help="display access logs"
dest="access_log",
help="display access logs",
default=None,
) )
@@ -266,6 +182,18 @@ class DevelopmentGroup(Group):
action="store_true", action="store_true",
help="Run the server in debug mode", 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( self.container.add_argument(
"-r", "-r",
"--reload", "--reload",
@@ -284,34 +212,12 @@ class DevelopmentGroup(Group):
action="append", action="append",
help="Extra directories to watch and reload on changes", help="Extra directories to watch and reload on changes",
) )
self.container.add_argument(
"-d",
"--dev",
dest="dev",
action="store_true",
help=("debug + auto reload"),
)
self.container.add_argument(
"--auto-tls",
dest="auto_tls",
action="store_true",
help=(
"Create a temporary TLS certificate for local development "
"(requires mkcert or trustme)"
),
)
class OutputGroup(Group): class OutputGroup(Group):
name = "Output" name = "Output"
def attach(self): def attach(self):
self.add_bool_arguments(
"--coffee",
dest="coffee",
default=False,
help="Uhm, coffee?",
)
self.add_bool_arguments( self.add_bool_arguments(
"--motd", "--motd",
dest="motd", dest="motd",

View File

@@ -1,35 +0,0 @@
from argparse import (
SUPPRESS,
Action,
ArgumentParser,
RawTextHelpFormatter,
_SubParsersAction,
)
from typing import Any
class SanicArgumentParser(ArgumentParser):
def _check_value(self, action: Action, value: Any) -> None:
if isinstance(action, SanicSubParsersAction):
return
super()._check_value(action, value)
class SanicHelpFormatter(RawTextHelpFormatter):
def add_usage(self, usage, actions, groups, prefix=None):
if not usage:
usage = SUPPRESS
# Add one linebreak, but not two
self.add_text("\x1b[1A")
super().add_usage(usage, actions, groups, prefix)
class SanicSubParsersAction(_SubParsersAction):
def __call__(self, parser, namespace, values, option_string=None):
self._name_parser_map
parser_name = values[0]
if parser_name not in self._name_parser_map:
self._name_parser_map[parser_name] = parser
values = ["<custom>", *values]
super().__call__(parser, namespace, values, option_string)

View File

@@ -1,105 +0,0 @@
from argparse import ArgumentParser
from sanic.application.logo import get_logo
from sanic.cli.base import SanicHelpFormatter, SanicSubParsersAction
def _add_shared(parser: ArgumentParser) -> None:
parser.add_argument(
"--host",
"-H",
default="localhost",
help="Inspector host address [default 127.0.0.1]",
)
parser.add_argument(
"--port",
"-p",
default=6457,
type=int,
help="Inspector port [default 6457]",
)
parser.add_argument(
"--secure",
"-s",
action="store_true",
help="Whether to access the Inspector via TLS encryption",
)
parser.add_argument("--api-key", "-k", help="Inspector authentication key")
parser.add_argument(
"--raw",
action="store_true",
help="Whether to output the raw response information",
)
class InspectorSubParser(ArgumentParser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
_add_shared(self)
if not self.description:
self.description = ""
self.description = get_logo(True) + self.description
def make_inspector_parser(parser: ArgumentParser) -> None:
_add_shared(parser)
subparsers = parser.add_subparsers(
action=SanicSubParsersAction,
dest="action",
description=(
"Run one or none of the below subcommands. Using inspect without "
"a subcommand will fetch general information about the state "
"of the application instance.\n\n"
"Or, you can optionally follow inspect with a subcommand. "
"If you have created a custom "
"Inspector instance, then you can run custom commands. See "
"https://sanic.dev/en/guide/deployment/inspector.html "
"for more details."
),
title=" Subcommands",
parser_class=InspectorSubParser,
)
reloader = subparsers.add_parser(
"reload",
help="Trigger a reload of the server workers",
formatter_class=SanicHelpFormatter,
)
reloader.add_argument(
"--zero-downtime",
action="store_true",
help=(
"Whether to wait for the new process to be online before "
"terminating the old"
),
)
subparsers.add_parser(
"shutdown",
help="Shutdown the application and all processes",
formatter_class=SanicHelpFormatter,
)
scale = subparsers.add_parser(
"scale",
help="Scale the number of workers",
formatter_class=SanicHelpFormatter,
)
scale.add_argument(
"replicas",
type=int,
help="Number of workers requested",
)
custom = subparsers.add_parser(
"<custom>",
help="Run a custom command",
description=(
"keyword arguments:\n When running a custom command, you can "
"add keyword arguments by appending them to your command\n\n"
"\tsanic inspect foo --one=1 --two=2"
),
formatter_class=SanicHelpFormatter,
)
custom.add_argument(
"positional",
nargs="*",
help="Add one or more non-keyword args to your custom command",
)

View File

@@ -1,119 +0,0 @@
from __future__ import annotations
import sys
from http.client import RemoteDisconnected
from textwrap import indent
from typing import Any, Dict, Optional
from urllib.error import URLError
from urllib.request import Request as URequest
from urllib.request import urlopen
from sanic.application.logo import get_logo
from sanic.application.motd import MOTDTTY
from sanic.log import Colors
try: # no cov
from ujson import dumps, loads
except ModuleNotFoundError: # no cov
from json import dumps, loads # type: ignore
class InspectorClient:
def __init__(
self,
host: str,
port: int,
secure: bool,
raw: bool,
api_key: Optional[str],
) -> None:
self.scheme = "https" if secure else "http"
self.host = host
self.port = port
self.raw = raw
self.api_key = api_key
for scheme in ("http", "https"):
full = f"{scheme}://"
if self.host.startswith(full):
self.scheme = scheme
self.host = self.host[len(full) :] # noqa E203
def do(self, action: str, **kwargs: Any) -> None:
if action == "info":
self.info()
return
result = self.request(action, **kwargs).get("result")
if result:
out = (
dumps(result)
if isinstance(result, (list, dict))
else str(result)
)
sys.stdout.write(out + "\n")
def info(self) -> None:
out = sys.stdout.write
response = self.request("", "GET")
if self.raw or not response:
return
data = response["result"]
display = data.pop("info")
extra = display.pop("extra", {})
display["packages"] = ", ".join(display["packages"])
MOTDTTY(get_logo(), self.base_url, display, extra).display(
version=False,
action="Inspecting",
out=out,
)
for name, info in data["workers"].items():
info = "\n".join(
f"\t{key}: {Colors.BLUE}{value}{Colors.END}"
for key, value in info.items()
)
out(
"\n"
+ indent(
"\n".join(
[
f"{Colors.BOLD}{Colors.SANIC}{name}{Colors.END}",
info,
]
),
" ",
)
+ "\n"
)
def request(self, action: str, method: str = "POST", **kwargs: Any) -> Any:
url = f"{self.base_url}/{action}"
params: Dict[str, Any] = {"method": method, "headers": {}}
if kwargs:
params["data"] = dumps(kwargs).encode()
params["headers"]["content-type"] = "application/json"
if self.api_key:
params["headers"]["authorization"] = f"Bearer {self.api_key}"
request = URequest(url, **params)
try:
with urlopen(request) as response: # nosec B310
raw = response.read()
loaded = loads(raw)
if self.raw:
sys.stdout.write(dumps(loaded.get("result")) + "\n")
return {}
return loaded
except (URLError, RemoteDisconnected) as e:
sys.stderr.write(
f"{Colors.RED}Could not connect to inspector at: "
f"{Colors.YELLOW}{self.base_url}{Colors.END}\n"
"Either the application is not running, or it did not start "
f"an inspector instance.\n{e}\n"
)
sys.exit(1)
@property
def base_url(self):
return f"{self.scheme}://{self.host}:{self.port}"

View File

@@ -1,69 +1,13 @@
import asyncio import asyncio
import os import os
import signal import signal
import sys
from contextlib import contextmanager from sys import argv
from enum import Enum
from typing import Awaitable, Union
from multidict import CIMultiDict # type: ignore from multidict import CIMultiDict # type: ignore
from sanic.helpers import Default
if sys.version_info < (3, 8): # no cov
StartMethod = Union[Default, str]
else: # no cov
from typing import Literal
StartMethod = Union[
Default, Literal["fork"], Literal["forkserver"], Literal["spawn"]
]
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
# Python 3.11 changed the way Enum formatting works for mixed-in types.
if sys.version_info < (3, 11, 0):
class StrEnum(str, Enum):
pass
else:
from enum import StrEnum # type: ignore # noqa
class UpperStrEnum(StrEnum):
def _generate_next_value_(name, start, count, last_values):
return name.upper()
def __eq__(self, value: object) -> bool:
value = str(value).upper()
return super().__eq__(value)
def __hash__(self) -> int:
return hash(self.value)
def __str__(self) -> str:
return self.value
@contextmanager
def use_context(method: StartMethod):
from sanic import Sanic
orig = Sanic.start_method
Sanic.start_method = method
yield
Sanic.start_method = orig
def enable_windows_color_support(): def enable_windows_color_support():
@@ -95,12 +39,12 @@ class Header(CIMultiDict):
return self.getall(key, default=[]) return self.getall(key, default=[])
use_trio = sys.argv[0].endswith("hypercorn") and "trio" in sys.argv use_trio = argv[0].endswith("hypercorn") and "trio" in argv
if use_trio: # pragma: no cover if use_trio: # pragma: no cover
import trio # type: ignore import trio # type: ignore
def stat_async(path) -> Awaitable[os.stat_result]: def stat_async(path):
return trio.Path(path).stat() return trio.Path(path).stat()
open_async = trio.open_file open_async = trio.open_file
@@ -120,7 +64,7 @@ def ctrlc_workaround_for_windows(app):
"""Asyncio wakeups to allow receiving SIGINT in Python""" """Asyncio wakeups to allow receiving SIGINT in Python"""
while not die: while not die:
# If someone else stopped the app, just exit # If someone else stopped the app, just exit
if app.state.is_stopping: if app.is_stopping:
return return
# Windows Python blocks signal handlers while the event loop is # Windows Python blocks signal handlers while the event loop is
# waiting for I/O. Frequent wakeups keep interrupts flowing. # waiting for I/O. Frequent wakeups keep interrupts flowing.
@@ -137,7 +81,3 @@ def ctrlc_workaround_for_windows(app):
die = False die = False
signal.signal(signal.SIGINT, ctrlc_handler) signal.signal(signal.SIGINT, ctrlc_handler)
app.add_task(stay_active) app.add_task(stay_active)
def is_atty() -> bool:
return bool(sys.stdout and sys.stdout.isatty())

View File

@@ -1,116 +1,67 @@
from __future__ import annotations from __future__ import annotations
import sys from inspect import isclass
from abc import ABCMeta
from inspect import getmembers, isclass, isdatadescriptor
from os import environ from os import environ
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, Optional, Sequence, Union from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from warnings import filterwarnings from warnings import warn
from sanic.constants import LocalCertCreator from sanic.errorpages import check_error_format
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
from sanic.helpers import Default, _default
from sanic.http import Http from sanic.http import Http
from sanic.log import error_logger
from sanic.utils import load_module_from_file_location, str_to_bool from sanic.utils import load_module_from_file_location, str_to_bool
if sys.version_info >= (3, 8): if TYPE_CHECKING: # no cov
from typing import Literal from sanic import Sanic
FilterWarningType = Union[
Literal["default"],
Literal["error"],
Literal["ignore"],
Literal["always"],
Literal["module"],
Literal["once"],
]
else:
FilterWarningType = str
SANIC_PREFIX = "SANIC_" SANIC_PREFIX = "SANIC_"
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"_FALLBACK_ERROR_FORMAT": _default, "ACCESS_LOG": True,
"ACCESS_LOG": False,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False, "AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False, "EVENT_AUTOREGISTER": False,
"DEPRECATION_FILTER": "once", "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
"INSPECTOR": False,
"INSPECTOR_HOST": "localhost",
"INSPECTOR_PORT": 6457,
"INSPECTOR_TLS_KEY": _default,
"INSPECTOR_TLS_CERT": _default,
"INSPECTOR_API_KEY": "",
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds "KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"KEEP_ALIVE": True, "KEEP_ALIVE": True,
"LOCAL_CERT_CREATOR": LocalCertCreator.AUTO,
"LOCAL_TLS_KEY": _default,
"LOCAL_TLS_CERT": _default,
"LOCALHOST": "localhost",
"MOTD": True, "MOTD": True,
"MOTD_DISPLAY": {}, "MOTD_DISPLAY": {},
"NOISY_EXCEPTIONS": False, "NOISY_EXCEPTIONS": False,
"PROXIES_COUNT": None, "PROXIES_COUNT": None,
"REAL_IP_HEADER": None, "REAL_IP_HEADER": None,
"REGISTER": True,
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB "REQUEST_BUFFER_SIZE": 65536, # 64 KiB
"REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384 "REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384
"REQUEST_ID_HEADER": "X-Request-ID", "REQUEST_ID_HEADER": "X-Request-ID",
"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
"TLS_CERT_PASSWORD": "", "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"TOUCHUP": _default,
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2**20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20, "WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20, "WEBSOCKET_PING_TIMEOUT": 20,
} }
class DescriptorMeta(ABCMeta): class Config(dict):
def __init__(cls, *_):
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
@staticmethod
def _is_setter(member: object):
return isdatadescriptor(member) and hasattr(member, "setter")
class Config(dict, metaclass=DescriptorMeta):
ACCESS_LOG: bool ACCESS_LOG: bool
AUTO_EXTEND: bool
AUTO_RELOAD: bool AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool EVENT_AUTOREGISTER: bool
DEPRECATION_FILTER: FilterWarningType 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
INSPECTOR: bool
INSPECTOR_HOST: str
INSPECTOR_PORT: int
INSPECTOR_TLS_KEY: Union[Path, str, Default]
INSPECTOR_TLS_CERT: Union[Path, str, Default]
INSPECTOR_API_KEY: str
KEEP_ALIVE_TIMEOUT: int KEEP_ALIVE_TIMEOUT: int
KEEP_ALIVE: bool KEEP_ALIVE: bool
LOCAL_CERT_CREATOR: Union[str, LocalCertCreator] NOISY_EXCEPTIONS: bool
LOCAL_TLS_KEY: Union[Path, str, Default]
LOCAL_TLS_CERT: Union[Path, str, Default]
LOCALHOST: str
MOTD: bool MOTD: bool
MOTD_DISPLAY: Dict[str, str] MOTD_DISPLAY: Dict[str, str]
NOISY_EXCEPTIONS: bool
PROXIES_COUNT: Optional[int] PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: Optional[str] REAL_IP_HEADER: Optional[str]
REGISTER: bool
REQUEST_BUFFER_SIZE: int REQUEST_BUFFER_SIZE: int
REQUEST_MAX_HEADER_SIZE: int REQUEST_MAX_HEADER_SIZE: int
REQUEST_ID_HEADER: str REQUEST_ID_HEADER: str
@@ -118,32 +69,24 @@ class Config(dict, metaclass=DescriptorMeta):
REQUEST_TIMEOUT: int REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int RESPONSE_TIMEOUT: int
SERVER_NAME: str SERVER_NAME: str
TLS_CERT_PASSWORD: str
TOUCHUP: Union[Default, bool]
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
def __init__( def __init__(
self, self,
defaults: Optional[ defaults: Dict[str, Union[str, bool, int, float, None]] = None,
Dict[str, Union[str, bool, int, float, None]] load_env: Optional[Union[bool, str]] = True,
] = None,
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._configure_warnings()
self._converters = [str, str_to_bool, float, int] self._app = app
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
@@ -151,6 +94,15 @@ class Config(dict, metaclass=DescriptorMeta):
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)
@@ -158,34 +110,22 @@ class Config(dict, metaclass=DescriptorMeta):
self._check_error_format() self._check_error_format()
self._init = True self._init = True
def __getattr__(self, attr: Any): def __getattr__(self, attr):
try: try:
return self[attr] return self[attr]
except KeyError as ke: except KeyError as ke:
raise AttributeError(f"Config has no '{ke.args[0]}'") raise AttributeError(f"Config has no '{ke.args[0]}'")
def __setattr__(self, attr: str, value: Any) -> None: def __setattr__(self, attr, value) -> None:
self.update({attr: value}) self.update({attr: value})
def __setitem__(self, attr: str, value: Any) -> None: def __setitem__(self, attr, value) -> None:
self.update({attr: value}) self.update({attr: value})
def update(self, *other: Any, **kwargs: Any) -> 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:
@@ -196,34 +136,31 @@ class Config(dict, metaclass=DescriptorMeta):
"REQUEST_MAX_SIZE", "REQUEST_MAX_SIZE",
): ):
self._configure_header_size() self._configure_header_size()
elif attr == "FALLBACK_ERROR_FORMAT":
if attr == "LOCAL_CERT_CREATOR" and not isinstance( self._check_error_format()
self.LOCAL_CERT_CREATOR, LocalCertCreator if self.app and value != self.app.error_handler.fallback:
): if self.app.error_handler.fallback != "auto":
self.LOCAL_CERT_CREATOR = LocalCertCreator[ warn(
self.LOCAL_CERT_CREATOR.upper() "Overriding non-default ErrorHandler fallback "
] "value. Changing from "
elif attr == "DEPRECATION_FILTER": f"{self.app.error_handler.fallback} to {value}."
self._configure_warnings() )
self.app.error_handler.fallback = value
elif attr == "LOGO":
self._LOGO = value
warn(
"Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.",
DeprecationWarning,
)
@property @property
def FALLBACK_ERROR_FORMAT(self) -> str: def app(self):
if isinstance(self._FALLBACK_ERROR_FORMAT, Default): return self._app
return DEFAULT_FORMAT
return self._FALLBACK_ERROR_FORMAT
@FALLBACK_ERROR_FORMAT.setter @property
def FALLBACK_ERROR_FORMAT(self, value): def LOGO(self):
self._check_error_format(value) return self._LOGO
if (
not isinstance(self._FALLBACK_ERROR_FORMAT, 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(
@@ -232,23 +169,14 @@ class Config(dict, metaclass=DescriptorMeta):
self.REQUEST_MAX_SIZE, self.REQUEST_MAX_SIZE,
) )
def _configure_warnings(self): def _check_error_format(self):
filterwarnings( check_error_format(self.FALLBACK_ERROR_FORMAT)
self.DEPRECATION_FILTER,
category=DeprecationWarning,
module=r"sanic.*",
)
def _check_error_format(self, format: Optional[str] = None):
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
def load_environment_vars(self, prefix=SANIC_PREFIX): 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 them to the
configuration if present. This is called automatically when Sanic configuration if present. This is called automatically when Sanic
starts up to load environment variables into config. Environment starts up to load environment variables into config.
variables should start with the defined prefix and should only
contain uppercase letters.
It will automatically hydrate the following types: It will automatically hydrate the following types:
@@ -256,31 +184,15 @@ class Config(dict, metaclass=DescriptorMeta):
- ``float`` - ``float``
- ``bool`` - ``bool``
Anything else will be imported as a ``str``. If you would like to add Anything else will be imported as a ``str``.
additional types to this list, you can use
:meth:`sanic.config.Config.register_type`. Just make sure that they
are registered before you instantiate your application.
.. code-block:: python
class Foo:
def __init__(self, name) -> None:
self.name = name
config = Config(converters=[Foo])
app = Sanic(__name__, config=config)
`See user guide re: config
<https://sanicframework.org/guide/deployment/configuration.html>`__
""" """
for key, value in environ.items(): for key, value in environ.items():
if not key.startswith(prefix) or not key.isupper(): if not key.startswith(prefix):
continue continue
_, config_key = key.split(prefix, 1) _, config_key = key.split(prefix, 1)
for converter in reversed(self._converters): for converter in (int, float, str_to_bool, str):
try: try:
self[config_key] = converter(value) self[config_key] = converter(value)
break break
@@ -355,17 +267,3 @@ class Config(dict, metaclass=DescriptorMeta):
self.update(config) self.update(config)
load = update_config load = update_config
def register_type(self, converter: Callable[[str], Any]) -> None:
"""
Allows for adding custom function to cast from a string value to any
other type. The function should raise ValueError if it is not the
correct type.
"""
if converter in self._converters:
error_logger.warning(
f"Configuration value converter '{converter.__name__}' has "
"already been registered"
)
return
self._converters.append(converter)

View File

@@ -1,9 +1,20 @@
from enum import auto from enum import Enum, auto
from sanic.compat import UpperStrEnum
class HTTPMethod(UpperStrEnum): class HTTPMethod(str, Enum):
def _generate_next_value_(name, start, count, last_values):
return name.upper()
def __eq__(self, value: object) -> bool:
value = str(value).upper()
return super().__eq__(value)
def __hash__(self) -> int:
return hash(self.value)
def __str__(self) -> str:
return self.value
GET = auto() GET = auto()
POST = auto() POST = auto()
PUT = auto() PUT = auto()
@@ -13,22 +24,5 @@ class HTTPMethod(UpperStrEnum):
DELETE = auto() DELETE = auto()
class LocalCertCreator(UpperStrEnum):
AUTO = auto()
TRUSTME = auto()
MKCERT = auto()
HTTP_METHODS = tuple(HTTPMethod.__members__.values()) HTTP_METHODS = tuple(HTTPMethod.__members__.values())
SAFE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD, HTTPMethod.OPTIONS)
IDEMPOTENT_HTTP_METHODS = (
HTTPMethod.GET,
HTTPMethod.HEAD,
HTTPMethod.PUT,
HTTPMethod.DELETE,
HTTPMethod.OPTIONS,
)
CACHEABLE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD)
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
DEFAULT_LOCAL_TLS_KEY = "key.pem"
DEFAULT_LOCAL_TLS_CERT = "cert.pem"

View File

@@ -12,7 +12,6 @@ Setting ``app.config.FALLBACK_ERROR_FORMAT = "auto"`` will enable a switch that
will attempt to provide an appropriate response format based upon the will attempt to provide an appropriate response format based upon the
request type. request type.
""" """
from __future__ import annotations
import sys import sys
import typing as t import typing as t
@@ -20,10 +19,10 @@ import typing as t
from functools import partial from functools import partial
from traceback import extract_tb from traceback import extract_tb
from sanic.exceptions import BadRequest, SanicException from sanic.exceptions import InvalidUsage, SanicException
from sanic.helpers import STATUS_CODES from sanic.helpers import STATUS_CODES
from sanic.pages.error import ErrorPage from sanic.request import Request
from sanic.response import html, json, text from sanic.response import HTTPResponse, html, json, text
dumps: t.Callable[..., str] dumps: t.Callable[..., str]
@@ -34,10 +33,7 @@ try:
except ImportError: # noqa except ImportError: # noqa
from json import dumps from json import dumps
if t.TYPE_CHECKING:
from sanic import HTTPResponse, Request
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."
@@ -160,21 +156,36 @@ class HTMLRenderer(BaseRenderer):
"{body}" "{body}"
) )
def _page(self, full: bool) -> HTTPResponse:
page = ErrorPage(
title=super().title,
text=super().text,
request=self.request,
exc=self.exception,
full=full,
)
return html(page.render(), status=self.status, headers=self.headers)
def full(self) -> HTTPResponse: def full(self) -> HTTPResponse:
return self._page(full=True) return html(
self.OUTPUT_HTML.format(
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(full=True),
),
status=self.status,
)
def minimal(self) -> HTTPResponse: def minimal(self) -> HTTPResponse:
return self._page(full=False) return html(
self.OUTPUT_HTML.format(
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(full=False),
),
status=self.status,
headers=self.headers,
)
@property
def text(self):
return escape(super().text)
@property
def title(self):
return escape(f"⚠️ {super().title}")
def _generate_body(self, *, full): def _generate_body(self, *, full):
lines = [] lines = []
@@ -392,13 +403,16 @@ CONTENT_TYPE_BY_RENDERERS = {
v: k for k, v in RENDERERS_BY_CONTENT_TYPE.items() v: k for k, v in RENDERERS_BY_CONTENT_TYPE.items()
} }
# Handler source code is checked for which response types it returns with the
# route error_format="auto" (default) to determine which format to use.
RESPONSE_MAPPING = { RESPONSE_MAPPING = {
"empty": "html",
"json": "json", "json": "json",
"text": "text", "text": "text",
"raw": "text",
"html": "html", "html": "html",
"JSONResponse": "json", "file": "html",
"file_stream": "text",
"stream": "text",
"redirect": "html",
"text/plain": "text", "text/plain": "text",
"text/html": "html", "text/html": "html",
"application/json": "json", "application/json": "json",
@@ -433,8 +447,8 @@ def exception_response(
# from the route # from the route
if request.route: if request.route:
try: try:
if request.route.extra.error_format: if request.route.ctx.error_format:
render_format = request.route.extra.error_format render_format = request.route.ctx.error_format
except AttributeError: except AttributeError:
... ...
@@ -491,7 +505,7 @@ def exception_response(
# $ curl localhost:8000 -d '{"foo": "bar"}' # $ curl localhost:8000 -d '{"foo": "bar"}'
# And provide them with JSONRenderer # And provide them with JSONRenderer
renderer = JSONRenderer if request.json else base renderer = JSONRenderer if request.json else base
except BadRequest: except InvalidUsage:
renderer = base renderer = base
else: else:
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer) renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)

View File

@@ -1,17 +1,8 @@
from asyncio import CancelledError
from typing import Any, Dict, Optional, Union from typing import Any, Dict, Optional, Union
from sanic.helpers import STATUS_CODES from sanic.helpers import STATUS_CODES
class RequestCancelled(CancelledError):
quiet = True
class ServerKilled(Exception):
...
class SanicException(Exception): class SanicException(Exception):
message: str = "" message: str = ""
@@ -51,7 +42,7 @@ class NotFound(SanicException):
quiet = True quiet = True
class BadRequest(SanicException): class InvalidUsage(SanicException):
""" """
**Status**: 400 Bad Request **Status**: 400 Bad Request
""" """
@@ -60,14 +51,7 @@ class BadRequest(SanicException):
quiet = True quiet = True
InvalidUsage = BadRequest class MethodNotSupported(SanicException):
class BadURL(BadRequest):
...
class MethodNotAllowed(SanicException):
""" """
**Status**: 405 Method Not Allowed **Status**: 405 Method Not Allowed
""" """
@@ -80,9 +64,6 @@ class MethodNotAllowed(SanicException):
self.headers = {"Allow": ", ".join(allowed_methods)} self.headers = {"Allow": ", ".join(allowed_methods)}
MethodNotSupported = MethodNotAllowed
class ServerError(SanicException): class ServerError(SanicException):
""" """
**Status**: 500 Internal Server Error **Status**: 500 Internal Server Error
@@ -144,19 +125,19 @@ class PayloadTooLarge(SanicException):
quiet = True quiet = True
class HeaderNotFound(BadRequest): class HeaderNotFound(InvalidUsage):
""" """
**Status**: 400 Bad Request **Status**: 400 Bad Request
""" """
class InvalidHeader(BadRequest): class InvalidHeader(InvalidUsage):
""" """
**Status**: 400 Bad Request **Status**: 400 Bad Request
""" """
class RangeNotSatisfiable(SanicException): class ContentRangeError(SanicException):
""" """
**Status**: 416 Range Not Satisfiable **Status**: 416 Range Not Satisfiable
""" """
@@ -169,10 +150,7 @@ class RangeNotSatisfiable(SanicException):
self.headers = {"Content-Range": f"bytes */{content_range.total}"} self.headers = {"Content-Range": f"bytes */{content_range.total}"}
ContentRangeError = RangeNotSatisfiable class HeaderExpectationFailed(SanicException):
class ExpectationFailed(SanicException):
""" """
**Status**: 417 Expectation Failed **Status**: 417 Expectation Failed
""" """
@@ -181,9 +159,6 @@ class ExpectationFailed(SanicException):
quiet = True quiet = True
HeaderExpectationFailed = ExpectationFailed
class Forbidden(SanicException): class Forbidden(SanicException):
""" """
**Status**: 403 Forbidden **Status**: 403 Forbidden
@@ -193,7 +168,7 @@ class Forbidden(SanicException):
quiet = True quiet = True
class InvalidRangeType(RangeNotSatisfiable): class InvalidRangeType(ContentRangeError):
""" """
**Status**: 416 Range Not Satisfiable **Status**: 416 Range Not Satisfiable
""" """
@@ -269,3 +244,25 @@ class InvalidSignal(SanicException):
class WebsocketClosed(SanicException): class WebsocketClosed(SanicException):
quiet = True quiet = True
message = "Client has closed the websocket connection" message = "Client has closed the websocket connection"
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
"""
Raise an exception based on SanicException. Returns the HTTP response
message appropriate for the given status code, unless provided.
STATUS_CODES from sanic.helpers for the given status code.
:param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages in
"""
import warnings
warnings.warn(
"sanic.exceptions.abort has been marked as deprecated, and will be "
"removed in release 21.12.\n To migrate your code, simply replace "
"abort(status_code, msg) with raise SanicException(msg, status_code), "
"or even better, raise an appropriate SanicException subclass."
)
raise SanicException(message=message, status_code=status_code)

View File

@@ -1,9 +1,14 @@
from __future__ import annotations from inspect import signature
from typing import Dict, List, Optional, Tuple, Type from typing import Dict, List, Optional, Tuple, Type
from warnings import warn
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
from sanic.log import deprecation, error_logger from sanic.exceptions import (
ContentRangeError,
HeaderNotFound,
InvalidRangeType,
)
from sanic.log import 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
@@ -18,42 +23,54 @@ class ErrorHandler:
by the developers to perform a wide range of tasks from recording the error by the developers to perform a wide range of tasks from recording the error
stats to reporting them to an external service that can be used for stats to reporting them to an external service that can be used for
realtime alerting system. realtime alerting system.
""" """
# Beginning in v22.3, the base renderer will be TextRenderer
def __init__( def __init__(
self, self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer
base: Type[BaseRenderer] = TextRenderer,
): ):
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.base = base self.base = base
@classmethod
def finalize(cls, error_handler, fallback: Optional[str] = None):
if (
fallback
and fallback != "auto"
and error_handler.fallback == "auto"
):
error_handler.fallback = fallback
if not isinstance(error_handler, cls):
error_logger.warning(
f"Error handler is non-conforming: {type(error_handler)}"
)
sig = signature(error_handler.lookup)
if len(sig.parameters) == 1:
warn(
"You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: "
"(exception, route_name: Optional[str]). "
"Until you upgrade your ErrorHandler.lookup, Blueprint "
"specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not "
"work at all.",
DeprecationWarning,
)
error_handler._lookup = error_handler._legacy_lookup
def _full_lookup(self, exception, route_name: Optional[str] = None): def _full_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception, route_name) return self.lookup(exception, route_name)
def _add( def _legacy_lookup(self, exception, route_name: Optional[str] = None):
self, return self.lookup(exception)
key: Tuple[Type[BaseException], Optional[str]],
handler: RouteHandler,
) -> None:
if key in self.cached_handlers:
exc, name = key
if name is None:
name = "__ALL_ROUTES__"
error_logger.warning(
f"Duplicate exception handler definition on: route={name} "
f"and exception={exc}"
)
deprecation(
"A duplicate exception handler definition was discovered. "
"This may cause unintended consequences. A warning has been "
"issued now, but it will not be allowed starting in v23.3.",
23.3,
)
self.cached_handlers[key] = handler
def add(self, exception, handler, route_names: Optional[List[str]] = None): def add(self, exception, handler, route_names: Optional[List[str]] = None):
""" """
@@ -68,11 +85,14 @@ class ErrorHandler:
:return: None :return: None
""" """
# self.handlers is deprecated and will be removed in version 22.3
self.handlers.append((exception, handler))
if route_names: if route_names:
for route in route_names: for route in route_names:
self._add((exception, route), handler) self.cached_handlers[(exception, route)] = handler
else: else:
self._add((exception, None), handler) self.cached_handlers[(exception, None)] = handler
def lookup(self, exception, route_name: Optional[str] = None): def lookup(self, exception, route_name: Optional[str] = None):
""" """
@@ -139,7 +159,7 @@ class ErrorHandler:
except Exception: except Exception:
try: try:
url = repr(request.url) url = repr(request.url)
except AttributeError: # no cov except AttributeError:
url = "unknown" url = "unknown"
response_message = ( response_message = (
"Exception raised in exception handler " '"%s" for uri: %s' "Exception raised in exception handler " '"%s" for uri: %s'
@@ -168,13 +188,12 @@ class ErrorHandler:
:return: :return:
""" """
self.log(request, exception) self.log(request, exception)
fallback = request.app.config.FALLBACK_ERROR_FORMAT
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
@@ -184,9 +203,80 @@ class ErrorHandler:
if quiet is False or noisy is True: if quiet is False or noisy is True:
try: try:
url = repr(request.url) url = repr(request.url)
except AttributeError: # no cov except AttributeError:
url = "unknown" url = "unknown"
error_logger.exception( error_logger.exception(
"Exception occurred while handling uri: %s", url "Exception occurred while handling uri: %s", url
) )
class ContentRangeHandler:
"""
A mechanism to parse and process the incoming request headers to
extract the content range information.
:param request: Incoming api request
:param stats: Stats related to the content
:type request: :class:`sanic.request.Request`
:type stats: :class:`posix.stat_result`
:ivar start: Content Range start
:ivar end: Content Range end
:ivar size: Length of the content
:ivar total: Total size identified by the :class:`posix.stat_result`
instance
:ivar ContentRangeHandler.headers: Content range header ``dict``
"""
__slots__ = ("start", "end", "size", "total", "headers")
def __init__(self, request, stats):
self.total = stats.st_size
_range = request.headers.getone("range", None)
if _range is None:
raise HeaderNotFound("Range Header Not Found")
unit, _, value = tuple(map(str.strip, _range.partition("=")))
if unit != "bytes":
raise InvalidRangeType(
"%s is not a valid Range Type" % (unit,), self
)
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
try:
self.start = int(start_b) if start_b else None
except ValueError:
raise ContentRangeError(
"'%s' is invalid for Content Range" % (start_b,), self
)
try:
self.end = int(end_b) if end_b else None
except ValueError:
raise ContentRangeError(
"'%s' is invalid for Content Range" % (end_b,), self
)
if self.end is None:
if self.start is None:
raise ContentRangeError(
"Invalid for Content Range parameters", self
)
else:
# this case represents `Content-Range: bytes 5-`
self.end = self.total - 1
else:
if self.start is None:
# this case represents `Content-Range: bytes -5`
self.start = self.total - self.end
self.end = self.total - 1
if self.start >= self.end:
raise ContentRangeError(
"Invalid for Content Range parameters", self
)
self.size = self.end - self.start + 1
self.headers = {
"Content-Range": "bytes %s-%s/%s"
% (self.start, self.end, self.total)
}
def __bool__(self):
return self.size > 0

View File

@@ -1,10 +0,0 @@
from .content_range import ContentRangeHandler
from .directory import DirectoryHandler
from .error import ErrorHandler
__all__ = (
"ContentRangeHandler",
"DirectoryHandler",
"ErrorHandler",
)

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