Compare commits

..

19 Commits

Author SHA1 Message Date
Arthur Goldberg
33a2e5bb1f fix issue where request.args.pop removed parameters inconsistently (#2111) 2021-04-12 19:26:28 +03:00
Adam Hopkins
5930bb67a6 Merge branch '19.12LTS' of github.com:sanic-org/sanic into 19.12LTS 2021-02-16 08:50:47 +02:00
Adam Hopkins
4c360f43fd Merge pull request #2025 from sanic-org/fix-uvloop-1912
Fix uvloop 1912
2021-02-16 08:49:45 +02:00
Adam Hopkins
6387f5ddc9 Bump version 2021-02-16 08:43:03 +02:00
Adam Hopkins
23a0308d40 Merge branch '19.12LTS' into fix-uvloop-1912 2021-02-16 08:36:42 +02:00
Adam Hopkins
a7d563d566 Merge pull request #2027 from ashleysommer/19.12LTS
Fix tox requirement install dependency problems in 19.12LTS
2021-02-16 08:36:14 +02:00
Ashley Sommer
f2d91bd4d2 Fix tox requirement install dependency problems in 19.12LTS
Remove old chardet requirement, add in our hard multidict requirement
2021-02-16 09:58:15 +10:00
Adam Hopkins
8c628c69fb fix uvloop version 2021-02-15 14:23:30 +02:00
Adam Hopkins
9b24fbb2f3 Prepare for release 2020-11-05 09:34:02 +02:00
Adam Hopkins
468f4ac7f1 Merge pull request #1966 from ashleysommer/asgs_chunk_1912
Backport #1965 to 19.12LTS
2020-11-05 09:31:01 +02:00
Adam Hopkins
be1ca93a23 Resolve mypy issue 2020-11-05 09:08:56 +02:00
Adam Hopkins
662c7c7f62 Fix linting and bump to 19.12.4 2020-11-05 08:57:40 +02:00
Ashley Sommer
3e4bec7f2c Fix Chunked Transport-Encoding in ASGI streaming response
In ASGI-mode, don't do sanic-side response chunk encoding, leave that to the ASGI-response-transport
Don't set content-length when using chunked-encoding in ASGI mode, this is incompatible with ASGI Chunked Transport-Encoding.

(cherry picked from commit c0839afdde)
2020-11-05 15:35:50 +10:00
Adam Hopkins
df4970a73d Merge branch '19.12LTS' of github.com:huge-success/sanic into 19.12LTS 2020-10-25 14:32:42 +02:00
Adam Hopkins
c5070bd449 Backport stream header fix (#1959)
Resolve headers as body in ASGI mode

* Bump version to 19.12.3

* Update multidict==5.0.0
2020-10-25 14:32:18 +02:00
Adam Hopkins
eb3d0a3f87 squash 2020-10-25 10:45:22 +02:00
Adam Hopkins
c09129ec63 Resolve headers as body in ASGI mode 2020-10-25 10:40:08 +02:00
Adam Hopkins
2a44a27236 Backport to 1912 (#1900)
* Cherry pick PRs to backport to 19.12LTS

Includes commits from:
https://github.com/huge-success/sanic/pull/1762
https://github.com/huge-success/sanic/pull/1764
https://github.com/huge-success/sanic/pull/1789

* Fix type annotation issue; run black and isort

* Update Makefile

Co-authored-by: Ashley Sommer <ashleysommer@gmail.com>
2020-07-29 13:54:33 +03:00
Adam Hopkins
bb9ff7cec1 Set version
Set version
2020-01-02 23:34:02 +02:00
121 changed files with 1975 additions and 4796 deletions

View File

@@ -12,17 +12,6 @@ environment:
PYTHON_VERSION: "3.7.x"
PYTHON_ARCH: "64"
- TOXENV: py38-no-ext
PYTHON: "C:\\Python38-x64"
PYTHON_VERSION: "3.8.x"
PYTHON_ARCH: "64"
# - TOXENV: py39-no-ext
# PYTHON: "C:\\Python39-x64\\python"
# PYTHONPATH: "C:\\Python39-x64"
# PYTHON_VERSION: "3.9.x"
# PYTHON_ARCH: "64"
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
install:

12
.github/FUNDING.yml vendored
View File

@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: sanic-org # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Questions and Help
url: https://community.sanicframework.org/c/questions-and-help
about: Do you need help with Sanic? Ask your questions here.

13
.github/ISSUE_TEMPLATE/help-wanted.md vendored Normal file
View File

@@ -0,0 +1,13 @@
---
name: Help wanted
about: Do you need help? Try community.sanicframework.org
---
*DELETE ALL BEFORE POSTING*
*Post your HELP WANTED questions on [the community forum](https://community.sanicframework.org/)*.
Checkout the community forum before posting any question here.
We prefer if you put these kinds of questions here:
https://community.sanicframework.org/c/questions-and-help

View File

@@ -1,40 +0,0 @@
name: "CodeQL"
on:
push:
branches:
- main
- "*LTS"
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
schedule:
- cron: '25 16 * * 0'
jobs:
analyze:
if: github.event.pull_request.draft == false
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,37 +0,0 @@
name: Coverage check
on:
push:
branches:
- main
- "*LTS"
tags:
- "!*" # Do not execute on tags
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
test:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.9]
os: [ubuntu-latest]
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies 🔨
run: |
python -m pip install --upgrade pip
pip install tox
- uses: paambaati/codeclimate-action@v2.5.3
if: always()
env:
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }}
with:
coverageCommand: tox -e coverage

View File

@@ -1,39 +0,0 @@
name: On Demand Task
on:
workflow_dispatch:
inputs:
python-version:
description: 'Version of Python to use for running Test'
required: false
default: "3.8"
tox-env:
description: 'Test Environment to Run'
required: true
default: ''
os:
description: 'Operating System to Run Test on'
required: false
default: ubuntu-latest
jobs:
onDemand:
name: tox-${{ matrix.config.tox-env }}-on-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["${{ github.event.inputs.os}}"]
config:
- { tox-env: "${{ github.event.inputs.tox-env }}", py-version: "${{ github.event.inputs.python-version }}"}
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Run tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.py-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
experimental-ignore-error: "yes"

View File

@@ -1,36 +0,0 @@
name: Security Analysis
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
bandit:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
- { python-version: 3.7, tox-env: security}
- { python-version: 3.8, tox-env: security}
- { python-version: 3.9, tox-env: security}
- { python-version: "3.10", tox-env: security}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
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 }}"

View File

@@ -1,32 +0,0 @@
name: Document Linter
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
docsLinter:
if: github.event.pull_request.draft == false
name: Lint Documentation
runs-on: ubuntu-latest
strategy:
matrix:
config:
- {python-version: "3.8", tox-env: "docs"}
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Run Document Linter
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 }}"

View File

@@ -1,33 +0,0 @@
name: Linter Checks
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
linter:
if: github.event.pull_request.draft == false
name: lint
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
- { python-version: 3.8, tox-env: lint}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
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 }}"

View File

@@ -1,41 +0,0 @@
name: Python PyPy Tests
on:
workflow_dispatch:
inputs:
tox-env:
description: "Tox Env to run on the PyPy Infra"
required: false
default: "pypy37"
pypy-version:
description: "Version of PyPy to use"
required: false
default: "pypy-3.7"
jobs:
testPyPy:
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: "${{ github.event.inputs.pypy-version }}",
tox-env: "${{ github.event.inputs.tox-env }}",
}
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 }}"
experimental-ignore-error: "true"
command-timeout: "600000"

View File

@@ -1,35 +0,0 @@
name: Python 3.7 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy37:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.7, tox-env: py37 }
- { python-version: 3.7, tox-env: py37-no-ext }
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 }}"
test-failure-retry: "3"

View File

@@ -1,35 +0,0 @@
name: Python 3.8 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy38:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.8, tox-env: py38 }
- { python-version: 3.8, tox-env: py38-no-ext }
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 }}"
test-failure-retry: "3"

View File

@@ -1,47 +0,0 @@
name: Python 3.9 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy39:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: 3.9,
tox-env: py39,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: 3.9,
tox-env: py39-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

@@ -1,35 +0,0 @@
name: Typing Checks
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
typeChecking:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
# - { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.8, tox-env: type-checking}
- { python-version: 3.9, tox-env: type-checking}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
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 }}"

View File

@@ -1,37 +0,0 @@
name: Run Unit Tests on Windows
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testsOnWindows:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
config:
- { python-version: 3.7, tox-env: py37-no-ext }
- { python-version: 3.8, tox-env: py38-no-ext }
- { python-version: 3.9, tox-env: py39-no-ext }
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Run Unit Tests
uses: ahopkins/custom-actions@pip-extra-args
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 }}"
experimental-ignore-error: "true"
command-timeout: "600000"
pip-extra-args: "--user"

View File

@@ -1,48 +0,0 @@
name: Publish Docker Images
on:
workflow_run:
workflows:
- 'Publish Artifacts'
types:
- completed
jobs:
publishDockerImages:
name: Docker Image Build [${{ matrix.python-version }}]
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.7", "3.8", "3.9"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Build Latest Base images for ${{ matrix.python-version }}
uses: harshanarayana/custom-actions@main
with:
docker-image-base-name: sanicframework/sanic-build
ignore-python-setup: 'true'
dockerfile-base-dir: './docker'
action: 'image-publish'
docker-image-tag: "${{ matrix.python-version }}"
docker-file-suffix: "base"
docker-build-args: "PYTHON_VERSION=${{ matrix.python-version }}"
registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }}
registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
push-images: 'true'
- name: Publish Sanic Docker Image for ${{ matrix.python-version }}
uses: harshanarayana/custom-actions@main
with:
docker-image-base-name: sanicframework/sanic
ignore-python-setup: 'true'
dockerfile-base-dir: './docker'
action: 'image-publish'
docker-build-args: "BASE_IMAGE_TAG=${{ matrix.python-version }}"
docker-image-prefix: "${{ matrix.python-version }}"
registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }}
registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
push-images: 'true'

View File

@@ -1,28 +0,0 @@
name: Publish Artifacts
on:
release:
types: [created]
jobs:
publishPythonPackage:
name: Publishing Sanic Release Artifacts
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.8"]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Publish Python Package
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.python-version }}
package-infra-name: "twine"
pypi-user: __token__
pypi-access-token: ${{ secrets.PYPI_ACCESS_TOKEN }}
action: "package-publish"
pypi-verify-metadata: "true"

View File

@@ -21,83 +21,41 @@ matrix:
dist: xenial
sudo: true
name: "Python 3.7 without Extensions"
- env: TOX_ENV=py38
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 with Extensions"
- env: TOX_ENV=py38-no-ext
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 without Extensions"
- env: TOX_ENV=py39
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 with Extensions"
- env: TOX_ENV=py39-no-ext
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 without Extensions"
- env: TOX_ENV=type-checking
python: 3.6
name: "Python 3.6 Type checks"
- env: TOX_ENV=type-checking
python: 3.7
name: "Python 3.7 Type checks"
- env: TOX_ENV=type-checking
python: 3.8
name: "Python 3.8 Type checks"
- env: TOX_ENV=type-checking
python: 3.9
dist: bionic
name: "Python 3.9 Type checks"
- env: TOX_ENV=lint
python: 3.6
name: "Python 3.6 Linter checks"
- env: TOX_ENV=check
python: 3.6
name: "Python 3.6 Package checks"
- env: TOX_ENV=security
python: 3.6
dist: xenial
sudo: true
name: "Python 3.6 Bandit security scan"
- env: TOX_ENV=security
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Bandit security scan"
- env: TOX_ENV=security
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 Bandit security scan"
- env: TOX_ENV=security
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 Bandit security scan"
- env: TOX_ENV=docs
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Documentation tests"
- env: TOX_ENV=pyNightly
python: "nightly"
python: 'nightly'
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: "nightly"
name: "Python nightly without Extensions"
python: 'nightly'
name: "Python nightly Extensions"
allow_failures:
- env: TOX_ENV=pyNightly
python: "nightly"
python: 'nightly'
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: "nightly"
name: "Python nightly without Extensions"
python: 'nightly'
name: "Python nightly Extensions"
install:
- pip install -U tox
- pip install codecov

View File

@@ -1,430 +1,3 @@
Version 20.12.5
===============
Bugfixes
********
*
`#2366 <https://github.com/sanic-org/sanic/pull/2366>`_
websocket dependency for websockets 9.1 security fix
Version 20.12.0
===============
Features
********
*
`#1945 <https://github.com/huge-success/sanic/pull/1945>`_
Static route more verbose if file not found
*
`#1954 <https://github.com/huge-success/sanic/pull/1954>`_
Fix static routes registration on a blueprint
*
`#1961 <https://github.com/huge-success/sanic/pull/1961>`_
Add Python 3.9 support
*
`#1962 <https://github.com/huge-success/sanic/pull/1962>`_
Sanic CLI upgrade
*
`#1967 <https://github.com/huge-success/sanic/pull/1967>`_
Update aiofile version requirements
*
`#1969 <https://github.com/huge-success/sanic/pull/1969>`_
Update multidict version requirements
*
`#1970 <https://github.com/huge-success/sanic/pull/1970>`_
Add py.typed file
*
`#1972 <https://github.com/huge-success/sanic/pull/1972>`_
Speed optimization in request handler
*
`#1979 <https://github.com/huge-success/sanic/pull/1979>`_
Add app registry and Sanic class level app retrieval
Bugfixes
********
*
`#1965 <https://github.com/huge-success/sanic/pull/1965>`_
Fix Chunked Transport-Encoding in ASGI streaming response
Deprecations and Removals
*************************
*
`#1981 <https://github.com/huge-success/sanic/pull/1981>`_
Cleanup and remove deprecated code
Developer infrastructure
************************
*
`#1956 <https://github.com/huge-success/sanic/pull/1956>`_
Fix load module test
*
`#1973 <https://github.com/huge-success/sanic/pull/1973>`_
Transition Travis from .org to .com
*
`#1986 <https://github.com/huge-success/sanic/pull/1986>`_
Update tox requirements
Improved Documentation
**********************
*
`#1951 <https://github.com/huge-success/sanic/pull/1951>`_
Documentation improvements
*
`#1983 <https://github.com/huge-success/sanic/pull/1983>`_
Remove duplicate contents in testing.rst
*
`#1984 <https://github.com/huge-success/sanic/pull/1984>`_
Fix typo in routing.rst
Version 20.9.1
===============
Bugfixes
********
*
`#1954 <https://github.com/huge-success/sanic/pull/1954>`_
Fix static route registration on blueprints
*
`#1957 <https://github.com/huge-success/sanic/pull/1957>`_
Removes duplicate headers in ASGI streaming body
Version 19.12.3
===============
Bugfixes
********
*
`#1959 <https://github.com/huge-success/sanic/pull/1959>`_
Removes duplicate headers in ASGI streaming body
Version 20.9.0
===============
Features
********
*
`#1887 <https://github.com/huge-success/sanic/pull/1887>`_
Pass subprotocols in websockets (both sanic server and ASGI)
*
`#1894 <https://github.com/huge-success/sanic/pull/1894>`_
Automatically set ``test_mode`` flag on app instance
*
`#1903 <https://github.com/huge-success/sanic/pull/1903>`_
Add new unified method for updating app values
*
`#1906 <https://github.com/huge-success/sanic/pull/1906>`_,
`#1909 <https://github.com/huge-success/sanic/pull/1909>`_
Adds WEBSOCKET_PING_TIMEOUT and WEBSOCKET_PING_INTERVAL configuration values
*
`#1935 <https://github.com/huge-success/sanic/pull/1935>`_
httpx version dependency updated, it is slated for removal as a dependency in v20.12
*
`#1937 <https://github.com/huge-success/sanic/pull/1937>`_
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
Bugfixes
********
*
`#1897 <https://github.com/huge-success/sanic/pull/1897>`_
Resolves exception from unread bytes in stream
Deprecations and Removals
*************************
*
`#1903 <https://github.com/huge-success/sanic/pull/1903>`_
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
Developer infrastructure
************************
*
`#1890 <https://github.com/huge-success/sanic/pull/1890>`_,
`#1891 <https://github.com/huge-success/sanic/pull/1891>`_
Update isort calls to be compatible with new API
*
`#1893 <https://github.com/huge-success/sanic/pull/1893>`_
Remove version section from setup.cfg
*
`#1924 <https://github.com/huge-success/sanic/pull/1924>`_
Adding --strict-markers for pytest
Improved Documentation
**********************
*
`#1922 <https://github.com/huge-success/sanic/pull/1922>`_
Add explicit ASGI compliance to the README
Version 20.6.3
===============
Bugfixes
********
*
`#1884 <https://github.com/huge-success/sanic/pull/1884>`_
Revert change to multiprocessing mode
Version 20.6.2
===============
Features
********
*
`#1641 <https://github.com/huge-success/sanic/pull/1641>`_
Socket binding implemented properly for IPv6 and UNIX sockets
Version 20.6.1
===============
Features
********
*
`#1760 <https://github.com/huge-success/sanic/pull/1760>`_
Add version parameter to websocket routes
*
`#1866 <https://github.com/huge-success/sanic/pull/1866>`_
Add ``sanic`` as an entry point command
*
`#1880 <https://github.com/huge-success/sanic/pull/1880>`_
Add handler names for websockets for url_for usage
Bugfixes
********
*
`#1776 <https://github.com/huge-success/sanic/pull/1776>`_
Bug fix for host parameter issue with lists
*
`#1842 <https://github.com/huge-success/sanic/pull/1842>`_
Fix static _handler pickling error
*
`#1827 <https://github.com/huge-success/sanic/pull/1827>`_
Fix reloader on OSX py38 and Windows
*
`#1848 <https://github.com/huge-success/sanic/pull/1848>`_
Reverse named_response_middlware execution order, to match normal response middleware execution order
*
`#1853 <https://github.com/huge-success/sanic/pull/1853>`_
Fix pickle error when attempting to pickle an application which contains websocket routes
Deprecations and Removals
*************************
*
`#1739 <https://github.com/huge-success/sanic/pull/1739>`_
Deprecate body_bytes to merge into body
Developer infrastructure
************************
*
`#1852 <https://github.com/huge-success/sanic/pull/1852>`_
Fix naming of CI test env on Python nightlies
*
`#1857 <https://github.com/huge-success/sanic/pull/1857>`_
Adjust websockets version to setup.py
*
`#1869 <https://github.com/huge-success/sanic/pull/1869>`_
Wrap run()'s "protocol" type annotation in Optional[]
Improved Documentation
**********************
*
`#1846 <https://github.com/huge-success/sanic/pull/1846>`_
Update docs to clarify response middleware execution order
*
`#1865 <https://github.com/huge-success/sanic/pull/1865>`_
Fixing rst format issue that was hiding documentation
Version 20.6.0
===============
*Released, but unintentionally ommitting PR #1880, so was replaced by 20.6.1*
Version 20.3.0
===============
Features
********
*
`#1762 <https://github.com/huge-success/sanic/pull/1762>`_
Add ``srv.start_serving()`` and ``srv.serve_forever()`` to ``AsyncioServer``
*
`#1767 <https://github.com/huge-success/sanic/pull/1767>`_
Make Sanic usable on ``hypercorn -k trio myweb.app``
*
`#1768 <https://github.com/huge-success/sanic/pull/1768>`_
No tracebacks on normal errors and prettier error pages
*
`#1769 <https://github.com/huge-success/sanic/pull/1769>`_
Code cleanup in file responses
*
`#1793 <https://github.com/huge-success/sanic/pull/1793>`_ and
`#1819 <https://github.com/huge-success/sanic/pull/1819>`_
Upgrade ``str.format()`` to f-strings
*
`#1798 <https://github.com/huge-success/sanic/pull/1798>`_
Allow multiple workers on MacOS with Python 3.8
*
`#1820 <https://github.com/huge-success/sanic/pull/1820>`_
Do not set content-type and content-length headers in exceptions
Bugfixes
********
*
`#1748 <https://github.com/huge-success/sanic/pull/1748>`_
Remove loop argument in ``asyncio.Event`` in Python 3.8
*
`#1764 <https://github.com/huge-success/sanic/pull/1764>`_
Allow route decorators to stack up again
*
`#1789 <https://github.com/huge-success/sanic/pull/1789>`_
Fix tests using hosts yielding incorrect ``url_for``
*
`#1808 <https://github.com/huge-success/sanic/pull/1808>`_
Fix Ctrl+C and tests on Windows
Deprecations and Removals
*************************
*
`#1800 <https://github.com/huge-success/sanic/pull/1800>`_
Begin deprecation in way of first-class streaming, removal of ``body_init``, ``body_push``, and ``body_finish``
*
`#1801 <https://github.com/huge-success/sanic/pull/1801>`_
Complete deprecation from `#1666 <https://github.com/huge-success/sanic/pull/1666>`_ of dictionary context on ``request`` objects.
*
`#1807 <https://github.com/huge-success/sanic/pull/1807>`_
Remove server config args that can be read directly from app
*
`#1818 <https://github.com/huge-success/sanic/pull/1818>`_
Complete deprecation of ``app.remove_route`` and ``request.raw_args``
Dependencies
************
*
`#1794 <https://github.com/huge-success/sanic/pull/1794>`_
Bump ``httpx`` to 0.11.1
*
`#1806 <https://github.com/huge-success/sanic/pull/1806>`_
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
Developer infrastructure
************************
*
`#1833 <https://github.com/huge-success/sanic/pull/1833>`_
Resolve broken documentation builds
Improved Documentation
**********************
*
`#1755 <https://github.com/huge-success/sanic/pull/1755>`_
Usage of ``response.empty()``
*
`#1778 <https://github.com/huge-success/sanic/pull/1778>`_
Update README
*
`#1783 <https://github.com/huge-success/sanic/pull/1783>`_
Fix typo
*
`#1784 <https://github.com/huge-success/sanic/pull/1784>`_
Corrected changelog for docs move of MD to RST (`#1691 <https://github.com/huge-success/sanic/pull/1691>`_)
*
`#1803 <https://github.com/huge-success/sanic/pull/1803>`_
Update config docs to match DEFAULT_CONFIG
*
`#1814 <https://github.com/huge-success/sanic/pull/1814>`_
Update getting_started.rst
*
`#1821 <https://github.com/huge-success/sanic/pull/1821>`_
Update to deployment
*
`#1822 <https://github.com/huge-success/sanic/pull/1822>`_
Update docs with changes done in 20.3
*
`#1834 <https://github.com/huge-success/sanic/pull/1834>`_
Order of listeners
Version 19.12.0
===============
@@ -451,7 +24,7 @@ Bugfixes
Improved Documentation
**********************
- Move docs from MD to RST
- Move docs from RST to MD
Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in
the future to update documentation. (`#1691 <https://github.com/huge-success/sanic/issues/1691>`__)
@@ -489,16 +62,16 @@ Version 19.6.2
Features
********
*
*
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_
Remove ``aiohttp`` dependency and create new ``SanicTestClient`` based upon
Remove ``aiohttp`` dependencey and create new ``SanicTestClient`` based upon
`requests-async <https://github.com/encode/requests-async>`_
*
*
`#1475 <https://github.com/huge-success/sanic/pull/1475>`_
Added ASGI support (Beta)
*
*
`#1436 <https://github.com/huge-success/sanic/pull/1436>`_
Add Configure support from object string
@@ -506,19 +79,19 @@ Features
Bugfixes
********
*
*
`#1587 <https://github.com/huge-success/sanic/pull/1587>`_
Add missing handle for Expect header.
*
*
`#1560 <https://github.com/huge-success/sanic/pull/1560>`_
Allow to disable Transfer-Encoding: chunked.
*
*
`#1558 <https://github.com/huge-success/sanic/pull/1558>`_
Fix graceful shutdown.
*
*
`#1594 <https://github.com/huge-success/sanic/pull/1594>`_
Strict Slashes behavior fix
@@ -529,11 +102,11 @@ Deprecations and Removals
`#1544 <https://github.com/huge-success/sanic/pull/1544>`_
Drop dependency on distutil
*
*
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_
Drop support for Python 3.5
*
*
`#1568 <https://github.com/huge-success/sanic/pull/1568>`_
Deprecate route removal.
@@ -550,39 +123,39 @@ Version 19.3
Features
********
*
*
`#1497 <https://github.com/huge-success/sanic/pull/1497>`_
Add support for zero-length and RFC 5987 encoded filename for
multipart/form-data requests.
*
*
`#1484 <https://github.com/huge-success/sanic/pull/1484>`_
The type of ``expires`` attribute of ``sanic.cookies.Cookie`` is now
enforced to be of type ``datetime``.
*
*
`#1482 <https://github.com/huge-success/sanic/pull/1482>`_
Add support for the ``stream`` parameter of ``sanic.Sanic.add_route()``
available to ``sanic.Blueprint.add_route()``.
*
*
`#1481 <https://github.com/huge-success/sanic/pull/1481>`_
Accept negative values for route parameters with type ``int`` or ``number``.
*
*
`#1476 <https://github.com/huge-success/sanic/pull/1476>`_
Deprecated the use of ``sanic.request.Request.raw_args`` - it has a
fundamental flaw in which is drops repeated query string parameters.
Added ``sanic.request.Request.query_args`` as a replacement for the
original use-case.
*
*
`#1472 <https://github.com/huge-success/sanic/pull/1472>`_
Remove an unwanted ``None`` check in Request class ``repr`` implementation.
This changes the default ``repr`` of a Request from ``<Request>`` to
``<Request: None />``
*
*
`#1470 <https://github.com/huge-success/sanic/pull/1470>`_
Added 2 new parameters to ``sanic.app.Sanic.create_server``\ :
@@ -593,21 +166,21 @@ Features
This is a breaking change.
*
*
`#1499 <https://github.com/huge-success/sanic/pull/1499>`_
Added a set of test cases that test and benchmark route resolution.
*
*
`#1457 <https://github.com/huge-success/sanic/pull/1457>`_
The type of the ``"max-age"`` value in a ``sanic.cookies.Cookie`` is now
enforced to be an integer. Non-integer values are replaced with ``0``.
*
*
`#1445 <https://github.com/huge-success/sanic/pull/1445>`_
Added the ``endpoint`` attribute to an incoming ``request``\ , containing the
name of the handler function.
*
*
`#1423 <https://github.com/huge-success/sanic/pull/1423>`_
Improved request streaming. ``request.stream`` is now a bounded-size buffer
instead of an unbounded queue. Callers must now call
@@ -620,7 +193,7 @@ Bugfixes
********
*
*
`#1502 <https://github.com/huge-success/sanic/pull/1502>`_
Sanic was prefetching ``time.time()`` and updating it once per second to
avoid excessive ``time.time()`` calls. The implementation was observed to
@@ -628,25 +201,25 @@ Bugfixes
to negligible, so this has been removed. Fixes
`#1500 <https://github.com/huge-success/sanic/pull/1500>`_
*
*
`#1501 <https://github.com/huge-success/sanic/pull/1501>`_
Fix a bug in the auto-reloader when the process was launched as a module
i.e. ``python -m init0.mod1`` where the sanic server is started
in ``init0/mod1.py`` with ``debug`` enabled and imports another module in
``init0``.
*
*
`#1376 <https://github.com/huge-success/sanic/pull/1376>`_
Allow sanic test client to bind to a random port by specifying
``port=None`` when constructing a ``SanicTestClient``
*
*
`#1399 <https://github.com/huge-success/sanic/pull/1399>`_
Added the ability to specify middleware on a blueprint group, so that all
routes produced from the blueprints in the group have the middleware
applied.
*
*
`#1442 <https://github.com/huge-success/sanic/pull/1442>`_
Allow the the use the ``SANIC_ACCESS_LOG`` environment variable to
enable/disable the access log when not explicitly passed to ``app.run()``.
@@ -688,7 +261,7 @@ Version 18.12
18.12.0
*******
*
*
Changes:
@@ -706,7 +279,7 @@ Version 18.12
* Deprecate Handler.log
* Pinned httptools requirement to version 0.0.10+
*
*
Fixes:

