Compare commits

..

1 Commits

Author SHA1 Message Date
Adam Hopkins
3b037f1faa v2.6.3 changelog and version 2020-06-29 14:56:56 +03:00
76 changed files with 669 additions and 2484 deletions

View File

@@ -17,12 +17,6 @@ environment:
PYTHON_VERSION: "3.8.x" PYTHON_VERSION: "3.8.x"
PYTHON_ARCH: "64" 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%" init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
install: 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

@@ -31,16 +31,6 @@ matrix:
dist: xenial dist: xenial
sudo: true sudo: true
name: "Python 3.8 without Extensions" 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 - env: TOX_ENV=type-checking
python: 3.6 python: 3.6
name: "Python 3.6 Type checks" name: "Python 3.6 Type checks"
@@ -50,10 +40,6 @@ matrix:
- env: TOX_ENV=type-checking - env: TOX_ENV=type-checking
python: 3.8 python: 3.8
name: "Python 3.8 Type checks" 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 - env: TOX_ENV=lint
python: 3.6 python: 3.6
name: "Python 3.6 Linter checks" name: "Python 3.6 Linter checks"
@@ -75,28 +61,23 @@ matrix:
dist: xenial dist: xenial
sudo: true sudo: true
name: "Python 3.8 Bandit security scan" 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 - env: TOX_ENV=docs
python: 3.7 python: 3.7
dist: xenial dist: xenial
sudo: true sudo: true
name: "Python 3.7 Documentation tests" name: "Python 3.7 Documentation tests"
- env: TOX_ENV=pyNightly - env: TOX_ENV=pyNightly
python: "nightly" python: 'nightly'
name: "Python nightly with Extensions" name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext - env: TOX_ENV=pyNightly-no-ext
python: "nightly" python: 'nightly'
name: "Python nightly without Extensions" name: "Python nightly without Extensions"
allow_failures: allow_failures:
- env: TOX_ENV=pyNightly - env: TOX_ENV=pyNightly
python: "nightly" python: 'nightly'
name: "Python nightly with Extensions" name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext - env: TOX_ENV=pyNightly-no-ext
python: "nightly" python: 'nightly'
name: "Python nightly without Extensions" name: "Python nightly without Extensions"
install: install:
- pip install -U tox - pip install -U tox

View File

@@ -1,201 +1,9 @@
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 Version 20.6.3
=============== ===============
Bugfixes Bugfixes
******** ********
* *
`#1884 <https://github.com/huge-success/sanic/pull/1884>`_ `#1884 <https://github.com/huge-success/sanic/pull/1884>`_
Revert change to multiprocessing mode Revert change to multiprocessing mode
@@ -206,7 +14,7 @@ Version 20.6.2
Features Features
******** ********
* *
`#1641 <https://github.com/huge-success/sanic/pull/1641>`_ `#1641 <https://github.com/huge-success/sanic/pull/1641>`_
Socket binding implemented properly for IPv6 and UNIX sockets Socket binding implemented properly for IPv6 and UNIX sockets
@@ -217,7 +25,7 @@ Version 20.6.1
Features Features
******** ********
* *
`#1760 <https://github.com/huge-success/sanic/pull/1760>`_ `#1760 <https://github.com/huge-success/sanic/pull/1760>`_
Add version parameter to websocket routes Add version parameter to websocket routes
@@ -228,7 +36,7 @@ Features
* *
`#1880 <https://github.com/huge-success/sanic/pull/1880>`_ `#1880 <https://github.com/huge-success/sanic/pull/1880>`_
Add handler names for websockets for url_for usage Add handler names for websockets for url_for usage
Bugfixes Bugfixes
******** ********
@@ -248,7 +56,7 @@ Bugfixes
* *
`#1848 <https://github.com/huge-success/sanic/pull/1848>`_ `#1848 <https://github.com/huge-success/sanic/pull/1848>`_
Reverse named_response_middlware execution order, to match normal response middleware execution order Reverse named_response_middlware execution order, to match normal response middleware execution order
* *
`#1853 <https://github.com/huge-success/sanic/pull/1853>`_ `#1853 <https://github.com/huge-success/sanic/pull/1853>`_
Fix pickle error when attempting to pickle an application which contains websocket routes Fix pickle error when attempting to pickle an application which contains websocket routes
@@ -300,28 +108,28 @@ Version 20.3.0
Features Features
******** ********
* *
`#1762 <https://github.com/huge-success/sanic/pull/1762>`_ `#1762 <https://github.com/huge-success/sanic/pull/1762>`_
Add ``srv.start_serving()`` and ``srv.serve_forever()`` to ``AsyncioServer`` Add ``srv.start_serving()`` and ``srv.serve_forever()`` to ``AsyncioServer``
* *
`#1767 <https://github.com/huge-success/sanic/pull/1767>`_ `#1767 <https://github.com/huge-success/sanic/pull/1767>`_
Make Sanic usable on ``hypercorn -k trio myweb.app`` Make Sanic usable on ``hypercorn -k trio myweb.app``
* *
`#1768 <https://github.com/huge-success/sanic/pull/1768>`_ `#1768 <https://github.com/huge-success/sanic/pull/1768>`_
No tracebacks on normal errors and prettier error pages No tracebacks on normal errors and prettier error pages
* *
`#1769 <https://github.com/huge-success/sanic/pull/1769>`_ `#1769 <https://github.com/huge-success/sanic/pull/1769>`_
Code cleanup in file responses Code cleanup in file responses
* *
`#1793 <https://github.com/huge-success/sanic/pull/1793>`_ and `#1793 <https://github.com/huge-success/sanic/pull/1793>`_ and
`#1819 <https://github.com/huge-success/sanic/pull/1819>`_ `#1819 <https://github.com/huge-success/sanic/pull/1819>`_
Upgrade ``str.format()`` to f-strings Upgrade ``str.format()`` to f-strings
* *
`#1798 <https://github.com/huge-success/sanic/pull/1798>`_ `#1798 <https://github.com/huge-success/sanic/pull/1798>`_
Allow multiple workers on MacOS with Python 3.8 Allow multiple workers on MacOS with Python 3.8
@@ -332,19 +140,19 @@ Features
Bugfixes Bugfixes
******** ********
* *
`#1748 <https://github.com/huge-success/sanic/pull/1748>`_ `#1748 <https://github.com/huge-success/sanic/pull/1748>`_
Remove loop argument in ``asyncio.Event`` in Python 3.8 Remove loop argument in ``asyncio.Event`` in Python 3.8
* *
`#1764 <https://github.com/huge-success/sanic/pull/1764>`_ `#1764 <https://github.com/huge-success/sanic/pull/1764>`_
Allow route decorators to stack up again Allow route decorators to stack up again
* *
`#1789 <https://github.com/huge-success/sanic/pull/1789>`_ `#1789 <https://github.com/huge-success/sanic/pull/1789>`_
Fix tests using hosts yielding incorrect ``url_for`` Fix tests using hosts yielding incorrect ``url_for``
* *
`#1808 <https://github.com/huge-success/sanic/pull/1808>`_ `#1808 <https://github.com/huge-success/sanic/pull/1808>`_
Fix Ctrl+C and tests on Windows Fix Ctrl+C and tests on Windows
@@ -358,7 +166,7 @@ Deprecations and Removals
* *
`#1801 <https://github.com/huge-success/sanic/pull/1801>`_ `#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. 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>`_ `#1807 <https://github.com/huge-success/sanic/pull/1807>`_
Remove server config args that can be read directly from app Remove server config args that can be read directly from app
@@ -381,22 +189,22 @@ Dependencies
Developer infrastructure Developer infrastructure
************************ ************************
* *
`#1833 <https://github.com/huge-success/sanic/pull/1833>`_ `#1833 <https://github.com/huge-success/sanic/pull/1833>`_
Resolve broken documentation builds Resolve broken documentation builds
Improved Documentation Improved Documentation
********************** **********************
* *
`#1755 <https://github.com/huge-success/sanic/pull/1755>`_ `#1755 <https://github.com/huge-success/sanic/pull/1755>`_
Usage of ``response.empty()`` Usage of ``response.empty()``
* *
`#1778 <https://github.com/huge-success/sanic/pull/1778>`_ `#1778 <https://github.com/huge-success/sanic/pull/1778>`_
Update README Update README
* *
`#1783 <https://github.com/huge-success/sanic/pull/1783>`_ `#1783 <https://github.com/huge-success/sanic/pull/1783>`_
Fix typo Fix typo
@@ -423,7 +231,7 @@ Improved Documentation
* *
`#1834 <https://github.com/huge-success/sanic/pull/1834>`_ `#1834 <https://github.com/huge-success/sanic/pull/1834>`_
Order of listeners Order of listeners
Version 19.12.0 Version 19.12.0
=============== ===============
@@ -489,16 +297,16 @@ Version 19.6.2
Features Features
******** ********
* *
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_ `#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>`_ `requests-async <https://github.com/encode/requests-async>`_
* *
`#1475 <https://github.com/huge-success/sanic/pull/1475>`_ `#1475 <https://github.com/huge-success/sanic/pull/1475>`_
Added ASGI support (Beta) Added ASGI support (Beta)
* *
`#1436 <https://github.com/huge-success/sanic/pull/1436>`_ `#1436 <https://github.com/huge-success/sanic/pull/1436>`_
Add Configure support from object string Add Configure support from object string
@@ -506,19 +314,19 @@ Features
Bugfixes Bugfixes
******** ********
* *
`#1587 <https://github.com/huge-success/sanic/pull/1587>`_ `#1587 <https://github.com/huge-success/sanic/pull/1587>`_
Add missing handle for Expect header. Add missing handle for Expect header.
* *
`#1560 <https://github.com/huge-success/sanic/pull/1560>`_ `#1560 <https://github.com/huge-success/sanic/pull/1560>`_
Allow to disable Transfer-Encoding: chunked. Allow to disable Transfer-Encoding: chunked.
* *
`#1558 <https://github.com/huge-success/sanic/pull/1558>`_ `#1558 <https://github.com/huge-success/sanic/pull/1558>`_
Fix graceful shutdown. Fix graceful shutdown.
* *
`#1594 <https://github.com/huge-success/sanic/pull/1594>`_ `#1594 <https://github.com/huge-success/sanic/pull/1594>`_
Strict Slashes behavior fix Strict Slashes behavior fix
@@ -529,11 +337,11 @@ Deprecations and Removals
`#1544 <https://github.com/huge-success/sanic/pull/1544>`_ `#1544 <https://github.com/huge-success/sanic/pull/1544>`_
Drop dependency on distutil Drop dependency on distutil
* *
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_ `#1562 <https://github.com/huge-success/sanic/pull/1562>`_
Drop support for Python 3.5 Drop support for Python 3.5
* *
`#1568 <https://github.com/huge-success/sanic/pull/1568>`_ `#1568 <https://github.com/huge-success/sanic/pull/1568>`_
Deprecate route removal. Deprecate route removal.
@@ -550,39 +358,39 @@ Version 19.3
Features Features
******** ********
* *
`#1497 <https://github.com/huge-success/sanic/pull/1497>`_ `#1497 <https://github.com/huge-success/sanic/pull/1497>`_
Add support for zero-length and RFC 5987 encoded filename for Add support for zero-length and RFC 5987 encoded filename for
multipart/form-data requests. multipart/form-data requests.
* *
`#1484 <https://github.com/huge-success/sanic/pull/1484>`_ `#1484 <https://github.com/huge-success/sanic/pull/1484>`_
The type of ``expires`` attribute of ``sanic.cookies.Cookie`` is now The type of ``expires`` attribute of ``sanic.cookies.Cookie`` is now
enforced to be of type ``datetime``. enforced to be of type ``datetime``.
* *
`#1482 <https://github.com/huge-success/sanic/pull/1482>`_ `#1482 <https://github.com/huge-success/sanic/pull/1482>`_
Add support for the ``stream`` parameter of ``sanic.Sanic.add_route()`` Add support for the ``stream`` parameter of ``sanic.Sanic.add_route()``
available to ``sanic.Blueprint.add_route()``. available to ``sanic.Blueprint.add_route()``.
* *
`#1481 <https://github.com/huge-success/sanic/pull/1481>`_ `#1481 <https://github.com/huge-success/sanic/pull/1481>`_
Accept negative values for route parameters with type ``int`` or ``number``. Accept negative values for route parameters with type ``int`` or ``number``.
* *
`#1476 <https://github.com/huge-success/sanic/pull/1476>`_ `#1476 <https://github.com/huge-success/sanic/pull/1476>`_
Deprecated the use of ``sanic.request.Request.raw_args`` - it has a Deprecated the use of ``sanic.request.Request.raw_args`` - it has a
fundamental flaw in which is drops repeated query string parameters. fundamental flaw in which is drops repeated query string parameters.
Added ``sanic.request.Request.query_args`` as a replacement for the Added ``sanic.request.Request.query_args`` as a replacement for the
original use-case. original use-case.
* *
`#1472 <https://github.com/huge-success/sanic/pull/1472>`_ `#1472 <https://github.com/huge-success/sanic/pull/1472>`_
Remove an unwanted ``None`` check in Request class ``repr`` implementation. Remove an unwanted ``None`` check in Request class ``repr`` implementation.
This changes the default ``repr`` of a Request from ``<Request>`` to This changes the default ``repr`` of a Request from ``<Request>`` to
``<Request: None />`` ``<Request: None />``
* *
`#1470 <https://github.com/huge-success/sanic/pull/1470>`_ `#1470 <https://github.com/huge-success/sanic/pull/1470>`_
Added 2 new parameters to ``sanic.app.Sanic.create_server``\ : Added 2 new parameters to ``sanic.app.Sanic.create_server``\ :
@@ -593,21 +401,21 @@ Features
This is a breaking change. This is a breaking change.
* *
`#1499 <https://github.com/huge-success/sanic/pull/1499>`_ `#1499 <https://github.com/huge-success/sanic/pull/1499>`_
Added a set of test cases that test and benchmark route resolution. Added a set of test cases that test and benchmark route resolution.
* *
`#1457 <https://github.com/huge-success/sanic/pull/1457>`_ `#1457 <https://github.com/huge-success/sanic/pull/1457>`_
The type of the ``"max-age"`` value in a ``sanic.cookies.Cookie`` is now 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``. enforced to be an integer. Non-integer values are replaced with ``0``.
* *
`#1445 <https://github.com/huge-success/sanic/pull/1445>`_ `#1445 <https://github.com/huge-success/sanic/pull/1445>`_
Added the ``endpoint`` attribute to an incoming ``request``\ , containing the Added the ``endpoint`` attribute to an incoming ``request``\ , containing the
name of the handler function. name of the handler function.
* *
`#1423 <https://github.com/huge-success/sanic/pull/1423>`_ `#1423 <https://github.com/huge-success/sanic/pull/1423>`_
Improved request streaming. ``request.stream`` is now a bounded-size buffer Improved request streaming. ``request.stream`` is now a bounded-size buffer
instead of an unbounded queue. Callers must now call instead of an unbounded queue. Callers must now call
@@ -620,7 +428,7 @@ Bugfixes
******** ********
* *
`#1502 <https://github.com/huge-success/sanic/pull/1502>`_ `#1502 <https://github.com/huge-success/sanic/pull/1502>`_
Sanic was prefetching ``time.time()`` and updating it once per second to Sanic was prefetching ``time.time()`` and updating it once per second to
avoid excessive ``time.time()`` calls. The implementation was observed to avoid excessive ``time.time()`` calls. The implementation was observed to
@@ -628,25 +436,25 @@ Bugfixes
to negligible, so this has been removed. Fixes to negligible, so this has been removed. Fixes
`#1500 <https://github.com/huge-success/sanic/pull/1500>`_ `#1500 <https://github.com/huge-success/sanic/pull/1500>`_
* *
`#1501 <https://github.com/huge-success/sanic/pull/1501>`_ `#1501 <https://github.com/huge-success/sanic/pull/1501>`_
Fix a bug in the auto-reloader when the process was launched as a module 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 i.e. ``python -m init0.mod1`` where the sanic server is started
in ``init0/mod1.py`` with ``debug`` enabled and imports another module in in ``init0/mod1.py`` with ``debug`` enabled and imports another module in
``init0``. ``init0``.
* *
`#1376 <https://github.com/huge-success/sanic/pull/1376>`_ `#1376 <https://github.com/huge-success/sanic/pull/1376>`_
Allow sanic test client to bind to a random port by specifying Allow sanic test client to bind to a random port by specifying
``port=None`` when constructing a ``SanicTestClient`` ``port=None`` when constructing a ``SanicTestClient``
* *
`#1399 <https://github.com/huge-success/sanic/pull/1399>`_ `#1399 <https://github.com/huge-success/sanic/pull/1399>`_
Added the ability to specify middleware on a blueprint group, so that all Added the ability to specify middleware on a blueprint group, so that all
routes produced from the blueprints in the group have the middleware routes produced from the blueprints in the group have the middleware
applied. applied.
* *
`#1442 <https://github.com/huge-success/sanic/pull/1442>`_ `#1442 <https://github.com/huge-success/sanic/pull/1442>`_
Allow the the use the ``SANIC_ACCESS_LOG`` environment variable to Allow the the use the ``SANIC_ACCESS_LOG`` environment variable to
enable/disable the access log when not explicitly passed to ``app.run()``. enable/disable the access log when not explicitly passed to ``app.run()``.
@@ -688,7 +496,7 @@ Version 18.12
18.12.0 18.12.0
******* *******
* *
Changes: Changes:
@@ -706,7 +514,7 @@ Version 18.12
* Deprecate Handler.log * Deprecate Handler.log
* Pinned httptools requirement to version 0.0.10+ * Pinned httptools requirement to version 0.0.10+
* *
Fixes: Fixes:

