Compare commits

..

4 Commits

Author SHA1 Message Date
Adam Hopkins
44b657dc59 Bump version 2021-04-20 01:34:52 +03:00
Adam Hopkins
9e889fc20b Comment out premature tests 2021-04-20 01:13:46 +03:00
Adam Hopkins
bae2d4cb57 Comment out unneeded test 2021-04-20 01:03:22 +03:00
Adam Hopkins
492d6fd19d merge test conflicts 2021-04-20 00:59:12 +03:00
99 changed files with 749 additions and 2504 deletions

View File

@@ -1,12 +0,0 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- ".github/"
- "changelogs/"
- "docker/"
- "docs/"
- "examples/"
- "hack/"
- "scripts/"

View File

@@ -1,12 +1,7 @@
[run]
branch = True
source = sanic
omit =
site-packages
sanic/__main__.py
sanic/reloader_helpers.py
sanic/simple.py
sanic/utils.py
omit = site-packages, sanic/utils.py, sanic/__main__.py
[html]
directory = coverage

View File

@@ -1,10 +1,22 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
branches: [ master ]
pull_request:
branches: [ main ]
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '25 16 * * 0'
@@ -17,18 +29,39 @@ jobs:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,40 +0,0 @@
name: Coverage check
# on:
# push:
# branches:
# - main
# tags:
# - "!*" # Do not execute on tags
# paths:
# - sanic/*
# - tests/*
# pull_request:
# paths:
# - "!*.MD"
on: [push, pull_request]
jobs:
test:
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,32 +0,0 @@
name: Security Analysis
on:
pull_request:
branches:
- main
jobs:
bandit:
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}
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,29 +0,0 @@
name: Document Linter
on:
pull_request:
branches:
- main
jobs:
docsLinter:
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,30 +0,0 @@
name: Linter Checks
on:
pull_request:
branches:
- main
jobs:
linter:
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,38 +0,0 @@
name: Python 3.7 Tests
on:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- sanic/*
- tests/*
jobs:
testPy37:
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.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,38 +0,0 @@
name: Python 3.8 Tests
on:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- sanic/*
- tests/*
jobs:
testPy38:
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.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,50 +0,0 @@
name: Python 3.9 Tests
on:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- sanic/*
- tests/*
jobs:
testPy39:
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: 3.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,32 +0,0 @@
name: Typing Checks
on:
pull_request:
branches:
- main
jobs:
typeChecking:
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,34 +0,0 @@
# name: Run Unit Tests on Windows
# on:
# pull_request:
# branches:
# - main
# jobs:
# testsOnWindows:
# 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"

4
.gitignore vendored
View File

@@ -6,7 +6,6 @@
.coverage
.coverage.*
coverage
coverage.xml
.tox
settings.py
.idea/*
@@ -19,6 +18,3 @@ build/*
.DS_Store
dist/*
pip-wheel-metadata/
.pytest_cache/*
.venv/*
.vscode/*

94
.travis.yml Normal file
View File

@@ -0,0 +1,94 @@
sudo: false
language: python
cache:
directories:
- $HOME/.cache/pip
matrix:
include:
- env: TOX_ENV=py37
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 with Extensions"
- env: TOX_ENV=py37-no-ext
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 without Extensions"
- env: TOX_ENV=py38
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 with Extensions"
- env: TOX_ENV=py38-no-ext
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 without Extensions"
- env: TOX_ENV=py39
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 with Extensions"
- env: TOX_ENV=py39-no-ext
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 without Extensions"
- env: TOX_ENV=type-checking
python: 3.7
name: "Python 3.7 Type checks"
- env: TOX_ENV=type-checking
python: 3.8
name: "Python 3.8 Type checks"
- env: TOX_ENV=type-checking
python: 3.9
dist: bionic
name: "Python 3.9 Type checks"
- env: TOX_ENV=security
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Bandit security scan"
- env: TOX_ENV=security
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 Bandit security scan"
- env: TOX_ENV=security
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 Bandit security scan"
- env: TOX_ENV=docs
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Documentation tests"
- env: TOX_ENV=pyNightly
python: "nightly"
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: "nightly"
name: "Python nightly without Extensions"
allow_failures:
- env: TOX_ENV=pyNightly
python: "nightly"
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: "nightly"
name: "Python nightly without Extensions"
install:
- pip install -U tox
- pip install codecov
script: travis_retry tox -e $TOX_ENV
after_success:
- codecov
deploy:
provider: pypi
user: brewmaster
password:
secure: "GoawLwmbtJOgKB6AJ0ZSYUUnNwIoonseHBxaAUH3zu79TS/Afrq+yB3lsVaMSG0CbyDgN4FrfD1phT1NzbvZ1VcLIOTDtCrmpQ1kLDw+zwgF40ab8sp8fPkKVHHHfCCs1mjltHIpxQa5lZTJcAs6Bpi/lbUWWwYxFzSV8pHw4W4hY09EHUd2o+evLTSVxaploetSt725DJUYKICUr2eAtCC11IDnIW4CzBJEx6krVV3uhzfTJW0Ls17x0c6sdZ9icMnV/G9xO/eQH6RIHe4xcrWJ6cmLDNKoGAkJp+BKr1CeVVg7Jw/MzPjvZKL2/ki6Beue1y6GUIy7lOS7jPVaOEhJ23b0zQwFcLMZw+Tt+E3v6QfHk+B/WBBBnM3zUZed9UI+QyW8+lqLLt39sQX0FO0P3eaDh8qTXtUuon2jTyFMMAMTFRTNpJmpAzuBH9yeMmDeALPTh0HphI+BkoUl5q1QbWFYjjnZMH2CatApxpLybt9A7rwm//PbOG0TSI93GEKNQ4w5DYryKTfwHzRBptNSephJSuxZYEfJsmUtas5es1D7Fe0PkyjxNNSU+eO+8wsTlitLUsJO4k0jAgy+cEKdU7YJ3J0GZVXocSkrNnUfd2hQPcJ3UtEJx3hLqqr8EM7EZBAasc1yGHh36NFetclzFY24YPih0G1+XurhTys="
on:
tags: true
distributions: "sdist bdist_wheel"

View File

@@ -1,95 +1,3 @@
Version 21.6.0
--------------
Features
********
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
Add ``response.eof()`` method for closing a stream in a handler
* `#2097 <https://github.com/sanic-org/sanic/pull/2097>`_
Allow case-insensitive HTTP Upgrade header
* `#2104 <https://github.com/sanic-org/sanic/pull/2104>`_
Explicit usage of CIMultiDict getters
* `#2109 <https://github.com/sanic-org/sanic/pull/2109>`_
Consistent use of error loggers
* `#2114 <https://github.com/sanic-org/sanic/pull/2114>`_
New ``client_ip`` access of connection info instance
* `#2119 <https://github.com/sanic-org/sanic/pull/2119>`_
Alternatate classes on instantiation for ``Config`` and ``Sanic.ctx``
* `#2133 <https://github.com/sanic-org/sanic/pull/2133>`_
Implement new version of AST router
* Proper differentiation between ``alpha`` and ``string`` param types
* Adds a ``slug`` param type, example: ``<foo:slug>``
* Deprecates ``<foo:string>`` in favor of ``<foo:str>``
* Deprecates ``<foo:number>`` in favor of ``<foo:float>``
* Adds a ``route.uri`` accessor
* `#2136 <https://github.com/sanic-org/sanic/pull/2136>`_
CLI improvements with new optional params
* `#2137 <https://github.com/sanic-org/sanic/pull/2137>`_
Add ``version_prefix`` to URL builders
* `#2140 <https://github.com/sanic-org/sanic/pull/2140>`_
Event autoregistration with ``EVENT_AUTOREGISTER``
* `#2146 <https://github.com/sanic-org/sanic/pull/2146>`_, `#2147 <https://github.com/sanic-org/sanic/pull/2147>`_
Require stricter names on ``Sanic()`` and ``Blueprint()``
* `#2150 <https://github.com/sanic-org/sanic/pull/2150>`_
Infinitely reusable and nestable ``Blueprint`` and ``BlueprintGroup``
* `#2154 <https://github.com/sanic-org/sanic/pull/2154>`_
Upgrade ``websockets`` dependency to min version
* `#2155 <https://github.com/sanic-org/sanic/pull/2155>`_
Allow for maximum header sizes to be increased: ``REQUEST_MAX_HEADER_SIZE``
* `#2157 <https://github.com/sanic-org/sanic/pull/2157>`_
Allow app factory pattern in CLI
* `#2165 <https://github.com/sanic-org/sanic/pull/2165>`_
Change HTTP methods to enums
* `#2167 <https://github.com/sanic-org/sanic/pull/2167>`_
Allow auto-reloading on additional directories
* `#2168 <https://github.com/sanic-org/sanic/pull/2168>`_
Add simple HTTP server to CLI
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
Additional methods for attaching ``HTTPMethodView``
Bugfixes
********
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
* `#2099 <https://github.com/sanic-org/sanic/pull/2099>`_
Fix static request handler logging exception on 404
* `#2110 <https://github.com/sanic-org/sanic/pull/2110>`_
Fix request.args.pop removes parameters inconsistently
* `#2107 <https://github.com/sanic-org/sanic/pull/2107>`_
Fix type hinting for load_env
* `#2127 <https://github.com/sanic-org/sanic/pull/2127>`_
Make sure ASGI ws subprotocols is a list
* `#2128 <https://github.com/sanic-org/sanic/pull/2128>`_
Fix issue where Blueprint exception handlers do not consistently route to proper handler
Deprecations and Removals
*************************
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
``CompositionView`` deprecated and marked for removal in 21.12
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
Deprecate StreamingHTTPResponse
Developer infrastructure
************************
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
Remove Travis CI in favor of GitHub Actions
Improved Documentation
**********************
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
Fix typo in documentation
* `#2100 <https://github.com/sanic-org/sanic/pull/2100>`_
Remove documentation for non-existent arguments
Version 21.3.2
--------------

View File

@@ -87,7 +87,7 @@ Permform ``flake8``\ , ``black`` and ``isort`` checks.
tox -e lint
Run type annotation checks
--------------------------
---------------
``tox`` environment -> ``[testenv:type-checking]``

View File

@@ -49,9 +49,6 @@ test: clean
test-coverage: clean
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
view-coverage:
sanic ./coverage --simple
install:
python setup.py install
@@ -88,7 +85,8 @@ docs-test: docs-clean
cd docs && make dummy
docs-serve:
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./
# python -m http.server --directory=./docs/_build/html 9999
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic
changelog:
python scripts/changelog.py

View File

@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
:stub-columns: 1
* - Build
- | |Py39Test| |Py38Test| |Py37Test| |Codecov|
- | |Build Status| |AppVeyor Build Status| |Codecov|
* - Docs
- | |UserGuide| |Documentation|
* - Package
@@ -29,12 +29,10 @@ Sanic | Build fast. Run fast.
:target: https://discord.gg/FARQzAEMAA
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/sanic-org/sanic
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml
.. |Py37Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml
.. |Build Status| image:: https://travis-ci.com/sanic-org/sanic.svg?branch=master
:target: https://travis-ci.com/sanic-org/sanic
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
:target: https://ci.appveyor.com/project/sanic-org/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg

14
codecov.yml Normal file
View File

@@ -0,0 +1,14 @@
codecov:
require_ci_to_pass: no
coverage:
precision: 3
round: nearest
status:
project:
default:
target: auto
threshold: 0.5%
patch:
default:
target: auto
threshold: 0.75%

View File

@@ -1,9 +1,28 @@
ARG BASE_IMAGE_TAG
FROM alpine:3.7
FROM sanicframework/sanic-build:${BASE_IMAGE_TAG}
RUN apk add --no-cache --update \
curl \
bash \
build-base \
ca-certificates \
git \
bzip2-dev \
linux-headers \
ncurses-dev \
openssl \
openssl-dev \
readline-dev \
sqlite-dev
RUN apk update
RUN update-ca-certificates
RUN rm -rf /var/cache/apk/*
RUN pip install sanic
RUN apk del build-base
ENV PYENV_ROOT="/root/.pyenv"
ENV PATH="$PYENV_ROOT/bin:$PATH"
ADD . /app
WORKDIR /app
RUN /app/docker/bin/install_python.sh 3.5.4 3.6.4
ENTRYPOINT ["./docker/bin/entrypoint.sh"]

View File

@@ -1,9 +0,0 @@
ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION}-alpine
RUN apk update
RUN apk add --no-cache --update build-base \
ca-certificates \
openssl
RUN update-ca-certificates
RUN rm -rf /var/cache/apk/*

11
docker/bin/entrypoint.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -e
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
source /root/.pyenv/completions/pyenv.bash
pip install tox
exec $@

17
docker/bin/install_python.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
set -e
export CFLAGS='-O2'
export EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000"
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
eval "$(pyenv init -)"
for ver in $@
do
pyenv install $ver
done
pyenv global $@
pip install --upgrade pip
pyenv rehash

View File

@@ -1,17 +0,0 @@
Application
===========
sanic.app
---------
.. automodule:: sanic.app
:members:
:show-inheritance:
:inherited-members:
sanic.config
------------
.. automodule:: sanic.config
:members:
:show-inheritance:

View File

@@ -1,17 +0,0 @@
Blueprints
==========
sanic.blueprints
----------------
.. automodule:: sanic.blueprints
:members:
:show-inheritance:
:inherited-members:
sanic.blueprint_group
---------------------
.. automodule:: sanic.blueprint_group
:members:
:special-members:

View File

@@ -1,47 +0,0 @@
Core
====
sanic.cookies
-------------
.. automodule:: sanic.cookies
:members:
:show-inheritance:
sanic.handlers
--------------
.. automodule:: sanic.handlers
:members:
:show-inheritance:
sanic.request
-------------
.. automodule:: sanic.request
:members:
:show-inheritance:
sanic.response
--------------
.. automodule:: sanic.response
:members:
:show-inheritance:
sanic.views
-----------
.. automodule:: sanic.views
:members:
:show-inheritance:
sanic.websocket
---------------
.. automodule:: sanic.websocket
:members:
:show-inheritance:

View File

@@ -1,16 +0,0 @@
Exceptions
==========
sanic.errorpages
----------------
.. automodule:: sanic.errorpages
:members:
:show-inheritance:
sanic.exceptions
----------------
.. automodule:: sanic.exceptions
:members:
:show-inheritance:

View File

@@ -1,18 +0,0 @@
Routing
=======
sanic_routing models
--------------------
.. autoclass:: sanic_routing.route::Route
:members:
.. autoclass:: sanic_routing.group::RouteGroup
:members:
sanic.router
------------
.. automodule:: sanic.router
:members:
:show-inheritance:

View File

@@ -1,25 +0,0 @@
Sanic Server
============
sanic.http
----------
.. automodule:: sanic.http
:members:
:show-inheritance:
sanic.server
------------
.. automodule:: sanic.server
:members:
:show-inheritance:
sanic.worker
------------
.. automodule:: sanic.worker
:members:
:show-inheritance:

View File

@@ -1,16 +0,0 @@
Utility
=======
sanic.compat
------------
.. automodule:: sanic.compat
:members:
:show-inheritance:
sanic.log
---------
.. automodule:: sanic.log
:members:
:show-inheritance:

View File

@@ -1,13 +1,132 @@
📑 API Reference
================
.. toctree::
:maxdepth: 2
sanic.app
---------
api/app
api/blueprints
api/core
api/exceptions
api/router
api/server
api/utility
.. automodule:: sanic.app
:members:
:show-inheritance:
:inherited-members:
sanic.blueprints
----------------
.. automodule:: sanic.blueprints
:members:
:show-inheritance:
:inherited-members:
sanic.blueprint_group
---------------------
.. automodule:: sanic.blueprint_group
:members:
:special-members:
sanic.compat
------------
.. automodule:: sanic.compat
:members:
:show-inheritance:
sanic.config
------------
.. automodule:: sanic.config
:members:
:show-inheritance:
sanic.cookies
-------------
.. automodule:: sanic.cookies
:members:
:show-inheritance:
sanic.errorpages
----------------
.. automodule:: sanic.errorpages
:members:
:show-inheritance:
sanic.exceptions
----------------
.. automodule:: sanic.exceptions
:members:
:show-inheritance:
sanic.handlers
--------------
.. automodule:: sanic.handlers
:members:
:show-inheritance:
sanic.http
----------
.. automodule:: sanic.http
:members:
:show-inheritance:
sanic.log
---------
.. automodule:: sanic.log
:members:
:show-inheritance:
sanic.request
-------------
.. automodule:: sanic.request
:members:
:show-inheritance:
sanic.response
--------------
.. automodule:: sanic.response
:members:
:show-inheritance:
sanic.router
------------
.. automodule:: sanic.router
:members:
:show-inheritance:
sanic.server
------------
.. automodule:: sanic.server
:members:
:show-inheritance:
sanic.views
-----------
.. automodule:: sanic.views
:members:
:show-inheritance:
sanic.websocket
---------------
.. automodule:: sanic.websocket
:members:
:show-inheritance:
sanic.worker
------------
.. automodule:: sanic.worker
:members:
:show-inheritance:

View File

@@ -1,4 +1,4 @@
♥️ Contributing
==============
===============
.. include:: ../../CONTRIBUTING.rst

View File

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

View File

@@ -1,7 +1,6 @@
from sanic.__version__ import __version__
from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.constants import HTTPMethod
from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
@@ -10,7 +9,6 @@ __all__ = (
"__version__",
"Sanic",
"Blueprint",
"HTTPMethod",
"HTTPResponse",
"Request",
"html",

View File

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

View File

@@ -1 +1 @@
__version__ = "21.6.0"
__version__ = "21.3.4"

View File

@@ -14,7 +14,6 @@ from asyncio.futures import Future
from collections import defaultdict, deque
from functools import partial
from inspect import isawaitable
from pathlib import Path
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc
@@ -44,7 +43,7 @@ from sanic.asgi import ASGIApp
from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint
from sanic.config import BASE_LOGO, SANIC_PREFIX, Config
from sanic.config import BASE_LOGO, Config
from sanic.exceptions import (
InvalidUsage,
SanicException,
@@ -79,7 +78,6 @@ class Sanic(BaseSanic):
"""
__fake_slots__ = (
"_asgi_app",
"_app_registry",
"_asgi_client",
"_blueprint_order",
@@ -91,7 +89,6 @@ class Sanic(BaseSanic):
"_future_signals",
"_test_client",
"_test_manager",
"auto_reload",
"asgi",
"blueprints",
"config",
@@ -106,7 +103,6 @@ class Sanic(BaseSanic):
"name",
"named_request_middleware",
"named_response_middleware",
"reload_dirs",
"request_class",
"request_middleware",
"response_middleware",
@@ -125,13 +121,10 @@ class Sanic(BaseSanic):
def __init__(
self,
name: str = None,
config: Optional[Config] = None,
ctx: Optional[Any] = None,
router: Optional[Router] = None,
signal_router: Optional[SignalRouter] = None,
error_handler: Optional[ErrorHandler] = None,
load_env: Union[bool, str] = True,
env_prefix: Optional[str] = SANIC_PREFIX,
load_env: bool = True,
request_class: Optional[Type[Request]] = None,
strict_slashes: bool = False,
log_config: Optional[Dict[str, Any]] = None,
@@ -139,38 +132,34 @@ class Sanic(BaseSanic):
register: Optional[bool] = None,
dumps: Optional[Callable[..., str]] = None,
) -> None:
super().__init__(name=name)
super().__init__()
if name is None:
raise SanicException(
"Sanic instance cannot be unnamed. "
"Please use Sanic(name='your_application_name') instead.",
)
# logging
if configure_logging:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
if config and (load_env is not True or env_prefix != SANIC_PREFIX):
raise SanicException(
"When instantiating Sanic with config, you cannot also pass "
"load_env or env_prefix"
)
self._asgi_client = None
self._blueprint_order: List[Blueprint] = []
self._test_client = None
self._test_manager = None
self.asgi = False
self.auto_reload = False
self.blueprints: Dict[str, Blueprint] = {}
self.config = config or Config(
load_env=load_env, env_prefix=env_prefix
)
self.config = Config(load_env=load_env)
self.configure_logging = configure_logging
self.ctx = ctx or SimpleNamespace()
self.ctx = SimpleNamespace()
self.debug = None
self.error_handler = error_handler or ErrorHandler()
self.is_running = False
self.is_stopping = False
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
self.name = name
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.reload_dirs: Set[Path] = set()
self.request_class = request_class
self.request_middleware: Deque[MiddlewareType] = deque()
self.response_middleware: Deque[MiddlewareType] = deque()
@@ -186,6 +175,7 @@ class Sanic(BaseSanic):
if register is not None:
self.config.REGISTER = register
if self.config.REGISTER:
self.__class__.register_app(self)
@@ -384,19 +374,11 @@ class Sanic(BaseSanic):
condition=condition,
)
async def event(
self, event: str, timeout: Optional[Union[int, float]] = None
):
def event(self, event: str, timeout: Optional[Union[int, float]] = None):
signal = self.signal_router.name_index.get(event)
if not signal:
if self.config.EVENT_AUTOREGISTER:
self.signal_router.reset()
self.add_signal(None, event)
signal = self.signal_router.name_index[event]
self.signal_router.finalize()
else:
raise NotFound("Could not find signal %s" % event)
return await wait_for(signal.ctx.event.wait(), timeout=timeout)
raise NotFound("Could not find signal %s" % event)
return wait_for(signal.ctx.event.wait(), timeout=timeout)
def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket.
@@ -420,33 +402,7 @@ class Sanic(BaseSanic):
"""
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
for item in blueprint:
params = {**options}
if isinstance(blueprint, BlueprintGroup):
if blueprint.url_prefix:
merge_from = [
options.get("url_prefix", ""),
blueprint.url_prefix,
]
if not isinstance(item, BlueprintGroup):
merge_from.append(item.url_prefix or "")
merged_prefix = "/".join(
u.strip("/") for u in merge_from
).rstrip("/")
params["url_prefix"] = f"/{merged_prefix}"
for _attr in ["version", "strict_slashes"]:
if getattr(item, _attr) is None:
params[_attr] = getattr(
blueprint, _attr
) or options.get(_attr)
if item.version_prefix == "/v":
if blueprint.version_prefix == "/v":
params["version_prefix"] = options.get(
"version_prefix"
)
else:
params["version_prefix"] = blueprint.version_prefix
self.blueprint(item, **params)
self.blueprint(item, **options)
return
if blueprint.name in self.blueprints:
assert self.blueprints[blueprint.name] is blueprint, (
@@ -611,12 +567,7 @@ class Sanic(BaseSanic):
# determine if the parameter supplied by the caller
# passes the test in the URL
if param_info.pattern:
pattern = (
param_info.pattern[1]
if isinstance(param_info.pattern, tuple)
else param_info.pattern
)
passes_pattern = pattern.match(supplied_param)
passes_pattern = param_info.pattern.match(supplied_param)
if not passes_pattern:
if param_info.cast != str:
msg = (
@@ -624,13 +575,13 @@ class Sanic(BaseSanic):
f"for parameter `{param_info.name}` does "
"not match pattern for type "
f"`{param_info.cast.__name__}`: "
f"{pattern.pattern}"
f"{param_info.pattern.pattern}"
)
else:
msg = (
f'Value "{supplied_param}" for parameter '
f"`{param_info.name}` does not satisfy "
f"pattern {pattern.pattern}"
f"pattern {param_info.pattern.pattern}"
)
raise URLBuildError(msg)
@@ -713,6 +664,11 @@ class Sanic(BaseSanic):
exception handling must be done here
:param request: HTTP Request object
:param write_callback: Synchronous response function to be
called with the response as the only argument
:param stream_callback: Coroutine that handles streaming a
StreamingHTTPResponse if produced by the handler.
:return: Nothing
"""
# Define `response` var here to remove warnings about
@@ -721,9 +677,7 @@ class Sanic(BaseSanic):
try:
# Fetch handler from router
route, handler, kwargs = self.router.get(
request.path,
request.method,
request.headers.getone("host", None),
request.path, request.method, request.headers.get("host")
)
request._match_info = kwargs
@@ -771,14 +725,17 @@ class Sanic(BaseSanic):
if response:
response = await request.respond(response)
elif not hasattr(handler, "is_websocket"):
else:
response = request.stream.response # type: ignore
# Make sure that response is finished / run StreamingHTTP callback
if isinstance(response, BaseHTTPResponse):
await response.send(end_stream=True)
else:
if not hasattr(handler, "is_websocket"):
try:
# Fastest method for checking if the property exists
handler.is_websocket # type: ignore
except AttributeError:
raise ServerError(
f"Invalid response type {response!r} "
"(need HTTPResponse)"
@@ -805,7 +762,6 @@ class Sanic(BaseSanic):
if self.asgi:
ws = request.transport.get_websocket_connection()
await ws.accept(subprotocols)
else:
protocol = request.transport.get_protocol()
protocol.app = self
@@ -878,7 +834,6 @@ class Sanic(BaseSanic):
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: None = None,
reload_dir: Optional[Union[List[str], str]] = None,
) -> None:
"""
Run the HTTP Server and listen until keyboard interrupt or term
@@ -913,18 +868,6 @@ class Sanic(BaseSanic):
:type unix: str
:return: Nothing
"""
if reload_dir:
if isinstance(reload_dir, str):
reload_dir = [reload_dir]
for directory in reload_dir:
direc = Path(directory)
if not direc.is_dir():
logger.warning(
f"Directory {directory} could not be located"
)
self.reload_dirs.add(Path(directory))
if loop is not None:
raise TypeError(
"loop is not a valid argument. To use an existing loop, "
@@ -934,9 +877,8 @@ class Sanic(BaseSanic):
)
if auto_reload or auto_reload is None and debug:
self.auto_reload = True
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
return reloader_helpers.watchdog(1.0, self)
return reloader_helpers.watchdog(1.0)
if sock is None:
host, port = host or "127.0.0.1", port or 8000
@@ -1235,10 +1177,6 @@ class Sanic(BaseSanic):
else:
logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
debug_mode = "enabled" if self.debug else "disabled"
logger.debug("Sanic auto-reload: enabled")
logger.debug(f"Sanic debug mode: {debug_mode}")
return server_settings
def _build_endpoint_name(self, *parts):

View File

@@ -140,6 +140,7 @@ class ASGIApp:
instance.ws = instance.transport.create_websocket_connection(
send, receive
)
await instance.ws.accept()
else:
raise ServerError("Received unknown ASGI scope")

View File

@@ -1,9 +1,6 @@
import re
from typing import Any, Tuple
from warnings import warn
from sanic.exceptions import SanicException
from sanic.mixins.exceptions import ExceptionMixin
from sanic.mixins.listeners import ListenerMixin
from sanic.mixins.middleware import MiddlewareMixin
@@ -11,9 +8,6 @@ from sanic.mixins.routes import RouteMixin
from sanic.mixins.signals import SignalMixin
VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$")
class BaseSanic(
RouteMixin,
MiddlewareMixin,
@@ -23,25 +17,7 @@ class BaseSanic(
):
__fake_slots__: Tuple[str, ...]
def __init__(self, name: str = None, *args, **kwargs) -> None:
class_name = self.__class__.__name__
if name is None:
raise SanicException(
f"{class_name} instance cannot be unnamed. "
"Please use Sanic(name='your_application_name') instead.",
)
if not VALID_NAME.match(name):
warn(
f"{class_name} instance named '{name}' uses a format that is"
f"deprecated. Starting in version 21.12, {class_name} objects "
"must be named only using alphanumeric characters, _, or -.",
DeprecationWarning,
)
self.name = name
def __init__(self, *args, **kwargs) -> None:
for base in BaseSanic.__bases__:
base.__init__(self, *args, **kwargs) # type: ignore
@@ -60,7 +36,6 @@ class BaseSanic(
f"Setting variables on {self.__class__.__name__} instances is "
"deprecated and will be removed in version 21.9. You should "
f"change your {self.__class__.__name__} instance to use "
f"instance.ctx.{name} instead.",
DeprecationWarning,
f"instance.ctx.{name} instead."
)
super().__setattr__(name, value)

View File

@@ -1,8 +1,8 @@
from __future__ import annotations
from collections.abc import MutableSequence
from typing import TYPE_CHECKING, List, Optional, Union
import sanic
if TYPE_CHECKING:
from sanic.blueprints import Blueprint
@@ -58,20 +58,13 @@ class BlueprintGroup(MutableSequence):
app.blueprint(bpg)
"""
__slots__ = (
"_blueprints",
"_url_prefix",
"_version",
"_strict_slashes",
"_version_prefix",
)
__slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes")
def __init__(
self,
url_prefix: Optional[str] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
):
"""
Create a new Blueprint Group
@@ -84,7 +77,6 @@ class BlueprintGroup(MutableSequence):
self._blueprints: List[Blueprint] = []
self._url_prefix = url_prefix
self._version = version
self._version_prefix = version_prefix
self._strict_slashes = strict_slashes
@property
@@ -97,7 +89,7 @@ class BlueprintGroup(MutableSequence):
return self._url_prefix
@property
def blueprints(self) -> List[Blueprint]:
def blueprints(self) -> List["sanic.Blueprint"]:
"""
Retrieve a list of all the available blueprints under this group.
@@ -124,15 +116,6 @@ class BlueprintGroup(MutableSequence):
"""
return self._strict_slashes
@property
def version_prefix(self) -> str:
"""
Version prefix; defaults to ``/v``
:return: str
"""
return self._version_prefix
def __iter__(self):
"""
Tun the class Blueprint Group into an Iterable item
@@ -187,16 +170,34 @@ class BlueprintGroup(MutableSequence):
"""
return len(self._blueprints)
def append(self, value: Blueprint) -> None:
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint":
"""
Sanitize the Blueprint Entity to override the Version and strict slash
behaviors as required.
:param bp: Sanic Blueprint entity Object
:return: Modified Blueprint
"""
if self._url_prefix:
merged_prefix = "/".join(
u.strip("/") for u in [self._url_prefix, bp.url_prefix or ""]
).rstrip("/")
bp.url_prefix = f"/{merged_prefix}"
for _attr in ["version", "strict_slashes"]:
if getattr(bp, _attr) is None:
setattr(bp, _attr, getattr(self, _attr))
return bp
def append(self, value: "sanic.Blueprint") -> None:
"""
The Abstract class `MutableSequence` leverages this append method to
perform the `BlueprintGroup.append` operation.
:param value: New `Blueprint` object.
:return: None
"""
self._blueprints.append(value)
self._blueprints.append(self._sanitize_blueprint(bp=value))
def insert(self, index: int, item: Blueprint) -> None:
def insert(self, index: int, item: "sanic.Blueprint") -> None:
"""
The Abstract class `MutableSequence` leverages this insert method to
perform the `BlueprintGroup.append` operation.
@@ -205,7 +206,7 @@ class BlueprintGroup(MutableSequence):
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints.insert(index, item)
self._blueprints.insert(index, self._sanitize_blueprint(item))
def middleware(self, *args, **kwargs):
"""

View File

@@ -62,20 +62,18 @@ class Blueprint(BaseSanic):
"strict_slashes",
"url_prefix",
"version",
"version_prefix",
"websocket_routes",
)
def __init__(
self,
name: str = None,
name: str,
url_prefix: Optional[str] = None,
host: Optional[str] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
):
super().__init__(name=name)
super().__init__()
self._apps: Set[Sanic] = set()
self.ctx = SimpleNamespace()
@@ -83,6 +81,7 @@ class Blueprint(BaseSanic):
self.host = host
self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares: List[MiddlewareType] = []
self.name = name
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
self.strict_slashes = strict_slashes
@@ -92,7 +91,6 @@ class Blueprint(BaseSanic):
else url_prefix
)
self.version = version
self.version_prefix = version_prefix
self.websocket_routes: List[Route] = []
def __repr__(self) -> str:
@@ -145,13 +143,7 @@ class Blueprint(BaseSanic):
return super().signal(event, *args, **kwargs)
@staticmethod
def group(
*blueprints,
url_prefix="",
version=None,
strict_slashes=None,
version_prefix: str = "/v",
):
def group(*blueprints, url_prefix="", version=None, strict_slashes=None):
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.
@@ -168,6 +160,8 @@ class Blueprint(BaseSanic):
for i in nested:
if isinstance(i, (list, tuple)):
yield from chain(i)
elif isinstance(i, BlueprintGroup):
yield from i.blueprints
else:
yield i
@@ -175,7 +169,6 @@ class Blueprint(BaseSanic):
url_prefix=url_prefix,
version=version,
strict_slashes=strict_slashes,
version_prefix=version_prefix,
)
for bp in chain(blueprints):
bps.append(bp)
@@ -193,9 +186,6 @@ class Blueprint(BaseSanic):
self._apps.add(app)
url_prefix = options.get("url_prefix", self.url_prefix)
opt_version = options.get("version", None)
opt_strict_slashes = options.get("strict_slashes", None)
opt_version_prefix = options.get("version_prefix", self.version_prefix)
routes = []
middleware = []
@@ -210,22 +200,12 @@ class Blueprint(BaseSanic):
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
version_prefix = self.version_prefix
for prefix in (
future.version_prefix,
opt_version_prefix,
):
if prefix and prefix != "/v":
version_prefix = prefix
break
version = self._extract_value(
future.version, opt_version, self.version
strict_slashes = (
self.strict_slashes
if future.strict_slashes is None
and self.strict_slashes is not None
else future.strict_slashes
)
strict_slashes = self._extract_value(
future.strict_slashes, opt_strict_slashes, self.strict_slashes
)
name = app._generate_name(future.name)
apply_route = FutureRoute(
@@ -235,14 +215,13 @@ class Blueprint(BaseSanic):
future.host or self.host,
strict_slashes,
future.stream,
version,
future.version or self.version,
name,
future.ignore_body,
future.websocket,
future.subprotocols,
future.unquote,
future.static,
version_prefix,
)
route = app._apply_route(apply_route)
@@ -279,6 +258,8 @@ class Blueprint(BaseSanic):
app._apply_signal(signal)
self.routes = [route for route in routes if isinstance(route, Route)]
# Deprecate these in 21.6
self.websocket_routes = [
route for route in self.routes if route.ctx.websocket
]
@@ -307,12 +288,3 @@ class Blueprint(BaseSanic):
return_when=asyncio.FIRST_COMPLETED,
timeout=timeout,
)
@staticmethod
def _extract_value(*values):
value = values[-1]
for v in values:
if v is not None:
value = v
break
return value

View File

@@ -1,10 +1,7 @@
from inspect import isclass
from os import environ
from pathlib import Path
from typing import Any, Dict, Optional, Union
from warnings import warn
from sanic.http import Http
from typing import Any, Union
from .utils import load_module_from_file_location, str_to_bool
@@ -18,64 +15,33 @@ BASE_LOGO = """
"""
DEFAULT_CONFIG = {
"ACCESS_LOG": True,
"EVENT_AUTOREGISTER": False,
"FALLBACK_ERROR_FORMAT": "html",
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FORWARDED_SECRET": None,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"KEEP_ALIVE": True,
"PROXIES_COUNT": None,
"REAL_IP_HEADER": None,
"REGISTER": True,
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB
"REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384
"REQUEST_ID_HEADER": "X-Request-ID",
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_BUFFER_QUEUE_SIZE": 100,
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"WEBSOCKET_MAX_QUEUE": 32,
"KEEP_ALIVE": True,
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20,
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"WEBSOCKET_PING_TIMEOUT": 20,
"WEBSOCKET_PING_INTERVAL": 20,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True,
"FORWARDED_SECRET": None,
"REAL_IP_HEADER": None,
"PROXIES_COUNT": None,
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"REQUEST_ID_HEADER": "X-Request-ID",
"FALLBACK_ERROR_FORMAT": "html",
"REGISTER": True,
}
class Config(dict):
ACCESS_LOG: bool
EVENT_AUTOREGISTER: bool
FALLBACK_ERROR_FORMAT: str
FORWARDED_FOR_HEADER: str
FORWARDED_SECRET: Optional[str]
GRACEFUL_SHUTDOWN_TIMEOUT: float
KEEP_ALIVE_TIMEOUT: int
KEEP_ALIVE: bool
PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: Optional[str]
REGISTER: bool
REQUEST_BUFFER_SIZE: int
REQUEST_MAX_HEADER_SIZE: int
REQUEST_ID_HEADER: str
REQUEST_MAX_SIZE: int
REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int
WEBSOCKET_MAX_QUEUE: int
WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: int
WEBSOCKET_READ_LIMIT: int
WEBSOCKET_WRITE_LIMIT: int
def __init__(
self,
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
load_env: Optional[Union[bool, str]] = True,
env_prefix: Optional[str] = SANIC_PREFIX,
keep_alive: Optional[bool] = None,
):
def __init__(self, defaults=None, load_env=True, keep_alive=None):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
@@ -84,22 +50,9 @@ class Config(dict):
if keep_alive is not None:
self.KEEP_ALIVE = keep_alive
if env_prefix != SANIC_PREFIX:
if env_prefix:
self.load_environment_vars(env_prefix)
elif load_env is not True:
if load_env:
self.load_environment_vars(prefix=load_env)
warn(
"Use of load_env is deprecated and will be removed in "
"21.12. Modify the configuration prefix by passing "
"env_prefix instead.",
DeprecationWarning,
)
else:
self.load_environment_vars(SANIC_PREFIX)
self._configure_header_size()
if load_env:
prefix = SANIC_PREFIX if load_env is True else load_env
self.load_environment_vars(prefix=prefix)
def __getattr__(self, attr):
try:
@@ -109,19 +62,6 @@ class Config(dict):
def __setattr__(self, attr, value):
self[attr] = value
if attr in (
"REQUEST_MAX_HEADER_SIZE",
"REQUEST_BUFFER_SIZE",
"REQUEST_MAX_SIZE",
):
self._configure_header_size()
def _configure_header_size(self):
Http.set_header_max_size(
self.REQUEST_MAX_HEADER_SIZE,
self.REQUEST_BUFFER_SIZE - 4096,
self.REQUEST_MAX_SIZE,
)
def load_environment_vars(self, prefix=SANIC_PREFIX):
"""

View File

@@ -1,28 +1,2 @@
from enum import Enum, auto
class HTTPMethod(str, Enum):
def _generate_next_value_(name, start, count, last_values):
return name.upper()
def __eq__(self, value: object) -> bool:
value = str(value).upper()
return super().__eq__(value)
def __hash__(self) -> int:
return hash(self.value)
def __str__(self) -> str:
return self.value
GET = auto()
POST = auto()
PUT = auto()
HEAD = auto()
OPTIONS = auto()
PATCH = auto()
DELETE = auto()
HTTP_METHODS = tuple(HTTPMethod.__members__.values())
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"

View File

@@ -366,7 +366,7 @@ def exception_response(
except InvalidUsage:
renderer = HTMLRenderer
content_type, *_ = request.headers.getone(
content_type, *_ = request.headers.get(
"content-type", ""
).split(";")
renderer = RENDERERS_BY_CONTENT_TYPE.get(

View File

@@ -3,18 +3,26 @@ from typing import Optional, Union
from sanic.helpers import STATUS_CODES
_sanic_exceptions = {}
def add_status_code(code, quiet=None):
"""
Decorator used for adding exceptions to :class:`SanicException`.
"""
def class_decorator(cls):
cls.status_code = code
if quiet or quiet is None and code != 500:
cls.quiet = True
_sanic_exceptions[code] = cls
return cls
return class_decorator
class SanicException(Exception):
def __init__(
self,
message: Optional[Union[str, bytes]] = None,
status_code: Optional[int] = None,
quiet: Optional[bool] = None,
) -> None:
if message is None and status_code is not None:
msg: bytes = STATUS_CODES.get(status_code, b"")
message = msg.decode("utf8")
def __init__(self, message, status_code=None, quiet=None):
super().__init__(message)
if status_code is not None:
@@ -25,42 +33,45 @@ class SanicException(Exception):
self.quiet = True
@add_status_code(404)
class NotFound(SanicException):
"""
**Status**: 404 Not Found
"""
status_code = 404
pass
@add_status_code(400)
class InvalidUsage(SanicException):
"""
**Status**: 400 Bad Request
"""
status_code = 400
pass
@add_status_code(405)
class MethodNotSupported(SanicException):
"""
**Status**: 405 Method Not Allowed
"""
status_code = 405
def __init__(self, message, method, allowed_methods):
super().__init__(message)
self.headers = {"Allow": ", ".join(allowed_methods)}
@add_status_code(500)
class ServerError(SanicException):
"""
**Status**: 500 Internal Server Error
"""
status_code = 500
pass
@add_status_code(503)
class ServiceUnavailable(SanicException):
"""
**Status**: 503 Service Unavailable
@@ -69,7 +80,7 @@ class ServiceUnavailable(SanicException):
down for maintenance). Generally, this is a temporary state.
"""
status_code = 503
pass
class URLBuildError(ServerError):
@@ -77,7 +88,7 @@ class URLBuildError(ServerError):
**Status**: 500 Internal Server Error
"""
status_code = 500
pass
class FileNotFound(NotFound):
@@ -91,6 +102,7 @@ class FileNotFound(NotFound):
self.relative_url = relative_url
@add_status_code(408)
class RequestTimeout(SanicException):
"""The Web server (running the Web site) thinks that there has been too
long an interval of time between 1) the establishment of an IP
@@ -100,15 +112,16 @@ class RequestTimeout(SanicException):
server has 'timed out' on that particular socket connection.
"""
status_code = 408
pass
@add_status_code(413)
class PayloadTooLarge(SanicException):
"""
**Status**: 413 Payload Too Large
"""
status_code = 413
pass
class HeaderNotFound(InvalidUsage):
@@ -116,35 +129,36 @@ class HeaderNotFound(InvalidUsage):
**Status**: 400 Bad Request
"""
status_code = 400
pass
@add_status_code(416)
class ContentRangeError(SanicException):
"""
**Status**: 416 Range Not Satisfiable
"""
status_code = 416
def __init__(self, message, content_range):
super().__init__(message)
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
@add_status_code(417)
class HeaderExpectationFailed(SanicException):
"""
**Status**: 417 Expectation Failed
"""
status_code = 417
pass
@add_status_code(403)
class Forbidden(SanicException):
"""
**Status**: 403 Forbidden
"""
status_code = 403
pass
class InvalidRangeType(ContentRangeError):
@@ -152,7 +166,7 @@ class InvalidRangeType(ContentRangeError):
**Status**: 416 Range Not Satisfiable
"""
status_code = 416
pass
class PyFileError(Exception):
@@ -160,6 +174,7 @@ class PyFileError(Exception):
super().__init__("could not execute config file %s", file)
@add_status_code(401)
class Unauthorized(SanicException):
"""
**Status**: 401 Unauthorized
@@ -195,8 +210,6 @@ class Unauthorized(SanicException):
realm="Restricted Area")
"""
status_code = 401
def __init__(self, message, status_code=None, scheme=None, **kwargs):
super().__init__(message, status_code)
@@ -228,13 +241,9 @@ def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
:param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages in
"""
import warnings
warnings.warn(
"sanic.exceptions.abort has been marked as deprecated, and will be "
"removed in release 21.12.\n To migrate your code, simply replace "
"abort(status_code, msg) with raise SanicException(msg, status_code), "
"or even better, raise an appropriate SanicException subclass."
)
raise SanicException(message=message, status_code=status_code)
if message is None:
msg: bytes = STATUS_CODES[status_code]
# These are stored as bytes in the STATUS_CODES dict
message = msg.decode("utf8")
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
raise sanic_exception(message=message, status_code=status_code)

View File

@@ -6,7 +6,7 @@ from sanic.exceptions import (
HeaderNotFound,
InvalidRangeType,
)
from sanic.log import error_logger
from sanic.log import logger
from sanic.response import text
@@ -25,6 +25,7 @@ class ErrorHandler:
handlers = None
cached_handlers = None
_missing = object()
def __init__(self):
self.handlers = []
@@ -44,9 +45,7 @@ class ErrorHandler:
:return: None
"""
# self.handlers to be deprecated and removed in version 21.12
self.handlers.append((exception, handler))
self.cached_handlers[exception] = handler
def lookup(self, exception):
"""
@@ -62,19 +61,14 @@ class ErrorHandler:
:return: Registered function if found ``None`` otherwise
"""
exception_class = type(exception)
if exception_class in self.cached_handlers:
return self.cached_handlers[exception_class]
for ancestor in type.mro(exception_class):
if ancestor in self.cached_handlers:
handler = self.cached_handlers[ancestor]
self.cached_handlers[exception_class] = handler
return handler
if ancestor is BaseException:
break
self.cached_handlers[exception_class] = None
handler = None
handler = self.cached_handlers.get(type(exception), self._missing)
if handler is self._missing:
for exception_class, handler in self.handlers:
if isinstance(exception, exception_class):
self.cached_handlers[type(exception)] = handler
return handler
self.cached_handlers[type(exception)] = None
handler = None
return handler
def response(self, request, exception):
@@ -107,7 +101,7 @@ class ErrorHandler:
response_message = (
"Exception raised in exception handler " '"%s" for uri: %s'
)
error_logger.exception(response_message, handler.__name__, url)
logger.exception(response_message, handler.__name__, url)
if self.debug:
return text(response_message % (handler.__name__, url), 500)
@@ -143,9 +137,7 @@ class ErrorHandler:
url = "unknown"
self.log(format_exc())
error_logger.exception(
"Exception occurred while handling uri: %s", url
)
logger.exception("Exception occurred while handling uri: %s", url)
return exception_response(request, exception, self.debug)
@@ -173,7 +165,7 @@ class ContentRangeHandler:
def __init__(self, request, stats):
self.total = stats.st_size
_range = request.headers.getone("range", None)
_range = request.headers.get("Range")
if _range is None:
raise HeaderNotFound("Range Header Not Found")
unit, _, value = tuple(map(str.strip, _range.partition("=")))

View File

@@ -102,7 +102,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
"""Parse traditional proxy headers."""
real_ip_header = config.REAL_IP_HEADER
proxies_count = config.PROXIES_COUNT
addr = real_ip_header and headers.getone(real_ip_header, None)
addr = real_ip_header and headers.get(real_ip_header)
if not addr and proxies_count:
assert proxies_count > 0
try:
@@ -131,7 +131,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
("port", "x-forwarded-port"),
("path", "x-forwarded-path"),
):
yield key, headers.getone(header, None)
yield key, headers.get(header)
return fwd_normalize(options())

View File

@@ -20,7 +20,7 @@ from sanic.exceptions import (
)
from sanic.headers import format_http1_response
from sanic.helpers import has_message_body
from sanic.log import access_logger, error_logger, logger
from sanic.log import access_logger, logger
class Stage(Enum):
@@ -64,9 +64,6 @@ class Http:
:raises RuntimeError:
"""
HEADER_CEILING = 16_384
HEADER_MAX_SIZE = 0
__slots__ = [
"_send",
"_receive_more",
@@ -147,7 +144,7 @@ class Http:
# Try to consume any remaining request body
if self.request_body:
if self.response and 200 <= self.response.status < 300:
error_logger.error(f"{self.request} body not consumed.")
logger.error(f"{self.request} body not consumed.")
try:
async for _ in self:
@@ -172,6 +169,7 @@ class Http:
"""
Receive and parse request header into self.request.
"""
HEADER_MAX_SIZE = min(8192, self.request_max_size)
# Receive until full header is in buffer
buf = self.recv_buffer
pos = 0
@@ -182,12 +180,12 @@ class Http:
break
pos = max(0, len(buf) - 3)
if pos >= self.HEADER_MAX_SIZE:
if pos >= HEADER_MAX_SIZE:
break
await self._receive_more()
if pos >= self.HEADER_MAX_SIZE:
if pos >= HEADER_MAX_SIZE:
raise PayloadTooLarge("Request header exceeds the size limit")
# Parse header content
@@ -221,9 +219,7 @@ class Http:
raise InvalidUsage("Bad Request")
headers_instance = Header(headers)
self.upgrade_websocket = (
headers_instance.getone("upgrade", "").lower() == "websocket"
)
self.upgrade_websocket = headers_instance.get("upgrade") == "websocket"
# Prepare a Request object
request = self.protocol.request_class(
@@ -240,7 +236,7 @@ class Http:
self.request_bytes_left = self.request_bytes = 0
if request_body:
headers = request.headers
expect = headers.getone("expect", None)
expect = headers.get("expect")
if expect is not None:
if expect.lower() == "100-continue":
@@ -248,7 +244,7 @@ class Http:
else:
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
if headers.getone("transfer-encoding", None) == "chunked":
if headers.get("transfer-encoding") == "chunked":
self.request_body = "chunked"
pos -= 2 # One CRLF stays in buffer
else:
@@ -543,10 +539,3 @@ class Http:
@property
def send(self):
return self.response_func
@classmethod
def set_header_max_size(cls, *sizes: int):
cls.HEADER_MAX_SIZE = min(
*sizes,
cls.HEADER_CEILING,
)

View File

@@ -35,7 +35,7 @@ class ListenerMixin:
"""
Create a listener from a decorated function.
To be used as a decorator:
To be used as a deocrator:
.. code-block:: python

View File

@@ -26,11 +26,10 @@ from sanic.views import CompositionView
class RouteMixin:
name: str
def __init__(self, *args, **kwargs) -> None:
self._future_routes: Set[FutureRoute] = set()
self._future_statics: Set[FutureStatic] = set()
self.name = ""
self.strict_slashes: Optional[bool] = False
def _apply_route(self, route: FutureRoute) -> List[Route]:
@@ -54,7 +53,6 @@ class RouteMixin:
websocket: bool = False,
unquote: bool = False,
static: bool = False,
version_prefix: str = "/v",
):
"""
Decorate a function to be registered as a route
@@ -68,8 +66,6 @@ class RouteMixin:
:param name: user defined route name for url_for
:param ignore_body: whether the handler should ignore request
body (eg. GET requests)
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: tuple of routes, decorated function
"""
@@ -96,7 +92,6 @@ class RouteMixin:
nonlocal subprotocols
nonlocal websocket
nonlocal static
nonlocal version_prefix
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
@@ -133,7 +128,6 @@ class RouteMixin:
subprotocols,
unquote,
static,
version_prefix,
)
self._future_routes.add(route)
@@ -160,9 +154,7 @@ class RouteMixin:
if apply:
self._apply_route(route)
if static:
return route, handler
return handler
return route, handler
return decorator
@@ -176,7 +168,6 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
stream: bool = False,
version_prefix: str = "/v",
):
"""A helper method to register class instance or
functions as a handler to the application url
@@ -191,8 +182,6 @@ class RouteMixin:
:param version:
:param name: user defined route name for url_for
:param stream: boolean specifying if the handler is a stream handler
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: function or class instance
"""
# Handle HTTPMethodView differently
@@ -225,7 +214,6 @@ class RouteMixin:
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
)(handler)
return handler
@@ -238,7 +226,6 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
):
"""
Add an API URL under the **GET** *HTTP* method
@@ -249,8 +236,6 @@ class RouteMixin:
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -261,7 +246,6 @@ class RouteMixin:
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
)
def post(
@@ -272,7 +256,6 @@ class RouteMixin:
stream: bool = False,
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
):
"""
Add an API URL under the **POST** *HTTP* method
@@ -283,8 +266,6 @@ class RouteMixin:
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -295,7 +276,6 @@ class RouteMixin:
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
)
def put(
@@ -306,7 +286,6 @@ class RouteMixin:
stream: bool = False,
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
):
"""
Add an API URL under the **PUT** *HTTP* method
@@ -317,8 +296,6 @@ class RouteMixin:
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -329,7 +306,6 @@ class RouteMixin:
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
)
def head(
@@ -340,7 +316,6 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
):
"""
Add an API URL under the **HEAD** *HTTP* method
@@ -359,8 +334,6 @@ class RouteMixin:
:param ignore_body: whether the handler should ignore request
body (eg. GET requests), defaults to True
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -371,7 +344,6 @@ class RouteMixin:
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
)
def options(
@@ -382,7 +354,6 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
):
"""
Add an API URL under the **OPTIONS** *HTTP* method
@@ -401,8 +372,6 @@ class RouteMixin:
:param ignore_body: whether the handler should ignore request
body (eg. GET requests), defaults to True
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -413,7 +382,6 @@ class RouteMixin:
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
)
def patch(
@@ -424,7 +392,6 @@ class RouteMixin:
stream=False,
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
):
"""
Add an API URL under the **PATCH** *HTTP* method
@@ -445,8 +412,6 @@ class RouteMixin:
:param ignore_body: whether the handler should ignore request
body (eg. GET requests), defaults to True
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -457,7 +422,6 @@ class RouteMixin:
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
)
def delete(
@@ -468,7 +432,6 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
):
"""
Add an API URL under the **DELETE** *HTTP* method
@@ -479,8 +442,6 @@ class RouteMixin:
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -491,7 +452,6 @@ class RouteMixin:
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
)
def websocket(
@@ -503,7 +463,6 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
apply: bool = True,
version_prefix: str = "/v",
):
"""
Decorate a function to be registered as a websocket route
@@ -515,8 +474,6 @@ class RouteMixin:
:param subprotocols: optional list of str with supported subprotocols
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: tuple of routes, decorated function
"""
return self.route(
@@ -529,7 +486,6 @@ class RouteMixin:
apply=apply,
subprotocols=subprotocols,
websocket=True,
version_prefix=version_prefix,
)
def add_websocket_route(
@@ -541,7 +497,6 @@ class RouteMixin:
subprotocols=None,
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
):
"""
A helper method to register a function as a websocket route.
@@ -558,8 +513,6 @@ class RouteMixin:
handshake
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Objected decorated by :func:`websocket`
"""
return self.websocket(
@@ -569,7 +522,6 @@ class RouteMixin:
subprotocols=subprotocols,
version=version,
name=name,
version_prefix=version_prefix,
)(handler)
def static(
@@ -713,10 +665,7 @@ class RouteMixin:
modified_since = strftime(
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
)
if (
request.headers.getone("if-modified-since", None)
== modified_since
):
if request.headers.get("If-Modified-Since") == modified_since:
return HTTPResponse(status=304)
headers["Last-Modified"] = modified_since
_range = None
@@ -769,18 +718,16 @@ class RouteMixin:
return await file(file_path, headers=headers, _range=_range)
except ContentRangeError:
raise
except FileNotFoundError:
except Exception:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
except Exception:
error_logger.exception(
f"Exception in static request handler:\
path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
def _register_static(
self,

View File

@@ -1,4 +1,4 @@
from typing import Any, Callable, Dict, Optional, Set
from typing import Any, Callable, Dict, Set
from sanic.models.futures import FutureSignal
from sanic.models.handler_types import SignalHandler
@@ -60,16 +60,10 @@ class SignalMixin:
def add_signal(
self,
handler: Optional[Callable[..., Any]],
handler,
event: str,
condition: Dict[str, Any] = None,
):
if not handler:
async def noop():
...
handler = noop
self.signal(event=event, condition=condition)(handler)
return handler

View File

@@ -23,7 +23,6 @@ class FutureRoute(NamedTuple):
subprotocols: Optional[List[str]]
unquote: bool
static: bool
version_prefix: str
class FutureListener(NamedTuple):

View File

@@ -1,4 +1,3 @@
import itertools
import os
import signal
import subprocess
@@ -6,9 +5,6 @@ import sys
from time import sleep
from sanic.config import BASE_LOGO
from sanic.log import logger
def _iter_module_files():
"""This iterates over all relevant Python files.
@@ -60,21 +56,7 @@ def restart_with_reloader():
)
def _check_file(filename, mtimes):
need_reload = False
mtime = os.stat(filename).st_mtime
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
elif mtime > old_time:
mtimes[filename] = mtime
need_reload = True
return need_reload
def watchdog(sleep_interval, app):
def watchdog(sleep_interval):
"""Watch project files, restart worker process if a change happened.
:param sleep_interval: interval in second.
@@ -91,25 +73,21 @@ def watchdog(sleep_interval, app):
worker_process = restart_with_reloader()
if app.config.LOGO:
logger.debug(
app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO
)
try:
while True:
need_reload = False
for filename in itertools.chain(
_iter_module_files(),
*(d.glob("**/*") for d in app.reload_dirs),
):
for filename in _iter_module_files():
try:
check = _check_file(filename, mtimes)
mtime = os.stat(filename).st_mtime
except OSError:
continue
if check:
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
elif mtime > old_time:
mtimes[filename] = mtime
need_reload = True
if need_reload:

View File

@@ -125,7 +125,7 @@ class Request:
self._name: Optional[str] = None
self.app = app
self.headers = Header(headers)
self.headers = headers
self.version = version
self.method = method
self.transport = transport
@@ -262,7 +262,7 @@ class Request:
app = Sanic("MyApp", request_class=IntRequest)
"""
if not self._id:
self._id = self.headers.getone(
self._id = self.headers.get(
self.app.config.REQUEST_ID_HEADER,
self.__class__.generate_id(self), # type: ignore
)
@@ -303,7 +303,7 @@ class Request:
:return: token related to request
"""
prefixes = ("Bearer", "Token")
auth_header = self.headers.getone("authorization", None)
auth_header = self.headers.get("Authorization")
if auth_header is not None:
for prefix in prefixes:
@@ -317,8 +317,8 @@ class Request:
if self.parsed_form is None:
self.parsed_form = RequestParameters()
self.parsed_files = RequestParameters()
content_type = self.headers.getone(
"content-type", DEFAULT_HTTP_CONTENT_TYPE
content_type = self.headers.get(
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
)
content_type, parameters = parse_content_header(content_type)
try:
@@ -378,12 +378,9 @@ class Request:
:type errors: str
:return: RequestParameters
"""
if (
keep_blank_values,
strict_parsing,
encoding,
errors,
) not in self.parsed_args:
if not self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
if self.query_string:
self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
@@ -437,12 +434,9 @@ class Request:
:type errors: str
:return: list
"""
if (
keep_blank_values,
strict_parsing,
encoding,
errors,
) not in self.parsed_not_grouped_args:
if not self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
if self.query_string:
self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
@@ -471,7 +465,7 @@ class Request:
"""
if self._cookies is None:
cookie = self.headers.getone("cookie", None)
cookie = self.headers.get("Cookie")
if cookie is not None:
cookies: SimpleCookie = SimpleCookie()
cookies.load(cookie)
@@ -488,7 +482,7 @@ class Request:
:return: Content-Type header form the request
:rtype: str
"""
return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE)
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
@property
def match_info(self):
@@ -505,7 +499,7 @@ class Request:
:return: peer ip of the socket
:rtype: str
"""
return self.conn_info.client_ip if self.conn_info else ""
return self.conn_info.client if self.conn_info else ""
@property
def port(self) -> int:
@@ -587,7 +581,7 @@ class Request:
if (
self.app.websocket_enabled
and self.headers.getone("upgrade", "").lower() == "websocket"
and self.headers.get("upgrade") == "websocket"
):
scheme = "ws"
else:
@@ -614,9 +608,7 @@ class Request:
server_name = self.app.config.get("SERVER_NAME")
if server_name:
return server_name.split("//", 1)[-1].split("/", 1)[0]
return str(
self.forwarded.get("host") or self.headers.getone("host", "")
)
return str(self.forwarded.get("host") or self.headers.get("host", ""))
@property
def server_name(self) -> str:

View File

@@ -143,7 +143,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
.. warning::
**Deprecated** and set for removal in v21.12. You can now achieve the
**Deprecated** and set for removal in v21.6. You can now achieve the
same functionality without a callback.
.. code-block:: python
@@ -174,16 +174,12 @@ class StreamingHTTPResponse(BaseHTTPResponse):
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: str = "text/plain; charset=utf-8",
ignore_deprecation_notice: bool = False,
chunked="deprecated",
):
if not ignore_deprecation_notice:
if chunked != "deprecated":
warn(
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
"will be removed in v21.12. Please upgrade your streaming "
"response implementation. You can learn more here: "
"https://sanicframework.org/en/guide/advanced/streaming.html"
"#response-streaming. If you use the builtin stream() or "
"file_stream() methods, this upgrade will be be done for you."
"The chunked argument has been deprecated and will be "
"removed in v21.6"
)
super().__init__()
@@ -207,9 +203,6 @@ class StreamingHTTPResponse(BaseHTTPResponse):
self.streaming_fn = None
await super().send(*args, **kwargs)
async def eof(self):
raise NotImplementedError
class HTTPResponse(BaseHTTPResponse):
"""
@@ -242,15 +235,6 @@ class HTTPResponse(BaseHTTPResponse):
self.headers = Header(headers or {})
self._cookies = None
async def eof(self):
await self.send("", True)
async def __aenter__(self):
return self.send
async def __aexit__(self, *_):
await self.eof()
def empty(
status=204, headers: Optional[Dict[str, str]] = None
@@ -412,6 +396,7 @@ async def file_stream(
mime_type: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None,
chunked="deprecated",
_range: Optional[Range] = None,
) -> StreamingHTTPResponse:
"""Return a streaming response object with file data.
@@ -424,6 +409,12 @@ async def file_stream(
:param chunked: Deprecated
:param _range:
"""
if chunked != "deprecated":
warn(
"The chunked argument has been deprecated and will be "
"removed in v21.6"
)
headers = headers or {}
if filename:
headers.setdefault(
@@ -462,7 +453,6 @@ async def file_stream(
status=status,
headers=headers,
content_type=mime_type,
ignore_deprecation_notice=True,
)
@@ -471,6 +461,7 @@ def stream(
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
chunked="deprecated",
):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
@@ -491,12 +482,17 @@ def stream(
:param headers: Custom Headers.
:param chunked: Deprecated
"""
if chunked != "deprecated":
warn(
"The chunked argument has been deprecated and will be "
"removed in v21.6"
)
return StreamingHTTPResponse(
streaming_fn,
headers=headers,
content_type=content_type,
status=status,
ignore_deprecation_notice=True,
)

View File

@@ -73,7 +73,6 @@ class Router(BaseRouter):
name: Optional[str] = None,
unquote: bool = False,
static: bool = False,
version_prefix: str = "/v",
) -> Union[Route, List[Route]]:
"""
Add a handler to the router
@@ -104,12 +103,12 @@ class Router(BaseRouter):
"""
if version is not None:
version = str(version).strip("/").lstrip("v")
uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")])
uri = "/".join([f"/v{version}", uri.lstrip("/")])
params = dict(
path=uri,
handler=handler,
methods=frozenset(map(str, methods)) if methods else None,
methods=methods,
name=name,
strict=strict_slashes,
unquote=unquote,

View File

@@ -39,7 +39,7 @@ from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.config import Config
from sanic.exceptions import RequestTimeout, ServiceUnavailable
from sanic.http import Http, Stage
from sanic.log import error_logger, logger
from sanic.log import logger
from sanic.models.protocol_types import TransportProtocol
from sanic.request import Request
@@ -65,7 +65,6 @@ class ConnInfo:
__slots__ = (
"client_port",
"client",
"client_ip",
"ctx",
"peername",
"server_port",
@@ -79,7 +78,6 @@ class ConnInfo:
self.peername = None
self.server = self.client = ""
self.server_port = self.client_port = 0
self.client_ip = ""
self.sockname = addr = transport.get_extra_info("sockname")
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
@@ -98,7 +96,6 @@ class ConnInfo:
if isinstance(addr, tuple):
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
self.client_ip = addr[0]
self.client_port = addr[1]
@@ -125,6 +122,7 @@ class HttpProtocol(asyncio.Protocol):
"response_timeout",
"keep_alive_timeout",
"request_max_size",
"request_buffer_queue_size",
"request_class",
"error_handler",
# enable or disable access log purpose
@@ -167,6 +165,9 @@ class HttpProtocol(asyncio.Protocol):
self.request_handler = self.app.handle_request
self.error_handler = self.app.error_handler
self.request_timeout = self.app.config.REQUEST_TIMEOUT
self.request_buffer_queue_size = (
self.app.config.REQUEST_BUFFER_QUEUE_SIZE
)
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
@@ -198,11 +199,11 @@ class HttpProtocol(asyncio.Protocol):
except CancelledError:
pass
except Exception:
error_logger.exception("protocol.connection_task uncaught")
logger.exception("protocol.connection_task uncaught")
finally:
if self.app.debug and self._http:
ip = self.transport.get_extra_info("peername")
error_logger.error(
logger.error(
"Connection lost before response written"
f" @ {ip} {self._http.request}"
)
@@ -211,7 +212,7 @@ class HttpProtocol(asyncio.Protocol):
try:
self.close()
except BaseException:
error_logger.exception("Closing failed")
logger.exception("Closing failed")
async def receive_more(self):
"""
@@ -257,7 +258,7 @@ class HttpProtocol(asyncio.Protocol):
return
self._task.cancel()
except Exception:
error_logger.exception("protocol.check_timeouts")
logger.exception("protocol.check_timeouts")
async def send(self, data):
"""
@@ -303,7 +304,7 @@ class HttpProtocol(asyncio.Protocol):
self.recv_buffer = bytearray()
self.conn_info = ConnInfo(self.transport, unix=self._unix)
except Exception:
error_logger.exception("protocol.connect_made")
logger.exception("protocol.connect_made")
def connection_lost(self, exc):
try:
@@ -312,7 +313,7 @@ class HttpProtocol(asyncio.Protocol):
if self._task:
self._task.cancel()
except Exception:
error_logger.exception("protocol.connection_lost")
logger.exception("protocol.connection_lost")
def pause_writing(self):
self._can_write.clear()
@@ -336,7 +337,7 @@ class HttpProtocol(asyncio.Protocol):
if self._data_received:
self._data_received.set()
except Exception:
error_logger.exception("protocol.data_received")
logger.exception("protocol.data_received")
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
@@ -555,7 +556,7 @@ def serve(
try:
http_server = loop.run_until_complete(server_coroutine)
except BaseException:
error_logger.exception("Unable to start server")
logger.exception("Unable to start server")
return
trigger_events(after_start, loop)

View File

@@ -48,7 +48,7 @@ class SignalRouter(BaseRouter):
f".{event}",
self.DEFAULT_METHOD,
self,
{"__params__": {}, "__matches__": {}},
{"__params__": {}},
extra=extra,
)
except NotFound:
@@ -59,13 +59,7 @@ class SignalRouter(BaseRouter):
terms.append(extra)
raise NotFound(message % tuple(terms))
params = param_basket["__params__"]
if not params:
params = {
param.name: param_basket["__matches__"][idx]
for idx, param in group.params.items()
}
params = param_basket.pop("__params__")
return group, [route.handler for route in group], params
async def _dispatch(

View File

@@ -1,21 +0,0 @@
from pathlib import Path
from sanic import Sanic
from sanic.exceptions import SanicException
from sanic.response import redirect
def create_simple_server(directory: Path):
if not directory.is_dir():
raise SanicException(
"Cannot setup Sanic Simple Server without a path to a directory"
)
app = Sanic("SimpleServer")
app.static("/", directory, name="main")
@app.get("/")
def index(_):
return redirect(app.url_for("main", filename="index.html"))
return app

View File

@@ -105,7 +105,6 @@ def load_module_from_file_location(
_mod_spec = spec_from_file_location(
name, location, *args, **kwargs
)
assert _mod_spec is not None # type assertion for mypy
module = module_from_spec(_mod_spec)
_mod_spec.loader.exec_module(module) # type: ignore

View File

@@ -1,25 +1,9 @@
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Callable,
Iterable,
List,
Optional,
Union,
)
from warnings import warn
from typing import Any, Callable, List
from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage
if TYPE_CHECKING:
from sanic import Sanic
from sanic.blueprints import Blueprint
class HTTPMethodView:
"""Simple class based implementation of view for the sanic.
You should implement methods (get, post, put, patch, delete) for the class
@@ -56,31 +40,6 @@ class HTTPMethodView:
decorators: List[Callable[[Callable[..., Any]], Callable[..., Any]]] = []
def __init_subclass__(
cls,
attach: Optional[Union[Sanic, Blueprint]] = None,
uri: str = "",
methods: Iterable[str] = frozenset({"GET"}),
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
name: Optional[str] = None,
stream: bool = False,
version_prefix: str = "/v",
) -> None:
if attach:
cls.attach(
attach,
uri=uri,
methods=methods,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
stream=stream,
version_prefix=version_prefix,
)
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
return handler(request, *args, **kwargs)
@@ -106,31 +65,6 @@ class HTTPMethodView:
view.__name__ = cls.__name__
return view
@classmethod
def attach(
cls,
to: Union[Sanic, Blueprint],
uri: str,
methods: Iterable[str] = frozenset({"GET"}),
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
name: Optional[str] = None,
stream: bool = False,
version_prefix: str = "/v",
) -> None:
to.add_route(
cls.as_view(),
uri=uri,
methods=methods,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
stream=stream,
version_prefix=version_prefix,
)
def stream(func):
func.is_stream = True
@@ -157,11 +91,6 @@ class CompositionView:
def __init__(self):
self.handlers = {}
self.name = self.__class__.__name__
warn(
"CompositionView has been deprecated and will be removed in "
"v21.12. Please update your view to HTTPMethodView.",
DeprecationWarning,
)
def __name__(self):
return self.name

View File

@@ -14,13 +14,9 @@ from websockets import ( # type: ignore
ConnectionClosed,
InvalidHandshake,
WebSocketCommonProtocol,
handshake,
)
# Despite the "legacy" namespace, the primary maintainer of websockets
# committed to maintaining backwards-compatibility until 2026 and will
# consider extending it if sanic continues depending on this module.
from websockets.legacy import handshake
from sanic.exceptions import InvalidUsage
from sanic.server import HttpProtocol
@@ -41,7 +37,7 @@ class WebSocketProtocol(HttpProtocol):
websocket_write_limit=2 ** 16,
websocket_ping_interval=20,
websocket_ping_timeout=20,
**kwargs,
**kwargs
):
super().__init__(*args, **kwargs)
self.websocket = None
@@ -130,9 +126,7 @@ class WebSocketProtocol(HttpProtocol):
ping_interval=self.websocket_ping_interval,
ping_timeout=self.websocket_ping_timeout,
)
# we use WebSocketCommonProtocol because we don't want the handshake
# logic from WebSocketServerProtocol; however, we must tell it that
# we're running on the server side
# Following two lines are required for websockets 8.x
self.websocket.is_client = False
self.websocket.side = "server"
self.websocket.subprotocol = subprotocol
@@ -154,7 +148,7 @@ class WebSocketConnection:
) -> None:
self._send = send
self._receive = receive
self._subprotocols = subprotocols or []
self.subprotocols = subprotocols or []
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
@@ -178,28 +172,13 @@ class WebSocketConnection:
receive = recv
async def accept(self, subprotocols: Optional[List[str]] = None) -> None:
subprotocol = None
if subprotocols:
for subp in subprotocols:
if subp in self.subprotocols:
subprotocol = subp
break
async def accept(self) -> None:
await self._send(
{
"type": "websocket.accept",
"subprotocol": subprotocol,
"subprotocol": ",".join(list(self.subprotocols)),
}
)
async def close(self) -> None:
pass
@property
def subprotocols(self):
return self._subprotocols
@subprotocols.setter
def subprotocols(self, subprotocols: Optional[List[str]] = None):
self._subprotocols = subprotocols or []