View File

@@ -26,8 +26,8 @@ Sanic | Build fast. Run fast.
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/huge-success/sanic
.. |Build Status| image:: https://travis-ci.com/huge-success/sanic.svg?branch=master
:target: https://travis-ci.com/huge-success/sanic
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
:target: https://travis-ci.org/huge-success/sanic
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
:target: https://ci.appveyor.com/project/huge-success/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
@@ -58,8 +58,6 @@ Sanic | Build fast. Run fast.
Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-asgi>`_.
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
The project is maintained by the community, for the community. **Contributions are welcome!**
@@ -85,9 +83,6 @@ 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
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/huge-success/sanic/issues/1517>`_), but setting ``workers=1`` should launch the server successfully.
Hello World Example
-------------------
@@ -106,7 +101,7 @@ Hello World Example
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
Sanic can now be easily run using ``sanic hello.app``.
Sanic can now be easily run using ``python3 hello.py``.
.. code::

View File

@@ -4,27 +4,19 @@
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.9 | | :heavy_check_mark: |
| 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 | until 2020-12 | :white_check_mark: |
| 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: |
:white_check_mark: = security/bug fixes
:heavy_check_mark: = full support
| Version | LTS | Supported |
| ------- | ------------------ | ------------------ |
| 19.6.0 | | :white_check_mark: |
| 19.3.1 | | :heavy_check_mark: |
| 18.12.0 | :heavy_check_mark: | :heavy_check_mark: |
| 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: |
## Reporting a Vulnerability

View File

@@ -1 +0,0 @@
Remove [version] section.

View File

@@ -1,3 +0,0 @@
Adds WEBSOCKET_PING_TIMEOUT and WEBSOCKET_PING_INTERVAL configuration values
Allows setting the ping_interval and ping_timeout arguments when initializing `WebSocketCommonProtocol`.

View File

@@ -1 +0,0 @@
Adds py.typed file to expose type information to other packages.

View File

@@ -28,7 +28,6 @@ Guides
sanic/debug_mode
sanic/testing
sanic/deploying
sanic/nginx
sanic/extensions
sanic/examples
sanic/changelog

View File

@@ -28,15 +28,14 @@ using all these methods would look like the following.
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic("class_views_example")
app = Sanic('some_name')
class SimpleView(HTTPMethodView):
def get(self, request):
return text('I am get method')
# You can also use async syntax
async def post(self, request):
def post(self, request):
return text('I am post method')
def put(self, request):
@@ -50,6 +49,22 @@ using all these methods would look like the following.
app.add_route(SimpleView.as_view(), '/')
You can also use `async` syntax.
.. code-block:: python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
class SimpleAsyncView(HTTPMethodView):
async def get(self, request):
return text('I am async get method')
app.add_route(SimpleAsyncView.as_view(), '/')
URL parameters
--------------
@@ -139,7 +154,7 @@ lambda:
from sanic.views import CompositionView
from sanic.response import text
app = Sanic("composition_example")
app = Sanic(__name__)
def get_handler(request):
return text('I am a get method')

View File

@@ -12,9 +12,9 @@ Sanic holds the configuration in the `config` attribute of the application objec
app = Sanic('myapp')
app.config.DB_NAME = 'appdb'
app.config['DB_USER'] = 'appuser'
app.config.DB_USER = 'appuser'
Since the config object has a type that inherits from dictionary, you can use its ``update`` method in order to set several values at once:
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
.. code-block:: python
@@ -39,98 +39,17 @@ Any variables defined with the `SANIC_` prefix will be applied to the sanic conf
.. code-block:: python
app = Sanic(__name__, load_env='MYAPP_')
app = Sanic(load_env='MYAPP_')
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
.. code-block:: python
app = Sanic(__name__, load_env=False)
From file, dict, or any object (having __dict__ attribute).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can store app configurations in: (1) a Python file, (2) a dictionary, or (3) in some other type of custom object.
In order to load configuration from ove of those, you can use ``app.upload_config()``.
**1) From file**
Let's say you have ``my_config.py`` file that looks like this:
.. code-block:: python
# my_config.py
A = 1
B = 2
Loading config from this file is as easy as:
.. code-block:: python
app.update_config("/path/to/my_config.py")
You can also use environment variables in the path name here.
Let's say you have an environment variable like this:
.. code-block:: shell
$ export my_path="/path/to"
Then you can use it like this:
.. code-block:: python
app.update_config("${my_path}/my_config.py")
.. note::
Just remember that you have to provide environment variables in the format ${environment_variable} and that $environment_variable is not expanded (is treated as "plain" text).
**2) From dict**
You can also set your app config by providing a ``dict``:
.. code-block:: python
d = {"A": 1, "B": 2}
app.update_config(d)
**3) From _any_ object**
App config can be taken from an object. Internally, it uses ``__dict__`` to retrieve keys and values.
For example, pass the class:
.. code-block:: python
class C:
A = 1
B = 2
app.update_config(C)
or, it can be instantiated:
.. code-block:: python
c = C()
app.update_config(c)
- From an object (having __dict__ attribute)
app = Sanic(load_env=False)
From an Object
~~~~~~~~~~~~~~
.. note::
Deprecated, will be removed in version 21.3.
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
.. code-block:: python
@@ -152,10 +71,6 @@ You could use a class or any other object as well.
From a File
~~~~~~~~~~~
.. note::
Deprecated, will be removed in version 21.3.
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
.. code-block:: python
@@ -183,7 +98,7 @@ The config files are regular Python files which are executed in order to load th
Builtin Configuration Values
----------------------------
Out of the box there are just a few predefined values which can be overwritten when creating the application. Note that websocket configuration values will have no impact if running in ASGI mode.
Out of the box there are just a few predefined values which can be overwritten when creating the application.
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| Variable | Default | Description |
@@ -200,29 +115,15 @@ Out of the box there are just a few predefined values which can be overwritten w
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_MAX_SIZE | 2^20 | Maximum size for incoming messages (bytes) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_MAX_QUEUE | 32 | Maximum length of the queue that holds incoming messages |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_READ_LIMIT | 2^16 | High-water limit of the buffer for incoming bytes |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_WRITE_LIMIT | 2^16 | High-water limit of the buffer for outgoing bytes |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_PING_INTERVAL | 20 | A Ping frame is sent every ping_interval seconds. |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_PING_TIMEOUT | 20 | Connection is closed when Pong is not received after ping_timeout seconds |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| ACCESS_LOG | True | Disable or enable access log |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| FORWARDED_SECRET | None | Used to securely identify a specific proxy server (see below) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| PROXIES_COUNT | None | The number of proxy servers in front of the app (e.g. nginx; see below) |
| PROXIES_COUNT | -1 | The number of proxy servers in front of the app (e.g. nginx; see below) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| FORWARDED_FOR_HEADER | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| REAL_IP_HEADER | None | The name of "X-Real-IP" HTTP header that contains real client ip |
| REAL_IP_HEADER | "X-Real-IP" | The name of "X-Real-IP" HTTP header that contains real client ip |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
The different Timeout variables:
@@ -327,7 +228,9 @@ Proxy config if using ...
* a proxy that supports `forwarded`: set `FORWARDED_SECRET` to the value that the proxy inserts in the header
* Apache Traffic Server: `CONFIG proxy.config.http.insert_forwarded STRING for|proto|host|by=_secret`
* NGHTTPX: `nghttpx --add-forwarded=for,proto,host,by --forwarded-for=ip --forwarded-by=_secret`
* NGINX: :ref:`nginx`.
* NGINX: after `the official instructions <https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/>`_, add anywhere in your config:
.. proxy_set_header Forwarded "$proxy_add_forwarded;by=\"_$server_name\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\";secret=_secret";
* a custom header with client IP: set `REAL_IP_HEADER` to the name of that header
* `x-forwarded-for`: set `PROXIES_COUNT` to `1` for a single proxy, or a greater number to allow Sanic to select the correct IP

View File

@@ -21,7 +21,7 @@ and the Automatic Reloader will be activated.
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
app = Sanic()
@app.route('/')
async def hello_world(request):
@@ -43,7 +43,7 @@ the ``auto_reload`` argument will activate or deactivate the Automatic Reloader.
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
app = Sanic()
@app.route('/')
async def hello_world(request):

View File

@@ -1,12 +1,9 @@
Deploying
=========
Sanic has three serving options: the inbuilt webserver,
Deploying Sanic is very simple using one of three options: the inbuilt webserver,
an `ASGI webserver <https://asgi.readthedocs.io/en/latest/implementations.html>`_, or `gunicorn`.
Sanic's own webserver is the fastest option, and it can be securely run on
the Internet. Still, it is also very common to place Sanic behind a reverse
proxy, as shown in :ref:`nginx`.
It is also very common to place Sanic behind a reverse proxy, like `nginx`.
Running via Sanic webserver
---------------------------
@@ -16,7 +13,6 @@ keyword arguments:
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
- `port` *(default `8000`)*: Port to host the server on.
- `unix` *(default `None`)*: Unix socket name to host the server on (instead of TCP).
- `debug` *(default `False`)*: Enables debug output (slows server).
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
- `sock` *(default `None`)*: Socket for the server to accept connections from.
@@ -51,15 +47,7 @@ If you like using command line arguments, you can launch a Sanic webserver by
executing the module. For example, if you initialized Sanic as `app` in a file
named `server.py`, you could run the server like so:
::
sanic server.app --host=0.0.0.0 --port=1337 --workers=4
It can also be called directly as a module.
::
python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4
.. python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4
With this way of running sanic, it is not necessary to invoke `app.run` in your
Python file. If you do, make sure you wrap it so that it only executes when
@@ -97,11 +85,7 @@ before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and sh
run consecutively and not actually around the server process beginning and ending (since that
is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and
`before_server_stop`.
Sanic has experimental support for running on `Trio <https://trio.readthedocs.io/en/stable/>`_ with::
hypercorn -k trio myapp:app
3. ASGI mode is still in "beta" as of Sanic v19.6.
Running via Gunicorn
--------------------
@@ -126,6 +110,28 @@ See the `Gunicorn Docs <http://docs.gunicorn.org/en/latest/settings.html#max-req
Other deployment considerations
-------------------------------
Running behind a reverse proxy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sanic can be used with a reverse proxy (e.g. nginx). There's a simple example of nginx configuration:
::
server {
listen 80;
server_name example.org;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
If you want to get real client ip, you should configure `X-Real-IP` and `X-Forwarded-For` HTTP headers and set `app.config.PROXIES_COUNT` to `1`; see the configuration page for more information.
Disable debug logging for performance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -25,7 +25,7 @@ A simple sanic application with a single ``async`` method with ``text`` and ``js
Simple App with ``Sanic Views``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Showcasing the simple mechanism of using :class:`sanic.views.HTTPMethodView` as well as a way to extend the same
Showcasing the simple mechanism of using :class:`sanic.viewes.HTTPMethodView` as well as a way to extend the same
into providing a custom ``async`` behavior for ``view``.
.. literalinclude:: ../../examples/simple_async_view.py

View File

@@ -59,7 +59,7 @@ You can also add an exception handler as such:
async def server_error_handler(request, exception):
return text("Oops, server error", status=500)
app = Sanic("error_handler_example")
app = Sanic()
app.error_handler.add(Exception, server_error_handler)
In some cases, you might want to add some more error handling
@@ -77,7 +77,7 @@ can subclass Sanic's default error handler as such:
# You custom error handling logic...
return super().default(request, exception)
app = Sanic("custom_error_handler_example")
app = Sanic()
app.error_handler = CustomErrorHandler()
Useful exceptions

View File

@@ -8,19 +8,19 @@ syntax, so earlier versions of python won't work.
1. Install Sanic
----------------
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.
> 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.
.. code-block:: bash
pip3 install sanic
To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` ( with`X` = `UVLOOP`/`UJSON`)
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`)
to true will stop that features installation.
.. code-block:: bash
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install --no-binary :all: sanic
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic
You can also install Sanic from `conda-forge <https://anaconda.org/conda-forge/sanic>`_
@@ -37,7 +37,7 @@ You can also install Sanic from `conda-forge <https://anaconda.org/conda-forge/s
from sanic import Sanic
from sanic.response import json
app = Sanic("hello_example")
app = Sanic()
@app.route("/")
async def test(request):
@@ -60,26 +60,3 @@ Open the address `http://0.0.0.0:8000 <http://0.0.0.0:8000>`_ in your web browse
the message *Hello world!*.
You now have a working Sanic server!
5. Application registry
-----------------------
When you instantiate a Sanic instance, that can be retrieved at a later time from the Sanic app registry. This can be useful, for example, if you need to access your Sanic instance from a location where it is not otherwise accessible.
.. code-block:: python
# ./path/to/server.py
from sanic import Sanic
app = Sanic("my_awesome_server")
# ./path/to/somewhere_else.py
from sanic import Sanic
app = Sanic.get_app("my_awesome_server")
If you call ``Sanic.get_app("non-existing")`` on an app that does not exist, it will raise ``SanicException`` by default. You can, instead, force the method to return a new instance of ``Sanic`` with that name:
.. code-block:: python
app = Sanic.get_app("my_awesome_server", force_create=True)

View File

@@ -15,7 +15,7 @@ Sanic aspires to be simple
from sanic import Sanic
from sanic.response import json
app = Sanic("App Name")
app = Sanic()
@app.route("/")
async def test(request):

View File

@@ -17,7 +17,7 @@ A simple example using default settings would be like this:
from sanic.log import logger
from sanic.response import text
app = Sanic('logging_example')
app = Sanic('test')
@app.route('/')
async def test(request):
@@ -47,7 +47,7 @@ initialize ``Sanic`` app:
.. code:: python
app = Sanic('logging_example', log_config=LOGGING_CONFIG)
app = Sanic('test', log_config=LOGGING_CONFIG)
And to close logging, simply assign access_log=False:
@@ -100,4 +100,4 @@ Log Context Parameter Parameter Value Datatype
The default access log format is ``%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d``
.. _python3 logging API: https://docs.python.org/3/howto/logging.html
.. _python3 logging API: https://docs.python.org/3/howto/logging.html

View File

@@ -14,8 +14,8 @@ There are two types of middleware: request and response. Both are declared
using the `@app.middleware` decorator, with the decorator's parameter being a
string representing its type: `'request'` or `'response'`.
* Request middleware receives only the `request` as an argument and are executed in the order they were added.
* Response middleware receives both the `request` and `response` and are executed in *reverse* order.
* Request middleware receives only the `request` as argument.
* Response middleware receives both the `request` and `response`.
The simplest middleware doesn't modify the request or response at all:
@@ -64,12 +64,12 @@ this.
app.run(host="0.0.0.0", port=8000)
The three middlewares are executed in the following order:
The three middlewares are executed in order:
1. The first request middleware **add_key** adds a new key `foo` into request context.
2. Request is routed to handler **index**, which gets the key from context and returns a text response.
3. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks.
4. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server*
3. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server*
4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks.
Responding early
----------------
@@ -132,24 +132,13 @@ For example:
async def close_db(app, loop):
await app.db.close()
Note:
The listeners are deconstructed in the reverse order of being constructed.
For example:
If the first listener in before_server_start handler setups a database connection,
ones registered after it can rely on that connection being alive both when they are started
and stopped, because stopping is done in reverse order, and the database connection is
torn down last.
It's also possible to register a listener using the `register_listener` method.
This may be useful if you define your listeners in another module besides
the one you instantiate your app in.
.. code-block:: python
app = Sanic(__name__)
app = Sanic()
async def setup_db(app, loop):
app.db = await db_setup()

View File

@@ -1,222 +0,0 @@
.. _nginx:
Nginx Deployment
================
Introduction
~~~~~~~~~~~~
Although Sanic can be run directly on Internet, it may be useful to use a proxy
server such as Nginx in front of it. This is particularly useful for running
multiple virtual hosts on the same IP, serving NodeJS or other services beside
a single Sanic app, and it also allows for efficient serving of static files.
SSL and HTTP/2 are also easily implemented on such proxy.
We are setting the Sanic app to serve only locally at `127.0.0.1:8000`, while the
Nginx installation is responsible for providing the service to public Internet
on domain `example.com`. Static files will be served from `/var/www/`.
Proxied Sanic app
~~~~~~~~~~~~~~~~~
The app needs to be setup with a secret key used to identify a trusted proxy,
so that real client IP and other information can be identified. This protects
against anyone on the Internet sending fake headers to spoof their IP addresses
and other details. Choose any random string and configure it both on the app
and in Nginx config.
.. code-block:: python
from sanic import Sanic
from sanic.response import text
app = Sanic("proxied_example")
app.config.FORWARDED_SECRET = "YOUR SECRET"
@app.get("/")
def index(request):
# This should display external (public) addresses:
return text(
f"{request.remote_addr} connected to {request.url_for('index')}\n"
f"Forwarded: {request.forwarded}\n"
)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000, workers=8, access_log=False)
Since this is going to be a system service, save your code to
`/srv/sanicexample/sanicexample.py`.
For testing, run your app in a terminal.
Nginx configuration
~~~~~~~~~~~~~~~~~~~
Quite much configuration is required to allow fast transparent proxying, but
for the most part these don't need to be modified, so bear with me.
Upstream servers need to be configured in a separate `upstream` block to enable
HTTP keep-alive, which can drastically improve performance, so we use this
instead of directly providing an upstream address in `proxy_pass` directive. In
this example, the upstream section is named by `server_name`, i.e. the public
domain name, which then also gets passed to Sanic in the `Host` header. You may
change the naming as you see fit. Multiple servers may also be provided for
load balancing and failover.
Change the two occurrences of `example.com` to your true domain name, and
instead of `YOUR SECRET` use the secret you chose for your app.
::
upstream example.com {
keepalive 100;
server 127.0.0.1:8000;
#server unix:/tmp/sanic.sock;
}
server {
server_name example.com;
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
# Serve static files if found, otherwise proxy to Sanic
location / {
root /var/www;
try_files $uri @sanic;
}
location @sanic {
proxy_pass http://$server_name;
# Allow fast streaming HTTP/1.1 pipes (keep-alive, unbuffered)
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_buffering off;
# Proxy forwarding (password configured in app.config.FORWARDED_SECRET)
proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\"";
# Allow websockets
proxy_set_header connection "upgrade";
proxy_set_header upgrade $http_upgrade;
}
}
To avoid cookie visibility issues and inconsistent addresses on search engines,
it is a good idea to redirect all visitors to one true domain, always using
HTTPS:
::
# Redirect all HTTP to HTTPS with no-WWW
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name ~^(?:www\.)?(.*)$;
return 301 https://$1$request_uri;
}
# Redirect WWW to no-WWW
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ~^www\.(.*)$;
return 301 $scheme://$1$request_uri;
}
The above config sections may be placed in `/etc/nginx/sites-available/default`
or in other site configs (be sure to symlink them to `sites-enabled` if you
create new ones).
Make sure that your SSL certificates are configured in the main config, or
add the `ssl_certificate` and `ssl_certificate_key` directives to each
`server` section that listens on SSL.
Additionally, copy&paste all of this into `nginx/conf.d/forwarded.conf`:
::
# RFC 7239 Forwarded header for Nginx proxy_pass
# Add within your server or location block:
# proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\"";
# Configure your upstream web server to identify this proxy by that password
# because otherwise anyone on the Internet could spoof these headers and fake
# their real IP address and other information to your service.
# Provide the full proxy chain in $proxy_forwarded
map $proxy_add_forwarded $proxy_forwarded {
default "$proxy_add_forwarded;by=\"_$hostname\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\"";
}
# The following mappings are based on
# https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
map $remote_addr $proxy_forwarded_elem {
# IPv4 addresses can be sent as-is
~^[0-9.]+$ "for=$remote_addr";
# IPv6 addresses need to be bracketed and quoted
~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
# Unix domain socket names cannot be represented in RFC 7239 syntax
default "for=unknown";
}
map $http_forwarded $proxy_add_forwarded {
# If the incoming Forwarded header is syntactically valid, append to it
"~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";
# Otherwise, replace it
default "$proxy_forwarded_elem";
}
For installs that don't use `conf.d` and `sites-available`, all of the above
configs may also be placed inside the `http` section of the main `nginx.conf`.
Reload Nginx config after changes:
::
sudo nginx -s reload
Now you should be able to connect your app on `https://example.com/`. Any 404
errors and such will be handled by Sanic's error pages, and whenever a static
file is present at a given path, it will be served by Nginx.
SSL certificates
~~~~~~~~~~~~~~~~
If you haven't already configured valid certificates on your server, now is a
good time to do so. Install `certbot` and `python3-certbot-nginx`, then run
::
certbot --nginx -d example.com -d www.example.com
`<https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/>`_
Running as a service
~~~~~~~~~~~~~~~~~~~~
This part is for Linux distributions based on `systemd`. Create a unit file
`/etc/systemd/system/sanicexample.service`::
[Unit]
Description=Sanic Example
[Service]
User=nobody
WorkingDirectory=/srv/sanicexample
ExecStart=/usr/bin/env python3 sanicexample.py
Restart=always
[Install]
WantedBy=multi-user.target
Then reload service files, start your service and enable it on boot::
sudo systemctl daemon-reload
sudo systemctl start sanicexample
sudo systemctl enable sanicexample

View File