View File

@@ -71,7 +71,7 @@ black:
black --config ./.black.toml sanic tests black --config ./.black.toml sanic tests
fix-import: black fix-import: black
isort sanic tests isort -rc sanic tests
docs-clean: docs-clean:

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 :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 .. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/huge-success/sanic :target: https://codecov.io/gh/huge-success/sanic
.. |Build Status| image:: https://travis-ci.com/huge-success/sanic.svg?branch=master .. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
:target: https://travis-ci.com/huge-success/sanic :target: https://travis-ci.org/huge-success/sanic
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true .. |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 :target: https://ci.appveyor.com/project/huge-success/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest .. |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 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/>`_. `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!** The project is maintained by the community, for the community. **Contributions are welcome!**
@@ -106,7 +104,7 @@ Hello World Example
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000) 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:: .. 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. 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 | | Version | LTS | Supported |
| ------- | ------------- | ------------------ | | ------- | ------------------ | ------------------ |
| 20.9 | | :heavy_check_mark: | | 19.6.0 | | :white_check_mark: |
| 20.6 | | :x: | | 19.3.1 | | :heavy_check_mark: |
| 20.3 | | :x: | | 18.12.0 | :heavy_check_mark: | :heavy_check_mark: |
| 19.12 | until 2021-12 | :white_check_mark: | | 0.8.3 | | :x: |
| 19.9 | | :x: | | 0.7.0 | | :x: |
| 19.6 | | :x: | | 0.6.0 | | :x: |
| 19.3 | | :x: | | 0.5.4 | | :x: |
| 18.12 | until 2020-12 | :white_check_mark: | | 0.4.1 | | :x: |
| 0.8.3 | | :x: | | 0.3.1 | | :x: |
| 0.7.0 | | :x: | | 0.2.0 | | :x: |
| 0.6.0 | | :x: | | 0.1.9 | | :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
## Reporting a Vulnerability ## 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

@@ -12,9 +12,9 @@ Sanic holds the configuration in the `config` attribute of the application objec
app = Sanic('myapp') app = Sanic('myapp')
app.config.DB_NAME = 'appdb' 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 .. code-block:: python
@@ -45,92 +45,11 @@ Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable
.. code-block:: python .. code-block:: python
app = Sanic(__name__, load_env=False) 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)
From an Object 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: 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 .. code-block:: python
@@ -152,10 +71,6 @@ You could use a class or any other object as well.
From a File 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: 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 .. 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 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 | | Variable | Default | Description |
@@ -208,10 +123,6 @@ Out of the box there are just a few predefined values which can be overwritten w
+---------------------------+-------------------+-----------------------------------------------------------------------------+ +---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_WRITE_LIMIT | 2^16 | High-water limit of the buffer for outgoing 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) | | GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+ +---------------------------+-------------------+-----------------------------------------------------------------------------+
| ACCESS_LOG | True | Disable or enable access log | | ACCESS_LOG | True | Disable or enable access log |

View File

@@ -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!*. the message *Hello world!*.
You now have a working Sanic server! 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

@@ -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): async def get_handler(request):
return text('GET request - {}'.format(request.args)) 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 .. code-block:: python

View File

@@ -88,5 +88,5 @@ When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `
app = Sanic(__name__) 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) app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)

View File

@@ -58,32 +58,6 @@ More information about
the available arguments to `httpx` can be found the available arguments to `httpx` can be found
[in the documentation for `httpx <https://www.encode.io/httpx/>`_. [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 Using a random port
------------------- -------------------

View File

@@ -51,9 +51,5 @@ You could setup your own WebSocket configuration through ``app.config``, like
app.config.WEBSOCKET_MAX_QUEUE = 32 app.config.WEBSOCKET_MAX_QUEUE = 32
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16 app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
app.config.WEBSOCKET_WRITE_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. Find more in ``Configuration`` section.

View File

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

View File

