Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5308fec354 | ||
|
|
5a48b94089 | ||
|
|
ba1c73d947 | ||
|
|
a6e78b70ab | ||
|
|
bb1174afc5 | ||
|
|
df8abe9cfd | ||
|
|
c3bca97ee1 | ||
|
|
8df80e276b | ||
|
|
30572c972d | ||
|
|
53da4dd091 | ||
|
|
108a4a99c7 | ||
|
|
7c180376d6 | ||
|
|
f39b8b32f7 | ||
|
|
c543d19f8a | ||
|
|
80fca9aef7 | ||
|
|
5bb9aa0c2c | ||
|
|
83c746ee57 | ||
|
|
aff6604636 | ||
|
|
2c80571a8a | ||
|
|
d964b552af | ||
|
|
48f8b37b74 | ||
|
|
141be0028d | ||
|
|
a140c47195 | ||
|
|
0c3a8392f2 | ||
|
|
16875b1f41 | ||
|
|
b1f31f2eeb | ||
|
|
d16b9e5a02 | ||
|
|
680484bdc8 | ||
|
|
05cd44b5dd | ||
|
|
ba374139f4 | ||
|
|
72a745bfd5 | ||
|
|
3a6fac7d59 | ||
|
|
28ba8e53df | ||
|
|
9b26358e63 | ||
|
|
e21521f45c | ||
|
|
30479765cb | ||
|
|
53a571ec6c | ||
|
|
ad97cac313 | ||
|
|
1a352ddf55 | ||
|
|
5ba43decf2 | ||
|
|
8f06d035cb | ||
|
|
b716f48c84 | ||
|
|
42b1e7143e | ||
|
|
eba7821a6d |
12
.codeclimate.yml
Normal file
12
.codeclimate.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
exclude_patterns:
|
||||
- "sanic/__main__.py"
|
||||
- "sanic/reloader_helpers.py"
|
||||
- "sanic/simple.py"
|
||||
- "sanic/utils.py"
|
||||
- ".github/"
|
||||
- "changelogs/"
|
||||
- "docker/"
|
||||
- "docs/"
|
||||
- "examples/"
|
||||
- "hack/"
|
||||
- "scripts/"
|
||||
@@ -1,7 +1,12 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = sanic
|
||||
omit = site-packages, sanic/utils.py, sanic/__main__.py
|
||||
omit =
|
||||
site-packages
|
||||
sanic/__main__.py
|
||||
sanic/reloader_helpers.py
|
||||
sanic/simple.py
|
||||
sanic/utils.py
|
||||
|
||||
[html]
|
||||
directory = coverage
|
||||
|
||||
37
.github/workflows/codeql-analysis.yml
vendored
37
.github/workflows/codeql-analysis.yml
vendored
@@ -1,22 +1,10 @@
|
||||
# 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: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '25 16 * * 0'
|
||||
|
||||
@@ -29,39 +17,18 @@ 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
|
||||
|
||||
40
.github/workflows/coverage.yml
vendored
Normal file
40
.github/workflows/coverage.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
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
|
||||
39
.github/workflows/on-demand.yml
vendored
Normal file
39
.github/workflows/on-demand.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
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"
|
||||
32
.github/workflows/pr-bandit.yml
vendored
Normal file
32
.github/workflows/pr-bandit.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
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 }}"
|
||||
29
.github/workflows/pr-docs.yml
vendored
Normal file
29
.github/workflows/pr-docs.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
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 }}"
|
||||
30
.github/workflows/pr-linter.yml
vendored
Normal file
30
.github/workflows/pr-linter.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
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 }}"
|
||||
41
.github/workflows/pr-python-pypy.yml
vendored
Normal file
41
.github/workflows/pr-python-pypy.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
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"
|
||||
38
.github/workflows/pr-python37.yml
vendored
Normal file
38
.github/workflows/pr-python37.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
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"
|
||||
38
.github/workflows/pr-python38.yml
vendored
Normal file
38
.github/workflows/pr-python38.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
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"
|
||||
50
.github/workflows/pr-python39.yml
vendored
Normal file
50
.github/workflows/pr-python39.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
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"
|
||||
32
.github/workflows/pr-type-check.yml
vendored
Normal file
32
.github/workflows/pr-type-check.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
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 }}"
|
||||
34
.github/workflows/pr-windows.yml
vendored
Normal file
34
.github/workflows/pr-windows.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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"
|
||||
48
.github/workflows/publish-images.yml
vendored
Normal file
48
.github/workflows/publish-images.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
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'
|
||||
28
.github/workflows/publish-package.yml
vendored
Normal file
28
.github/workflows/publish-package.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
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
4
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage
|
||||
coverage.xml
|
||||
.tox
|
||||
settings.py
|
||||
.idea/*
|
||||
@@ -18,3 +19,6 @@ build/*
|
||||
.DS_Store
|
||||
dist/*
|
||||
pip-wheel-metadata/
|
||||
.pytest_cache/*
|
||||
.venv/*
|
||||
.vscode/*
|
||||
|
||||
94
.travis.yml
94
.travis.yml
@@ -1,94 +0,0 @@
|
||||
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"
|
||||
@@ -1,3 +1,95 @@
|
||||
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
|
||||
--------------
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ Permform ``flake8``\ , ``black`` and ``isort`` checks.
|
||||
tox -e lint
|
||||
|
||||
Run type annotation checks
|
||||
---------------
|
||||
--------------------------
|
||||
|
||||
``tox`` environment -> ``[testenv:type-checking]``
|
||||
|
||||
|
||||
6
Makefile
6
Makefile
@@ -49,6 +49,9 @@ 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
|
||||
|
||||
@@ -85,8 +88,7 @@ docs-test: docs-clean
|
||||
cd docs && make dummy
|
||||
|
||||
docs-serve:
|
||||
# python -m http.server --directory=./docs/_build/html 9999
|
||||
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic
|
||||
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./
|
||||
|
||||
changelog:
|
||||
python scripts/changelog.py
|
||||
|
||||
12
README.rst
12
README.rst
@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
|
||||
:stub-columns: 1
|
||||
|
||||
* - Build
|
||||
- | |Build Status| |AppVeyor Build Status| |Codecov|
|
||||
- | |Py39Test| |Py38Test| |Py37Test| |Codecov|
|
||||
* - Docs
|
||||
- | |UserGuide| |Documentation|
|
||||
* - Package
|
||||
@@ -29,10 +29,12 @@ 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
|
||||
.. |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
|
||||
.. |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
|
||||
.. |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
14
codecov.yml
@@ -1,14 +0,0 @@
|
||||
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%
|
||||
@@ -1,28 +1,9 @@
|
||||
FROM alpine:3.7
|
||||
ARG 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
|
||||
FROM sanicframework/sanic-build:${BASE_IMAGE_TAG}
|
||||
|
||||
RUN apk update
|
||||
RUN update-ca-certificates
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
|
||||
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"]
|
||||
RUN pip install sanic
|
||||
RUN apk del build-base
|
||||
|
||||
9
docker/Dockerfile-base
Normal file
9
docker/Dockerfile-base
Normal file
@@ -0,0 +1,9 @@
|
||||
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/*
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
eval "$(pyenv init -)"
|
||||
eval "$(pyenv virtualenv-init -)"
|
||||
source /root/.pyenv/completions/pyenv.bash
|
||||
|
||||
pip install tox
|
||||
|
||||
exec $@
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/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
|
||||
17
docs/sanic/api/app.rst
Normal file
17
docs/sanic/api/app.rst
Normal file
@@ -0,0 +1,17 @@
|
||||
Application
|
||||
===========
|
||||
|
||||
sanic.app
|
||||
---------
|
||||
|
||||
.. automodule:: sanic.app
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
sanic.config
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.config
|
||||
:members:
|
||||
:show-inheritance:
|
||||
17
docs/sanic/api/blueprints.rst
Normal file
17
docs/sanic/api/blueprints.rst
Normal file
@@ -0,0 +1,17 @@
|
||||
Blueprints
|
||||
==========
|
||||
|
||||
sanic.blueprints
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.blueprints
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
sanic.blueprint_group
|
||||
---------------------
|
||||
|
||||
.. automodule:: sanic.blueprint_group
|
||||
:members:
|
||||
:special-members:
|
||||
47
docs/sanic/api/core.rst
Normal file
47
docs/sanic/api/core.rst
Normal file
@@ -0,0 +1,47 @@
|
||||
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:
|
||||
16
docs/sanic/api/exceptions.rst
Normal file
16
docs/sanic/api/exceptions.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
sanic.errorpages
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.errorpages
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.exceptions
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.exceptions
|
||||
:members:
|
||||
:show-inheritance:
|
||||
18
docs/sanic/api/router.rst
Normal file
18
docs/sanic/api/router.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
Routing
|
||||
=======
|
||||
|
||||
sanic_routing models
|
||||
--------------------
|
||||
|
||||
.. autoclass:: sanic_routing.route::Route
|
||||
:members:
|
||||
|
||||
.. autoclass:: sanic_routing.group::RouteGroup
|
||||
:members:
|
||||
|
||||
sanic.router
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.router
|
||||
:members:
|
||||
:show-inheritance:
|
||||
25
docs/sanic/api/server.rst
Normal file
25
docs/sanic/api/server.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
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:
|
||||
16
docs/sanic/api/utility.rst
Normal file
16
docs/sanic/api/utility.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
Utility
|
||||
=======
|
||||
|
||||
sanic.compat
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.compat
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.log
|
||||
---------
|
||||
|
||||
.. automodule:: sanic.log
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -1,132 +1,13 @@
|
||||
📑 API Reference
|
||||
================
|
||||
|
||||
sanic.app
|
||||
---------
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
.. 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:
|
||||
api/app
|
||||
api/blueprints
|
||||
api/core
|
||||
api/exceptions
|
||||
api/router
|
||||
api/server
|
||||
api/utility
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
♥️ Contributing
|
||||
===============
|
||||
==============
|
||||
|
||||
.. include:: ../../CONTRIBUTING.rst
|
||||
|
||||
6
hack/Dockerfile
Normal file
6
hack/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM catthehacker/ubuntu:act-latest
|
||||
SHELL [ "/bin/bash", "-c" ]
|
||||
ENTRYPOINT []
|
||||
RUN apt-get update
|
||||
RUN apt-get install gcc -y
|
||||
RUN apt-get install -y --no-install-recommends g++
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -9,6 +10,7 @@ __all__ = (
|
||||
"__version__",
|
||||
"Sanic",
|
||||
"Blueprint",
|
||||
"HTTPMethod",
|
||||
"HTTPResponse",
|
||||
"Request",
|
||||
"html",
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sanic_routing import __version__ as __routing_version__ # type: ignore
|
||||
|
||||
from sanic import __version__
|
||||
from sanic.app import Sanic
|
||||
from sanic.config import BASE_LOGO
|
||||
from sanic.log import logger
|
||||
from sanic.log import error_logger
|
||||
from sanic.simple import create_simple_server
|
||||
|
||||
|
||||
class SanicArgumentParser(ArgumentParser):
|
||||
def add_bool_arguments(self, *args, **kwargs):
|
||||
group = self.add_mutually_exclusive_group()
|
||||
group.add_argument(*args, action="store_true", **kwargs)
|
||||
kwargs["help"] = "no " + kwargs["help"]
|
||||
kwargs["help"] = f"no {kwargs['help']}\n "
|
||||
group.add_argument(
|
||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||
)
|
||||
@@ -25,7 +29,30 @@ def main():
|
||||
parser = SanicArgumentParser(
|
||||
prog="sanic",
|
||||
description=BASE_LOGO,
|
||||
formatter_class=RawDescriptionHelpFormatter,
|
||||
formatter_class=lambda prog: RawTextHelpFormatter(
|
||||
prog, max_help_position=33
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"Sanic {__version__}; Routing {__routing_version__}",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--factory",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Treat app as an application factory, "
|
||||
"i.e. a () -> <Sanic app> callable"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--simple",
|
||||
dest="simple",
|
||||
action="store_true",
|
||||
help="Run Sanic as a Simple Server (module arg should be a path)\n ",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-H",
|
||||
@@ -33,7 +60,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",
|
||||
@@ -41,7 +68,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",
|
||||
@@ -49,13 +76,16 @@ def main():
|
||||
dest="unix",
|
||||
type=str,
|
||||
default="",
|
||||
help="location of unix socket",
|
||||
help="location of unix socket\n ",
|
||||
)
|
||||
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."
|
||||
"--key", dest="key", type=str, help="location of keyfile for SSL\n "
|
||||
)
|
||||
parser.add_bool_arguments(
|
||||
"--access-logs", dest="access_log", help="display access logs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
@@ -63,20 +93,31 @@ def main():
|
||||
dest="workers",
|
||||
type=int,
|
||||
default=1,
|
||||
help="number of worker processes [default 1]",
|
||||
help="number of worker processes [default 1]\n ",
|
||||
)
|
||||
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("-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(
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"Sanic {__version__}",
|
||||
"-R",
|
||||
"--reload-dir",
|
||||
dest="path",
|
||||
action="append",
|
||||
help="Extra directories to watch and reload on changes\n ",
|
||||
)
|
||||
parser.add_argument(
|
||||
"module", help="path to your Sanic app. Example: path.to.server:app"
|
||||
"module",
|
||||
help=(
|
||||
"Path to your Sanic app. Example: path.to.server:app\n"
|
||||
"If running a Simple Server, path to directory to serve. "
|
||||
"Example: ./\n"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -85,47 +126,71 @@ def main():
|
||||
if module_path not in sys.path:
|
||||
sys.path.append(module_path)
|
||||
|
||||
if ":" in args.module:
|
||||
module_name, app_name = args.module.rsplit(":", 1)
|
||||
if args.simple:
|
||||
path = Path(args.module)
|
||||
app = create_simple_server(path)
|
||||
else:
|
||||
module_parts = args.module.split(".")
|
||||
module_name = ".".join(module_parts[:-1])
|
||||
app_name = module_parts[-1]
|
||||
delimiter = ":" if ":" in args.module else "."
|
||||
module_name, app_name = args.module.rsplit(delimiter, 1)
|
||||
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
app_name = type(app).__name__
|
||||
if app_name.endswith("()"):
|
||||
args.factory = True
|
||||
app_name = app_name[:-2]
|
||||
|
||||
if not isinstance(app, Sanic):
|
||||
raise ValueError(
|
||||
f"Module is not a Sanic app, it is a {app_name}. "
|
||||
f"Perhaps you meant {args.module}.app?"
|
||||
)
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
if args.factory:
|
||||
app = app()
|
||||
|
||||
app_type_name = type(app).__name__
|
||||
|
||||
if not isinstance(app, Sanic):
|
||||
raise ValueError(
|
||||
f"Module is not a Sanic app, it is a {app_type_name}. "
|
||||
f"Perhaps you meant {args.module}.app?"
|
||||
)
|
||||
if args.cert is not None or args.key is not None:
|
||||
ssl = {
|
||||
ssl: Optional[Dict[str, Any]] = {
|
||||
"cert": args.cert,
|
||||
"key": args.key,
|
||||
} # type: Optional[Dict[str, Any]]
|
||||
}
|
||||
else:
|
||||
ssl = None
|
||||
|
||||
app.run(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
unix=args.unix,
|
||||
workers=args.workers,
|
||||
debug=args.debug,
|
||||
access_log=args.access_log,
|
||||
ssl=ssl,
|
||||
)
|
||||
kwargs = {
|
||||
"host": args.host,
|
||||
"port": args.port,
|
||||
"unix": args.unix,
|
||||
"workers": args.workers,
|
||||
"debug": args.debug,
|
||||
"access_log": args.access_log,
|
||||
"ssl": ssl,
|
||||
}
|
||||
if args.auto_reload:
|
||||
kwargs["auto_reload"] = True
|
||||
|
||||
if args.path:
|
||||
if args.auto_reload or args.debug:
|
||||
kwargs["reload_dir"] = args.path
|
||||
else:
|
||||
error_logger.warning(
|
||||
"Ignoring '--reload-dir' since auto reloading was not "
|
||||
"enabled. If you would like to watch directories for "
|
||||
"changes, consider using --debug or --auto-reload."
|
||||
)
|
||||
|
||||
app.run(**kwargs)
|
||||
except ImportError as e:
|
||||
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"
|
||||
)
|
||||
if module_name.startswith(e.name):
|
||||
error_logger.error(
|
||||
f"No module named {e.name} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
except ValueError:
|
||||
logger.exception("Failed to run app")
|
||||
error_logger.exception("Failed to run app")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "21.3.2"
|
||||
__version__ = "21.6.2"
|
||||
|
||||
126
sanic/app.py
126
sanic/app.py
@@ -14,6 +14,7 @@ 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
|
||||
@@ -43,7 +44,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, Config
|
||||
from sanic.config import BASE_LOGO, SANIC_PREFIX, Config
|
||||
from sanic.exceptions import (
|
||||
InvalidUsage,
|
||||
SanicException,
|
||||
@@ -78,6 +79,7 @@ class Sanic(BaseSanic):
|
||||
"""
|
||||
|
||||
__fake_slots__ = (
|
||||
"_asgi_app",
|
||||
"_app_registry",
|
||||
"_asgi_client",
|
||||
"_blueprint_order",
|
||||
@@ -89,6 +91,7 @@ class Sanic(BaseSanic):
|
||||
"_future_signals",
|
||||
"_test_client",
|
||||
"_test_manager",
|
||||
"auto_reload",
|
||||
"asgi",
|
||||
"blueprints",
|
||||
"config",
|
||||
@@ -103,6 +106,7 @@ class Sanic(BaseSanic):
|
||||
"name",
|
||||
"named_request_middleware",
|
||||
"named_response_middleware",
|
||||
"reload_dirs",
|
||||
"request_class",
|
||||
"request_middleware",
|
||||
"response_middleware",
|
||||
@@ -121,10 +125,13 @@ 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: bool = True,
|
||||
load_env: Union[bool, str] = True,
|
||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||
request_class: Optional[Type[Request]] = None,
|
||||
strict_slashes: bool = False,
|
||||
log_config: Optional[Dict[str, Any]] = None,
|
||||
@@ -132,34 +139,38 @@ class Sanic(BaseSanic):
|
||||
register: Optional[bool] = None,
|
||||
dumps: Optional[Callable[..., str]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
super().__init__(name=name)
|
||||
|
||||
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(load_env=load_env)
|
||||
self.config = config or Config(
|
||||
load_env=load_env, env_prefix=env_prefix
|
||||
)
|
||||
self.configure_logging = configure_logging
|
||||
self.ctx = SimpleNamespace()
|
||||
self.ctx = ctx or 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()
|
||||
@@ -175,7 +186,6 @@ class Sanic(BaseSanic):
|
||||
|
||||
if register is not None:
|
||||
self.config.REGISTER = register
|
||||
|
||||
if self.config.REGISTER:
|
||||
self.__class__.register_app(self)
|
||||
|
||||
@@ -374,11 +384,19 @@ class Sanic(BaseSanic):
|
||||
condition=condition,
|
||||
)
|
||||
|
||||
def event(self, event: str, timeout: Optional[Union[int, float]] = None):
|
||||
async def event(
|
||||
self, event: str, timeout: Optional[Union[int, float]] = None
|
||||
):
|
||||
signal = self.signal_router.name_index.get(event)
|
||||
if not signal:
|
||||
raise NotFound("Could not find signal %s" % event)
|
||||
return wait_for(signal.ctx.event.wait(), timeout=timeout)
|
||||
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)
|
||||
|
||||
def enable_websocket(self, enable=True):
|
||||
"""Enable or disable the support for websocket.
|
||||
@@ -402,7 +420,33 @@ class Sanic(BaseSanic):
|
||||
"""
|
||||
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
||||
for item in blueprint:
|
||||
self.blueprint(item, **options)
|
||||
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)
|
||||
return
|
||||
if blueprint.name in self.blueprints:
|
||||
assert self.blueprints[blueprint.name] is blueprint, (
|
||||
@@ -567,7 +611,12 @@ class Sanic(BaseSanic):
|
||||
# determine if the parameter supplied by the caller
|
||||
# passes the test in the URL
|
||||
if param_info.pattern:
|
||||
passes_pattern = param_info.pattern.match(supplied_param)
|
||||
pattern = (
|
||||
param_info.pattern[1]
|
||||
if isinstance(param_info.pattern, tuple)
|
||||
else param_info.pattern
|
||||
)
|
||||
passes_pattern = pattern.match(supplied_param)
|
||||
if not passes_pattern:
|
||||
if param_info.cast != str:
|
||||
msg = (
|
||||
@@ -575,13 +624,13 @@ class Sanic(BaseSanic):
|
||||
f"for parameter `{param_info.name}` does "
|
||||
"not match pattern for type "
|
||||
f"`{param_info.cast.__name__}`: "
|
||||
f"{param_info.pattern.pattern}"
|
||||
f"{pattern.pattern}"
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
f'Value "{supplied_param}" for parameter '
|
||||
f"`{param_info.name}` does not satisfy "
|
||||
f"pattern {param_info.pattern.pattern}"
|
||||
f"pattern {pattern.pattern}"
|
||||
)
|
||||
raise URLBuildError(msg)
|
||||
|
||||
@@ -664,11 +713,6 @@ 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
|
||||
@@ -677,7 +721,9 @@ class Sanic(BaseSanic):
|
||||
try:
|
||||
# Fetch handler from router
|
||||
route, handler, kwargs = self.router.get(
|
||||
request.path, request.method, request.headers.get("host")
|
||||
request.path,
|
||||
request.method,
|
||||
request.headers.getone("host", None),
|
||||
)
|
||||
|
||||
request._match_info = kwargs
|
||||
@@ -725,17 +771,14 @@ class Sanic(BaseSanic):
|
||||
|
||||
if response:
|
||||
response = await request.respond(response)
|
||||
else:
|
||||
elif not hasattr(handler, "is_websocket"):
|
||||
response = request.stream.response # type: ignore
|
||||
# Make sure that response is finished / run StreamingHTTP callback
|
||||
|
||||
# Make sure that response is finished / run StreamingHTTP callback
|
||||
if isinstance(response, BaseHTTPResponse):
|
||||
await response.send(end_stream=True)
|
||||
else:
|
||||
try:
|
||||
# Fastest method for checking if the property exists
|
||||
handler.is_websocket # type: ignore
|
||||
except AttributeError:
|
||||
if not hasattr(handler, "is_websocket"):
|
||||
raise ServerError(
|
||||
f"Invalid response type {response!r} "
|
||||
"(need HTTPResponse)"
|
||||
@@ -762,6 +805,7 @@ 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
|
||||
@@ -834,6 +878,7 @@ 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
|
||||
@@ -868,6 +913,18 @@ 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, "
|
||||
@@ -877,8 +934,9 @@ 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)
|
||||
return reloader_helpers.watchdog(1.0, self)
|
||||
|
||||
if sock is None:
|
||||
host, port = host or "127.0.0.1", port or 8000
|
||||
@@ -1177,6 +1235,10 @@ 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):
|
||||
|
||||
@@ -140,7 +140,6 @@ class ASGIApp:
|
||||
instance.ws = instance.transport.create_websocket_connection(
|
||||
send, receive
|
||||
)
|
||||
await instance.ws.accept()
|
||||
else:
|
||||
raise ServerError("Received unknown ASGI scope")
|
||||
|
||||
@@ -164,10 +163,12 @@ class ASGIApp:
|
||||
Read and stream the body in chunks from an incoming ASGI message.
|
||||
"""
|
||||
message = await self.transport.receive()
|
||||
body = message.get("body", b"")
|
||||
if not message.get("more_body", False):
|
||||
self.request_body = False
|
||||
return None
|
||||
return message.get("body", b"")
|
||||
if not body:
|
||||
return None
|
||||
return body
|
||||
|
||||
async def __aiter__(self):
|
||||
while self.request_body:
|
||||
@@ -206,4 +207,7 @@ class ASGIApp:
|
||||
"""
|
||||
Handle the incoming request.
|
||||
"""
|
||||
await self.sanic_app.handle_request(self.request)
|
||||
try:
|
||||
await self.sanic_app.handle_request(self.request)
|
||||
except Exception as e:
|
||||
await self.sanic_app.handle_exception(self.request, e)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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
|
||||
@@ -8,6 +11,9 @@ 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,
|
||||
@@ -17,7 +23,25 @@ class BaseSanic(
|
||||
):
|
||||
__fake_slots__: Tuple[str, ...]
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
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
|
||||
|
||||
for base in BaseSanic.__bases__:
|
||||
base.__init__(self, *args, **kwargs) # type: ignore
|
||||
|
||||
@@ -36,6 +60,7 @@ 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."
|
||||
f"instance.ctx.{name} instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from collections.abc import MutableSequence
|
||||
from typing import List, Optional, Union
|
||||
from __future__ import annotations
|
||||
|
||||
import sanic
|
||||
from collections.abc import MutableSequence
|
||||
from typing import TYPE_CHECKING, List, Optional, Union
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
|
||||
class BlueprintGroup(MutableSequence):
|
||||
@@ -54,9 +58,21 @@ class BlueprintGroup(MutableSequence):
|
||||
app.blueprint(bpg)
|
||||
"""
|
||||
|
||||
__slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes")
|
||||
__slots__ = (
|
||||
"_blueprints",
|
||||
"_url_prefix",
|
||||
"_version",
|
||||
"_strict_slashes",
|
||||
"_version_prefix",
|
||||
)
|
||||
|
||||
def __init__(self, url_prefix=None, version=None, strict_slashes=None):
|
||||
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
|
||||
|
||||
@@ -65,13 +81,14 @@ class BlueprintGroup(MutableSequence):
|
||||
inherited by each of the Blueprint
|
||||
:param strict_slashes: URL Strict slash behavior indicator
|
||||
"""
|
||||
self._blueprints = []
|
||||
self._blueprints: List[Blueprint] = []
|
||||
self._url_prefix = url_prefix
|
||||
self._version = version
|
||||
self._version_prefix = version_prefix
|
||||
self._strict_slashes = strict_slashes
|
||||
|
||||
@property
|
||||
def url_prefix(self) -> str:
|
||||
def url_prefix(self) -> Optional[Union[int, str, float]]:
|
||||
"""
|
||||
Retrieve the URL prefix being used for the Current Blueprint Group
|
||||
|
||||
@@ -80,7 +97,7 @@ class BlueprintGroup(MutableSequence):
|
||||
return self._url_prefix
|
||||
|
||||
@property
|
||||
def blueprints(self) -> List["sanic.Blueprint"]:
|
||||
def blueprints(self) -> List[Blueprint]:
|
||||
"""
|
||||
Retrieve a list of all the available blueprints under this group.
|
||||
|
||||
@@ -107,6 +124,15 @@ 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
|
||||
@@ -161,34 +187,16 @@ class BlueprintGroup(MutableSequence):
|
||||
"""
|
||||
return len(self._blueprints)
|
||||
|
||||
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:
|
||||
def append(self, value: 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(self._sanitize_blueprint(bp=value))
|
||||
self._blueprints.append(value)
|
||||
|
||||
def insert(self, index: int, item: "sanic.Blueprint") -> None:
|
||||
def insert(self, index: int, item: Blueprint) -> None:
|
||||
"""
|
||||
The Abstract class `MutableSequence` leverages this insert method to
|
||||
perform the `BlueprintGroup.append` operation.
|
||||
@@ -197,7 +205,7 @@ class BlueprintGroup(MutableSequence):
|
||||
:param item: New `Blueprint` object.
|
||||
:return: None
|
||||
"""
|
||||
self._blueprints.insert(index, self._sanitize_blueprint(item))
|
||||
self._blueprints.insert(index, item)
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -62,18 +62,20 @@ class Blueprint(BaseSanic):
|
||||
"strict_slashes",
|
||||
"url_prefix",
|
||||
"version",
|
||||
"version_prefix",
|
||||
"websocket_routes",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
name: str = None,
|
||||
url_prefix: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version_prefix: str = "/v",
|
||||
):
|
||||
super().__init__()
|
||||
super().__init__(name=name)
|
||||
|
||||
self._apps: Set[Sanic] = set()
|
||||
self.ctx = SimpleNamespace()
|
||||
@@ -81,7 +83,6 @@ 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
|
||||
@@ -91,6 +92,7 @@ class Blueprint(BaseSanic):
|
||||
else url_prefix
|
||||
)
|
||||
self.version = version
|
||||
self.version_prefix = version_prefix
|
||||
self.websocket_routes: List[Route] = []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -143,7 +145,13 @@ class Blueprint(BaseSanic):
|
||||
return super().signal(event, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def group(*blueprints, url_prefix="", version=None, strict_slashes=None):
|
||||
def group(
|
||||
*blueprints,
|
||||
url_prefix="",
|
||||
version=None,
|
||||
strict_slashes=None,
|
||||
version_prefix: str = "/v",
|
||||
):
|
||||
"""
|
||||
Create a list of blueprints, optionally grouping them under a
|
||||
general URL prefix.
|
||||
@@ -160,8 +168,6 @@ 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
|
||||
|
||||
@@ -169,6 +175,7 @@ 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)
|
||||
@@ -186,6 +193,9 @@ 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 = []
|
||||
@@ -200,12 +210,22 @@ class Blueprint(BaseSanic):
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
|
||||
strict_slashes = (
|
||||
self.strict_slashes
|
||||
if future.strict_slashes is None
|
||||
and self.strict_slashes is not None
|
||||
else future.strict_slashes
|
||||
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._extract_value(
|
||||
future.strict_slashes, opt_strict_slashes, self.strict_slashes
|
||||
)
|
||||
|
||||
name = app._generate_name(future.name)
|
||||
|
||||
apply_route = FutureRoute(
|
||||
@@ -215,13 +235,14 @@ class Blueprint(BaseSanic):
|
||||
future.host or self.host,
|
||||
strict_slashes,
|
||||
future.stream,
|
||||
future.version or self.version,
|
||||
version,
|
||||
name,
|
||||
future.ignore_body,
|
||||
future.websocket,
|
||||
future.subprotocols,
|
||||
future.unquote,
|
||||
future.static,
|
||||
version_prefix,
|
||||
)
|
||||
|
||||
route = app._apply_route(apply_route)
|
||||
@@ -258,8 +279,6 @@ 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
|
||||
]
|
||||
@@ -288,3 +307,12 @@ 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
|
||||
|
||||
102
sanic/config.py
102
sanic/config.py
@@ -1,7 +1,10 @@
|
||||
from inspect import isclass
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
from sanic.http import Http
|
||||
|
||||
from .utils import load_module_from_file_location, str_to_bool
|
||||
|
||||
@@ -15,33 +18,64 @@ BASE_LOGO = """
|
||||
"""
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||
"REQUEST_BUFFER_QUEUE_SIZE": 100,
|
||||
"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_TIMEOUT": 60, # 60 seconds
|
||||
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||
"KEEP_ALIVE": True,
|
||||
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
||||
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
||||
"WEBSOCKET_MAX_QUEUE": 32,
|
||||
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
||||
"WEBSOCKET_PING_INTERVAL": 20,
|
||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||
"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):
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
||||
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,
|
||||
):
|
||||
defaults = defaults or {}
|
||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||
|
||||
@@ -50,9 +84,22 @@ class Config(dict):
|
||||
if keep_alive is not None:
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
|
||||
if load_env:
|
||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||
self.load_environment_vars(prefix=prefix)
|
||||
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()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
@@ -62,6 +109,19 @@ 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):
|
||||
"""
|
||||
|
||||
@@ -1,2 +1,28 @@
|
||||
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
|
||||
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())
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
@@ -366,7 +366,7 @@ def exception_response(
|
||||
except InvalidUsage:
|
||||
renderer = HTMLRenderer
|
||||
|
||||
content_type, *_ = request.headers.get(
|
||||
content_type, *_ = request.headers.getone(
|
||||
"content-type", ""
|
||||
).split(";")
|
||||
renderer = RENDERERS_BY_CONTENT_TYPE.get(
|
||||
|
||||
@@ -3,26 +3,18 @@ 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, status_code=None, quiet=None):
|
||||
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")
|
||||
|
||||
super().__init__(message)
|
||||
|
||||
if status_code is not None:
|
||||
@@ -33,45 +25,45 @@ class SanicException(Exception):
|
||||
self.quiet = True
|
||||
|
||||
|
||||
@add_status_code(404)
|
||||
class NotFound(SanicException):
|
||||
"""
|
||||
**Status**: 404 Not Found
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 404
|
||||
quiet = True
|
||||
|
||||
|
||||
@add_status_code(400)
|
||||
class InvalidUsage(SanicException):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 400
|
||||
quiet = True
|
||||
|
||||
|
||||
@add_status_code(405)
|
||||
class MethodNotSupported(SanicException):
|
||||
"""
|
||||
**Status**: 405 Method Not Allowed
|
||||
"""
|
||||
|
||||
status_code = 405
|
||||
quiet = True
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 500
|
||||
|
||||
|
||||
@add_status_code(503)
|
||||
class ServiceUnavailable(SanicException):
|
||||
"""
|
||||
**Status**: 503 Service Unavailable
|
||||
@@ -80,7 +72,8 @@ class ServiceUnavailable(SanicException):
|
||||
down for maintenance). Generally, this is a temporary state.
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 503
|
||||
quiet = True
|
||||
|
||||
|
||||
class URLBuildError(ServerError):
|
||||
@@ -88,7 +81,7 @@ class URLBuildError(ServerError):
|
||||
**Status**: 500 Internal Server Error
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 500
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
@@ -102,7 +95,6 @@ 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
|
||||
@@ -112,16 +104,17 @@ class RequestTimeout(SanicException):
|
||||
server has 'timed out' on that particular socket connection.
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 408
|
||||
quiet = True
|
||||
|
||||
|
||||
@add_status_code(413)
|
||||
class PayloadTooLarge(SanicException):
|
||||
"""
|
||||
**Status**: 413 Payload Too Large
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 413
|
||||
quiet = True
|
||||
|
||||
|
||||
class HeaderNotFound(InvalidUsage):
|
||||
@@ -129,36 +122,39 @@ class HeaderNotFound(InvalidUsage):
|
||||
**Status**: 400 Bad Request
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 400
|
||||
quiet = True
|
||||
|
||||
|
||||
@add_status_code(416)
|
||||
class ContentRangeError(SanicException):
|
||||
"""
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""
|
||||
|
||||
status_code = 416
|
||||
quiet = True
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 417
|
||||
quiet = True
|
||||
|
||||
|
||||
@add_status_code(403)
|
||||
class Forbidden(SanicException):
|
||||
"""
|
||||
**Status**: 403 Forbidden
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 403
|
||||
quiet = True
|
||||
|
||||
|
||||
class InvalidRangeType(ContentRangeError):
|
||||
@@ -166,7 +162,8 @@ class InvalidRangeType(ContentRangeError):
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 416
|
||||
quiet = True
|
||||
|
||||
|
||||
class PyFileError(Exception):
|
||||
@@ -174,7 +171,6 @@ class PyFileError(Exception):
|
||||
super().__init__("could not execute config file %s", file)
|
||||
|
||||
|
||||
@add_status_code(401)
|
||||
class Unauthorized(SanicException):
|
||||
"""
|
||||
**Status**: 401 Unauthorized
|
||||
@@ -210,6 +206,9 @@ class Unauthorized(SanicException):
|
||||
realm="Restricted Area")
|
||||
"""
|
||||
|
||||
status_code = 401
|
||||
quiet = True
|
||||
|
||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||
super().__init__(message, status_code)
|
||||
|
||||
@@ -241,9 +240,13 @@ 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
|
||||
"""
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -6,7 +6,7 @@ from sanic.exceptions import (
|
||||
HeaderNotFound,
|
||||
InvalidRangeType,
|
||||
)
|
||||
from sanic.log import logger
|
||||
from sanic.log import error_logger
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ class ErrorHandler:
|
||||
|
||||
handlers = None
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = []
|
||||
@@ -45,7 +44,9 @@ 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):
|
||||
"""
|
||||
@@ -61,14 +62,19 @@ class ErrorHandler:
|
||||
|
||||
:return: Registered function if found ``None`` otherwise
|
||||
"""
|
||||
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
|
||||
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
|
||||
return handler
|
||||
|
||||
def response(self, request, exception):
|
||||
@@ -101,7 +107,7 @@ class ErrorHandler:
|
||||
response_message = (
|
||||
"Exception raised in exception handler " '"%s" for uri: %s'
|
||||
)
|
||||
logger.exception(response_message, handler.__name__, url)
|
||||
error_logger.exception(response_message, handler.__name__, url)
|
||||
|
||||
if self.debug:
|
||||
return text(response_message % (handler.__name__, url), 500)
|
||||
@@ -137,7 +143,9 @@ class ErrorHandler:
|
||||
url = "unknown"
|
||||
|
||||
self.log(format_exc())
|
||||
logger.exception("Exception occurred while handling uri: %s", url)
|
||||
error_logger.exception(
|
||||
"Exception occurred while handling uri: %s", url
|
||||
)
|
||||
|
||||
return exception_response(request, exception, self.debug)
|
||||
|
||||
@@ -165,7 +173,7 @@ class ContentRangeHandler:
|
||||
|
||||
def __init__(self, request, stats):
|
||||
self.total = stats.st_size
|
||||
_range = request.headers.get("Range")
|
||||
_range = request.headers.getone("range", None)
|
||||
if _range is None:
|
||||
raise HeaderNotFound("Range Header Not Found")
|
||||
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
||||
|
||||
@@ -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.get(real_ip_header)
|
||||
addr = real_ip_header and headers.getone(real_ip_header, None)
|
||||
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.get(header)
|
||||
yield key, headers.getone(header, None)
|
||||
|
||||
return fwd_normalize(options())
|
||||
|
||||
|
||||
@@ -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, logger
|
||||
from sanic.log import access_logger, error_logger, logger
|
||||
|
||||
|
||||
class Stage(Enum):
|
||||
@@ -64,6 +64,9 @@ class Http:
|
||||
:raises RuntimeError:
|
||||
"""
|
||||
|
||||
HEADER_CEILING = 16_384
|
||||
HEADER_MAX_SIZE = 0
|
||||
|
||||
__slots__ = [
|
||||
"_send",
|
||||
"_receive_more",
|
||||
@@ -82,6 +85,7 @@ class Http:
|
||||
"request_max_size",
|
||||
"response",
|
||||
"response_func",
|
||||
"response_size",
|
||||
"response_bytes_left",
|
||||
"upgrade_websocket",
|
||||
]
|
||||
@@ -91,19 +95,23 @@ class Http:
|
||||
self._receive_more = protocol.receive_more
|
||||
self.recv_buffer = protocol.recv_buffer
|
||||
self.protocol = protocol
|
||||
self.expecting_continue: bool = False
|
||||
self.keep_alive = True
|
||||
self.stage: Stage = Stage.IDLE
|
||||
self.init_for_request()
|
||||
|
||||
def init_for_request(self):
|
||||
"""Init/reset all per-request variables."""
|
||||
self.exception = None
|
||||
self.expecting_continue: bool = False
|
||||
self.head_only = None
|
||||
self.request_body = None
|
||||
self.request_bytes = None
|
||||
self.request_bytes_left = None
|
||||
self.request_max_size = protocol.request_max_size
|
||||
self.keep_alive = True
|
||||
self.head_only = None
|
||||
self.request_max_size = self.protocol.request_max_size
|
||||
self.request: Request = None
|
||||
self.response: BaseHTTPResponse = None
|
||||
self.exception = None
|
||||
self.url = None
|
||||
self.upgrade_websocket = False
|
||||
self.url = None
|
||||
|
||||
def __bool__(self):
|
||||
"""Test if request handling is in progress"""
|
||||
@@ -143,8 +151,11 @@ class Http:
|
||||
# Try to consume any remaining request body
|
||||
if self.request_body:
|
||||
if self.response and 200 <= self.response.status < 300:
|
||||
logger.error(f"{self.request} body not consumed.")
|
||||
|
||||
error_logger.error(f"{self.request} body not consumed.")
|
||||
# Limit the size because the handler may have set it infinite
|
||||
self.request_max_size = min(
|
||||
self.request_max_size, self.protocol.request_max_size
|
||||
)
|
||||
try:
|
||||
async for _ in self:
|
||||
pass
|
||||
@@ -156,11 +167,19 @@ class Http:
|
||||
await sleep(0.001)
|
||||
self.keep_alive = False
|
||||
|
||||
# Clean up to free memory and for the next request
|
||||
if self.request:
|
||||
self.request.stream = None
|
||||
if self.response:
|
||||
self.response.stream = None
|
||||
|
||||
self.init_for_request()
|
||||
|
||||
# Exit and disconnect if no more requests can be taken
|
||||
if self.stage is not Stage.IDLE or not self.keep_alive:
|
||||
break
|
||||
|
||||
# Wait for next request
|
||||
# Wait for the next request
|
||||
if not self.recv_buffer:
|
||||
await self._receive_more()
|
||||
|
||||
@@ -168,7 +187,6 @@ 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
|
||||
@@ -179,12 +197,12 @@ class Http:
|
||||
break
|
||||
|
||||
pos = max(0, len(buf) - 3)
|
||||
if pos >= HEADER_MAX_SIZE:
|
||||
if pos >= self.HEADER_MAX_SIZE:
|
||||
break
|
||||
|
||||
await self._receive_more()
|
||||
|
||||
if pos >= HEADER_MAX_SIZE:
|
||||
if pos >= self.HEADER_MAX_SIZE:
|
||||
raise PayloadTooLarge("Request header exceeds the size limit")
|
||||
|
||||
# Parse header content
|
||||
@@ -218,7 +236,9 @@ class Http:
|
||||
raise InvalidUsage("Bad Request")
|
||||
|
||||
headers_instance = Header(headers)
|
||||
self.upgrade_websocket = headers_instance.get("upgrade") == "websocket"
|
||||
self.upgrade_websocket = (
|
||||
headers_instance.getone("upgrade", "").lower() == "websocket"
|
||||
)
|
||||
|
||||
# Prepare a Request object
|
||||
request = self.protocol.request_class(
|
||||
@@ -235,7 +255,7 @@ class Http:
|
||||
self.request_bytes_left = self.request_bytes = 0
|
||||
if request_body:
|
||||
headers = request.headers
|
||||
expect = headers.get("expect")
|
||||
expect = headers.getone("expect", None)
|
||||
|
||||
if expect is not None:
|
||||
if expect.lower() == "100-continue":
|
||||
@@ -243,7 +263,7 @@ class Http:
|
||||
else:
|
||||
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
|
||||
|
||||
if headers.get("transfer-encoding") == "chunked":
|
||||
if headers.getone("transfer-encoding", None) == "chunked":
|
||||
self.request_body = "chunked"
|
||||
pos -= 2 # One CRLF stays in buffer
|
||||
else:
|
||||
@@ -270,6 +290,7 @@ class Http:
|
||||
size = len(data)
|
||||
headers = res.headers
|
||||
status = res.status
|
||||
self.response_size = size
|
||||
|
||||
if not isinstance(status, int) or status < 200:
|
||||
raise RuntimeError(f"Invalid response status {status!r}")
|
||||
@@ -424,7 +445,9 @@ class Http:
|
||||
req, res = self.request, self.response
|
||||
extra = {
|
||||
"status": getattr(res, "status", 0),
|
||||
"byte": getattr(self, "response_bytes_left", -1),
|
||||
"byte": getattr(
|
||||
self, "response_bytes_left", getattr(self, "response_size", -1)
|
||||
),
|
||||
"host": "UNKNOWN",
|
||||
"request": "nil",
|
||||
}
|
||||
@@ -478,8 +501,6 @@ class Http:
|
||||
self.keep_alive = False
|
||||
raise InvalidUsage("Bad chunked encoding")
|
||||
|
||||
del buf[: pos + 2]
|
||||
|
||||
if size <= 0:
|
||||
self.request_body = None
|
||||
|
||||
@@ -487,8 +508,17 @@ class Http:
|
||||
self.keep_alive = False
|
||||
raise InvalidUsage("Bad chunked encoding")
|
||||
|
||||
# Consume CRLF, chunk size 0 and the two CRLF that follow
|
||||
pos += 4
|
||||
# Might need to wait for the final CRLF
|
||||
while len(buf) < pos:
|
||||
await self._receive_more()
|
||||
del buf[:pos]
|
||||
return None
|
||||
|
||||
# Remove CRLF, chunk size and the CRLF that follows
|
||||
del buf[: pos + 2]
|
||||
|
||||
self.request_bytes_left = size
|
||||
self.request_bytes += size
|
||||
|
||||
@@ -535,3 +565,10 @@ 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,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ class ListenerMixin:
|
||||
"""
|
||||
Create a listener from a decorated function.
|
||||
|
||||
To be used as a deocrator:
|
||||
To be used as a decorator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -26,10 +26,11 @@ 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]:
|
||||
@@ -45,7 +46,7 @@ class RouteMixin:
|
||||
host: Optional[str] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
stream: bool = False,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
ignore_body: bool = False,
|
||||
apply: bool = True,
|
||||
@@ -53,6 +54,7 @@ class RouteMixin:
|
||||
websocket: bool = False,
|
||||
unquote: bool = False,
|
||||
static: bool = False,
|
||||
version_prefix: str = "/v",
|
||||
):
|
||||
"""
|
||||
Decorate a function to be registered as a route
|
||||
@@ -66,6 +68,8 @@ 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
|
||||
"""
|
||||
|
||||
@@ -92,6 +96,7 @@ 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
|
||||
@@ -128,6 +133,7 @@ class RouteMixin:
|
||||
subprotocols,
|
||||
unquote,
|
||||
static,
|
||||
version_prefix,
|
||||
)
|
||||
|
||||
self._future_routes.add(route)
|
||||
@@ -154,7 +160,9 @@ class RouteMixin:
|
||||
if apply:
|
||||
self._apply_route(route)
|
||||
|
||||
return route, handler
|
||||
if static:
|
||||
return route, handler
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -168,6 +176,7 @@ 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
|
||||
@@ -182,6 +191,8 @@ 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
|
||||
@@ -214,6 +225,7 @@ class RouteMixin:
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
)(handler)
|
||||
return handler
|
||||
|
||||
@@ -226,6 +238,7 @@ 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
|
||||
@@ -236,6 +249,8 @@ 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(
|
||||
@@ -246,6 +261,7 @@ class RouteMixin:
|
||||
version=version,
|
||||
name=name,
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
|
||||
def post(
|
||||
@@ -256,6 +272,7 @@ 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
|
||||
@@ -266,6 +283,8 @@ 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(
|
||||
@@ -276,6 +295,7 @@ class RouteMixin:
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
|
||||
def put(
|
||||
@@ -286,6 +306,7 @@ 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
|
||||
@@ -296,6 +317,8 @@ 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(
|
||||
@@ -306,6 +329,7 @@ class RouteMixin:
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
|
||||
def head(
|
||||
@@ -316,6 +340,7 @@ 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
|
||||
@@ -334,6 +359,8 @@ 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(
|
||||
@@ -344,6 +371,7 @@ class RouteMixin:
|
||||
version=version,
|
||||
name=name,
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
|
||||
def options(
|
||||
@@ -354,6 +382,7 @@ 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
|
||||
@@ -372,6 +401,8 @@ 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(
|
||||
@@ -382,6 +413,7 @@ class RouteMixin:
|
||||
version=version,
|
||||
name=name,
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
|
||||
def patch(
|
||||
@@ -392,6 +424,7 @@ 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
|
||||
@@ -412,6 +445,8 @@ 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(
|
||||
@@ -422,6 +457,7 @@ class RouteMixin:
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
|
||||
def delete(
|
||||
@@ -432,6 +468,7 @@ 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
|
||||
@@ -442,6 +479,8 @@ 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(
|
||||
@@ -452,6 +491,7 @@ class RouteMixin:
|
||||
version=version,
|
||||
name=name,
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
|
||||
def websocket(
|
||||
@@ -463,6 +503,7 @@ 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
|
||||
@@ -474,6 +515,8 @@ 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(
|
||||
@@ -486,6 +529,7 @@ class RouteMixin:
|
||||
apply=apply,
|
||||
subprotocols=subprotocols,
|
||||
websocket=True,
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
|
||||
def add_websocket_route(
|
||||
@@ -497,6 +541,7 @@ 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.
|
||||
@@ -513,6 +558,8 @@ 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(
|
||||
@@ -522,6 +569,7 @@ class RouteMixin:
|
||||
subprotocols=subprotocols,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
)(handler)
|
||||
|
||||
def static(
|
||||
@@ -665,7 +713,10 @@ class RouteMixin:
|
||||
modified_since = strftime(
|
||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
||||
)
|
||||
if request.headers.get("If-Modified-Since") == modified_since:
|
||||
if (
|
||||
request.headers.getone("if-modified-since", None)
|
||||
== modified_since
|
||||
):
|
||||
return HTTPResponse(status=304)
|
||||
headers["Last-Modified"] = modified_since
|
||||
_range = None
|
||||
@@ -718,16 +769,18 @@ class RouteMixin:
|
||||
return await file(file_path, headers=headers, _range=_range)
|
||||
except ContentRangeError:
|
||||
raise
|
||||
except Exception:
|
||||
error_logger.exception(
|
||||
f"File not found: path={file_or_directory}, "
|
||||
f"relative_url={__file_uri__}"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Callable, Dict, Set
|
||||
from typing import Any, Callable, Dict, Optional, Set
|
||||
|
||||
from sanic.models.futures import FutureSignal
|
||||
from sanic.models.handler_types import SignalHandler
|
||||
@@ -60,10 +60,16 @@ class SignalMixin:
|
||||
|
||||
def add_signal(
|
||||
self,
|
||||
handler,
|
||||
handler: Optional[Callable[..., Any]],
|
||||
event: str,
|
||||
condition: Dict[str, Any] = None,
|
||||
):
|
||||
if not handler:
|
||||
|
||||
async def noop():
|
||||
...
|
||||
|
||||
handler = noop
|
||||
self.signal(event=event, condition=condition)(handler)
|
||||
return handler
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class FutureRoute(NamedTuple):
|
||||
subprotocols: Optional[List[str]]
|
||||
unquote: bool
|
||||
static: bool
|
||||
version_prefix: str
|
||||
|
||||
|
||||
class FutureListener(NamedTuple):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import itertools
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
@@ -5,6 +6,9 @@ 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.
|
||||
@@ -56,7 +60,21 @@ def restart_with_reloader():
|
||||
)
|
||||
|
||||
|
||||
def watchdog(sleep_interval):
|
||||
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):
|
||||
"""Watch project files, restart worker process if a change happened.
|
||||
|
||||
:param sleep_interval: interval in second.
|
||||
@@ -73,21 +91,25 @@ def watchdog(sleep_interval):
|
||||
|
||||
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 _iter_module_files():
|
||||
for filename in itertools.chain(
|
||||
_iter_module_files(),
|
||||
*(d.glob("**/*") for d in app.reload_dirs),
|
||||
):
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
check = _check_file(filename, mtimes)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
old_time = mtimes.get(filename)
|
||||
if old_time is None:
|
||||
mtimes[filename] = mtime
|
||||
elif mtime > old_time:
|
||||
mtimes[filename] = mtime
|
||||
if check:
|
||||
need_reload = True
|
||||
|
||||
if need_reload:
|
||||
|
||||
@@ -125,7 +125,7 @@ class Request:
|
||||
self._name: Optional[str] = None
|
||||
self.app = app
|
||||
|
||||
self.headers = headers
|
||||
self.headers = Header(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.get(
|
||||
self._id = self.headers.getone(
|
||||
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.get("Authorization")
|
||||
auth_header = self.headers.getone("authorization", None)
|
||||
|
||||
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.get(
|
||||
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
|
||||
content_type = self.headers.getone(
|
||||
"content-type", DEFAULT_HTTP_CONTENT_TYPE
|
||||
)
|
||||
content_type, parameters = parse_content_header(content_type)
|
||||
try:
|
||||
@@ -378,9 +378,12 @@ class Request:
|
||||
:type errors: str
|
||||
:return: RequestParameters
|
||||
"""
|
||||
if not self.parsed_args[
|
||||
(keep_blank_values, strict_parsing, encoding, errors)
|
||||
]:
|
||||
if (
|
||||
keep_blank_values,
|
||||
strict_parsing,
|
||||
encoding,
|
||||
errors,
|
||||
) not in self.parsed_args:
|
||||
if self.query_string:
|
||||
self.parsed_args[
|
||||
(keep_blank_values, strict_parsing, encoding, errors)
|
||||
@@ -434,9 +437,12 @@ class Request:
|
||||
:type errors: str
|
||||
:return: list
|
||||
"""
|
||||
if not self.parsed_not_grouped_args[
|
||||
(keep_blank_values, strict_parsing, encoding, errors)
|
||||
]:
|
||||
if (
|
||||
keep_blank_values,
|
||||
strict_parsing,
|
||||
encoding,
|
||||
errors,
|
||||
) not in self.parsed_not_grouped_args:
|
||||
if self.query_string:
|
||||
self.parsed_not_grouped_args[
|
||||
(keep_blank_values, strict_parsing, encoding, errors)
|
||||
@@ -465,7 +471,7 @@ class Request:
|
||||
"""
|
||||
|
||||
if self._cookies is None:
|
||||
cookie = self.headers.get("Cookie")
|
||||
cookie = self.headers.getone("cookie", None)
|
||||
if cookie is not None:
|
||||
cookies: SimpleCookie = SimpleCookie()
|
||||
cookies.load(cookie)
|
||||
@@ -482,7 +488,7 @@ class Request:
|
||||
:return: Content-Type header form the request
|
||||
:rtype: str
|
||||
"""
|
||||
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||
return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def match_info(self):
|
||||
@@ -499,7 +505,7 @@ class Request:
|
||||
:return: peer ip of the socket
|
||||
:rtype: str
|
||||
"""
|
||||
return self.conn_info.client if self.conn_info else ""
|
||||
return self.conn_info.client_ip if self.conn_info else ""
|
||||
|
||||
@property
|
||||
def port(self) -> int:
|
||||
@@ -581,7 +587,7 @@ class Request:
|
||||
|
||||
if (
|
||||
self.app.websocket_enabled
|
||||
and self.headers.get("upgrade") == "websocket"
|
||||
and self.headers.getone("upgrade", "").lower() == "websocket"
|
||||
):
|
||||
scheme = "ws"
|
||||
else:
|
||||
@@ -608,7 +614,9 @@ 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.get("host", ""))
|
||||
return str(
|
||||
self.forwarded.get("host") or self.headers.getone("host", "")
|
||||
)
|
||||
|
||||
@property
|
||||
def server_name(self) -> str:
|
||||
|
||||
@@ -143,7 +143,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
|
||||
.. warning::
|
||||
|
||||
**Deprecated** and set for removal in v21.6. You can now achieve the
|
||||
**Deprecated** and set for removal in v21.12. You can now achieve the
|
||||
same functionality without a callback.
|
||||
|
||||
.. code-block:: python
|
||||
@@ -174,12 +174,16 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
status: int = 200,
|
||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||
content_type: str = "text/plain; charset=utf-8",
|
||||
chunked="deprecated",
|
||||
ignore_deprecation_notice: bool = False,
|
||||
):
|
||||
if chunked != "deprecated":
|
||||
if not ignore_deprecation_notice:
|
||||
warn(
|
||||
"The chunked argument has been deprecated and will be "
|
||||
"removed in v21.6"
|
||||
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
|
||||
"will be removed in v21.12. Please upgrade your streaming "
|
||||
"response implementation. You can learn more here: "
|
||||
"https://sanicframework.org/en/guide/advanced/streaming.html"
|
||||
"#response-streaming. If you use the builtin stream() or "
|
||||
"file_stream() methods, this upgrade will be be done for you."
|
||||
)
|
||||
|
||||
super().__init__()
|
||||
@@ -203,6 +207,9 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
self.streaming_fn = None
|
||||
await super().send(*args, **kwargs)
|
||||
|
||||
async def eof(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HTTPResponse(BaseHTTPResponse):
|
||||
"""
|
||||
@@ -235,6 +242,15 @@ 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
|
||||
@@ -396,7 +412,6 @@ 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.
|
||||
@@ -409,12 +424,6 @@ 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(
|
||||
@@ -453,6 +462,7 @@ async def file_stream(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
ignore_deprecation_notice=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -461,7 +471,6 @@ 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`.
|
||||
@@ -482,17 +491,12 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class Router(BaseRouter):
|
||||
return self.resolve(
|
||||
path=path,
|
||||
method=method,
|
||||
extra={"host": host},
|
||||
extra={"host": host} if host else None,
|
||||
)
|
||||
except RoutingNotFound as e:
|
||||
raise NotFound("Requested URL {} not found".format(e.path))
|
||||
@@ -73,6 +73,7 @@ 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
|
||||
@@ -103,12 +104,12 @@ class Router(BaseRouter):
|
||||
"""
|
||||
if version is not None:
|
||||
version = str(version).strip("/").lstrip("v")
|
||||
uri = "/".join([f"/v{version}", uri.lstrip("/")])
|
||||
uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")])
|
||||
|
||||
params = dict(
|
||||
path=uri,
|
||||
handler=handler,
|
||||
methods=methods,
|
||||
methods=frozenset(map(str, methods)) if methods else None,
|
||||
name=name,
|
||||
strict=strict_slashes,
|
||||
unquote=unquote,
|
||||
@@ -161,7 +162,7 @@ class Router(BaseRouter):
|
||||
|
||||
@property
|
||||
def routes_all(self):
|
||||
return self.routes
|
||||
return {route.parts: route for route in self.routes}
|
||||
|
||||
@property
|
||||
def routes_static(self):
|
||||
|
||||
@@ -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 logger
|
||||
from sanic.log import error_logger, logger
|
||||
from sanic.models.protocol_types import TransportProtocol
|
||||
from sanic.request import Request
|
||||
|
||||
@@ -65,6 +65,7 @@ class ConnInfo:
|
||||
__slots__ = (
|
||||
"client_port",
|
||||
"client",
|
||||
"client_ip",
|
||||
"ctx",
|
||||
"peername",
|
||||
"server_port",
|
||||
@@ -78,6 +79,7 @@ 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"))
|
||||
|
||||
@@ -96,6 +98,7 @@ 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]
|
||||
|
||||
|
||||
@@ -122,7 +125,6 @@ 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
|
||||
@@ -165,9 +167,6 @@ 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
|
||||
@@ -199,11 +198,11 @@ class HttpProtocol(asyncio.Protocol):
|
||||
except CancelledError:
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("protocol.connection_task uncaught")
|
||||
error_logger.exception("protocol.connection_task uncaught")
|
||||
finally:
|
||||
if self.app.debug and self._http:
|
||||
ip = self.transport.get_extra_info("peername")
|
||||
logger.error(
|
||||
error_logger.error(
|
||||
"Connection lost before response written"
|
||||
f" @ {ip} {self._http.request}"
|
||||
)
|
||||
@@ -212,7 +211,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
try:
|
||||
self.close()
|
||||
except BaseException:
|
||||
logger.exception("Closing failed")
|
||||
error_logger.exception("Closing failed")
|
||||
|
||||
async def receive_more(self):
|
||||
"""
|
||||
@@ -258,7 +257,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
return
|
||||
self._task.cancel()
|
||||
except Exception:
|
||||
logger.exception("protocol.check_timeouts")
|
||||
error_logger.exception("protocol.check_timeouts")
|
||||
|
||||
async def send(self, data):
|
||||
"""
|
||||
@@ -304,7 +303,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.recv_buffer = bytearray()
|
||||
self.conn_info = ConnInfo(self.transport, unix=self._unix)
|
||||
except Exception:
|
||||
logger.exception("protocol.connect_made")
|
||||
error_logger.exception("protocol.connect_made")
|
||||
|
||||
def connection_lost(self, exc):
|
||||
try:
|
||||
@@ -313,7 +312,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
except Exception:
|
||||
logger.exception("protocol.connection_lost")
|
||||
error_logger.exception("protocol.connection_lost")
|
||||
|
||||
def pause_writing(self):
|
||||
self._can_write.clear()
|
||||
@@ -337,7 +336,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
if self._data_received:
|
||||
self._data_received.set()
|
||||
except Exception:
|
||||
logger.exception("protocol.data_received")
|
||||
error_logger.exception("protocol.data_received")
|
||||
|
||||
|
||||
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
|
||||
@@ -556,7 +555,7 @@ def serve(
|
||||
try:
|
||||
http_server = loop.run_until_complete(server_coroutine)
|
||||
except BaseException:
|
||||
logger.exception("Unable to start server")
|
||||
error_logger.exception("Unable to start server")
|
||||
return
|
||||
|
||||
trigger_events(after_start, loop)
|
||||
|
||||
@@ -5,7 +5,7 @@ import asyncio
|
||||
from inspect import isawaitable
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from sanic_routing import BaseRouter, Route # type: ignore
|
||||
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
|
||||
from sanic_routing.exceptions import NotFound # type: ignore
|
||||
from sanic_routing.utils import path_to_parts # type: ignore
|
||||
|
||||
@@ -20,17 +20,11 @@ RESERVED_NAMESPACES = (
|
||||
|
||||
|
||||
class Signal(Route):
|
||||
def get_handler(self, raw_path, method, _):
|
||||
method = method or self.router.DEFAULT_METHOD
|
||||
raw_path = raw_path.lstrip(self.router.delimiter)
|
||||
try:
|
||||
return self.handlers[raw_path][method]
|
||||
except (IndexError, KeyError):
|
||||
raise self.router.method_handler_exception(
|
||||
f"Method '{method}' not found on {self}",
|
||||
method=method,
|
||||
allowed_methods=set(self.methods[raw_path]),
|
||||
)
|
||||
...
|
||||
|
||||
|
||||
class SignalGroup(RouteGroup):
|
||||
...
|
||||
|
||||
|
||||
class SignalRouter(BaseRouter):
|
||||
@@ -38,6 +32,7 @@ class SignalRouter(BaseRouter):
|
||||
super().__init__(
|
||||
delimiter=".",
|
||||
route_class=Signal,
|
||||
group_class=SignalGroup,
|
||||
stacking=True,
|
||||
)
|
||||
self.ctx.loop = None
|
||||
@@ -49,7 +44,13 @@ class SignalRouter(BaseRouter):
|
||||
):
|
||||
extra = condition or {}
|
||||
try:
|
||||
return self.resolve(f".{event}", extra=extra)
|
||||
group, param_basket = self.find_route(
|
||||
f".{event}",
|
||||
self.DEFAULT_METHOD,
|
||||
self,
|
||||
{"__params__": {}, "__matches__": {}},
|
||||
extra=extra,
|
||||
)
|
||||
except NotFound:
|
||||
message = "Could not find signal %s"
|
||||
terms: List[Union[str, Optional[Dict[str, str]]]] = [event]
|
||||
@@ -58,16 +59,26 @@ 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()
|
||||
}
|
||||
|
||||
return group, [route.handler for route in group], params
|
||||
|
||||
async def _dispatch(
|
||||
self,
|
||||
event: str,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
condition: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
signal, handlers, params = self.get(event, condition=condition)
|
||||
group, handlers, params = self.get(event, condition=condition)
|
||||
|
||||
signal_event = signal.ctx.event
|
||||
signal_event.set()
|
||||
events = [signal.ctx.event for signal in group]
|
||||
for signal_event in events:
|
||||
signal_event.set()
|
||||
if context:
|
||||
params.update(context)
|
||||
|
||||
@@ -78,7 +89,8 @@ class SignalRouter(BaseRouter):
|
||||
if isawaitable(maybe_coroutine):
|
||||
await maybe_coroutine
|
||||
finally:
|
||||
signal_event.clear()
|
||||
for signal_event in events:
|
||||
signal_event.clear()
|
||||
|
||||
async def dispatch(
|
||||
self,
|
||||
@@ -116,7 +128,7 @@ class SignalRouter(BaseRouter):
|
||||
handler,
|
||||
requirements=condition,
|
||||
name=name,
|
||||
overwrite=True,
|
||||
append=True,
|
||||
) # type: ignore
|
||||
|
||||
def finalize(self, do_compile: bool = True):
|
||||
@@ -125,7 +137,7 @@ class SignalRouter(BaseRouter):
|
||||
except RuntimeError:
|
||||
raise RuntimeError("Cannot finalize signals outside of event loop")
|
||||
|
||||
for signal in self.routes.values():
|
||||
for signal in self.routes:
|
||||
signal.ctx.event = asyncio.Event()
|
||||
|
||||
return super().finalize(do_compile=do_compile)
|
||||
|
||||
21
sanic/simple.py
Normal file
21
sanic/simple.py
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
@@ -105,6 +105,7 @@ 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
|
||||
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
from typing import Any, Callable, List
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
from warnings import warn
|
||||
|
||||
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
|
||||
@@ -40,6 +56,31 @@ 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)
|
||||
@@ -65,6 +106,31 @@ 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
|
||||
@@ -91,6 +157,11 @@ 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
|
||||
|
||||
@@ -14,9 +14,13 @@ 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
|
||||
|
||||
@@ -37,7 +41,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
|
||||
@@ -126,7 +130,9 @@ class WebSocketProtocol(HttpProtocol):
|
||||
ping_interval=self.websocket_ping_interval,
|
||||
ping_timeout=self.websocket_ping_timeout,
|
||||
)
|
||||
# Following two lines are required for websockets 8.x
|
||||
# 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
|
||||
self.websocket.is_client = False
|
||||
self.websocket.side = "server"
|
||||
self.websocket.subprotocol = subprotocol
|
||||
@@ -148,7 +154,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"}
|
||||
@@ -172,13 +178,28 @@ class WebSocketConnection:
|
||||
|
||||
receive = recv
|
||||
|
||||
async def accept(self) -> None:
|
||||
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
|
||||
|
||||
await self._send(
|
||||
{
|
||||
"type": "websocket.accept",
|
||||
"subprotocol": ",".join(list(self.subprotocols)),
|
||||
"subprotocol": subprotocol,
|
||||
}
|
||||
)
|
||||
|
||||
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 []
|
||||
|
||||
6
setup.py
6
setup.py
@@ -83,17 +83,17 @@ ujson = "ujson>=1.35" + env_dependency
|
||||
uvloop = "uvloop>=0.5.3" + env_dependency
|
||||
|
||||
requirements = [
|
||||
"sanic-routing",
|
||||
"sanic-routing~=0.7",
|
||||
"httptools>=0.0.10",
|
||||
uvloop,
|
||||
ujson,
|
||||
"aiofiles>=0.6.0",
|
||||
"websockets>=8.1,<9.0",
|
||||
"websockets>=9.0",
|
||||
"multidict>=5.0,<6.0",
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
"sanic-testing",
|
||||
"sanic-testing>=0.7.0b1",
|
||||
"pytest==5.2.1",
|
||||
"multidict>=5.0,<6.0",
|
||||
"gunicorn==20.0.4",
|
||||
|
||||
@@ -15,6 +15,7 @@ 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
|
||||
|
||||
@@ -140,5 +141,5 @@ def url_param_generator():
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def app(request):
|
||||
app = Sanic(request.node.name)
|
||||
app = Sanic(slugify.sub("-", request.node.name))
|
||||
return app
|
||||
|
||||
36
tests/fake/server.py
Normal file
36
tests/fake/server.py
Normal file
@@ -0,0 +1,36 @@
|
||||
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
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
|
||||
@@ -276,7 +277,7 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
|
||||
assert response.status == 500
|
||||
assert "Mock SanicException" in response.text
|
||||
assert (
|
||||
"sanic.root",
|
||||
"sanic.error",
|
||||
logging.ERROR,
|
||||
f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'",
|
||||
) in caplog.record_tuples
|
||||
@@ -389,7 +390,7 @@ def test_app_no_registry_env():
|
||||
|
||||
|
||||
def test_app_set_attribute_warning(app):
|
||||
with pytest.warns(UserWarning) as record:
|
||||
with pytest.warns(DeprecationWarning) as record:
|
||||
app.foo = 1
|
||||
|
||||
assert len(record) == 1
|
||||
@@ -412,3 +413,42 @@ 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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from collections import deque, namedtuple
|
||||
|
||||
@@ -8,7 +7,7 @@ import uvicorn
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.asgi import MockTransport
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.exceptions import Forbidden, InvalidUsage, ServiceUnavailable
|
||||
from sanic.request import Request
|
||||
from sanic.response import json, text
|
||||
from sanic.websocket import WebSocketConnection
|
||||
@@ -219,7 +218,7 @@ async def test_websocket_accept_with_no_subprotocols(
|
||||
|
||||
message = message_stack.popleft()
|
||||
assert message["type"] == "websocket.accept"
|
||||
assert message["subprotocol"] == ""
|
||||
assert message["subprotocol"] is None
|
||||
assert "bytes" not in message
|
||||
|
||||
|
||||
@@ -228,7 +227,7 @@ async def test_websocket_accept_with_subprotocol(send, receive, message_stack):
|
||||
subprotocols = ["graphql-ws"]
|
||||
|
||||
ws = WebSocketConnection(send, receive, subprotocols)
|
||||
await ws.accept()
|
||||
await ws.accept(subprotocols)
|
||||
|
||||
assert len(message_stack) == 1
|
||||
|
||||
@@ -245,13 +244,13 @@ async def test_websocket_accept_with_multiple_subprotocols(
|
||||
subprotocols = ["graphql-ws", "hello", "world"]
|
||||
|
||||
ws = WebSocketConnection(send, receive, subprotocols)
|
||||
await ws.accept()
|
||||
await ws.accept(["hello", "world"])
|
||||
|
||||
assert len(message_stack) == 1
|
||||
|
||||
message = message_stack.popleft()
|
||||
assert message["type"] == "websocket.accept"
|
||||
assert message["subprotocol"] == "graphql-ws,hello,world"
|
||||
assert message["subprotocol"] == "hello"
|
||||
assert "bytes" not in message
|
||||
|
||||
|
||||
@@ -347,3 +346,32 @@ async def test_content_type(app):
|
||||
|
||||
_, response = await app.asgi_client.get("/custom")
|
||||
assert response.headers.get("content-type") == "somethingelse"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_handle_exception(app):
|
||||
@app.get("/error-prone")
|
||||
def _request(request):
|
||||
raise ServiceUnavailable(message="Service unavailable")
|
||||
|
||||
_, response = await app.asgi_client.get("/wrong-path")
|
||||
assert response.status_code == 404
|
||||
|
||||
_, response = await app.asgi_client.get("/error-prone")
|
||||
assert response.status_code == 503
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_exception_suppressed_by_middleware(app):
|
||||
@app.get("/error-prone")
|
||||
def _request(request):
|
||||
raise ServiceUnavailable(message="Service unavailable")
|
||||
|
||||
@app.on_request
|
||||
def forbidden(request):
|
||||
raise Forbidden(message="forbidden")
|
||||
|
||||
_, response = await app.asgi_client.get("/wrong-path")
|
||||
assert response.status_code == 403
|
||||
|
||||
_, response = await app.asgi_client.get("/error-prone")
|
||||
assert response.status_code == 403
|
||||
@@ -41,3 +41,62 @@ 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
|
||||
|
||||
@@ -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) == 2
|
||||
assert len(blueprint_group_1) == 1
|
||||
|
||||
|
||||
def test_blueprint_group_insert():
|
||||
@@ -215,6 +215,61 @@ def test_blueprint_group_insert():
|
||||
group.insert(0, blueprint_1)
|
||||
group.insert(0, blueprint_2)
|
||||
group.insert(0, blueprint_3)
|
||||
assert group.blueprints[1].strict_slashes is False
|
||||
assert group.blueprints[2].strict_slashes is True
|
||||
assert group.blueprints[0].url_prefix == "/test"
|
||||
|
||||
@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
|
||||
|
||||
@@ -1028,7 +1028,7 @@ def test_blueprint_registered_multiple_apps():
|
||||
|
||||
def test_bp_set_attribute_warning():
|
||||
bp = Blueprint("bp")
|
||||
with pytest.warns(UserWarning) as record:
|
||||
with pytest.warns(DeprecationWarning) as record:
|
||||
bp.foo = 1
|
||||
|
||||
assert len(record) == 1
|
||||
|
||||
133
tests/test_cli.py
Normal file
133
tests/test_cli.py
Normal file
@@ -0,0 +1,133 @@
|
||||
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")
|
||||
@@ -59,14 +59,14 @@ def test_load_from_object_string_exception(app):
|
||||
app.config.load("test_config.Config.test")
|
||||
|
||||
|
||||
def test_auto_load_env():
|
||||
def test_auto_env_prefix():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic(name=__name__)
|
||||
assert app.config.TEST_ANSWER == 42
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
|
||||
def test_auto_load_bool_env():
|
||||
def test_auto_bool_env_prefix():
|
||||
environ["SANIC_TEST_ANSWER"] = "True"
|
||||
app = Sanic(name=__name__)
|
||||
assert app.config.TEST_ANSWER is True
|
||||
@@ -80,6 +80,12 @@ 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)
|
||||
|
||||
|
||||
def test_load_env_prefix():
|
||||
environ["MYAPP_TEST_ANSWER"] = "42"
|
||||
app = Sanic(name=__name__, load_env="MYAPP_")
|
||||
@@ -87,6 +93,14 @@ 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"]
|
||||
|
||||
|
||||
def test_load_env_prefix_float_values():
|
||||
environ["MYAPP_TEST_ROI"] = "2.3"
|
||||
app = Sanic(name=__name__, load_env="MYAPP_")
|
||||
@@ -101,6 +115,27 @@ 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(
|
||||
"""
|
||||
|
||||
28
tests/test_constants.py
Normal file
28
tests/test_constants.py
Normal file
@@ -0,0 +1,28 @@
|
||||
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"
|
||||
@@ -1,3 +1,5 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
@@ -7,6 +9,7 @@ from sanic.exceptions import (
|
||||
Forbidden,
|
||||
InvalidUsage,
|
||||
NotFound,
|
||||
SanicException,
|
||||
ServerError,
|
||||
Unauthorized,
|
||||
abort,
|
||||
@@ -68,16 +71,19 @@ def exception_app():
|
||||
|
||||
@app.route("/abort/401")
|
||||
def handler_401_error(request):
|
||||
abort(401)
|
||||
raise SanicException(status_code=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):
|
||||
abort(500, message="Abort")
|
||||
raise SanicException(message="Custom Message", status_code=500)
|
||||
|
||||
@app.route("/divide_by_zero")
|
||||
def handle_unhandled_exception(request):
|
||||
@@ -208,14 +214,21 @@ def test_exception_in_exception_handler_debug_on(exception_app):
|
||||
assert response.body.startswith(b"Exception raised in exception ")
|
||||
|
||||
|
||||
def test_abort(exception_app):
|
||||
"""Test the abort function"""
|
||||
def test_sanic_exception(exception_app):
|
||||
"""Test sanic exceptions are handled"""
|
||||
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 "Abort" in response.text
|
||||
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]
|
||||
|
||||
@@ -7,6 +7,13 @@ 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",
|
||||
[
|
||||
@@ -76,15 +83,75 @@ 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(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import platform
|
||||
|
||||
from asyncio import sleep as aio_sleep
|
||||
from json import JSONDecodeError
|
||||
@@ -241,7 +242,9 @@ def test_keep_alive_timeout_reuse():
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS,
|
||||
bool(environ.get("SANIC_NO_UVLOOP"))
|
||||
or OS_IS_WINDOWS
|
||||
or platform.system() != "Linux",
|
||||
reason="Not testable with current client",
|
||||
)
|
||||
def test_keep_alive_client_timeout():
|
||||
|
||||
@@ -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()
|
||||
root = logging.getLogger("sanic.root")
|
||||
root.addHandler(logging.StreamHandler(stream))
|
||||
monkeypatch.setattr(sanic.server, "logger", root)
|
||||
error = logging.getLogger("sanic.error")
|
||||
error.addHandler(logging.StreamHandler(stream))
|
||||
monkeypatch.setattr(sanic.server, "error_logger", error)
|
||||
|
||||
@app.route("/conn_lost")
|
||||
async def conn_lost(request):
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from asyncio import CancelledError
|
||||
from itertools import count
|
||||
|
||||
from sanic.exceptions import NotFound, SanicException
|
||||
from sanic.exceptions import NotFound
|
||||
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.root",
|
||||
"sanic.error",
|
||||
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.root",
|
||||
"sanic.error",
|
||||
logging.ERROR,
|
||||
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
||||
) not in caplog.record_tuples
|
||||
|
||||
@@ -209,13 +209,13 @@ def test_named_static_routes():
|
||||
return text("OK2")
|
||||
|
||||
assert app.router.routes_all[("test",)].name == "app.route_test"
|
||||
assert app.router.routes_static[("test",)].name == "app.route_test"
|
||||
assert app.router.routes_static[("test",)][0].name == "app.route_test"
|
||||
assert app.url_for("route_test") == "/test"
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for("handler1")
|
||||
|
||||
assert app.router.routes_all[("pizazz",)].name == "app.route_pizazz"
|
||||
assert app.router.routes_static[("pizazz",)].name == "app.route_pizazz"
|
||||
assert app.router.routes_static[("pizazz",)][0].name == "app.route_pizazz"
|
||||
assert app.url_for("route_pizazz") == "/pizazz"
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for("handler2")
|
||||
@@ -234,7 +234,7 @@ def test_named_dynamic_route():
|
||||
app.router.routes_all[
|
||||
(
|
||||
"folder",
|
||||
"<name>",
|
||||
"<name:str>",
|
||||
)
|
||||
].name
|
||||
== "app.route_dynamic"
|
||||
@@ -347,13 +347,13 @@ def test_static_add_named_route():
|
||||
app.add_route(handler2, "/test2", name="route_test2")
|
||||
|
||||
assert app.router.routes_all[("test",)].name == "app.route_test"
|
||||
assert app.router.routes_static[("test",)].name == "app.route_test"
|
||||
assert app.router.routes_static[("test",)][0].name == "app.route_test"
|
||||
assert app.url_for("route_test") == "/test"
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for("handler1")
|
||||
|
||||
assert app.router.routes_all[("test2",)].name == "app.route_test2"
|
||||
assert app.router.routes_static[("test2",)].name == "app.route_test2"
|
||||
assert app.router.routes_static[("test2",)][0].name == "app.route_test2"
|
||||
assert app.url_for("route_test2") == "/test2"
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for("handler2")
|
||||
@@ -369,7 +369,8 @@ def test_dynamic_add_named_route():
|
||||
|
||||
app.add_route(handler, "/folder/<name>", name="route_dynamic")
|
||||
assert (
|
||||
app.router.routes_all[("folder", "<name>")].name == "app.route_dynamic"
|
||||
app.router.routes_all[("folder", "<name:str>")].name
|
||||
== "app.route_dynamic"
|
||||
)
|
||||
assert app.url_for("route_dynamic", name="test") == "/folder/test"
|
||||
with pytest.raises(URLBuildError):
|
||||
|
||||
105
tests/test_pipelining.py
Normal file
105
tests/test_pipelining.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from httpx import AsyncByteStream
|
||||
from sanic_testing.reusable import ReusableClient
|
||||
|
||||
from sanic.response import json, text
|
||||
|
||||
|
||||
def test_no_body_requests(app):
|
||||
@app.get("/")
|
||||
async def handler(request):
|
||||
return json(
|
||||
{
|
||||
"request_id": str(request.id),
|
||||
"connection_id": id(request.conn_info),
|
||||
}
|
||||
)
|
||||
|
||||
client = ReusableClient(app, port=1234)
|
||||
|
||||
with client:
|
||||
_, response1 = client.get("/")
|
||||
_, response2 = client.get("/")
|
||||
|
||||
assert response1.status == response2.status == 200
|
||||
assert response1.json["request_id"] != response2.json["request_id"]
|
||||
assert response1.json["connection_id"] == response2.json["connection_id"]
|
||||
|
||||
|
||||
def test_json_body_requests(app):
|
||||
@app.post("/")
|
||||
async def handler(request):
|
||||
return json(
|
||||
{
|
||||
"request_id": str(request.id),
|
||||
"connection_id": id(request.conn_info),
|
||||
"foo": request.json.get("foo"),
|
||||
}
|
||||
)
|
||||
|
||||
client = ReusableClient(app, port=1234)
|
||||
|
||||
with client:
|
||||
_, response1 = client.post("/", json={"foo": True})
|
||||
_, response2 = client.post("/", json={"foo": True})
|
||||
|
||||
assert response1.status == response2.status == 200
|
||||
assert response1.json["foo"] is response2.json["foo"] is True
|
||||
assert response1.json["request_id"] != response2.json["request_id"]
|
||||
assert response1.json["connection_id"] == response2.json["connection_id"]
|
||||
|
||||
|
||||
def test_streaming_body_requests(app):
|
||||
@app.post("/", stream=True)
|
||||
async def handler(request):
|
||||
data = [part.decode("utf-8") async for part in request.stream]
|
||||
return json(
|
||||
{
|
||||
"request_id": str(request.id),
|
||||
"connection_id": id(request.conn_info),
|
||||
"data": data,
|
||||
}
|
||||
)
|
||||
|
||||
data = ["hello", "world"]
|
||||
|
||||
class Data(AsyncByteStream):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
async def __aiter__(self):
|
||||
for value in self.data:
|
||||
yield value.encode("utf-8")
|
||||
|
||||
client = ReusableClient(app, port=1234)
|
||||
|
||||
with client:
|
||||
_, response1 = client.post("/", data=Data(data))
|
||||
_, response2 = client.post("/", data=Data(data))
|
||||
|
||||
assert response1.status == response2.status == 200
|
||||
assert response1.json["data"] == response2.json["data"] == data
|
||||
assert response1.json["request_id"] != response2.json["request_id"]
|
||||
assert response1.json["connection_id"] == response2.json["connection_id"]
|
||||
|
||||
|
||||
def test_bad_headers(app):
|
||||
@app.get("/")
|
||||
async def handler(request):
|
||||
return text("")
|
||||
|
||||
@app.on_response
|
||||
async def reqid(request, response):
|
||||
response.headers["x-request-id"] = request.id
|
||||
|
||||
client = ReusableClient(app, port=1234)
|
||||
bad_headers = {"bad": "bad" * 5_000}
|
||||
|
||||
with client:
|
||||
_, response1 = client.get("/")
|
||||
_, response2 = client.get("/", headers=bad_headers)
|
||||
|
||||
assert response1.status == 200
|
||||
assert response2.status == 413
|
||||
assert (
|
||||
response1.headers["x-request-id"] != response2.headers["x-request-id"]
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
from urllib.parse import quote, unquote
|
||||
from urllib.parse import quote
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ try:
|
||||
except ImportError:
|
||||
flags = 0
|
||||
|
||||
TIMER_DELAY = 2
|
||||
|
||||
|
||||
def terminate(proc):
|
||||
if flags:
|
||||
@@ -56,6 +58,40 @@ 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()
|
||||
@@ -90,9 +126,10 @@ async def test_reloader_live(runargs, mode):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
filename = os.path.join(tmpdir, "reloader.py")
|
||||
text = write_app(filename, **runargs)
|
||||
proc = Popen(argv[mode], cwd=tmpdir, stdout=PIPE, creationflags=flags)
|
||||
command = argv[mode]
|
||||
proc = Popen(command, cwd=tmpdir, stdout=PIPE, creationflags=flags)
|
||||
try:
|
||||
timeout = Timer(5, terminate, [proc])
|
||||
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?)
|
||||
@@ -107,3 +144,40 @@ 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)
|
||||
|
||||
@@ -20,6 +20,7 @@ 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
|
||||
@@ -28,6 +29,7 @@ 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)
|
||||
|
||||
@@ -104,7 +106,7 @@ def test_route_assigned_to_request(app):
|
||||
return response.empty()
|
||||
|
||||
request, _ = app.test_client.get("/")
|
||||
assert request.route is list(app.router.routes.values())[0]
|
||||
assert request.route is list(app.router.routes)[0]
|
||||
|
||||
|
||||
def test_protocol_attribute(app):
|
||||
@@ -120,3 +122,21 @@ 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"
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
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
|
||||
|
||||
@@ -253,6 +253,31 @@ async def test_empty_json_asgi(app):
|
||||
assert response.body == b"null"
|
||||
|
||||
|
||||
def test_echo_json(app):
|
||||
@app.post("/")
|
||||
async def handler(request):
|
||||
return json(request.json)
|
||||
|
||||
data = {"foo": "bar"}
|
||||
request, response = app.test_client.post("/", json=data)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.json == data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_echo_json_asgi(app):
|
||||
@app.post("/")
|
||||
async def handler(request):
|
||||
return json(request.json)
|
||||
|
||||
data = {"foo": "bar"}
|
||||
request, response = await app.asgi_client.post("/", json=data)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.json == data
|
||||
|
||||
|
||||
def test_invalid_json(app):
|
||||
@app.post("/")
|
||||
async def handler(request):
|
||||
@@ -292,6 +317,17 @@ 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")
|
||||
|
||||
request, response = app.test_client.get("/", params=[("test1", "1")])
|
||||
|
||||
assert request.args.pop("test1") == ["1"]
|
||||
assert "test1" not in request.args
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_string_asgi(app):
|
||||
@app.route("/")
|
||||
@@ -2159,3 +2195,70 @@ def test_safe_method_with_body(app):
|
||||
assert request.body == data.encode("utf-8")
|
||||
assert request.json.get("test") == "OK"
|
||||
assert response.body == b"OK"
|
||||
|
||||
|
||||
def test_conflicting_body_methods_overload(app):
|
||||
@app.put("/")
|
||||
@app.put("/p/")
|
||||
@app.put("/p/<foo>")
|
||||
async def put(request, foo=None):
|
||||
return json(
|
||||
{"name": request.route.name, "body": str(request.body), "foo": foo}
|
||||
)
|
||||
|
||||
@app.delete("/p/<foo>")
|
||||
async def delete(request, foo):
|
||||
return json(
|
||||
{"name": request.route.name, "body": str(request.body), "foo": foo}
|
||||
)
|
||||
|
||||
payload = {"test": "OK"}
|
||||
data = str(json_dumps(payload).encode())
|
||||
|
||||
_, response = app.test_client.put("/", json=payload)
|
||||
assert response.status == 200
|
||||
assert response.json == {
|
||||
"name": "test_conflicting_body_methods_overload.put",
|
||||
"foo": None,
|
||||
"body": data,
|
||||
}
|
||||
_, response = app.test_client.put("/p", json=payload)
|
||||
assert response.status == 200
|
||||
assert response.json == {
|
||||
"name": "test_conflicting_body_methods_overload.put",
|
||||
"foo": None,
|
||||
"body": data,
|
||||
}
|
||||
_, response = app.test_client.put("/p/test", json=payload)
|
||||
assert response.status == 200
|
||||
assert response.json == {
|
||||
"name": "test_conflicting_body_methods_overload.put",
|
||||
"foo": "test",
|
||||
"body": data,
|
||||
}
|
||||
_, response = app.test_client.delete("/p/test")
|
||||
assert response.status == 200
|
||||
assert response.json == {
|
||||
"name": "test_conflicting_body_methods_overload.delete",
|
||||
"foo": "test",
|
||||
"body": str("".encode()),
|
||||
}
|
||||
|
||||
|
||||
def test_handler_overload(app):
|
||||
@app.get("/long/sub/route/param_a/<param_a:str>/param_b/<param_b:str>")
|
||||
@app.post("/long/sub/route/")
|
||||
def handler(request, **kwargs):
|
||||
return json(kwargs)
|
||||
|
||||
_, response = app.test_client.get(
|
||||
"/long/sub/route/param_a/foo/param_b/bar"
|
||||
)
|
||||
assert response.status == 200
|
||||
assert response.json == {
|
||||
"param_a": "foo",
|
||||
"param_b": "bar",
|
||||
}
|
||||
_, response = app.test_client.post("/long/sub/route")
|
||||
assert response.status == 200
|
||||
assert response.json == {}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
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,
|
||||
@@ -26,7 +22,6 @@ from sanic.response import (
|
||||
stream,
|
||||
text,
|
||||
)
|
||||
from sanic.server import HttpProtocol
|
||||
|
||||
|
||||
JSON_DATA = {"ok": True}
|
||||
@@ -65,7 +60,9 @@ def test_method_not_allowed():
|
||||
}
|
||||
|
||||
request, response = app.test_client.post("/")
|
||||
assert set(response.headers["Allow"].split(", ")) == {"GET", "HEAD"}
|
||||
assert set(response.headers["Allow"].split(", ")) == {
|
||||
"GET",
|
||||
}
|
||||
|
||||
app.router.reset()
|
||||
|
||||
@@ -78,7 +75,6 @@ def test_method_not_allowed():
|
||||
assert set(response.headers["Allow"].split(", ")) == {
|
||||
"GET",
|
||||
"POST",
|
||||
"HEAD",
|
||||
}
|
||||
assert response.headers["Content-Length"] == "0"
|
||||
|
||||
@@ -87,7 +83,6 @@ def test_method_not_allowed():
|
||||
assert set(response.headers["Allow"].split(", ")) == {
|
||||
"GET",
|
||||
"POST",
|
||||
"HEAD",
|
||||
}
|
||||
assert response.headers["Content-Length"] == "0"
|
||||
|
||||
@@ -229,7 +224,6 @@ def non_chunked_streaming_app(app):
|
||||
sample_streaming_fn,
|
||||
headers={"Content-Length": "7"},
|
||||
content_type="text/csv",
|
||||
chunked=False,
|
||||
)
|
||||
|
||||
return app
|
||||
@@ -256,11 +250,7 @@ async def test_chunked_streaming_returns_correct_content_asgi(streaming_app):
|
||||
|
||||
|
||||
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
|
||||
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]
|
||||
request, response = non_chunked_streaming_app.test_client.get("/")
|
||||
|
||||
assert "Transfer-Encoding" not in response.headers
|
||||
assert response.headers["Content-Type"] == "text/csv"
|
||||
@@ -534,3 +524,19 @@ 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
|
||||
|
||||
@@ -258,7 +258,7 @@ def test_route_strict_slash(app):
|
||||
def test_route_invalid_parameter_syntax(app):
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
@app.get("/get/<:string>", strict_slashes=True)
|
||||
@app.get("/get/<:str>", 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:string>")
|
||||
@app.route("/folder/<name:str>")
|
||||
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:number>")
|
||||
@app.route("/weight/<weight:float>")
|
||||
async def handler(request, weight):
|
||||
results.append(weight)
|
||||
return text("OK")
|
||||
@@ -543,9 +543,6 @@ def test_dynamic_route_regex(app):
|
||||
async def handler(request, folder_id):
|
||||
return text("OK")
|
||||
|
||||
app.router.finalize()
|
||||
print(app.router.find_route_src)
|
||||
|
||||
request, response = app.test_client.get("/folder/test")
|
||||
assert response.status == 200
|
||||
|
||||
@@ -587,6 +584,8 @@ def test_dynamic_route_path(app):
|
||||
async def handler(request, path):
|
||||
return text("OK")
|
||||
|
||||
app.router.finalize()
|
||||
|
||||
request, response = app.test_client.get("/path/1/info")
|
||||
assert response.status == 200
|
||||
|
||||
@@ -824,7 +823,7 @@ def test_dynamic_add_route_string(app):
|
||||
results.append(name)
|
||||
return text("OK")
|
||||
|
||||
app.add_route(handler, "/folder/<name:string>")
|
||||
app.add_route(handler, "/folder/<name:str>")
|
||||
request, response = app.test_client.get("/folder/test123")
|
||||
|
||||
assert response.text == "OK"
|
||||
@@ -860,7 +859,7 @@ def test_dynamic_add_route_number(app):
|
||||
results.append(weight)
|
||||
return text("OK")
|
||||
|
||||
app.add_route(handler, "/weight/<weight:number>")
|
||||
app.add_route(handler, "/weight/<weight:float>")
|
||||
|
||||
request, response = app.test_client.get("/weight/12345")
|
||||
assert response.text == "OK"
|
||||
@@ -1008,14 +1007,8 @@ def test_unmergeable_overload_routes(app):
|
||||
async def handler2(request):
|
||||
return text("OK1")
|
||||
|
||||
assert (
|
||||
len(
|
||||
dict(list(app.router.static_routes.values())[0].handlers)[
|
||||
"overload_whole"
|
||||
]
|
||||
)
|
||||
== 3
|
||||
)
|
||||
assert len(app.router.static_routes) == 1
|
||||
assert len(app.router.static_routes[("overload_whole",)].methods) == 3
|
||||
|
||||
request, response = app.test_client.get("/overload_whole")
|
||||
assert response.text == "OK1"
|
||||
@@ -1073,7 +1066,8 @@ 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 == 405
|
||||
assert response.status == 200
|
||||
assert response.json == {"ad_id": "1234"}
|
||||
|
||||
request, response = app.test_client.post("/ads/post")
|
||||
assert response.status == 200
|
||||
|
||||
@@ -28,7 +28,8 @@ def test_add_signal_decorator(app):
|
||||
async def async_signal(*_):
|
||||
...
|
||||
|
||||
assert len(app.signal_router.routes) == 1
|
||||
assert len(app.signal_router.routes) == 2
|
||||
assert len(app.signal_router.dynamic_routes) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -79,13 +80,13 @@ async def test_dispatch_signal_triggers_triggers_event(app):
|
||||
def sync_signal(*args):
|
||||
nonlocal app
|
||||
nonlocal counter
|
||||
signal, *_ = app.signal_router.get("foo.bar.baz")
|
||||
counter += signal.ctx.event.is_set()
|
||||
group, *_ = app.signal_router.get("foo.bar.baz")
|
||||
for signal in group:
|
||||
counter += signal.ctx.event.is_set()
|
||||
|
||||
app.signal_router.finalize()
|
||||
|
||||
await app.dispatch("foo.bar.baz")
|
||||
signal, *_ = app.signal_router.get("foo.bar.baz")
|
||||
|
||||
assert counter == 1
|
||||
|
||||
@@ -224,7 +225,7 @@ async def test_dispatch_signal_triggers_event_on_bp(app):
|
||||
|
||||
app.blueprint(bp)
|
||||
app.signal_router.finalize()
|
||||
signal, *_ = app.signal_router.get(
|
||||
signal_group, *_ = app.signal_router.get(
|
||||
"foo.bar.baz", condition={"blueprint": "bp"}
|
||||
)
|
||||
|
||||
@@ -233,7 +234,8 @@ async def test_dispatch_signal_triggers_event_on_bp(app):
|
||||
assert isawaitable(waiter)
|
||||
|
||||
fut = asyncio.ensure_future(do_wait())
|
||||
signal.ctx.event.set()
|
||||
for signal in signal_group:
|
||||
signal.ctx.event.set()
|
||||
await fut
|
||||
|
||||
assert bp_counter == 1
|
||||
@@ -255,17 +257,60 @@ def test_bad_finalize(app):
|
||||
assert counter == 0
|
||||
|
||||
|
||||
def test_event_not_exist(app):
|
||||
@pytest.mark.asyncio
|
||||
async def test_event_not_exist(app):
|
||||
with pytest.raises(NotFound, match="Could not find signal does.not.exist"):
|
||||
app.event("does.not.exist")
|
||||
await app.event("does.not.exist")
|
||||
|
||||
|
||||
def test_event_not_exist_on_bp(app):
|
||||
@pytest.mark.asyncio
|
||||
async 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"):
|
||||
bp.event("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")
|
||||
|
||||
|
||||
def test_event_on_bp_not_registered():
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from time import gmtime, strftime
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import text
|
||||
from sanic.exceptions import FileNotFound
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def static_file_directory():
|
||||
@@ -454,3 +459,51 @@ def test_nested_dir(app, static_file_directory):
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == "foo\n"
|
||||
|
||||
|
||||
def test_stack_trace_on_not_found(app, static_file_directory, caplog):
|
||||
app.static("/static", static_file_directory)
|
||||
|
||||
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])
|
||||
|
||||
assert response.status == 404
|
||||
assert counter[logging.INFO] == 5
|
||||
assert counter[logging.ERROR] == 0
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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])
|
||||
|
||||
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):
|
||||
app.static("/file", get_file_path(static_file_directory, "test.file"))
|
||||
app.static("/png", get_file_path(static_file_directory, "python.png"))
|
||||
|
||||
_, response = app.test_client.get("/file")
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(
|
||||
static_file_directory, "test.file"
|
||||
)
|
||||
|
||||
_, response = app.test_client.get("/png")
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(
|
||||
static_file_directory, "python.png"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
from time import monotonic as current_time
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
@@ -175,6 +176,10 @@ 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
|
||||
|
||||
@@ -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:number>"
|
||||
"<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:float>"
|
||||
)
|
||||
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:number>/another-word")
|
||||
@app.route("path/<possibly_neg:float>/another-word")
|
||||
def good(request, possibly_neg):
|
||||
assert isinstance(possibly_neg, (int, float))
|
||||
return text(f"this should pass with `{possibly_neg}`")
|
||||
|
||||
141
tests/test_versioning.py
Normal file
141
tests/test_versioning.py
Normal file
@@ -0,0 +1,141 @@
|
||||
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
|
||||
@@ -77,6 +77,56 @@ 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")
|
||||
|
||||
@@ -218,15 +268,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)
|
||||
@@ -244,3 +294,12 @@ 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
28
tox.ini
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = py37, py38, py39, pyNightly, {py37,py38,py39,pyNightly}-no-ext, lint, check, security, docs
|
||||
envlist = py37, py38, py39, pyNightly, pypy37, {py37,py38,py39,pyNightly,pypy37}-no-ext, lint, check, security, docs, type-checking
|
||||
|
||||
[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
|
||||
sanic-testing>=0.6.0
|
||||
coverage==5.3
|
||||
pytest==5.2.1
|
||||
pytest-cov
|
||||
@@ -18,7 +18,7 @@ deps =
|
||||
beautifulsoup4
|
||||
gunicorn==20.0.4
|
||||
uvicorn
|
||||
websockets>=8.1,<9.0
|
||||
websockets>=9.0
|
||||
commands =
|
||||
pytest {posargs:tests --cov sanic}
|
||||
- coverage combine --append
|
||||
@@ -39,7 +39,8 @@ commands =
|
||||
|
||||
[testenv:type-checking]
|
||||
deps =
|
||||
mypy
|
||||
mypy>=0.901
|
||||
types-ujson
|
||||
|
||||
commands =
|
||||
mypy sanic
|
||||
@@ -75,6 +76,23 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user