@@ -56,6 +56,7 @@ The difference between Request.args and Request.query_args for the queryset `?ke
"url": request.url,
"query_string": request.query_string,
"args": request.args,
"raw_args": request.raw_args,
"query_args": request.query_args,
})
@@ -71,9 +72,12 @@ The difference between Request.args and Request.query_args for the queryset `?ke
"url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3",
"query_string":"key1=value1&key2=value2&key1=value3",
"args":{"key1":["value1","value3"],"key2":["value2"]},
"raw_args":{"key1":"value1","key2":"value2"},
"query_args":[["key1","value1"],["key2","value2"],["key1","value3"]]
}
- `raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions.
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
.. code-block:: python
@@ -202,7 +206,7 @@ The output will be:
Accessing values using `get` and `getlist`
------------------------------------------
The `request.args` returns a subclass of `dict` called `RequestParameters`.
The `request.args` returns a subclass of `dict` called `RequestParameters`.
The key difference when using this object is the distinction between the `get` and `getlist` methods.
- `get(key, default=None)` operates as normal, except that when the value of
@@ -224,14 +228,14 @@ The key difference when using this object is the distinction between the `get` a
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
app = Sanic(name="example")
@app.route("/")
def get_handler(request):
return json({
"p1": request.args.getlist("p1")
})
Accessing the handler name with the request.endpoint attribute
--------------------------------------------------------------
@@ -243,7 +247,7 @@ route will return "hello".
from sanic.response import text
from sanic import Sanic
app = Sanic(__name__)
app = Sanic()
@app.get("/")
def hello(request):

View File

@@ -107,19 +107,6 @@ Response without encoding the body
def handle_request(request):
return response.raw(b'raw data')
Empty
--------------
For responding with an empty message as defined by `RFC 2616 <https://tools.ietf.org/search/rfc2616#section-7.2.1>`_
.. code-block:: python
from sanic import response
@app.route('/empty')
async def handle_request(request):
return response.empty()
Modify headers or status
------------------------

View File

@@ -133,7 +133,7 @@ which allows the handler function to work with any of the HTTP methods in the li
async def get_handler(request):
return text('GET request - {}'.format(request.args))
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is also a route with no host, it will be the default.
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
.. code-block:: python
@@ -406,7 +406,7 @@ Build URL for static files
==========================
Sanic supports using `url_for` method to build static file urls. In case if the static url
is pointing to a directory, `filename` parameter to the `url_for` can be ignored.
is pointing to a directory, `filename` parameter to the `url_for` can be ignored. q
.. code-block:: python

View File

@@ -16,7 +16,7 @@ IPv6 example:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('::', 7777))
app = Sanic("ipv6_example")
app = Sanic()
@app.route("/")
@@ -46,7 +46,7 @@ UNIX socket example:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_socket)
app = Sanic("unix_socket_example")
app = Sanic()
@app.route("/")

View File

@@ -88,5 +88,5 @@ When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `
app = Sanic(__name__)
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8MiB
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)

View File

@@ -16,7 +16,7 @@ Sanic allows you to get request data by stream, as below. When the request ends,
from sanic.response import stream, text
bp = Blueprint('blueprint_request_stream')
app = Sanic(__name__)
app = Sanic('request_stream')
class SimpleView(HTTPMethodView):

View File

@@ -58,32 +58,6 @@ More information about
the available arguments to `httpx` can be found
[in the documentation for `httpx <https://www.encode.io/httpx/>`_.
.. code-block:: python
@pytest.mark.asyncio
async def test_index_returns_200():
request, response = await app.asgi_client.put('/')
assert response.status == 200
.. note::
Whenever one of the test clients run, you can test your app instance to determine if it is in testing mode:
`app.test_mode`.
Additionally, Sanic has an asynchronous testing client. The difference is that the async client will not stand up an
instance of your application, but will instead reach inside it using ASGI. All listeners and middleware are still
executed.
.. code-block:: python
@pytest.mark.asyncio
async def test_index_returns_200():
request, response = await app.asgi_client.put('/')
assert response.status == 200
.. note::
Whenever one of the test clients run, you can test your app instance to determine if it is in testing mode:
`app.test_mode`.
Using a random port
-------------------

View File

@@ -12,7 +12,7 @@ To setup a WebSocket:
from sanic.response import json
from sanic.websocket import WebSocketProtocol
app = Sanic("websocket_example")
app = Sanic()
@app.websocket('/feed')
async def feed(request, ws):
@@ -51,9 +51,5 @@ You could setup your own WebSocket configuration through ``app.config``, like
app.config.WEBSOCKET_MAX_QUEUE = 32
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
app.config.WEBSOCKET_WRITE_LIMIT = 2 ** 16
app.config.WEBSOCKET_PING_INTERVAL = 20
app.config.WEBSOCKET_PING_TIMEOUT = 20
These settings will have no impact if running in ASGI mode.
Find more in ``Configuration`` section.

19
environment.yml Normal file
View File

@@ -0,0 +1,19 @@
name: py36
dependencies:
- pip=18.1=py36_0
- python=3.6=0
- setuptools=40.4.3=py36_0
- pip:
- httptools>=0.0.10
- uvloop>=0.5.3
- ujson>=1.35
- aiofiles>=0.3.0
- websockets>=6.0,<7.0
- multidict>=4.0,<5.0
- sphinx==1.8.3
- sphinx_rtd_theme==0.4.2
- recommonmark==0.5.0
- httpx==0.9.3
- sphinxcontrib-asyncio>=0.2.0
- docutils==0.14
- pygments==2.3.1

View File

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

View File

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

View File

@@ -7,7 +7,6 @@
"""
from pathlib import Path
from sanic import Sanic, response

View File

@@ -1,9 +1,2 @@
version: 2
python:
version: 3.8
install:
- method: pip
path: .
extra_requirements:
- docs
system_packages: true
conda:
file: environment.yml

View File

@@ -1,105 +1,39 @@
import os
import sys
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from argparse import ArgumentParser
from importlib import import_module
from typing import Any, Dict, Optional
from sanic import __version__
from sanic.app import Sanic
from sanic.config import BASE_LOGO
from sanic.log import logger
class SanicArgumentParser(ArgumentParser):
def add_bool_arguments(self, *args, **kwargs):
group = self.add_mutually_exclusive_group()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = "no " + kwargs["help"]
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
def main():
parser = SanicArgumentParser(
prog="sanic",
description=BASE_LOGO,
formatter_class=RawDescriptionHelpFormatter,
)
parser.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="host address [default 127.0.0.1]",
)
parser.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="port to serve on [default 8000]",
)
parser.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket",
)
if __name__ == "__main__":
parser = ArgumentParser(prog="sanic")
parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
parser.add_argument("--port", dest="port", type=int, default=8000)
parser.add_argument(
"--cert", dest="cert", type=str, help="location of certificate for SSL"
)
parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL."
)
parser.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="number of worker processes [default 1]",
)
parser.add_argument("--workers", dest="workers", type=int, default=1)
parser.add_argument("--debug", dest="debug", action="store_true")
parser.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"Sanic {__version__}",
)
parser.add_argument(
"module", help="path to your Sanic app. Example: path.to.server:app"
)
parser.add_argument("module")
args = parser.parse_args()
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if ":" in args.module:
module_name, app_name = args.module.rsplit(":", 1)
else:
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
module = import_module(module_name)
app = getattr(module, app_name, None)
app_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_name}. "
f"Perhaps you meant {args.module}.app?"
"Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?".format(
type(app).__name__, args.module
)
)
if args.cert is not None or args.key is not None:
ssl = {
@@ -112,21 +46,15 @@ def main():
app.run(
host=args.host,
port=args.port,
unix=args.unix,
workers=args.workers,
debug=args.debug,
access_log=args.access_log,
ssl=ssl,
)
except ImportError as e:
logger.error(
f"No module named {e.name} found.\n"
f" Example File: project/sanic_server.py -> app\n"
f" Example Module: project.sanic_server.app"
"No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app".format(e.name)
)
except ValueError:
logger.exception("Failed to run app")
if __name__ == "__main__":
main()

View File

@@ -1 +1 @@
__version__ = "20.12.5"
__version__ = "19.12.5"

View File

@@ -2,17 +2,17 @@ import logging
import logging.config
import os
import re
import warnings
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from collections import defaultdict, deque
from functools import partial
from inspect import isawaitable, signature
from inspect import getmodulename, isawaitable, signature, stack
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc
from typing import Any, Dict, Optional, Type, Union
from urllib.parse import urlencode, urlunparse
from warnings import warn
from sanic import reloader_helpers
from sanic.asgi import ASGIApp
@@ -38,9 +38,6 @@ from sanic.websocket import ConnectionClosed, WebSocketProtocol
class Sanic:
_app_registry: Dict[str, "Sanic"] = {}
test_mode = False
def __init__(
self,
name=None,
@@ -51,15 +48,19 @@ class Sanic:
strict_slashes=False,
log_config=None,
configure_logging=True,
register=None,
):
# Get name from previous stack frame
if name is None:
raise SanicException(
"Sanic instance cannot be unnamed. "
warnings.warn(
"Sanic(name=None) is deprecated and None value support "
"for `name` will be removed in the next release. "
"Please use Sanic(name='your_application_name') instead.",
DeprecationWarning,
stacklevel=2,
)
frame_records = stack()[1]
name = getmodulename(frame_records[1])
# logging
if configure_logging:
@@ -67,7 +68,7 @@ class Sanic:
self.name = name
self.asgi = False
self.router = router or Router(self)
self.router = router or Router()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env)
@@ -80,7 +81,6 @@ class Sanic:
self.sock = None
self.strict_slashes = strict_slashes
self.listeners = defaultdict(list)
self.is_stopping = False
self.is_running = False
self.is_request_stream = False
self.websocket_enabled = False
@@ -90,12 +90,6 @@ class Sanic:
# Register alternative method names
self.go_fast = self.run
if register is not None:
self.config.REGISTER = register
if self.config.REGISTER:
self.__class__.register_app(self)
@property
def loop(self):
"""Synonymous with asyncio.get_event_loop().
@@ -122,12 +116,24 @@ class Sanic:
:param task: future, couroutine or awaitable
"""
try:
loop = self.loop # Will raise SanicError if loop is not started
self._loop_add_task(task, self, loop)
if callable(task):
try:
self.loop.create_task(task(self))
except TypeError:
self.loop.create_task(task())
else:
self.loop.create_task(task)
except SanicException:
self.listener("before_server_start")(
partial(self._loop_add_task, task)
)
@self.listener("before_server_start")
def run(app, loop):
if callable(task):
try:
loop.create_task(task(self))
except TypeError:
loop.create_task(task())
else:
loop.create_task(task)
# Decorator
def listener(self, event):
@@ -197,11 +203,9 @@ class Sanic:
args = list(signature(handler).parameters.keys())
if not args:
handler_name = handler.__name__
raise ValueError(
f"Required parameter `request` missing "
f"in the {handler_name}() route?"
"Required parameter `request` missing "
"in the {0}() route?".format(handler.__name__)
)
if stream:
@@ -455,13 +459,7 @@ class Sanic:
# Decorator
def websocket(
self,
uri,
host=None,
strict_slashes=None,
subprotocols=None,
version=None,
name=None,
self, uri, host=None, strict_slashes=None, subprotocols=None, name=None
):
"""
Decorate a function to be registered as a websocket route
@@ -492,10 +490,47 @@ class Sanic:
routes, handler = handler
else:
routes = []
websocket_handler = partial(
self._websocket_handler, handler, subprotocols=subprotocols
)
websocket_handler.__name__ = handler.__name__
async def websocket_handler(request, *args, **kwargs):
request.app = self
if not getattr(handler, "__blueprintname__", False):
request.endpoint = handler.__name__
else:
request.endpoint = (
getattr(handler, "__blueprintname__", "")
+ handler.__name__
)
pass
if self.asgi:
ws = request.transport.get_websocket_connection()
else:
try:
protocol = request.transport.get_protocol()
except AttributeError:
# On Python3.5 the Transport classes in asyncio do not
# have a get_protocol() method as in uvloop
protocol = request.transport._protocol
protocol.app = self
ws = await protocol.websocket_handshake(
request, subprotocols
)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.add(fut)
try:
await fut
except (CancelledError, ConnectionClosed):
pass
finally:
self.websocket_tasks.remove(fut)
await ws.close()
routes.extend(
self.router.add(
uri=uri,
@@ -503,7 +538,6 @@ class Sanic:
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
)
@@ -518,7 +552,6 @@ class Sanic:
host=None,
strict_slashes=None,
subprotocols=None,
version=None,
name=None,
):
"""
@@ -546,7 +579,6 @@ class Sanic:
host=host,
strict_slashes=strict_slashes,
subprotocols=subprotocols,
version=version,
name=name,
)(handler)
@@ -559,10 +591,36 @@ class Sanic:
if not self.websocket_enabled:
# if the server is stopped, we want to cancel any ongoing
# websocket tasks, to allow the server to exit promptly
self.listener("before_server_stop")(self._cancel_websocket_tasks)
@self.listener("before_server_stop")
def cancel_websocket_tasks(app, loop):
for task in self.websocket_tasks:
task.cancel()
self.websocket_enabled = enable
def remove_route(self, uri, clean_cache=True, host=None):
"""
This method provides the app user a mechanism by which an already
existing route can be removed from the :class:`Sanic` object
.. warning::
remove_route is deprecated in v19.06 and will be removed
from future versions.
:param uri: URL Path to be removed from the app
:param clean_cache: Instruct sanic if it needs to clean up the LRU
route cache
:param host: IP address or FQDN specific to the host
:return: None
"""
warnings.warn(
"remove_route is deprecated and will be removed "
"from future versions.",
DeprecationWarning,
stacklevel=2,
)
self.router.remove(uri, clean_cache, host)
# Decorator
def exception(self, *exceptions):
"""Decorate a function to be registered as a handler for exceptions
@@ -620,7 +678,7 @@ class Sanic:
if _rn not in self.named_response_middleware:
self.named_response_middleware[_rn] = deque()
if middleware not in self.named_response_middleware[_rn]:
self.named_response_middleware[_rn].appendleft(middleware)
self.named_response_middleware[_rn].append(middleware)
# Decorator
def middleware(self, middleware_or_request):
@@ -678,10 +736,9 @@ class Sanic:
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param content_type: user defined content type for header
:return: routes registered on the router
:rtype: List[sanic.router.Route]
:return: None
"""
return static_register(
static_register(
self,
uri,
file_or_directory,
@@ -716,6 +773,28 @@ class Sanic:
self._blueprint_order.append(blueprint)
blueprint.register(self, options)
def register_blueprint(self, *args, **kwargs):
"""
Proxy method provided for invoking the :func:`blueprint` method
.. note::
To be deprecated in 1.0. Use :func:`blueprint` instead.
:param args: Blueprint object or (list, tuple) thereof
:param kwargs: option dictionary with blueprint defaults
:return: None
"""
if self.debug:
warnings.simplefilter("default")
warnings.warn(
"Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method"
" instead",
DeprecationWarning,
)
return self.blueprint(*args, **kwargs)
def url_for(self, view_name: str, **kwargs):
r"""Build a URL based on a view name and the values provided.
@@ -746,27 +825,9 @@ class Sanic:
kw.update(name=view_name)
uri, route = self.router.find_route_by_view_name(view_name, **kw)
# TODO(laggardkernel): this fix should be removed in v21.3.
# Try again without the unnecessary prefix "websocket_handler_",
# which was added by accident on non-blueprint handlers. GH-2021
if not (uri and route) and view_name.startswith("websocket_handler_"):
view_name = view_name[18:]
uri, route = self.router.find_route_by_view_name(view_name, **kw)
if uri and route:
warn(
"The bug of adding unnecessary `websocket_handler_` "
"prefix in param `view_name` for non-blueprint handlers "
"is fixed. This backward support will be removed in "
"v21.3. Please update `Sanic.url_for()` callings in your "
"code soon.",
DeprecationWarning,
stacklevel=2,
)
if not (uri and route):
raise URLBuildError(
f"Endpoint with name `{view_name}` was not found"
"Endpoint with name `{}` was not found".format(view_name)
)
# If the route has host defined, split that off
@@ -788,7 +849,7 @@ class Sanic:
if filename.startswith("/"):
filename = filename[1:]
uri = f"{folder_}/{filename}"
uri = "{}/{}".format(folder_, filename)
if uri != "/" and uri.endswith("/"):
uri = uri[:-1]
@@ -824,7 +885,7 @@ class Sanic:
for match in matched_params:
name, _type, pattern = self.router.parse_parameter_string(match)
# we only want to match against each individual parameter
specific_pattern = f"^{pattern}$"
specific_pattern = "^{}$".format(pattern)
supplied_param = None
if name in kwargs:
@@ -832,7 +893,9 @@ class Sanic:
del kwargs[name]
else:
raise URLBuildError(
f"Required parameter `{name}` was not passed to url_for"
"Required parameter `{}` was not passed to url_for".format(
name
)
)
supplied_param = str(supplied_param)
@@ -842,22 +905,23 @@ class Sanic:
if not passes_pattern:
if _type != str:
type_name = _type.__name__
msg = (
f'Value "{supplied_param}" '
f"for parameter `{name}` does not "
f"match pattern for type `{type_name}`: {pattern}"
'Value "{}" for parameter `{}` does not '
"match pattern for type `{}`: {}".format(
supplied_param, name, _type.__name__, pattern
)
)
else:
msg = (
f'Value "{supplied_param}" for parameter `{name}` '
f"does not satisfy pattern {pattern}"
'Value "{}" for parameter `{}` '
"does not satisfy pattern {}".format(
supplied_param, name, pattern
)
)
raise URLBuildError(msg)
# replace the parameter in the URL with the supplied value
replacement_regex = f"(<{name}.*?>)"
replacement_regex = "(<{}.*?>)".format(name)
out = re.sub(replacement_regex, supplied_param, out)
@@ -898,9 +962,7 @@ class Sanic:
name = None
try:
# Fetch handler from router
handler, args, kwargs, uri, name, endpoint = self.router.get(
request
)
handler, args, kwargs, uri, name = self.router.get(request)
# -------------------------------------------- #
# Request Middleware
@@ -922,8 +984,16 @@ class Sanic:
"handler from the router"
)
)
request.endpoint = endpoint
else:
if not getattr(handler, "__blueprintname__", False):
request.endpoint = self._build_endpoint_name(
handler.__name__
)
else:
request.endpoint = self._build_endpoint_name(
getattr(handler, "__blueprintname__", ""),
handler.__name__,
)
# Run response handler
response = handler(request, *args, **kwargs)
@@ -952,8 +1022,9 @@ class Sanic:
)
elif self.debug:
response = HTTPResponse(
f"Error while "
f"handling error: {e}\nStack: {format_exc()}",
"Error while handling error: {}\nStack: {}".format(
e, format_exc()
),
status=500,
)
else:
@@ -1016,18 +1087,16 @@ class Sanic:
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
debug: bool = False,
auto_reload: Optional[bool] = None,
ssl: Union[dict, SSLContext, None] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
stop_event: Any = None,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: None = None,
**kwargs: Any
) -> None:
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
@@ -1038,9 +1107,6 @@ class Sanic:
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param auto_reload: Reload app whenever its source code is changed.
Enabled by default in debug mode.
:type auto_relaod: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
@@ -1053,15 +1119,16 @@ class Sanic:
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param stop_event: event to be triggered
before stopping the app - deprecated
:type stop_event: None
:param register_sys_signals: Register SIG* events
:type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param unix: Unix socket to listen on instead of TCP port
:type unix: str
:return: Nothing
"""
if loop is not None:
if "loop" in kwargs:
raise TypeError(
"loop is not a valid argument. To use an existing loop, "
"change to create_server().\nSee more: "
@@ -1069,9 +1136,13 @@ class Sanic:
"#asynchronous-support"
)
if auto_reload or auto_reload is None and debug:
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
return reloader_helpers.watchdog(1.0)
# Default auto_reload to false
auto_reload = False
# If debug is set, default it to true (unless on windows)
if debug and os.name == "posix":
auto_reload = True
# Allow for overriding either of the defaults
auto_reload = kwargs.get("auto_reload", auto_reload)
if sock is None:
host, port = host or "127.0.0.1", port or 8000
@@ -1080,6 +1151,13 @@ class Sanic:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
if stop_event is not None:
if debug:
warnings.simplefilter("default")
warnings.warn(
"stop_event will be removed from future versions.",
DeprecationWarning,
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log
@@ -1090,7 +1168,6 @@ class Sanic:
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
workers=workers,
protocol=protocol,
backlog=backlog,
@@ -1100,15 +1177,19 @@ class Sanic:
try:
self.is_running = True
self.is_stopping = False
if workers > 1 and os.name != "posix":
logger.warn(
f"Multiprocessing is currently not supported on {os.name},"
" using workers=1 instead"
)
workers = 1
if workers == 1:
serve(**server_settings)
if auto_reload and os.name != "posix":
# This condition must be removed after implementing
# auto reloader for other operating systems.
raise NotImplementedError
if (
auto_reload
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
):
reloader_helpers.watchdog(2)
else:
serve(**server_settings)
else:
serve_multiple(server_settings, workers)
except BaseException:
@@ -1122,22 +1203,19 @@ class Sanic:
def stop(self):
"""This kills the Sanic"""
if not self.is_stopping:
self.is_stopping = True
get_event_loop().stop()
get_event_loop().stop()
async def create_server(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
debug: bool = False,
ssl: Union[dict, SSLContext, None] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
stop_event: Any = None,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
return_asyncio_server=False,
asyncio_server_kwargs=None,
) -> Optional[AsyncioServer]:
@@ -1168,6 +1246,9 @@ class Sanic:
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param stop_event: event to be triggered
before stopping the app - deprecated
:type stop_event: None
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need
@@ -1187,6 +1268,13 @@ class Sanic:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
if stop_event is not None:
if debug:
warnings.simplefilter("default")
warnings.warn(
"stop_event will be removed from future versions.",
DeprecationWarning,
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log
@@ -1197,7 +1285,6 @@ class Sanic:
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
loop=get_event_loop(),
protocol=protocol,
backlog=backlog,
@@ -1263,11 +1350,11 @@ class Sanic:
debug=False,
ssl=None,
sock=None,
unix=None,
workers=1,
loop=None,
protocol=HttpProtocol,
backlog=100,
stop_event=None,
register_sys_signals=True,
run_async=False,
auto_reload=False,
@@ -1282,6 +1369,13 @@ class Sanic:
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
context.load_cert_chain(cert, keyfile=key)
ssl = context
if stop_event is not None:
if debug:
warnings.simplefilter("default")
warnings.warn(
"stop_event will be removed from future versions.",
DeprecationWarning,
)
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError(
"PROXIES_COUNT cannot be negative. "
@@ -1294,16 +1388,33 @@ class Sanic:
server_settings = {
"protocol": protocol,
"request_class": self.request_class,
"is_request_stream": self.is_request_stream,
"router": self.router,
"host": host,
"port": port,
"sock": sock,
"unix": unix,
"ssl": ssl,
"app": self,
"signal": Signal(),
"debug": debug,
"request_handler": self.handle_request,
"error_handler": self.error_handler,
"request_timeout": self.config.REQUEST_TIMEOUT,
"response_timeout": self.config.RESPONSE_TIMEOUT,
"keep_alive_timeout": self.config.KEEP_ALIVE_TIMEOUT,
"request_max_size": self.config.REQUEST_MAX_SIZE,
"request_buffer_queue_size": self.config.REQUEST_BUFFER_QUEUE_SIZE,
"keep_alive": self.config.KEEP_ALIVE,
"loop": loop,
"register_sys_signals": register_sys_signals,
"backlog": backlog,
"access_log": self.config.ACCESS_LOG,
"websocket_max_size": self.config.WEBSOCKET_MAX_SIZE,
"websocket_max_queue": self.config.WEBSOCKET_MAX_QUEUE,
"websocket_read_limit": self.config.WEBSOCKET_READ_LIMIT,
"websocket_write_limit": self.config.WEBSOCKET_WRITE_LIMIT,
"graceful_shutdown_timeout": self.config.GRACEFUL_SHUTDOWN_TIMEOUT,
}
# -------------------------------------------- #
@@ -1340,14 +1451,11 @@ class Sanic:
server_settings["run_async"] = True
# Serve
if host and port:
if host and port and os.environ.get("SANIC_SERVER_RUNNING") != "true":
proto = "http"
if ssl is not None:
proto = "https"
if unix:
logger.info(f"Goin' Fast @ {unix} {proto}://...")
else:
logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
logger.info("Goin' Fast @ {}://{}:{}".format(proto, host, port))
return server_settings
@@ -1355,55 +1463,6 @@ class Sanic:
parts = [self.name, *parts]
return ".".join(parts)
@classmethod
def _loop_add_task(cls, task, app, loop):
if callable(task):
try:
loop.create_task(task(app))
except TypeError:
loop.create_task(task())
else:
loop.create_task(task)
@classmethod
def _cancel_websocket_tasks(cls, app, loop):
for task in app.websocket_tasks:
task.cancel()
async def _websocket_handler(
self, handler, request, *args, subprotocols=None, **kwargs
):
request.app = self
if not getattr(handler, "__blueprintname__", False):
request.endpoint = handler.__name__
else:
request.endpoint = (
getattr(handler, "__blueprintname__", "") + handler.__name__
)
pass
if self.asgi:
ws = request.transport.get_websocket_connection()
else:
protocol = request.transport.get_protocol()
protocol.app = self
ws = await protocol.websocket_handshake(request, subprotocols)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.add(fut)
try:
await fut
except (CancelledError, ConnectionClosed):
pass
finally:
self.websocket_tasks.remove(fut)
await ws.close()
# -------------------------------------------------------------------- #
# ASGI
# -------------------------------------------------------------------- #
@@ -1417,40 +1476,3 @@ class Sanic:
await asgi_app()
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
# -------------------------------------------------------------------- #
# Configuration
# -------------------------------------------------------------------- #
def update_config(self, config: Union[bytes, str, dict, Any]):
"""Update app.config.
Please refer to config.py::Config.update_config for documentation."""
self.config.update_config(config)
# -------------------------------------------------------------------- #
# Class methods
# -------------------------------------------------------------------- #
@classmethod
def register_app(cls, app: "Sanic") -> None:
"""Register a Sanic instance"""
if not isinstance(app, cls):
raise SanicException("Registered app must be an instance of Sanic")
name = app.name
if name in cls._app_registry and not cls.test_mode:
raise SanicException(f'Sanic app name "{name}" already in use.')
cls._app_registry[name] = app
@classmethod
def get_app(cls, name: str, *, force_create: bool = False) -> "Sanic":
"""Retrieve an instantiated Sanic instance"""
try:
return cls._app_registry[name]
except KeyError:
if force_create:
return cls(name)
raise SanicException(f'Sanic app name "{name}" not found.')

View File

@@ -22,7 +22,7 @@ from sanic.exceptions import InvalidUsage, ServerError
from sanic.log import logger
from sanic.request import Request
from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.server import ConnInfo, StreamBuffer
from sanic.server import StreamBuffer
from sanic.websocket import WebSocketConnection
@@ -98,9 +98,7 @@ class MockTransport:
def create_websocket_connection(
self, send: ASGISend, receive: ASGIReceive
) -> WebSocketConnection:
self._websocket_connection = WebSocketConnection(
send, receive, self.scope.get("subprotocols", [])
)
self._websocket_connection = WebSocketConnection(send, receive)
return self._websocket_connection
def add_task(self) -> None:
@@ -257,7 +255,6 @@ class ASGIApp:
instance.transport,
sanic_app,
)
instance.request.conn_info = ConnInfo(instance.transport)
if sanic_app.is_request_stream:
is_stream_handler = sanic_app.router.is_stream_handler(
@@ -314,9 +311,7 @@ class ASGIApp:
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
async def stream_callback(
self, response: Union[HTTPResponse, StreamingHTTPResponse]
) -> None:
async def stream_callback(self, response: HTTPResponse) -> None:
"""
Write the response.
"""

View File

@@ -143,18 +143,7 @@ class Blueprint:
if _routes:
routes += _routes
# Static Files
for future in self.statics:
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
_routes = app.static(
uri, future.file_or_directory, *future.args, **future.kwargs
)
if _routes:
routes += _routes
route_names = [route.name for route in routes if route]
route_names = [route.name for route in routes]
# Middleware
for future in self.middlewares:
if future.args or future.kwargs:
@@ -162,7 +151,7 @@ class Blueprint:
future.middleware,
route_names,
*future.args,
**future.kwargs,
**future.kwargs
)
else:
app.register_named_middleware(future.middleware, route_names)
@@ -171,6 +160,14 @@ class Blueprint:
for future in self.exceptions:
app.exception(*future.args, **future.kwargs)(future.handler)
# Static Files
for future in self.statics:
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
app.static(
uri, future.file_or_directory, *future.args, **future.kwargs
)
# Event listeners
for event, listeners in self.listeners.items():
for listener in listeners:
@@ -286,13 +283,6 @@ class Blueprint:
strict_slashes = self.strict_slashes
def decorator(handler):
nonlocal uri
nonlocal host
nonlocal strict_slashes
nonlocal version
nonlocal name
name = f"{self.name}.{name or handler.__name__}"
route = FutureRoute(
handler, uri, [], host, strict_slashes, False, version, name
)
@@ -386,7 +376,7 @@ class Blueprint:
"""
name = kwargs.pop("name", "static")
if not name.startswith(self.name + "."):
name = f"{self.name}.{name}"
name = "{}.{}".format(self.name, name)
kwargs.update(name=name)
strict_slashes = kwargs.get("strict_slashes")