@@ -1,83 +1,28 @@
import os import os
import sys import sys
from argparse import ArgumentParser, RawDescriptionHelpFormatter from argparse import ArgumentParser
from importlib import import_module from importlib import import_module
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from sanic import __version__
from sanic.app import Sanic from sanic.app import Sanic
from sanic.config import BASE_LOGO
from sanic.log import logger 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(): def main():
parser = SanicArgumentParser( parser = ArgumentParser(prog="sanic")
prog="sanic", parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
description=BASE_LOGO, parser.add_argument("--port", dest="port", type=int, default=8000)
formatter_class=RawDescriptionHelpFormatter, parser.add_argument("--unix", dest="unix", type=str, default="")
)
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",
)
parser.add_argument( parser.add_argument(
"--cert", dest="cert", type=str, help="location of certificate for SSL" "--cert", dest="cert", type=str, help="location of certificate for SSL"
) )
parser.add_argument( parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL." "--key", dest="key", type=str, help="location of keyfile for SSL."
) )
parser.add_argument( parser.add_argument("--workers", dest="workers", type=int, default=1)
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="number of worker processes [default 1]",
)
parser.add_argument("--debug", dest="debug", action="store_true") parser.add_argument("--debug", dest="debug", action="store_true")
parser.add_bool_arguments( parser.add_argument("module")
"--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"
)
args = parser.parse_args() args = parser.parse_args()
try: try:
@@ -85,12 +30,9 @@ def main():
if module_path not in sys.path: if module_path not in sys.path:
sys.path.append(module_path) sys.path.append(module_path)
if ":" in args.module: module_parts = args.module.split(".")
module_name, app_name = args.module.rsplit(":", 1) module_name = ".".join(module_parts[:-1])
else: 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) module = import_module(module_name)
app = getattr(module, app_name, None) app = getattr(module, app_name, None)
@@ -115,7 +57,6 @@ def main():
unix=args.unix, unix=args.unix,
workers=args.workers, workers=args.workers,
debug=args.debug, debug=args.debug,
access_log=args.access_log,
ssl=ssl, ssl=ssl,
) )
except ImportError as e: except ImportError as e:

View File

@@ -1 +1 @@
__version__ = "20.12.6" __version__ = "20.6.3"

View File

