Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
93a0246c03 | ||
|
|
dfd1787a49 | ||
|
|
4998fd54c0 | ||
|
|
7be5f0ed3d | ||
|
|
938d2b5923 | ||
|
|
13630a79ad |
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]
|
[run]
|
||||||
branch = True
|
branch = True
|
||||||
source = sanic
|
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]
|
[html]
|
||||||
directory = coverage
|
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"
|
name: "CodeQL"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ main ]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
branches: [ main ]
|
||||||
branches: [ master ]
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '25 16 * * 0'
|
- cron: '25 16 * * 0'
|
||||||
|
|
||||||
@@ -29,39 +17,18 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'python' ]
|
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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
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
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
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
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
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.*
|
||||||
coverage
|
coverage
|
||||||
|
coverage.xml
|
||||||
.tox
|
.tox
|
||||||
settings.py
|
settings.py
|
||||||
.idea/*
|
.idea/*
|
||||||
@@ -18,3 +19,6 @@ build/*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist/*
|
dist/*
|
||||||
pip-wheel-metadata/
|
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"
|
|
||||||
113
CHANGELOG.rst
113
CHANGELOG.rst
@@ -1,3 +1,116 @@
|
|||||||
|
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
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
|
||||||
|
Disable response timeout on websocket connections
|
||||||
|
|
||||||
|
* `#2085 <https://github.com/sanic-org/sanic/pull/2085>`_
|
||||||
|
Make sure that blueprints with no slash is maintained when applied
|
||||||
|
|
||||||
|
Version 21.3.1
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
|
||||||
|
Static files inside subfolders are not accessible (404)
|
||||||
|
|
||||||
Version 21.3.0
|
Version 21.3.0
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ Permform ``flake8``\ , ``black`` and ``isort`` checks.
|
|||||||
tox -e lint
|
tox -e lint
|
||||||
|
|
||||||
Run type annotation checks
|
Run type annotation checks
|
||||||
---------------
|
--------------------------
|
||||||
|
|
||||||
``tox`` environment -> ``[testenv:type-checking]``
|
``tox`` environment -> ``[testenv:type-checking]``
|
||||||
|
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -49,6 +49,9 @@ test: clean
|
|||||||
test-coverage: clean
|
test-coverage: clean
|
||||||
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
|
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
|
||||||
|
|
||||||
|
view-coverage:
|
||||||
|
sanic ./coverage --simple
|
||||||
|
|
||||||
install:
|
install:
|
||||||
python setup.py install
|
python setup.py install
|
||||||
|
|
||||||
@@ -85,8 +88,7 @@ docs-test: docs-clean
|
|||||||
cd docs && make dummy
|
cd docs && make dummy
|
||||||
|
|
||||||
docs-serve:
|
docs-serve:
|
||||||
# python -m http.server --directory=./docs/_build/html 9999
|
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./
|
||||||
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic
|
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
python scripts/changelog.py
|
python scripts/changelog.py
|
||||||
|
|||||||
12
README.rst
12
README.rst
@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
|
|||||||
:stub-columns: 1
|
:stub-columns: 1
|
||||||
|
|
||||||
* - Build
|
* - Build
|
||||||
- | |Build Status| |AppVeyor Build Status| |Codecov|
|
- | |Py39Test| |Py38Test| |Py37Test| |Codecov|
|
||||||
* - Docs
|
* - Docs
|
||||||
- | |UserGuide| |Documentation|
|
- | |UserGuide| |Documentation|
|
||||||
* - Package
|
* - Package
|
||||||
@@ -29,10 +29,12 @@ Sanic | Build fast. Run fast.
|
|||||||
:target: https://discord.gg/FARQzAEMAA
|
:target: https://discord.gg/FARQzAEMAA
|
||||||
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
|
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/sanic-org/sanic
|
:target: https://codecov.io/gh/sanic-org/sanic
|
||||||
.. |Build Status| image:: https://travis-ci.com/sanic-org/sanic.svg?branch=master
|
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
|
||||||
:target: https://travis-ci.com/sanic-org/sanic
|
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
|
||||||
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
|
||||||
:target: https://ci.appveyor.com/project/sanic-org/sanic
|
: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
|
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||||
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
||||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
.. |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 \
|
FROM sanicframework/sanic-build:${BASE_IMAGE_TAG}
|
||||||
curl \
|
|
||||||
bash \
|
|
||||||
build-base \
|
|
||||||
ca-certificates \
|
|
||||||
git \
|
|
||||||
bzip2-dev \
|
|
||||||
linux-headers \
|
|
||||||
ncurses-dev \
|
|
||||||
openssl \
|
|
||||||
openssl-dev \
|
|
||||||
readline-dev \
|
|
||||||
sqlite-dev
|
|
||||||
|
|
||||||
|
RUN apk update
|
||||||
RUN update-ca-certificates
|
RUN update-ca-certificates
|
||||||
RUN rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
ENV PYENV_ROOT="/root/.pyenv"
|
RUN pip install sanic
|
||||||
ENV PATH="$PYENV_ROOT/bin:$PATH"
|
RUN apk del build-base
|
||||||
|
|
||||||
ADD . /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN /app/docker/bin/install_python.sh 3.5.4 3.6.4
|
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/bin/entrypoint.sh"]
|
|
||||||
|
|||||||
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
|
📑 API Reference
|
||||||
================
|
================
|
||||||
|
|
||||||
sanic.app
|
.. toctree::
|
||||||
---------
|
:maxdepth: 2
|
||||||
|
|
||||||
.. automodule:: sanic.app
|
api/app
|
||||||
:members:
|
api/blueprints
|
||||||
:show-inheritance:
|
api/core
|
||||||
:inherited-members:
|
api/exceptions
|
||||||
|
api/router
|
||||||
sanic.blueprints
|
api/server
|
||||||
----------------
|
api/utility
|
||||||
|
|
||||||
.. 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:
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
♥️ Contributing
|
♥️ Contributing
|
||||||
===============
|
==============
|
||||||
|
|
||||||
.. include:: ../../CONTRIBUTING.rst
|
.. include:: ../../CONTRIBUTING.rst
|
||||||
|
|||||||
BIN
examples/static/favicon.ico
Normal file
BIN
examples/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
examples/static/images/logo.png
Normal file
BIN
examples/static/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
2
examples/static/robots.txt
Normal file
2
examples/static/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
6
examples/static_assets.py
Normal file
6
examples/static_assets.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
app.static("/", "./static")
|
||||||
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.__version__ import __version__
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
|
from sanic.constants import HTTPMethod
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import HTTPResponse, html, json, text
|
from sanic.response import HTTPResponse, html, json, text
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ __all__ = (
|
|||||||
"__version__",
|
"__version__",
|
||||||
"Sanic",
|
"Sanic",
|
||||||
"Blueprint",
|
"Blueprint",
|
||||||
|
"HTTPMethod",
|
||||||
"HTTPResponse",
|
"HTTPResponse",
|
||||||
"Request",
|
"Request",
|
||||||
"html",
|
"html",
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from sanic_routing import __version__ as __routing_version__ # type: ignore
|
||||||
|
|
||||||
from sanic import __version__
|
from sanic import __version__
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.config import BASE_LOGO
|
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):
|
class SanicArgumentParser(ArgumentParser):
|
||||||
def add_bool_arguments(self, *args, **kwargs):
|
def add_bool_arguments(self, *args, **kwargs):
|
||||||
group = self.add_mutually_exclusive_group()
|
group = self.add_mutually_exclusive_group()
|
||||||
group.add_argument(*args, action="store_true", **kwargs)
|
group.add_argument(*args, action="store_true", **kwargs)
|
||||||
kwargs["help"] = "no " + kwargs["help"]
|
kwargs["help"] = f"no {kwargs['help']}\n "
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||||
)
|
)
|
||||||
@@ -25,7 +29,30 @@ def main():
|
|||||||
parser = SanicArgumentParser(
|
parser = SanicArgumentParser(
|
||||||
prog="sanic",
|
prog="sanic",
|
||||||
description=BASE_LOGO,
|
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(
|
parser.add_argument(
|
||||||
"-H",
|
"-H",
|
||||||
@@ -33,7 +60,7 @@ def main():
|
|||||||
dest="host",
|
dest="host",
|
||||||
type=str,
|
type=str,
|
||||||
default="127.0.0.1",
|
default="127.0.0.1",
|
||||||
help="host address [default 127.0.0.1]",
|
help="Host address [default 127.0.0.1]",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p",
|
"-p",
|
||||||
@@ -41,7 +68,7 @@ def main():
|
|||||||
dest="port",
|
dest="port",
|
||||||
type=int,
|
type=int,
|
||||||
default=8000,
|
default=8000,
|
||||||
help="port to serve on [default 8000]",
|
help="Port to serve on [default 8000]",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-u",
|
"-u",
|
||||||
@@ -49,13 +76,16 @@ def main():
|
|||||||
dest="unix",
|
dest="unix",
|
||||||
type=str,
|
type=str,
|
||||||
default="",
|
default="",
|
||||||
help="location of unix socket",
|
help="location of unix socket\n ",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
"--cert", dest="cert", type=str, help="Location of certificate for SSL"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--key", dest="key", type=str, help="location of keyfile for SSL."
|
"--key", dest="key", type=str, help="location of keyfile for SSL\n "
|
||||||
|
)
|
||||||
|
parser.add_bool_arguments(
|
||||||
|
"--access-logs", dest="access_log", help="display access logs"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-w",
|
"-w",
|
||||||
@@ -63,20 +93,31 @@ def main():
|
|||||||
dest="workers",
|
dest="workers",
|
||||||
type=int,
|
type=int,
|
||||||
default=1,
|
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_argument("-d", "--debug", dest="debug", action="store_true")
|
||||||
parser.add_bool_arguments(
|
parser.add_argument(
|
||||||
"--access-logs", dest="access_log", help="display access logs"
|
"-r",
|
||||||
|
"--reload",
|
||||||
|
"--auto-reload",
|
||||||
|
dest="auto_reload",
|
||||||
|
action="store_true",
|
||||||
|
help="Watch source directory for file changes and reload on changes",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-v",
|
"-R",
|
||||||
"--version",
|
"--reload-dir",
|
||||||
action="version",
|
dest="path",
|
||||||
version=f"Sanic {__version__}",
|
action="append",
|
||||||
|
help="Extra directories to watch and reload on changes\n ",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -85,47 +126,71 @@ def main():
|
|||||||
if module_path not in sys.path:
|
if module_path not in sys.path:
|
||||||
sys.path.append(module_path)
|
sys.path.append(module_path)
|
||||||
|
|
||||||
if ":" in args.module:
|
if args.simple:
|
||||||
module_name, app_name = args.module.rsplit(":", 1)
|
path = Path(args.module)
|
||||||
|
app = create_simple_server(path)
|
||||||
else:
|
else:
|
||||||
module_parts = args.module.split(".")
|
delimiter = ":" if ":" in args.module else "."
|
||||||
module_name = ".".join(module_parts[:-1])
|
module_name, app_name = args.module.rsplit(delimiter, 1)
|
||||||
app_name = module_parts[-1]
|
|
||||||
|
|
||||||
module = import_module(module_name)
|
if app_name.endswith("()"):
|
||||||
app = getattr(module, app_name, None)
|
args.factory = True
|
||||||
app_name = type(app).__name__
|
app_name = app_name[:-2]
|
||||||
|
|
||||||
if not isinstance(app, Sanic):
|
module = import_module(module_name)
|
||||||
raise ValueError(
|
app = getattr(module, app_name, None)
|
||||||
f"Module is not a Sanic app, it is a {app_name}. "
|
if args.factory:
|
||||||
f"Perhaps you meant {args.module}.app?"
|
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:
|
if args.cert is not None or args.key is not None:
|
||||||
ssl = {
|
ssl: Optional[Dict[str, Any]] = {
|
||||||
"cert": args.cert,
|
"cert": args.cert,
|
||||||
"key": args.key,
|
"key": args.key,
|
||||||
} # type: Optional[Dict[str, Any]]
|
}
|
||||||
else:
|
else:
|
||||||
ssl = None
|
ssl = None
|
||||||
|
|
||||||
app.run(
|
kwargs = {
|
||||||
host=args.host,
|
"host": args.host,
|
||||||
port=args.port,
|
"port": args.port,
|
||||||
unix=args.unix,
|
"unix": args.unix,
|
||||||
workers=args.workers,
|
"workers": args.workers,
|
||||||
debug=args.debug,
|
"debug": args.debug,
|
||||||
access_log=args.access_log,
|
"access_log": args.access_log,
|
||||||
ssl=ssl,
|
"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:
|
except ImportError as e:
|
||||||
logger.error(
|
if module_name.startswith(e.name):
|
||||||
f"No module named {e.name} found.\n"
|
error_logger.error(
|
||||||
f" Example File: project/sanic_server.py -> app\n"
|
f"No module named {e.name} found.\n"
|
||||||
f" Example Module: project.sanic_server.app"
|
" Example File: project/sanic_server.py -> app\n"
|
||||||
)
|
" Example Module: project.sanic_server.app"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.exception("Failed to run app")
|
error_logger.exception("Failed to run app")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "21.3.0"
|
__version__ = "21.6.0"
|
||||||
|
|||||||
126
sanic/app.py
126
sanic/app.py
@@ -14,6 +14,7 @@ from asyncio.futures import Future
|
|||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
|
from pathlib import Path
|
||||||
from socket import socket
|
from socket import socket
|
||||||
from ssl import Purpose, SSLContext, create_default_context
|
from ssl import Purpose, SSLContext, create_default_context
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
@@ -43,7 +44,7 @@ from sanic.asgi import ASGIApp
|
|||||||
from sanic.base import BaseSanic
|
from sanic.base import BaseSanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.blueprints import Blueprint
|
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 (
|
from sanic.exceptions import (
|
||||||
InvalidUsage,
|
InvalidUsage,
|
||||||
SanicException,
|
SanicException,
|
||||||
@@ -78,6 +79,7 @@ class Sanic(BaseSanic):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__fake_slots__ = (
|
__fake_slots__ = (
|
||||||
|
"_asgi_app",
|
||||||
"_app_registry",
|
"_app_registry",
|
||||||
"_asgi_client",
|
"_asgi_client",
|
||||||
"_blueprint_order",
|
"_blueprint_order",
|
||||||
@@ -89,6 +91,7 @@ class Sanic(BaseSanic):
|
|||||||
"_future_signals",
|
"_future_signals",
|
||||||
"_test_client",
|
"_test_client",
|
||||||
"_test_manager",
|
"_test_manager",
|
||||||
|
"auto_reload",
|
||||||
"asgi",
|
"asgi",
|
||||||
"blueprints",
|
"blueprints",
|
||||||
"config",
|
"config",
|
||||||
@@ -103,6 +106,7 @@ class Sanic(BaseSanic):
|
|||||||
"name",
|
"name",
|
||||||
"named_request_middleware",
|
"named_request_middleware",
|
||||||
"named_response_middleware",
|
"named_response_middleware",
|
||||||
|
"reload_dirs",
|
||||||
"request_class",
|
"request_class",
|
||||||
"request_middleware",
|
"request_middleware",
|
||||||
"response_middleware",
|
"response_middleware",
|
||||||
@@ -121,10 +125,13 @@ class Sanic(BaseSanic):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
|
config: Optional[Config] = None,
|
||||||
|
ctx: Optional[Any] = None,
|
||||||
router: Optional[Router] = None,
|
router: Optional[Router] = None,
|
||||||
signal_router: Optional[SignalRouter] = None,
|
signal_router: Optional[SignalRouter] = None,
|
||||||
error_handler: Optional[ErrorHandler] = 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,
|
request_class: Optional[Type[Request]] = None,
|
||||||
strict_slashes: bool = False,
|
strict_slashes: bool = False,
|
||||||
log_config: Optional[Dict[str, Any]] = None,
|
log_config: Optional[Dict[str, Any]] = None,
|
||||||
@@ -132,34 +139,38 @@ class Sanic(BaseSanic):
|
|||||||
register: Optional[bool] = None,
|
register: Optional[bool] = None,
|
||||||
dumps: Optional[Callable[..., str]] = None,
|
dumps: Optional[Callable[..., str]] = None,
|
||||||
) -> 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
|
# logging
|
||||||
if configure_logging:
|
if configure_logging:
|
||||||
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
||||||
|
|
||||||
|
if 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._asgi_client = None
|
||||||
self._blueprint_order: List[Blueprint] = []
|
self._blueprint_order: List[Blueprint] = []
|
||||||
self._test_client = None
|
self._test_client = None
|
||||||
self._test_manager = None
|
self._test_manager = None
|
||||||
self.asgi = False
|
self.asgi = False
|
||||||
|
self.auto_reload = False
|
||||||
self.blueprints: Dict[str, Blueprint] = {}
|
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.configure_logging = configure_logging
|
||||||
self.ctx = SimpleNamespace()
|
self.ctx = ctx or SimpleNamespace()
|
||||||
self.debug = None
|
self.debug = None
|
||||||
self.error_handler = error_handler or ErrorHandler()
|
self.error_handler = error_handler or ErrorHandler()
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.is_stopping = False
|
self.is_stopping = False
|
||||||
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
|
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
|
||||||
self.name = name
|
|
||||||
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||||
self.named_response_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_class = request_class
|
||||||
self.request_middleware: Deque[MiddlewareType] = deque()
|
self.request_middleware: Deque[MiddlewareType] = deque()
|
||||||
self.response_middleware: Deque[MiddlewareType] = deque()
|
self.response_middleware: Deque[MiddlewareType] = deque()
|
||||||
@@ -175,7 +186,6 @@ class Sanic(BaseSanic):
|
|||||||
|
|
||||||
if register is not None:
|
if register is not None:
|
||||||
self.config.REGISTER = register
|
self.config.REGISTER = register
|
||||||
|
|
||||||
if self.config.REGISTER:
|
if self.config.REGISTER:
|
||||||
self.__class__.register_app(self)
|
self.__class__.register_app(self)
|
||||||
|
|
||||||
@@ -374,11 +384,19 @@ class Sanic(BaseSanic):
|
|||||||
condition=condition,
|
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)
|
signal = self.signal_router.name_index.get(event)
|
||||||
if not signal:
|
if not signal:
|
||||||
raise NotFound("Could not find signal %s" % event)
|
if self.config.EVENT_AUTOREGISTER:
|
||||||
return wait_for(signal.ctx.event.wait(), timeout=timeout)
|
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):
|
def enable_websocket(self, enable=True):
|
||||||
"""Enable or disable the support for websocket.
|
"""Enable or disable the support for websocket.
|
||||||
@@ -402,7 +420,33 @@ class Sanic(BaseSanic):
|
|||||||
"""
|
"""
|
||||||
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
||||||
for item in blueprint:
|
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
|
return
|
||||||
if blueprint.name in self.blueprints:
|
if blueprint.name in self.blueprints:
|
||||||
assert self.blueprints[blueprint.name] is blueprint, (
|
assert self.blueprints[blueprint.name] is blueprint, (
|
||||||
@@ -567,7 +611,12 @@ class Sanic(BaseSanic):
|
|||||||
# determine if the parameter supplied by the caller
|
# determine if the parameter supplied by the caller
|
||||||
# passes the test in the URL
|
# passes the test in the URL
|
||||||
if param_info.pattern:
|
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 not passes_pattern:
|
||||||
if param_info.cast != str:
|
if param_info.cast != str:
|
||||||
msg = (
|
msg = (
|
||||||
@@ -575,13 +624,13 @@ class Sanic(BaseSanic):
|
|||||||
f"for parameter `{param_info.name}` does "
|
f"for parameter `{param_info.name}` does "
|
||||||
"not match pattern for type "
|
"not match pattern for type "
|
||||||
f"`{param_info.cast.__name__}`: "
|
f"`{param_info.cast.__name__}`: "
|
||||||
f"{param_info.pattern.pattern}"
|
f"{pattern.pattern}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = (
|
msg = (
|
||||||
f'Value "{supplied_param}" for parameter '
|
f'Value "{supplied_param}" for parameter '
|
||||||
f"`{param_info.name}` does not satisfy "
|
f"`{param_info.name}` does not satisfy "
|
||||||
f"pattern {param_info.pattern.pattern}"
|
f"pattern {pattern.pattern}"
|
||||||
)
|
)
|
||||||
raise URLBuildError(msg)
|
raise URLBuildError(msg)
|
||||||
|
|
||||||
@@ -664,11 +713,6 @@ class Sanic(BaseSanic):
|
|||||||
exception handling must be done here
|
exception handling must be done here
|
||||||
|
|
||||||
:param request: HTTP Request object
|
: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
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
# Define `response` var here to remove warnings about
|
# Define `response` var here to remove warnings about
|
||||||
@@ -677,7 +721,9 @@ class Sanic(BaseSanic):
|
|||||||
try:
|
try:
|
||||||
# Fetch handler from router
|
# Fetch handler from router
|
||||||
route, handler, kwargs = self.router.get(
|
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
|
request._match_info = kwargs
|
||||||
@@ -725,17 +771,14 @@ class Sanic(BaseSanic):
|
|||||||
|
|
||||||
if response:
|
if response:
|
||||||
response = await request.respond(response)
|
response = await request.respond(response)
|
||||||
else:
|
elif not hasattr(handler, "is_websocket"):
|
||||||
response = request.stream.response # type: ignore
|
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):
|
if isinstance(response, BaseHTTPResponse):
|
||||||
await response.send(end_stream=True)
|
await response.send(end_stream=True)
|
||||||
else:
|
else:
|
||||||
try:
|
if not hasattr(handler, "is_websocket"):
|
||||||
# Fastest method for checking if the property exists
|
|
||||||
handler.is_websocket # type: ignore
|
|
||||||
except AttributeError:
|
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
f"Invalid response type {response!r} "
|
f"Invalid response type {response!r} "
|
||||||
"(need HTTPResponse)"
|
"(need HTTPResponse)"
|
||||||
@@ -762,6 +805,7 @@ class Sanic(BaseSanic):
|
|||||||
|
|
||||||
if self.asgi:
|
if self.asgi:
|
||||||
ws = request.transport.get_websocket_connection()
|
ws = request.transport.get_websocket_connection()
|
||||||
|
await ws.accept(subprotocols)
|
||||||
else:
|
else:
|
||||||
protocol = request.transport.get_protocol()
|
protocol = request.transport.get_protocol()
|
||||||
protocol.app = self
|
protocol.app = self
|
||||||
@@ -834,6 +878,7 @@ class Sanic(BaseSanic):
|
|||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
loop: None = None,
|
loop: None = None,
|
||||||
|
reload_dir: Optional[Union[List[str], str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Run the HTTP Server and listen until keyboard interrupt or term
|
Run the HTTP Server and listen until keyboard interrupt or term
|
||||||
@@ -868,6 +913,18 @@ class Sanic(BaseSanic):
|
|||||||
:type unix: str
|
:type unix: str
|
||||||
:return: Nothing
|
: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:
|
if loop is not None:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"loop is not a valid argument. To use an existing loop, "
|
"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:
|
if auto_reload or auto_reload is None and debug:
|
||||||
|
self.auto_reload = True
|
||||||
if os.environ.get("SANIC_SERVER_RUNNING") != "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:
|
if sock is None:
|
||||||
host, port = host or "127.0.0.1", port or 8000
|
host, port = host or "127.0.0.1", port or 8000
|
||||||
@@ -1177,6 +1235,10 @@ class Sanic(BaseSanic):
|
|||||||
else:
|
else:
|
||||||
logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
|
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
|
return server_settings
|
||||||
|
|
||||||
def _build_endpoint_name(self, *parts):
|
def _build_endpoint_name(self, *parts):
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ class ASGIApp:
|
|||||||
instance.ws = instance.transport.create_websocket_connection(
|
instance.ws = instance.transport.create_websocket_connection(
|
||||||
send, receive
|
send, receive
|
||||||
)
|
)
|
||||||
await instance.ws.accept()
|
|
||||||
else:
|
else:
|
||||||
raise ServerError("Received unknown ASGI scope")
|
raise ServerError("Received unknown ASGI scope")
|
||||||
|
|
||||||
@@ -164,10 +163,12 @@ class ASGIApp:
|
|||||||
Read and stream the body in chunks from an incoming ASGI message.
|
Read and stream the body in chunks from an incoming ASGI message.
|
||||||
"""
|
"""
|
||||||
message = await self.transport.receive()
|
message = await self.transport.receive()
|
||||||
|
body = message.get("body", b"")
|
||||||
if not message.get("more_body", False):
|
if not message.get("more_body", False):
|
||||||
self.request_body = False
|
self.request_body = False
|
||||||
return None
|
if not body:
|
||||||
return message.get("body", b"")
|
return None
|
||||||
|
return body
|
||||||
|
|
||||||
async def __aiter__(self):
|
async def __aiter__(self):
|
||||||
while self.request_body:
|
while self.request_body:
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from typing import Any, Tuple
|
from typing import Any, Tuple
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
|
from sanic.exceptions import SanicException
|
||||||
from sanic.mixins.exceptions import ExceptionMixin
|
from sanic.mixins.exceptions import ExceptionMixin
|
||||||
from sanic.mixins.listeners import ListenerMixin
|
from sanic.mixins.listeners import ListenerMixin
|
||||||
from sanic.mixins.middleware import MiddlewareMixin
|
from sanic.mixins.middleware import MiddlewareMixin
|
||||||
@@ -8,6 +11,9 @@ from sanic.mixins.routes import RouteMixin
|
|||||||
from sanic.mixins.signals import SignalMixin
|
from sanic.mixins.signals import SignalMixin
|
||||||
|
|
||||||
|
|
||||||
|
VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$")
|
||||||
|
|
||||||
|
|
||||||
class BaseSanic(
|
class BaseSanic(
|
||||||
RouteMixin,
|
RouteMixin,
|
||||||
MiddlewareMixin,
|
MiddlewareMixin,
|
||||||
@@ -17,7 +23,25 @@ class BaseSanic(
|
|||||||
):
|
):
|
||||||
__fake_slots__: Tuple[str, ...]
|
__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__:
|
for base in BaseSanic.__bases__:
|
||||||
base.__init__(self, *args, **kwargs) # type: ignore
|
base.__init__(self, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
@@ -36,6 +60,7 @@ class BaseSanic(
|
|||||||
f"Setting variables on {self.__class__.__name__} instances is "
|
f"Setting variables on {self.__class__.__name__} instances is "
|
||||||
"deprecated and will be removed in version 21.9. You should "
|
"deprecated and will be removed in version 21.9. You should "
|
||||||
f"change your {self.__class__.__name__} instance to use "
|
f"change your {self.__class__.__name__} instance to use "
|
||||||
f"instance.ctx.{name} instead."
|
f"instance.ctx.{name} instead.",
|
||||||
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
super().__setattr__(name, value)
|
super().__setattr__(name, value)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
from collections.abc import MutableSequence
|
from __future__ import annotations
|
||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
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):
|
class BlueprintGroup(MutableSequence):
|
||||||
@@ -54,9 +58,21 @@ class BlueprintGroup(MutableSequence):
|
|||||||
app.blueprint(bpg)
|
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
|
Create a new Blueprint Group
|
||||||
|
|
||||||
@@ -65,13 +81,14 @@ class BlueprintGroup(MutableSequence):
|
|||||||
inherited by each of the Blueprint
|
inherited by each of the Blueprint
|
||||||
:param strict_slashes: URL Strict slash behavior indicator
|
:param strict_slashes: URL Strict slash behavior indicator
|
||||||
"""
|
"""
|
||||||
self._blueprints = []
|
self._blueprints: List[Blueprint] = []
|
||||||
self._url_prefix = url_prefix
|
self._url_prefix = url_prefix
|
||||||
self._version = version
|
self._version = version
|
||||||
|
self._version_prefix = version_prefix
|
||||||
self._strict_slashes = strict_slashes
|
self._strict_slashes = strict_slashes
|
||||||
|
|
||||||
@property
|
@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
|
Retrieve the URL prefix being used for the Current Blueprint Group
|
||||||
|
|
||||||
@@ -80,7 +97,7 @@ class BlueprintGroup(MutableSequence):
|
|||||||
return self._url_prefix
|
return self._url_prefix
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def blueprints(self) -> List["sanic.Blueprint"]:
|
def blueprints(self) -> List[Blueprint]:
|
||||||
"""
|
"""
|
||||||
Retrieve a list of all the available blueprints under this group.
|
Retrieve a list of all the available blueprints under this group.
|
||||||
|
|
||||||
@@ -107,6 +124,15 @@ class BlueprintGroup(MutableSequence):
|
|||||||
"""
|
"""
|
||||||
return self._strict_slashes
|
return self._strict_slashes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version_prefix(self) -> str:
|
||||||
|
"""
|
||||||
|
Version prefix; defaults to ``/v``
|
||||||
|
|
||||||
|
:return: str
|
||||||
|
"""
|
||||||
|
return self._version_prefix
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""
|
"""
|
||||||
Tun the class Blueprint Group into an Iterable item
|
Tun the class Blueprint Group into an Iterable item
|
||||||
@@ -161,34 +187,16 @@ class BlueprintGroup(MutableSequence):
|
|||||||
"""
|
"""
|
||||||
return len(self._blueprints)
|
return len(self._blueprints)
|
||||||
|
|
||||||
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint":
|
def append(self, value: Blueprint) -> None:
|
||||||
"""
|
|
||||||
Sanitize the Blueprint Entity to override the Version and strict slash
|
|
||||||
behaviors as required.
|
|
||||||
|
|
||||||
:param bp: Sanic Blueprint entity Object
|
|
||||||
:return: Modified Blueprint
|
|
||||||
"""
|
|
||||||
if self._url_prefix:
|
|
||||||
merged_prefix = "/".join(
|
|
||||||
u.strip("/") for u in [self._url_prefix, bp.url_prefix or ""]
|
|
||||||
).rstrip("/")
|
|
||||||
bp.url_prefix = f"/{merged_prefix}"
|
|
||||||
for _attr in ["version", "strict_slashes"]:
|
|
||||||
if getattr(bp, _attr) is None:
|
|
||||||
setattr(bp, _attr, getattr(self, _attr))
|
|
||||||
return bp
|
|
||||||
|
|
||||||
def append(self, value: "sanic.Blueprint") -> None:
|
|
||||||
"""
|
"""
|
||||||
The Abstract class `MutableSequence` leverages this append method to
|
The Abstract class `MutableSequence` leverages this append method to
|
||||||
perform the `BlueprintGroup.append` operation.
|
perform the `BlueprintGroup.append` operation.
|
||||||
:param value: New `Blueprint` object.
|
:param value: New `Blueprint` object.
|
||||||
:return: None
|
: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
|
The Abstract class `MutableSequence` leverages this insert method to
|
||||||
perform the `BlueprintGroup.append` operation.
|
perform the `BlueprintGroup.append` operation.
|
||||||
@@ -197,7 +205,7 @@ class BlueprintGroup(MutableSequence):
|
|||||||
:param item: New `Blueprint` object.
|
:param item: New `Blueprint` object.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._blueprints.insert(index, self._sanitize_blueprint(item))
|
self._blueprints.insert(index, item)
|
||||||
|
|
||||||
def middleware(self, *args, **kwargs):
|
def middleware(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -62,18 +62,20 @@ class Blueprint(BaseSanic):
|
|||||||
"strict_slashes",
|
"strict_slashes",
|
||||||
"url_prefix",
|
"url_prefix",
|
||||||
"version",
|
"version",
|
||||||
|
"version_prefix",
|
||||||
"websocket_routes",
|
"websocket_routes",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str = None,
|
||||||
url_prefix: Optional[str] = None,
|
url_prefix: Optional[str] = None,
|
||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__(name=name)
|
||||||
|
|
||||||
self._apps: Set[Sanic] = set()
|
self._apps: Set[Sanic] = set()
|
||||||
self.ctx = SimpleNamespace()
|
self.ctx = SimpleNamespace()
|
||||||
@@ -81,12 +83,16 @@ class Blueprint(BaseSanic):
|
|||||||
self.host = host
|
self.host = host
|
||||||
self.listeners: Dict[str, List[ListenerType]] = {}
|
self.listeners: Dict[str, List[ListenerType]] = {}
|
||||||
self.middlewares: List[MiddlewareType] = []
|
self.middlewares: List[MiddlewareType] = []
|
||||||
self.name = name
|
|
||||||
self.routes: List[Route] = []
|
self.routes: List[Route] = []
|
||||||
self.statics: List[RouteHandler] = []
|
self.statics: List[RouteHandler] = []
|
||||||
self.strict_slashes = strict_slashes
|
self.strict_slashes = strict_slashes
|
||||||
self.url_prefix = url_prefix
|
self.url_prefix = (
|
||||||
|
url_prefix[:-1]
|
||||||
|
if url_prefix and url_prefix.endswith("/")
|
||||||
|
else url_prefix
|
||||||
|
)
|
||||||
self.version = version
|
self.version = version
|
||||||
|
self.version_prefix = version_prefix
|
||||||
self.websocket_routes: List[Route] = []
|
self.websocket_routes: List[Route] = []
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@@ -139,7 +145,13 @@ class Blueprint(BaseSanic):
|
|||||||
return super().signal(event, *args, **kwargs)
|
return super().signal(event, *args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@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
|
Create a list of blueprints, optionally grouping them under a
|
||||||
general URL prefix.
|
general URL prefix.
|
||||||
@@ -156,8 +168,6 @@ class Blueprint(BaseSanic):
|
|||||||
for i in nested:
|
for i in nested:
|
||||||
if isinstance(i, (list, tuple)):
|
if isinstance(i, (list, tuple)):
|
||||||
yield from chain(i)
|
yield from chain(i)
|
||||||
elif isinstance(i, BlueprintGroup):
|
|
||||||
yield from i.blueprints
|
|
||||||
else:
|
else:
|
||||||
yield i
|
yield i
|
||||||
|
|
||||||
@@ -165,6 +175,7 @@ class Blueprint(BaseSanic):
|
|||||||
url_prefix=url_prefix,
|
url_prefix=url_prefix,
|
||||||
version=version,
|
version=version,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)
|
)
|
||||||
for bp in chain(blueprints):
|
for bp in chain(blueprints):
|
||||||
bps.append(bp)
|
bps.append(bp)
|
||||||
@@ -182,6 +193,9 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
self._apps.add(app)
|
self._apps.add(app)
|
||||||
url_prefix = options.get("url_prefix", self.url_prefix)
|
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 = []
|
routes = []
|
||||||
middleware = []
|
middleware = []
|
||||||
@@ -196,12 +210,22 @@ class Blueprint(BaseSanic):
|
|||||||
# Prepend the blueprint URI prefix if available
|
# Prepend the blueprint URI prefix if available
|
||||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||||
|
|
||||||
strict_slashes = (
|
version_prefix = self.version_prefix
|
||||||
self.strict_slashes
|
for prefix in (
|
||||||
if future.strict_slashes is None
|
future.version_prefix,
|
||||||
and self.strict_slashes is not None
|
opt_version_prefix,
|
||||||
else future.strict_slashes
|
):
|
||||||
|
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)
|
name = app._generate_name(future.name)
|
||||||
|
|
||||||
apply_route = FutureRoute(
|
apply_route = FutureRoute(
|
||||||
@@ -211,13 +235,14 @@ class Blueprint(BaseSanic):
|
|||||||
future.host or self.host,
|
future.host or self.host,
|
||||||
strict_slashes,
|
strict_slashes,
|
||||||
future.stream,
|
future.stream,
|
||||||
future.version or self.version,
|
version,
|
||||||
name,
|
name,
|
||||||
future.ignore_body,
|
future.ignore_body,
|
||||||
future.websocket,
|
future.websocket,
|
||||||
future.subprotocols,
|
future.subprotocols,
|
||||||
future.unquote,
|
future.unquote,
|
||||||
future.static,
|
future.static,
|
||||||
|
version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
route = app._apply_route(apply_route)
|
route = app._apply_route(apply_route)
|
||||||
@@ -254,8 +279,6 @@ class Blueprint(BaseSanic):
|
|||||||
app._apply_signal(signal)
|
app._apply_signal(signal)
|
||||||
|
|
||||||
self.routes = [route for route in routes if isinstance(route, Route)]
|
self.routes = [route for route in routes if isinstance(route, Route)]
|
||||||
|
|
||||||
# Deprecate these in 21.6
|
|
||||||
self.websocket_routes = [
|
self.websocket_routes = [
|
||||||
route for route in self.routes if route.ctx.websocket
|
route for route in self.routes if route.ctx.websocket
|
||||||
]
|
]
|
||||||
@@ -284,3 +307,12 @@ class Blueprint(BaseSanic):
|
|||||||
return_when=asyncio.FIRST_COMPLETED,
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
timeout=timeout,
|
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 inspect import isclass
|
||||||
from os import environ
|
from os import environ
|
||||||
from pathlib import Path
|
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
|
from .utils import load_module_from_file_location, str_to_bool
|
||||||
|
|
||||||
@@ -15,33 +18,64 @@ BASE_LOGO = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
"ACCESS_LOG": True,
|
||||||
"REQUEST_BUFFER_QUEUE_SIZE": 100,
|
"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_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
|
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||||
"RESPONSE_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_QUEUE": 32,
|
||||||
|
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
||||||
|
"WEBSOCKET_PING_INTERVAL": 20,
|
||||||
|
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||||
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
||||||
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
|
||||||
"WEBSOCKET_PING_INTERVAL": 20,
|
|
||||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
|
||||||
"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):
|
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 {}
|
defaults = defaults or {}
|
||||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
|
|
||||||
@@ -50,9 +84,22 @@ class Config(dict):
|
|||||||
if keep_alive is not None:
|
if keep_alive is not None:
|
||||||
self.KEEP_ALIVE = keep_alive
|
self.KEEP_ALIVE = keep_alive
|
||||||
|
|
||||||
if load_env:
|
if env_prefix != SANIC_PREFIX:
|
||||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
if env_prefix:
|
||||||
self.load_environment_vars(prefix=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):
|
def __getattr__(self, attr):
|
||||||
try:
|
try:
|
||||||
@@ -62,6 +109,19 @@ class Config(dict):
|
|||||||
|
|
||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value):
|
||||||
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):
|
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"
|
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ def exception_response(
|
|||||||
except InvalidUsage:
|
except InvalidUsage:
|
||||||
renderer = HTMLRenderer
|
renderer = HTMLRenderer
|
||||||
|
|
||||||
content_type, *_ = request.headers.get(
|
content_type, *_ = request.headers.getone(
|
||||||
"content-type", ""
|
"content-type", ""
|
||||||
).split(";")
|
).split(";")
|
||||||
renderer = RENDERERS_BY_CONTENT_TYPE.get(
|
renderer = RENDERERS_BY_CONTENT_TYPE.get(
|
||||||
|
|||||||
@@ -3,26 +3,18 @@ from typing import Optional, Union
|
|||||||
from sanic.helpers import STATUS_CODES
|
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):
|
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)
|
super().__init__(message)
|
||||||
|
|
||||||
if status_code is not None:
|
if status_code is not None:
|
||||||
@@ -33,45 +25,42 @@ class SanicException(Exception):
|
|||||||
self.quiet = True
|
self.quiet = True
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(404)
|
|
||||||
class NotFound(SanicException):
|
class NotFound(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 404 Not Found
|
**Status**: 404 Not Found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 404
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(400)
|
|
||||||
class InvalidUsage(SanicException):
|
class InvalidUsage(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 400 Bad Request
|
**Status**: 400 Bad Request
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 400
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(405)
|
|
||||||
class MethodNotSupported(SanicException):
|
class MethodNotSupported(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 405 Method Not Allowed
|
**Status**: 405 Method Not Allowed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
status_code = 405
|
||||||
|
|
||||||
def __init__(self, message, method, allowed_methods):
|
def __init__(self, message, method, allowed_methods):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.headers = {"Allow": ", ".join(allowed_methods)}
|
self.headers = {"Allow": ", ".join(allowed_methods)}
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(500)
|
|
||||||
class ServerError(SanicException):
|
class ServerError(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 500 Internal Server Error
|
**Status**: 500 Internal Server Error
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 500
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(503)
|
|
||||||
class ServiceUnavailable(SanicException):
|
class ServiceUnavailable(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 503 Service Unavailable
|
**Status**: 503 Service Unavailable
|
||||||
@@ -80,7 +69,7 @@ class ServiceUnavailable(SanicException):
|
|||||||
down for maintenance). Generally, this is a temporary state.
|
down for maintenance). Generally, this is a temporary state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 503
|
||||||
|
|
||||||
|
|
||||||
class URLBuildError(ServerError):
|
class URLBuildError(ServerError):
|
||||||
@@ -88,7 +77,7 @@ class URLBuildError(ServerError):
|
|||||||
**Status**: 500 Internal Server Error
|
**Status**: 500 Internal Server Error
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 500
|
||||||
|
|
||||||
|
|
||||||
class FileNotFound(NotFound):
|
class FileNotFound(NotFound):
|
||||||
@@ -102,7 +91,6 @@ class FileNotFound(NotFound):
|
|||||||
self.relative_url = relative_url
|
self.relative_url = relative_url
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(408)
|
|
||||||
class RequestTimeout(SanicException):
|
class RequestTimeout(SanicException):
|
||||||
"""The Web server (running the Web site) thinks that there has been too
|
"""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
|
long an interval of time between 1) the establishment of an IP
|
||||||
@@ -112,16 +100,15 @@ class RequestTimeout(SanicException):
|
|||||||
server has 'timed out' on that particular socket connection.
|
server has 'timed out' on that particular socket connection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 408
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(413)
|
|
||||||
class PayloadTooLarge(SanicException):
|
class PayloadTooLarge(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 413 Payload Too Large
|
**Status**: 413 Payload Too Large
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 413
|
||||||
|
|
||||||
|
|
||||||
class HeaderNotFound(InvalidUsage):
|
class HeaderNotFound(InvalidUsage):
|
||||||
@@ -129,36 +116,35 @@ class HeaderNotFound(InvalidUsage):
|
|||||||
**Status**: 400 Bad Request
|
**Status**: 400 Bad Request
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 400
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(416)
|
|
||||||
class ContentRangeError(SanicException):
|
class ContentRangeError(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 416 Range Not Satisfiable
|
**Status**: 416 Range Not Satisfiable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
status_code = 416
|
||||||
|
|
||||||
def __init__(self, message, content_range):
|
def __init__(self, message, content_range):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
|
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(417)
|
|
||||||
class HeaderExpectationFailed(SanicException):
|
class HeaderExpectationFailed(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 417 Expectation Failed
|
**Status**: 417 Expectation Failed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 417
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(403)
|
|
||||||
class Forbidden(SanicException):
|
class Forbidden(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 403 Forbidden
|
**Status**: 403 Forbidden
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 403
|
||||||
|
|
||||||
|
|
||||||
class InvalidRangeType(ContentRangeError):
|
class InvalidRangeType(ContentRangeError):
|
||||||
@@ -166,7 +152,7 @@ class InvalidRangeType(ContentRangeError):
|
|||||||
**Status**: 416 Range Not Satisfiable
|
**Status**: 416 Range Not Satisfiable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
status_code = 416
|
||||||
|
|
||||||
|
|
||||||
class PyFileError(Exception):
|
class PyFileError(Exception):
|
||||||
@@ -174,7 +160,6 @@ class PyFileError(Exception):
|
|||||||
super().__init__("could not execute config file %s", file)
|
super().__init__("could not execute config file %s", file)
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(401)
|
|
||||||
class Unauthorized(SanicException):
|
class Unauthorized(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 401 Unauthorized
|
**Status**: 401 Unauthorized
|
||||||
@@ -210,6 +195,8 @@ class Unauthorized(SanicException):
|
|||||||
realm="Restricted Area")
|
realm="Restricted Area")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
status_code = 401
|
||||||
|
|
||||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||||
super().__init__(message, status_code)
|
super().__init__(message, status_code)
|
||||||
|
|
||||||
@@ -241,9 +228,13 @@ def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
|||||||
:param status_code: The HTTP status code to return.
|
:param status_code: The HTTP status code to return.
|
||||||
:param message: The HTTP response body. Defaults to the messages in
|
:param message: The HTTP response body. Defaults to the messages in
|
||||||
"""
|
"""
|
||||||
if message is None:
|
import warnings
|
||||||
msg: bytes = STATUS_CODES[status_code]
|
|
||||||
# These are stored as bytes in the STATUS_CODES dict
|
warnings.warn(
|
||||||
message = msg.decode("utf8")
|
"sanic.exceptions.abort has been marked as deprecated, and will be "
|
||||||
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
"removed in release 21.12.\n To migrate your code, simply replace "
|
||||||
raise sanic_exception(message=message, status_code=status_code)
|
"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,
|
HeaderNotFound,
|
||||||
InvalidRangeType,
|
InvalidRangeType,
|
||||||
)
|
)
|
||||||
from sanic.log import logger
|
from sanic.log import error_logger
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +25,6 @@ class ErrorHandler:
|
|||||||
|
|
||||||
handlers = None
|
handlers = None
|
||||||
cached_handlers = None
|
cached_handlers = None
|
||||||
_missing = object()
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.handlers = []
|
self.handlers = []
|
||||||
@@ -45,7 +44,9 @@ class ErrorHandler:
|
|||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
# self.handlers to be deprecated and removed in version 21.12
|
||||||
self.handlers.append((exception, handler))
|
self.handlers.append((exception, handler))
|
||||||
|
self.cached_handlers[exception] = handler
|
||||||
|
|
||||||
def lookup(self, exception):
|
def lookup(self, exception):
|
||||||
"""
|
"""
|
||||||
@@ -61,14 +62,19 @@ class ErrorHandler:
|
|||||||
|
|
||||||
:return: Registered function if found ``None`` otherwise
|
:return: Registered function if found ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
handler = self.cached_handlers.get(type(exception), self._missing)
|
exception_class = type(exception)
|
||||||
if handler is self._missing:
|
if exception_class in self.cached_handlers:
|
||||||
for exception_class, handler in self.handlers:
|
return self.cached_handlers[exception_class]
|
||||||
if isinstance(exception, exception_class):
|
|
||||||
self.cached_handlers[type(exception)] = handler
|
for ancestor in type.mro(exception_class):
|
||||||
return handler
|
if ancestor in self.cached_handlers:
|
||||||
self.cached_handlers[type(exception)] = None
|
handler = self.cached_handlers[ancestor]
|
||||||
handler = None
|
self.cached_handlers[exception_class] = handler
|
||||||
|
return handler
|
||||||
|
if ancestor is BaseException:
|
||||||
|
break
|
||||||
|
self.cached_handlers[exception_class] = None
|
||||||
|
handler = None
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def response(self, request, exception):
|
def response(self, request, exception):
|
||||||
@@ -101,7 +107,7 @@ class ErrorHandler:
|
|||||||
response_message = (
|
response_message = (
|
||||||
"Exception raised in exception handler " '"%s" for uri: %s'
|
"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:
|
if self.debug:
|
||||||
return text(response_message % (handler.__name__, url), 500)
|
return text(response_message % (handler.__name__, url), 500)
|
||||||
@@ -137,7 +143,9 @@ class ErrorHandler:
|
|||||||
url = "unknown"
|
url = "unknown"
|
||||||
|
|
||||||
self.log(format_exc())
|
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)
|
return exception_response(request, exception, self.debug)
|
||||||
|
|
||||||
@@ -165,7 +173,7 @@ class ContentRangeHandler:
|
|||||||
|
|
||||||
def __init__(self, request, stats):
|
def __init__(self, request, stats):
|
||||||
self.total = stats.st_size
|
self.total = stats.st_size
|
||||||
_range = request.headers.get("Range")
|
_range = request.headers.getone("range", None)
|
||||||
if _range is None:
|
if _range is None:
|
||||||
raise HeaderNotFound("Range Header Not Found")
|
raise HeaderNotFound("Range Header Not Found")
|
||||||
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
|
|||||||
"""Parse traditional proxy headers."""
|
"""Parse traditional proxy headers."""
|
||||||
real_ip_header = config.REAL_IP_HEADER
|
real_ip_header = config.REAL_IP_HEADER
|
||||||
proxies_count = config.PROXIES_COUNT
|
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:
|
if not addr and proxies_count:
|
||||||
assert proxies_count > 0
|
assert proxies_count > 0
|
||||||
try:
|
try:
|
||||||
@@ -131,7 +131,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
|
|||||||
("port", "x-forwarded-port"),
|
("port", "x-forwarded-port"),
|
||||||
("path", "x-forwarded-path"),
|
("path", "x-forwarded-path"),
|
||||||
):
|
):
|
||||||
yield key, headers.get(header)
|
yield key, headers.getone(header, None)
|
||||||
|
|
||||||
return fwd_normalize(options())
|
return fwd_normalize(options())
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from sanic.exceptions import (
|
|||||||
)
|
)
|
||||||
from sanic.headers import format_http1_response
|
from sanic.headers import format_http1_response
|
||||||
from sanic.helpers import has_message_body
|
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):
|
class Stage(Enum):
|
||||||
@@ -64,6 +64,9 @@ class Http:
|
|||||||
:raises RuntimeError:
|
:raises RuntimeError:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
HEADER_CEILING = 16_384
|
||||||
|
HEADER_MAX_SIZE = 0
|
||||||
|
|
||||||
__slots__ = [
|
__slots__ = [
|
||||||
"_send",
|
"_send",
|
||||||
"_receive_more",
|
"_receive_more",
|
||||||
@@ -82,6 +85,7 @@ class Http:
|
|||||||
"request_max_size",
|
"request_max_size",
|
||||||
"response",
|
"response",
|
||||||
"response_func",
|
"response_func",
|
||||||
|
"response_size",
|
||||||
"response_bytes_left",
|
"response_bytes_left",
|
||||||
"upgrade_websocket",
|
"upgrade_websocket",
|
||||||
]
|
]
|
||||||
@@ -143,7 +147,7 @@ class Http:
|
|||||||
# Try to consume any remaining request body
|
# Try to consume any remaining request body
|
||||||
if self.request_body:
|
if self.request_body:
|
||||||
if self.response and 200 <= self.response.status < 300:
|
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.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for _ in self:
|
async for _ in self:
|
||||||
@@ -168,7 +172,6 @@ class Http:
|
|||||||
"""
|
"""
|
||||||
Receive and parse request header into self.request.
|
Receive and parse request header into self.request.
|
||||||
"""
|
"""
|
||||||
HEADER_MAX_SIZE = min(8192, self.request_max_size)
|
|
||||||
# Receive until full header is in buffer
|
# Receive until full header is in buffer
|
||||||
buf = self.recv_buffer
|
buf = self.recv_buffer
|
||||||
pos = 0
|
pos = 0
|
||||||
@@ -179,12 +182,12 @@ class Http:
|
|||||||
break
|
break
|
||||||
|
|
||||||
pos = max(0, len(buf) - 3)
|
pos = max(0, len(buf) - 3)
|
||||||
if pos >= HEADER_MAX_SIZE:
|
if pos >= self.HEADER_MAX_SIZE:
|
||||||
break
|
break
|
||||||
|
|
||||||
await self._receive_more()
|
await self._receive_more()
|
||||||
|
|
||||||
if pos >= HEADER_MAX_SIZE:
|
if pos >= self.HEADER_MAX_SIZE:
|
||||||
raise PayloadTooLarge("Request header exceeds the size limit")
|
raise PayloadTooLarge("Request header exceeds the size limit")
|
||||||
|
|
||||||
# Parse header content
|
# Parse header content
|
||||||
@@ -218,7 +221,9 @@ class Http:
|
|||||||
raise InvalidUsage("Bad Request")
|
raise InvalidUsage("Bad Request")
|
||||||
|
|
||||||
headers_instance = Header(headers)
|
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
|
# Prepare a Request object
|
||||||
request = self.protocol.request_class(
|
request = self.protocol.request_class(
|
||||||
@@ -235,7 +240,7 @@ class Http:
|
|||||||
self.request_bytes_left = self.request_bytes = 0
|
self.request_bytes_left = self.request_bytes = 0
|
||||||
if request_body:
|
if request_body:
|
||||||
headers = request.headers
|
headers = request.headers
|
||||||
expect = headers.get("expect")
|
expect = headers.getone("expect", None)
|
||||||
|
|
||||||
if expect is not None:
|
if expect is not None:
|
||||||
if expect.lower() == "100-continue":
|
if expect.lower() == "100-continue":
|
||||||
@@ -243,7 +248,7 @@ class Http:
|
|||||||
else:
|
else:
|
||||||
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
|
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
|
||||||
|
|
||||||
if headers.get("transfer-encoding") == "chunked":
|
if headers.getone("transfer-encoding", None) == "chunked":
|
||||||
self.request_body = "chunked"
|
self.request_body = "chunked"
|
||||||
pos -= 2 # One CRLF stays in buffer
|
pos -= 2 # One CRLF stays in buffer
|
||||||
else:
|
else:
|
||||||
@@ -270,6 +275,7 @@ class Http:
|
|||||||
size = len(data)
|
size = len(data)
|
||||||
headers = res.headers
|
headers = res.headers
|
||||||
status = res.status
|
status = res.status
|
||||||
|
self.response_size = size
|
||||||
|
|
||||||
if not isinstance(status, int) or status < 200:
|
if not isinstance(status, int) or status < 200:
|
||||||
raise RuntimeError(f"Invalid response status {status!r}")
|
raise RuntimeError(f"Invalid response status {status!r}")
|
||||||
@@ -424,7 +430,9 @@ class Http:
|
|||||||
req, res = self.request, self.response
|
req, res = self.request, self.response
|
||||||
extra = {
|
extra = {
|
||||||
"status": getattr(res, "status", 0),
|
"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",
|
"host": "UNKNOWN",
|
||||||
"request": "nil",
|
"request": "nil",
|
||||||
}
|
}
|
||||||
@@ -535,3 +543,10 @@ class Http:
|
|||||||
@property
|
@property
|
||||||
def send(self):
|
def send(self):
|
||||||
return self.response_func
|
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.
|
Create a listener from a decorated function.
|
||||||
|
|
||||||
To be used as a deocrator:
|
To be used as a decorator:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ from sanic.views import CompositionView
|
|||||||
|
|
||||||
|
|
||||||
class RouteMixin:
|
class RouteMixin:
|
||||||
|
name: str
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_routes: Set[FutureRoute] = set()
|
self._future_routes: Set[FutureRoute] = set()
|
||||||
self._future_statics: Set[FutureStatic] = set()
|
self._future_statics: Set[FutureStatic] = set()
|
||||||
self.name = ""
|
|
||||||
self.strict_slashes: Optional[bool] = False
|
self.strict_slashes: Optional[bool] = False
|
||||||
|
|
||||||
def _apply_route(self, route: FutureRoute) -> List[Route]:
|
def _apply_route(self, route: FutureRoute) -> List[Route]:
|
||||||
@@ -45,7 +46,7 @@ class RouteMixin:
|
|||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ignore_body: bool = False,
|
ignore_body: bool = False,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
@@ -53,6 +54,7 @@ class RouteMixin:
|
|||||||
websocket: bool = False,
|
websocket: bool = False,
|
||||||
unquote: bool = False,
|
unquote: bool = False,
|
||||||
static: bool = False,
|
static: bool = False,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a route
|
Decorate a function to be registered as a route
|
||||||
@@ -66,12 +68,14 @@ class RouteMixin:
|
|||||||
:param name: user defined route name for url_for
|
:param name: user defined route name for url_for
|
||||||
:param ignore_body: whether the handler should ignore request
|
:param ignore_body: whether the handler should ignore request
|
||||||
body (eg. GET requests)
|
body (eg. GET requests)
|
||||||
|
:param version_prefix: URL path that should be before the version
|
||||||
|
value; default: ``/v``
|
||||||
:return: tuple of routes, decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Fix case where the user did not prefix the URL with a /
|
# Fix case where the user did not prefix the URL with a /
|
||||||
# and will probably get confused as to why it's not working
|
# and will probably get confused as to why it's not working
|
||||||
if not uri.startswith("/"):
|
if not uri.startswith("/") and (uri or hasattr(self, "router")):
|
||||||
uri = "/" + uri
|
uri = "/" + uri
|
||||||
|
|
||||||
if strict_slashes is None:
|
if strict_slashes is None:
|
||||||
@@ -92,6 +96,7 @@ class RouteMixin:
|
|||||||
nonlocal subprotocols
|
nonlocal subprotocols
|
||||||
nonlocal websocket
|
nonlocal websocket
|
||||||
nonlocal static
|
nonlocal static
|
||||||
|
nonlocal version_prefix
|
||||||
|
|
||||||
if isinstance(handler, tuple):
|
if isinstance(handler, tuple):
|
||||||
# if a handler fn is already wrapped in a route, the handler
|
# if a handler fn is already wrapped in a route, the handler
|
||||||
@@ -128,6 +133,7 @@ class RouteMixin:
|
|||||||
subprotocols,
|
subprotocols,
|
||||||
unquote,
|
unquote,
|
||||||
static,
|
static,
|
||||||
|
version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._future_routes.add(route)
|
self._future_routes.add(route)
|
||||||
@@ -154,7 +160,9 @@ class RouteMixin:
|
|||||||
if apply:
|
if apply:
|
||||||
self._apply_route(route)
|
self._apply_route(route)
|
||||||
|
|
||||||
return route, handler
|
if static:
|
||||||
|
return route, handler
|
||||||
|
return handler
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
@@ -168,6 +176,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""A helper method to register class instance or
|
"""A helper method to register class instance or
|
||||||
functions as a handler to the application url
|
functions as a handler to the application url
|
||||||
@@ -182,6 +191,8 @@ class RouteMixin:
|
|||||||
:param version:
|
:param version:
|
||||||
:param name: user defined route name for url_for
|
:param name: user defined route name for url_for
|
||||||
:param stream: boolean specifying if the handler is a stream handler
|
: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
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
# Handle HTTPMethodView differently
|
# Handle HTTPMethodView differently
|
||||||
@@ -214,6 +225,7 @@ class RouteMixin:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)(handler)
|
)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
@@ -226,6 +238,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **GET** *HTTP* method
|
Add an API URL under the **GET** *HTTP* method
|
||||||
@@ -236,6 +249,8 @@ class RouteMixin:
|
|||||||
URLs need to terminate with a */*
|
URLs need to terminate with a */*
|
||||||
:param version: API Version
|
:param version: API Version
|
||||||
:param name: Unique name that can be used to identify the Route
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -246,6 +261,7 @@ class RouteMixin:
|
|||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
def post(
|
def post(
|
||||||
@@ -256,6 +272,7 @@ class RouteMixin:
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **POST** *HTTP* method
|
Add an API URL under the **POST** *HTTP* method
|
||||||
@@ -266,6 +283,8 @@ class RouteMixin:
|
|||||||
URLs need to terminate with a */*
|
URLs need to terminate with a */*
|
||||||
:param version: API Version
|
:param version: API Version
|
||||||
:param name: Unique name that can be used to identify the Route
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -276,6 +295,7 @@ class RouteMixin:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
def put(
|
def put(
|
||||||
@@ -286,6 +306,7 @@ class RouteMixin:
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PUT** *HTTP* method
|
Add an API URL under the **PUT** *HTTP* method
|
||||||
@@ -296,6 +317,8 @@ class RouteMixin:
|
|||||||
URLs need to terminate with a */*
|
URLs need to terminate with a */*
|
||||||
:param version: API Version
|
:param version: API Version
|
||||||
:param name: Unique name that can be used to identify the Route
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -306,6 +329,7 @@ class RouteMixin:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
def head(
|
def head(
|
||||||
@@ -316,6 +340,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **HEAD** *HTTP* method
|
Add an API URL under the **HEAD** *HTTP* method
|
||||||
@@ -334,6 +359,8 @@ class RouteMixin:
|
|||||||
:param ignore_body: whether the handler should ignore request
|
:param ignore_body: whether the handler should ignore request
|
||||||
body (eg. GET requests), defaults to True
|
body (eg. GET requests), defaults to True
|
||||||
:type ignore_body: bool, optional
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -344,6 +371,7 @@ class RouteMixin:
|
|||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
def options(
|
def options(
|
||||||
@@ -354,6 +382,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **OPTIONS** *HTTP* method
|
Add an API URL under the **OPTIONS** *HTTP* method
|
||||||
@@ -372,6 +401,8 @@ class RouteMixin:
|
|||||||
:param ignore_body: whether the handler should ignore request
|
:param ignore_body: whether the handler should ignore request
|
||||||
body (eg. GET requests), defaults to True
|
body (eg. GET requests), defaults to True
|
||||||
:type ignore_body: bool, optional
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -382,6 +413,7 @@ class RouteMixin:
|
|||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
def patch(
|
def patch(
|
||||||
@@ -392,6 +424,7 @@ class RouteMixin:
|
|||||||
stream=False,
|
stream=False,
|
||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PATCH** *HTTP* method
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
@@ -412,6 +445,8 @@ class RouteMixin:
|
|||||||
:param ignore_body: whether the handler should ignore request
|
:param ignore_body: whether the handler should ignore request
|
||||||
body (eg. GET requests), defaults to True
|
body (eg. GET requests), defaults to True
|
||||||
:type ignore_body: bool, optional
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -422,6 +457,7 @@ class RouteMixin:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
@@ -432,6 +468,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **DELETE** *HTTP* method
|
Add an API URL under the **DELETE** *HTTP* method
|
||||||
@@ -442,6 +479,8 @@ class RouteMixin:
|
|||||||
URLs need to terminate with a */*
|
URLs need to terminate with a */*
|
||||||
:param version: API Version
|
:param version: API Version
|
||||||
:param name: Unique name that can be used to identify the Route
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -452,6 +491,7 @@ class RouteMixin:
|
|||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
def websocket(
|
def websocket(
|
||||||
@@ -463,6 +503,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a websocket route
|
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 subprotocols: optional list of str with supported subprotocols
|
||||||
:param name: A unique name assigned to the URL so that it can
|
:param name: A unique name assigned to the URL so that it can
|
||||||
be used with :func:`url_for`
|
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: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -486,6 +529,7 @@ class RouteMixin:
|
|||||||
apply=apply,
|
apply=apply,
|
||||||
subprotocols=subprotocols,
|
subprotocols=subprotocols,
|
||||||
websocket=True,
|
websocket=True,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_websocket_route(
|
def add_websocket_route(
|
||||||
@@ -497,6 +541,7 @@ class RouteMixin:
|
|||||||
subprotocols=None,
|
subprotocols=None,
|
||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
A helper method to register a function as a websocket route.
|
A helper method to register a function as a websocket route.
|
||||||
@@ -513,6 +558,8 @@ class RouteMixin:
|
|||||||
handshake
|
handshake
|
||||||
:param name: A unique name assigned to the URL so that it can
|
:param name: A unique name assigned to the URL so that it can
|
||||||
be used with :func:`url_for`
|
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: Objected decorated by :func:`websocket`
|
||||||
"""
|
"""
|
||||||
return self.websocket(
|
return self.websocket(
|
||||||
@@ -522,6 +569,7 @@ class RouteMixin:
|
|||||||
subprotocols=subprotocols,
|
subprotocols=subprotocols,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
|
version_prefix=version_prefix,
|
||||||
)(handler)
|
)(handler)
|
||||||
|
|
||||||
def static(
|
def static(
|
||||||
@@ -665,7 +713,10 @@ class RouteMixin:
|
|||||||
modified_since = strftime(
|
modified_since = strftime(
|
||||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
"%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)
|
return HTTPResponse(status=304)
|
||||||
headers["Last-Modified"] = modified_since
|
headers["Last-Modified"] = modified_since
|
||||||
_range = None
|
_range = None
|
||||||
@@ -718,16 +769,18 @@ class RouteMixin:
|
|||||||
return await file(file_path, headers=headers, _range=_range)
|
return await file(file_path, headers=headers, _range=_range)
|
||||||
except ContentRangeError:
|
except ContentRangeError:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except FileNotFoundError:
|
||||||
error_logger.exception(
|
|
||||||
f"File not found: path={file_or_directory}, "
|
|
||||||
f"relative_url={__file_uri__}"
|
|
||||||
)
|
|
||||||
raise FileNotFound(
|
raise FileNotFound(
|
||||||
"File not found",
|
"File not found",
|
||||||
path=file_or_directory,
|
path=file_or_directory,
|
||||||
relative_url=__file_uri__,
|
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(
|
def _register_static(
|
||||||
self,
|
self,
|
||||||
@@ -776,7 +829,7 @@ class RouteMixin:
|
|||||||
# If we're not trying to match a file directly,
|
# If we're not trying to match a file directly,
|
||||||
# serve from the folder
|
# serve from the folder
|
||||||
if not path.isfile(file_or_directory):
|
if not path.isfile(file_or_directory):
|
||||||
uri += "/<__file_uri__>"
|
uri += "/<__file_uri__:path>"
|
||||||
|
|
||||||
# special prefix for static files
|
# special prefix for static files
|
||||||
# if not static.name.startswith("_static_"):
|
# if not static.name.startswith("_static_"):
|
||||||
|
|||||||
@@ -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.futures import FutureSignal
|
||||||
from sanic.models.handler_types import SignalHandler
|
from sanic.models.handler_types import SignalHandler
|
||||||
@@ -60,10 +60,16 @@ class SignalMixin:
|
|||||||
|
|
||||||
def add_signal(
|
def add_signal(
|
||||||
self,
|
self,
|
||||||
handler,
|
handler: Optional[Callable[..., Any]],
|
||||||
event: str,
|
event: str,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Dict[str, Any] = None,
|
||||||
):
|
):
|
||||||
|
if not handler:
|
||||||
|
|
||||||
|
async def noop():
|
||||||
|
...
|
||||||
|
|
||||||
|
handler = noop
|
||||||
self.signal(event=event, condition=condition)(handler)
|
self.signal(event=event, condition=condition)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class FutureRoute(NamedTuple):
|
|||||||
subprotocols: Optional[List[str]]
|
subprotocols: Optional[List[str]]
|
||||||
unquote: bool
|
unquote: bool
|
||||||
static: bool
|
static: bool
|
||||||
|
version_prefix: str
|
||||||
|
|
||||||
|
|
||||||
class FutureListener(NamedTuple):
|
class FutureListener(NamedTuple):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import itertools
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -5,6 +6,9 @@ import sys
|
|||||||
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
from sanic.config import BASE_LOGO
|
||||||
|
from sanic.log import logger
|
||||||
|
|
||||||
|
|
||||||
def _iter_module_files():
|
def _iter_module_files():
|
||||||
"""This iterates over all relevant Python 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.
|
"""Watch project files, restart worker process if a change happened.
|
||||||
|
|
||||||
:param sleep_interval: interval in second.
|
:param sleep_interval: interval in second.
|
||||||
@@ -73,21 +91,25 @@ def watchdog(sleep_interval):
|
|||||||
|
|
||||||
worker_process = restart_with_reloader()
|
worker_process = restart_with_reloader()
|
||||||
|
|
||||||
|
if app.config.LOGO:
|
||||||
|
logger.debug(
|
||||||
|
app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
need_reload = False
|
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:
|
try:
|
||||||
mtime = os.stat(filename).st_mtime
|
check = _check_file(filename, mtimes)
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
old_time = mtimes.get(filename)
|
if check:
|
||||||
if old_time is None:
|
|
||||||
mtimes[filename] = mtime
|
|
||||||
elif mtime > old_time:
|
|
||||||
mtimes[filename] = mtime
|
|
||||||
need_reload = True
|
need_reload = True
|
||||||
|
|
||||||
if need_reload:
|
if need_reload:
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class Request:
|
|||||||
self._name: Optional[str] = None
|
self._name: Optional[str] = None
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
self.headers = headers
|
self.headers = Header(headers)
|
||||||
self.version = version
|
self.version = version
|
||||||
self.method = method
|
self.method = method
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
@@ -262,7 +262,7 @@ class Request:
|
|||||||
app = Sanic("MyApp", request_class=IntRequest)
|
app = Sanic("MyApp", request_class=IntRequest)
|
||||||
"""
|
"""
|
||||||
if not self._id:
|
if not self._id:
|
||||||
self._id = self.headers.get(
|
self._id = self.headers.getone(
|
||||||
self.app.config.REQUEST_ID_HEADER,
|
self.app.config.REQUEST_ID_HEADER,
|
||||||
self.__class__.generate_id(self), # type: ignore
|
self.__class__.generate_id(self), # type: ignore
|
||||||
)
|
)
|
||||||
@@ -303,7 +303,7 @@ class Request:
|
|||||||
:return: token related to request
|
:return: token related to request
|
||||||
"""
|
"""
|
||||||
prefixes = ("Bearer", "Token")
|
prefixes = ("Bearer", "Token")
|
||||||
auth_header = self.headers.get("Authorization")
|
auth_header = self.headers.getone("authorization", None)
|
||||||
|
|
||||||
if auth_header is not None:
|
if auth_header is not None:
|
||||||
for prefix in prefixes:
|
for prefix in prefixes:
|
||||||
@@ -317,8 +317,8 @@ class Request:
|
|||||||
if self.parsed_form is None:
|
if self.parsed_form is None:
|
||||||
self.parsed_form = RequestParameters()
|
self.parsed_form = RequestParameters()
|
||||||
self.parsed_files = RequestParameters()
|
self.parsed_files = RequestParameters()
|
||||||
content_type = self.headers.get(
|
content_type = self.headers.getone(
|
||||||
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
|
"content-type", DEFAULT_HTTP_CONTENT_TYPE
|
||||||
)
|
)
|
||||||
content_type, parameters = parse_content_header(content_type)
|
content_type, parameters = parse_content_header(content_type)
|
||||||
try:
|
try:
|
||||||
@@ -378,9 +378,12 @@ class Request:
|
|||||||
:type errors: str
|
:type errors: str
|
||||||
:return: RequestParameters
|
:return: RequestParameters
|
||||||
"""
|
"""
|
||||||
if not self.parsed_args[
|
if (
|
||||||
(keep_blank_values, strict_parsing, encoding, errors)
|
keep_blank_values,
|
||||||
]:
|
strict_parsing,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
) not in self.parsed_args:
|
||||||
if self.query_string:
|
if self.query_string:
|
||||||
self.parsed_args[
|
self.parsed_args[
|
||||||
(keep_blank_values, strict_parsing, encoding, errors)
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
@@ -434,9 +437,12 @@ class Request:
|
|||||||
:type errors: str
|
:type errors: str
|
||||||
:return: list
|
:return: list
|
||||||
"""
|
"""
|
||||||
if not self.parsed_not_grouped_args[
|
if (
|
||||||
(keep_blank_values, strict_parsing, encoding, errors)
|
keep_blank_values,
|
||||||
]:
|
strict_parsing,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
) not in self.parsed_not_grouped_args:
|
||||||
if self.query_string:
|
if self.query_string:
|
||||||
self.parsed_not_grouped_args[
|
self.parsed_not_grouped_args[
|
||||||
(keep_blank_values, strict_parsing, encoding, errors)
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
@@ -465,7 +471,7 @@ class Request:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if self._cookies is None:
|
if self._cookies is None:
|
||||||
cookie = self.headers.get("Cookie")
|
cookie = self.headers.getone("cookie", None)
|
||||||
if cookie is not None:
|
if cookie is not None:
|
||||||
cookies: SimpleCookie = SimpleCookie()
|
cookies: SimpleCookie = SimpleCookie()
|
||||||
cookies.load(cookie)
|
cookies.load(cookie)
|
||||||
@@ -482,7 +488,7 @@ class Request:
|
|||||||
:return: Content-Type header form the request
|
:return: Content-Type header form the request
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
|
return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def match_info(self):
|
def match_info(self):
|
||||||
@@ -499,7 +505,7 @@ class Request:
|
|||||||
:return: peer ip of the socket
|
:return: peer ip of the socket
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
return self.conn_info.client if self.conn_info else ""
|
return self.conn_info.client_ip if self.conn_info else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
@@ -581,7 +587,7 @@ class Request:
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
self.app.websocket_enabled
|
self.app.websocket_enabled
|
||||||
and self.headers.get("upgrade") == "websocket"
|
and self.headers.getone("upgrade", "").lower() == "websocket"
|
||||||
):
|
):
|
||||||
scheme = "ws"
|
scheme = "ws"
|
||||||
else:
|
else:
|
||||||
@@ -608,7 +614,9 @@ class Request:
|
|||||||
server_name = self.app.config.get("SERVER_NAME")
|
server_name = self.app.config.get("SERVER_NAME")
|
||||||
if server_name:
|
if server_name:
|
||||||
return server_name.split("//", 1)[-1].split("/", 1)[0]
|
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
|
@property
|
||||||
def server_name(self) -> str:
|
def server_name(self) -> str:
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
|
|
||||||
.. warning::
|
.. 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.
|
same functionality without a callback.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@@ -174,12 +174,16 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
status: int = 200,
|
status: int = 200,
|
||||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||||
content_type: str = "text/plain; charset=utf-8",
|
content_type: str = "text/plain; charset=utf-8",
|
||||||
chunked="deprecated",
|
ignore_deprecation_notice: bool = False,
|
||||||
):
|
):
|
||||||
if chunked != "deprecated":
|
if not ignore_deprecation_notice:
|
||||||
warn(
|
warn(
|
||||||
"The chunked argument has been deprecated and will be "
|
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
|
||||||
"removed in v21.6"
|
"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__()
|
super().__init__()
|
||||||
@@ -203,6 +207,9 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
self.streaming_fn = None
|
self.streaming_fn = None
|
||||||
await super().send(*args, **kwargs)
|
await super().send(*args, **kwargs)
|
||||||
|
|
||||||
|
async def eof(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class HTTPResponse(BaseHTTPResponse):
|
class HTTPResponse(BaseHTTPResponse):
|
||||||
"""
|
"""
|
||||||
@@ -235,6 +242,15 @@ class HTTPResponse(BaseHTTPResponse):
|
|||||||
self.headers = Header(headers or {})
|
self.headers = Header(headers or {})
|
||||||
self._cookies = None
|
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(
|
def empty(
|
||||||
status=204, headers: Optional[Dict[str, str]] = None
|
status=204, headers: Optional[Dict[str, str]] = None
|
||||||
@@ -396,7 +412,6 @@ async def file_stream(
|
|||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
filename: Optional[str] = None,
|
filename: Optional[str] = None,
|
||||||
chunked="deprecated",
|
|
||||||
_range: Optional[Range] = None,
|
_range: Optional[Range] = None,
|
||||||
) -> StreamingHTTPResponse:
|
) -> StreamingHTTPResponse:
|
||||||
"""Return a streaming response object with file data.
|
"""Return a streaming response object with file data.
|
||||||
@@ -409,12 +424,6 @@ async def file_stream(
|
|||||||
:param chunked: Deprecated
|
:param chunked: Deprecated
|
||||||
:param _range:
|
:param _range:
|
||||||
"""
|
"""
|
||||||
if chunked != "deprecated":
|
|
||||||
warn(
|
|
||||||
"The chunked argument has been deprecated and will be "
|
|
||||||
"removed in v21.6"
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
if filename:
|
if filename:
|
||||||
headers.setdefault(
|
headers.setdefault(
|
||||||
@@ -453,6 +462,7 @@ async def file_stream(
|
|||||||
status=status,
|
status=status,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=mime_type,
|
content_type=mime_type,
|
||||||
|
ignore_deprecation_notice=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -461,7 +471,6 @@ def stream(
|
|||||||
status: int = 200,
|
status: int = 200,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
content_type: str = "text/plain; charset=utf-8",
|
content_type: str = "text/plain; charset=utf-8",
|
||||||
chunked="deprecated",
|
|
||||||
):
|
):
|
||||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||||
@@ -482,17 +491,12 @@ def stream(
|
|||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param chunked: Deprecated
|
:param chunked: Deprecated
|
||||||
"""
|
"""
|
||||||
if chunked != "deprecated":
|
|
||||||
warn(
|
|
||||||
"The chunked argument has been deprecated and will be "
|
|
||||||
"removed in v21.6"
|
|
||||||
)
|
|
||||||
|
|
||||||
return StreamingHTTPResponse(
|
return StreamingHTTPResponse(
|
||||||
streaming_fn,
|
streaming_fn,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
status=status,
|
status=status,
|
||||||
|
ignore_deprecation_notice=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class Router(BaseRouter):
|
|||||||
return self.resolve(
|
return self.resolve(
|
||||||
path=path,
|
path=path,
|
||||||
method=method,
|
method=method,
|
||||||
extra={"host": host},
|
extra={"host": host} if host else None,
|
||||||
)
|
)
|
||||||
except RoutingNotFound as e:
|
except RoutingNotFound as e:
|
||||||
raise NotFound("Requested URL {} not found".format(e.path))
|
raise NotFound("Requested URL {} not found".format(e.path))
|
||||||
@@ -73,6 +73,7 @@ class Router(BaseRouter):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
unquote: bool = False,
|
unquote: bool = False,
|
||||||
static: bool = False,
|
static: bool = False,
|
||||||
|
version_prefix: str = "/v",
|
||||||
) -> Union[Route, List[Route]]:
|
) -> Union[Route, List[Route]]:
|
||||||
"""
|
"""
|
||||||
Add a handler to the router
|
Add a handler to the router
|
||||||
@@ -103,12 +104,12 @@ class Router(BaseRouter):
|
|||||||
"""
|
"""
|
||||||
if version is not None:
|
if version is not None:
|
||||||
version = str(version).strip("/").lstrip("v")
|
version = str(version).strip("/").lstrip("v")
|
||||||
uri = "/".join([f"/v{version}", uri.lstrip("/")])
|
uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")])
|
||||||
|
|
||||||
params = dict(
|
params = dict(
|
||||||
path=uri,
|
path=uri,
|
||||||
handler=handler,
|
handler=handler,
|
||||||
methods=methods,
|
methods=frozenset(map(str, methods)) if methods else None,
|
||||||
name=name,
|
name=name,
|
||||||
strict=strict_slashes,
|
strict=strict_slashes,
|
||||||
unquote=unquote,
|
unquote=unquote,
|
||||||
@@ -161,7 +162,7 @@ class Router(BaseRouter):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def routes_all(self):
|
def routes_all(self):
|
||||||
return self.routes
|
return {route.parts: route for route in self.routes}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def routes_static(self):
|
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.config import Config
|
||||||
from sanic.exceptions import RequestTimeout, ServiceUnavailable
|
from sanic.exceptions import RequestTimeout, ServiceUnavailable
|
||||||
from sanic.http import Http, Stage
|
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.models.protocol_types import TransportProtocol
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
|
|
||||||
@@ -65,6 +65,7 @@ class ConnInfo:
|
|||||||
__slots__ = (
|
__slots__ = (
|
||||||
"client_port",
|
"client_port",
|
||||||
"client",
|
"client",
|
||||||
|
"client_ip",
|
||||||
"ctx",
|
"ctx",
|
||||||
"peername",
|
"peername",
|
||||||
"server_port",
|
"server_port",
|
||||||
@@ -78,6 +79,7 @@ class ConnInfo:
|
|||||||
self.peername = None
|
self.peername = None
|
||||||
self.server = self.client = ""
|
self.server = self.client = ""
|
||||||
self.server_port = self.client_port = 0
|
self.server_port = self.client_port = 0
|
||||||
|
self.client_ip = ""
|
||||||
self.sockname = addr = transport.get_extra_info("sockname")
|
self.sockname = addr = transport.get_extra_info("sockname")
|
||||||
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
|
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
|
||||||
|
|
||||||
@@ -96,6 +98,7 @@ class ConnInfo:
|
|||||||
|
|
||||||
if isinstance(addr, tuple):
|
if isinstance(addr, tuple):
|
||||||
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
|
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
|
||||||
|
self.client_ip = addr[0]
|
||||||
self.client_port = addr[1]
|
self.client_port = addr[1]
|
||||||
|
|
||||||
|
|
||||||
@@ -122,7 +125,6 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
"response_timeout",
|
"response_timeout",
|
||||||
"keep_alive_timeout",
|
"keep_alive_timeout",
|
||||||
"request_max_size",
|
"request_max_size",
|
||||||
"request_buffer_queue_size",
|
|
||||||
"request_class",
|
"request_class",
|
||||||
"error_handler",
|
"error_handler",
|
||||||
# enable or disable access log purpose
|
# enable or disable access log purpose
|
||||||
@@ -165,9 +167,6 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self.request_handler = self.app.handle_request
|
self.request_handler = self.app.handle_request
|
||||||
self.error_handler = self.app.error_handler
|
self.error_handler = self.app.error_handler
|
||||||
self.request_timeout = self.app.config.REQUEST_TIMEOUT
|
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.response_timeout = self.app.config.RESPONSE_TIMEOUT
|
||||||
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
|
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
|
||||||
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
|
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
|
||||||
@@ -199,11 +198,11 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
except CancelledError:
|
except CancelledError:
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("protocol.connection_task uncaught")
|
error_logger.exception("protocol.connection_task uncaught")
|
||||||
finally:
|
finally:
|
||||||
if self.app.debug and self._http:
|
if self.app.debug and self._http:
|
||||||
ip = self.transport.get_extra_info("peername")
|
ip = self.transport.get_extra_info("peername")
|
||||||
logger.error(
|
error_logger.error(
|
||||||
"Connection lost before response written"
|
"Connection lost before response written"
|
||||||
f" @ {ip} {self._http.request}"
|
f" @ {ip} {self._http.request}"
|
||||||
)
|
)
|
||||||
@@ -212,7 +211,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
try:
|
try:
|
||||||
self.close()
|
self.close()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception("Closing failed")
|
error_logger.exception("Closing failed")
|
||||||
|
|
||||||
async def receive_more(self):
|
async def receive_more(self):
|
||||||
"""
|
"""
|
||||||
@@ -234,11 +233,16 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
if stage is Stage.IDLE and duration > self.keep_alive_timeout:
|
if stage is Stage.IDLE and duration > self.keep_alive_timeout:
|
||||||
logger.debug("KeepAlive Timeout. Closing connection.")
|
logger.debug("KeepAlive Timeout. Closing connection.")
|
||||||
elif stage is Stage.REQUEST and duration > self.request_timeout:
|
elif stage is Stage.REQUEST and duration > self.request_timeout:
|
||||||
|
logger.debug("Request Timeout. Closing connection.")
|
||||||
self._http.exception = RequestTimeout("Request Timeout")
|
self._http.exception = RequestTimeout("Request Timeout")
|
||||||
|
elif stage is Stage.HANDLER and self._http.upgrade_websocket:
|
||||||
|
logger.debug("Handling websocket. Timeouts disabled.")
|
||||||
|
return
|
||||||
elif (
|
elif (
|
||||||
stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED)
|
stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED)
|
||||||
and duration > self.response_timeout
|
and duration > self.response_timeout
|
||||||
):
|
):
|
||||||
|
logger.debug("Response Timeout. Closing connection.")
|
||||||
self._http.exception = ServiceUnavailable("Response Timeout")
|
self._http.exception = ServiceUnavailable("Response Timeout")
|
||||||
else:
|
else:
|
||||||
interval = (
|
interval = (
|
||||||
@@ -253,7 +257,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
return
|
return
|
||||||
self._task.cancel()
|
self._task.cancel()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("protocol.check_timeouts")
|
error_logger.exception("protocol.check_timeouts")
|
||||||
|
|
||||||
async def send(self, data):
|
async def send(self, data):
|
||||||
"""
|
"""
|
||||||
@@ -299,7 +303,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self.recv_buffer = bytearray()
|
self.recv_buffer = bytearray()
|
||||||
self.conn_info = ConnInfo(self.transport, unix=self._unix)
|
self.conn_info = ConnInfo(self.transport, unix=self._unix)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("protocol.connect_made")
|
error_logger.exception("protocol.connect_made")
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
try:
|
try:
|
||||||
@@ -308,7 +312,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
if self._task:
|
if self._task:
|
||||||
self._task.cancel()
|
self._task.cancel()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("protocol.connection_lost")
|
error_logger.exception("protocol.connection_lost")
|
||||||
|
|
||||||
def pause_writing(self):
|
def pause_writing(self):
|
||||||
self._can_write.clear()
|
self._can_write.clear()
|
||||||
@@ -332,7 +336,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
if self._data_received:
|
if self._data_received:
|
||||||
self._data_received.set()
|
self._data_received.set()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("protocol.data_received")
|
error_logger.exception("protocol.data_received")
|
||||||
|
|
||||||
|
|
||||||
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
|
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
|
||||||
@@ -551,7 +555,7 @@ def serve(
|
|||||||
try:
|
try:
|
||||||
http_server = loop.run_until_complete(server_coroutine)
|
http_server = loop.run_until_complete(server_coroutine)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception("Unable to start server")
|
error_logger.exception("Unable to start server")
|
||||||
return
|
return
|
||||||
|
|
||||||
trigger_events(after_start, loop)
|
trigger_events(after_start, loop)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import asyncio
|
|||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
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.exceptions import NotFound # type: ignore
|
||||||
from sanic_routing.utils import path_to_parts # type: ignore
|
from sanic_routing.utils import path_to_parts # type: ignore
|
||||||
|
|
||||||
@@ -20,17 +20,11 @@ RESERVED_NAMESPACES = (
|
|||||||
|
|
||||||
|
|
||||||
class Signal(Route):
|
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:
|
class SignalGroup(RouteGroup):
|
||||||
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 SignalRouter(BaseRouter):
|
class SignalRouter(BaseRouter):
|
||||||
@@ -38,6 +32,7 @@ class SignalRouter(BaseRouter):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
delimiter=".",
|
delimiter=".",
|
||||||
route_class=Signal,
|
route_class=Signal,
|
||||||
|
group_class=SignalGroup,
|
||||||
stacking=True,
|
stacking=True,
|
||||||
)
|
)
|
||||||
self.ctx.loop = None
|
self.ctx.loop = None
|
||||||
@@ -49,7 +44,13 @@ class SignalRouter(BaseRouter):
|
|||||||
):
|
):
|
||||||
extra = condition or {}
|
extra = condition or {}
|
||||||
try:
|
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:
|
except NotFound:
|
||||||
message = "Could not find signal %s"
|
message = "Could not find signal %s"
|
||||||
terms: List[Union[str, Optional[Dict[str, str]]]] = [event]
|
terms: List[Union[str, Optional[Dict[str, str]]]] = [event]
|
||||||
@@ -58,16 +59,26 @@ class SignalRouter(BaseRouter):
|
|||||||
terms.append(extra)
|
terms.append(extra)
|
||||||
raise NotFound(message % tuple(terms))
|
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(
|
async def _dispatch(
|
||||||
self,
|
self,
|
||||||
event: str,
|
event: str,
|
||||||
context: Optional[Dict[str, Any]] = None,
|
context: Optional[Dict[str, Any]] = None,
|
||||||
condition: Optional[Dict[str, str]] = None,
|
condition: Optional[Dict[str, str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
signal, handlers, params = self.get(event, condition=condition)
|
group, handlers, params = self.get(event, condition=condition)
|
||||||
|
|
||||||
signal_event = signal.ctx.event
|
events = [signal.ctx.event for signal in group]
|
||||||
signal_event.set()
|
for signal_event in events:
|
||||||
|
signal_event.set()
|
||||||
if context:
|
if context:
|
||||||
params.update(context)
|
params.update(context)
|
||||||
|
|
||||||
@@ -78,7 +89,8 @@ class SignalRouter(BaseRouter):
|
|||||||
if isawaitable(maybe_coroutine):
|
if isawaitable(maybe_coroutine):
|
||||||
await maybe_coroutine
|
await maybe_coroutine
|
||||||
finally:
|
finally:
|
||||||
signal_event.clear()
|
for signal_event in events:
|
||||||
|
signal_event.clear()
|
||||||
|
|
||||||
async def dispatch(
|
async def dispatch(
|
||||||
self,
|
self,
|
||||||
@@ -116,7 +128,7 @@ class SignalRouter(BaseRouter):
|
|||||||
handler,
|
handler,
|
||||||
requirements=condition,
|
requirements=condition,
|
||||||
name=name,
|
name=name,
|
||||||
overwrite=True,
|
append=True,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
def finalize(self, do_compile: bool = True):
|
def finalize(self, do_compile: bool = True):
|
||||||
@@ -125,7 +137,7 @@ class SignalRouter(BaseRouter):
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
raise RuntimeError("Cannot finalize signals outside of event loop")
|
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()
|
signal.ctx.event = asyncio.Event()
|
||||||
|
|
||||||
return super().finalize(do_compile=do_compile)
|
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(
|
_mod_spec = spec_from_file_location(
|
||||||
name, location, *args, **kwargs
|
name, location, *args, **kwargs
|
||||||
)
|
)
|
||||||
|
assert _mod_spec is not None # type assertion for mypy
|
||||||
module = module_from_spec(_mod_spec)
|
module = module_from_spec(_mod_spec)
|
||||||
_mod_spec.loader.exec_module(module) # type: ignore
|
_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.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import InvalidUsage
|
from sanic.exceptions import InvalidUsage
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
|
||||||
class HTTPMethodView:
|
class HTTPMethodView:
|
||||||
"""Simple class based implementation of view for the sanic.
|
"""Simple class based implementation of view for the sanic.
|
||||||
You should implement methods (get, post, put, patch, delete) for the class
|
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]]] = []
|
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):
|
def dispatch_request(self, request, *args, **kwargs):
|
||||||
handler = getattr(self, request.method.lower(), None)
|
handler = getattr(self, request.method.lower(), None)
|
||||||
return handler(request, *args, **kwargs)
|
return handler(request, *args, **kwargs)
|
||||||
@@ -65,6 +106,31 @@ class HTTPMethodView:
|
|||||||
view.__name__ = cls.__name__
|
view.__name__ = cls.__name__
|
||||||
return view
|
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):
|
def stream(func):
|
||||||
func.is_stream = True
|
func.is_stream = True
|
||||||
@@ -91,6 +157,11 @@ class CompositionView:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
self.name = self.__class__.__name__
|
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):
|
def __name__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@@ -14,9 +14,13 @@ from websockets import ( # type: ignore
|
|||||||
ConnectionClosed,
|
ConnectionClosed,
|
||||||
InvalidHandshake,
|
InvalidHandshake,
|
||||||
WebSocketCommonProtocol,
|
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.exceptions import InvalidUsage
|
||||||
from sanic.server import HttpProtocol
|
from sanic.server import HttpProtocol
|
||||||
|
|
||||||
@@ -37,7 +41,7 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
websocket_write_limit=2 ** 16,
|
websocket_write_limit=2 ** 16,
|
||||||
websocket_ping_interval=20,
|
websocket_ping_interval=20,
|
||||||
websocket_ping_timeout=20,
|
websocket_ping_timeout=20,
|
||||||
**kwargs
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.websocket = None
|
self.websocket = None
|
||||||
@@ -126,7 +130,9 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
ping_interval=self.websocket_ping_interval,
|
ping_interval=self.websocket_ping_interval,
|
||||||
ping_timeout=self.websocket_ping_timeout,
|
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.is_client = False
|
||||||
self.websocket.side = "server"
|
self.websocket.side = "server"
|
||||||
self.websocket.subprotocol = subprotocol
|
self.websocket.subprotocol = subprotocol
|
||||||
@@ -148,7 +154,7 @@ class WebSocketConnection:
|
|||||||
) -> None:
|
) -> None:
|
||||||
self._send = send
|
self._send = send
|
||||||
self._receive = receive
|
self._receive = receive
|
||||||
self.subprotocols = subprotocols or []
|
self._subprotocols = subprotocols or []
|
||||||
|
|
||||||
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
|
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
|
||||||
message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
|
message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
|
||||||
@@ -172,13 +178,28 @@ class WebSocketConnection:
|
|||||||
|
|
||||||
receive = recv
|
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(
|
await self._send(
|
||||||
{
|
{
|
||||||
"type": "websocket.accept",
|
"type": "websocket.accept",
|
||||||
"subprotocol": ",".join(list(self.subprotocols)),
|
"subprotocol": subprotocol,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subprotocols(self):
|
||||||
|
return self._subprotocols
|
||||||
|
|
||||||
|
@subprotocols.setter
|
||||||
|
def subprotocols(self, subprotocols: Optional[List[str]] = None):
|
||||||
|
self._subprotocols = subprotocols or []
|
||||||
|
|||||||
10
setup.py
10
setup.py
@@ -8,7 +8,7 @@ import sys
|
|||||||
|
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import find_packages, setup
|
||||||
from setuptools.command.test import test as TestCommand
|
from setuptools.command.test import test as TestCommand
|
||||||
|
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ with open_local(["README.rst"]) as rm:
|
|||||||
setup_kwargs = {
|
setup_kwargs = {
|
||||||
"name": "sanic",
|
"name": "sanic",
|
||||||
"version": version,
|
"version": version,
|
||||||
"url": "http://github.com/huge-success/sanic/",
|
"url": "http://github.com/sanic-org/sanic/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Sanic Community",
|
"author": "Sanic Community",
|
||||||
"author_email": "admhpkns@gmail.com",
|
"author_email": "admhpkns@gmail.com",
|
||||||
@@ -83,17 +83,17 @@ ujson = "ujson>=1.35" + env_dependency
|
|||||||
uvloop = "uvloop>=0.5.3" + env_dependency
|
uvloop = "uvloop>=0.5.3" + env_dependency
|
||||||
|
|
||||||
requirements = [
|
requirements = [
|
||||||
"sanic-routing",
|
"sanic-routing==0.7.0",
|
||||||
"httptools>=0.0.10",
|
"httptools>=0.0.10",
|
||||||
uvloop,
|
uvloop,
|
||||||
ujson,
|
ujson,
|
||||||
"aiofiles>=0.6.0",
|
"aiofiles>=0.6.0",
|
||||||
"websockets>=8.1,<9.0",
|
"websockets>=9.0",
|
||||||
"multidict>=5.0,<6.0",
|
"multidict>=5.0,<6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"sanic-testing",
|
"sanic-testing>=0.6.0",
|
||||||
"pytest==5.2.1",
|
"pytest==5.2.1",
|
||||||
"multidict>=5.0,<6.0",
|
"multidict>=5.0,<6.0",
|
||||||
"gunicorn==20.0.4",
|
"gunicorn==20.0.4",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from sanic.constants import HTTP_METHODS
|
|||||||
from sanic.router import Router
|
from sanic.router import Router
|
||||||
|
|
||||||
|
|
||||||
|
slugify = re.compile(r"[^a-zA-Z0-9_\-]")
|
||||||
random.seed("Pack my box with five dozen liquor jugs.")
|
random.seed("Pack my box with five dozen liquor jugs.")
|
||||||
Sanic.test_mode = True
|
Sanic.test_mode = True
|
||||||
|
|
||||||
@@ -140,5 +141,5 @@ def url_param_generator():
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def app(request):
|
def app(request):
|
||||||
app = Sanic(request.node.name)
|
app = Sanic(slugify.sub("-", request.node.name))
|
||||||
return app
|
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
|
||||||
1
tests/static/nested/dir/foo.txt
Normal file
1
tests/static/nested/dir/foo.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
foo
|
||||||
@@ -9,6 +9,7 @@ from unittest.mock import Mock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
from sanic.config import Config
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.response import text
|
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 response.status == 500
|
||||||
assert "Mock SanicException" in response.text
|
assert "Mock SanicException" in response.text
|
||||||
assert (
|
assert (
|
||||||
"sanic.root",
|
"sanic.error",
|
||||||
logging.ERROR,
|
logging.ERROR,
|
||||||
f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'",
|
f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'",
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
@@ -389,7 +390,7 @@ def test_app_no_registry_env():
|
|||||||
|
|
||||||
|
|
||||||
def test_app_set_attribute_warning(app):
|
def test_app_set_attribute_warning(app):
|
||||||
with pytest.warns(UserWarning) as record:
|
with pytest.warns(DeprecationWarning) as record:
|
||||||
app.foo = 1
|
app.foo = 1
|
||||||
|
|
||||||
assert len(record) == 1
|
assert len(record) == 1
|
||||||
@@ -412,3 +413,42 @@ def test_subclass_initialisation():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
CustomSanic("test_subclass_initialisation")
|
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 asyncio
|
||||||
import sys
|
|
||||||
|
|
||||||
from collections import deque, namedtuple
|
from collections import deque, namedtuple
|
||||||
|
|
||||||
@@ -219,7 +218,7 @@ async def test_websocket_accept_with_no_subprotocols(
|
|||||||
|
|
||||||
message = message_stack.popleft()
|
message = message_stack.popleft()
|
||||||
assert message["type"] == "websocket.accept"
|
assert message["type"] == "websocket.accept"
|
||||||
assert message["subprotocol"] == ""
|
assert message["subprotocol"] is None
|
||||||
assert "bytes" not in message
|
assert "bytes" not in message
|
||||||
|
|
||||||
|
|
||||||
@@ -228,7 +227,7 @@ async def test_websocket_accept_with_subprotocol(send, receive, message_stack):
|
|||||||
subprotocols = ["graphql-ws"]
|
subprotocols = ["graphql-ws"]
|
||||||
|
|
||||||
ws = WebSocketConnection(send, receive, subprotocols)
|
ws = WebSocketConnection(send, receive, subprotocols)
|
||||||
await ws.accept()
|
await ws.accept(subprotocols)
|
||||||
|
|
||||||
assert len(message_stack) == 1
|
assert len(message_stack) == 1
|
||||||
|
|
||||||
@@ -245,13 +244,13 @@ async def test_websocket_accept_with_multiple_subprotocols(
|
|||||||
subprotocols = ["graphql-ws", "hello", "world"]
|
subprotocols = ["graphql-ws", "hello", "world"]
|
||||||
|
|
||||||
ws = WebSocketConnection(send, receive, subprotocols)
|
ws = WebSocketConnection(send, receive, subprotocols)
|
||||||
await ws.accept()
|
await ws.accept(["hello", "world"])
|
||||||
|
|
||||||
assert len(message_stack) == 1
|
assert len(message_stack) == 1
|
||||||
|
|
||||||
message = message_stack.popleft()
|
message = message_stack.popleft()
|
||||||
assert message["type"] == "websocket.accept"
|
assert message["type"] == "websocket.accept"
|
||||||
assert message["subprotocol"] == "graphql-ws,hello,world"
|
assert message["subprotocol"] == "hello"
|
||||||
assert "bytes" not in message
|
assert "bytes" not in message
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,3 +41,62 @@ def test_bp_repr_with_values(bp):
|
|||||||
'Blueprint(name="my_bp", url_prefix="/foo", host="example.com", '
|
'Blueprint(name="my_bp", url_prefix="/foo", host="example.com", '
|
||||||
"version=3, strict_slashes=True)"
|
"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_1 = Blueprint.group(
|
||||||
Blueprint.group(blueprint_1, blueprint_2)
|
Blueprint.group(blueprint_1, blueprint_2)
|
||||||
)
|
)
|
||||||
assert len(blueprint_group_1) == 2
|
assert len(blueprint_group_1) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_blueprint_group_insert():
|
def test_blueprint_group_insert():
|
||||||
@@ -215,6 +215,61 @@ def test_blueprint_group_insert():
|
|||||||
group.insert(0, blueprint_1)
|
group.insert(0, blueprint_1)
|
||||||
group.insert(0, blueprint_2)
|
group.insert(0, blueprint_2)
|
||||||
group.insert(0, blueprint_3)
|
group.insert(0, blueprint_3)
|
||||||
assert group.blueprints[1].strict_slashes is False
|
|
||||||
assert group.blueprints[2].strict_slashes is True
|
@blueprint_1.route("/")
|
||||||
assert group.blueprints[0].url_prefix == "/test"
|
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():
|
def test_bp_set_attribute_warning():
|
||||||
bp = Blueprint("bp")
|
bp = Blueprint("bp")
|
||||||
with pytest.warns(UserWarning) as record:
|
with pytest.warns(DeprecationWarning) as record:
|
||||||
bp.foo = 1
|
bp.foo = 1
|
||||||
|
|
||||||
assert len(record) == 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")
|
app.config.load("test_config.Config.test")
|
||||||
|
|
||||||
|
|
||||||
def test_auto_load_env():
|
def test_auto_env_prefix():
|
||||||
environ["SANIC_TEST_ANSWER"] = "42"
|
environ["SANIC_TEST_ANSWER"] = "42"
|
||||||
app = Sanic(name=__name__)
|
app = Sanic(name=__name__)
|
||||||
assert app.config.TEST_ANSWER == 42
|
assert app.config.TEST_ANSWER == 42
|
||||||
del environ["SANIC_TEST_ANSWER"]
|
del environ["SANIC_TEST_ANSWER"]
|
||||||
|
|
||||||
|
|
||||||
def test_auto_load_bool_env():
|
def test_auto_bool_env_prefix():
|
||||||
environ["SANIC_TEST_ANSWER"] = "True"
|
environ["SANIC_TEST_ANSWER"] = "True"
|
||||||
app = Sanic(name=__name__)
|
app = Sanic(name=__name__)
|
||||||
assert app.config.TEST_ANSWER is True
|
assert app.config.TEST_ANSWER is True
|
||||||
@@ -80,6 +80,12 @@ def test_dont_load_env():
|
|||||||
del environ["SANIC_TEST_ANSWER"]
|
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():
|
def test_load_env_prefix():
|
||||||
environ["MYAPP_TEST_ANSWER"] = "42"
|
environ["MYAPP_TEST_ANSWER"] = "42"
|
||||||
app = Sanic(name=__name__, load_env="MYAPP_")
|
app = Sanic(name=__name__, load_env="MYAPP_")
|
||||||
@@ -87,6 +93,14 @@ def test_load_env_prefix():
|
|||||||
del environ["MYAPP_TEST_ANSWER"]
|
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():
|
def test_load_env_prefix_float_values():
|
||||||
environ["MYAPP_TEST_ROI"] = "2.3"
|
environ["MYAPP_TEST_ROI"] = "2.3"
|
||||||
app = Sanic(name=__name__, load_env="MYAPP_")
|
app = Sanic(name=__name__, load_env="MYAPP_")
|
||||||
@@ -101,6 +115,27 @@ def test_load_env_prefix_string_value():
|
|||||||
del environ["MYAPP_TEST_TOKEN"]
|
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):
|
def test_load_from_file(app):
|
||||||
config = dedent(
|
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
|
import pytest
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
@@ -7,6 +9,7 @@ from sanic.exceptions import (
|
|||||||
Forbidden,
|
Forbidden,
|
||||||
InvalidUsage,
|
InvalidUsage,
|
||||||
NotFound,
|
NotFound,
|
||||||
|
SanicException,
|
||||||
ServerError,
|
ServerError,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
abort,
|
abort,
|
||||||
@@ -68,16 +71,19 @@ def exception_app():
|
|||||||
|
|
||||||
@app.route("/abort/401")
|
@app.route("/abort/401")
|
||||||
def handler_401_error(request):
|
def handler_401_error(request):
|
||||||
abort(401)
|
raise SanicException(status_code=401)
|
||||||
|
|
||||||
@app.route("/abort")
|
@app.route("/abort")
|
||||||
def handler_500_error(request):
|
def handler_500_error(request):
|
||||||
|
raise SanicException(status_code=500)
|
||||||
|
|
||||||
|
@app.route("/old_abort")
|
||||||
|
def handler_old_abort_error(request):
|
||||||
abort(500)
|
abort(500)
|
||||||
return text("OK")
|
|
||||||
|
|
||||||
@app.route("/abort/message")
|
@app.route("/abort/message")
|
||||||
def handler_abort_message(request):
|
def handler_abort_message(request):
|
||||||
abort(500, message="Abort")
|
raise SanicException(message="Custom Message", status_code=500)
|
||||||
|
|
||||||
@app.route("/divide_by_zero")
|
@app.route("/divide_by_zero")
|
||||||
def handle_unhandled_exception(request):
|
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 ")
|
assert response.body.startswith(b"Exception raised in exception ")
|
||||||
|
|
||||||
|
|
||||||
def test_abort(exception_app):
|
def test_sanic_exception(exception_app):
|
||||||
"""Test the abort function"""
|
"""Test sanic exceptions are handled"""
|
||||||
request, response = exception_app.test_client.get("/abort/401")
|
request, response = exception_app.test_client.get("/abort/401")
|
||||||
assert response.status == 401
|
assert response.status == 401
|
||||||
|
|
||||||
request, response = exception_app.test_client.get("/abort")
|
request, response = exception_app.test_client.get("/abort")
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
|
# check fallback message
|
||||||
|
assert "Internal Server Error" in response.text
|
||||||
|
|
||||||
request, response = exception_app.test_client.get("/abort/message")
|
request, response = exception_app.test_client.get("/abort/message")
|
||||||
assert response.status == 500
|
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
|
from sanic.http import Http
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def raised_ceiling():
|
||||||
|
Http.HEADER_CEILING = 32_768
|
||||||
|
yield
|
||||||
|
Http.HEADER_CEILING = 16_384
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"input, expected",
|
"input, expected",
|
||||||
[
|
[
|
||||||
@@ -76,15 +83,75 @@ async def test_header_size_exceeded():
|
|||||||
recv_buffer += b"123"
|
recv_buffer += b"123"
|
||||||
|
|
||||||
protocol = Mock()
|
protocol = Mock()
|
||||||
|
Http.set_header_max_size(1)
|
||||||
http = Http(protocol)
|
http = Http(protocol)
|
||||||
http._receive_more = _receive_more
|
http._receive_more = _receive_more
|
||||||
http.request_max_size = 1
|
|
||||||
http.recv_buffer = recv_buffer
|
http.recv_buffer = recv_buffer
|
||||||
|
|
||||||
with pytest.raises(PayloadTooLarge):
|
with pytest.raises(PayloadTooLarge):
|
||||||
await http.http1_request_header()
|
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):
|
def test_raw_headers(app):
|
||||||
app.route("/")(lambda _: text(""))
|
app.route("/")(lambda _: text(""))
|
||||||
request, _ = app.test_client.get(
|
request, _ = app.test_client.get(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import platform
|
||||||
|
|
||||||
from asyncio import sleep as aio_sleep
|
from asyncio import sleep as aio_sleep
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
@@ -241,7 +242,9 @@ def test_keep_alive_timeout_reuse():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@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",
|
reason="Not testable with current client",
|
||||||
)
|
)
|
||||||
def test_keep_alive_client_timeout():
|
def test_keep_alive_client_timeout():
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ def test_logging_pass_customer_logconfig():
|
|||||||
def test_log_connection_lost(app, debug, monkeypatch):
|
def test_log_connection_lost(app, debug, monkeypatch):
|
||||||
""" Should not log Connection lost exception on non debug """
|
""" Should not log Connection lost exception on non debug """
|
||||||
stream = StringIO()
|
stream = StringIO()
|
||||||
root = logging.getLogger("sanic.root")
|
error = logging.getLogger("sanic.error")
|
||||||
root.addHandler(logging.StreamHandler(stream))
|
error.addHandler(logging.StreamHandler(stream))
|
||||||
monkeypatch.setattr(sanic.server, "logger", root)
|
monkeypatch.setattr(sanic.server, "error_logger", error)
|
||||||
|
|
||||||
@app.route("/conn_lost")
|
@app.route("/conn_lost")
|
||||||
async def conn_lost(request):
|
async def conn_lost(request):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
from itertools import count
|
from itertools import count
|
||||||
|
|
||||||
from sanic.exceptions import NotFound, SanicException
|
from sanic.exceptions import NotFound
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import HTTPResponse, text
|
from sanic.response import HTTPResponse, text
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ def test_middleware_response_raise_cancelled_error(app, caplog):
|
|||||||
|
|
||||||
assert response.status == 503
|
assert response.status == 503
|
||||||
assert (
|
assert (
|
||||||
"sanic.root",
|
"sanic.error",
|
||||||
logging.ERROR,
|
logging.ERROR,
|
||||||
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
||||||
) not in caplog.record_tuples
|
) not in caplog.record_tuples
|
||||||
@@ -174,7 +174,7 @@ def test_middleware_response_raise_exception(app, caplog):
|
|||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
# 404 errors are not logged
|
# 404 errors are not logged
|
||||||
assert (
|
assert (
|
||||||
"sanic.root",
|
"sanic.error",
|
||||||
logging.ERROR,
|
logging.ERROR,
|
||||||
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
||||||
) not in caplog.record_tuples
|
) not in caplog.record_tuples
|
||||||
|
|||||||
@@ -209,13 +209,13 @@ def test_named_static_routes():
|
|||||||
return text("OK2")
|
return text("OK2")
|
||||||
|
|
||||||
assert app.router.routes_all[("test",)].name == "app.route_test"
|
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"
|
assert app.url_for("route_test") == "/test"
|
||||||
with pytest.raises(URLBuildError):
|
with pytest.raises(URLBuildError):
|
||||||
app.url_for("handler1")
|
app.url_for("handler1")
|
||||||
|
|
||||||
assert app.router.routes_all[("pizazz",)].name == "app.route_pizazz"
|
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"
|
assert app.url_for("route_pizazz") == "/pizazz"
|
||||||
with pytest.raises(URLBuildError):
|
with pytest.raises(URLBuildError):
|
||||||
app.url_for("handler2")
|
app.url_for("handler2")
|
||||||
@@ -234,7 +234,7 @@ def test_named_dynamic_route():
|
|||||||
app.router.routes_all[
|
app.router.routes_all[
|
||||||
(
|
(
|
||||||
"folder",
|
"folder",
|
||||||
"<name>",
|
"<name:str>",
|
||||||
)
|
)
|
||||||
].name
|
].name
|
||||||
== "app.route_dynamic"
|
== "app.route_dynamic"
|
||||||
@@ -347,13 +347,13 @@ def test_static_add_named_route():
|
|||||||
app.add_route(handler2, "/test2", name="route_test2")
|
app.add_route(handler2, "/test2", name="route_test2")
|
||||||
|
|
||||||
assert app.router.routes_all[("test",)].name == "app.route_test"
|
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"
|
assert app.url_for("route_test") == "/test"
|
||||||
with pytest.raises(URLBuildError):
|
with pytest.raises(URLBuildError):
|
||||||
app.url_for("handler1")
|
app.url_for("handler1")
|
||||||
|
|
||||||
assert app.router.routes_all[("test2",)].name == "app.route_test2"
|
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"
|
assert app.url_for("route_test2") == "/test2"
|
||||||
with pytest.raises(URLBuildError):
|
with pytest.raises(URLBuildError):
|
||||||
app.url_for("handler2")
|
app.url_for("handler2")
|
||||||
@@ -369,7 +369,8 @@ def test_dynamic_add_named_route():
|
|||||||
|
|
||||||
app.add_route(handler, "/folder/<name>", name="route_dynamic")
|
app.add_route(handler, "/folder/<name>", name="route_dynamic")
|
||||||
assert (
|
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"
|
assert app.url_for("route_dynamic", name="test") == "/folder/test"
|
||||||
with pytest.raises(URLBuildError):
|
with pytest.raises(URLBuildError):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from urllib.parse import quote, unquote
|
from urllib.parse import quote
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
flags = 0
|
flags = 0
|
||||||
|
|
||||||
|
TIMER_DELAY = 2
|
||||||
|
|
||||||
|
|
||||||
def terminate(proc):
|
def terminate(proc):
|
||||||
if flags:
|
if flags:
|
||||||
@@ -56,6 +58,40 @@ def write_app(filename, **runargs):
|
|||||||
return text
|
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):
|
def scanner(proc):
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
line = line.decode().strip()
|
line = line.decode().strip()
|
||||||
@@ -90,9 +126,10 @@ async def test_reloader_live(runargs, mode):
|
|||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
filename = os.path.join(tmpdir, "reloader.py")
|
filename = os.path.join(tmpdir, "reloader.py")
|
||||||
text = write_app(filename, **runargs)
|
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:
|
try:
|
||||||
timeout = Timer(5, terminate, [proc])
|
timeout = Timer(TIMER_DELAY, terminate, [proc])
|
||||||
timeout.start()
|
timeout.start()
|
||||||
# Python apparently keeps using the old source sometimes if
|
# Python apparently keeps using the old source sometimes if
|
||||||
# we don't sleep before rewrite (pycache timestamp problem?)
|
# we don't sleep before rewrite (pycache timestamp problem?)
|
||||||
@@ -107,3 +144,40 @@ async def test_reloader_live(runargs, mode):
|
|||||||
terminate(proc)
|
terminate(proc)
|
||||||
with suppress(TimeoutExpired):
|
with suppress(TimeoutExpired):
|
||||||
proc.wait(timeout=3)
|
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())
|
monkeypatch.setattr(Request, "generate_id", Mock())
|
||||||
Request.generate_id.return_value = 1
|
Request.generate_id.return_value = 1
|
||||||
request = Request(b"/", {}, None, "GET", None, Mock())
|
request = Request(b"/", {}, None, "GET", None, Mock())
|
||||||
|
request.app.config.REQUEST_ID_HEADER = "foo"
|
||||||
|
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
request.id
|
request.id
|
||||||
@@ -28,6 +29,7 @@ def test_request_id_generates_from_request(monkeypatch):
|
|||||||
|
|
||||||
def test_request_id_defaults_uuid():
|
def test_request_id_defaults_uuid():
|
||||||
request = Request(b"/", {}, None, "GET", None, Mock())
|
request = Request(b"/", {}, None, "GET", None, Mock())
|
||||||
|
request.app.config.REQUEST_ID_HEADER = "foo"
|
||||||
|
|
||||||
assert isinstance(request.id, UUID)
|
assert isinstance(request.id, UUID)
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ def test_route_assigned_to_request(app):
|
|||||||
return response.empty()
|
return response.empty()
|
||||||
|
|
||||||
request, _ = app.test_client.get("/")
|
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):
|
def test_protocol_attribute(app):
|
||||||
@@ -120,3 +122,21 @@ def test_protocol_attribute(app):
|
|||||||
_ = app.test_client.get("/", headers=headers)
|
_ = app.test_client.get("/", headers=headers)
|
||||||
|
|
||||||
assert isinstance(retrieved, HttpProtocol)
|
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 import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
from sanic.server import HttpProtocol
|
|
||||||
from sanic.views import CompositionView, HTTPMethodView
|
from sanic.views import CompositionView, HTTPMethodView
|
||||||
from sanic.views import stream as stream_decorator
|
from sanic.views import stream as stream_decorator
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
import httpcore
|
import httpcore
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from httpcore._async.base import (
|
|
||||||
AsyncByteStream,
|
|
||||||
AsyncHTTPTransport,
|
|
||||||
ConnectionState,
|
|
||||||
NewConnectionRequired,
|
|
||||||
)
|
|
||||||
from httpcore._async.connection import AsyncHTTPConnection
|
|
||||||
from httpcore._async.connection_pool import ResponseByteStream
|
|
||||||
from httpcore._exceptions import LocalProtocolError, UnsupportedProtocol
|
|
||||||
from httpcore._types import TimeoutDict
|
|
||||||
from httpcore._utils import url_to_origin
|
|
||||||
from sanic_testing.testing import SanicTestClient
|
from sanic_testing.testing import SanicTestClient
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|||||||
@@ -253,6 +253,31 @@ async def test_empty_json_asgi(app):
|
|||||||
assert response.body == b"null"
|
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):
|
def test_invalid_json(app):
|
||||||
@app.post("/")
|
@app.post("/")
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
@@ -292,6 +317,17 @@ def test_query_string(app):
|
|||||||
assert request.args.get("test3", default="My value") == "My value"
|
assert request.args.get("test3", default="My value") == "My value"
|
||||||
|
|
||||||
|
|
||||||
|
def test_popped_stays_popped(app):
|
||||||
|
@app.route("/")
|
||||||
|
async def handler(request):
|
||||||
|
return text("OK")
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/", params=[("test1", "1")])
|
||||||
|
|
||||||
|
assert request.args.pop("test1") == ["1"]
|
||||||
|
assert "test1" not in request.args
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_query_string_asgi(app):
|
async def test_query_string_asgi(app):
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@@ -2159,3 +2195,70 @@ def test_safe_method_with_body(app):
|
|||||||
assert request.body == data.encode("utf-8")
|
assert request.body == data.encode("utf-8")
|
||||||
assert request.json.get("test") == "OK"
|
assert request.json.get("test") == "OK"
|
||||||
assert response.body == b"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 asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import warnings
|
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from random import choice
|
from random import choice
|
||||||
from unittest.mock import MagicMock
|
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiofiles import os as async_os
|
from aiofiles import os as async_os
|
||||||
from sanic_testing.testing import HOST, PORT
|
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import (
|
from sanic.response import (
|
||||||
HTTPResponse,
|
HTTPResponse,
|
||||||
StreamingHTTPResponse,
|
|
||||||
empty,
|
empty,
|
||||||
file,
|
file,
|
||||||
file_stream,
|
file_stream,
|
||||||
@@ -26,7 +22,6 @@ from sanic.response import (
|
|||||||
stream,
|
stream,
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
from sanic.server import HttpProtocol
|
|
||||||
|
|
||||||
|
|
||||||
JSON_DATA = {"ok": True}
|
JSON_DATA = {"ok": True}
|
||||||
@@ -65,7 +60,9 @@ def test_method_not_allowed():
|
|||||||
}
|
}
|
||||||
|
|
||||||
request, response = app.test_client.post("/")
|
request, response = app.test_client.post("/")
|
||||||
assert set(response.headers["Allow"].split(", ")) == {"GET", "HEAD"}
|
assert set(response.headers["Allow"].split(", ")) == {
|
||||||
|
"GET",
|
||||||
|
}
|
||||||
|
|
||||||
app.router.reset()
|
app.router.reset()
|
||||||
|
|
||||||
@@ -78,7 +75,6 @@ def test_method_not_allowed():
|
|||||||
assert set(response.headers["Allow"].split(", ")) == {
|
assert set(response.headers["Allow"].split(", ")) == {
|
||||||
"GET",
|
"GET",
|
||||||
"POST",
|
"POST",
|
||||||
"HEAD",
|
|
||||||
}
|
}
|
||||||
assert response.headers["Content-Length"] == "0"
|
assert response.headers["Content-Length"] == "0"
|
||||||
|
|
||||||
@@ -87,7 +83,6 @@ def test_method_not_allowed():
|
|||||||
assert set(response.headers["Allow"].split(", ")) == {
|
assert set(response.headers["Allow"].split(", ")) == {
|
||||||
"GET",
|
"GET",
|
||||||
"POST",
|
"POST",
|
||||||
"HEAD",
|
|
||||||
}
|
}
|
||||||
assert response.headers["Content-Length"] == "0"
|
assert response.headers["Content-Length"] == "0"
|
||||||
|
|
||||||
@@ -229,7 +224,6 @@ def non_chunked_streaming_app(app):
|
|||||||
sample_streaming_fn,
|
sample_streaming_fn,
|
||||||
headers={"Content-Length": "7"},
|
headers={"Content-Length": "7"},
|
||||||
content_type="text/csv",
|
content_type="text/csv",
|
||||||
chunked=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return app
|
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):
|
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("/")
|
||||||
request, response = non_chunked_streaming_app.test_client.get("/")
|
|
||||||
|
|
||||||
assert len(record) == 1
|
|
||||||
assert "removed in v21.6" in record[0].message.args[0]
|
|
||||||
|
|
||||||
assert "Transfer-Encoding" not in response.headers
|
assert "Transfer-Encoding" not in response.headers
|
||||||
assert response.headers["Content-Type"] == "text/csv"
|
assert response.headers["Content-Type"] == "text/csv"
|
||||||
@@ -534,3 +524,19 @@ def test_empty_response(app):
|
|||||||
request, response = app.test_client.get("/test")
|
request, response = app.test_client.get("/test")
|
||||||
assert response.content_type is None
|
assert response.content_type is None
|
||||||
assert response.body == b""
|
assert response.body == b""
|
||||||
|
|
||||||
|
|
||||||
|
def test_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
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.exceptions import ServiceUnavailable
|
from sanic.exceptions import ServiceUnavailable
|
||||||
|
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
@@ -13,6 +17,8 @@ response_timeout_app.config.RESPONSE_TIMEOUT = 1
|
|||||||
response_timeout_default_app.config.RESPONSE_TIMEOUT = 1
|
response_timeout_default_app.config.RESPONSE_TIMEOUT = 1
|
||||||
response_handler_cancelled_app.config.RESPONSE_TIMEOUT = 1
|
response_handler_cancelled_app.config.RESPONSE_TIMEOUT = 1
|
||||||
|
|
||||||
|
response_handler_cancelled_app.ctx.flag = False
|
||||||
|
|
||||||
|
|
||||||
@response_timeout_app.route("/1")
|
@response_timeout_app.route("/1")
|
||||||
async def handler_1(request):
|
async def handler_1(request):
|
||||||
@@ -25,32 +31,17 @@ def handler_exception(request, exception):
|
|||||||
return text("Response Timeout from error_handler.", 503)
|
return text("Response Timeout from error_handler.", 503)
|
||||||
|
|
||||||
|
|
||||||
def test_server_error_response_timeout():
|
|
||||||
request, response = response_timeout_app.test_client.get("/1")
|
|
||||||
assert response.status == 503
|
|
||||||
assert response.text == "Response Timeout from error_handler."
|
|
||||||
|
|
||||||
|
|
||||||
@response_timeout_default_app.route("/1")
|
@response_timeout_default_app.route("/1")
|
||||||
async def handler_2(request):
|
async def handler_2(request):
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
|
|
||||||
def test_default_server_error_response_timeout():
|
|
||||||
request, response = response_timeout_default_app.test_client.get("/1")
|
|
||||||
assert response.status == 503
|
|
||||||
assert "Response Timeout" in response.text
|
|
||||||
|
|
||||||
|
|
||||||
response_handler_cancelled_app.flag = False
|
|
||||||
|
|
||||||
|
|
||||||
@response_handler_cancelled_app.exception(asyncio.CancelledError)
|
@response_handler_cancelled_app.exception(asyncio.CancelledError)
|
||||||
def handler_cancelled(request, exception):
|
def handler_cancelled(request, exception):
|
||||||
# If we get a CancelledError, it means sanic has already sent a response,
|
# If we get a CancelledError, it means sanic has already sent a response,
|
||||||
# we should not ever have to handle a CancelledError.
|
# we should not ever have to handle a CancelledError.
|
||||||
response_handler_cancelled_app.flag = True
|
response_handler_cancelled_app.ctx.flag = True
|
||||||
return text("App received CancelledError!", 500)
|
return text("App received CancelledError!", 500)
|
||||||
# The client will never receive this response, because the socket
|
# The client will never receive this response, because the socket
|
||||||
# is already closed when we get a CancelledError.
|
# is already closed when we get a CancelledError.
|
||||||
@@ -62,8 +53,44 @@ async def handler_3(request):
|
|||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
|
|
||||||
|
def test_server_error_response_timeout():
|
||||||
|
request, response = response_timeout_app.test_client.get("/1")
|
||||||
|
assert response.status == 503
|
||||||
|
assert response.text == "Response Timeout from error_handler."
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_server_error_response_timeout():
|
||||||
|
request, response = response_timeout_default_app.test_client.get("/1")
|
||||||
|
assert response.status == 503
|
||||||
|
assert "Response Timeout" in response.text
|
||||||
|
|
||||||
|
|
||||||
def test_response_handler_cancelled():
|
def test_response_handler_cancelled():
|
||||||
request, response = response_handler_cancelled_app.test_client.get("/1")
|
request, response = response_handler_cancelled_app.test_client.get("/1")
|
||||||
assert response.status == 503
|
assert response.status == 503
|
||||||
assert "Response Timeout" in response.text
|
assert "Response Timeout" in response.text
|
||||||
assert response_handler_cancelled_app.flag is False
|
assert response_handler_cancelled_app.ctx.flag is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_response_timeout_not_applied(caplog):
|
||||||
|
modified_config = LOGGING_CONFIG_DEFAULTS
|
||||||
|
modified_config["loggers"]["sanic.root"]["level"] = "DEBUG"
|
||||||
|
|
||||||
|
app = Sanic("test_logging", log_config=modified_config)
|
||||||
|
app.config.RESPONSE_TIMEOUT = 1
|
||||||
|
app.ctx.event = asyncio.Event()
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def ws_handler(request, ws):
|
||||||
|
sleep(2)
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
request.app.ctx.event.set()
|
||||||
|
|
||||||
|
with caplog.at_level(logging.DEBUG):
|
||||||
|
_ = app.test_client.websocket("/ws")
|
||||||
|
assert app.ctx.event.is_set()
|
||||||
|
assert (
|
||||||
|
"sanic.root",
|
||||||
|
10,
|
||||||
|
"Handling websocket. Timeouts disabled.",
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ def test_route_strict_slash(app):
|
|||||||
def test_route_invalid_parameter_syntax(app):
|
def test_route_invalid_parameter_syntax(app):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
|
||||||
@app.get("/get/<:string>", strict_slashes=True)
|
@app.get("/get/<:str>", strict_slashes=True)
|
||||||
def handler(request):
|
def handler(request):
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ def test_dynamic_route(app):
|
|||||||
def test_dynamic_route_string(app):
|
def test_dynamic_route_string(app):
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
@app.route("/folder/<name:string>")
|
@app.route("/folder/<name:str>")
|
||||||
async def handler(request, name):
|
async def handler(request, name):
|
||||||
results.append(name)
|
results.append(name)
|
||||||
return text("OK")
|
return text("OK")
|
||||||
@@ -513,7 +513,7 @@ def test_dynamic_route_int(app):
|
|||||||
def test_dynamic_route_number(app):
|
def test_dynamic_route_number(app):
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
@app.route("/weight/<weight:number>")
|
@app.route("/weight/<weight:float>")
|
||||||
async def handler(request, weight):
|
async def handler(request, weight):
|
||||||
results.append(weight)
|
results.append(weight)
|
||||||
return text("OK")
|
return text("OK")
|
||||||
@@ -543,9 +543,6 @@ def test_dynamic_route_regex(app):
|
|||||||
async def handler(request, folder_id):
|
async def handler(request, folder_id):
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
app.router.finalize()
|
|
||||||
print(app.router.find_route_src)
|
|
||||||
|
|
||||||
request, response = app.test_client.get("/folder/test")
|
request, response = app.test_client.get("/folder/test")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
@@ -587,6 +584,8 @@ def test_dynamic_route_path(app):
|
|||||||
async def handler(request, path):
|
async def handler(request, path):
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
|
app.router.finalize()
|
||||||
|
|
||||||
request, response = app.test_client.get("/path/1/info")
|
request, response = app.test_client.get("/path/1/info")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
@@ -824,7 +823,7 @@ def test_dynamic_add_route_string(app):
|
|||||||
results.append(name)
|
results.append(name)
|
||||||
return text("OK")
|
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")
|
request, response = app.test_client.get("/folder/test123")
|
||||||
|
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
@@ -860,7 +859,7 @@ def test_dynamic_add_route_number(app):
|
|||||||
results.append(weight)
|
results.append(weight)
|
||||||
return text("OK")
|
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")
|
request, response = app.test_client.get("/weight/12345")
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
@@ -1008,14 +1007,8 @@ def test_unmergeable_overload_routes(app):
|
|||||||
async def handler2(request):
|
async def handler2(request):
|
||||||
return text("OK1")
|
return text("OK1")
|
||||||
|
|
||||||
assert (
|
assert len(app.router.static_routes) == 1
|
||||||
len(
|
assert len(app.router.static_routes[("overload_whole",)].methods) == 3
|
||||||
dict(list(app.router.static_routes.values())[0].handlers)[
|
|
||||||
"overload_whole"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
== 3
|
|
||||||
)
|
|
||||||
|
|
||||||
request, response = app.test_client.get("/overload_whole")
|
request, response = app.test_client.get("/overload_whole")
|
||||||
assert response.text == "OK1"
|
assert response.text == "OK1"
|
||||||
@@ -1073,7 +1066,8 @@ def test_uri_with_different_method_and_different_params(app):
|
|||||||
return json({"action": action})
|
return json({"action": action})
|
||||||
|
|
||||||
request, response = app.test_client.get("/ads/1234")
|
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")
|
request, response = app.test_client.post("/ads/post")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
@@ -1175,3 +1169,59 @@ def test_route_with_bad_named_param(app):
|
|||||||
|
|
||||||
with pytest.raises(SanicException):
|
with pytest.raises(SanicException):
|
||||||
app.router.finalize()
|
app.router.finalize()
|
||||||
|
|
||||||
|
|
||||||
|
def test_routes_with_and_without_slash_definitions(app):
|
||||||
|
bar = Blueprint("bar", url_prefix="bar")
|
||||||
|
baz = Blueprint("baz", url_prefix="/baz")
|
||||||
|
fizz = Blueprint("fizz", url_prefix="fizz/")
|
||||||
|
buzz = Blueprint("buzz", url_prefix="/buzz/")
|
||||||
|
|
||||||
|
instances = (
|
||||||
|
(app, "foo"),
|
||||||
|
(bar, "bar"),
|
||||||
|
(baz, "baz"),
|
||||||
|
(fizz, "fizz"),
|
||||||
|
(buzz, "buzz"),
|
||||||
|
)
|
||||||
|
|
||||||
|
for instance, term in instances:
|
||||||
|
route = f"/{term}" if isinstance(instance, Sanic) else ""
|
||||||
|
|
||||||
|
@instance.get(route, strict_slashes=True)
|
||||||
|
def get_without(request):
|
||||||
|
return text(f"{term}_without")
|
||||||
|
|
||||||
|
@instance.get(f"{route}/", strict_slashes=True)
|
||||||
|
def get_with(request):
|
||||||
|
return text(f"{term}_with")
|
||||||
|
|
||||||
|
@instance.post(route, strict_slashes=True)
|
||||||
|
def post_without(request):
|
||||||
|
return text(f"{term}_without")
|
||||||
|
|
||||||
|
@instance.post(f"{route}/", strict_slashes=True)
|
||||||
|
def post_with(request):
|
||||||
|
return text(f"{term}_with")
|
||||||
|
|
||||||
|
app.blueprint(bar)
|
||||||
|
app.blueprint(baz)
|
||||||
|
app.blueprint(fizz)
|
||||||
|
app.blueprint(buzz)
|
||||||
|
|
||||||
|
for _, term in instances:
|
||||||
|
_, response = app.test_client.get(f"/{term}")
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == f"{term}_without"
|
||||||
|
|
||||||
|
_, response = app.test_client.get(f"/{term}/")
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == f"{term}_with"
|
||||||
|
|
||||||
|
_, response = app.test_client.post(f"/{term}")
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == f"{term}_without"
|
||||||
|
|
||||||
|
_, response = app.test_client.post(f"/{term}/")
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == f"{term}_with"
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ def test_add_signal_decorator(app):
|
|||||||
async def async_signal(*_):
|
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(
|
@pytest.mark.parametrize(
|
||||||
@@ -79,13 +80,13 @@ async def test_dispatch_signal_triggers_triggers_event(app):
|
|||||||
def sync_signal(*args):
|
def sync_signal(*args):
|
||||||
nonlocal app
|
nonlocal app
|
||||||
nonlocal counter
|
nonlocal counter
|
||||||
signal, *_ = app.signal_router.get("foo.bar.baz")
|
group, *_ = app.signal_router.get("foo.bar.baz")
|
||||||
counter += signal.ctx.event.is_set()
|
for signal in group:
|
||||||
|
counter += signal.ctx.event.is_set()
|
||||||
|
|
||||||
app.signal_router.finalize()
|
app.signal_router.finalize()
|
||||||
|
|
||||||
await app.dispatch("foo.bar.baz")
|
await app.dispatch("foo.bar.baz")
|
||||||
signal, *_ = app.signal_router.get("foo.bar.baz")
|
|
||||||
|
|
||||||
assert counter == 1
|
assert counter == 1
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ async def test_dispatch_signal_triggers_event_on_bp(app):
|
|||||||
|
|
||||||
app.blueprint(bp)
|
app.blueprint(bp)
|
||||||
app.signal_router.finalize()
|
app.signal_router.finalize()
|
||||||
signal, *_ = app.signal_router.get(
|
signal_group, *_ = app.signal_router.get(
|
||||||
"foo.bar.baz", condition={"blueprint": "bp"}
|
"foo.bar.baz", condition={"blueprint": "bp"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -233,7 +234,8 @@ async def test_dispatch_signal_triggers_event_on_bp(app):
|
|||||||
assert isawaitable(waiter)
|
assert isawaitable(waiter)
|
||||||
|
|
||||||
fut = asyncio.ensure_future(do_wait())
|
fut = asyncio.ensure_future(do_wait())
|
||||||
signal.ctx.event.set()
|
for signal in signal_group:
|
||||||
|
signal.ctx.event.set()
|
||||||
await fut
|
await fut
|
||||||
|
|
||||||
assert bp_counter == 1
|
assert bp_counter == 1
|
||||||
@@ -255,17 +257,60 @@ def test_bad_finalize(app):
|
|||||||
assert counter == 0
|
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"):
|
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")
|
bp = Blueprint("bp")
|
||||||
app.blueprint(bp)
|
app.blueprint(bp)
|
||||||
|
|
||||||
with pytest.raises(NotFound, match="Could not find signal does.not.exist"):
|
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():
|
def test_event_on_bp_not_registered():
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sanic import text
|
||||||
|
from sanic.exceptions import FileNotFound
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def static_file_directory():
|
def static_file_directory():
|
||||||
@@ -445,3 +450,60 @@ def test_static_name(app, static_file_directory, static_name, file_name):
|
|||||||
request, response = app.test_client.get(f"/static/{file_name}")
|
request, response = app.test_client.get(f"/static/{file_name}")
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_dir(app, static_file_directory):
|
||||||
|
app.static("/static", static_file_directory)
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/static/nested/dir/foo.txt")
|
||||||
|
|
||||||
|
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] == 1
|
||||||
|
|
||||||
|
|
||||||
|
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
|
import asyncio
|
||||||
|
|
||||||
from time import monotonic as current_time
|
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user