View File

@@ -1,52 +1,6 @@
import asyncio
import signal
from sys import argv
from multidict import CIMultiDict # type: ignore
class Header(CIMultiDict):
def get_all(self, key):
return self.getall(key, default=[])
use_trio = argv[0].endswith("hypercorn") and "trio" in argv
if use_trio:
from trio import Path # type: ignore
from trio import open_file as open_async # type: ignore
def stat_async(path):
return Path(path).stat()
else:
from aiofiles import open as aio_open # type: ignore
from aiofiles.os import stat as stat_async # type: ignore # noqa: F401
async def open_async(file, mode="r", **kwargs):
return aio_open(file, mode, **kwargs)
def ctrlc_workaround_for_windows(app):
async def stay_active(app):
"""Asyncio wakeups to allow receiving SIGINT in Python"""
while not die:
# If someone else stopped the app, just exit
if app.is_stopping:
return
# Windows Python blocks signal handlers while the event loop is
# waiting for I/O. Frequent wakeups keep interrupts flowing.
await asyncio.sleep(0.1)
# Can't be called from signal handler, so call it from here
app.stop()
def ctrlc_handler(sig, frame):
nonlocal die
if die:
raise KeyboardInterrupt("Non-graceful Ctrl+C")
die = True
die = False
signal.signal(signal.SIGINT, ctrlc_handler)
app.add_task(stay_active)

View File