@@ -2,18 +2,17 @@ import logging
import logging.config import logging.config
import os import os
import re import re
import sys import warnings
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from collections import defaultdict, deque from collections import defaultdict, deque
from functools import partial from functools import partial
from inspect import isawaitable, signature from inspect import getmodulename, isawaitable, signature, stack
from socket import socket from socket import socket
from ssl import Purpose, SSLContext, create_default_context from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc from traceback import format_exc
from typing import Any, Dict, Optional, Type, Union from typing import Any, Dict, Optional, Type, Union
from urllib.parse import urlencode, urlunparse from urllib.parse import urlencode, urlunparse
from warnings import warn
from sanic import reloader_helpers from sanic import reloader_helpers
from sanic.asgi import ASGIApp from sanic.asgi import ASGIApp
@@ -39,9 +38,6 @@ from sanic.websocket import ConnectionClosed, WebSocketProtocol
class Sanic: class Sanic:
_app_registry: Dict[str, "Sanic"] = {}
test_mode = False
def __init__( def __init__(
self, self,
name=None, name=None,
@@ -52,35 +48,27 @@ class Sanic:
strict_slashes=False, strict_slashes=False,
log_config=None, log_config=None,
configure_logging=True, configure_logging=True,
register=None,
): ):
# Get name from previous stack frame # Get name from previous stack frame
if name is None: if name is None:
raise SanicException( warnings.warn(
"Sanic instance cannot be unnamed. " "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.", "Please use Sanic(name='your_application_name') instead.",
DeprecationWarning,
stacklevel=2,
) )
frame_records = stack()[1]
name = getmodulename(frame_records[1])
# logging # logging
if configure_logging: if configure_logging:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
if sys.version_info >= (3, 10):
error_logger.error(
"Unsupported version of Python has been detected.\n\nPython "
f"version {sys.version} is not supported by this version of "
"Sanic. There is a security advisory that has been issued for "
"Sanic v20.12 while running Python 3.10+. You should either "
"use a supported version of Python (v3.6 - v3.9) or upgrade "
"Sanic to v21+.\n\nPlease see https://github.com/sanic-org/"
"sanic/security/advisories/GHSA-7p79-6x2v-5h88 for "
"more information.\n"
)
self.name = name self.name = name
self.asgi = False self.asgi = False
self.router = router or Router(self) self.router = router or Router()
self.request_class = request_class self.request_class = request_class
self.error_handler = error_handler or ErrorHandler() self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env) self.config = Config(load_env=load_env)
@@ -103,12 +91,6 @@ class Sanic:
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
if register is not None:
self.config.REGISTER = register
if self.config.REGISTER:
self.__class__.register_app(self)
@property @property
def loop(self): def loop(self):
"""Synonymous with asyncio.get_event_loop(). """Synonymous with asyncio.get_event_loop().
@@ -508,7 +490,9 @@ class Sanic:
websocket_handler = partial( websocket_handler = partial(
self._websocket_handler, handler, subprotocols=subprotocols self._websocket_handler, handler, subprotocols=subprotocols
) )
websocket_handler.__name__ = handler.__name__ websocket_handler.__name__ = (
"websocket_handler_" + handler.__name__
)
routes.extend( routes.extend(
self.router.add( self.router.add(
uri=uri, uri=uri,
@@ -691,10 +675,9 @@ class Sanic:
:param strict_slashes: Instruct :class:`Sanic` to check if the request :param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */* URLs need to terminate with a */*
:param content_type: user defined content type for header :param content_type: user defined content type for header
:return: routes registered on the router :return: None
:rtype: List[sanic.router.Route]
""" """
return static_register( static_register(
self, self,
uri, uri,
file_or_directory, file_or_directory,
@@ -729,6 +712,28 @@ class Sanic:
self._blueprint_order.append(blueprint) self._blueprint_order.append(blueprint)
blueprint.register(self, options) 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): def url_for(self, view_name: str, **kwargs):
r"""Build a URL based on a view name and the values provided. r"""Build a URL based on a view name and the values provided.
@@ -759,24 +764,6 @@ class Sanic:
kw.update(name=view_name) kw.update(name=view_name)
uri, route = self.router.find_route_by_view_name(view_name, **kw) 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): if not (uri and route):
raise URLBuildError( raise URLBuildError(
f"Endpoint with name `{view_name}` was not found" f"Endpoint with name `{view_name}` was not found"
@@ -911,9 +898,7 @@ class Sanic:
name = None name = None
try: try:
# Fetch handler from router # Fetch handler from router
handler, args, kwargs, uri, name, endpoint = self.router.get( handler, args, kwargs, uri, name = self.router.get(request)
request
)
# -------------------------------------------- # # -------------------------------------------- #
# Request Middleware # Request Middleware
@@ -935,8 +920,16 @@ class Sanic:
"handler from the router" "handler from the router"
) )
) )
else:
request.endpoint = endpoint 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 # Run response handler
response = handler(request, *args, **kwargs) response = handler(request, *args, **kwargs)
@@ -1037,6 +1030,7 @@ class Sanic:
workers: int = 1, workers: int = 1,
protocol: Optional[Type[Protocol]] = None, protocol: Optional[Type[Protocol]] = None,
backlog: int = 100, backlog: int = 100,
stop_event: Any = None,
register_sys_signals: bool = True, register_sys_signals: bool = True,
access_log: Optional[bool] = None, access_log: Optional[bool] = None,
unix: Optional[str] = None, unix: Optional[str] = None,
@@ -1066,6 +1060,9 @@ class Sanic:
:param backlog: a number of unaccepted connections that the system :param backlog: a number of unaccepted connections that the system
will allow before refusing new connections will allow before refusing new connections
:type backlog: int :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 :param register_sys_signals: Register SIG* events
:type register_sys_signals: bool :type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server) :param access_log: Enables writing access logs (slows server)
@@ -1093,6 +1090,13 @@ class Sanic:
protocol = ( protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol 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 passed explicitly change config.ACCESS_LOG
if access_log is not None: if access_log is not None:
self.config.ACCESS_LOG = access_log self.config.ACCESS_LOG = access_log
@@ -1149,6 +1153,7 @@ class Sanic:
sock: Optional[socket] = None, sock: Optional[socket] = None,
protocol: Type[Protocol] = None, protocol: Type[Protocol] = None,
backlog: int = 100, backlog: int = 100,
stop_event: Any = None,
access_log: Optional[bool] = None, access_log: Optional[bool] = None,
unix: Optional[str] = None, unix: Optional[str] = None,
return_asyncio_server=False, return_asyncio_server=False,
@@ -1181,6 +1186,9 @@ class Sanic:
:param backlog: a number of unaccepted connections that the system :param backlog: a number of unaccepted connections that the system
will allow before refusing new connections will allow before refusing new connections
:type backlog: int :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) :param access_log: Enables writing access logs (slows server)
:type access_log: bool :type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need :param return_asyncio_server: flag that defines whether there's a need
@@ -1200,6 +1208,13 @@ class Sanic:
protocol = ( protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol 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 passed explicitly change config.ACCESS_LOG
if access_log is not None: if access_log is not None:
self.config.ACCESS_LOG = access_log self.config.ACCESS_LOG = access_log
@@ -1281,6 +1296,7 @@ class Sanic:
loop=None, loop=None,
protocol=HttpProtocol, protocol=HttpProtocol,
backlog=100, backlog=100,
stop_event=None,
register_sys_signals=True, register_sys_signals=True,
run_async=False, run_async=False,
auto_reload=False, auto_reload=False,
@@ -1295,6 +1311,13 @@ class Sanic:
context = create_default_context(purpose=Purpose.CLIENT_AUTH) context = create_default_context(purpose=Purpose.CLIENT_AUTH)
context.load_cert_chain(cert, keyfile=key) context.load_cert_chain(cert, keyfile=key)
ssl = context 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: if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError( raise ValueError(
"PROXIES_COUNT cannot be negative. " "PROXIES_COUNT cannot be negative. "
@@ -1428,42 +1451,3 @@ class Sanic:
self.asgi = True self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send) asgi_app = await ASGIApp.create(self, scope, receive, send)
await asgi_app() 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

@@ -98,9 +98,7 @@ class MockTransport:
def create_websocket_connection( def create_websocket_connection(
self, send: ASGISend, receive: ASGIReceive self, send: ASGISend, receive: ASGIReceive
) -> WebSocketConnection: ) -> WebSocketConnection:
self._websocket_connection = WebSocketConnection( self._websocket_connection = WebSocketConnection(send, receive)
send, receive, self.scope.get("subprotocols", [])
)
return self._websocket_connection return self._websocket_connection
def add_task(self) -> None: def add_task(self) -> None:
@@ -312,19 +310,13 @@ class ASGIApp:
callback = None if self.ws else self.stream_callback callback = None if self.ws else self.stream_callback
await handler(self.request, None, callback) await handler(self.request, None, callback)
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable async def stream_callback(self, response: HTTPResponse) -> None:
async def stream_callback(
self, response: Union[HTTPResponse, StreamingHTTPResponse]
) -> None:
""" """
Write the response. Write the response.
""" """
headers: List[Tuple[bytes, bytes]] = [] headers: List[Tuple[bytes, bytes]] = []
cookies: Dict[str, str] = {} cookies: Dict[str, str] = {}
content_length: List[str] = []
try: try:
content_length = response.headers.popall("content-length", [])
cookies = { cookies = {
v.key: v v.key: v
for _, v in list( for _, v in list(
@@ -356,23 +348,11 @@ class ASGIApp:
if name not in (b"Set-Cookie",) if name not in (b"Set-Cookie",)
] ]
response.asgi = True if "content-length" not in response.headers and not isinstance(
is_streaming = isinstance(response, StreamingHTTPResponse) response, StreamingHTTPResponse
if is_streaming and getattr(response, "chunked", False): ):
# disable sanic chunking, this is done at the ASGI-server level
setattr(response, "chunked", False)
# content-length header is removed to signal to the ASGI-server
# to use automatic-chunking if it supports it
elif len(content_length) > 0:
headers += [ headers += [
(b"content-length", str(content_length[0]).encode("latin-1")) (b"content-length", str(len(response.body)).encode("latin-1"))
]
elif not is_streaming:
headers += [
(
b"content-length",
str(len(getattr(response, "body", b""))).encode("latin-1"),
)
] ]
if "content-type" not in response.headers: if "content-type" not in response.headers:

View File

@@ -143,18 +143,7 @@ class Blueprint:
if _routes: if _routes:
routes += _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 if route]
# Middleware # Middleware
for future in self.middlewares: for future in self.middlewares:
if future.args or future.kwargs: if future.args or future.kwargs:
@@ -171,6 +160,14 @@ class Blueprint:
for future in self.exceptions: for future in self.exceptions:
app.exception(*future.args, **future.kwargs)(future.handler) 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 # Event listeners
for event, listeners in self.listeners.items(): for event, listeners in self.listeners.items():
for listener in listeners: for listener in listeners:

View File

@@ -14,12 +14,12 @@ class Header(CIMultiDict):
use_trio = argv[0].endswith("hypercorn") and "trio" in argv use_trio = argv[0].endswith("hypercorn") and "trio" in argv
if use_trio: if use_trio:
from trio import Path # type: ignore from trio import open_file as open_async, Path # type: ignore
from trio import open_file as open_async # type: ignore
def stat_async(path): def stat_async(path):
return Path(path).stat() return Path(path).stat()
else: else:
from aiofiles import open as aio_open # type: ignore from aiofiles import open as aio_open # type: ignore
from aiofiles.os import stat as stat_async # type: ignore # noqa: F401 from aiofiles.os import stat as stat_async # type: ignore # noqa: F401

View File

@@ -1,15 +1,8 @@
from os import environ import os
from typing import Any, Union import types
# NOTE(tomaszdrozdz): remove in version: 21.3 from sanic.exceptions import PyFileError
# We replace from_envvar(), from_object(), from_pyfile() config object methods from sanic.helpers import import_string
# 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
SANIC_PREFIX = "SANIC_" SANIC_PREFIX = "SANIC_"
@@ -31,16 +24,12 @@ DEFAULT_CONFIG = {
"WEBSOCKET_MAX_QUEUE": 32, "WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16, "WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16, "WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"WEBSOCKET_PING_TIMEOUT": 20,
"WEBSOCKET_PING_INTERVAL": 20,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True, "ACCESS_LOG": True,
"FORWARDED_SECRET": None, "FORWARDED_SECRET": None,
"REAL_IP_HEADER": None, "REAL_IP_HEADER": None,
"PROXIES_COUNT": None, "PROXIES_COUNT": None,
"FORWARDED_FOR_HEADER": "X-Forwarded-For", "FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FALLBACK_ERROR_FORMAT": "html",
"REGISTER": True,
} }
@@ -67,23 +56,76 @@ class Config(dict):
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
self[attr] = value self[attr] = value
# NOTE(tomaszdrozdz): remove in version: 21.3 def from_envvar(self, variable_name):
# We replace from_envvar(), from_object(), from_pyfile() config object """Load a configuration from an environment variable pointing to
# methods with one simpler update_config() method. a configuration file.
# We also replace "loading module from file code" in from_pyfile()
# in a favour of load_module_from_file_location(). :param variable_name: name of the environment variable
# Please see pull request: 1903 :return: bool. ``True`` if able to load config, ``False`` otherwise.
# and issue: 1895 """
from_envvar = from_envvar config_file = os.environ.get(variable_name)
from_pyfile = from_pyfile if not config_file:
from_object = from_object 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): def load_environment_vars(self, prefix=SANIC_PREFIX):
""" """
Looks for prefixed environment variables and applies Looks for prefixed environment variables and applies
them to the configuration if present. them to the configuration if present.
""" """
for k, v in environ.items(): for k, v in os.environ.items():
if k.startswith(prefix): if k.startswith(prefix):
_, config_key = k.split(prefix, 1) _, config_key = k.split(prefix, 1)
try: try:
@@ -93,47 +135,23 @@ class Config(dict):
self[config_key] = float(v) self[config_key] = float(v)
except ValueError: except ValueError:
try: try:
self[config_key] = str_to_bool(v) self[config_key] = strtobool(v)
except ValueError: except ValueError:
self[config_key] = v 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 The function was modified to walk its talk and actually return bool
holding settings. and not int.
"""
# /some/py/file val = val.lower()
A = 1 if val in ("y", "yes", "t", "true", "on", "1"):
B = 2 return True
elif val in ("n", "no", "f", "false", "off", "0"):
config.update_config("${some}/py/file") return False
else:
Yes you can put environment variable here, but they must be provided raise ValueError("invalid truth value %r" % (val,))
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)

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,283 +1,13 @@
import sys import sys
import typing as t
from functools import partial
from traceback import extract_tb from traceback import extract_tb
from sanic.exceptions import InvalidUsage, SanicException from sanic.exceptions import SanicException
from sanic.helpers import STATUS_CODES from sanic.helpers import STATUS_CODES
from sanic.request import Request from sanic.response import html
from sanic.response import HTTPResponse, html, json, text
try: # Here, There Be Dragons (custom HTML formatting to follow)
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): def escape(text):
@@ -285,46 +15,103 @@ def escape(text):
return f"{text}".replace("&", "&amp;").replace("<", "&lt;") return f"{text}".replace("&", "&amp;").replace("<", "&lt;")
RENDERERS_BY_CONFIG = { def exception_response(request, exception, debug):
"html": HTMLRenderer, status = 500
"json": JSONRenderer, text = (
"text": TextRenderer, "The server encountered an internal error "
} "and cannot complete your request."
)
RENDERERS_BY_CONTENT_TYPE = { headers = {}
"multipart/form-data": HTMLRenderer, if isinstance(exception, SanicException):
"application/json": JSONRenderer, text = f"{exception}"
"text/plain": TextRenderer, status = getattr(exception, "status_code", status)
} headers = getattr(exception, "headers", headers)
elif debug:
text = f"{exception}"
status_text = STATUS_CODES.get(status, b"Error Occurred").decode()
title = escape(f"{status}{status_text}")
text = escape(text)
if debug and not getattr(exception, "quiet", False):
return html(
f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>"
f"<style>{TRACEBACK_STYLE}</style>\n"
f"<h1>⚠️ {title}</h1><p>{text}\n"
f"{_render_traceback_html(request, exception)}",
status=status,
)
# Keeping it minimal with trailing newline for pretty curl/console output
return html(
f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>"
"<style>html { font-family: sans-serif }</style>\n"
f"<h1>⚠️ {title}</h1><p>{text}\n",
status=status,
headers=headers,
)
def exception_response( def _render_exception(exception):
request: Request, frames = extract_tb(exception.__traceback__)
exception: Exception, frame_html = "".join(TRACEBACK_LINE_HTML.format(frame) for frame in frames)
debug: bool, return TRACEBACK_WRAPPER_HTML.format(
renderer: t.Type[t.Optional[BaseRenderer]] = None, exc_name=escape(exception.__class__.__name__),
) -> HTTPResponse: exc_value=escape(exception),
"""Render a response for the default FALLBACK exception handler""" frame_html=frame_html,
)
if not renderer:
renderer = HTMLRenderer
if request: def _render_traceback_html(request, exception):
if request.app.config.FALLBACK_ERROR_FORMAT == "auto": exc_type, exc_value, tb = sys.exc_info()
try: exceptions = []
renderer = JSONRenderer if request.json else HTMLRenderer while exc_value:
except InvalidUsage: exceptions.append(_render_exception(exc_value))
renderer = HTMLRenderer exc_value = exc_value.__cause__
content_type, *_ = request.headers.get( traceback_html = TRACEBACK_BORDER.join(reversed(exceptions))
"content-type", "" appname = escape(request.app.name)
).split(";") name = escape(exception.__class__.__name__)
renderer = RENDERERS_BY_CONTENT_TYPE.get( value = escape(exception)
content_type, renderer path = escape(request.path)
) return (
else: f"<h2>Traceback of {appname} (most recent call last):</h2>"
render_format = request.app.config.FALLBACK_ERROR_FORMAT f"{traceback_html}"
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer) "<div class=summary><p>"
f"<b>{name}: {value}</b> while handling path <code>{path}</code>"
)
renderer = t.cast(t.Type[BaseRenderer], renderer)
return renderer(request, exception, debug).render() 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>"
)

View File

@@ -169,18 +169,14 @@ class Unauthorized(SanicException):
} }
class LoadFileException(SanicException):
pass
def abort(status_code, message=None): def abort(status_code, message=None):
""" """
Raise an exception based on SanicException. Returns the HTTP response Raise an exception based on SanicException. Returns the HTTP response
message appropriate for the given status code, unless provided. message appropriate for the given status code, unless provided.
:param status_code: The HTTP status code to return. :param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages in :param message: The HTTP response body. Defaults to the messages
STATUS_CODES from sanic.helpers for the given status code. in response.py for the given status code.
""" """
if message is None: if message is None:
message = STATUS_CODES.get(status_code) message = STATUS_CODES.get(status_code)

View File

View File

@@ -50,7 +50,7 @@ class StreamBuffer:
self._queue = asyncio.Queue(buffer_size) self._queue = asyncio.Queue(buffer_size)
async def read(self): async def read(self):
"""Stop reading when gets None""" """ Stop reading when gets None """
payload = await self._queue.get() payload = await self._queue.get()
self._queue.task_done() self._queue.task_done()
return payload return payload
@@ -136,18 +136,15 @@ class Request:
return f"<{class_name}: {self.method} {self.path}>" return f"<{class_name}: {self.method} {self.path}>"
def body_init(self): def body_init(self):
""".. deprecated:: 20.3 """.. deprecated:: 20.3"""
To be removed in 21.3"""
self.body = [] self.body = []
def body_push(self, data): def body_push(self, data):
""".. deprecated:: 20.3 """.. deprecated:: 20.3"""
To be removed in 21.3"""
self.body.append(data) self.body.append(data)
def body_finish(self): def body_finish(self):
""".. deprecated:: 20.3 """.. deprecated:: 20.3"""
To be removed in 21.3"""
self.body = b"".join(self.body) self.body = b"".join(self.body)
async def receive_body(self): async def receive_body(self):
@@ -265,12 +262,9 @@ class Request:
:type errors: str :type errors: str
:return: RequestParameters :return: RequestParameters
""" """
if ( if not self.parsed_args[
keep_blank_values, (keep_blank_values, strict_parsing, encoding, errors)
strict_parsing, ]:
encoding,
errors,
) not in self.parsed_args:
if self.query_string: if self.query_string:
self.parsed_args[ self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors) (keep_blank_values, strict_parsing, encoding, errors)
@@ -324,12 +318,9 @@ class Request:
:type errors: str :type errors: str
:return: list :return: list
""" """
if ( if not self.parsed_not_grouped_args[
keep_blank_values, (keep_blank_values, strict_parsing, encoding, errors)
strict_parsing, ]:
encoding,
errors,
) not in self.parsed_not_grouped_args:
if self.query_string: if self.query_string:
self.parsed_not_grouped_args[ self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors) (keep_blank_values, strict_parsing, encoding, errors)

View File

@@ -1,3 +1,5 @@
import warnings
from functools import partial from functools import partial
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path
@@ -12,20 +14,15 @@ from sanic.helpers import has_message_body, remove_entity_headers
try: try:
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
except ImportError: 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 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=(",", ":")) json_dumps = partial(dumps, separators=(",", ":"))
class BaseHTTPResponse: class BaseHTTPResponse:
def __init__(self):
self.asgi = False
def _encode_body(self, data): def _encode_body(self, data):
if data is None:
return b""
return data.encode() if hasattr(data, "encode") else data return data.encode() if hasattr(data, "encode") else data
def _parse_headers(self): def _parse_headers(self):
@@ -45,7 +42,7 @@ class BaseHTTPResponse:
body=b"", body=b"",
): ):
""".. deprecated:: 20.3: """.. deprecated:: 20.3:
This function is not public API and will be removed in 21.3.""" This function is not public API and will be removed."""
# self.headers get priority over content_type # self.headers get priority over content_type
if self.content_type and "Content-Type" not in self.headers: if self.content_type and "Content-Type" not in self.headers:
@@ -83,8 +80,6 @@ class StreamingHTTPResponse(BaseHTTPResponse):
content_type="text/plain; charset=utf-8", content_type="text/plain; charset=utf-8",
chunked=True, chunked=True,
): ):
super().__init__()
self.content_type = content_type self.content_type = content_type
self.streaming_fn = streaming_fn self.streaming_fn = streaming_fn
self.status = status self.status = status
@@ -100,8 +95,6 @@ class StreamingHTTPResponse(BaseHTTPResponse):
""" """
data = self._encode_body(data) 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.
if self.chunked: if self.chunked:
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
else: else:
@@ -116,14 +109,13 @@ class StreamingHTTPResponse(BaseHTTPResponse):
""" """
if version != "1.1": if version != "1.1":
self.chunked = False self.chunked = False
if not getattr(self, "asgi", False): headers = self.get_headers(
headers = self.get_headers( version,
version, keep_alive=keep_alive,
keep_alive=keep_alive, keep_alive_timeout=keep_alive_timeout,
keep_alive_timeout=keep_alive_timeout, )
) await self.protocol.push_data(headers)
await self.protocol.push_data(headers) await self.protocol.drain()
await self.protocol.drain()
await self.streaming_fn(self) await self.streaming_fn(self)
if self.chunked: if self.chunked:
await self.protocol.push_data(b"0\r\n\r\n") await self.protocol.push_data(b"0\r\n\r\n")
@@ -149,15 +141,20 @@ class HTTPResponse(BaseHTTPResponse):
status=200, status=200,
headers=None, headers=None,
content_type=None, content_type=None,
body_bytes=b"",
): ):
super().__init__()
self.content_type = content_type self.content_type = content_type
self.body = self._encode_body(body) self.body = body_bytes if body is None else self._encode_body(body)
self.status = status self.status = status
self.headers = Header(headers or {}) self.headers = Header(headers or {})
self._cookies = None self._cookies = None
if body_bytes:
warnings.warn(
"Parameter `body_bytes` is deprecated, use `body` instead",
DeprecationWarning,
)
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
body = b"" body = b""
if has_message_body(self.status): if has_message_body(self.status):
@@ -221,10 +218,20 @@ def text(
:param content_type: the content type (string) of the response :param content_type: the content type (string) of the response
""" """
if not isinstance(body, str): if not isinstance(body, str):
raise TypeError( warnings.warn(
f"Bad body type. Expected str, got {type(body).__name__})" "Types other than str will be deprecated in future versions for"
f" response.text, got type {type(body).__name__})",
DeprecationWarning,
) )
# Type conversions are deprecated and quite b0rked but still supported for
# text() until applications get fixed. This try-except should be removed.
try:
# Avoid repr(body).encode() b0rkage for body that is already encoded.
# memoryview used only to test bytes-ishness.
with memoryview(body):
pass
except TypeError:
body = f"{body}" # no-op if body is already str
return HTTPResponse( return HTTPResponse(
body, status=status, headers=headers, content_type=content_type body, status=status, headers=headers, content_type=content_type
) )
@@ -242,10 +249,7 @@ def raw(
:param content_type: the content type (string) of the response. :param content_type: the content type (string) of the response.
""" """
return HTTPResponse( return HTTPResponse(
body=body, body=body, status=status, headers=headers, content_type=content_type,
status=status,
headers=headers,
content_type=content_type,
) )

View File

@@ -11,16 +11,7 @@ from sanic.views import CompositionView
Route = namedtuple( Route = namedtuple(
"Route", "Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
[
"handler",
"methods",
"pattern",
"parameters",
"name",
"uri",
"endpoint",
],
) )
Parameter = namedtuple("Parameter", ["name", "cast"]) Parameter = namedtuple("Parameter", ["name", "cast"])
@@ -88,8 +79,7 @@ class Router:
routes_always_check = None routes_always_check = None
parameter_pattern = re.compile(r"<(.+?)>") parameter_pattern = re.compile(r"<(.+?)>")
def __init__(self, app): def __init__(self):
self.app = app
self.routes_all = {} self.routes_all = {}
self.routes_names = {} self.routes_names = {}
self.routes_static_files = {} self.routes_static_files = {}
@@ -309,15 +299,11 @@ class Router:
handler_name = f"{bp_name}.{name or handler.__name__}" handler_name = f"{bp_name}.{name or handler.__name__}"
else: else:
handler_name = name or getattr( handler_name = name or getattr(handler, "__name__", None)
handler, "__name__", handler.__class__.__name__
)
if route: if route:
route = merge_route(route, methods, handler) route = merge_route(route, methods, handler)
else: else:
endpoint = self.app._build_endpoint_name(handler_name)
route = Route( route = Route(
handler=handler, handler=handler,
methods=methods, methods=methods,
@@ -325,7 +311,6 @@ class Router:
parameters=parameters, parameters=parameters,
name=handler_name, name=handler_name,
uri=uri, uri=uri,
endpoint=endpoint,
) )
self.routes_all[uri] = route self.routes_all[uri] = route
@@ -464,11 +449,10 @@ class Router:
route_handler = route.handler route_handler = route.handler
if hasattr(route_handler, "handlers"): if hasattr(route_handler, "handlers"):
route_handler = route_handler.handlers[method] route_handler = route_handler.handlers[method]
return route_handler, [], kwargs, route.uri, route.name
return route_handler, [], kwargs, route.uri, route.name, route.endpoint
def is_stream_handler(self, request): def is_stream_handler(self, request):
"""Handler for request is stream or not. """ Handler for request is stream or not.
:param request: Request object :param request: Request object
:return: bool :return: bool
""" """

View File

@@ -14,13 +14,11 @@ from ipaddress import ip_address
from signal import SIG_IGN, SIGINT, SIGTERM, Signals from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func from signal import signal as signal_func
from time import time from time import time
from typing import Dict, Type, Union
from httptools import HttpRequestParser # type: ignore from httptools import HttpRequestParser # type: ignore
from httptools.parser.errors import HttpParserError # type: ignore from httptools.parser.errors import HttpParserError # type: ignore
from sanic.compat import Header, ctrlc_workaround_for_windows from sanic.compat import Header, ctrlc_workaround_for_windows
from sanic.config import Config
from sanic.exceptions import ( from sanic.exceptions import (
HeaderExpectationFailed, HeaderExpectationFailed,
InvalidUsage, InvalidUsage,
@@ -169,11 +167,7 @@ class HttpProtocol(asyncio.Protocol):
self.request_class = self.app.request_class or Request self.request_class = self.app.request_class or Request
self.is_request_stream = self.app.is_request_stream self.is_request_stream = self.app.is_request_stream
self._is_stream_handler = False self._is_stream_handler = False
self._not_paused = ( self._not_paused = asyncio.Event(loop=deprecated_loop)
asyncio.Event()
if sys.version_info >= (3, 10)
else asyncio.Event(loop=deprecated_loop)
)
self._total_request_size = 0 self._total_request_size = 0
self._request_timeout_handler = None self._request_timeout_handler = None
self._response_timeout_handler = None self._response_timeout_handler = None
@@ -422,13 +416,12 @@ class HttpProtocol(asyncio.Protocol):
async def stream_append(self): async def stream_append(self):
while self._body_chunks: while self._body_chunks:
body = self._body_chunks.popleft() body = self._body_chunks.popleft()
if self.request: if self.request.stream.is_full():
if self.request.stream.is_full(): self.transport.pause_reading()
self.transport.pause_reading() await self.request.stream.put(body)
await self.request.stream.put(body) self.transport.resume_reading()
self.transport.resume_reading() else:
else: await self.request.stream.put(body)
await self.request.stream.put(body)
def on_message_complete(self): def on_message_complete(self):
# Entire request (headers and whole body) is received. # Entire request (headers and whole body) is received.
@@ -851,7 +844,6 @@ def serve(
app.asgi = False app.asgi = False
connections = connections if connections is not None else set() connections = connections if connections is not None else set()
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
server = partial( server = partial(
protocol, protocol,
loop=loop, loop=loop,
@@ -860,7 +852,6 @@ def serve(
app=app, app=app,
state=state, state=state,
unix=unix, unix=unix,
**protocol_kwargs,
) )
asyncio_server_kwargs = ( asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {} asyncio_server_kwargs if asyncio_server_kwargs else {}
@@ -957,21 +948,6 @@ def serve(
remove_unix_socket(unix) 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: def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
"""Create TCP server socket. """Create TCP server socket.
:param host: IPv4, IPv6 or hostname may be specified :param host: IPv4, IPv6 or hostname may be specified

View File

@@ -13,7 +13,6 @@ from sanic.exceptions import (
InvalidUsage, InvalidUsage,
) )
from sanic.handlers import ContentRangeHandler from sanic.handlers import ContentRangeHandler
from sanic.log import error_logger
from sanic.response import HTTPResponse, file, file_stream from sanic.response import HTTPResponse, file, file_stream
@@ -41,10 +40,6 @@ async def _static_request_handler(
# match filenames which got encoded (filenames with spaces etc) # match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path)) file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_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( raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri "File not found", path=file_or_directory, relative_url=file_uri
) )
@@ -99,10 +94,6 @@ async def _static_request_handler(
except ContentRangeError: except ContentRangeError:
raise raise
except Exception: except Exception:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={file_uri}"
)
raise FileNotFound( raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri "File not found", path=file_or_directory, relative_url=file_uri
) )
@@ -143,8 +134,6 @@ def register(
threshold size to switch to file_stream() threshold size to switch to file_stream()
:param name: user defined name used for url_for :param name: user defined name used for url_for
:param content_type: user defined content type for header :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, # If we're not trying to match a file directly,
# serve from the folder # serve from the folder
@@ -166,11 +155,10 @@ def register(
) )
) )
_routes, _ = app.route( app.route(
uri, uri,
methods=["GET", "HEAD"], methods=["GET", "HEAD"],
name=name, name=name,
host=host, host=host,
strict_slashes=strict_slashes, strict_slashes=strict_slashes,
)(_handler) )(_handler)
return _routes

View File

@@ -11,8 +11,6 @@ from sanic.response import text
ASGI_HOST = "mockserver" ASGI_HOST = "mockserver"
ASGI_PORT = 1234
ASGI_BASE_URL = f"http://{ASGI_HOST}:{ASGI_PORT}"
HOST = "127.0.0.1" HOST = "127.0.0.1"
PORT = None PORT = None
@@ -24,14 +22,6 @@ class SanicTestClient:
self.port = port self.port = port
self.host = host 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): def get_new_session(self):
return httpx.AsyncClient(verify=False) return httpx.AsyncClient(verify=False)
@@ -105,9 +95,7 @@ class SanicTestClient:
if self.port: if self.port:
server_kwargs = dict( server_kwargs = dict(
host=host or self.host, host=host or self.host, port=self.port, **server_kwargs,
port=self.port,
**server_kwargs,
) )
host, port = host or self.host, self.port host, port = host or self.host, self.port
else: else:
@@ -197,33 +185,30 @@ async def app_call_with_return(self, scope, receive, send):
return await asgi_app() return await asgi_app()
class SanicASGIDispatch(httpx.ASGIDispatch):
pass
class SanicASGITestClient(httpx.AsyncClient): class SanicASGITestClient(httpx.AsyncClient):
def __init__( def __init__(
self, self,
app, app,
base_url: str = ASGI_BASE_URL, base_url: str = f"http://{ASGI_HOST}",
suppress_exceptions: bool = False, suppress_exceptions: bool = False,
) -> None: ) -> None:
app.__class__.__call__ = app_call_with_return app.__class__.__call__ = app_call_with_return
app.asgi = True app.asgi = True
self.app = app 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 or 0))
super().__init__(dispatch=dispatch, base_url=base_url)
self.last_request = None self.last_request = None
def _collect_request(request): def _collect_request(request):
self.last_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) app.request_middleware.appendleft(_collect_request)
async def request(self, method, url, gather_request=True, *args, **kwargs): async def request(self, method, url, gather_request=True, *args, **kwargs):

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,7 +90,6 @@ class CompositionView:
def __init__(self): def __init__(self):
self.handlers = {} self.handlers = {}
self.name = self.__class__.__name__
def add(self, methods, handler, stream=False): def add(self, methods, handler, stream=False):
if stream: if stream:

View File

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

View File

@@ -5,7 +5,7 @@ import signal
import sys import sys
import traceback 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.server import HttpProtocol, Signal, serve, trigger_events
from sanic.websocket import WebSocketProtocol from sanic.websocket import WebSocketProtocol
@@ -174,7 +174,7 @@ class GunicornWorker(base.Worker):
@staticmethod @staticmethod
def _create_ssl_context(cfg): def _create_ssl_context(cfg):
"""Creates SSLContext instance for usage in asyncio.create_server. """ Creates SSLContext instance for usage in asyncio.create_server.
See ssl.SSLSocket.__init__ for more details. See ssl.SSLSocket.__init__ for more details.
""" """
ctx = ssl.SSLContext(cfg.ssl_version) ctx = ssl.SSLContext(cfg.ssl_version)

View File

@@ -11,3 +11,11 @@ line_length = 79
lines_after_imports = 2 lines_after_imports = 2
lines_between_types = 1 lines_between_types = 1
multi_line_output = 3 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

@@ -5,7 +5,6 @@ import codecs
import os import os
import re import re
import sys import sys
from distutils.util import strtobool from distutils.util import strtobool
from setuptools import setup from setuptools import setup
@@ -25,7 +24,6 @@ class PyTest(TestCommand):
def run_tests(self): def run_tests(self):
import shlex import shlex
import pytest import pytest
errno = pytest.main(shlex.split(self.pytest_args)) errno = pytest.main(shlex.split(self.pytest_args))
@@ -40,9 +38,7 @@ def open_local(paths, mode="r", encoding="utf8"):
with open_local(["sanic", "__version__.py"], encoding="latin1") as fp: with open_local(["sanic", "__version__.py"], encoding="latin1") as fp:
try: try:
version = re.findall( version = re.findall(r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M)[0]
r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M
)[0]
except IndexError: except IndexError:
raise RuntimeError("Unable to determine version.") raise RuntimeError("Unable to determine version.")
@@ -57,12 +53,10 @@ setup_kwargs = {
"author": "Sanic Community", "author": "Sanic Community",
"author_email": "admhpkns@gmail.com", "author_email": "admhpkns@gmail.com",
"description": ( "description": (
"A web server and web framework that's written to go fast. " "A web server and web framework that's written to go fast. Build fast. Run fast."
"Build fast. Run fast."
), ),
"long_description": long_description, "long_description": long_description,
"packages": ["sanic"], "packages": ["sanic"],
"package_data": {"sanic": ["py.typed"]},
"platforms": "any", "platforms": "any",
"python_requires": ">=3.6", "python_requires": ">=3.6",
"classifiers": [ "classifiers": [
@@ -72,14 +66,11 @@ setup_kwargs = {
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
], ],
"entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]}, "entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]},
} }
env_dependency = ( env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"'
'; sys_platform != "win32" ' 'and implementation_name == "cpython"'
)
ujson = "ujson>=1.35" + env_dependency ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency uvloop = "uvloop>=0.5.3" + env_dependency
@@ -87,25 +78,24 @@ requirements = [
"httptools>=0.0.10", "httptools>=0.0.10",
uvloop, uvloop,
ujson, ujson,
"aiofiles>=0.6.0", "aiofiles>=0.3.0",
"websockets>=8.1,<=9.1", "websockets>=8.1,<9.0",
"multidict>=5.0,<6.0", "multidict>=4.0,<5.0",
"httpx==0.15.4", "httpx==0.11.1",
] ]
tests_require = [ tests_require = [
"pytest==5.2.1", "pytest==5.2.1",
"multidict>=5.0,<6.0", "multidict>=4.0,<5.0",
"gunicorn==20.0.4", "gunicorn",
"pytest-cov", "pytest-cov",
"httpcore==0.11.*", "httpcore==0.3.0",
"beautifulsoup4", "beautifulsoup4",
uvloop, uvloop,
ujson, ujson,
"pytest-sanic", "pytest-sanic",
"pytest-sugar", "pytest-sugar",
"pytest-benchmark", "pytest-benchmark",
"pytest-dependency",
] ]
docs_require = [ docs_require = [

View File

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

View File

@@ -1,6 +1,7 @@
# Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker simple_server:main # Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker simple_server:main
""" Minimal helloworld application. """ Minimal helloworld application.
""" """
import ujson import ujson
from wheezy.http import HTTPResponse, WSGIApplication from wheezy.http import HTTPResponse, WSGIApplication
@@ -38,7 +39,6 @@ main = WSGIApplication(
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
from wsgiref.simple_server import make_server from wsgiref.simple_server import make_server
try: try:

View File

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

View File

@@ -3,8 +3,6 @@ import logging
import sys import sys
from inspect import isawaitable from inspect import isawaitable
from os import environ
from unittest.mock import patch
import pytest import pytest
@@ -118,7 +116,7 @@ def test_app_route_raise_value_error(app):
def test_app_handle_request_handler_is_none(app, monkeypatch): def test_app_handle_request_handler_is_none(app, monkeypatch):
def mockreturn(*args, **kwargs): def mockreturn(*args, **kwargs):
return None, [], {}, "", "", None return None, [], {}, "", ""
# Not sure how to make app.router.get() return None, so use mock here. # Not sure how to make app.router.get() return None, so use mock here.
monkeypatch.setattr(app.router, "get", mockreturn) monkeypatch.setattr(app.router, "get", mockreturn)
@@ -127,7 +125,7 @@ def test_app_handle_request_handler_is_none(app, monkeypatch):
def handler(request): def handler(request):
return text("test") return text("test")
_, response = app.test_client.get("/test") request, response = app.test_client.get("/test")
assert ( assert (
"'None' was returned while requesting a handler from the router" "'None' was returned while requesting a handler from the router"
@@ -150,43 +148,6 @@ def test_app_enable_websocket(app, websocket_enabled, enable):
assert app.websocket_enabled == True 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): def test_handle_request_with_nested_exception(app, monkeypatch):
err_msg = "Mock Exception" err_msg = "Mock Exception"
@@ -259,66 +220,5 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
def test_app_name_required(): def test_app_name_required():
with pytest.raises(SanicException): with pytest.deprecated_call():
Sanic() 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,6 @@
import asyncio
import sys
from collections import deque, namedtuple from collections import deque, namedtuple
import pytest import pytest
@@ -79,6 +82,14 @@ def test_listeners_triggered(app):
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
server.run() server.run()
all_tasks = (
asyncio.Task.all_tasks()
if sys.version_info < (3, 7)
else asyncio.all_tasks(asyncio.get_event_loop())
)
for task in all_tasks:
task.cancel()
assert before_server_start assert before_server_start
assert after_server_start assert after_server_start
assert before_server_stop assert before_server_stop
@@ -121,6 +132,14 @@ def test_listeners_triggered_async(app):
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
server.run() server.run()
all_tasks = (
asyncio.Task.all_tasks()
if sys.version_info < (3, 7)
else asyncio.all_tasks(asyncio.get_event_loop())
)
for task in all_tasks:
task.cancel()
assert before_server_start assert before_server_start
assert after_server_start assert after_server_start
assert before_server_stop assert before_server_stop
@@ -189,53 +208,6 @@ async def test_websocket_receive(send, receive, message_stack):
assert text == msg["text"] 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): def test_improper_websocket_connection(transport, send, receive):
with pytest.raises(InvalidUsage): with pytest.raises(InvalidUsage):
transport.get_websocket_connection() transport.get_websocket_connection()

View File

@@ -736,37 +736,6 @@ def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
assert response.status == 200 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): def test_route_handler_add(app: Sanic):
view = CompositionView() view = CompositionView()
@@ -825,6 +794,21 @@ def test_duplicate_blueprint(app):
) )
@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"
)
def test_strict_slashes_behavior_adoption(app): def test_strict_slashes_behavior_adoption(app):
app.strict_slashes = True app.strict_slashes = True

View File

@@ -13,7 +13,7 @@ from sanic.exceptions import PyFileError
@contextmanager @contextmanager
def temp_path(): def temp_path():
"""a simple cross platform replacement for NamedTemporaryFile""" """ a simple cross platform replacement for NamedTemporaryFile """
with TemporaryDirectory() as td: with TemporaryDirectory() as td:
yield Path(td, "file") yield Path(td, "file")

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

@@ -3,7 +3,6 @@ import asyncio
from asyncio import sleep as aio_sleep from asyncio import sleep as aio_sleep
from json import JSONDecodeError from json import JSONDecodeError
import httpcore
import httpx import httpx
from sanic import Sanic, server from sanic import Sanic, server
@@ -13,26 +12,67 @@ from sanic.testing import HOST, SanicTestClient
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
old_conn = None
PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port 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
class ReusableSanicConnectionPool(
httpx.dispatch.connection_pool.ConnectionPool
):
@property
def cert(self):
return self.ssl.cert
class ReusableSanicConnectionPool(httpcore.AsyncConnectionPool): @property
last_reused_connection = None def verify(self):
return self.ssl.verify
async def _get_connection_from_pool(self, *args, **kwargs): @property
conn = await super()._get_connection_from_pool(*args, **kwargs) def trust_env(self):
self.__class__.last_reused_connection = conn return self.ssl.trust_env
return conn
@property
def http2(self):
return self.ssl.http2
async def acquire_connection(self, origin, timeout):
global old_conn
connection = self.pop_connection(origin)
if connection is None:
pool_timeout = None if timeout is None else timeout.pool_timeout
await self.max_connections.acquire(timeout=pool_timeout)
ssl_config = httpx.config.SSLConfig(
cert=self.cert,
verify=self.verify,
trust_env=self.trust_env,
http2=self.http2,
)
connection = httpx.dispatch.connection.HTTPConnection(
origin,
ssl=ssl_config,
backend=self.backend,
release_func=self.release_connection,
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.AsyncClient):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
transport = ReusableSanicConnectionPool() dispatch = ReusableSanicConnectionPool()
super().__init__(transport=transport, *args, **kwargs) super().__init__(dispatch=dispatch, *args, **kwargs)
class ReuseableSanicTestClient(SanicTestClient): class ReuseableSanicTestClient(SanicTestClient):
@@ -204,8 +244,8 @@ async def handler3(request):
def test_keep_alive_timeout_reuse(): def test_keep_alive_timeout_reuse():
"""If the server keep-alive timeout and client keep-alive timeout are """If the server keep-alive timeout and client keep-alive timeout are
both longer than the delay, the client _and_ server will successfully both longer than the delay, the client _and_ server will successfully
reuse the existing connection.""" reuse the existing connection."""
try: try:
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
@@ -218,7 +258,6 @@ def test_keep_alive_timeout_reuse():
request, response = client.get("/1") request, response = client.get("/1")
assert response.status == 200 assert response.status == 200
assert response.text == "OK" assert response.text == "OK"
assert ReusableSanicConnectionPool.last_reused_connection
finally: finally:
client.kill_server() client.kill_server()
@@ -231,15 +270,20 @@ def test_keep_alive_client_timeout():
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop) client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
headers = {"Connection": "keep-alive"} headers = {"Connection": "keep-alive"}
request, response = client.get( try:
"/1", headers=headers, request_keepalive=1 request, response = client.get(
) "/1", headers=headers, request_keepalive=1
assert response.status == 200 )
assert response.text == "OK" assert response.status == 200
loop.run_until_complete(aio_sleep(2)) assert response.text == "OK"
exception = None loop.run_until_complete(aio_sleep(2))
request, response = client.get("/1", request_keepalive=1) exception = None
assert ReusableSanicConnectionPool.last_reused_connection is 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: finally:
client.kill_server() client.kill_server()
@@ -254,14 +298,22 @@ def test_keep_alive_server_timeout():
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop) client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
headers = {"Connection": "keep-alive"} headers = {"Connection": "keep-alive"}
request, response = client.get( try:
"/1", headers=headers, request_keepalive=60 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: finally:
client.kill_server() 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

@@ -102,7 +102,7 @@ def test_logging_pass_customer_logconfig():
@pytest.mark.parametrize("debug", (True, False)) @pytest.mark.parametrize("debug", (True, False))
def test_log_connection_lost(app, debug, monkeypatch): 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() stream = StringIO()
root = logging.getLogger("sanic.root") root = logging.getLogger("sanic.root")
root.addHandler(logging.StreamHandler(stream)) root.addHandler(logging.StreamHandler(stream))

View File

@@ -33,23 +33,6 @@ def test_custom_context(app):
} }
) )
@app.middleware("response")
def modify(request, response):
# Using response-middleware to access request ctx
try:
user = request.ctx.user
except AttributeError as e:
user = str(e)
try:
invalid = request.ctx.missing
except AttributeError as e:
invalid = str(e)
j = loads(response.body)
j["response_mw_valid"] = user
j["response_mw_invalid"] = invalid
return json(j)
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert response.json == { assert response.json == {
"user": "sanic", "user": "sanic",
@@ -58,8 +41,6 @@ def test_custom_context(app):
"has_session": True, "has_session": True,
"has_missing": False, "has_missing": False,
"invalid": "'types.SimpleNamespace' object has no attribute 'missing'", "invalid": "'types.SimpleNamespace' object has no attribute 'missing'",
"response_mw_valid": "sanic",
"response_mw_invalid": "'types.SimpleNamespace' object has no attribute 'missing'",
} }

View File

@@ -1,12 +1,9 @@
import asyncio
import pytest import pytest
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.exceptions import HeaderExpectationFailed from sanic.exceptions import HeaderExpectationFailed
from sanic.request import StreamBuffer from sanic.request import StreamBuffer
from sanic.response import json, stream, text from sanic.response import json, stream, text
from sanic.server import HttpProtocol
from sanic.views import CompositionView, HTTPMethodView from sanic.views import CompositionView, HTTPMethodView
from sanic.views import stream as stream_decorator from sanic.views import stream as stream_decorator
@@ -340,22 +337,6 @@ def test_request_stream_handle_exception(app):
assert "Method GET not allowed for URL /post/random_id" in response.text assert "Method GET not allowed for URL /post/random_id" in response.text
@pytest.mark.asyncio
async def test_request_stream_unread(app):
"""ensure no error is raised when leaving unread bytes in byte-buffer"""
err = None
protocol = HttpProtocol(loop=asyncio.get_event_loop(), app=app)
try:
protocol.request = None
protocol._body_chunks.append("this is a test")
await protocol.stream_append()
except AttributeError as e:
err = e
assert err is None and not protocol._body_chunks
def test_request_stream_blueprint(app): def test_request_stream_blueprint(app):
"""for self.is_request_stream = True""" """for self.is_request_stream = True"""
bp = Blueprint("test_blueprint_request_stream_blueprint") bp = Blueprint("test_blueprint_request_stream_blueprint")

View File

@@ -1,54 +1,64 @@
import asyncio import asyncio
from typing import cast
import httpcore
import httpx import httpx
from httpcore._async.base import (
AsyncByteStream,
AsyncHTTPTransport,
ConnectionState,
NewConnectionRequired,
)
from httpcore._async.connection import AsyncHTTPConnection
from httpcore._async.connection_pool import ResponseByteStream
from httpcore._exceptions import LocalProtocolError, UnsupportedProtocol
from httpcore._types import TimeoutDict
from httpcore._utils import url_to_origin
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.testing import SanicTestClient from sanic.testing import SanicTestClient
class DelayableHTTPConnection(httpcore._async.connection.AsyncHTTPConnection): class DelayableHTTPConnection(httpx.dispatch.connection.HTTPConnection):
async def arequest(self, *args, **kwargs): def __init__(self, *args, **kwargs):
await asyncio.sleep(2) self._request_delay = None
return await super().arequest(*args, **kwargs) if "request_delay" in kwargs:
self._request_delay = kwargs.pop("request_delay")
super().__init__(*args, **kwargs)
async def send(self, request, timeout=None):
if self.connection is None:
self.connection = await self.connect(timeout=timeout)
async def _open_socket(self, *args, **kwargs):
retval = await super()._open_socket(*args, **kwargs)
if self._request_delay: if self._request_delay:
await asyncio.sleep(self._request_delay) await asyncio.sleep(self._request_delay)
return retval
response = await self.connection.send(request, timeout=timeout)
return response
class DelayableSanicConnectionPool(httpcore.AsyncConnectionPool): class DelayableSanicConnectionPool(
httpx.dispatch.connection_pool.ConnectionPool
):
def __init__(self, request_delay=None, *args, **kwargs): def __init__(self, request_delay=None, *args, **kwargs):
self._request_delay = request_delay self._request_delay = request_delay
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
async def _add_to_pool(self, connection, timeout): async def acquire_connection(self, origin, timeout=None):
connection.__class__ = DelayableHTTPConnection connection = self.pop_connection(origin)
connection._request_delay = self._request_delay
await super()._add_to_pool(connection, timeout) if connection is None:
pool_timeout = None if timeout is None else timeout.pool_timeout
await self.max_connections.acquire(timeout=pool_timeout)
connection = DelayableHTTPConnection(
origin,
ssl=self.ssl,
backend=self.backend,
release_func=self.release_connection,
uds=self.uds,
request_delay=self._request_delay,
)
self.active_connections.add(connection)
return connection
class DelayableSanicSession(httpx.AsyncClient): class DelayableSanicSession(httpx.AsyncClient):
def __init__(self, request_delay=None, *args, **kwargs) -> None: def __init__(self, request_delay=None, *args, **kwargs) -> None:
transport = DelayableSanicConnectionPool(request_delay=request_delay) dispatch = DelayableSanicConnectionPool(request_delay=request_delay)
super().__init__(transport=transport, *args, **kwargs) super().__init__(dispatch=dispatch, *args, **kwargs)
class DelayableSanicTestClient(SanicTestClient): class DelayableSanicTestClient(SanicTestClient):

View File

@@ -12,14 +12,7 @@ from sanic import Blueprint, Sanic
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters
from sanic.response import html, json, text from sanic.response import html, json, text
from sanic.testing import ( from sanic.testing import ASGI_HOST, HOST, PORT, SanicTestClient
ASGI_BASE_URL,
ASGI_HOST,
ASGI_PORT,
HOST,
PORT,
SanicTestClient,
)
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@@ -66,10 +59,7 @@ async def test_ip_asgi(app):
request, response = await app.asgi_client.get("/") request, response = await app.asgi_client.get("/")
if response.text.endswith("/") and not ASGI_BASE_URL.endswith("/"): assert response.text == "http://mockserver/"
response.text[:-1] == ASGI_BASE_URL
else:
assert response.text == ASGI_BASE_URL
def test_text(app): def test_text(app):
@@ -290,17 +280,6 @@ def test_query_string(app):
assert request.args.get("test3", default="My value") == "My value" assert request.args.get("test3", default="My value") == "My value"
def test_popped_stays_popped(app):
@app.route("/")
async def handler(request):
return text("OK")
request, response = app.test_client.get("/", params=[("test1", "1")])
assert request.args.pop("test1") == ["1"]
assert "test1" not in request.args
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_query_string_asgi(app): async def test_query_string_asgi(app):
@app.route("/") @app.route("/")
@@ -594,7 +573,7 @@ async def test_standard_forwarded_asgi(app):
assert response.json() == {"for": "127.0.0.2", "proto": "ws"} assert response.json() == {"for": "127.0.0.2", "proto": "ws"}
assert request.remote_addr == "127.0.0.2" assert request.remote_addr == "127.0.0.2"
assert request.scheme == "ws" assert request.scheme == "ws"
assert request.server_port == ASGI_PORT assert request.server_port == 80
app.config.FORWARDED_SECRET = "mySecret" app.config.FORWARDED_SECRET = "mySecret"
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
@@ -1065,9 +1044,9 @@ def test_url_attributes_no_ssl(app, path, query, expected_url):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path,query,expected_url", "path,query,expected_url",
[ [
("/foo", "", "{}/foo"), ("/foo", "", "http://{}/foo"),
("/bar/baz", "", "{}/bar/baz"), ("/bar/baz", "", "http://{}/bar/baz"),
("/moo/boo", "arg1=val1", "{}/moo/boo?arg1=val1"), ("/moo/boo", "arg1=val1", "http://{}/moo/boo?arg1=val1"),
], ],
) )
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -1078,7 +1057,7 @@ async def test_url_attributes_no_ssl_asgi(app, path, query, expected_url):
app.add_route(handler, path) app.add_route(handler, path)
request, response = await app.asgi_client.get(path + f"?{query}") request, response = await app.asgi_client.get(path + f"?{query}")
assert request.url == expected_url.format(ASGI_BASE_URL) assert request.url == expected_url.format(ASGI_HOST)
parsed = urlparse(request.url) parsed = urlparse(request.url)
@@ -1976,7 +1955,10 @@ def test_server_name_and_url_for(app):
app.config.SERVER_NAME = "my-server" # This means default port app.config.SERVER_NAME = "my-server" # This means default port
assert app.url_for("handler", _external=True) == "http://my-server/foo" assert app.url_for("handler", _external=True) == "http://my-server/foo"
request, response = app.test_client.get("/foo") request, response = app.test_client.get("/foo")
assert request.url_for("handler") == f"http://my-server/foo" assert (
request.url_for("handler")
== f"http://my-server/foo"
)
app.config.SERVER_NAME = "https://my-server/path" app.config.SERVER_NAME = "https://my-server/path"
request, response = app.test_client.get("/foo") request, response = app.test_client.get("/foo")

View File

@@ -41,8 +41,7 @@ def test_response_body_not_a_string(app):
return text(random_num) return text(random_num)
request, response = app.test_client.get("/hello") request, response = app.test_client.get("/hello")
assert response.status == 500 assert response.text == str(random_num)
assert b"Internal Server Error" in response.body
async def sample_streaming_fn(response): async def sample_streaming_fn(response):
@@ -236,12 +235,6 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
assert response.text == "foo,bar" assert response.text == "foo,bar"
@pytest.mark.asyncio
async def test_chunked_streaming_returns_correct_content_asgi(streaming_app):
request, response = await streaming_app.asgi_client.get("/")
assert response.text == "foo,bar"
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
request, response = non_chunked_streaming_app.test_client.get("/") request, response = non_chunked_streaming_app.test_client.get("/")
assert "Transfer-Encoding" not in response.headers assert "Transfer-Encoding" not in response.headers
@@ -249,16 +242,6 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
assert response.headers["Content-Length"] == "7" assert response.headers["Content-Length"] == "7"
@pytest.mark.asyncio
async def test_non_chunked_streaming_adds_correct_headers_asgi(
non_chunked_streaming_app,
):
request, response = await non_chunked_streaming_app.asgi_client.get("/")
assert "Transfer-Encoding" not in response.headers
assert response.headers["Content-Type"] == "text/csv"
assert response.headers["Content-Length"] == "7"
def test_non_chunked_streaming_returns_correct_content( def test_non_chunked_streaming_returns_correct_content(
non_chunked_streaming_app, non_chunked_streaming_app,
): ):
@@ -625,3 +608,17 @@ def test_empty_response(app):
request, response = app.test_client.get("/test") request, response = app.test_client.get("/test")
assert response.content_type is None assert response.content_type is None
assert response.body == b"" assert response.body == b""
def test_response_body_bytes_deprecated(app):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
HTTPResponse(body_bytes=b"bytes")
assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert (
"Parameter `body_bytes` is deprecated, use `body` instead"
in str(w[0].message)
)

View File

@@ -4,7 +4,6 @@ import os
import subprocess import subprocess
import sys import sys
import httpcore
import httpx import httpx
import pytest import pytest
@@ -140,9 +139,8 @@ def test_unix_connection():
@app.listener("after_server_start") @app.listener("after_server_start")
async def client(app, loop): async def client(app, loop):
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
try: try:
async with httpx.AsyncClient(transport=transport) as client: async with httpx.AsyncClient(uds=SOCKPATH) as client:
r = await client.get("http://myhost.invalid/") r = await client.get("http://myhost.invalid/")
assert r.status_code == 200 assert r.status_code == 200
assert r.text == os.path.abspath(SOCKPATH) assert r.text == os.path.abspath(SOCKPATH)
@@ -181,9 +179,8 @@ async def test_zero_downtime():
from time import monotonic as current_time from time import monotonic as current_time
async def client(): async def client():
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
for _ in range(40): for _ in range(40):
async with httpx.AsyncClient(transport=transport) as client: async with httpx.AsyncClient(uds=SOCKPATH) as client:
r = await client.get("http://localhost/sleep/0.1") r = await client.get("http://localhost/sleep/0.1")
assert r.status_code == 200 assert r.status_code == 200
assert r.text == f"Slept 0.1 seconds.\n" assert r.text == f"Slept 0.1 seconds.\n"

View File

@@ -1,36 +0,0 @@
from pathlib import Path
import pytest
_test_setting_as_dict = {"TEST_SETTING_VALUE": 1}
_test_setting_as_class = type("C", (), {"TEST_SETTING_VALUE": 1})
_test_setting_as_module = str(
Path(__file__).parent / "static/app_test_config.py"
)
@pytest.mark.parametrize(
"conf_object",
[
_test_setting_as_dict,
_test_setting_as_class,
pytest.param(
_test_setting_as_module,
marks=pytest.mark.dependency(
depends=["test_load_module_from_file_location"],
scope="session",
),
),
],
ids=["from_dict", "from_class", "from_file"],
)
def test_update(app, conf_object):
app.update_config(conf_object)
assert app.config["TEST_SETTING_VALUE"] == 1
def test_update_from_lowercase_key(app):
d = {"test_setting_value": 1}
app.update_config(d)
assert "test_setting_value" not in app.config

View File

@@ -348,13 +348,3 @@ def test_methodview_naming(methodview_app):
assert viewone_url == "/view_one" assert viewone_url == "/view_one"
assert viewtwo_url == "/view_two" assert viewtwo_url == "/view_two"
def test_url_for_with_websocket_handlers(app):
# Test for a specific bugfix in GH-2021
@app.websocket("/ws")
async def my_handler(request, ws):
pass
assert app.url_for("my_handler") == "/ws"
assert app.url_for("websocket_handler_my_handler") == "/ws"

30
tox.ini
View File

@@ -1,26 +1,25 @@
[tox] [tox]
envlist = py36, py37, py38, py39, pyNightly, {py36,py37,py38,py39,pyNightly}-no-ext, lint, check, security, docs envlist = py36, py37, py38, pyNightly, {py36,py37,py38,pyNightly}-no-ext, lint, check, security, docs
[testenv] [testenv]
usedevelop = True usedevelop = True
setenv = setenv =
{py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UJSON=1 {py36,py37,py38,pyNightly}-no-ext: SANIC_NO_UJSON=1
{py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 {py36,py37,py38,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
deps = deps =
coverage==5.3 coverage
pytest==5.2.1 pytest==5.2.1
pytest-cov pytest-cov
pytest-sanic pytest-sanic
pytest-sugar pytest-sugar
pytest-benchmark httpcore==0.3.0
pytest-dependency httpx==0.11.1
httpcore==0.11.* chardet<=2.3.0
httpx==0.15.4
multidict>=5.0,<6.0
beautifulsoup4 beautifulsoup4
gunicorn==20.0.4 gunicorn
pytest-benchmark
uvicorn uvicorn
websockets>=8.1,<=9.1 websockets>=8.1,<9.0
commands = commands =
pytest {posargs:tests --cov sanic} pytest {posargs:tests --cov sanic}
- coverage combine --append - coverage combine --append
@@ -31,13 +30,13 @@ commands =
deps = deps =
flake8 flake8
black black
isort>=5.0.0 isort
bandit bandit
commands = commands =
flake8 sanic flake8 sanic
black --config ./.black.toml --check --verbose sanic/ black --config ./.black.toml --check --verbose sanic/
isort --check-only sanic isort --check-only --recursive sanic
[testenv:type-checking] [testenv:type-checking]
deps = deps =
@@ -56,9 +55,6 @@ commands =
[pytest] [pytest]
filterwarnings = filterwarnings =
ignore:.*async with lock.* instead:DeprecationWarning ignore:.*async with lock.* instead:DeprecationWarning
addopts = --strict-markers
markers =
asyncio
[testenv:security] [testenv:security]
deps = deps =
@@ -76,7 +72,7 @@ deps =
recommonmark>=0.5.0 recommonmark>=0.5.0
docutils docutils
pygments pygments
gunicorn==20.0.4 gunicorn
commands = commands =
make docs-test make docs-test