View File

@@ -83,17 +83,17 @@ ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency
requirements = [
"sanic-routing==0.7.0",
"sanic-routing>=0.6.0",
"httptools>=0.0.10",
uvloop,
ujson,
"aiofiles>=0.6.0",
"websockets>=9.0",
"websockets>=8.1,<9.0",
"multidict>=5.0,<6.0",
]
tests_require = [
"sanic-testing>=0.6.0",
"sanic-testing",
"pytest==5.2.1",
"multidict>=5.0,<6.0",
"gunicorn==20.0.4",

View File

@@ -15,7 +15,6 @@ from sanic.constants import HTTP_METHODS
from sanic.router import Router
slugify = re.compile(r"[^a-zA-Z0-9_\-]")
random.seed("Pack my box with five dozen liquor jugs.")
Sanic.test_mode = True
@@ -141,5 +140,5 @@ def url_param_generator():
@pytest.fixture(scope="function")
def app(request):
app = Sanic(slugify.sub("-", request.node.name))
app = Sanic(request.node.name)
return app

View File

@@ -1,36 +0,0 @@
import json
import logging
from sanic import Sanic, text
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
LOGGING_CONFIG = {**LOGGING_CONFIG_DEFAULTS}
LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s"
LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG"
app = Sanic(__name__, log_config=LOGGING_CONFIG)
@app.get("/")
async def handler(request):
return text(request.ip)
@app.before_server_start
async def app_info_dump(app: Sanic, _):
app_data = {
"access_log": app.config.ACCESS_LOG,
"auto_reload": app.auto_reload,
"debug": app.debug,
}
logger.info(json.dumps(app_data))
@app.after_server_start
async def shutdown(app: Sanic, _):
app.stop()
def create_app():
return app

View File

@@ -9,7 +9,6 @@ from unittest.mock import Mock, patch
import pytest
from sanic import Sanic
from sanic.config import Config
from sanic.exceptions import SanicException
from sanic.response import text
@@ -277,7 +276,7 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
assert response.status == 500
assert "Mock SanicException" in response.text
assert (
"sanic.error",
"sanic.root",
logging.ERROR,
f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'",
) in caplog.record_tuples
@@ -390,7 +389,7 @@ def test_app_no_registry_env():
def test_app_set_attribute_warning(app):
with pytest.warns(DeprecationWarning) as record:
with pytest.warns(UserWarning) as record:
app.foo = 1
assert len(record) == 1
@@ -413,42 +412,3 @@ def test_subclass_initialisation():
pass
CustomSanic("test_subclass_initialisation")
def test_bad_custom_config():
with pytest.raises(
SanicException,
match=(
"When instantiating Sanic with config, you cannot also pass "
"load_env or env_prefix"
),
):
Sanic("test", config=1, load_env=1)
with pytest.raises(
SanicException,
match=(
"When instantiating Sanic with config, you cannot also pass "
"load_env or env_prefix"
),
):
Sanic("test", config=1, env_prefix=1)
def test_custom_config():
class CustomConfig(Config):
...
config = CustomConfig()
app = Sanic("custom", config=config)
assert app.config == config
def test_custom_context():
class CustomContext:
...
ctx = CustomContext()
app = Sanic("custom", ctx=ctx)
assert app.ctx == ctx

View File

@@ -1,4 +1,5 @@
import asyncio
import sys
from collections import deque, namedtuple
@@ -218,7 +219,7 @@ async def test_websocket_accept_with_no_subprotocols(
message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] is None
assert message["subprotocol"] == ""
assert "bytes" not in message
@@ -227,7 +228,7 @@ async def test_websocket_accept_with_subprotocol(send, receive, message_stack):
subprotocols = ["graphql-ws"]
ws = WebSocketConnection(send, receive, subprotocols)
await ws.accept(subprotocols)
await ws.accept()
assert len(message_stack) == 1
@@ -244,13 +245,13 @@ async def test_websocket_accept_with_multiple_subprotocols(
subprotocols = ["graphql-ws", "hello", "world"]
ws = WebSocketConnection(send, receive, subprotocols)
await ws.accept(["hello", "world"])
await ws.accept()
assert len(message_stack) == 1
message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == "hello"
assert message["subprotocol"] == "graphql-ws,hello,world"
assert "bytes" not in message

View File

@@ -41,62 +41,3 @@ def test_bp_repr_with_values(bp):
'Blueprint(name="my_bp", url_prefix="/foo", host="example.com", '
"version=3, strict_slashes=True)"
)
@pytest.mark.parametrize(
"name",
(
"something",
"some-thing",
"some_thing",
"Something",
"SomeThing",
"Some-Thing",
"Some_Thing",
"SomeThing123",
"something123",
"some-thing123",
"some_thing123",
"some-Thing123",
"some_Thing123",
),
)
def test_names_okay(name):
app = Sanic(name)
bp = Blueprint(name)
assert app.name == name
assert bp.name == name
@pytest.mark.parametrize(
"name",
(
"123something",
"some thing",
"something!",
),
)
def test_names_not_okay(name):
app_message = (
f"Sanic instance named '{name}' uses a format that isdeprecated. "
"Starting in version 21.12, Sanic objects must be named only using "
"alphanumeric characters, _, or -."
)
bp_message = (
f"Blueprint instance named '{name}' uses a format that isdeprecated. "
"Starting in version 21.12, Blueprint objects must be named only using "
"alphanumeric characters, _, or -."
)
with pytest.warns(DeprecationWarning) as app_e:
app = Sanic(name)
with pytest.warns(DeprecationWarning) as bp_e:
bp = Blueprint(name)
assert app.name == name
assert bp.name == name
assert app_e[0].message.args[0] == app_message
assert bp_e[0].message.args[0] == bp_message

View File

@@ -200,7 +200,7 @@ def test_bp_group_as_nested_group():
blueprint_group_1 = Blueprint.group(
Blueprint.group(blueprint_1, blueprint_2)
)
assert len(blueprint_group_1) == 1
assert len(blueprint_group_1) == 2
def test_blueprint_group_insert():
@@ -215,61 +215,6 @@ def test_blueprint_group_insert():
group.insert(0, blueprint_1)
group.insert(0, blueprint_2)
group.insert(0, blueprint_3)
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
@blueprint_3.route("/")
def blueprint_3_default_route(request):
return text("BP3_OK")
app = Sanic("PropTest")
app.blueprint(group)
app.router.finalize()
routes = [(route.path, route.strict) for route in app.router.routes]
assert len(routes) == 3
assert ("v1/test/bp1/", True) in routes
assert ("v1.3/test/bp2", False) in routes
assert ("v1.3/test", False) in routes
def test_bp_group_properties():
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
group = Blueprint.group(
blueprint_1,
blueprint_2,
version=1,
version_prefix="/api/v",
url_prefix="/grouped",
strict_slashes=True,
)
primary = Blueprint.group(group, url_prefix="/primary")
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
app = Sanic("PropTest")
app.blueprint(group)
app.blueprint(primary)
app.router.finalize()
routes = [route.path for route in app.router.routes]
assert len(routes) == 4
assert "api/v1/grouped/bp1/" in routes
assert "api/v1/grouped/bp2/" in routes
assert "api/v1/primary/grouped/bp1" in routes
assert "api/v1/primary/grouped/bp2" in routes
assert group.blueprints[1].strict_slashes is False
assert group.blueprints[2].strict_slashes is True
assert group.blueprints[0].url_prefix == "/test"

View File

@@ -1028,7 +1028,7 @@ def test_blueprint_registered_multiple_apps():
def test_bp_set_attribute_warning():
bp = Blueprint("bp")
with pytest.warns(DeprecationWarning) as record:
with pytest.warns(UserWarning) as record:
bp.foo = 1
assert len(record) == 1

View File

@@ -1,133 +0,0 @@
import json
import subprocess
from pathlib import Path
import pytest
from sanic_routing import __version__ as __routing_version__
from sanic import __version__
from sanic.config import BASE_LOGO
def capture(command):
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=Path(__file__).parent,
)
try:
out, err = proc.communicate(timeout=0.5)
except subprocess.TimeoutExpired:
proc.kill()
out, err = proc.communicate()
return out, err, proc.returncode
@pytest.mark.parametrize(
"appname",
(
"fake.server.app",
"fake.server:app",
"fake.server:create_app()",
"fake.server.create_app()",
),
)
def test_server_run(appname):
command = ["sanic", appname]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
@pytest.mark.parametrize(
"cmd",
(
("--host=localhost", "--port=9999"),
("-H", "localhost", "-p", "9999"),
),
)
def test_host_port(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://localhost:9999"
@pytest.mark.parametrize(
"num,cmd",
(
(1, (f"--workers={1}",)),
(2, (f"--workers={2}",)),
(4, (f"--workers={4}",)),
(1, ("-w", "1")),
(2, ("-w", "2")),
(4, ("-w", "4")),
),
)
def test_num_workers(num, cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
worker_lines = [line for line in lines if b"worker" in line]
assert exitcode != 1
assert len(worker_lines) == num * 2
@pytest.mark.parametrize("cmd", ("--debug", "-d"))
def test_debug(cmd):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[9]
info = json.loads(app_info)
assert (b"\n".join(lines[:6])).decode("utf-8") == BASE_LOGO
assert info["debug"] is True
assert info["auto_reload"] is True
@pytest.mark.parametrize("cmd", ("--auto-reload", "-r"))
def test_auto_reload(cmd):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[9]
info = json.loads(app_info)
assert info["debug"] is False
assert info["auto_reload"] is True
@pytest.mark.parametrize(
"cmd,expected", (("--access-log", True), ("--no-access-log", False))
)
def test_access_logs(cmd, expected):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[9]
info = json.loads(app_info)
assert info["access_log"] is expected
@pytest.mark.parametrize("cmd", ("--version", "-v"))
def test_version(cmd):
command = ["sanic", cmd]
out, err, exitcode = capture(command)
version_string = f"Sanic {__version__}; Routing {__routing_version__}\n"
assert out == version_string.encode("utf-8")

View File

@@ -59,14 +59,14 @@ def test_load_from_object_string_exception(app):
app.config.load("test_config.Config.test")
def test_auto_env_prefix():
def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(name=__name__)
assert app.config.TEST_ANSWER == 42
del environ["SANIC_TEST_ANSWER"]
def test_auto_bool_env_prefix():
def test_auto_load_bool_env():
environ["SANIC_TEST_ANSWER"] = "True"
app = Sanic(name=__name__)
assert app.config.TEST_ANSWER is True
@@ -80,10 +80,10 @@ def test_dont_load_env():
del environ["SANIC_TEST_ANSWER"]
@pytest.mark.parametrize("load_env", [None, False, "", "MYAPP_"])
def test_load_env_deprecation(load_env):
with pytest.warns(DeprecationWarning, match=r"21\.12"):
_ = Sanic(name=__name__, load_env=load_env)
# @pytest.mark.parametrize("load_env", [None, False, "", "MYAPP_"])
# def test_load_env_deprecation(load_env):
# with pytest.warns(DeprecationWarning, match=r"21\.12"):
# _ = Sanic(name=__name__, load_env=load_env)
def test_load_env_prefix():
@@ -93,12 +93,12 @@ def test_load_env_prefix():
del environ["MYAPP_TEST_ANSWER"]
@pytest.mark.parametrize("env_prefix", [None, ""])
def test_empty_load_env_prefix(env_prefix):
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(name=__name__, env_prefix=env_prefix)
assert getattr(app.config, "TEST_ANSWER", None) is None
del environ["SANIC_TEST_ANSWER"]
# @pytest.mark.parametrize("env_prefix", [None, ""])
# def test_empty_load_env_prefix(env_prefix):
# environ["SANIC_TEST_ANSWER"] = "42"
# app = Sanic(name=__name__, env_prefix=env_prefix)
# assert getattr(app.config, "TEST_ANSWER", None) is None
# del environ["SANIC_TEST_ANSWER"]
def test_load_env_prefix_float_values():
@@ -115,27 +115,6 @@ def test_load_env_prefix_string_value():
del environ["MYAPP_TEST_TOKEN"]
def test_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(name=__name__, env_prefix="MYAPP_")
assert app.config.TEST_ANSWER == 42
del environ["MYAPP_TEST_ANSWER"]
def test_env_prefix_float_values():
environ["MYAPP_TEST_ROI"] = "2.3"
app = Sanic(name=__name__, env_prefix="MYAPP_")
assert app.config.TEST_ROI == 2.3
del environ["MYAPP_TEST_ROI"]
def test_env_prefix_string_value():
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
app = Sanic(name=__name__, env_prefix="MYAPP_")
assert app.config.TEST_TOKEN == "somerandomtesttoken"
del environ["MYAPP_TEST_TOKEN"]
def test_load_from_file(app):
config = dedent(
"""

View File

@@ -1,28 +0,0 @@
from crypt import methods
from sanic import text
from sanic.constants import HTTP_METHODS, HTTPMethod
def test_string_compat():
assert "GET" == HTTPMethod.GET
assert "GET" in HTTP_METHODS
assert "get" == HTTPMethod.GET
assert "get" in HTTP_METHODS
assert HTTPMethod.GET.lower() == "get"
assert HTTPMethod.GET.upper() == "GET"
def test_use_in_routes(app):
@app.route("/", methods=[HTTPMethod.GET, HTTPMethod.POST])
def handler(_):
return text("It works")
_, response = app.test_client.get("/")
assert response.status == 200
assert response.text == "It works"
_, response = app.test_client.post("/")
assert response.status == 200
assert response.text == "It works"

View File

@@ -1,5 +1,3 @@
import warnings
import pytest
from bs4 import BeautifulSoup
@@ -9,7 +7,6 @@ from sanic.exceptions import (
Forbidden,
InvalidUsage,
NotFound,
SanicException,
ServerError,
Unauthorized,
abort,
@@ -71,19 +68,16 @@ def exception_app():
@app.route("/abort/401")
def handler_401_error(request):
raise SanicException(status_code=401)
abort(401)
@app.route("/abort")
def handler_500_error(request):
raise SanicException(status_code=500)
@app.route("/old_abort")
def handler_old_abort_error(request):
abort(500)
return text("OK")
@app.route("/abort/message")
def handler_abort_message(request):
raise SanicException(message="Custom Message", status_code=500)
abort(500, message="Abort")
@app.route("/divide_by_zero")
def handle_unhandled_exception(request):
@@ -214,21 +208,14 @@ def test_exception_in_exception_handler_debug_on(exception_app):
assert response.body.startswith(b"Exception raised in exception ")
def test_sanic_exception(exception_app):
"""Test sanic exceptions are handled"""
def test_abort(exception_app):
"""Test the abort function"""
request, response = exception_app.test_client.get("/abort/401")
assert response.status == 401
request, response = exception_app.test_client.get("/abort")
assert response.status == 500
# check fallback message
assert "Internal Server Error" in response.text
request, response = exception_app.test_client.get("/abort/message")
assert response.status == 500
assert "Custom Message" in response.text
with warnings.catch_warnings(record=True) as w:
request, response = exception_app.test_client.get("/old_abort")
assert response.status == 500
assert len(w) == 1 and "deprecated" in w[0].message.args[0]
assert "Abort" in response.text

View File

@@ -7,13 +7,6 @@ from sanic.exceptions import PayloadTooLarge
from sanic.http import Http
@pytest.fixture
def raised_ceiling():
Http.HEADER_CEILING = 32_768
yield
Http.HEADER_CEILING = 16_384
@pytest.mark.parametrize(
"input, expected",
[
@@ -83,75 +76,15 @@ async def test_header_size_exceeded():
recv_buffer += b"123"
protocol = Mock()
Http.set_header_max_size(1)
http = Http(protocol)
http._receive_more = _receive_more
http.request_max_size = 1
http.recv_buffer = recv_buffer
with pytest.raises(PayloadTooLarge):
await http.http1_request_header()
@pytest.mark.asyncio
async def test_header_size_increased_okay():
recv_buffer = bytearray()
async def _receive_more():
nonlocal recv_buffer
recv_buffer += b"123"
protocol = Mock()
Http.set_header_max_size(12_288)
http = Http(protocol)
http._receive_more = _receive_more
http.recv_buffer = recv_buffer
with pytest.raises(PayloadTooLarge):
await http.http1_request_header()
assert len(recv_buffer) == 12_291
@pytest.mark.asyncio
async def test_header_size_exceeded_maxed_out():
recv_buffer = bytearray()
async def _receive_more():
nonlocal recv_buffer
recv_buffer += b"123"
protocol = Mock()
Http.set_header_max_size(18_432)
http = Http(protocol)
http._receive_more = _receive_more
http.recv_buffer = recv_buffer
with pytest.raises(PayloadTooLarge):
await http.http1_request_header()
assert len(recv_buffer) == 16_389
@pytest.mark.asyncio
async def test_header_size_exceeded_raised_ceiling(raised_ceiling):
recv_buffer = bytearray()
async def _receive_more():
nonlocal recv_buffer
recv_buffer += b"123"
protocol = Mock()
http = Http(protocol)
Http.set_header_max_size(65_536)
http._receive_more = _receive_more
http.recv_buffer = recv_buffer
with pytest.raises(PayloadTooLarge):
await http.http1_request_header()
assert len(recv_buffer) == 32_772
def test_raw_headers(app):
app.route("/")(lambda _: text(""))
request, _ = app.test_client.get(

View File

@@ -1,5 +1,4 @@
import asyncio
import platform
from asyncio import sleep as aio_sleep
from json import JSONDecodeError
@@ -242,9 +241,7 @@ def test_keep_alive_timeout_reuse():
@pytest.mark.skipif(
bool(environ.get("SANIC_NO_UVLOOP"))
or OS_IS_WINDOWS
or platform.system() != "Linux",
bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS,
reason="Not testable with current client",
)
def test_keep_alive_client_timeout():

View File

@@ -113,9 +113,9 @@ def test_logging_pass_customer_logconfig():
def test_log_connection_lost(app, debug, monkeypatch):
""" Should not log Connection lost exception on non debug """
stream = StringIO()
error = logging.getLogger("sanic.error")
error.addHandler(logging.StreamHandler(stream))
monkeypatch.setattr(sanic.server, "error_logger", error)
root = logging.getLogger("sanic.root")
root.addHandler(logging.StreamHandler(stream))
monkeypatch.setattr(sanic.server, "logger", root)
@app.route("/conn_lost")
async def conn_lost(request):

View File

@@ -3,7 +3,7 @@ import logging
from asyncio import CancelledError
from itertools import count
from sanic.exceptions import NotFound
from sanic.exceptions import NotFound, SanicException
from sanic.request import Request
from sanic.response import HTTPResponse, text
@@ -156,7 +156,7 @@ def test_middleware_response_raise_cancelled_error(app, caplog):
assert response.status == 503
assert (
"sanic.error",
"sanic.root",
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) not in caplog.record_tuples
@@ -174,7 +174,7 @@ def test_middleware_response_raise_exception(app, caplog):
assert response.status == 404
# 404 errors are not logged
assert (
"sanic.error",
"sanic.root",
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) not in caplog.record_tuples

View File

@@ -234,7 +234,7 @@ def test_named_dynamic_route():
app.router.routes_all[
(
"folder",
"<name:str>",
"<name>",
)
].name
== "app.route_dynamic"
@@ -369,8 +369,7 @@ def test_dynamic_add_named_route():
app.add_route(handler, "/folder/<name>", name="route_dynamic")
assert (
app.router.routes_all[("folder", "<name:str>")].name
== "app.route_dynamic"
app.router.routes_all[("folder", "<name>")].name == "app.route_dynamic"
)
assert app.url_for("route_dynamic", name="test") == "/folder/test"
with pytest.raises(URLBuildError):

View File

@@ -1,4 +1,4 @@
from urllib.parse import quote
from urllib.parse import quote, unquote
import pytest

View File

@@ -23,8 +23,6 @@ try:
except ImportError:
flags = 0
TIMER_DELAY = 2
def terminate(proc):
if flags:
@@ -58,40 +56,6 @@ def write_app(filename, **runargs):
return text
def write_json_config_app(filename, jsonfile, **runargs):
with open(filename, "w") as f:
f.write(
dedent(
f"""\
import os
from sanic import Sanic
import json
app = Sanic(__name__)
with open("{jsonfile}", "r") as f:
config = json.load(f)
app.config.update_config(config)
app.route("/")(lambda x: x)
@app.listener("after_server_start")
def complete(*args):
print("complete", os.getpid(), app.config.FOO)
if __name__ == "__main__":
app.run(**{runargs!r})
"""
)
)
def write_file(filename):
text = secrets.token_urlsafe()
with open(filename, "w") as f:
f.write(f"""{{"FOO": "{text}"}}""")
return text
def scanner(proc):
for line in proc.stdout:
line = line.decode().strip()
@@ -126,10 +90,9 @@ async def test_reloader_live(runargs, mode):
with TemporaryDirectory() as tmpdir:
filename = os.path.join(tmpdir, "reloader.py")
text = write_app(filename, **runargs)
command = argv[mode]
proc = Popen(command, cwd=tmpdir, stdout=PIPE, creationflags=flags)
proc = Popen(argv[mode], cwd=tmpdir, stdout=PIPE, creationflags=flags)
try:
timeout = Timer(TIMER_DELAY, terminate, [proc])
timeout = Timer(5, terminate, [proc])
timeout.start()
# Python apparently keeps using the old source sometimes if
# we don't sleep before rewrite (pycache timestamp problem?)
@@ -144,40 +107,3 @@ async def test_reloader_live(runargs, mode):
terminate(proc)
with suppress(TimeoutExpired):
proc.wait(timeout=3)
@pytest.mark.parametrize(
"runargs, mode",
[
(dict(port=42102, auto_reload=True), "script"),
(dict(port=42103, debug=True), "module"),
({}, "sanic"),
],
)
async def test_reloader_live_with_dir(runargs, mode):
with TemporaryDirectory() as tmpdir:
filename = os.path.join(tmpdir, "reloader.py")
config_file = os.path.join(tmpdir, "config.json")
runargs["reload_dir"] = tmpdir
write_json_config_app(filename, config_file, **runargs)
text = write_file(config_file)
command = argv[mode]
if mode == "sanic":
command += ["--reload-dir", tmpdir]
proc = Popen(command, cwd=tmpdir, stdout=PIPE, creationflags=flags)
try:
timeout = Timer(TIMER_DELAY, terminate, [proc])
timeout.start()
# Python apparently keeps using the old source sometimes if
# we don't sleep before rewrite (pycache timestamp problem?)
sleep(1)
line = scanner(proc)
assert text in next(line)
# Edit source code and try again
text = write_file(config_file)
assert text in next(line)
finally:
timeout.cancel()
terminate(proc)
with suppress(TimeoutExpired):
proc.wait(timeout=3)

View File

@@ -20,7 +20,6 @@ def test_request_id_generates_from_request(monkeypatch):
monkeypatch.setattr(Request, "generate_id", Mock())
Request.generate_id.return_value = 1
request = Request(b"/", {}, None, "GET", None, Mock())
request.app.config.REQUEST_ID_HEADER = "foo"
for _ in range(10):
request.id
@@ -29,7 +28,6 @@ def test_request_id_generates_from_request(monkeypatch):
def test_request_id_defaults_uuid():
request = Request(b"/", {}, None, "GET", None, Mock())
request.app.config.REQUEST_ID_HEADER = "foo"
assert isinstance(request.id, UUID)
@@ -122,21 +120,3 @@ def test_protocol_attribute(app):
_ = app.test_client.get("/", headers=headers)
assert isinstance(retrieved, HttpProtocol)
def test_ipv6_address_is_not_wrapped(app):
@app.get("/")
async def get(request):
return response.json(
{
"client_ip": request.conn_info.client_ip,
"client": request.conn_info.client,
}
)
request, resp = app.test_client.get("/", host="::1")
assert request.route is list(app.router.routes)[0]
assert resp.json["client"] == "[::1]"
assert resp.json["client_ip"] == "::1"
assert request.ip == "::1"

View File

@@ -8,6 +8,7 @@ import pytest
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json, text
from sanic.server import HttpProtocol
from sanic.views import CompositionView, HTTPMethodView
from sanic.views import stream as stream_decorator

View File

@@ -1,8 +1,21 @@
import asyncio
from typing import cast
import httpcore
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_testing.testing import SanicTestClient
from sanic import Sanic

View File

@@ -317,15 +317,15 @@ def test_query_string(app):
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")
# def test_popped_stays_popped(app):
# @app.route("/")
# async def handler(request):
# return text("OK")
request, response = app.test_client.get("/", params=[("test1", "1")])
# request, response = app.test_client.get("/", params=[("test1", "1")])
assert request.args.pop("test1") == ["1"]
assert "test1" not in request.args
# assert request.args.pop("test1") == ["1"]
# assert "test1" not in request.args
@pytest.mark.asyncio
@@ -2246,7 +2246,9 @@ def test_conflicting_body_methods_overload(app):
def test_handler_overload(app):
@app.get("/long/sub/route/param_a/<param_a:str>/param_b/<param_b:str>")
@app.get(
"/long/sub/route/param_a/<param_a:string>/param_b/<param_b:string>"
)
@app.post("/long/sub/route/")
def handler(request, **kwargs):
return json(kwargs)

View File

@@ -1,19 +1,23 @@
import asyncio
import inspect
import os
import warnings
from collections import namedtuple
from mimetypes import guess_type
from random import choice
from unittest.mock import MagicMock
from urllib.parse import unquote
import pytest
from aiofiles import os as async_os
from sanic_testing.testing import HOST, PORT
from sanic import Sanic
from sanic.response import (
HTTPResponse,
StreamingHTTPResponse,
empty,
file,
file_stream,
@@ -22,6 +26,7 @@ from sanic.response import (
stream,
text,
)
from sanic.server import HttpProtocol
JSON_DATA = {"ok": True}
@@ -224,6 +229,7 @@ def non_chunked_streaming_app(app):
sample_streaming_fn,
headers={"Content-Length": "7"},
content_type="text/csv",
chunked=False,
)
return app
@@ -250,7 +256,11 @@ async def test_chunked_streaming_returns_correct_content_asgi(streaming_app):
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
request, response = non_chunked_streaming_app.test_client.get("/")
with pytest.warns(UserWarning) as record:
request, response = non_chunked_streaming_app.test_client.get("/")
assert len(record) == 1
assert "removed in v21.6" in record[0].message.args[0]
assert "Transfer-Encoding" not in response.headers
assert response.headers["Content-Type"] == "text/csv"
@@ -524,19 +534,3 @@ def test_empty_response(app):
request, response = app.test_client.get("/test")
assert response.content_type is None
assert response.body == b""
def test_direct_response_stream(app):
@app.route("/")
async def test(request):
response = await request.respond(content_type="text/csv")
await response.send("foo,")
await response.send("bar")
await response.eof()
return response
_, response = app.test_client.get("/")
assert response.text == "foo,bar"
assert response.headers["Transfer-Encoding"] == "chunked"
assert response.headers["Content-Type"] == "text/csv"
assert "Content-Length" not in response.headers

View File

@@ -258,7 +258,7 @@ def test_route_strict_slash(app):
def test_route_invalid_parameter_syntax(app):
with pytest.raises(ValueError):
@app.get("/get/<:str>", strict_slashes=True)
@app.get("/get/<:string>", strict_slashes=True)
def handler(request):
return text("OK")
@@ -478,7 +478,7 @@ def test_dynamic_route(app):
def test_dynamic_route_string(app):
results = []
@app.route("/folder/<name:str>")
@app.route("/folder/<name:string>")
async def handler(request, name):
results.append(name)
return text("OK")
@@ -513,7 +513,7 @@ def test_dynamic_route_int(app):
def test_dynamic_route_number(app):
results = []
@app.route("/weight/<weight:float>")
@app.route("/weight/<weight:number>")
async def handler(request, weight):
results.append(weight)
return text("OK")
@@ -585,6 +585,7 @@ def test_dynamic_route_path(app):
return text("OK")
app.router.finalize()
print(app.router.find_route_src)
request, response = app.test_client.get("/path/1/info")
assert response.status == 200
@@ -823,7 +824,7 @@ def test_dynamic_add_route_string(app):
results.append(name)
return text("OK")
app.add_route(handler, "/folder/<name:str>")
app.add_route(handler, "/folder/<name:string>")
request, response = app.test_client.get("/folder/test123")
assert response.text == "OK"
@@ -859,7 +860,7 @@ def test_dynamic_add_route_number(app):
results.append(weight)
return text("OK")
app.add_route(handler, "/weight/<weight:float>")
app.add_route(handler, "/weight/<weight:number>")
request, response = app.test_client.get("/weight/12345")
assert response.text == "OK"
@@ -1066,8 +1067,7 @@ def test_uri_with_different_method_and_different_params(app):
return json({"action": action})
request, response = app.test_client.get("/ads/1234")
assert response.status == 200
assert response.json == {"ad_id": "1234"}
assert response.status == 405
request, response = app.test_client.post("/ads/post")
assert response.status == 200

View File

@@ -257,60 +257,17 @@ def test_bad_finalize(app):
assert counter == 0
@pytest.mark.asyncio
async def test_event_not_exist(app):
def test_event_not_exist(app):
with pytest.raises(NotFound, match="Could not find signal does.not.exist"):
await app.event("does.not.exist")
app.event("does.not.exist")
@pytest.mark.asyncio
async def test_event_not_exist_on_bp(app):
def test_event_not_exist_on_bp(app):
bp = Blueprint("bp")
app.blueprint(bp)
with pytest.raises(NotFound, match="Could not find signal does.not.exist"):
await bp.event("does.not.exist")
@pytest.mark.asyncio
async def test_event_not_exist_with_autoregister(app):
app.config.EVENT_AUTOREGISTER = True
try:
await app.event("does.not.exist", timeout=0.1)
except asyncio.TimeoutError:
...
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_non_exist_event_with_autoregister(app):
@app.signal("some.stand.in")
async def signal_handler():
...
app.config.EVENT_AUTOREGISTER = True
app_counter = 0
app.signal_router.finalize()
async def do_wait():
nonlocal app_counter
await app.event("foo.bar.baz")
app_counter += 1
fut = asyncio.ensure_future(do_wait())
await app.dispatch("foo.bar.baz")
await fut
assert app_counter == 1
@pytest.mark.asyncio
async def test_dispatch_not_exist(app):
@app.signal("do.something.start")
async def signal_handler():
...
app.signal_router.finalize()
await app.dispatch("does.not.exist")
bp.event("does.not.exist")
def test_event_on_bp_not_registered():

View File

@@ -474,22 +474,22 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog):
assert counter[logging.ERROR] == 1
def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
app.static("/static", static_file_directory)
# def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
# app.static("/static", static_file_directory)
@app.exception(FileNotFound)
async def file_not_found(request, exception):
return text(f"No file: {request.path}", status=404)
# @app.exception(FileNotFound)
# async def file_not_found(request, exception):
# return text(f"No file: {request.path}", status=404)
with caplog.at_level(logging.INFO):
_, response = app.test_client.get("/static/non_existing_file.file")
# with caplog.at_level(logging.INFO):
# _, response = app.test_client.get("/static/non_existing_file.file")
counter = Counter([r[1] for r in caplog.record_tuples])
# counter = Counter([r[1] for r in caplog.record_tuples])
assert response.status == 404
assert counter[logging.INFO] == 5
assert logging.ERROR not in counter
assert response.text == "No file: /static/non_existing_file.file"
# assert response.status == 404
# assert counter[logging.INFO] == 5
# assert logging.ERROR not in counter
# assert response.text == "No file: /static/non_existing_file.file"
def test_multiple_statics(app, static_file_directory):

View File

@@ -1,5 +1,6 @@
import asyncio
from time import monotonic as current_time
from unittest.mock import Mock
import pytest

View File

@@ -1,7 +1,6 @@
import asyncio
import logging
import os
import platform
import subprocess
import sys
@@ -176,10 +175,6 @@ def test_unix_connection_multiple_workers():
app_multi.run(host="myhost.invalid", unix=SOCKPATH, workers=2)
@pytest.mark.xfail(
condition=platform.system() != "Linux",
reason="Flaky Test on Non Linux Infra",
)
async def test_zero_downtime():
"""Graceful server termination and socket replacement on restarts"""
from signal import SIGINT

View File

@@ -143,7 +143,7 @@ def test_fails_url_build_if_params_not_passed(app):
COMPLEX_PARAM_URL = (
"/<foo:int>/<four_letter_string:[A-z]{4}>/"
"<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:float>"
"<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>"
)
PASSING_KWARGS = {
"foo": 4,
@@ -168,7 +168,7 @@ def test_fails_with_int_message(app):
expected_error = (
r'Value "not_int" for parameter `foo` '
r"does not match pattern for type `int`: ^-?\d+$"
r"does not match pattern for type `int`: ^-?\d+"
)
assert str(e.value) == expected_error
@@ -223,7 +223,7 @@ def test_fails_with_number_message(app):
@pytest.mark.parametrize("number", [3, -3, 13.123, -13.123])
def test_passes_with_negative_number_message(app, number):
@app.route("path/<possibly_neg:float>/another-word")
@app.route("path/<possibly_neg:number>/another-word")
def good(request, possibly_neg):
assert isinstance(possibly_neg, (int, float))
return text(f"this should pass with `{possibly_neg}`")

View File

@@ -1,141 +0,0 @@
import pytest
from sanic import Blueprint, text
@pytest.fixture
def handler():
def handler(_):
return text("Done.")
return handler
def test_route(app, handler):
app.route("/", version=1)(handler)
_, response = app.test_client.get("/v1")
assert response.status == 200
def test_bp(app, handler):
bp = Blueprint(__file__, version=1)
bp.route("/")(handler)
app.blueprint(bp)
_, response = app.test_client.get("/v1")
assert response.status == 200
def test_bp_use_route(app, handler):
bp = Blueprint(__file__, version=1)
bp.route("/", version=1.1)(handler)
app.blueprint(bp)
_, response = app.test_client.get("/v1.1")
assert response.status == 200
def test_bp_group(app, handler):
bp = Blueprint(__file__)
bp.route("/")(handler)
group = Blueprint.group(bp, version=1)
app.blueprint(group)
_, response = app.test_client.get("/v1")
assert response.status == 200
def test_bp_group_use_bp(app, handler):
bp = Blueprint(__file__, version=1.1)
bp.route("/")(handler)
group = Blueprint.group(bp, version=1)
app.blueprint(group)
_, response = app.test_client.get("/v1.1")
assert response.status == 200
def test_bp_group_use_registration(app, handler):
bp = Blueprint(__file__, version=1.1)
bp.route("/")(handler)
group = Blueprint.group(bp, version=1)
app.blueprint(group, version=1.2)
_, response = app.test_client.get("/v1.2")
assert response.status == 200
def test_bp_group_use_route(app, handler):
bp = Blueprint(__file__, version=1.1)
bp.route("/", version=1.3)(handler)
group = Blueprint.group(bp, version=1)
app.blueprint(group, version=1.2)
_, response = app.test_client.get("/v1.3")
assert response.status == 200
def test_version_prefix_route(app, handler):
app.route("/", version=1, version_prefix="/api/v")(handler)
_, response = app.test_client.get("/api/v1")
assert response.status == 200
def test_version_prefix_bp(app, handler):
bp = Blueprint(__file__, version=1, version_prefix="/api/v")
bp.route("/")(handler)
app.blueprint(bp)
_, response = app.test_client.get("/api/v1")
assert response.status == 200
def test_version_prefix_bp_use_route(app, handler):
bp = Blueprint(__file__, version=1, version_prefix="/ignore/v")
bp.route("/", version=1.1, version_prefix="/api/v")(handler)
app.blueprint(bp)
_, response = app.test_client.get("/api/v1.1")
assert response.status == 200
def test_version_prefix_bp_group(app, handler):
bp = Blueprint(__file__)
bp.route("/")(handler)
group = Blueprint.group(bp, version=1, version_prefix="/api/v")
app.blueprint(group)
_, response = app.test_client.get("/api/v1")
assert response.status == 200
def test_version_prefix_bp_group_use_bp(app, handler):
bp = Blueprint(__file__, version=1.1, version_prefix="/api/v")
bp.route("/")(handler)
group = Blueprint.group(bp, version=1, version_prefix="/ignore/v")
app.blueprint(group)
_, response = app.test_client.get("/api/v1.1")
assert response.status == 200
def test_version_prefix_bp_group_use_registration(app, handler):
bp = Blueprint(__file__, version=1.1, version_prefix="/alsoignore/v")
bp.route("/")(handler)
group = Blueprint.group(bp, version=1, version_prefix="/ignore/v")
app.blueprint(group, version=1.2, version_prefix="/api/v")
_, response = app.test_client.get("/api/v1.2")
assert response.status == 200
def test_version_prefix_bp_group_use_route(app, handler):
bp = Blueprint(__file__, version=1.1, version_prefix="/alsoignore/v")
bp.route("/", version=1.3, version_prefix="/api/v")(handler)
group = Blueprint.group(bp, version=1, version_prefix="/ignore/v")
app.blueprint(group, version=1.2, version_prefix="/stillignoring/v")
_, response = app.test_client.get("/api/v1.3")
assert response.status == 200

View File

@@ -77,56 +77,6 @@ def test_with_bp(app):
assert response.text == "I am get method"
def test_with_attach(app):
class DummyView(HTTPMethodView):
def get(self, request):
return text("I am get method")
DummyView.attach(app, "/")
request, response = app.test_client.get("/")
assert response.text == "I am get method"
def test_with_sub_init(app):
class DummyView(HTTPMethodView, attach=app, uri="/"):
def get(self, request):
return text("I am get method")
request, response = app.test_client.get("/")
assert response.text == "I am get method"
def test_with_attach_and_bp(app):
bp = Blueprint("test_text")
class DummyView(HTTPMethodView):
def get(self, request):
return text("I am get method")
DummyView.attach(bp, "/")
app.blueprint(bp)
request, response = app.test_client.get("/")
assert response.text == "I am get method"
def test_with_sub_init_and_bp(app):
bp = Blueprint("test_text")
class DummyView(HTTPMethodView, attach=bp, uri="/"):
def get(self, request):
return text("I am get method")
app.blueprint(bp)
request, response = app.test_client.get("/")
assert response.text == "I am get method"
def test_with_bp_with_url_prefix(app):
bp = Blueprint("test_text", url_prefix="/test1")
@@ -268,15 +218,15 @@ def test_composition_view_runs_methods_as_expected(app, method):
assert response.status == 200
assert response.text == "first method"
response = view(request)
assert response.body.decode() == "first method"
# response = view(request)
# assert response.body.decode() == "first method"
if method in ["DELETE", "PATCH"]:
request, response = getattr(app.test_client, method.lower())("/")
assert response.text == "second method"
# if method in ["DELETE", "PATCH"]:
# request, response = getattr(app.test_client, method.lower())("/")
# assert response.text == "second method"
response = view(request)
assert response.body.decode() == "second method"
# response = view(request)
# assert response.body.decode() == "second method"
@pytest.mark.parametrize("method", HTTP_METHODS)
@@ -294,12 +244,3 @@ def test_composition_view_rejects_invalid_methods(app, method):
if method in ["DELETE", "PATCH"]:
request, response = getattr(app.test_client, method.lower())("/")
assert response.status == 405
def test_composition_view_deprecation():
message = (
"CompositionView has been deprecated and will be removed in v21.12. "
"Please update your view to HTTPMethodView."
)
with pytest.warns(DeprecationWarning, match=message):
CompositionView()

28
tox.ini
View File

@@ -1,5 +1,5 @@
[tox]
envlist = py37, py38, py39, pyNightly, pypy37, {py37,py38,py39,pyNightly,pypy37}-no-ext, lint, check, security, docs, type-checking
envlist = py37, py38, py39, pyNightly, {py37,py38,py39,pyNightly}-no-ext, lint, check, security, docs
[testenv]
usedevelop = True
@@ -7,7 +7,7 @@ setenv =
{py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UJSON=1
{py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
deps =
sanic-testing>=0.6.0
sanic-testing
coverage==5.3
pytest==5.2.1
pytest-cov
@@ -18,7 +18,7 @@ deps =
beautifulsoup4
gunicorn==20.0.4
uvicorn
websockets>=9.0
websockets>=8.1,<9.0
commands =
pytest {posargs:tests --cov sanic}
- coverage combine --append
@@ -39,8 +39,7 @@ commands =
[testenv:type-checking]
deps =
mypy>=0.901
types-ujson
mypy
commands =
mypy sanic
@@ -76,23 +75,6 @@ deps =
docutils
pygments
gunicorn==20.0.4
commands =
make docs-test
[testenv:coverage]
usedevelop = True
deps =
sanic-testing>=0.6.0
coverage==5.3
pytest==5.2.1
pytest-cov
pytest-sanic
pytest-sugar
pytest-benchmark
chardet==3.*
beautifulsoup4
gunicorn==20.0.4
uvicorn
websockets>=9.0
commands =
pytest tests --cov=./sanic --cov-report=xml