@@ -1,15 +1,8 @@
from os import environ
from typing import Any, Union
import os
import types
# NOTE(tomaszdrozdz): remove in version: 21.3
# We replace from_envvar(), from_object(), from_pyfile() config object methods
# with one simpler update_config() method.
# We also replace "loading module from file code" in from_pyfile()
# in a favour of load_module_from_file_location().
# Please see pull request: 1903
# and issue: 1895
from .deprecated import from_envvar, from_object, from_pyfile # noqa
from .utils import load_module_from_file_location, str_to_bool
from sanic.exceptions import PyFileError
from sanic.helpers import import_string
SANIC_PREFIX = "SANIC_"
@@ -27,20 +20,16 @@ DEFAULT_CONFIG = {
"RESPONSE_TIMEOUT": 60, # 60 seconds
"KEEP_ALIVE": True,
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabytes
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"WEBSOCKET_PING_TIMEOUT": 20,
"WEBSOCKET_PING_INTERVAL": 20,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True,
"FORWARDED_SECRET": None,
"REAL_IP_HEADER": None,
"PROXIES_COUNT": None,
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FALLBACK_ERROR_FORMAT": "html",
"REGISTER": True,
}
@@ -62,28 +51,81 @@ class Config(dict):
try:
return self[attr]
except KeyError as ke:
raise AttributeError(f"Config has no '{ke.args[0]}'")
raise AttributeError("Config has no '{}'".format(ke.args[0]))
def __setattr__(self, attr, value):
self[attr] = value
# NOTE(tomaszdrozdz): remove in version: 21.3
# We replace from_envvar(), from_object(), from_pyfile() config object
# methods with one simpler update_config() method.
# We also replace "loading module from file code" in from_pyfile()
# in a favour of load_module_from_file_location().
# Please see pull request: 1903
# and issue: 1895
from_envvar = from_envvar
from_pyfile = from_pyfile
from_object = from_object
def from_envvar(self, variable_name):
"""Load a configuration from an environment variable pointing to
a configuration file.
:param variable_name: name of the environment variable
:return: bool. ``True`` if able to load config, ``False`` otherwise.
"""
config_file = os.environ.get(variable_name)
if not config_file:
raise RuntimeError(
"The environment variable %r is not set and "
"thus configuration could not be loaded." % variable_name
)
return self.from_pyfile(config_file)
def from_pyfile(self, filename):
"""Update the values in the config from a Python file.
Only the uppercase variables in that module are stored in the config.
:param filename: an absolute path to the config file
"""
module = types.ModuleType("config")
module.__file__ = filename
try:
with open(filename) as config_file:
exec( # nosec
compile(config_file.read(), filename, "exec"),
module.__dict__,
)
except IOError as e:
e.strerror = "Unable to load configuration file (%s)" % e.strerror
raise
except Exception as e:
raise PyFileError(filename) from e
self.from_object(module)
return True
def from_object(self, obj):
"""Update the values from the given object.
Objects are usually either modules or classes.
Just the uppercase variables in that object are stored in the config.
Example usage::
from yourapplication import default_config
app.config.from_object(default_config)
or also:
app.config.from_object('myproject.config.MyConfigClass')
You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
package because the package might be installed system wide.
:param obj: an object holding the configuration
"""
if isinstance(obj, str):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
def load_environment_vars(self, prefix=SANIC_PREFIX):
"""
Looks for prefixed environment variables and applies
them to the configuration if present.
"""
for k, v in environ.items():
for k, v in os.environ.items():
if k.startswith(prefix):
_, config_key = k.split(prefix, 1)
try:
@@ -93,47 +135,23 @@ class Config(dict):
self[config_key] = float(v)
except ValueError:
try:
self[config_key] = str_to_bool(v)
self[config_key] = strtobool(v)
except ValueError:
self[config_key] = v
def update_config(self, config: Union[bytes, str, dict, Any]):
"""Update app.config.
Note:: only upper case settings are considered.
def strtobool(val):
"""
This function was borrowed from distutils.utils. While distutils
is part of stdlib, it feels odd to use distutils in main application code.
You can upload app config by providing path to py file
holding settings.
# /some/py/file
A = 1
B = 2
config.update_config("${some}/py/file")
Yes you can put environment variable here, but they must be provided
in format: ${some_env_var}, and mark that $some_env_var is treated
as plain string.
You can upload app config by providing dict holding settings.
d = {"A": 1, "B": 2}
config.update_config(d)
You can upload app config by providing any object holding settings,
but in such case config.__dict__ will be used as dict holding settings.
class C:
A = 1
B = 2
config.update_config(C)"""
if isinstance(config, (bytes, str)):
config = load_module_from_file_location(location=config)
if not isinstance(config, dict):
config = config.__dict__
config = dict(filter(lambda i: i[0].isupper(), config.items()))
self.update(config)
The function was modified to walk its talk and actually return bool
and not int.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return True
elif val in ("n", "no", "f", "false", "off", "0"):
return False
else:
raise ValueError("invalid truth value %r" % (val,))

View File

@@ -1,106 +0,0 @@
# NOTE(tomaszdrozdz): remove in version: 21.3
# We replace from_envvar(), from_object(), from_pyfile() config object methods
# with one simpler update_config() method.
# We also replace "loading module from file code" in from_pyfile()
# in a favour of load_module_from_file_location().
# Please see pull request: 1903
# and issue: 1895
import types
from os import environ
from typing import Any
from warnings import warn
from sanic.exceptions import PyFileError
from sanic.helpers import import_string
def from_envvar(self, variable_name: str) -> bool:
"""Load a configuration from an environment variable pointing to
a configuration file.
:param variable_name: name of the environment variable
:return: bool. ``True`` if able to load config, ``False`` otherwise.
"""
warn(
"Using `from_envvar` method is deprecated and will be removed in "
"v21.3, use `app.update_config` method instead.",
DeprecationWarning,
stacklevel=2,
)
config_file = environ.get(variable_name)
if not config_file:
raise RuntimeError(
f"The environment variable {variable_name} is not set and "
f"thus configuration could not be loaded."
)
return self.from_pyfile(config_file)
def from_pyfile(self, filename: str) -> bool:
"""Update the values in the config from a Python file.
Only the uppercase variables in that module are stored in the config.
:param filename: an absolute path to the config file
"""
warn(
"Using `from_pyfile` method is deprecated and will be removed in "
"v21.3, use `app.update_config` method instead.",
DeprecationWarning,
stacklevel=2,
)
module = types.ModuleType("config")
module.__file__ = filename
try:
with open(filename) as config_file:
exec( # nosec
compile(config_file.read(), filename, "exec"),
module.__dict__,
)
except IOError as e:
e.strerror = "Unable to load configuration file (e.strerror)"
raise
except Exception as e:
raise PyFileError(filename) from e
self.from_object(module)
return True
def from_object(self, obj: Any) -> None:
"""Update the values from the given object.
Objects are usually either modules or classes.
Just the uppercase variables in that object are stored in the config.
Example usage::
from yourapplication import default_config
app.config.from_object(default_config)
or also:
app.config.from_object('myproject.config.MyConfigClass')
You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
package because the package might be installed system wide.
:param obj: an object holding the configuration
"""
warn(
"Using `from_object` method is deprecated and will be removed in "
"v21.3, use `app.update_config` method instead.",
DeprecationWarning,
stacklevel=2,
)
if isinstance(obj, str):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

View File

@@ -1,330 +0,0 @@
import sys
import typing as t
from functools import partial
from traceback import extract_tb
from sanic.exceptions import InvalidUsage, SanicException
from sanic.helpers import STATUS_CODES
from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
try:
from ujson import dumps
dumps = partial(dumps, escape_forward_slashes=False)
except ImportError: # noqa
from json import dumps # type: ignore
FALLBACK_TEXT = (
"The server encountered an internal error and "
"cannot complete your request."
)
FALLBACK_STATUS = 500
class BaseRenderer:
def __init__(self, request, exception, debug):
self.request = request
self.exception = exception
self.debug = debug
@property
def headers(self):
if isinstance(self.exception, SanicException):
return getattr(self.exception, "headers", {})
return {}
@property
def status(self):
if isinstance(self.exception, SanicException):
return getattr(self.exception, "status_code", FALLBACK_STATUS)
return FALLBACK_STATUS
@property
def text(self):
if self.debug or isinstance(self.exception, SanicException):
return str(self.exception)
return FALLBACK_TEXT
@property
def title(self):
status_text = STATUS_CODES.get(self.status, b"Error Occurred").decode()
return f"{self.status}{status_text}"
def render(self):
output = (
self.full
if self.debug and not getattr(self.exception, "quiet", False)
else self.minimal
)
return output()
def minimal(self): # noqa
raise NotImplementedError
def full(self): # noqa
raise NotImplementedError
class HTMLRenderer(BaseRenderer):
TRACEBACK_STYLE = """
html { font-family: sans-serif }
h2 { color: #888; }
.tb-wrapper p { margin: 0 }
.frame-border { margin: 1rem }
.frame-line > * { padding: 0.3rem 0.6rem }
.frame-line { margin-bottom: 0.3rem }
.frame-code { font-size: 16px; padding-left: 4ch }
.tb-wrapper { border: 1px solid #eee }
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
.frame-descriptor { background: #e2eafb; font-size: 14px }
"""
TRACEBACK_WRAPPER_HTML = (
"<div class=tb-header>{exc_name}: {exc_value}</div>"
"<div class=tb-wrapper>{frame_html}</div>"
)
TRACEBACK_BORDER = (
"<div class=frame-border>"
"The above exception was the direct cause of the following exception:"
"</div>"
)
TRACEBACK_LINE_HTML = (
"<div class=frame-line>"
"<p class=frame-descriptor>"
"File {0.filename}, line <i>{0.lineno}</i>, "
"in <code><b>{0.name}</b></code>"
"<p class=frame-code><code>{0.line}</code>"
"</div>"
)
OUTPUT_HTML = (
"<!DOCTYPE html><html lang=en>"
"<meta charset=UTF-8><title>{title}</title>\n"
"<style>{style}</style>\n"
"<h1>{title}</h1><p>{text}\n"
"{body}"
)
def full(self):
return html(
self.OUTPUT_HTML.format(
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(),
),
status=self.status,
)
def minimal(self):
return html(
self.OUTPUT_HTML.format(
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body="",
),
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):
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines = [
f"<h2>Traceback of {appname} (most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
"</div>",
]
return "\n".join(lines)
def _format_exc(self, exc):
frames = extract_tb(exc.__traceback__)
frame_html = "".join(
self.TRACEBACK_LINE_HTML.format(frame) for frame in frames
)
return self.TRACEBACK_WRAPPER_HTML.format(
exc_name=escape(exc.__class__.__name__),
exc_value=escape(exc),
frame_html=frame_html,
)
class TextRenderer(BaseRenderer):
OUTPUT_TEXT = "{title}\n{bar}\n{text}\n\n{body}"
SPACER = " "
def full(self):
return text(
self.OUTPUT_TEXT.format(
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body=self._generate_body(),
),
status=self.status,
)
def minimal(self):
return text(
self.OUTPUT_TEXT.format(
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body="",
),
status=self.status,
headers=self.headers,
)
@property
def title(self):
return f"⚠️ {super().title}"
def _generate_body(self):
_, exc_value, __ = sys.exc_info()
exceptions = []
# traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
lines = [
f"{self.exception.__class__.__name__}: {self.exception} while "
f"handling path {self.request.path}",
f"Traceback of {self.request.app.name} (most recent call last):\n",
]
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
return "\n".join(lines + exceptions[::-1])
def _format_exc(self, exc):
frames = "\n\n".join(
[
f"{self.SPACER * 2}File {frame.filename}, "
f"line {frame.lineno}, in "
f"{frame.name}\n{self.SPACER * 2}{frame.line}"
for frame in extract_tb(exc.__traceback__)
]
)
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
class JSONRenderer(BaseRenderer):
def full(self):
output = self._generate_output(full=True)
return json(output, status=self.status, dumps=dumps)
def minimal(self):
output = self._generate_output(full=False)
return json(output, status=self.status, dumps=dumps)
def _generate_output(self, *, full):
output = {
"description": self.title,
"status": self.status,
"message": self.text,
}
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(
{
"type": exc_value.__class__.__name__,
"exception": str(exc_value),
"frames": [
{
"file": frame.filename,
"line": frame.lineno,
"name": frame.name,
"src": frame.line,
}
for frame in extract_tb(exc_value.__traceback__)
],
}
)
exc_value = exc_value.__cause__
output["path"] = self.request.path
output["args"] = self.request.args
output["exceptions"] = exceptions[::-1]
return output
@property
def title(self):
return STATUS_CODES.get(self.status, b"Error Occurred").decode()
def escape(text):
"""Minimal HTML escaping, not for attribute values (unlike html.escape)."""
return f"{text}".replace("&", "&amp;").replace("<", "&lt;")
RENDERERS_BY_CONFIG = {
"html": HTMLRenderer,
"json": JSONRenderer,
"text": TextRenderer,
}
RENDERERS_BY_CONTENT_TYPE = {
"multipart/form-data": HTMLRenderer,
"application/json": JSONRenderer,
"text/plain": TextRenderer,
}
def exception_response(
request: Request,
exception: Exception,
debug: bool,
renderer: t.Type[t.Optional[BaseRenderer]] = None,
) -> HTTPResponse:
"""Render a response for the default FALLBACK exception handler"""
if not renderer:
renderer = HTMLRenderer
if request:
if request.app.config.FALLBACK_ERROR_FORMAT == "auto":
try:
renderer = JSONRenderer if request.json else HTMLRenderer
except InvalidUsage:
renderer = HTMLRenderer
content_type, *_ = request.headers.get(
"content-type", ""
).split(";")
renderer = RENDERERS_BY_CONTENT_TYPE.get(
content_type, renderer
)
else:
render_format = request.app.config.FALLBACK_ERROR_FORMAT
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)
renderer = t.cast(t.Type[BaseRenderer], renderer)
return renderer(request, exception, debug).render()

View File

@@ -1,18 +1,133 @@
from sanic.helpers import STATUS_CODES
TRACEBACK_STYLE = """
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
}
p {
margin: 0;
}
.summary {
padding: 10px;
}
h1 {
margin-bottom: 0;
}
h3 {
margin-top: 10px;
}
h3 code {
font-size: 24px;
}
.frame-line > * {
padding: 5px 10px;
}
.frame-line {
margin-bottom: 5px;
}
.frame-code {
font-size: 16px;
padding-left: 30px;
}
.tb-wrapper {
border: 1px solid #f3f3f3;
}
.tb-header {
background-color: #f3f3f3;
padding: 5px 10px;
}
.tb-border {
padding-top: 20px;
}
.frame-descriptor {
background-color: #e2eafb;
}
.frame-descriptor {
font-size: 14px;
}
</style>
"""
TRACEBACK_WRAPPER_HTML = """
<html>
<head>
{style}
</head>
<body>
{inner_html}
<div class="summary">
<p>
<b>{exc_name}: {exc_value}</b>
while handling path <code>{path}</code>
</p>
</div>
</body>
</html>
"""
TRACEBACK_WRAPPER_INNER_HTML = """
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
</div>
"""
TRACEBACK_BORDER = """
<div class="tb-border">
<b><i>
The above exception was the direct cause of the
following exception:
</i></b>
</div>
"""
TRACEBACK_LINE_HTML = """
<div class="frame-line">
<p class="frame-descriptor">
File {0.filename}, line <i>{0.lineno}</i>,
in <code><b>{0.name}</b></code>
</p>
<p class="frame-code"><code>{0.line}</code></p>
</div>
"""
INTERNAL_SERVER_ERROR_HTML = """
<h1>Internal Server Error</h1>
<p>
The server encountered an internal error and cannot complete
your request.
</p>
"""
_sanic_exceptions = {}
def add_status_code(code, quiet=None):
def add_status_code(code):
"""
Decorator used for adding exceptions to :class:`SanicException`.
"""
def class_decorator(cls):
cls.status_code = code
if quiet or quiet is None and code != 500:
cls.quiet = True
_sanic_exceptions[code] = cls
return cls
@@ -20,16 +135,12 @@ def add_status_code(code, quiet=None):
class SanicException(Exception):
def __init__(self, message, status_code=None, quiet=None):
def __init__(self, message, status_code=None):
super().__init__(message)
if status_code is not None:
self.status_code = status_code
# quiet=None/False/True with None meaning choose by status
if quiet or quiet is None and status_code not in (None, 500):
self.quiet = True
@add_status_code(404)
class NotFound(SanicException):
@@ -45,7 +156,10 @@ class InvalidUsage(SanicException):
class MethodNotSupported(SanicException):
def __init__(self, message, method, allowed_methods):
super().__init__(message)
self.headers = {"Allow": ", ".join(allowed_methods)}
self.headers = dict()
self.headers["Allow"] = ", ".join(allowed_methods)
if method in ["HEAD", "PATCH", "PUT", "DELETE"]:
self.headers["Content-Length"] = 0
@add_status_code(500)
@@ -98,7 +212,10 @@ class HeaderNotFound(InvalidUsage):
class ContentRangeError(SanicException):
def __init__(self, message, content_range):
super().__init__(message)
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
self.headers = {
"Content-Type": "text/plain",
"Content-Range": "bytes */%s" % (content_range.total,),
}
@add_status_code(417)
@@ -165,22 +282,18 @@ class Unauthorized(SanicException):
challenge = ", ".join(values)
self.headers = {
"WWW-Authenticate": f"{scheme} {challenge}".rstrip()
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
}
class LoadFileException(SanicException):
pass
def abort(status_code, message=None):
"""
Raise an exception based on SanicException. Returns the HTTP response
message appropriate for the given status code, unless provided.
:param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages in
STATUS_CODES from sanic.helpers for the given status code.
:param message: The HTTP response body. Defaults to the messages
in response.py for the given status code.
"""
if message is None:
message = STATUS_CODES.get(status_code)

View File

@@ -1,13 +1,21 @@
from traceback import format_exc
import sys
from traceback import extract_tb, format_exc
from sanic.errorpages import exception_response
from sanic.exceptions import (
INTERNAL_SERVER_ERROR_HTML,
TRACEBACK_BORDER,
TRACEBACK_LINE_HTML,
TRACEBACK_STYLE,
TRACEBACK_WRAPPER_HTML,
TRACEBACK_WRAPPER_INNER_HTML,
ContentRangeError,
HeaderNotFound,
InvalidRangeType,
SanicException,
)
from sanic.log import logger
from sanic.response import text
from sanic.response import html, text
class ErrorHandler:
@@ -32,6 +40,35 @@ class ErrorHandler:
self.cached_handlers = {}
self.debug = False
def _render_exception(self, exception):
frames = extract_tb(exception.__traceback__)
frame_html = []
for frame in frames:
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
return TRACEBACK_WRAPPER_INNER_HTML.format(
exc_name=exception.__class__.__name__,
exc_value=exception,
frame_html="".join(frame_html),
)
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._render_exception(exc_value))
exc_value = exc_value.__cause__
return TRACEBACK_WRAPPER_HTML.format(
style=TRACEBACK_STYLE,
exc_name=exception.__class__.__name__,
exc_value=exception,
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
path=request.path,
)
def add(self, exception, handler):
"""
Add a new exception handler to an already existing handler object.
@@ -129,17 +166,27 @@ class ErrorHandler:
:class:`Exception`
:return:
"""
quiet = getattr(exception, "quiet", False)
if quiet is False:
try:
url = repr(request.url)
except AttributeError:
url = "unknown"
self.log(format_exc())
try:
url = repr(request.url)
except AttributeError:
url = "unknown"
self.log(format_exc())
logger.exception("Exception occurred while handling uri: %s", url)
response_message = "Exception occurred while handling uri: %s"
logger.exception(response_message, url)
return exception_response(request, exception, self.debug)
if issubclass(type(exception), SanicException):
return text(
"Error: {}".format(exception),
status=getattr(exception, "status_code", 500),
headers=getattr(exception, "headers", dict()),
)
elif self.debug:
html_output = self._render_traceback_html(exception, request)
return html(html_output, status=500)
else:
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
class ContentRangeHandler:

View File

@@ -3,8 +3,6 @@ import re
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from urllib.parse import unquote
from sanic.helpers import STATUS_CODES
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
Options = Dict[str, Union[int, str]] # key=value fields in various headers
@@ -182,19 +180,3 @@ def format_http1(headers: HeaderIterable) -> bytes:
- Values are converted into strings if necessary.
"""
return "".join(f"{name}: {val}\r\n" for name, val in headers).encode()
def format_http1_response(
status: int, headers: HeaderIterable, body=b""
) -> bytes:
"""Format a full HTTP/1.1 response.
- If `body` is included, content-length must be specified in headers.
"""
headerbytes = format_http1(headers)
return b"HTTP/1.1 %d %b\r\n%b\r\n%b" % (
status,
STATUS_CODES.get(status, b"UNKNOWN"),
headerbytes,
body,
)

View File

View File

@@ -3,6 +3,7 @@ import signal
import subprocess
import sys
from multiprocessing import Process
from time import sleep
@@ -34,26 +35,101 @@ def _iter_module_files():
def _get_args_for_reloading():
"""Returns the executable."""
rv = [sys.executable]
main_module = sys.modules["__main__"]
mod_spec = getattr(main_module, "__spec__", None)
if sys.argv[0] in ("", "-c"):
raise RuntimeError(
f"Autoreloader cannot work with argv[0]={sys.argv[0]!r}"
)
if mod_spec:
# Parent exe was launched as a module rather than a script
return [sys.executable, "-m", mod_spec.name] + sys.argv[1:]
return [sys.executable] + sys.argv
rv.extend(["-m", mod_spec.name])
if len(sys.argv) > 1:
rv.extend(sys.argv[1:])
else:
rv.extend(sys.argv)
return rv
def restart_with_reloader():
"""Create a new process and a subprocess in it with the same arguments as
this one.
"""
return subprocess.Popen(
_get_args_for_reloading(),
env={**os.environ, "SANIC_SERVER_RUNNING": "true"},
cwd = os.getcwd()
args = _get_args_for_reloading()
new_environ = os.environ.copy()
new_environ["SANIC_SERVER_RUNNING"] = "true"
cmd = " ".join(args)
worker_process = Process(
target=subprocess.call,
args=(cmd,),
kwargs={"cwd": cwd, "shell": True, "env": new_environ},
)
worker_process.start()
return worker_process
def kill_process_children_unix(pid):
"""Find and kill child processes of a process (maximum two level).
:param pid: PID of parent process (process ID)
:return: Nothing
"""
root_process_path = "/proc/{pid}/task/{pid}/children".format(pid=pid)
if not os.path.isfile(root_process_path):
return
with open(root_process_path) as children_list_file:
children_list_pid = children_list_file.read().split()
for child_pid in children_list_pid:
children_proc_path = "/proc/%s/task/%s/children" % (
child_pid,
child_pid,
)
if not os.path.isfile(children_proc_path):
continue
with open(children_proc_path) as children_list_file_2:
children_list_pid_2 = children_list_file_2.read().split()
for _pid in children_list_pid_2:
try:
os.kill(int(_pid), signal.SIGTERM)
except ProcessLookupError:
continue
try:
os.kill(int(child_pid), signal.SIGTERM)
except ProcessLookupError:
continue
def kill_process_children_osx(pid):
"""Find and kill child processes of a process.
:param pid: PID of parent process (process ID)
:return: Nothing
"""
subprocess.run(["pkill", "-P", str(pid)])
def kill_process_children(pid):
"""Find and kill child processes of a process.
:param pid: PID of parent process (process ID)
:return: Nothing
"""
if sys.platform == "darwin":
kill_process_children_osx(pid)
elif sys.platform == "linux":
kill_process_children_unix(pid)
else:
pass # should signal error here
def kill_program_completly(proc):
"""Kill worker and it's child processes and exit.
:param proc: worker process (process ID)
:return: Nothing
"""
kill_process_children(proc.pid)
proc.terminate()
os._exit(0)
def watchdog(sleep_interval):
@@ -62,42 +138,30 @@ def watchdog(sleep_interval):
:param sleep_interval: interval in second.
:return: Nothing
"""
def interrupt_self(*args):
raise KeyboardInterrupt
mtimes = {}
signal.signal(signal.SIGTERM, interrupt_self)
if os.name == "nt":
signal.signal(signal.SIGBREAK, interrupt_self)
worker_process = restart_with_reloader()
signal.signal(
signal.SIGTERM, lambda *args: kill_program_completly(worker_process)
)
signal.signal(
signal.SIGINT, lambda *args: kill_program_completly(worker_process)
)
while True:
for filename in _iter_module_files():
try:
mtime = os.stat(filename).st_mtime
except OSError:
continue
try:
while True:
need_reload = False
for filename in _iter_module_files():
try:
mtime = os.stat(filename).st_mtime
except OSError:
continue
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
elif mtime > old_time:
mtimes[filename] = mtime
need_reload = True
if need_reload:
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
continue
elif mtime > old_time:
kill_process_children(worker_process.pid)
worker_process.terminate()
worker_process.wait()
worker_process = restart_with_reloader()
mtimes[filename] = mtime
break
sleep(sleep_interval)
except KeyboardInterrupt:
pass
finally:
worker_process.terminate()
worker_process.wait()
sleep(sleep_interval)

View File

@@ -1,5 +1,6 @@
import asyncio
import email.utils
import warnings
from collections import defaultdict, namedtuple
from http.cookies import SimpleCookie
@@ -50,19 +51,11 @@ class StreamBuffer:
self._queue = asyncio.Queue(buffer_size)
async def read(self):
"""Stop reading when gets None"""
""" Stop reading when gets None """
payload = await self._queue.get()
self._queue.task_done()
return payload
async def __aiter__(self):
"""Support `async for data in request.stream`"""
while True:
data = await self.read()
if not data:
break
yield data
async def put(self, payload):
await self._queue.put(payload)
@@ -87,7 +80,6 @@ class Request:
"_socket",
"app",
"body",
"conn_info",
"ctx",
"endpoint",
"headers",
@@ -118,7 +110,6 @@ class Request:
# Init but do not inhale
self.body_init()
self.conn_info = None
self.ctx = SimpleNamespace()
self.parsed_forwarded = None
self.parsed_json = None
@@ -132,40 +123,44 @@ class Request:
self.endpoint = None
def __repr__(self):
class_name = self.__class__.__name__
return f"<{class_name}: {self.method} {self.path}>"
return "<{0}: {1} {2}>".format(
self.__class__.__name__, self.method, self.path
)
def get(self, key, default=None):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
return self.ctx.__dict__.get(key, default)
def __contains__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
return key in self.ctx.__dict__
def __getitem__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
return self.ctx.__dict__[key]
def __delitem__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
del self.ctx.__dict__[key]
def __setitem__(self, key, value):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
setattr(self.ctx, key, value)
def body_init(self):
""".. deprecated:: 20.3
To be removed in 21.3"""
self.body = []
def body_push(self, data):
""".. deprecated:: 20.3
To be removed in 21.3"""
self.body.append(data)
def body_finish(self):
""".. deprecated:: 20.3
To be removed in 21.3"""
self.body = b"".join(self.body)
async def receive_body(self):
"""Receive request.body, if not already received.
Streaming handlers may call this to receive the full body.
This is added as a compatibility shim in Sanic 20.3 because future
versions of Sanic will make all requests streaming and will use this
function instead of the non-async body_init/push/finish functions.
Please make an issue if your code depends on the old functionality and
cannot be upgraded to the new API.
"""
if not self.stream:
return
self.body = b"".join([data async for data in self.stream])
@property
def json(self):
if self.parsed_json is None:
@@ -290,6 +285,18 @@ class Request:
args = property(get_args)
@property
def raw_args(self) -> dict:
if self.app.debug: # pragma: no cover
warnings.simplefilter("default")
warnings.warn(
"Use of raw_args will be deprecated in "
"the future versions. Please use args or query_args "
"properties instead",
DeprecationWarning,
)
return {k: v[0] for k, v in self.args.items()}
def get_query_args(
self,
keep_blank_values: bool = False,
@@ -360,55 +367,56 @@ class Request:
self._cookies = {}
return self._cookies
@property
def content_type(self):
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
@property
def match_info(self):
"""return matched info after resolving route"""
return self.app.router.get(self)[2]
# Transport properties (obtained from local interface only)
@property
def ip(self):
"""
:return: peer ip of the socket
"""
return self.conn_info.client if self.conn_info else ""
if not hasattr(self, "_socket"):
self._get_address()
return self._ip
@property
def port(self):
"""
:return: peer port of the socket
"""
return self.conn_info.client_port if self.conn_info else 0
if not hasattr(self, "_socket"):
self._get_address()
return self._port
@property
def socket(self):
return self.conn_info.peername if self.conn_info else (None, None)
if not hasattr(self, "_socket"):
self._get_address()
return self._socket
def _get_address(self):
self._socket = self.transport.get_extra_info("peername") or (
None,
None,
)
self._ip = self._socket[0]
self._port = self._socket[1]
@property
def path(self) -> str:
"""Path of the local HTTP request."""
return self._parsed_url.path.decode("utf-8")
def server_name(self):
"""
Attempt to get the server's external hostname in this order:
`config.SERVER_NAME`, proxied or direct Host headers
:func:`Request.host`
# Proxy properties (using SERVER_NAME/forwarded/request/transport info)
:return: the server name without port number
:rtype: str
"""
server_name = self.app.config.get("SERVER_NAME")
if server_name:
host = server_name.split("//", 1)[-1].split("/", 1)[0]
return parse_host(host)[0]
return parse_host(self.host)[0]
@property
def forwarded(self):
"""
Active proxy information obtained from request headers, as specified in
Sanic configuration.
Field names by, for, proto, host, port and path are normalized.
- for and by IPv6 addresses are bracketed
- port (int) is only set by port headers, not from host.
- path is url-unencoded
Additional values may be available from new style Forwarded headers.
"""
if self.parsed_forwarded is None:
self.parsed_forwarded = (
parse_forwarded(self.headers, self.app.config)
@@ -418,30 +426,50 @@ class Request:
return self.parsed_forwarded
@property
def remote_addr(self) -> str:
def server_port(self):
"""
Client IP address, if available.
1. proxied remote address `self.forwarded['for']`
2. local remote address `self.ip`
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
Attempt to get the server's external port number in this order:
`config.SERVER_NAME`, proxied or direct Host headers
:func:`Request.host`,
actual port used by the transport layer socket.
:return: server port
:rtype: int
"""
if self.forwarded:
return self.forwarded.get("port") or (
80 if self.scheme in ("http", "ws") else 443
)
return (
parse_host(self.host)[1]
or self.transport.get_extra_info("sockname")[1]
)
@property
def remote_addr(self):
"""Attempt to return the original client ip based on `forwarded`,
`x-forwarded-for` or `x-real-ip`. If HTTP headers are unavailable or
untrusted, returns an empty string.
:return: original client ip.
"""
if not hasattr(self, "_remote_addr"):
self._remote_addr = self.forwarded.get("for", "") # or self.ip
self._remote_addr = self.forwarded.get("for", "")
return self._remote_addr
@property
def scheme(self) -> str:
def scheme(self):
"""
Determine request scheme.
1. `config.SERVER_NAME` if in full URL format
2. proxied proto/scheme
3. local connection protocol
Attempt to get the request scheme.
Seeking the value in this order:
`forwarded` header, `x-forwarded-proto` header,
`x-scheme` header, the sanic app itself.
:return: http|https|ws|wss or arbitrary value given by the headers.
:rtype: str
"""
if "//" in self.app.config.get("SERVER_NAME", ""):
return self.app.config.SERVER_NAME.split("//")[0]
if "proto" in self.forwarded:
return self.forwarded["proto"]
forwarded_proto = self.forwarded.get("proto")
if forwarded_proto:
return forwarded_proto
if (
self.app.websocket_enabled
@@ -457,41 +485,25 @@ class Request:
return scheme
@property
def host(self) -> str:
def host(self):
"""
The currently effective server 'host' (hostname or hostname:port).
1. `config.SERVER_NAME` overrides any client headers
2. proxied host of original request
3. request host header
hostname and port may be separated by
`sanic.headers.parse_host(request.host)`.
:return: the first matching host found, or empty string
:return: proxied or direct Host header. Hostname and port number may be
separated by sanic.headers.parse_host(request.host).
"""
server_name = self.app.config.get("SERVER_NAME")
if server_name:
return server_name.split("//", 1)[-1].split("/", 1)[0]
return self.forwarded.get("host") or self.headers.get("host", "")
return self.forwarded.get("host", self.headers.get("Host", ""))
@property
def server_name(self) -> str:
"""The hostname the client connected to, by `request.host`."""
return parse_host(self.host)[0] or ""
def content_type(self):
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
@property
def server_port(self) -> int:
"""
The port the client connected to, by forwarded `port` or
`request.host`.
Default port is returned as 80 and 443 based on `request.scheme`.
"""
port = self.forwarded.get("port") or parse_host(self.host)[1]
return port or (80 if self.scheme in ("http", "ws") else 443)
def match_info(self):
"""return matched info after resolving route"""
return self.app.router.get(self)[2]
@property
def server_path(self) -> str:
"""Full path of current URL. Uses proxied or local path."""
return self.forwarded.get("path") or self.path
def path(self):
return self._parsed_url.path.decode("utf-8")
@property
def query_string(self):
@@ -532,7 +544,7 @@ class Request:
):
netloc = host
else:
netloc = f"{host}:{port}"
netloc = "{}:{}".format(host, port)
return self.app.url_for(
view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs

View File

@@ -3,19 +3,21 @@ from mimetypes import guess_type
from os import path
from urllib.parse import quote_plus
from sanic.compat import Header, open_async
from aiofiles import open as open_async # type: ignore
from sanic.compat import Header
from sanic.cookies import CookieJar
from sanic.headers import format_http1, format_http1_response
from sanic.helpers import has_message_body, remove_entity_headers
from sanic.headers import format_http1
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
try:
from ujson import dumps as json_dumps
except ImportError:
# This is done in order to ensure that the JSON response is
# kept consistent across both ujson and inbuilt json usage.
from json import dumps
# This is done in order to ensure that the JSON response is
# kept consistent across both ujson and inbuilt json usage.
json_dumps = partial(dumps, separators=(",", ":"))
@@ -24,9 +26,12 @@ class BaseHTTPResponse:
self.asgi = False
def _encode_body(self, data):
if data is None:
return b""
return data.encode() if hasattr(data, "encode") else data
try:
# Try to encode it regularly
return data.encode()
except AttributeError:
# Convert it to a str if you can't
return str(data).encode()
def _parse_headers(self):
return format_http1(self.headers.items())
@@ -37,32 +42,6 @@ class BaseHTTPResponse:
self._cookies = CookieJar(self.headers)
return self._cookies
def get_headers(
self,
version="1.1",
keep_alive=False,
keep_alive_timeout=None,
body=b"",
):
""".. deprecated:: 20.3:
This function is not public API and will be removed in 21.3."""
# self.headers get priority over content_type
if self.content_type and "Content-Type" not in self.headers:
self.headers["Content-Type"] = self.content_type
if keep_alive:
self.headers["Connection"] = "keep-alive"
if keep_alive_timeout is not None:
self.headers["Keep-Alive"] = keep_alive_timeout
else:
self.headers["Connection"] = "close"
if self.status in (304, 412):
self.headers = remove_entity_headers(self.headers)
return format_http1_response(self.status, self.headers.items(), body)
class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = (
@@ -80,7 +59,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
streaming_fn,
status=200,
headers=None,
content_type="text/plain; charset=utf-8",
content_type="text/plain",
chunked=True,
):
super().__init__()
@@ -96,9 +75,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
async def write(self, data):
"""Writes a chunk of data to the streaming response.
:param data: str or bytes-ish data to be written.
:param data: bytes-ish data to be written.
"""
data = self._encode_body(data)
if type(data) != bytes:
data = self._encode_body(data)
# `chunked` will always be False in ASGI-mode, even if the underlying
# ASGI Transport implements Chunked transport. That does it itself.
@@ -116,12 +96,12 @@ class StreamingHTTPResponse(BaseHTTPResponse):
"""
if version != "1.1":
self.chunked = False
headers = self.get_headers(
version,
keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout,
)
if not getattr(self, "asgi", False):
headers = self.get_headers(
version,
keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout,
)
await self.protocol.push_data(headers)
await self.protocol.drain()
await self.streaming_fn(self)
@@ -133,11 +113,33 @@ class StreamingHTTPResponse(BaseHTTPResponse):
def get_headers(
self, version="1.1", keep_alive=False, keep_alive_timeout=None
):
# This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python
timeout_header = b""
if keep_alive and keep_alive_timeout is not None:
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
if self.chunked and version == "1.1":
self.headers["Transfer-Encoding"] = "chunked"
self.headers.pop("Content-Length", None)
self.headers["Content-Type"] = self.headers.get(
"Content-Type", self.content_type
)
return super().get_headers(version, keep_alive, keep_alive_timeout)
headers = self._parse_headers()
if self.status == 200:
status = b"OK"
else:
status = STATUS_CODES.get(self.status)
return (b"HTTP/%b %d %b\r\n" b"%b" b"%b\r\n") % (
version.encode(),
self.status,
status,
timeout_header,
headers,
)
class HTTPResponse(BaseHTTPResponse):
@@ -149,16 +151,28 @@ class HTTPResponse(BaseHTTPResponse):
status=200,
headers=None,
content_type=None,
body_bytes=b"",
):
super().__init__()
self.content_type = content_type
self.body = self._encode_body(body)
if body is not None:
self.body = self._encode_body(body)
else:
self.body = body_bytes
self.status = status
self.headers = Header(headers or {})
self._cookies = None
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
# This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python
timeout_header = b""
if keep_alive and keep_alive_timeout is not None:
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
body = b""
if has_message_body(self.status):
body = self.body
@@ -166,7 +180,31 @@ class HTTPResponse(BaseHTTPResponse):
"Content-Length", len(self.body)
)
return self.get_headers(version, keep_alive, keep_alive_timeout, body)
# self.headers get priority over content_type
if self.content_type and "Content-Type" not in self.headers:
self.headers["Content-Type"] = self.content_type
if self.status in (304, 412):
self.headers = remove_entity_headers(self.headers)
headers = self._parse_headers()
if self.status == 200:
status = b"OK"
else:
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
return (
b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b"
) % (
version.encode(),
self.status,
status,
b"keep-alive" if keep_alive else b"close",
timeout_header,
headers,
body,
)
@property
def cookies(self):
@@ -182,7 +220,7 @@ def empty(status=204, headers=None):
:param status Response code.
:param headers Custom Headers.
"""
return HTTPResponse(body=b"", status=status, headers=headers)
return HTTPResponse(body_bytes=b"", status=status, headers=headers)
def json(
@@ -191,7 +229,7 @@ def json(
headers=None,
content_type="application/json",
dumps=json_dumps,
**kwargs,
**kwargs
):
"""
Returns response object with body in json format.
@@ -220,11 +258,6 @@ def text(
:param headers: Custom Headers.
:param content_type: the content type (string) of the response
"""
if not isinstance(body, str):
raise TypeError(
f"Bad body type. Expected str, got {type(body).__name__})"
)
return HTTPResponse(
body, status=status, headers=headers, content_type=content_type
)
@@ -242,7 +275,7 @@ def raw(
:param content_type: the content type (string) of the response.
"""
return HTTPResponse(
body=body,
body_bytes=body,
status=status,
headers=headers,
content_type=content_type,
@@ -253,14 +286,10 @@ def html(body, status=200, headers=None):
"""
Returns response object with body in html format.
:param body: str or bytes-ish, or an object with __html__ or _repr_html_.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
if hasattr(body, "__html__"):
body = body.__html__()
elif hasattr(body, "_repr_html_"):
body = body._repr_html_()
return HTTPResponse(
body,
status=status,
@@ -288,27 +317,29 @@ async def file(
headers = headers or {}
if filename:
headers.setdefault(
"Content-Disposition", f'attachment; filename="{filename}"'
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
)
filename = filename or path.split(location)[-1]
async with await open_async(location, mode="rb") as f:
async with open_async(location, mode="rb") as _file:
if _range:
await f.seek(_range.start)
out_stream = await f.read(_range.size)
headers[
"Content-Range"
] = f"bytes {_range.start}-{_range.end}/{_range.total}"
await _file.seek(_range.start)
out_stream = await _file.read(_range.size)
headers["Content-Range"] = "bytes %s-%s/%s" % (
_range.start,
_range.end,
_range.total,
)
status = 206
else:
out_stream = await f.read()
out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
return HTTPResponse(
body=out_stream,
status=status,
headers=headers,
content_type=mime_type,
body_bytes=out_stream,
)
@@ -335,36 +366,43 @@ async def file_stream(
headers = headers or {}
if filename:
headers.setdefault(
"Content-Disposition", f'attachment; filename="{filename}"'
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
)
filename = filename or path.split(location)[-1]
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
if _range:
start = _range.start
end = _range.end
total = _range.total
headers["Content-Range"] = f"bytes {start}-{end}/{total}"
status = 206
_file = await open_async(location, mode="rb")
async def _streaming_fn(response):
async with await open_async(location, mode="rb") as f:
nonlocal _file, chunk_size
try:
if _range:
await f.seek(_range.start)
chunk_size = min((_range.size, chunk_size))
await _file.seek(_range.start)
to_send = _range.size
while to_send > 0:
content = await f.read(min((_range.size, chunk_size)))
content = await _file.read(chunk_size)
if len(content) < 1:
break
to_send -= len(content)
await response.write(content)
else:
while True:
content = await f.read(chunk_size)
content = await _file.read(chunk_size)
if len(content) < 1:
break
await response.write(content)
finally:
await _file.close()
return # Returning from this fn closes the stream
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
if _range:
headers["Content-Range"] = "bytes %s-%s/%s" % (
_range.start,
_range.end,
_range.total,
)
status = 206
return StreamingHTTPResponse(
streaming_fn=_streaming_fn,
status=status,

View File

@@ -11,16 +11,7 @@ from sanic.views import CompositionView
Route = namedtuple(
"Route",
[
"handler",
"methods",
"pattern",
"parameters",
"name",
"uri",
"endpoint",
],
"Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
)
Parameter = namedtuple("Parameter", ["name", "cast"])
@@ -88,8 +79,7 @@ class Router:
routes_always_check = None
parameter_pattern = re.compile(r"<(.+?)>")
def __init__(self, app):
self.app = app
def __init__(self):
self.routes_all = {}
self.routes_names = {}
self.routes_static_files = {}
@@ -119,7 +109,7 @@ class Router:
name, pattern = parameter_string.split(":", 1)
if not name:
raise ValueError(
f"Invalid parameter syntax: {parameter_string}"
"Invalid parameter syntax: {}".format(parameter_string)
)
default = (str, pattern)
@@ -153,7 +143,7 @@ class Router:
routes = []
if version is not None:
version = re.escape(str(version).strip("/").lstrip("v"))
uri = "/".join([f"/v{version}", uri.lstrip("/")])
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
# add regular version
routes.append(self._add(uri, methods, handler, host, name))
@@ -213,8 +203,8 @@ class Router:
else:
if not isinstance(host, Iterable):
raise ValueError(
f"Expected either string or Iterable of "
f"host strings, not {host!r}"
"Expected either string or Iterable of "
"host strings, not {!r}".format(host)
)
for host_ in host:
@@ -235,7 +225,8 @@ class Router:
if name in parameter_names:
raise ParameterNameConflicts(
f"Multiple parameter named <{name}> " f"in route uri {uri}"
"Multiple parameter named <{name}> "
"in route uri {uri}".format(name=name, uri=uri)
)
parameter_names.add(name)
@@ -249,23 +240,23 @@ class Router:
elif re.search(r"/", pattern):
properties["unhashable"] = True
return f"({pattern})"
return "({})".format(pattern)
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
pattern = re.compile(fr"^{pattern_string}$")
pattern = re.compile(r"^{}$".format(pattern_string))
def merge_route(route, methods, handler):
# merge to the existing route when possible.
if not route.methods or not methods:
# method-unspecified routes are not mergeable.
raise RouteExists(f"Route already registered: {uri}")
raise RouteExists("Route already registered: {}".format(uri))
elif route.methods.intersection(methods):
# already existing method is not overloadable.
duplicated = methods.intersection(route.methods)
duplicated_methods = ",".join(list(duplicated))
raise RouteExists(
f"Route already registered: {uri} [{duplicated_methods}]"
"Route already registered: {} [{}]".format(
uri, ",".join(list(duplicated))
)
)
if isinstance(route.handler, CompositionView):
view = route.handler
@@ -305,19 +296,15 @@ class Router:
name = name.split("_static_", 1)[-1]
if hasattr(handler, "__blueprintname__"):
bp_name = handler.__blueprintname__
handler_name = f"{bp_name}.{name or handler.__name__}"
else:
handler_name = name or getattr(
handler, "__name__", handler.__class__.__name__
handler_name = "{}.{}".format(
handler.__blueprintname__, name or handler.__name__
)
else:
handler_name = name or getattr(handler, "__name__", None)
if route:
route = merge_route(route, methods, handler)
else:
endpoint = self.app._build_endpoint_name(handler_name)
route = Route(
handler=handler,
methods=methods,
@@ -325,7 +312,6 @@ class Router:
parameters=parameters,
name=handler_name,
uri=uri,
endpoint=endpoint,
)
self.routes_all[uri] = route
@@ -366,6 +352,37 @@ class Router:
else:
return -1, None
def remove(self, uri, clean_cache=True, host=None):
if host is not None:
uri = host + uri
try:
route = self.routes_all.pop(uri)
for handler_name, pairs in self.routes_names.items():
if pairs[0] == uri:
self.routes_names.pop(handler_name)
break
for handler_name, pairs in self.routes_static_files.items():
if pairs[0] == uri:
self.routes_static_files.pop(handler_name)
break
except KeyError:
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
if route in self.routes_always_check:
self.routes_always_check.remove(route)
elif (
url_hash(uri) in self.routes_dynamic
and route in self.routes_dynamic[url_hash(uri)]
):
self.routes_dynamic[url_hash(uri)].remove(route)
else:
self.routes_static.pop(uri)
if clean_cache:
self._get.cache_clear()
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def find_route_by_view_name(self, view_name, name=None):
"""Find a route in the router based on the specified view name.
@@ -425,7 +442,7 @@ class Router:
# Check against known static routes
route = self.routes_static.get(url)
method_not_supported = MethodNotSupported(
f"Method {method} not allowed for URL {url}",
"Method {} not allowed for URL {}".format(method, url),
method=method,
allowed_methods=self.get_supported_methods(url),
)
@@ -455,7 +472,7 @@ class Router:
# Route was found but the methods didn't match
if route_found:
raise method_not_supported
raise NotFound(f"Requested URL {url} not found")
raise NotFound("Requested URL {} not found".format(url))
kwargs = {
p.name: p.cast(value)
@@ -464,8 +481,7 @@ class Router:
route_handler = route.handler
if hasattr(route_handler, "handlers"):
route_handler = route_handler.handlers[method]
return route_handler, [], kwargs, route.uri, route.name, route.endpoint
return route_handler, [], kwargs, route.uri, route.name
def is_stream_handler(self, request):
"""Handler for request is stream or not.

View File

@@ -1,26 +1,20 @@
import asyncio
import multiprocessing
import os
import secrets
import socket
import stat
import sys
import traceback
from collections import deque
from functools import partial
from inspect import isawaitable
from ipaddress import ip_address
from multiprocessing import Process
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from socket import SO_REUSEADDR, SOL_SOCKET, socket
from time import time
from typing import Dict, Type, Union
from httptools import HttpRequestParser # type: ignore
from httptools.parser.errors import HttpParserError # type: ignore
from sanic.compat import Header, ctrlc_workaround_for_windows
from sanic.config import Config
from sanic.compat import Header
from sanic.exceptions import (
HeaderExpectationFailed,
InvalidUsage,
@@ -42,48 +36,11 @@ try:
except ImportError:
pass
OS_IS_WINDOWS = os.name == "nt"
class Signal:
stopped = False
class ConnInfo:
"""Local and remote addresses and SSL status info."""
__slots__ = (
"sockname",
"peername",
"server",
"server_port",
"client",
"client_port",
"ssl",
)
def __init__(self, transport, unix=None):
self.ssl = bool(transport.get_extra_info("sslcontext"))
self.server = self.client = ""
self.server_port = self.client_port = 0
self.peername = None
self.sockname = addr = transport.get_extra_info("sockname")
if isinstance(addr, str): # UNIX socket
self.server = unix or addr
return
# IPv4 (ip, port) or IPv6 (ip, port, flowinfo, scopeid)
if isinstance(addr, tuple):
self.server = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
self.server_port = addr[1]
# self.server gets non-standard port appended
if addr[1] != (443 if self.ssl else 80):
self.server = f"{self.server}:{addr[1]}"
self.peername = addr = transport.get_extra_info("peername")
if isinstance(addr, tuple):
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
self.client_port = addr[1]
class HttpProtocol(asyncio.Protocol):
"""
This class provides a basic HTTP implementation of the sanic framework.
@@ -97,7 +54,6 @@ class HttpProtocol(asyncio.Protocol):
"transport",
"connections",
"signal",
"conn_info",
# request params
"parser",
"request",
@@ -112,6 +68,7 @@ class HttpProtocol(asyncio.Protocol):
"request_buffer_queue_size",
"request_class",
"is_request_stream",
"router",
"error_handler",
# enable or disable access log purpose
"access_log",
@@ -129,7 +86,7 @@ class HttpProtocol(asyncio.Protocol):
"_keep_alive",
"_header_fragment",
"state",
"_unix",
"_debug",
"_body_chunks",
)
@@ -138,38 +95,46 @@ class HttpProtocol(asyncio.Protocol):
*,
loop,
app,
request_handler,
error_handler,
signal=Signal(),
connections=None,
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
request_max_size=None,
request_buffer_queue_size=100,
request_class=None,
access_log=True,
keep_alive=True,
is_request_stream=False,
router=None,
state=None,
unix=None,
**kwargs,
debug=False,
**kwargs
):
asyncio.set_event_loop(loop)
self.loop = loop
deprecated_loop = self.loop if sys.version_info < (3, 7) else None
self.app = app
self.transport = None
self.conn_info = None
self.request = None
self.parser = None
self.url = None
self.headers = None
self.router = router
self.signal = signal
self.access_log = self.app.config.ACCESS_LOG
self.access_log = access_log
self.connections = connections if connections is not None else set()
self.request_handler = self.app.handle_request
self.error_handler = self.app.error_handler
self.request_timeout = self.app.config.REQUEST_TIMEOUT
self.request_buffer_queue_size = (
self.app.config.REQUEST_BUFFER_QUEUE_SIZE
)
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
self.request_class = self.app.request_class or Request
self.is_request_stream = self.app.is_request_stream
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
self.request_buffer_queue_size = request_buffer_queue_size
self.response_timeout = response_timeout
self.keep_alive_timeout = keep_alive_timeout
self.request_max_size = request_max_size
self.request_class = request_class or Request
self.is_request_stream = is_request_stream
self._is_stream_handler = False
self._not_paused = asyncio.Event(loop=deprecated_loop)
self._not_paused = asyncio.Event(loop=loop)
self._total_request_size = 0
self._request_timeout_handler = None
self._response_timeout_handler = None
@@ -178,12 +143,12 @@ class HttpProtocol(asyncio.Protocol):
self._last_response_time = None
self._request_handler_task = None
self._request_stream_task = None
self._keep_alive = self.app.config.KEEP_ALIVE
self._keep_alive = keep_alive
self._header_fragment = b""
self.state = state if state else {}
if "requests_count" not in self.state:
self.state["requests_count"] = 0
self._unix = unix
self._debug = debug
self._not_paused.set()
self._body_chunks = deque()
@@ -212,7 +177,6 @@ class HttpProtocol(asyncio.Protocol):
self.request_timeout, self.request_timeout_callback
)
self.transport = transport
self.conn_info = ConnInfo(transport, unix=self._unix)
self._last_request_time = time()
def connection_lost(self, exc):
@@ -312,7 +276,7 @@ class HttpProtocol(asyncio.Protocol):
self.parser.feed_data(data)
except HttpParserError:
message = "Bad Request"
if self.app.debug:
if self._debug:
message += "\n" + traceback.format_exc()
self.write_error(InvalidUsage(message))
@@ -350,7 +314,6 @@ class HttpProtocol(asyncio.Protocol):
transport=self.transport,
app=self.app,
)
self.request.conn_info = self.conn_info
# Remove any existing KeepAlive handler here,
# It will be recreated if required on the new request.
if self._keep_alive_timeout_handler:
@@ -361,7 +324,7 @@ class HttpProtocol(asyncio.Protocol):
self.expect_handler()
if self.is_request_stream:
self._is_stream_handler = self.app.router.is_stream_handler(
self._is_stream_handler = self.router.is_stream_handler(
self.request
)
if self._is_stream_handler:
@@ -380,7 +343,9 @@ class HttpProtocol(asyncio.Protocol):
self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
else:
self.write_error(
HeaderExpectationFailed(f"Unknown Expect: {expect}")
HeaderExpectationFailed(
"Unknown Expect: {expect}".format(expect=expect)
)
)
def on_body(self, body):
@@ -418,13 +383,12 @@ class HttpProtocol(asyncio.Protocol):
async def stream_append(self):
while self._body_chunks:
body = self._body_chunks.popleft()
if self.request:
if self.request.stream.is_full():
self.transport.pause_reading()
await self.request.stream.put(body)
self.transport.resume_reading()
else:
await self.request.stream.put(body)
if self.request.stream.is_full():
self.transport.pause_reading()
await self.request.stream.put(body)
self.transport.resume_reading()
else:
await self.request.stream.put(body)
def on_message_complete(self):
# Entire request (headers and whole body) is received.
@@ -488,9 +452,13 @@ class HttpProtocol(asyncio.Protocol):
extra["host"] = "UNKNOWN"
if self.request is not None:
if self.request.ip:
extra["host"] = f"{self.request.ip}:{self.request.port}"
extra["host"] = "{0}:{1}".format(
self.request.ip, self.request.port
)
extra["request"] = f"{self.request.method} {self.request.url}"
extra["request"] = "{0} {1}".format(
self.request.method, self.request.url
)
else:
extra["request"] = "nil"
@@ -520,14 +488,16 @@ class HttpProtocol(asyncio.Protocol):
)
self.write_error(ServerError("Invalid response type"))
except RuntimeError:
if self.app.debug:
if self._debug:
logger.error(
"Connection lost before response written @ %s",
self.request.ip,
)
keep_alive = False
except Exception as e:
self.bail_out(f"Writing response failed, connection closed {e!r}")
self.bail_out(
"Writing response failed, connection closed {}".format(repr(e))
)
finally:
if not keep_alive:
self.transport.close()
@@ -571,14 +541,16 @@ class HttpProtocol(asyncio.Protocol):
)
self.write_error(ServerError("Invalid response type"))
except RuntimeError:
if self.app.debug:
if self._debug:
logger.error(
"Connection lost before response written @ %s",
self.request.ip,
)
keep_alive = False
except Exception as e:
self.bail_out(f"Writing response failed, connection closed {e!r}")
self.bail_out(
"Writing response failed, connection closed {}".format(repr(e))
)
finally:
if not keep_alive:
self.transport.close()
@@ -602,14 +574,14 @@ class HttpProtocol(asyncio.Protocol):
version = self.request.version if self.request else "1.1"
self.transport.write(response.output(version))
except RuntimeError:
if self.app.debug:
if self._debug:
logger.error(
"Connection lost before error written @ %s",
self.request.ip if self.request else "Unknown",
)
except Exception as e:
self.bail_out(
f"Writing error failed, connection closed {e!r}",
"Writing error failed, connection closed {}".format(repr(e)),
from_error=True,
)
finally:
@@ -670,7 +642,7 @@ class HttpProtocol(asyncio.Protocol):
:return: boolean - True if closed, false if staying open
"""
if not self.parser and self.transport is not None:
if not self.parser:
self.transport.close()
return True
return False
@@ -792,13 +764,20 @@ def serve(
host,
port,
app,
request_handler,
error_handler,
before_start=None,
after_start=None,
before_stop=None,
after_stop=None,
debug=False,
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
ssl=None,
sock=None,
unix=None,
request_max_size=None,
request_buffer_queue_size=100,
reuse_port=False,
loop=None,
protocol=HttpProtocol,
@@ -808,13 +787,25 @@ def serve(
run_async=False,
connections=None,
signal=Signal(),
request_class=None,
access_log=True,
keep_alive=True,
is_request_stream=False,
router=None,
websocket_max_size=None,
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16,
state=None,
graceful_shutdown_timeout=15.0,
asyncio_server_kwargs=None,
):
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
:param port: Port to host on
:param request_handler: Sanic request handler with middleware
:param error_handler: Sanic error handler with middleware
:param before_start: function to be executed before the server starts
listening. Takes arguments `app` instance and `loop`
:param after_start: function to be executed after the server starts
@@ -825,13 +816,35 @@ def serve(
:param after_stop: function to be executed when a stop signal is
received after it is respected. Takes arguments
`app` instance and `loop`
:param debug: enables debug output (slows server)
:param request_timeout: time in seconds
:param response_timeout: time in seconds
:param keep_alive_timeout: time in seconds
:param ssl: SSLContext
:param sock: Socket for the server to accept connections from
:param unix: Unix socket to listen on instead of TCP port
:param request_max_size: size in bytes, `None` for no limit
:param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop
:param protocol: subclass of asyncio protocol class
:param run_async: bool: Do not create a new event loop for the server,
and return an AsyncServer object rather than running it
:param request_class: Request class to use
:param access_log: disable/enable access log
:param websocket_max_size: enforces the maximum size for
incoming messages in bytes.
:param websocket_max_queue: sets the maximum length of the queue
that holds incoming messages.
:param websocket_read_limit: sets the high-water limit of the buffer for
incoming bytes, the low-water limit is half
the high-water limit.
:param websocket_write_limit: sets the high-water limit of the buffer for
outgoing bytes, the low-water limit is a
quarter of the high-water limit.
:param is_request_stream: disable/enable Request.stream
:param request_buffer_queue_size: streaming request buffer queue size
:param router: Router object
:param graceful_shutdown_timeout: How long take to Force close non-idle
connection
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
create_server method
:return: Nothing
@@ -841,48 +854,59 @@ def serve(
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if app.debug:
loop.set_debug(app.debug)
if debug:
loop.set_debug(debug)
app.asgi = False
connections = connections if connections is not None else set()
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
server = partial(
protocol,
loop=loop,
connections=connections,
signal=signal,
app=app,
request_handler=request_handler,
error_handler=error_handler,
request_timeout=request_timeout,
response_timeout=response_timeout,
keep_alive_timeout=keep_alive_timeout,
request_max_size=request_max_size,
request_buffer_queue_size=request_buffer_queue_size,
request_class=request_class,
access_log=access_log,
keep_alive=keep_alive,
is_request_stream=is_request_stream,
router=router,
websocket_max_size=websocket_max_size,
websocket_max_queue=websocket_max_queue,
websocket_read_limit=websocket_read_limit,
websocket_write_limit=websocket_write_limit,
state=state,
unix=unix,
**protocol_kwargs,
debug=debug,
)
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
)
# UNIX sockets are always bound by us (to preserve semantics between modes)
if unix:
sock = bind_unix_socket(unix, backlog=backlog)
server_coroutine = loop.create_server(
server,
None if sock else host,
None if sock else port,
host,
port,
ssl=ssl,
reuse_port=reuse_port,
sock=sock,
backlog=backlog,
**asyncio_server_kwargs,
**asyncio_server_kwargs
)
if run_async:
return AsyncioServer(
loop=loop,
serve_coro=server_coroutine,
connections=connections,
after_start=after_start,
before_stop=before_stop,
after_stop=after_stop,
loop,
server_coroutine,
connections,
after_start,
before_stop,
after_stop,
)
trigger_events(before_start, loop)
@@ -901,11 +925,15 @@ def serve(
# Register signals for graceful termination
if register_sys_signals:
if OS_IS_WINDOWS:
ctrlc_workaround_for_windows(app)
else:
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
loop.add_signal_handler(_signal, app.stop)
_singals = (SIGTERM,) if run_multiple else (SIGINT, SIGTERM)
for _signal in _singals:
try:
loop.add_signal_handler(_signal, loop.stop)
except NotImplementedError:
logger.warning(
"Sanic tried to use loop.add_signal_handler "
"but it is not implemented on this platform."
)
pid = os.getpid()
try:
logger.info("Starting worker [%s]", pid)
@@ -929,9 +957,8 @@ def serve(
# We should provide graceful_shutdown_timeout,
# instead of letting connection hangs forever.
# Let's roughly calcucate time.
graceful = app.config.GRACEFUL_SHUTDOWN_TIMEOUT
start_shutdown = 0
while connections and (start_shutdown < graceful):
while connections and (start_shutdown < graceful_shutdown_timeout):
loop.run_until_complete(asyncio.sleep(0.1))
start_shutdown = start_shutdown + 0.1
@@ -944,106 +971,12 @@ def serve(
else:
conn.close()
_shutdown = asyncio.gather(*coros)
_shutdown = asyncio.gather(*coros, loop=loop)
loop.run_until_complete(_shutdown)
trigger_events(after_stop, loop)
loop.close()
remove_unix_socket(unix)
def _build_protocol_kwargs(
protocol: Type[HttpProtocol], config: Config
) -> Dict[str, Union[int, float]]:
if hasattr(protocol, "websocket_handshake"):
return {
"websocket_max_size": config.WEBSOCKET_MAX_SIZE,
"websocket_max_queue": config.WEBSOCKET_MAX_QUEUE,
"websocket_read_limit": config.WEBSOCKET_READ_LIMIT,
"websocket_write_limit": config.WEBSOCKET_WRITE_LIMIT,
"websocket_ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
"websocket_ping_interval": config.WEBSOCKET_PING_INTERVAL,
}
return {}
def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
"""Create TCP server socket.
:param host: IPv4, IPv6 or hostname may be specified
:param port: TCP port number
:param backlog: Maximum number of connections to queue
:return: socket.socket object
"""
try: # IP address: family must be specified for IPv6 at least
ip = ip_address(host)
host = str(ip)
sock = socket.socket(
socket.AF_INET6 if ip.version == 6 else socket.AF_INET
)
except ValueError: # Hostname, may become AF_INET or AF_INET6
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
sock.listen(backlog)
return sock
def bind_unix_socket(path: str, *, mode=0o666, backlog=100) -> socket.socket:
"""Create unix socket.
:param path: filesystem path
:param backlog: Maximum number of connections to queue
:return: socket.socket object
"""
"""Open or atomically replace existing socket with zero downtime."""
# Sanitise and pre-verify socket path
path = os.path.abspath(path)
folder = os.path.dirname(path)
if not os.path.isdir(folder):
raise FileNotFoundError(f"Socket folder does not exist: {folder}")
try:
if not stat.S_ISSOCK(os.stat(path, follow_symlinks=False).st_mode):
raise FileExistsError(f"Existing file is not a socket: {path}")
except FileNotFoundError:
pass
# Create new socket with a random temporary name
tmp_path = f"{path}.{secrets.token_urlsafe()}"
sock = socket.socket(socket.AF_UNIX)
try:
# Critical section begins (filename races)
sock.bind(tmp_path)
try:
os.chmod(tmp_path, mode)
# Start listening before rename to avoid connection failures
sock.listen(backlog)
os.rename(tmp_path, path)
except: # noqa: E722
try:
os.unlink(tmp_path)
finally:
raise
except: # noqa: E722
try:
sock.close()
finally:
raise
return sock
def remove_unix_socket(path: str) -> None:
"""Remove dead unix socket during server exit."""
if not path:
return
try:
if stat.S_ISSOCK(os.stat(path, follow_symlinks=False).st_mode):
# Is it actually dead (doesn't belong to a new server instance)?
with socket.socket(socket.AF_UNIX) as testsock:
try:
testsock.connect(path)
except ConnectionRefusedError:
os.unlink(path)
except FileNotFoundError:
pass
def serve_multiple(server_settings, workers):
@@ -1058,17 +991,11 @@ def serve_multiple(server_settings, workers):
server_settings["reuse_port"] = True
server_settings["run_multiple"] = True
# Create a listening socket or use the one in settings
sock = server_settings.get("sock")
unix = server_settings["unix"]
backlog = server_settings["backlog"]
if unix:
sock = bind_unix_socket(unix, backlog=backlog)
server_settings["unix"] = unix
if sock is None:
sock = bind_socket(
server_settings["host"], server_settings["port"], backlog=backlog
)
# Handling when custom socket is not provided.
if server_settings.get("sock") is None:
sock = socket()
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind((server_settings["host"], server_settings["port"]))
sock.set_inheritable(True)
server_settings["sock"] = sock
server_settings["host"] = None
@@ -1083,10 +1010,9 @@ def serve_multiple(server_settings, workers):
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
mp = multiprocessing.get_context("fork")
for _ in range(workers):
process = mp.Process(target=serve, kwargs=server_settings)
process = Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start()
processes.append(process)
@@ -1097,6 +1023,4 @@ def serve_multiple(server_settings, workers):
# the above processes will block this until they're stopped
for process in processes:
process.terminate()
sock.close()
remove_unix_socket(unix)
server_settings.get("sock").close()

View File

@@ -1,11 +1,11 @@
from functools import partial, wraps
from mimetypes import guess_type
from os import path
from re import sub
from time import gmtime, strftime
from urllib.parse import unquote
from sanic.compat import stat_async
from aiofiles.os import stat # type: ignore
from sanic.exceptions import (
ContentRangeError,
FileNotFound,
@@ -13,101 +13,9 @@ from sanic.exceptions import (
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler
from sanic.log import error_logger
from sanic.response import HTTPResponse, file, file_stream
async def _static_request_handler(
file_or_directory,
use_modified_since,
use_content_range,
stream_large_files,
request,
content_type=None,
file_uri=None,
):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if file_uri and "../" in file_uri:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
root_path = file_path = file_or_directory
if file_uri:
file_path = path.join(file_or_directory, sub("^[/]*", "", file_uri))
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))):
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={file_uri}"
)
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
try:
headers = {}
# Check if the client has been sent this file before
# and it has not been modified since
stats = None
if use_modified_since:
stats = await stat_async(file_path)
modified_since = strftime(
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
)
if request.headers.get("If-Modified-Since") == modified_since:
return HTTPResponse(status=304)
headers["Last-Modified"] = modified_since
_range = None
if use_content_range:
_range = None
if not stats:
stats = await stat_async(file_path)
headers["Accept-Ranges"] = "bytes"
headers["Content-Length"] = str(stats.st_size)
if request.method != "HEAD":
try:
_range = ContentRangeHandler(request, stats)
except HeaderNotFound:
pass
else:
del headers["Content-Length"]
for key, value in _range.headers.items():
headers[key] = value
headers["Content-Type"] = (
content_type or guess_type(file_path)[0] or "text/plain"
)
if request.method == "HEAD":
return HTTPResponse(headers=headers)
else:
if stream_large_files:
if type(stream_large_files) == int:
threshold = stream_large_files
else:
threshold = 1024 * 1024
if not stats:
stats = await stat_async(file_path)
if stats.st_size >= threshold:
return await file_stream(
file_path, headers=headers, _range=_range
)
return await file(file_path, headers=headers, _range=_range)
except ContentRangeError:
raise
except Exception:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={file_uri}"
)
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
def register(
app,
uri,
@@ -143,34 +51,96 @@ def register(
threshold size to switch to file_stream()
:param name: user defined name used for url_for
:param content_type: user defined content type for header
:return: registered static routes
:rtype: List[sanic.router.Route]
"""
# If we're not trying to match a file directly,
# serve from the folder
if not path.isfile(file_or_directory):
uri += "<file_uri:" + pattern + ">"
async def _handler(request, file_uri=None):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if file_uri and "../" in file_uri:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
root_path = file_path = file_or_directory
if file_uri:
file_path = path.join(
file_or_directory, sub("^[/]*", "", file_uri)
)
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))):
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
try:
headers = {}
# Check if the client has been sent this file before
# and it has not been modified since
stats = None
if use_modified_since:
stats = await stat(file_path)
modified_since = strftime(
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
)
if request.headers.get("If-Modified-Since") == modified_since:
return HTTPResponse(status=304)
headers["Last-Modified"] = modified_since
_range = None
if use_content_range:
_range = None
if not stats:
stats = await stat(file_path)
headers["Accept-Ranges"] = "bytes"
headers["Content-Length"] = str(stats.st_size)
if request.method != "HEAD":
try:
_range = ContentRangeHandler(request, stats)
except HeaderNotFound:
pass
else:
del headers["Content-Length"]
for key, value in _range.headers.items():
headers[key] = value
headers["Content-Type"] = (
content_type or guess_type(file_path)[0] or "text/plain"
)
if request.method == "HEAD":
return HTTPResponse(headers=headers)
else:
if stream_large_files:
if type(stream_large_files) == int:
threshold = stream_large_files
else:
threshold = 1024 * 1024
if not stats:
stats = await stat(file_path)
if stats.st_size >= threshold:
return await file_stream(
file_path, headers=headers, _range=_range
)
return await file(file_path, headers=headers, _range=_range)
except ContentRangeError:
raise
except Exception:
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
# special prefix for static files
if not name.startswith("_static_"):
name = f"_static_{name}"
name = "_static_{}".format(name)
_handler = wraps(_static_request_handler)(
partial(
_static_request_handler,
file_or_directory,
use_modified_since,
use_content_range,
stream_large_files,
content_type=content_type,
)
)
_routes, _ = app.route(
app.route(
uri,
methods=["GET", "HEAD"],
name=name,
host=host,
strict_slashes=strict_slashes,
)(_handler)
return _routes

View File

@@ -11,10 +11,8 @@ from sanic.response import text
ASGI_HOST = "mockserver"
ASGI_PORT = 1234
ASGI_BASE_URL = f"http://{ASGI_HOST}:{ASGI_PORT}"
HOST = "127.0.0.1"
PORT = None
PORT = 42101
class SanicTestClient:
@@ -24,16 +22,8 @@ class SanicTestClient:
self.port = port
self.host = host
@app.listener("after_server_start")
def _start_test_mode(sanic, *args, **kwargs):
sanic.test_mode = True
@app.listener("before_server_end")
def _end_test_mode(sanic, *args, **kwargs):
sanic.test_mode = False
def get_new_session(self):
return httpx.AsyncClient(verify=False)
return httpx.Client()
async def _local_request(self, method, url, *args, **kwargs):
logger.info(url)
@@ -48,22 +38,20 @@ class SanicTestClient:
try:
response = await getattr(session, method.lower())(
url, *args, **kwargs
url, verify=False, *args, **kwargs
)
except NameError:
raise Exception(response.status_code)
response.body = await response.aread()
response.status = response.status_code
response.content_type = response.headers.get("content-type")
# response can be decoded as json after response._content
# is set by response.aread()
try:
response.json = response.json()
except (JSONDecodeError, UnicodeDecodeError):
response.json = None
response.body = await response.read()
response.status = response.status_code
response.content_type = response.headers.get("content-type")
if raw_cookies:
response.raw_cookies = {}
@@ -105,9 +93,7 @@ class SanicTestClient:
if self.port:
server_kwargs = dict(
host=host or self.host,
port=self.port,
**server_kwargs,
host=host or self.host, port=self.port, **server_kwargs
)
host, port = host or self.host, self.port
else:
@@ -115,19 +101,17 @@ class SanicTestClient:
sock.bind((host or self.host, 0))
server_kwargs = dict(sock=sock, **server_kwargs)
host, port = sock.getsockname()
self.port = port
if uri.startswith(
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
):
url = uri
else:
uri = uri if uri.startswith("/") else f"/{uri}"
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
scheme = "ws" if method == "websocket" else "http"
url = f"{scheme}://{host}:{port}{uri}"
# Tests construct URLs using PORT = None, which means random port not
# known until this function is called, so fix that here
url = url.replace(":None/", f":{port}/")
url = "{scheme}://{host}:{port}{uri}".format(
scheme=scheme, host=host, port=port, uri=uri
)
@self.app.listener("after_server_start")
async def _collect_response(sanic, loop):
@@ -145,7 +129,7 @@ class SanicTestClient:
self.app.listeners["after_server_start"].pop()
if exceptions:
raise ValueError(f"Exception during request: {exceptions}")
raise ValueError("Exception during request: {}".format(exceptions))
if gather_request:
try:
@@ -153,13 +137,17 @@ class SanicTestClient:
return request, response
except BaseException: # noqa
raise ValueError(
f"Request and response object expected, got ({results})"
"Request and response object expected, got ({})".format(
results
)
)
else:
try:
return results[-1]
except BaseException: # noqa
raise ValueError(f"Request object expected, got ({results})")
raise ValueError(
"Request object expected, got ({})".format(results)
)
def get(self, *args, **kwargs):
return self._sanic_endpoint_test("get", *args, **kwargs)
@@ -197,33 +185,30 @@ async def app_call_with_return(self, scope, receive, send):
return await asgi_app()
class SanicASGITestClient(httpx.AsyncClient):
class SanicASGIDispatch(httpx.dispatch.ASGIDispatch):
pass
class SanicASGITestClient(httpx.Client):
def __init__(
self,
app,
base_url: str = ASGI_BASE_URL,
base_url: str = "http://{}".format(ASGI_HOST),
suppress_exceptions: bool = False,
) -> None:
app.__class__.__call__ = app_call_with_return
app.asgi = True
self.app = app
transport = httpx.ASGITransport(app=app, client=(ASGI_HOST, ASGI_PORT))
super().__init__(transport=transport, base_url=base_url)
dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT))
super().__init__(dispatch=dispatch, base_url=base_url)
self.last_request = None
def _collect_request(request):
self.last_request = request
@app.listener("after_server_start")
def _start_test_mode(sanic, *args, **kwargs):
sanic.test_mode = True
@app.listener("before_server_end")
def _end_test_mode(sanic, *args, **kwargs):
sanic.test_mode = False
app.request_middleware.appendleft(_collect_request)
async def request(self, method, url, gather_request=True, *args, **kwargs):
@@ -239,7 +224,7 @@ class SanicASGITestClient(httpx.AsyncClient):
async def websocket(self, uri, subprotocols=None, *args, **kwargs):
scheme = "ws"
path = uri
root_path = f"{scheme}://{ASGI_HOST}"
root_path = "{}://{}".format(scheme, ASGI_HOST)
headers = kwargs.get("headers", {})
headers.setdefault("connection", "upgrade")

View File

@@ -1,99 +0,0 @@
from importlib.util import module_from_spec, spec_from_file_location
from os import environ as os_environ
from re import findall as re_findall
from typing import Union
from .exceptions import LoadFileException
def str_to_bool(val: str) -> bool:
"""Takes string and tries to turn it into bool as human would do.
If val is in case insensitive (
"y", "yes", "yep", "yup", "t",
"true", "on", "enable", "enabled", "1"
) returns True.
If val is in case insensitive (
"n", "no", "f", "false", "off", "disable", "disabled", "0"
) returns False.
Else Raise ValueError."""
val = val.lower()
if val in {
"y",
"yes",
"yep",
"yup",
"t",
"true",
"on",
"enable",
"enabled",
"1",
}:
return True
elif val in {"n", "no", "f", "false", "off", "disable", "disabled", "0"}:
return False
else:
raise ValueError(f"Invalid truth value {val}")
def load_module_from_file_location(
location: Union[bytes, str], encoding: str = "utf8", *args, **kwargs
):
"""Returns loaded module provided as a file path.
:param args:
Coresponds to importlib.util.spec_from_file_location location
parameters,but with this differences:
- It has to be of a string or bytes type.
- You can also use here environment variables
in format ${some_env_var}.
Mark that $some_env_var will not be resolved as environment variable.
:encoding:
If location parameter is of a bytes type, then use this encoding
to decode it into string.
:param args:
Coresponds to the rest of importlib.util.spec_from_file_location
parameters.
:param kwargs:
Coresponds to the rest of importlib.util.spec_from_file_location
parameters.
For example You can:
some_module = load_module_from_file_location(
"some_module_name",
"/some/path/${some_env_var}"
)
"""
# 1) Parse location.
if isinstance(location, bytes):
location = location.decode(encoding)
# A) Check if location contains any environment variables
# in format ${some_env_var}.
env_vars_in_location = set(re_findall(r"\${(.+?)}", location))
# B) Check these variables exists in environment.
not_defined_env_vars = env_vars_in_location.difference(os_environ.keys())
if not_defined_env_vars:
raise LoadFileException(
"The following environment variables are not set: "
f"{', '.join(not_defined_env_vars)}"
)
# C) Substitute them in location.
for env_var in env_vars_in_location:
location = location.replace("${" + env_var + "}", os_environ[env_var])
# 2) Load and return module.
name = location.split("/")[-1].split(".")[
0
] # get just the file name without path and .py extension
_mod_spec = spec_from_file_location(name, location, *args, **kwargs)
module = module_from_spec(_mod_spec)
_mod_spec.loader.exec_module(module) # type: ignore
return module

View File

@@ -90,17 +90,20 @@ class CompositionView:
def __init__(self):
self.handlers = {}
self.name = self.__class__.__name__
def add(self, methods, handler, stream=False):
if stream:
handler.is_stream = stream
for method in methods:
if method not in HTTP_METHODS:
raise InvalidUsage(f"{method} is not a valid HTTP method.")
raise InvalidUsage(
"{} is not a valid HTTP method.".format(method)
)
if method in self.handlers:
raise InvalidUsage(f"Method {method} is already registered.")
raise InvalidUsage(
"Method {} is already registered.".format(method)
)
self.handlers[method] = handler
def __call__(self, request, *args, **kwargs):

View File

@@ -3,7 +3,6 @@ from typing import (
Awaitable,
Callable,
Dict,
List,
MutableMapping,
Optional,
Union,
@@ -35,8 +34,6 @@ class WebSocketProtocol(HttpProtocol):
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16,
websocket_ping_interval=20,
websocket_ping_timeout=20,
**kwargs
):
super().__init__(*args, **kwargs)
@@ -47,8 +44,6 @@ class WebSocketProtocol(HttpProtocol):
self.websocket_max_queue = websocket_max_queue
self.websocket_read_limit = websocket_read_limit
self.websocket_write_limit = websocket_write_limit
self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout
# timeouts make no sense for websocket routes
def request_timeout_callback(self):
@@ -118,13 +113,11 @@ class WebSocketProtocol(HttpProtocol):
# hook up the websocket protocol
self.websocket = WebSocketCommonProtocol(
close_timeout=self.websocket_timeout,
timeout=self.websocket_timeout,
max_size=self.websocket_max_size,
max_queue=self.websocket_max_queue,
read_limit=self.websocket_read_limit,
write_limit=self.websocket_write_limit,
ping_interval=self.websocket_ping_interval,
ping_timeout=self.websocket_ping_timeout,
)
# Following two lines are required for websockets 8.x
self.websocket.is_client = False
@@ -144,11 +137,9 @@ class WebSocketConnection:
self,
send: Callable[[ASIMessage], Awaitable[None]],
receive: Callable[[], Awaitable[ASIMessage]],
subprotocols: Optional[List[str]] = None,
) -> None:
self._send = send
self._receive = receive
self.subprotocols = subprotocols or []
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
@@ -173,14 +164,7 @@ class WebSocketConnection:
receive = recv
async def accept(self) -> None:
await self._send(
{
"type": "websocket.accept",
"subprotocol": ",".join(
[subprotocol for subprotocol in self.subprotocols]
),
}
)
await self._send({"type": "websocket.accept", "subprotocol": ""})
async def close(self) -> None:
pass

View File

@@ -5,7 +5,7 @@ import signal
import sys
import traceback
from gunicorn.workers import base as base # type: ignore
import gunicorn.workers.base as base # type: ignore
from sanic.server import HttpProtocol, Signal, serve, trigger_events
from sanic.websocket import WebSocketProtocol

View File

@@ -11,3 +11,11 @@ line_length = 79
lines_after_imports = 2
lines_between_types = 1
multi_line_output = 3
not_skip = __init__.py
[version]
current_version = 19.12.0
files = sanic/__version__.py
current_version_pattern = __version__ = "{current_version}"
new_version_pattern = __version__ = "{new_version}"

View File

@@ -57,12 +57,10 @@ setup_kwargs = {
"author": "Sanic Community",
"author_email": "admhpkns@gmail.com",
"description": (
"A web server and web framework that's written to go fast. "
"Build fast. Run fast."
"A web server and web framework that's written to go fast. Build fast. Run fast."
),
"long_description": long_description,
"packages": ["sanic"],
"package_data": {"sanic": ["py.typed"]},
"platforms": "any",
"python_requires": ">=3.6",
"classifiers": [
@@ -71,41 +69,37 @@ setup_kwargs = {
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
"entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]},
}
env_dependency = (
'; sys_platform != "win32" ' 'and implementation_name == "cpython"'
)
ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency
uvloop = "uvloop>=0.5.3,<0.15.0" + env_dependency
requirements = [
"httptools>=0.0.10",
uvloop,
ujson,
"aiofiles>=0.6.0",
"websockets>=8.1,<=9.1",
"multidict>=5.0,<6.0",
"httpx==0.15.4",
"aiofiles>=0.3.0",
"websockets>=7.0,<9.0",
"multidict==5.0.0",
"httpx==0.9.3",
]
tests_require = [
"pytest==5.2.1",
"multidict>=5.0,<6.0",
"gunicorn==20.0.4",
"multidict==5.0.0",
"gunicorn",
"pytest-cov",
"httpcore==0.11.*",
"httpcore==0.3.0",
"beautifulsoup4",
uvloop,
ujson,
"pytest-sanic",
"pytest-sugar",
"pytest-benchmark",
"pytest-dependency",
]
docs_require = [

View File

@@ -11,7 +11,6 @@ from sanic.router import RouteExists, Router
random.seed("Pack my box with five dozen liquor jugs.")
Sanic.test_mode = True
if sys.platform in ["win32", "cygwin"]:
collect_ignore = ["test_worker.py"]
@@ -96,15 +95,15 @@ class RouteStringGenerator:
@pytest.fixture(scope="function")
def sanic_router(app):
def sanic_router():
# noinspection PyProtectedMember
def _setup(route_details: tuple) -> (Router, tuple):
router = Router(app)
router = Router()
added_router = []
for method, route in route_details:
try:
router._add(
uri=f"/{route}",
uri="/{}".format(route),
methods=frozenset({method}),
host="localhost",
handler=_handler,

View File

@@ -1,6 +1,7 @@
# Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker simple_server:main
""" Minimal helloworld application.
"""
import ujson
from wheezy.http import HTTPResponse, WSGIApplication

View File

@@ -1 +0,0 @@
TEST_SETTING_VALUE = 1

View File

@@ -3,12 +3,9 @@ import logging
import sys
from inspect import isawaitable
from os import environ
from unittest.mock import patch
import pytest
from sanic import Sanic
from sanic.exceptions import SanicException
from sanic.response import text
@@ -51,7 +48,6 @@ def test_asyncio_server_no_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43123,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
@@ -66,7 +62,6 @@ def test_asyncio_server_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43124,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
@@ -74,8 +69,7 @@ def test_asyncio_server_start_serving(app):
assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
wait_close = srv.close()
loop.run_until_complete(wait_close)
srv.close()
# Looks like we can't easily test `serve_forever()`
@@ -118,7 +112,7 @@ def test_app_route_raise_value_error(app):
def test_app_handle_request_handler_is_none(app, monkeypatch):
def mockreturn(*args, **kwargs):
return None, [], {}, "", "", None
return None, [], {}, "", ""
# Not sure how to make app.router.get() return None, so use mock here.
monkeypatch.setattr(app.router, "get", mockreturn)
@@ -127,11 +121,11 @@ def test_app_handle_request_handler_is_none(app, monkeypatch):
def handler(request):
return text("test")
_, response = app.test_client.get("/test")
request, response = app.test_client.get("/test")
assert (
"'None' was returned while requesting a handler from the router"
in response.text
response.text
== "Error: 'None' was returned while requesting a handler from the router"
)
@@ -150,43 +144,6 @@ def test_app_enable_websocket(app, websocket_enabled, enable):
assert app.websocket_enabled == True
@patch("sanic.app.WebSocketProtocol")
def test_app_websocket_parameters(websocket_protocol_mock, app):
app.config.WEBSOCKET_MAX_SIZE = 44
app.config.WEBSOCKET_MAX_QUEUE = 45
app.config.WEBSOCKET_READ_LIMIT = 46
app.config.WEBSOCKET_WRITE_LIMIT = 47
app.config.WEBSOCKET_PING_TIMEOUT = 48
app.config.WEBSOCKET_PING_INTERVAL = 50
@app.websocket("/ws")
async def handler(request, ws):
await ws.send("test")
try:
# This will fail because WebSocketProtocol is mocked and only the call kwargs matter
app.test_client.get("/ws")
except:
pass
websocket_protocol_call_args = websocket_protocol_mock.call_args
ws_kwargs = websocket_protocol_call_args[1]
assert ws_kwargs["websocket_max_size"] == app.config.WEBSOCKET_MAX_SIZE
assert ws_kwargs["websocket_max_queue"] == app.config.WEBSOCKET_MAX_QUEUE
assert ws_kwargs["websocket_read_limit"] == app.config.WEBSOCKET_READ_LIMIT
assert (
ws_kwargs["websocket_write_limit"] == app.config.WEBSOCKET_WRITE_LIMIT
)
assert (
ws_kwargs["websocket_ping_timeout"]
== app.config.WEBSOCKET_PING_TIMEOUT
)
assert (
ws_kwargs["websocket_ping_interval"]
== app.config.WEBSOCKET_PING_INTERVAL
)
def test_handle_request_with_nested_exception(app, monkeypatch):
err_msg = "Mock Exception"
@@ -227,7 +184,9 @@ def test_handle_request_with_nested_exception_debug(app, monkeypatch):
request, response = app.test_client.get("/", debug=True)
assert response.status == 500
assert response.text.startswith(
f"Error while handling error: {err_msg}\nStack: Traceback (most recent call last):\n"
"Error while handling error: {}\nStack: Traceback (most recent call last):\n".format(
err_msg
)
)
@@ -247,78 +206,10 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
with caplog.at_level(logging.ERROR):
request, response = app.test_client.get("/")
port = request.server_port
assert port > 0
assert response.status == 500
assert "Mock SanicException" in response.text
assert response.text == "Error: Mock SanicException"
assert (
"sanic.root",
logging.ERROR,
f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'",
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) in caplog.record_tuples
def test_app_name_required():
with pytest.raises(SanicException):
Sanic()
def test_app_has_test_mode_sync():
app = Sanic("test")
@app.get("/")
def handler(request):
assert request.app.test_mode
return text("test")
_, response = app.test_client.get("/")
assert response.status == 200
def test_app_registry():
instance = Sanic("test")
assert Sanic._app_registry["test"] is instance
def test_app_registry_wrong_type():
with pytest.raises(SanicException):
Sanic.register_app(1)
def test_app_registry_name_reuse():
Sanic("test")
Sanic.test_mode = False
with pytest.raises(SanicException):
Sanic("test")
Sanic.test_mode = True
Sanic("test")
def test_app_registry_retrieval():
instance = Sanic("test")
assert Sanic.get_app("test") is instance
def test_get_app_does_not_exist():
with pytest.raises(SanicException):
Sanic.get_app("does-not-exist")
def test_get_app_does_not_exist_force_create():
assert isinstance(
Sanic.get_app("does-not-exist", force_create=True), Sanic
)
def test_app_no_registry():
Sanic("no-register", register=False)
with pytest.raises(SanicException):
Sanic.get_app("no-register")
def test_app_no_registry_env():
environ["SANIC_REGISTER"] = "False"
Sanic("no-register")
with pytest.raises(SanicException):
Sanic.get_app("no-register")
del environ["SANIC_REGISTER"]

View File

@@ -1,3 +1,5 @@
import asyncio
from collections import deque, namedtuple
import pytest
@@ -79,6 +81,9 @@ def test_listeners_triggered(app):
with pytest.warns(UserWarning):
server.run()
for task in asyncio.Task.all_tasks():
task.cancel()
assert before_server_start
assert after_server_start
assert before_server_stop
@@ -121,6 +126,9 @@ def test_listeners_triggered_async(app):
with pytest.warns(UserWarning):
server.run()
for task in asyncio.Task.all_tasks():
task.cancel()
assert before_server_start
assert after_server_start
assert before_server_stop
@@ -189,53 +197,6 @@ async def test_websocket_receive(send, receive, message_stack):
assert text == msg["text"]
@pytest.mark.asyncio
async def test_websocket_accept_with_no_subprotocols(
send, receive, message_stack
):
ws = WebSocketConnection(send, receive)
await ws.accept()
assert len(message_stack) == 1
message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == ""
assert "bytes" not in message
@pytest.mark.asyncio
async def test_websocket_accept_with_subprotocol(send, receive, message_stack):
subprotocols = ["graphql-ws"]
ws = WebSocketConnection(send, receive, subprotocols)
await ws.accept()
assert len(message_stack) == 1
message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == "graphql-ws"
assert "bytes" not in message
@pytest.mark.asyncio
async def test_websocket_accept_with_multiple_subprotocols(
send, receive, message_stack
):
subprotocols = ["graphql-ws", "hello", "world"]
ws = WebSocketConnection(send, receive, subprotocols)
await ws.accept()
assert len(message_stack) == 1
message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == "graphql-ws,hello,world"
assert "bytes" not in message
def test_improper_websocket_connection(transport, send, receive):
with pytest.raises(InvalidUsage):
transport.get_websocket_connection()
@@ -260,7 +221,7 @@ async def test_request_class_custom():
class MyCustomRequest(Request):
pass
app = Sanic(name=__name__, request_class=MyCustomRequest)
app = Sanic(request_class=MyCustomRequest)
@app.get("/custom")
def custom_request(request):

View File

@@ -18,4 +18,4 @@ def test_bad_request_response(app):
app.run(host="127.0.0.1", port=42101, debug=False)
assert lines[0] == b"HTTP/1.1 400 Bad Request\r\n"
assert b"Bad Request" in lines[-1]
assert lines[-1] == b"Error: Bad Request"

View File

@@ -40,9 +40,9 @@ def test_bp_group_with_additional_route_params(app: Sanic):
)
def blueprint_2_named_method(request: Request, param):
if request.method == "DELETE":
return text(f"DELETE_{param}")
return text("DELETE_{}".format(param))
elif request.method == "PATCH":
return text(f"PATCH_{param}")
return text("PATCH_{}".format(param))
blueprint_group = Blueprint.group(
blueprint_1, blueprint_2, url_prefix="/api"

View File

@@ -46,19 +46,19 @@ def test_versioned_routes_get(app, method):
func = getattr(bp, method)
if callable(func):
@func(f"/{method}", version=1)
@func("/{}".format(method), version=1)
def handler(request):
return text("OK")
else:
print(func)
raise Exception(f"{func} is not callable")
raise Exception("{} is not callable".format(func))
app.blueprint(bp)
client_method = getattr(app.test_client, method)
request, response = client_method(f"/v1/{method}")
request, response = client_method("/v1/{}".format(method))
assert response.status == 200
@@ -252,90 +252,8 @@ def test_several_bp_with_host(app):
assert response.text == "Hello3"
def test_bp_with_host_list(app):
bp = Blueprint(
"test_bp_host",
url_prefix="/test1",
host=["example.com", "sub.example.com"],
)
@bp.route("/")
def handler1(request):
return text("Hello")
@bp.route("/", host=["sub1.example.com"])
def handler2(request):
return text("Hello subdomain!")
app.blueprint(bp)
headers = {"Host": "example.com"}
request, response = app.test_client.get("/test1/", headers=headers)
assert response.text == "Hello"
headers = {"Host": "sub.example.com"}
request, response = app.test_client.get("/test1/", headers=headers)
assert response.text == "Hello"
headers = {"Host": "sub1.example.com"}
request, response = app.test_client.get("/test1/", headers=headers)
assert response.text == "Hello subdomain!"
def test_several_bp_with_host_list(app):
bp = Blueprint(
"test_text",
url_prefix="/test",
host=["example.com", "sub.example.com"],
)
bp2 = Blueprint(
"test_text2",
url_prefix="/test",
host=["sub1.example.com", "sub2.example.com"],
)
@bp.route("/")
def handler(request):
return text("Hello")
@bp2.route("/")
def handler1(request):
return text("Hello2")
@bp2.route("/other/")
def handler2(request):
return text("Hello3")
app.blueprint(bp)
app.blueprint(bp2)
assert bp.host == ["example.com", "sub.example.com"]
headers = {"Host": "example.com"}
request, response = app.test_client.get("/test/", headers=headers)
assert response.text == "Hello"
assert bp.host == ["example.com", "sub.example.com"]
headers = {"Host": "sub.example.com"}
request, response = app.test_client.get("/test/", headers=headers)
assert response.text == "Hello"
assert bp2.host == ["sub1.example.com", "sub2.example.com"]
headers = {"Host": "sub1.example.com"}
request, response = app.test_client.get("/test/", headers=headers)
assert response.text == "Hello2"
request, response = app.test_client.get("/test/other/", headers=headers)
assert response.text == "Hello3"
assert bp2.host == ["sub1.example.com", "sub2.example.com"]
headers = {"Host": "sub2.example.com"}
request, response = app.test_client.get("/test/", headers=headers)
assert response.text == "Hello2"
request, response = app.test_client.get("/test/other/", headers=headers)
assert response.text == "Hello3"
def test_bp_middleware(app):
blueprint = Blueprint("test_bp_middleware")
blueprint = Blueprint("test_middleware")
@blueprint.middleware("response")
async def process_response(request, response):
@@ -353,46 +271,6 @@ def test_bp_middleware(app):
assert response.text == "FAIL"
def test_bp_middleware_order(app):
blueprint = Blueprint("test_bp_middleware_order")
order = list()
@blueprint.middleware("request")
def mw_1(request):
order.append(1)
@blueprint.middleware("request")
def mw_2(request):
order.append(2)
@blueprint.middleware("request")
def mw_3(request):
order.append(3)
@blueprint.middleware("response")
def mw_4(request, response):
order.append(6)
@blueprint.middleware("response")
def mw_5(request, response):
order.append(5)
@blueprint.middleware("response")
def mw_6(request, response):
order.append(4)
@blueprint.route("/")
def process_response(request):
return text("OK")
app.blueprint(blueprint)
order.clear()
request, response = app.test_client.get("/")
assert response.status == 200
assert order == [1, 2, 3, 4, 5, 6]
def test_bp_exception_handler(app):
blueprint = Blueprint("test_middleware")
@@ -675,7 +553,9 @@ def test_bp_group_with_default_url_prefix(app):
from uuid import uuid4
resource_id = str(uuid4())
request, response = app.test_client.get(f"/api/v1/resources/{resource_id}")
request, response = app.test_client.get(
"/api/v1/resources/{0}".format(resource_id)
)
assert response.json == {"resource_id": resource_id}
@@ -736,37 +616,6 @@ def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
assert response.status == 200
@pytest.mark.parametrize("file_name", ["test.file"])
def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, "rb") as file:
file.read()
triggered = False
bp = Blueprint(name="test_mw", url_prefix="")
@bp.middleware("request")
def bp_mw1(request):
nonlocal triggered
triggered = True
bp.static(
"/test.file",
get_file_path(static_file_directory, file_name),
strict_slashes=True,
name="static",
)
app.blueprint(bp)
uri = app.url_for("test_mw.static")
assert uri == "/test.file"
_, response = app.test_client.get("/test.file")
assert triggered is True
def test_route_handler_add(app: Sanic):
view = CompositionView()
@@ -820,8 +669,23 @@ def test_duplicate_blueprint(app):
app.blueprint(bp1)
assert str(excinfo.value) == (
f'A blueprint with the name "{bp_name}" is already registered. '
'A blueprint with the name "{}" is already registered. '
"Blueprint names must be unique."
).format(bp_name)
@pytest.mark.parametrize("debug", [True, False, None])
def test_register_blueprint(app, debug):
bp = Blueprint("bp")
app.debug = debug
with pytest.warns(DeprecationWarning) as record:
app.register_blueprint(bp)
assert record[0].message.args[0] == (
"Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method"
" instead"
)

View File

@@ -13,7 +13,7 @@ from sanic.exceptions import PyFileError
@contextmanager
def temp_path():
"""a simple cross platform replacement for NamedTemporaryFile"""
""" a simple cross platform replacement for NamedTemporaryFile """
with TemporaryDirectory() as td:
yield Path(td, "file")
@@ -44,42 +44,42 @@ def test_load_from_object_string_exception(app):
def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(name=__name__)
app = Sanic()
assert app.config.TEST_ANSWER == 42
del environ["SANIC_TEST_ANSWER"]
def test_auto_load_bool_env():
environ["SANIC_TEST_ANSWER"] = "True"
app = Sanic(name=__name__)
app = Sanic()
assert app.config.TEST_ANSWER == True
del environ["SANIC_TEST_ANSWER"]
def test_dont_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(name=__name__, load_env=False)
app = Sanic(load_env=False)
assert getattr(app.config, "TEST_ANSWER", None) is None
del environ["SANIC_TEST_ANSWER"]
def test_load_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(name=__name__, load_env="MYAPP_")
app = Sanic(load_env="MYAPP_")
assert app.config.TEST_ANSWER == 42
del environ["MYAPP_TEST_ANSWER"]
def test_load_env_prefix_float_values():
environ["MYAPP_TEST_ROI"] = "2.3"
app = Sanic(name=__name__, load_env="MYAPP_")
app = Sanic(load_env="MYAPP_")
assert app.config.TEST_ROI == 2.3
del environ["MYAPP_TEST_ROI"]
def test_load_env_prefix_string_value():
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
app = Sanic(name=__name__, load_env="MYAPP_")
app = Sanic(load_env="MYAPP_")
assert app.config.TEST_TOKEN == "somerandomtesttoken"
del environ["MYAPP_TEST_TOKEN"]

View File

@@ -15,8 +15,7 @@ from sanic.response import text
def test_cookies(app):
@app.route("/")
def handler(request):
cookie_value = request.cookies["test"]
response = text(f"Cookies are: {cookie_value}")
response = text("Cookies are: {}".format(request.cookies["test"]))
response.cookies["right_back"] = "at you"
return response
@@ -32,8 +31,7 @@ def test_cookies(app):
async def test_cookies_asgi(app):
@app.route("/")
def handler(request):
cookie_value = request.cookies["test"]
response = text(f"Cookies are: {cookie_value}")
response = text("Cookies are: {}".format(request.cookies["test"]))
response.cookies["right_back"] = "at you"
return response
@@ -54,7 +52,7 @@ def test_false_cookies_encoded(app, httponly, expected):
response = text("hello cookies")
response.cookies["hello"] = "world"
response.cookies["hello"]["httponly"] = httponly
return text(response.cookies["hello"].encode("utf8").decode())
return text(response.cookies["hello"].encode("utf8"))
request, response = app.test_client.get("/")
@@ -80,8 +78,7 @@ def test_false_cookies(app, httponly, expected):
def test_http2_cookies(app):
@app.route("/")
async def handler(request):
cookie_value = request.cookies["test"]
response = text(f"Cookies are: {cookie_value}")
response = text("Cookies are: {}".format(request.cookies["test"]))
return response
headers = {"cookie": "test=working!"}

View File

@@ -17,12 +17,12 @@ def test_create_task(app):
@app.route("/early")
def not_set(request):
return text(str(e.is_set()))
return text(e.is_set())
@app.route("/late")
async def set(request):
await asyncio.sleep(0.1)
return text(str(e.is_set()))
return text(e.is_set())
request, response = app.test_client.get("/early")
assert response.body == b"False"

View File

@@ -20,7 +20,7 @@ class CustomRequest(Request):
def test_custom_request():
app = Sanic(name=__name__, request_class=CustomRequest)
app = Sanic(request_class=CustomRequest)
@app.route("/post", methods=["POST"])
async def post_handler(request):

View File

@@ -1,86 +0,0 @@
import pytest
from sanic import Sanic
from sanic.errorpages import exception_response
from sanic.exceptions import NotFound
from sanic.request import Request
from sanic.response import HTTPResponse
@pytest.fixture
def app():
app = Sanic("error_page_testing")
@app.route("/error", methods=["GET", "POST"])
def err(request):
raise Exception("something went wrong")
return app
@pytest.fixture
def fake_request(app):
return Request(b"/foobar", {}, "1.1", "GET", None, app)
@pytest.mark.parametrize(
"fallback,content_type, exception, status",
(
(None, "text/html; charset=utf-8", Exception, 500),
("html", "text/html; charset=utf-8", Exception, 500),
("auto", "text/html; charset=utf-8", Exception, 500),
("text", "text/plain; charset=utf-8", Exception, 500),
("json", "application/json", Exception, 500),
(None, "text/html; charset=utf-8", NotFound, 404),
("html", "text/html; charset=utf-8", NotFound, 404),
("auto", "text/html; charset=utf-8", NotFound, 404),
("text", "text/plain; charset=utf-8", NotFound, 404),
("json", "application/json", NotFound, 404),
),
)
def test_should_return_html_valid_setting(
fake_request, fallback, content_type, exception, status
):
if fallback:
fake_request.app.config.FALLBACK_ERROR_FORMAT = fallback
try:
raise exception("bad stuff")
except Exception as e:
response = exception_response(fake_request, e, True)
assert isinstance(response, HTTPResponse)
assert response.status == status
assert response.content_type == content_type
def test_auto_fallback_with_data(app):
app.config.FALLBACK_ERROR_FORMAT = "auto"
_, response = app.test_client.get("/error")
assert response.status == 500
assert response.content_type == "text/html; charset=utf-8"
_, response = app.test_client.post("/error", json={"foo": "bar"})
assert response.status == 500
assert response.content_type == "application/json"
_, response = app.test_client.post("/error", data={"foo": "bar"})
assert response.status == 500
assert response.content_type == "text/html; charset=utf-8"
def test_auto_fallback_with_content_type(app):
app.config.FALLBACK_ERROR_FORMAT = "auto"
_, response = app.test_client.get(
"/error", headers={"content-type": "application/json"}
)
assert response.status == 500
assert response.content_type == "application/json"
_, response = app.test_client.get(
"/error", headers={"content-type": "text/plain"}
)
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"

View File

@@ -172,7 +172,7 @@ def test_handled_unhandled_exception(exception_app):
request, response = exception_app.test_client.get("/divide_by_zero")
assert response.status == 500
soup = BeautifulSoup(response.body, "html.parser")
assert "Internal Server Error" in soup.h1.text
assert soup.h1.text == "Internal Server Error"
message = " ".join(soup.p.text.split())
assert message == (
@@ -218,4 +218,4 @@ def test_abort(exception_app):
request, response = exception_app.test_client.get("/abort/message")
assert response.status == 500
assert "Abort" in response.text
assert response.text == "Error: Abort"

View File

@@ -43,7 +43,7 @@ def handler_6(request, arg):
try:
foo = 1 / arg
except Exception as e:
raise e from ValueError(f"{arg}")
raise e from ValueError("{}".format(arg))
return text(foo)
@@ -86,7 +86,7 @@ def test_html_traceback_output_in_debug_mode():
summary_text = " ".join(soup.select(".summary")[0].text.split())
assert (
"NameError: name 'bar' is not defined while handling path /4"
"NameError: name 'bar' " "is not defined while handling path /4"
) == summary_text
@@ -112,7 +112,7 @@ def test_chained_exception_handler():
summary_text = " ".join(soup.select(".summary")[0].text.split())
assert (
"ZeroDivisionError: division by zero while handling path /6/0"
"ZeroDivisionError: division by zero " "while handling path /6/0"
) == summary_text

View File

@@ -3,36 +3,56 @@ import asyncio
from asyncio import sleep as aio_sleep
from json import JSONDecodeError
import httpcore
import httpx
from sanic import Sanic, server
from sanic.response import text
from sanic.testing import HOST, SanicTestClient
from sanic.testing import HOST, PORT, SanicTestClient
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port
from httpcore._async.base import ConnectionState
from httpcore._async.connection import AsyncHTTPConnection
from httpcore._types import Origin
old_conn = None
class ReusableSanicConnectionPool(httpcore.AsyncConnectionPool):
last_reused_connection = None
class ReusableSanicConnectionPool(
httpx.dispatch.connection_pool.ConnectionPool
):
async def acquire_connection(self, origin, timeout):
global old_conn
connection = self.pop_connection(origin)
async def _get_connection_from_pool(self, *args, **kwargs):
conn = await super()._get_connection_from_pool(*args, **kwargs)
self.__class__.last_reused_connection = conn
return conn
if connection is None:
pool_timeout = None if timeout is None else timeout.pool_timeout
await self.max_connections.acquire(timeout=pool_timeout)
connection = httpx.dispatch.connection.HTTPConnection(
origin,
verify=self.verify,
cert=self.cert,
http2=self.http2,
backend=self.backend,
release_func=self.release_connection,
trust_env=self.trust_env,
uds=self.uds,
)
self.active_connections.add(connection)
if old_conn is not None:
if old_conn != connection:
raise RuntimeError(
"We got a new connection, wanted the same one!"
)
old_conn = connection
return connection
class ResusableSanicSession(httpx.AsyncClient):
class ResusableSanicSession(httpx.Client):
def __init__(self, *args, **kwargs) -> None:
transport = ReusableSanicConnectionPool()
super().__init__(transport=transport, *args, **kwargs)
dispatch = ReusableSanicConnectionPool()
super().__init__(dispatch=dispatch, *args, **kwargs)
class ReuseableSanicTestClient(SanicTestClient):
@@ -77,9 +97,11 @@ class ReuseableSanicTestClient(SanicTestClient):
):
url = uri
else:
uri = uri if uri.startswith("/") else f"/{uri}"
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
scheme = "http"
url = f"{scheme}://{HOST}:{PORT}{uri}"
url = "{scheme}://{host}:{port}{uri}".format(
scheme=scheme, host=HOST, port=PORT, uri=uri
)
@self.app.listener("after_server_start")
async def _collect_response(loop):
@@ -112,7 +134,7 @@ class ReuseableSanicTestClient(SanicTestClient):
self.app.listeners["after_server_start"].pop()
if exceptions:
raise ValueError(f"Exception during request: {exceptions}")
raise ValueError("Exception during request: {}".format(exceptions))
if gather_request:
self.app.request_middleware.pop()
@@ -121,13 +143,17 @@ class ReuseableSanicTestClient(SanicTestClient):
return request, response
except Exception:
raise ValueError(
f"Request and response object expected, got ({results})"
"Request and response object expected, got ({})".format(
results
)
)
else:
try:
return results[-1]
except Exception:
raise ValueError(f"Request object expected, got ({results})")
raise ValueError(
"Request object expected, got ({})".format(results)
)
def kill_server(self):
try:
@@ -137,7 +163,7 @@ class ReuseableSanicTestClient(SanicTestClient):
self._server = None
if self._session:
self._loop.run_until_complete(self._session.aclose())
self._loop.run_until_complete(self._session.close())
self._session = None
except Exception as e3:
@@ -156,7 +182,7 @@ class ReuseableSanicTestClient(SanicTestClient):
self._session = self.get_new_session()
try:
response = await getattr(self._session, method.lower())(
url, timeout=request_keepalive, *args, **kwargs
url, verify=False, timeout=request_keepalive, *args, **kwargs
)
except NameError:
raise Exception(response.status_code)
@@ -166,7 +192,7 @@ class ReuseableSanicTestClient(SanicTestClient):
except (JSONDecodeError, UnicodeDecodeError):
response.json = None
response.body = await response.aread()
response.body = await response.read()
response.status = response.status_code
response.content_type = response.headers.get("content-type")
@@ -218,7 +244,6 @@ def test_keep_alive_timeout_reuse():
request, response = client.get("/1")
assert response.status == 200
assert response.text == "OK"
assert ReusableSanicConnectionPool.last_reused_connection
finally:
client.kill_server()
@@ -231,15 +256,20 @@ def test_keep_alive_client_timeout():
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
headers = {"Connection": "keep-alive"}
request, response = client.get(
"/1", headers=headers, request_keepalive=1
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(2))
exception = None
request, response = client.get("/1", request_keepalive=1)
assert ReusableSanicConnectionPool.last_reused_connection is None
try:
request, response = client.get(
"/1", headers=headers, request_keepalive=1
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(2))
exception = None
request, response = client.get("/1", request_keepalive=1)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert "got a new connection" in exception.args[0]
finally:
client.kill_server()
@@ -254,14 +284,22 @@ def test_keep_alive_server_timeout():
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
headers = {"Connection": "keep-alive"}
request, response = client.get(
"/1", headers=headers, request_keepalive=60
try:
request, response = client.get(
"/1", headers=headers, request_keepalive=60
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(3))
exception = None
request, response = client.get("/1", request_keepalive=60)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert (
"Connection reset" in exception.args[0]
or "got a new connection" in exception.args[0]
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(3))
exception = None
request, response = client.get("/1", request_keepalive=60)
assert ReusableSanicConnectionPool.last_reused_connection is None
finally:
client.kill_server()

View File

@@ -1,38 +0,0 @@
from pathlib import Path
from types import ModuleType
import pytest
from sanic.exceptions import LoadFileException
from sanic.utils import load_module_from_file_location
@pytest.fixture
def loaded_module_from_file_location():
return load_module_from_file_location(
str(Path(__file__).parent / "static" / "app_test_config.py")
)
@pytest.mark.dependency(name="test_load_module_from_file_location")
def test_load_module_from_file_location(loaded_module_from_file_location):
assert isinstance(loaded_module_from_file_location, ModuleType)
@pytest.mark.dependency(depends=["test_load_module_from_file_location"])
def test_loaded_module_from_file_location_name(
loaded_module_from_file_location,
):
name = loaded_module_from_file_location.__name__
if "C:\\" in name:
name = name.split("\\")[-1]
assert name == "app_test_config"
def test_load_module_from_file_location_with_non_existing_env_variable():
with pytest.raises(
LoadFileException,
match="The following environment variables are not set: MuuMilk",
):
load_module_from_file_location("${MuuMilk}")

View File

@@ -1,5 +1,4 @@
import logging
import os
import uuid
from importlib import reload
@@ -13,7 +12,6 @@ import sanic
from sanic import Sanic
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
from sanic.response import text
from sanic.testing import SanicTestClient
logging_format = """module: %(module)s; \
@@ -102,7 +100,7 @@ def test_logging_pass_customer_logconfig():
@pytest.mark.parametrize("debug", (True, False))
def test_log_connection_lost(app, debug, monkeypatch):
"""Should not log Connection lost exception on non debug"""
""" Should not log Connection lost exception on non debug """
stream = StringIO()
root = logging.getLogger("sanic.root")
root.addHandler(logging.StreamHandler(stream))
@@ -129,7 +127,7 @@ def test_log_connection_lost(app, debug, monkeypatch):
def test_logger(caplog):
rand_string = str(uuid.uuid4())
app = Sanic(name=__name__)
app = Sanic()
@app.get("/")
def log_info(request):
@@ -139,67 +137,15 @@ def test_logger(caplog):
with caplog.at_level(logging.INFO):
request, response = app.test_client.get("/")
port = request.server_port
# Note: testing with random port doesn't show the banner because it doesn't
# define host and port. This test supports both modes.
if caplog.record_tuples[0] == (
"sanic.root",
logging.INFO,
f"Goin' Fast @ http://127.0.0.1:{port}",
):
caplog.record_tuples.pop(0)
assert caplog.record_tuples[0] == (
"sanic.root",
logging.INFO,
f"http://127.0.0.1:{port}/",
)
assert caplog.record_tuples[1] == ("sanic.root", logging.INFO, rand_string)
assert caplog.record_tuples[-1] == (
"sanic.root",
logging.INFO,
"Server Stopped",
)
def test_logger_static_and_secure(caplog):
# Same as test_logger, except for more coverage:
# - test_client initialised separately for static port
# - using ssl
rand_string = str(uuid.uuid4())
app = Sanic(name=__name__)
@app.get("/")
def log_info(request):
logger.info(rand_string)
return text("hello")
current_dir = os.path.dirname(os.path.realpath(__file__))
ssl_cert = os.path.join(current_dir, "certs/selfsigned.cert")
ssl_key = os.path.join(current_dir, "certs/selfsigned.key")
ssl_dict = {"cert": ssl_cert, "key": ssl_key}
test_client = SanicTestClient(app, port=42101)
with caplog.at_level(logging.INFO):
request, response = test_client.get(
f"https://127.0.0.1:{test_client.port}/",
server_kwargs=dict(ssl=ssl_dict),
)
port = test_client.port
assert caplog.record_tuples[0] == (
"sanic.root",
logging.INFO,
f"Goin' Fast @ https://127.0.0.1:{port}",
"Goin' Fast @ http://127.0.0.1:42101",
)
assert caplog.record_tuples[1] == (
"sanic.root",
logging.INFO,
f"https://127.0.0.1:{port}/",
"http://127.0.0.1:42101/",
)
assert caplog.record_tuples[2] == ("sanic.root", logging.INFO, rand_string)
assert caplog.record_tuples[-1] == (

View File

@@ -49,10 +49,10 @@ def test_logo_false(app, caplog):
loop.run_until_complete(_server.wait_closed())
app.stop()
banner, port = caplog.record_tuples[ROW][2].rsplit(":", 1)
assert caplog.record_tuples[ROW][1] == logging.INFO
assert banner == "Goin' Fast @ http://127.0.0.1"
assert int(port) > 0
assert caplog.record_tuples[ROW][
2
] == "Goin' Fast @ http://127.0.0.1:{}".format(PORT)
def test_logo_true(app, caplog):

View File

@@ -2,7 +2,7 @@ import logging
from asyncio import CancelledError
from sanic.exceptions import NotFound, SanicException
from sanic.exceptions import NotFound
from sanic.request import Request
from sanic.response import HTTPResponse, text
@@ -93,7 +93,7 @@ def test_middleware_response_raise_cancelled_error(app, caplog):
"sanic.root",
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) not in caplog.record_tuples
) in caplog.record_tuples
def test_middleware_response_raise_exception(app, caplog):
@@ -102,16 +102,14 @@ def test_middleware_response_raise_exception(app, caplog):
raise Exception("Exception at response middleware")
with caplog.at_level(logging.ERROR):
reqrequest, response = app.test_client.get("/fail")
reqrequest, response = app.test_client.get("/")
assert response.status == 404
# 404 errors are not logged
assert (
"sanic.root",
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) not in caplog.record_tuples
# Middleware exception ignored but logged
) in caplog.record_tuples
assert (
"sanic.error",
logging.ERROR,

View File

@@ -87,15 +87,3 @@ def test_pickle_app_with_bp(app, protocol):
request, response = up_p_app.test_client.get("/")
assert up_p_app.is_request_stream is False
assert response.text == "Hello"
@pytest.mark.parametrize("protocol", [3, 4])
def test_pickle_app_with_static(app, protocol):
app.route("/")(handler)
app.static("/static", "/tmp/static")
p_app = pickle.dumps(app, protocol=protocol)
del app
up_p_app = pickle.loads(p_app)
assert up_p_app
request, response = up_p_app.test_client.get("/static/missing.txt")
assert response.status == 404

View File

@@ -21,13 +21,13 @@ def test_versioned_named_routes_get(app, method):
bp = Blueprint("test_bp", url_prefix="/bp")
method = method.lower()
route_name = f"route_{method}"
route_name2 = f"route2_{method}"
route_name = "route_{}".format(method)
route_name2 = "route2_{}".format(method)
func = getattr(app, method)
if callable(func):
@func(f"/{method}", version=1, name=route_name)
@func("/{}".format(method), version=1, name=route_name)
def handler(request):
return text("OK")
@@ -38,7 +38,7 @@ def test_versioned_named_routes_get(app, method):
func = getattr(bp, method)
if callable(func):
@func(f"/{method}", version=1, name=route_name2)
@func("/{}".format(method), version=1, name=route_name2)
def handler2(request):
return text("OK")
@@ -48,14 +48,14 @@ def test_versioned_named_routes_get(app, method):
app.blueprint(bp)
assert app.router.routes_all[f"/v1/{method}"].name == route_name
assert app.router.routes_all["/v1/{}".format(method)].name == route_name
route = app.router.routes_all[f"/v1/bp/{method}"]
assert route.name == f"test_bp.{route_name2}"
route = app.router.routes_all["/v1/bp/{}".format(method)]
assert route.name == "test_bp.{}".format(route_name2)
assert app.url_for(route_name) == f"/v1/{method}"
url = app.url_for(f"test_bp.{route_name2}")
assert url == f"/v1/bp/{method}"
assert app.url_for(route_name) == "/v1/{}".format(method)
url = app.url_for("test_bp.{}".format(route_name2))
assert url == "/v1/bp/{}".format(method)
with pytest.raises(URLBuildError):
app.url_for("handler")

View File

@@ -27,7 +27,7 @@ def test_payload_too_large_at_data_received_default(app):
response = app.test_client.get("/1", gather_request=False)
assert response.status == 413
assert "Payload Too Large" in response.text
assert response.text == "Error: Payload Too Large"
def test_payload_too_large_at_on_header_default(app):
@@ -40,4 +40,4 @@ def test_payload_too_large_at_on_header_default(app):
data = "a" * 1000
response = app.test_client.post("/1", gather_request=False, data=data)
assert response.status == 413
assert "Payload Too Large" in response.text
assert response.text == "Error: Payload Too Large"

View File

@@ -115,14 +115,14 @@ def test_redirect_with_params(app, test_str):
@app.route("/api/v1/test/<test>/")
async def init_handler(request, test):
return redirect(f"/api/v2/test/{use_in_uri}/")
return redirect("/api/v2/test/{}/".format(use_in_uri))
@app.route("/api/v2/test/<test>/")
async def target_handler(request, test):
assert test == test_str
return text("OK")
_, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/")
_, response = app.test_client.get("/api/v1/test/{}/".format(use_in_uri))
assert response.status == 200
assert response.content == b"OK"

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