Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05002d7ee4 | ||
|
|
b4360d4a20 | ||
|
|
3b85b3bbad | ||
|
|
6e55e73da1 | ||
|
|
89d942451f | ||
|
|
4d6205e6fe | ||
|
|
1684b0b986 | ||
|
|
4f5faa4a3c | ||
|
|
cbb77b536a | ||
|
|
35c76253bf | ||
|
|
8d86c3c598 | ||
|
|
97635111af | ||
|
|
7f3fe40cd4 | ||
|
|
ea34bcd849 | ||
|
|
05f758583b | ||
|
|
760c74a293 | ||
|
|
9def46beb8 | ||
|
|
04be8e95a5 | ||
|
|
78ced20fc7 | ||
|
|
c3003413d3 | ||
|
|
fe3fdc5d83 | ||
|
|
b66fb6f9e8 | ||
|
|
bf6175fb20 | ||
|
|
7475897a03 | ||
|
|
58ca887be4 | ||
|
|
449bc417a3 | ||
|
|
262f89f2b6 | ||
|
|
38337446cf | ||
|
|
ac1331ea4c | ||
|
|
2b947e831f | ||
|
|
112715eb80 | ||
|
|
ea9cf365bc | ||
|
|
b9b3b4051a | ||
|
|
ecb6db29e6 | ||
|
|
6515dde64b | ||
|
|
01d2a2aa3c | ||
|
|
39e12accb8 | ||
|
|
39fe6ea5b1 | ||
|
|
fc4b7df088 | ||
|
|
35f28f7a64 | ||
|
|
614be40438 | ||
|
|
bde0428d0c | ||
|
|
63567c2ae4 | ||
|
|
ec10f337b6 | ||
|
|
d0f0e73e96 | ||
|
|
b4fe2c8a6b | ||
|
|
33da0771d1 | ||
|
|
75994cd915 | ||
|
|
c0839afdde | ||
|
|
5961da3f57 | ||
|
|
41f1809351 | ||
|
|
5fbdcb62e4 | ||
|
|
677b83e9f8 | ||
|
|
6a5c8becac | ||
|
|
fd23b99d60 | ||
|
|
634b586df3 | ||
|
|
4ca3e98082 | ||
|
|
d18a776964 | ||
|
|
d6b4d7d265 | ||
|
|
33ee4c21b3 | ||
|
|
a026cd7195 | ||
|
|
7b1bce8d90 | ||
|
|
217a7c5161 | ||
|
|
2949e3422d | ||
|
|
16ea99b0c0 | ||
|
|
19b84ce9f0 | ||
|
|
e5aed4c067 | ||
|
|
9e048bc0c3 | ||
|
|
5d7b0735ce | ||
|
|
12521cd5b4 | ||
|
|
7dbd3eb5e8 | ||
|
|
96364aacc0 | ||
|
|
fc18f86964 | ||
|
|
fb3d368a78 | ||
|
|
f41435fae3 |
@@ -17,6 +17,12 @@ environment:
|
|||||||
PYTHON_VERSION: "3.8.x"
|
PYTHON_VERSION: "3.8.x"
|
||||||
PYTHON_ARCH: "64"
|
PYTHON_ARCH: "64"
|
||||||
|
|
||||||
|
# - TOXENV: py39-no-ext
|
||||||
|
# PYTHON: "C:\\Python39-x64\\python"
|
||||||
|
# PYTHONPATH: "C:\\Python39-x64"
|
||||||
|
# PYTHON_VERSION: "3.9.x"
|
||||||
|
# PYTHON_ARCH: "64"
|
||||||
|
|
||||||
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
|||||||
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: sanic-org # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
40
.github/workflows/codeql-analysis.yml
vendored
Normal file
40
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
schedule:
|
||||||
|
- cron: '25 16 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'python' ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
||||||
37
.github/workflows/coverage.yml
vendored
Normal file
37
.github/workflows/coverage.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Coverage check
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
tags:
|
||||||
|
- "!*" # Do not execute on tags
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.9]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies 🔨
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install tox
|
||||||
|
- uses: paambaati/codeclimate-action@v2.5.3
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }}
|
||||||
|
with:
|
||||||
|
coverageCommand: tox -e coverage
|
||||||
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"
|
||||||
36
.github/workflows/pr-bandit.yml
vendored
Normal file
36
.github/workflows/pr-bandit.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Security Analysis
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bandit:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: type-check-${{ matrix.config.python-version }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
config:
|
||||||
|
- { python-version: 3.7, tox-env: security}
|
||||||
|
- { python-version: 3.8, tox-env: security}
|
||||||
|
- { python-version: 3.9, tox-env: security}
|
||||||
|
- { python-version: "3.10", tox-env: security}
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
id: checkout-branch
|
||||||
|
|
||||||
|
- name: Run Linter Checks
|
||||||
|
id: linter-check
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }}"
|
||||||
32
.github/workflows/pr-docs.yml
vendored
Normal file
32
.github/workflows/pr-docs.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Document Linter
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docsLinter:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: Lint Documentation
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
config:
|
||||||
|
- {python-version: "3.8", tox-env: "docs"}
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Run Document Linter
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }}"
|
||||||
33
.github/workflows/pr-linter.yml
vendored
Normal file
33
.github/workflows/pr-linter.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Linter Checks
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linter:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: lint
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
config:
|
||||||
|
- { python-version: 3.8, tox-env: lint}
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
id: checkout-branch
|
||||||
|
|
||||||
|
- name: Run Linter Checks
|
||||||
|
id: linter-check
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }}"
|
||||||
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"
|
||||||
35
.github/workflows/pr-python37.yml
vendored
Normal file
35
.github/workflows/pr-python37.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Python 3.7 Tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testPy37:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
# os: [ubuntu-latest, macos-latest]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
config:
|
||||||
|
- { python-version: 3.7, tox-env: py37 }
|
||||||
|
- { python-version: 3.7, tox-env: py37-no-ext }
|
||||||
|
steps:
|
||||||
|
- name: Checkout the Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
id: checkout-branch
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }}"
|
||||||
|
test-failure-retry: "3"
|
||||||
35
.github/workflows/pr-python38.yml
vendored
Normal file
35
.github/workflows/pr-python38.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Python 3.8 Tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testPy38:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
# os: [ubuntu-latest, macos-latest]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
config:
|
||||||
|
- { python-version: 3.8, tox-env: py38 }
|
||||||
|
- { python-version: 3.8, tox-env: py38-no-ext }
|
||||||
|
steps:
|
||||||
|
- name: Checkout the Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
id: checkout-branch
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }}"
|
||||||
|
test-failure-retry: "3"
|
||||||
47
.github/workflows/pr-python39.yml
vendored
Normal file
47
.github/workflows/pr-python39.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Python 3.9 Tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testPy39:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
# os: [ubuntu-latest, macos-latest]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
config:
|
||||||
|
- {
|
||||||
|
python-version: 3.9,
|
||||||
|
tox-env: py39,
|
||||||
|
ignore-error-flake: "false",
|
||||||
|
command-timeout: "0",
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
python-version: 3.9,
|
||||||
|
tox-env: py39-no-ext,
|
||||||
|
ignore-error-flake: "true",
|
||||||
|
command-timeout: "600000",
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Checkout the Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
id: checkout-branch
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
|
||||||
|
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
|
||||||
|
command-timeout: "${{ matrix.config.command-timeout }}"
|
||||||
|
test-failure-retry: "3"
|
||||||
35
.github/workflows/pr-type-check.yml
vendored
Normal file
35
.github/workflows/pr-type-check.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Typing Checks
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
typeChecking:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: type-check-${{ matrix.config.python-version }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
config:
|
||||||
|
# - { python-version: 3.7, tox-env: type-checking}
|
||||||
|
- { python-version: 3.8, tox-env: type-checking}
|
||||||
|
- { python-version: 3.9, tox-env: type-checking}
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
id: checkout-branch
|
||||||
|
|
||||||
|
- name: Run Linter Checks
|
||||||
|
id: linter-check
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }}"
|
||||||
37
.github/workflows/pr-windows.yml
vendored
Normal file
37
.github/workflows/pr-windows.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Run Unit Tests on Windows
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testsOnWindows:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: ut-${{ matrix.config.tox-env }}
|
||||||
|
runs-on: windows-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
config:
|
||||||
|
- { python-version: 3.7, tox-env: py37-no-ext }
|
||||||
|
- { python-version: 3.8, tox-env: py38-no-ext }
|
||||||
|
- { python-version: 3.9, tox-env: py39-no-ext }
|
||||||
|
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
uses: ahopkins/custom-actions@pip-extra-args
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }}"
|
||||||
|
experimental-ignore-error: "true"
|
||||||
|
command-timeout: "600000"
|
||||||
|
pip-extra-args: "--user"
|
||||||
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"
|
||||||
27
.travis.yml
27
.travis.yml
@@ -31,6 +31,16 @@ matrix:
|
|||||||
dist: xenial
|
dist: xenial
|
||||||
sudo: true
|
sudo: true
|
||||||
name: "Python 3.8 without Extensions"
|
name: "Python 3.8 without Extensions"
|
||||||
|
- env: TOX_ENV=py39
|
||||||
|
python: 3.9
|
||||||
|
dist: bionic
|
||||||
|
sudo: true
|
||||||
|
name: "Python 3.9 with Extensions"
|
||||||
|
- env: TOX_ENV=py39-no-ext
|
||||||
|
python: 3.9
|
||||||
|
dist: bionic
|
||||||
|
sudo: true
|
||||||
|
name: "Python 3.9 without Extensions"
|
||||||
- env: TOX_ENV=type-checking
|
- env: TOX_ENV=type-checking
|
||||||
python: 3.6
|
python: 3.6
|
||||||
name: "Python 3.6 Type checks"
|
name: "Python 3.6 Type checks"
|
||||||
@@ -40,6 +50,10 @@ matrix:
|
|||||||
- env: TOX_ENV=type-checking
|
- env: TOX_ENV=type-checking
|
||||||
python: 3.8
|
python: 3.8
|
||||||
name: "Python 3.8 Type checks"
|
name: "Python 3.8 Type checks"
|
||||||
|
- env: TOX_ENV=type-checking
|
||||||
|
python: 3.9
|
||||||
|
dist: bionic
|
||||||
|
name: "Python 3.9 Type checks"
|
||||||
- env: TOX_ENV=lint
|
- env: TOX_ENV=lint
|
||||||
python: 3.6
|
python: 3.6
|
||||||
name: "Python 3.6 Linter checks"
|
name: "Python 3.6 Linter checks"
|
||||||
@@ -61,23 +75,28 @@ matrix:
|
|||||||
dist: xenial
|
dist: xenial
|
||||||
sudo: true
|
sudo: true
|
||||||
name: "Python 3.8 Bandit security scan"
|
name: "Python 3.8 Bandit security scan"
|
||||||
|
- env: TOX_ENV=security
|
||||||
|
python: 3.9
|
||||||
|
dist: bionic
|
||||||
|
sudo: true
|
||||||
|
name: "Python 3.9 Bandit security scan"
|
||||||
- env: TOX_ENV=docs
|
- env: TOX_ENV=docs
|
||||||
python: 3.7
|
python: 3.7
|
||||||
dist: xenial
|
dist: xenial
|
||||||
sudo: true
|
sudo: true
|
||||||
name: "Python 3.7 Documentation tests"
|
name: "Python 3.7 Documentation tests"
|
||||||
- env: TOX_ENV=pyNightly
|
- env: TOX_ENV=pyNightly
|
||||||
python: 'nightly'
|
python: "nightly"
|
||||||
name: "Python nightly with Extensions"
|
name: "Python nightly with Extensions"
|
||||||
- env: TOX_ENV=pyNightly-no-ext
|
- env: TOX_ENV=pyNightly-no-ext
|
||||||
python: 'nightly'
|
python: "nightly"
|
||||||
name: "Python nightly without Extensions"
|
name: "Python nightly without Extensions"
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: TOX_ENV=pyNightly
|
- env: TOX_ENV=pyNightly
|
||||||
python: 'nightly'
|
python: "nightly"
|
||||||
name: "Python nightly with Extensions"
|
name: "Python nightly with Extensions"
|
||||||
- env: TOX_ENV=pyNightly-no-ext
|
- env: TOX_ENV=pyNightly-no-ext
|
||||||
python: 'nightly'
|
python: "nightly"
|
||||||
name: "Python nightly without Extensions"
|
name: "Python nightly without Extensions"
|
||||||
install:
|
install:
|
||||||
- pip install -U tox
|
- pip install -U tox
|
||||||
|
|||||||
122
CHANGELOG.rst
122
CHANGELOG.rst
@@ -1,3 +1,125 @@
|
|||||||
|
Version 20.12.5
|
||||||
|
===============
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
*
|
||||||
|
`#2366 <https://github.com/sanic-org/sanic/pull/2366>`_
|
||||||
|
websocket dependency for websockets 9.1 security fix
|
||||||
|
|
||||||
|
Version 20.12.0
|
||||||
|
===============
|
||||||
|
|
||||||
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1945 <https://github.com/huge-success/sanic/pull/1945>`_
|
||||||
|
Static route more verbose if file not found
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1954 <https://github.com/huge-success/sanic/pull/1954>`_
|
||||||
|
Fix static routes registration on a blueprint
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1961 <https://github.com/huge-success/sanic/pull/1961>`_
|
||||||
|
Add Python 3.9 support
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1962 <https://github.com/huge-success/sanic/pull/1962>`_
|
||||||
|
Sanic CLI upgrade
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1967 <https://github.com/huge-success/sanic/pull/1967>`_
|
||||||
|
Update aiofile version requirements
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1969 <https://github.com/huge-success/sanic/pull/1969>`_
|
||||||
|
Update multidict version requirements
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1970 <https://github.com/huge-success/sanic/pull/1970>`_
|
||||||
|
Add py.typed file
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1972 <https://github.com/huge-success/sanic/pull/1972>`_
|
||||||
|
Speed optimization in request handler
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1979 <https://github.com/huge-success/sanic/pull/1979>`_
|
||||||
|
Add app registry and Sanic class level app retrieval
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1965 <https://github.com/huge-success/sanic/pull/1965>`_
|
||||||
|
Fix Chunked Transport-Encoding in ASGI streaming response
|
||||||
|
|
||||||
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1981 <https://github.com/huge-success/sanic/pull/1981>`_
|
||||||
|
Cleanup and remove deprecated code
|
||||||
|
|
||||||
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1956 <https://github.com/huge-success/sanic/pull/1956>`_
|
||||||
|
Fix load module test
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1973 <https://github.com/huge-success/sanic/pull/1973>`_
|
||||||
|
Transition Travis from .org to .com
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1986 <https://github.com/huge-success/sanic/pull/1986>`_
|
||||||
|
Update tox requirements
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1951 <https://github.com/huge-success/sanic/pull/1951>`_
|
||||||
|
Documentation improvements
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1983 <https://github.com/huge-success/sanic/pull/1983>`_
|
||||||
|
Remove duplicate contents in testing.rst
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1984 <https://github.com/huge-success/sanic/pull/1984>`_
|
||||||
|
Fix typo in routing.rst
|
||||||
|
|
||||||
|
|
||||||
|
Version 20.9.1
|
||||||
|
===============
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1954 <https://github.com/huge-success/sanic/pull/1954>`_
|
||||||
|
Fix static route registration on blueprints
|
||||||
|
*
|
||||||
|
`#1957 <https://github.com/huge-success/sanic/pull/1957>`_
|
||||||
|
Removes duplicate headers in ASGI streaming body
|
||||||
|
|
||||||
|
|
||||||
|
Version 19.12.3
|
||||||
|
===============
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1959 <https://github.com/huge-success/sanic/pull/1959>`_
|
||||||
|
Removes duplicate headers in ASGI streaming body
|
||||||
|
|
||||||
|
|
||||||
Version 20.9.0
|
Version 20.9.0
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ Sanic | Build fast. Run fast.
|
|||||||
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||||
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
|
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/huge-success/sanic
|
:target: https://codecov.io/gh/huge-success/sanic
|
||||||
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
|
.. |Build Status| image:: https://travis-ci.com/huge-success/sanic.svg?branch=master
|
||||||
:target: https://travis-ci.org/huge-success/sanic
|
:target: https://travis-ci.com/huge-success/sanic
|
||||||
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
||||||
:target: https://ci.appveyor.com/project/huge-success/sanic
|
:target: https://ci.appveyor.com/project/huge-success/sanic
|
||||||
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||||
|
|||||||
1
changelogs/1970.misc.rst
Normal file
1
changelogs/1970.misc.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Adds py.typed file to expose type information to other packages.
|
||||||
@@ -60,3 +60,26 @@ Open the address `http://0.0.0.0:8000 <http://0.0.0.0:8000>`_ in your web browse
|
|||||||
the message *Hello world!*.
|
the message *Hello world!*.
|
||||||
|
|
||||||
You now have a working Sanic server!
|
You now have a working Sanic server!
|
||||||
|
|
||||||
|
5. Application registry
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
When you instantiate a Sanic instance, that can be retrieved at a later time from the Sanic app registry. This can be useful, for example, if you need to access your Sanic instance from a location where it is not otherwise accessible.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# ./path/to/server.py
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
app = Sanic("my_awesome_server")
|
||||||
|
|
||||||
|
# ./path/to/somewhere_else.py
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
app = Sanic.get_app("my_awesome_server")
|
||||||
|
|
||||||
|
If you call ``Sanic.get_app("non-existing")`` on an app that does not exist, it will raise ``SanicException`` by default. You can, instead, force the method to return a new instance of ``Sanic`` with that name:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic.get_app("my_awesome_server", force_create=True)
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ which allows the handler function to work with any of the HTTP methods in the li
|
|||||||
async def get_handler(request):
|
async def get_handler(request):
|
||||||
return text('GET request - {}'.format(request.args))
|
return text('GET request - {}'.format(request.args))
|
||||||
|
|
||||||
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
|
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is also a route with no host, it will be the default.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|||||||
@@ -88,5 +88,5 @@ When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `
|
|||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
|
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8MiB
|
||||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
|
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
|
||||||
|
|||||||
@@ -58,10 +58,6 @@ More information about
|
|||||||
the available arguments to `httpx` can be found
|
the available arguments to `httpx` can be found
|
||||||
[in the documentation for `httpx <https://www.encode.io/httpx/>`_.
|
[in the documentation for `httpx <https://www.encode.io/httpx/>`_.
|
||||||
|
|
||||||
Additionally, Sanic has an asynchronous testing client. The difference is that the async client will not stand up an
|
|
||||||
instance of your application, but will instead reach inside it using ASGI. All listeners and middleware are still
|
|
||||||
executed.
|
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_index_returns_200():
|
async def test_index_returns_200():
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,83 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from sanic import __version__
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
|
from sanic.config import BASE_LOGO
|
||||||
from sanic.log import logger
|
from sanic.log import logger
|
||||||
|
|
||||||
|
|
||||||
|
class SanicArgumentParser(ArgumentParser):
|
||||||
|
def add_bool_arguments(self, *args, **kwargs):
|
||||||
|
group = self.add_mutually_exclusive_group()
|
||||||
|
group.add_argument(*args, action="store_true", **kwargs)
|
||||||
|
kwargs["help"] = "no " + kwargs["help"]
|
||||||
|
group.add_argument(
|
||||||
|
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = ArgumentParser(prog="sanic")
|
parser = SanicArgumentParser(
|
||||||
parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
|
prog="sanic",
|
||||||
parser.add_argument("--port", dest="port", type=int, default=8000)
|
description=BASE_LOGO,
|
||||||
parser.add_argument("--unix", dest="unix", type=str, default="")
|
formatter_class=RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-H",
|
||||||
|
"--host",
|
||||||
|
dest="host",
|
||||||
|
type=str,
|
||||||
|
default="127.0.0.1",
|
||||||
|
help="host address [default 127.0.0.1]",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--port",
|
||||||
|
dest="port",
|
||||||
|
type=int,
|
||||||
|
default=8000,
|
||||||
|
help="port to serve on [default 8000]",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-u",
|
||||||
|
"--unix",
|
||||||
|
dest="unix",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="location of unix socket",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--key", dest="key", type=str, help="location of keyfile for SSL."
|
"--key", dest="key", type=str, help="location of keyfile for SSL."
|
||||||
)
|
)
|
||||||
parser.add_argument("--workers", dest="workers", type=int, default=1)
|
parser.add_argument(
|
||||||
|
"-w",
|
||||||
|
"--workers",
|
||||||
|
dest="workers",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="number of worker processes [default 1]",
|
||||||
|
)
|
||||||
parser.add_argument("--debug", dest="debug", action="store_true")
|
parser.add_argument("--debug", dest="debug", action="store_true")
|
||||||
parser.add_argument("module")
|
parser.add_bool_arguments(
|
||||||
|
"--access-logs", dest="access_log", help="display access logs"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v",
|
||||||
|
"--version",
|
||||||
|
action="version",
|
||||||
|
version=f"Sanic {__version__}",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"module", help="path to your Sanic app. Example: path.to.server:app"
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -30,6 +85,9 @@ def main():
|
|||||||
if module_path not in sys.path:
|
if module_path not in sys.path:
|
||||||
sys.path.append(module_path)
|
sys.path.append(module_path)
|
||||||
|
|
||||||
|
if ":" in args.module:
|
||||||
|
module_name, app_name = args.module.rsplit(":", 1)
|
||||||
|
else:
|
||||||
module_parts = args.module.split(".")
|
module_parts = args.module.split(".")
|
||||||
module_name = ".".join(module_parts[:-1])
|
module_name = ".".join(module_parts[:-1])
|
||||||
app_name = module_parts[-1]
|
app_name = module_parts[-1]
|
||||||
@@ -57,6 +115,7 @@ def main():
|
|||||||
unix=args.unix,
|
unix=args.unix,
|
||||||
workers=args.workers,
|
workers=args.workers,
|
||||||
debug=args.debug,
|
debug=args.debug,
|
||||||
|
access_log=args.access_log,
|
||||||
ssl=ssl,
|
ssl=ssl,
|
||||||
)
|
)
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "20.9.0"
|
__version__ = "20.12.7"
|
||||||
|
|||||||
163
sanic/app.py
163
sanic/app.py
@@ -2,17 +2,18 @@ import logging
|
|||||||
import logging.config
|
import logging.config
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import warnings
|
import sys
|
||||||
|
|
||||||
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
|
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import getmodulename, isawaitable, signature, stack
|
from inspect import isawaitable, signature
|
||||||
from socket import socket
|
from socket import socket
|
||||||
from ssl import Purpose, SSLContext, create_default_context
|
from ssl import Purpose, SSLContext, create_default_context
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from typing import Any, Dict, Optional, Type, Union
|
from typing import Any, Dict, Optional, Type, Union
|
||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic import reloader_helpers
|
from sanic import reloader_helpers
|
||||||
from sanic.asgi import ASGIApp
|
from sanic.asgi import ASGIApp
|
||||||
@@ -38,6 +39,9 @@ from sanic.websocket import ConnectionClosed, WebSocketProtocol
|
|||||||
|
|
||||||
|
|
||||||
class Sanic:
|
class Sanic:
|
||||||
|
_app_registry: Dict[str, "Sanic"] = {}
|
||||||
|
test_mode = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -48,27 +52,35 @@ class Sanic:
|
|||||||
strict_slashes=False,
|
strict_slashes=False,
|
||||||
log_config=None,
|
log_config=None,
|
||||||
configure_logging=True,
|
configure_logging=True,
|
||||||
|
register=None,
|
||||||
):
|
):
|
||||||
|
|
||||||
# Get name from previous stack frame
|
# Get name from previous stack frame
|
||||||
if name is None:
|
if name is None:
|
||||||
warnings.warn(
|
raise SanicException(
|
||||||
"Sanic(name=None) is deprecated and None value support "
|
"Sanic instance cannot be unnamed. "
|
||||||
"for `name` will be removed in the next release. "
|
|
||||||
"Please use Sanic(name='your_application_name') instead.",
|
"Please use Sanic(name='your_application_name') instead.",
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
)
|
||||||
frame_records = stack()[1]
|
|
||||||
name = getmodulename(frame_records[1])
|
|
||||||
|
|
||||||
# logging
|
# logging
|
||||||
if configure_logging:
|
if configure_logging:
|
||||||
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
error_logger.error(
|
||||||
|
"Unsupported version of Python has been detected.\n\nPython "
|
||||||
|
f"version {sys.version} is not supported by this version of "
|
||||||
|
"Sanic. There is a security advisory that has been issued for "
|
||||||
|
"Sanic v20.12 while running Python 3.10+. You should either "
|
||||||
|
"use a supported version of Python (v3.6 - v3.9) or upgrade "
|
||||||
|
"Sanic to v21+.\n\nPlease see https://github.com/sanic-org/"
|
||||||
|
"sanic/security/advisories/GHSA-7p79-6x2v-5h88 for "
|
||||||
|
"more information.\n"
|
||||||
|
)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.asgi = False
|
self.asgi = False
|
||||||
self.router = router or Router()
|
self.router = router or Router(self)
|
||||||
self.request_class = request_class
|
self.request_class = request_class
|
||||||
self.error_handler = error_handler or ErrorHandler()
|
self.error_handler = error_handler or ErrorHandler()
|
||||||
self.config = Config(load_env=load_env)
|
self.config = Config(load_env=load_env)
|
||||||
@@ -90,7 +102,12 @@ class Sanic:
|
|||||||
self.named_response_middleware = {}
|
self.named_response_middleware = {}
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
self.test_mode = False
|
|
||||||
|
if register is not None:
|
||||||
|
self.config.REGISTER = register
|
||||||
|
|
||||||
|
if self.config.REGISTER:
|
||||||
|
self.__class__.register_app(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loop(self):
|
def loop(self):
|
||||||
@@ -491,9 +508,7 @@ class Sanic:
|
|||||||
websocket_handler = partial(
|
websocket_handler = partial(
|
||||||
self._websocket_handler, handler, subprotocols=subprotocols
|
self._websocket_handler, handler, subprotocols=subprotocols
|
||||||
)
|
)
|
||||||
websocket_handler.__name__ = (
|
websocket_handler.__name__ = handler.__name__
|
||||||
"websocket_handler_" + handler.__name__
|
|
||||||
)
|
|
||||||
routes.extend(
|
routes.extend(
|
||||||
self.router.add(
|
self.router.add(
|
||||||
uri=uri,
|
uri=uri,
|
||||||
@@ -676,9 +691,10 @@ class Sanic:
|
|||||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||||
URLs need to terminate with a */*
|
URLs need to terminate with a */*
|
||||||
:param content_type: user defined content type for header
|
:param content_type: user defined content type for header
|
||||||
:return: None
|
:return: routes registered on the router
|
||||||
|
:rtype: List[sanic.router.Route]
|
||||||
"""
|
"""
|
||||||
static_register(
|
return static_register(
|
||||||
self,
|
self,
|
||||||
uri,
|
uri,
|
||||||
file_or_directory,
|
file_or_directory,
|
||||||
@@ -713,28 +729,6 @@ class Sanic:
|
|||||||
self._blueprint_order.append(blueprint)
|
self._blueprint_order.append(blueprint)
|
||||||
blueprint.register(self, options)
|
blueprint.register(self, options)
|
||||||
|
|
||||||
def register_blueprint(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Proxy method provided for invoking the :func:`blueprint` method
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
To be deprecated in 1.0. Use :func:`blueprint` instead.
|
|
||||||
|
|
||||||
:param args: Blueprint object or (list, tuple) thereof
|
|
||||||
:param kwargs: option dictionary with blueprint defaults
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.debug:
|
|
||||||
warnings.simplefilter("default")
|
|
||||||
warnings.warn(
|
|
||||||
"Use of register_blueprint will be deprecated in "
|
|
||||||
"version 1.0. Please use the blueprint method"
|
|
||||||
" instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
return self.blueprint(*args, **kwargs)
|
|
||||||
|
|
||||||
def url_for(self, view_name: str, **kwargs):
|
def url_for(self, view_name: str, **kwargs):
|
||||||
r"""Build a URL based on a view name and the values provided.
|
r"""Build a URL based on a view name and the values provided.
|
||||||
|
|
||||||
@@ -765,6 +759,24 @@ class Sanic:
|
|||||||
kw.update(name=view_name)
|
kw.update(name=view_name)
|
||||||
|
|
||||||
uri, route = self.router.find_route_by_view_name(view_name, **kw)
|
uri, route = self.router.find_route_by_view_name(view_name, **kw)
|
||||||
|
|
||||||
|
# TODO(laggardkernel): this fix should be removed in v21.3.
|
||||||
|
# Try again without the unnecessary prefix "websocket_handler_",
|
||||||
|
# which was added by accident on non-blueprint handlers. GH-2021
|
||||||
|
if not (uri and route) and view_name.startswith("websocket_handler_"):
|
||||||
|
view_name = view_name[18:]
|
||||||
|
uri, route = self.router.find_route_by_view_name(view_name, **kw)
|
||||||
|
if uri and route:
|
||||||
|
warn(
|
||||||
|
"The bug of adding unnecessary `websocket_handler_` "
|
||||||
|
"prefix in param `view_name` for non-blueprint handlers "
|
||||||
|
"is fixed. This backward support will be removed in "
|
||||||
|
"v21.3. Please update `Sanic.url_for()` callings in your "
|
||||||
|
"code soon.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
if not (uri and route):
|
if not (uri and route):
|
||||||
raise URLBuildError(
|
raise URLBuildError(
|
||||||
f"Endpoint with name `{view_name}` was not found"
|
f"Endpoint with name `{view_name}` was not found"
|
||||||
@@ -899,7 +911,9 @@ class Sanic:
|
|||||||
name = None
|
name = None
|
||||||
try:
|
try:
|
||||||
# Fetch handler from router
|
# Fetch handler from router
|
||||||
handler, args, kwargs, uri, name = self.router.get(request)
|
handler, args, kwargs, uri, name, endpoint = self.router.get(
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
@@ -921,16 +935,8 @@ class Sanic:
|
|||||||
"handler from the router"
|
"handler from the router"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
if not getattr(handler, "__blueprintname__", False):
|
request.endpoint = endpoint
|
||||||
request.endpoint = self._build_endpoint_name(
|
|
||||||
handler.__name__
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
request.endpoint = self._build_endpoint_name(
|
|
||||||
getattr(handler, "__blueprintname__", ""),
|
|
||||||
handler.__name__,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Run response handler
|
# Run response handler
|
||||||
response = handler(request, *args, **kwargs)
|
response = handler(request, *args, **kwargs)
|
||||||
@@ -1031,7 +1037,6 @@ class Sanic:
|
|||||||
workers: int = 1,
|
workers: int = 1,
|
||||||
protocol: Optional[Type[Protocol]] = None,
|
protocol: Optional[Type[Protocol]] = None,
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
stop_event: Any = None,
|
|
||||||
register_sys_signals: bool = True,
|
register_sys_signals: bool = True,
|
||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
@@ -1061,9 +1066,6 @@ class Sanic:
|
|||||||
:param backlog: a number of unaccepted connections that the system
|
:param backlog: a number of unaccepted connections that the system
|
||||||
will allow before refusing new connections
|
will allow before refusing new connections
|
||||||
:type backlog: int
|
:type backlog: int
|
||||||
:param stop_event: event to be triggered
|
|
||||||
before stopping the app - deprecated
|
|
||||||
:type stop_event: None
|
|
||||||
:param register_sys_signals: Register SIG* events
|
:param register_sys_signals: Register SIG* events
|
||||||
:type register_sys_signals: bool
|
:type register_sys_signals: bool
|
||||||
:param access_log: Enables writing access logs (slows server)
|
:param access_log: Enables writing access logs (slows server)
|
||||||
@@ -1091,13 +1093,6 @@ class Sanic:
|
|||||||
protocol = (
|
protocol = (
|
||||||
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
||||||
)
|
)
|
||||||
if stop_event is not None:
|
|
||||||
if debug:
|
|
||||||
warnings.simplefilter("default")
|
|
||||||
warnings.warn(
|
|
||||||
"stop_event will be removed from future versions.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
# if access_log is passed explicitly change config.ACCESS_LOG
|
# if access_log is passed explicitly change config.ACCESS_LOG
|
||||||
if access_log is not None:
|
if access_log is not None:
|
||||||
self.config.ACCESS_LOG = access_log
|
self.config.ACCESS_LOG = access_log
|
||||||
@@ -1154,7 +1149,6 @@ class Sanic:
|
|||||||
sock: Optional[socket] = None,
|
sock: Optional[socket] = None,
|
||||||
protocol: Type[Protocol] = None,
|
protocol: Type[Protocol] = None,
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
stop_event: Any = None,
|
|
||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
return_asyncio_server=False,
|
return_asyncio_server=False,
|
||||||
@@ -1187,9 +1181,6 @@ class Sanic:
|
|||||||
:param backlog: a number of unaccepted connections that the system
|
:param backlog: a number of unaccepted connections that the system
|
||||||
will allow before refusing new connections
|
will allow before refusing new connections
|
||||||
:type backlog: int
|
:type backlog: int
|
||||||
:param stop_event: event to be triggered
|
|
||||||
before stopping the app - deprecated
|
|
||||||
:type stop_event: None
|
|
||||||
:param access_log: Enables writing access logs (slows server)
|
:param access_log: Enables writing access logs (slows server)
|
||||||
:type access_log: bool
|
:type access_log: bool
|
||||||
:param return_asyncio_server: flag that defines whether there's a need
|
:param return_asyncio_server: flag that defines whether there's a need
|
||||||
@@ -1209,13 +1200,6 @@ class Sanic:
|
|||||||
protocol = (
|
protocol = (
|
||||||
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
||||||
)
|
)
|
||||||
if stop_event is not None:
|
|
||||||
if debug:
|
|
||||||
warnings.simplefilter("default")
|
|
||||||
warnings.warn(
|
|
||||||
"stop_event will be removed from future versions.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
# if access_log is passed explicitly change config.ACCESS_LOG
|
# if access_log is passed explicitly change config.ACCESS_LOG
|
||||||
if access_log is not None:
|
if access_log is not None:
|
||||||
self.config.ACCESS_LOG = access_log
|
self.config.ACCESS_LOG = access_log
|
||||||
@@ -1297,7 +1281,6 @@ class Sanic:
|
|||||||
loop=None,
|
loop=None,
|
||||||
protocol=HttpProtocol,
|
protocol=HttpProtocol,
|
||||||
backlog=100,
|
backlog=100,
|
||||||
stop_event=None,
|
|
||||||
register_sys_signals=True,
|
register_sys_signals=True,
|
||||||
run_async=False,
|
run_async=False,
|
||||||
auto_reload=False,
|
auto_reload=False,
|
||||||
@@ -1312,13 +1295,6 @@ class Sanic:
|
|||||||
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
|
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
|
||||||
context.load_cert_chain(cert, keyfile=key)
|
context.load_cert_chain(cert, keyfile=key)
|
||||||
ssl = context
|
ssl = context
|
||||||
if stop_event is not None:
|
|
||||||
if debug:
|
|
||||||
warnings.simplefilter("default")
|
|
||||||
warnings.warn(
|
|
||||||
"stop_event will be removed from future versions.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
|
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"PROXIES_COUNT cannot be negative. "
|
"PROXIES_COUNT cannot be negative. "
|
||||||
@@ -1453,12 +1429,41 @@ class Sanic:
|
|||||||
asgi_app = await ASGIApp.create(self, scope, receive, send)
|
asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||||
await asgi_app()
|
await asgi_app()
|
||||||
|
|
||||||
|
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Configuration
|
# Configuration
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||||
"""Update app.config.
|
"""Update app.config.
|
||||||
|
|
||||||
Please refer to config.py::Config.update_config for documentation."""
|
Please refer to config.py::Config.update_config for documentation."""
|
||||||
|
|
||||||
self.config.update_config(config)
|
self.config.update_config(config)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- #
|
||||||
|
# Class methods
|
||||||
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_app(cls, app: "Sanic") -> None:
|
||||||
|
"""Register a Sanic instance"""
|
||||||
|
if not isinstance(app, cls):
|
||||||
|
raise SanicException("Registered app must be an instance of Sanic")
|
||||||
|
|
||||||
|
name = app.name
|
||||||
|
if name in cls._app_registry and not cls.test_mode:
|
||||||
|
raise SanicException(f'Sanic app name "{name}" already in use.')
|
||||||
|
|
||||||
|
cls._app_registry[name] = app
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_app(cls, name: str, *, force_create: bool = False) -> "Sanic":
|
||||||
|
"""Retrieve an instantiated Sanic instance"""
|
||||||
|
try:
|
||||||
|
return cls._app_registry[name]
|
||||||
|
except KeyError:
|
||||||
|
if force_create:
|
||||||
|
return cls(name)
|
||||||
|
raise SanicException(f'Sanic app name "{name}" not found.')
|
||||||
|
|||||||
@@ -312,13 +312,19 @@ class ASGIApp:
|
|||||||
callback = None if self.ws else self.stream_callback
|
callback = None if self.ws else self.stream_callback
|
||||||
await handler(self.request, None, callback)
|
await handler(self.request, None, callback)
|
||||||
|
|
||||||
async def stream_callback(self, response: HTTPResponse) -> None:
|
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
|
||||||
|
|
||||||
|
async def stream_callback(
|
||||||
|
self, response: Union[HTTPResponse, StreamingHTTPResponse]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Write the response.
|
Write the response.
|
||||||
"""
|
"""
|
||||||
headers: List[Tuple[bytes, bytes]] = []
|
headers: List[Tuple[bytes, bytes]] = []
|
||||||
cookies: Dict[str, str] = {}
|
cookies: Dict[str, str] = {}
|
||||||
|
content_length: List[str] = []
|
||||||
try:
|
try:
|
||||||
|
content_length = response.headers.popall("content-length", [])
|
||||||
cookies = {
|
cookies = {
|
||||||
v.key: v
|
v.key: v
|
||||||
for _, v in list(
|
for _, v in list(
|
||||||
@@ -350,11 +356,23 @@ class ASGIApp:
|
|||||||
if name not in (b"Set-Cookie",)
|
if name not in (b"Set-Cookie",)
|
||||||
]
|
]
|
||||||
|
|
||||||
if "content-length" not in response.headers and not isinstance(
|
response.asgi = True
|
||||||
response, StreamingHTTPResponse
|
is_streaming = isinstance(response, StreamingHTTPResponse)
|
||||||
):
|
if is_streaming and getattr(response, "chunked", False):
|
||||||
|
# disable sanic chunking, this is done at the ASGI-server level
|
||||||
|
setattr(response, "chunked", False)
|
||||||
|
# content-length header is removed to signal to the ASGI-server
|
||||||
|
# to use automatic-chunking if it supports it
|
||||||
|
elif len(content_length) > 0:
|
||||||
headers += [
|
headers += [
|
||||||
(b"content-length", str(len(response.body)).encode("latin-1"))
|
(b"content-length", str(content_length[0]).encode("latin-1"))
|
||||||
|
]
|
||||||
|
elif not is_streaming:
|
||||||
|
headers += [
|
||||||
|
(
|
||||||
|
b"content-length",
|
||||||
|
str(len(getattr(response, "body", b""))).encode("latin-1"),
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
if "content-type" not in response.headers:
|
if "content-type" not in response.headers:
|
||||||
|
|||||||
@@ -143,7 +143,18 @@ class Blueprint:
|
|||||||
if _routes:
|
if _routes:
|
||||||
routes += _routes
|
routes += _routes
|
||||||
|
|
||||||
|
# Static Files
|
||||||
|
for future in self.statics:
|
||||||
|
# Prepend the blueprint URI prefix if available
|
||||||
|
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||||
|
_routes = app.static(
|
||||||
|
uri, future.file_or_directory, *future.args, **future.kwargs
|
||||||
|
)
|
||||||
|
if _routes:
|
||||||
|
routes += _routes
|
||||||
|
|
||||||
route_names = [route.name for route in routes if route]
|
route_names = [route.name for route in routes if route]
|
||||||
|
|
||||||
# Middleware
|
# Middleware
|
||||||
for future in self.middlewares:
|
for future in self.middlewares:
|
||||||
if future.args or future.kwargs:
|
if future.args or future.kwargs:
|
||||||
@@ -160,14 +171,6 @@ class Blueprint:
|
|||||||
for future in self.exceptions:
|
for future in self.exceptions:
|
||||||
app.exception(*future.args, **future.kwargs)(future.handler)
|
app.exception(*future.args, **future.kwargs)(future.handler)
|
||||||
|
|
||||||
# Static Files
|
|
||||||
for future in self.statics:
|
|
||||||
# Prepend the blueprint URI prefix if available
|
|
||||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
|
||||||
app.static(
|
|
||||||
uri, future.file_or_directory, *future.args, **future.kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Event listeners
|
# Event listeners
|
||||||
for event, listeners in self.listeners.items():
|
for event, listeners in self.listeners.items():
|
||||||
for listener in listeners:
|
for listener in listeners:
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ if use_trio:
|
|||||||
def stat_async(path):
|
def stat_async(path):
|
||||||
return Path(path).stat()
|
return Path(path).stat()
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
from aiofiles import open as aio_open # type: ignore
|
from aiofiles import open as aio_open # type: ignore
|
||||||
from aiofiles.os import stat as stat_async # type: ignore # noqa: F401
|
from aiofiles.os import stat as stat_async # type: ignore # noqa: F401
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ DEFAULT_CONFIG = {
|
|||||||
"PROXIES_COUNT": None,
|
"PROXIES_COUNT": None,
|
||||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||||
"FALLBACK_ERROR_FORMAT": "html",
|
"FALLBACK_ERROR_FORMAT": "html",
|
||||||
|
"REGISTER": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -179,8 +179,8 @@ def abort(status_code, message=None):
|
|||||||
message appropriate for the given status code, unless provided.
|
message appropriate for the given status code, unless provided.
|
||||||
|
|
||||||
:param status_code: The HTTP status code to return.
|
:param status_code: The HTTP status code to return.
|
||||||
:param message: The HTTP response body. Defaults to the messages
|
:param message: The HTTP response body. Defaults to the messages in
|
||||||
in response.py for the given status code.
|
STATUS_CODES from sanic.helpers for the given status code.
|
||||||
"""
|
"""
|
||||||
if message is None:
|
if message is None:
|
||||||
message = STATUS_CODES.get(status_code)
|
message = STATUS_CODES.get(status_code)
|
||||||
|
|||||||
0
sanic/py.typed
Normal file
0
sanic/py.typed
Normal file
@@ -136,15 +136,18 @@ class Request:
|
|||||||
return f"<{class_name}: {self.method} {self.path}>"
|
return f"<{class_name}: {self.method} {self.path}>"
|
||||||
|
|
||||||
def body_init(self):
|
def body_init(self):
|
||||||
""".. deprecated:: 20.3"""
|
""".. deprecated:: 20.3
|
||||||
|
To be removed in 21.3"""
|
||||||
self.body = []
|
self.body = []
|
||||||
|
|
||||||
def body_push(self, data):
|
def body_push(self, data):
|
||||||
""".. deprecated:: 20.3"""
|
""".. deprecated:: 20.3
|
||||||
|
To be removed in 21.3"""
|
||||||
self.body.append(data)
|
self.body.append(data)
|
||||||
|
|
||||||
def body_finish(self):
|
def body_finish(self):
|
||||||
""".. deprecated:: 20.3"""
|
""".. deprecated:: 20.3
|
||||||
|
To be removed in 21.3"""
|
||||||
self.body = b"".join(self.body)
|
self.body = b"".join(self.body)
|
||||||
|
|
||||||
async def receive_body(self):
|
async def receive_body(self):
|
||||||
@@ -262,9 +265,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)
|
||||||
@@ -318,9 +324,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)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import warnings
|
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
@@ -22,7 +20,12 @@ except ImportError:
|
|||||||
|
|
||||||
|
|
||||||
class BaseHTTPResponse:
|
class BaseHTTPResponse:
|
||||||
|
def __init__(self):
|
||||||
|
self.asgi = False
|
||||||
|
|
||||||
def _encode_body(self, data):
|
def _encode_body(self, data):
|
||||||
|
if data is None:
|
||||||
|
return b""
|
||||||
return data.encode() if hasattr(data, "encode") else data
|
return data.encode() if hasattr(data, "encode") else data
|
||||||
|
|
||||||
def _parse_headers(self):
|
def _parse_headers(self):
|
||||||
@@ -42,7 +45,7 @@ class BaseHTTPResponse:
|
|||||||
body=b"",
|
body=b"",
|
||||||
):
|
):
|
||||||
""".. deprecated:: 20.3:
|
""".. deprecated:: 20.3:
|
||||||
This function is not public API and will be removed."""
|
This function is not public API and will be removed in 21.3."""
|
||||||
|
|
||||||
# self.headers get priority over content_type
|
# self.headers get priority over content_type
|
||||||
if self.content_type and "Content-Type" not in self.headers:
|
if self.content_type and "Content-Type" not in self.headers:
|
||||||
@@ -80,6 +83,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
content_type="text/plain; charset=utf-8",
|
content_type="text/plain; charset=utf-8",
|
||||||
chunked=True,
|
chunked=True,
|
||||||
):
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
self.streaming_fn = streaming_fn
|
self.streaming_fn = streaming_fn
|
||||||
self.status = status
|
self.status = status
|
||||||
@@ -95,6 +100,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
"""
|
"""
|
||||||
data = self._encode_body(data)
|
data = self._encode_body(data)
|
||||||
|
|
||||||
|
# `chunked` will always be False in ASGI-mode, even if the underlying
|
||||||
|
# ASGI Transport implements Chunked transport. That does it itself.
|
||||||
if self.chunked:
|
if self.chunked:
|
||||||
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
||||||
else:
|
else:
|
||||||
@@ -109,6 +116,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
"""
|
"""
|
||||||
if version != "1.1":
|
if version != "1.1":
|
||||||
self.chunked = False
|
self.chunked = False
|
||||||
|
if not getattr(self, "asgi", False):
|
||||||
headers = self.get_headers(
|
headers = self.get_headers(
|
||||||
version,
|
version,
|
||||||
keep_alive=keep_alive,
|
keep_alive=keep_alive,
|
||||||
@@ -141,20 +149,15 @@ class HTTPResponse(BaseHTTPResponse):
|
|||||||
status=200,
|
status=200,
|
||||||
headers=None,
|
headers=None,
|
||||||
content_type=None,
|
content_type=None,
|
||||||
body_bytes=b"",
|
|
||||||
):
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
self.body = body_bytes if body is None else self._encode_body(body)
|
self.body = self._encode_body(body)
|
||||||
self.status = status
|
self.status = status
|
||||||
self.headers = Header(headers or {})
|
self.headers = Header(headers or {})
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
|
|
||||||
if body_bytes:
|
|
||||||
warnings.warn(
|
|
||||||
"Parameter `body_bytes` is deprecated, use `body` instead",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||||
body = b""
|
body = b""
|
||||||
if has_message_body(self.status):
|
if has_message_body(self.status):
|
||||||
@@ -218,20 +221,10 @@ def text(
|
|||||||
:param content_type: the content type (string) of the response
|
:param content_type: the content type (string) of the response
|
||||||
"""
|
"""
|
||||||
if not isinstance(body, str):
|
if not isinstance(body, str):
|
||||||
warnings.warn(
|
raise TypeError(
|
||||||
"Types other than str will be deprecated in future versions for"
|
f"Bad body type. Expected str, got {type(body).__name__})"
|
||||||
f" response.text, got type {type(body).__name__})",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
)
|
||||||
# Type conversions are deprecated and quite b0rked but still supported for
|
|
||||||
# text() until applications get fixed. This try-except should be removed.
|
|
||||||
try:
|
|
||||||
# Avoid repr(body).encode() b0rkage for body that is already encoded.
|
|
||||||
# memoryview used only to test bytes-ishness.
|
|
||||||
with memoryview(body):
|
|
||||||
pass
|
|
||||||
except TypeError:
|
|
||||||
body = f"{body}" # no-op if body is already str
|
|
||||||
return HTTPResponse(
|
return HTTPResponse(
|
||||||
body, status=status, headers=headers, content_type=content_type
|
body, status=status, headers=headers, content_type=content_type
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ from sanic.views import CompositionView
|
|||||||
|
|
||||||
|
|
||||||
Route = namedtuple(
|
Route = namedtuple(
|
||||||
"Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
|
"Route",
|
||||||
|
[
|
||||||
|
"handler",
|
||||||
|
"methods",
|
||||||
|
"pattern",
|
||||||
|
"parameters",
|
||||||
|
"name",
|
||||||
|
"uri",
|
||||||
|
"endpoint",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
Parameter = namedtuple("Parameter", ["name", "cast"])
|
Parameter = namedtuple("Parameter", ["name", "cast"])
|
||||||
|
|
||||||
@@ -79,7 +88,8 @@ class Router:
|
|||||||
routes_always_check = None
|
routes_always_check = None
|
||||||
parameter_pattern = re.compile(r"<(.+?)>")
|
parameter_pattern = re.compile(r"<(.+?)>")
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
self.routes_all = {}
|
self.routes_all = {}
|
||||||
self.routes_names = {}
|
self.routes_names = {}
|
||||||
self.routes_static_files = {}
|
self.routes_static_files = {}
|
||||||
@@ -299,11 +309,15 @@ class Router:
|
|||||||
|
|
||||||
handler_name = f"{bp_name}.{name or handler.__name__}"
|
handler_name = f"{bp_name}.{name or handler.__name__}"
|
||||||
else:
|
else:
|
||||||
handler_name = name or getattr(handler, "__name__", None)
|
handler_name = name or getattr(
|
||||||
|
handler, "__name__", handler.__class__.__name__
|
||||||
|
)
|
||||||
|
|
||||||
if route:
|
if route:
|
||||||
route = merge_route(route, methods, handler)
|
route = merge_route(route, methods, handler)
|
||||||
else:
|
else:
|
||||||
|
endpoint = self.app._build_endpoint_name(handler_name)
|
||||||
|
|
||||||
route = Route(
|
route = Route(
|
||||||
handler=handler,
|
handler=handler,
|
||||||
methods=methods,
|
methods=methods,
|
||||||
@@ -311,6 +325,7 @@ class Router:
|
|||||||
parameters=parameters,
|
parameters=parameters,
|
||||||
name=handler_name,
|
name=handler_name,
|
||||||
uri=uri,
|
uri=uri,
|
||||||
|
endpoint=endpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.routes_all[uri] = route
|
self.routes_all[uri] = route
|
||||||
@@ -449,7 +464,8 @@ class Router:
|
|||||||
route_handler = route.handler
|
route_handler = route.handler
|
||||||
if hasattr(route_handler, "handlers"):
|
if hasattr(route_handler, "handlers"):
|
||||||
route_handler = route_handler.handlers[method]
|
route_handler = route_handler.handlers[method]
|
||||||
return route_handler, [], kwargs, route.uri, route.name
|
|
||||||
|
return route_handler, [], kwargs, route.uri, route.name, route.endpoint
|
||||||
|
|
||||||
def is_stream_handler(self, request):
|
def is_stream_handler(self, request):
|
||||||
"""Handler for request is stream or not.
|
"""Handler for request is stream or not.
|
||||||
|
|||||||
@@ -169,7 +169,11 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self.request_class = self.app.request_class or Request
|
self.request_class = self.app.request_class or Request
|
||||||
self.is_request_stream = self.app.is_request_stream
|
self.is_request_stream = self.app.is_request_stream
|
||||||
self._is_stream_handler = False
|
self._is_stream_handler = False
|
||||||
self._not_paused = asyncio.Event(loop=deprecated_loop)
|
self._not_paused = (
|
||||||
|
asyncio.Event()
|
||||||
|
if sys.version_info >= (3, 10)
|
||||||
|
else asyncio.Event(loop=deprecated_loop)
|
||||||
|
)
|
||||||
self._total_request_size = 0
|
self._total_request_size = 0
|
||||||
self._request_timeout_handler = None
|
self._request_timeout_handler = None
|
||||||
self._response_timeout_handler = None
|
self._response_timeout_handler = None
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path, sep
|
||||||
from re import sub
|
from pathlib import Path
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ from sanic.exceptions import (
|
|||||||
InvalidUsage,
|
InvalidUsage,
|
||||||
)
|
)
|
||||||
from sanic.handlers import ContentRangeHandler
|
from sanic.handlers import ContentRangeHandler
|
||||||
|
from sanic.log import error_logger
|
||||||
from sanic.response import HTTPResponse, file, file_stream
|
from sanic.response import HTTPResponse, file, file_stream
|
||||||
|
|
||||||
|
|
||||||
@@ -25,24 +26,41 @@ async def _static_request_handler(
|
|||||||
content_type=None,
|
content_type=None,
|
||||||
file_uri=None,
|
file_uri=None,
|
||||||
):
|
):
|
||||||
# Using this to determine if the URL is trying to break out of the path
|
|
||||||
# served. os.path.realpath seems to be very slow
|
|
||||||
if file_uri and "../" in file_uri:
|
|
||||||
raise InvalidUsage("Invalid URL")
|
|
||||||
# Merge served directory and requested file if provided
|
# Merge served directory and requested file if provided
|
||||||
# Strip all / that in the beginning of the URL to help prevent python
|
file_path_raw = Path(unquote(file_or_directory))
|
||||||
# from herping a derp and treating the uri as an absolute path
|
root_path = file_path = file_path_raw.resolve()
|
||||||
root_path = file_path = file_or_directory
|
not_found = FileNotFound(
|
||||||
if file_uri:
|
"File not found",
|
||||||
file_path = path.join(file_or_directory, sub("^[/]*", "", file_uri))
|
path=file_or_directory,
|
||||||
|
relative_url=file_uri,
|
||||||
# URL decode the path sent by the browser otherwise we won't be able to
|
|
||||||
# match filenames which got encoded (filenames with spaces etc)
|
|
||||||
file_path = path.abspath(unquote(file_path))
|
|
||||||
if not file_path.startswith(path.abspath(unquote(root_path))):
|
|
||||||
raise FileNotFound(
|
|
||||||
"File not found", path=file_or_directory, relative_url=file_uri
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if file_uri:
|
||||||
|
# Strip all / that in the beginning of the URL to help prevent
|
||||||
|
# python from herping a derp and treating the uri as an
|
||||||
|
# absolute path
|
||||||
|
unquoted_file_uri = unquote(file_uri).lstrip("/")
|
||||||
|
file_path_raw = Path(file_or_directory, unquoted_file_uri)
|
||||||
|
file_path = file_path_raw.resolve()
|
||||||
|
if (
|
||||||
|
file_path < root_path and not file_path_raw.is_symlink()
|
||||||
|
) or ".." in file_path_raw.parts:
|
||||||
|
error_logger.exception(
|
||||||
|
f"File not found: path={file_or_directory}, "
|
||||||
|
f"relative_url={file_uri}"
|
||||||
|
)
|
||||||
|
raise not_found
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_path.relative_to(root_path)
|
||||||
|
except ValueError:
|
||||||
|
if not file_path_raw.is_symlink():
|
||||||
|
error_logger.exception(
|
||||||
|
f"File not found: path={file_or_directory}, "
|
||||||
|
f"relative_url={file_uri}"
|
||||||
|
)
|
||||||
|
raise not_found
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {}
|
headers = {}
|
||||||
# Check if the client has been sent this file before
|
# Check if the client has been sent this file before
|
||||||
@@ -94,6 +112,10 @@ async def _static_request_handler(
|
|||||||
except ContentRangeError:
|
except ContentRangeError:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
|
error_logger.exception(
|
||||||
|
f"File not found: path={file_or_directory}, "
|
||||||
|
f"relative_url={file_uri}"
|
||||||
|
)
|
||||||
raise FileNotFound(
|
raise FileNotFound(
|
||||||
"File not found", path=file_or_directory, relative_url=file_uri
|
"File not found", path=file_or_directory, relative_url=file_uri
|
||||||
)
|
)
|
||||||
@@ -134,6 +156,8 @@ def register(
|
|||||||
threshold size to switch to file_stream()
|
threshold size to switch to file_stream()
|
||||||
:param name: user defined name used for url_for
|
:param name: user defined name used for url_for
|
||||||
:param content_type: user defined content type for header
|
:param content_type: user defined content type for header
|
||||||
|
:return: registered static routes
|
||||||
|
:rtype: List[sanic.router.Route]
|
||||||
"""
|
"""
|
||||||
# If we're not trying to match a file directly,
|
# If we're not trying to match a file directly,
|
||||||
# serve from the folder
|
# serve from the folder
|
||||||
@@ -155,10 +179,11 @@ def register(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
app.route(
|
_routes, _ = app.route(
|
||||||
uri,
|
uri,
|
||||||
methods=["GET", "HEAD"],
|
methods=["GET", "HEAD"],
|
||||||
name=name,
|
name=name,
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
)(_handler)
|
)(_handler)
|
||||||
|
return _routes
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class CompositionView:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
|
self.name = self.__class__.__name__
|
||||||
|
|
||||||
def add(self, methods, handler, stream=False):
|
def add(self, methods, handler, stream=False):
|
||||||
if stream:
|
if stream:
|
||||||
|
|||||||
28
setup.py
28
setup.py
@@ -5,6 +5,7 @@ import codecs
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
@@ -24,6 +25,7 @@ class PyTest(TestCommand):
|
|||||||
|
|
||||||
def run_tests(self):
|
def run_tests(self):
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
errno = pytest.main(shlex.split(self.pytest_args))
|
errno = pytest.main(shlex.split(self.pytest_args))
|
||||||
@@ -38,7 +40,9 @@ def open_local(paths, mode="r", encoding="utf8"):
|
|||||||
|
|
||||||
with open_local(["sanic", "__version__.py"], encoding="latin1") as fp:
|
with open_local(["sanic", "__version__.py"], encoding="latin1") as fp:
|
||||||
try:
|
try:
|
||||||
version = re.findall(r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M)[0]
|
version = re.findall(
|
||||||
|
r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M
|
||||||
|
)[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise RuntimeError("Unable to determine version.")
|
raise RuntimeError("Unable to determine version.")
|
||||||
|
|
||||||
@@ -53,10 +57,12 @@ setup_kwargs = {
|
|||||||
"author": "Sanic Community",
|
"author": "Sanic Community",
|
||||||
"author_email": "admhpkns@gmail.com",
|
"author_email": "admhpkns@gmail.com",
|
||||||
"description": (
|
"description": (
|
||||||
"A web server and web framework that's written to go fast. Build fast. Run fast."
|
"A web server and web framework that's written to go fast. "
|
||||||
|
"Build fast. Run fast."
|
||||||
),
|
),
|
||||||
"long_description": long_description,
|
"long_description": long_description,
|
||||||
"packages": ["sanic"],
|
"packages": ["sanic"],
|
||||||
|
"package_data": {"sanic": ["py.typed"]},
|
||||||
"platforms": "any",
|
"platforms": "any",
|
||||||
"python_requires": ">=3.6",
|
"python_requires": ">=3.6",
|
||||||
"classifiers": [
|
"classifiers": [
|
||||||
@@ -66,11 +72,14 @@ setup_kwargs = {
|
|||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
],
|
],
|
||||||
"entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]},
|
"entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]},
|
||||||
}
|
}
|
||||||
|
|
||||||
env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"'
|
env_dependency = (
|
||||||
|
'; sys_platform != "win32" ' 'and implementation_name == "cpython"'
|
||||||
|
)
|
||||||
ujson = "ujson>=1.35" + env_dependency
|
ujson = "ujson>=1.35" + env_dependency
|
||||||
uvloop = "uvloop>=0.5.3" + env_dependency
|
uvloop = "uvloop>=0.5.3" + env_dependency
|
||||||
|
|
||||||
@@ -78,24 +87,25 @@ requirements = [
|
|||||||
"httptools>=0.0.10",
|
"httptools>=0.0.10",
|
||||||
uvloop,
|
uvloop,
|
||||||
ujson,
|
ujson,
|
||||||
"aiofiles>=0.3.0",
|
"aiofiles>=0.6.0",
|
||||||
"websockets>=8.1,<9.0",
|
"websockets>=8.1,<=9.1",
|
||||||
"multidict>=4.0,<5.0",
|
"multidict>=5.0,<6.0",
|
||||||
"httpx==0.15.4",
|
"httpx==0.15.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"pytest==5.2.1",
|
"pytest==5.2.1",
|
||||||
"multidict>=4.0,<5.0",
|
"multidict>=5.0,<6.0",
|
||||||
"gunicorn",
|
"gunicorn==20.0.4",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"httpcore==0.3.0",
|
"httpcore==0.11.*",
|
||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
uvloop,
|
uvloop,
|
||||||
ujson,
|
ujson,
|
||||||
"pytest-sanic",
|
"pytest-sanic",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-benchmark",
|
"pytest-benchmark",
|
||||||
|
"pytest-dependency",
|
||||||
]
|
]
|
||||||
|
|
||||||
docs_require = [
|
docs_require = [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from sanic.router import RouteExists, Router
|
|||||||
|
|
||||||
|
|
||||||
random.seed("Pack my box with five dozen liquor jugs.")
|
random.seed("Pack my box with five dozen liquor jugs.")
|
||||||
|
Sanic.test_mode = True
|
||||||
|
|
||||||
if sys.platform in ["win32", "cygwin"]:
|
if sys.platform in ["win32", "cygwin"]:
|
||||||
collect_ignore = ["test_worker.py"]
|
collect_ignore = ["test_worker.py"]
|
||||||
@@ -95,10 +96,10 @@ class RouteStringGenerator:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def sanic_router():
|
def sanic_router(app):
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
def _setup(route_details: tuple) -> (Router, tuple):
|
def _setup(route_details: tuple) -> (Router, tuple):
|
||||||
router = Router()
|
router = Router(app)
|
||||||
added_router = []
|
added_router = []
|
||||||
for method, route in route_details:
|
for method, route in route_details:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
|
from os import environ
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -117,7 +118,7 @@ def test_app_route_raise_value_error(app):
|
|||||||
|
|
||||||
def test_app_handle_request_handler_is_none(app, monkeypatch):
|
def test_app_handle_request_handler_is_none(app, monkeypatch):
|
||||||
def mockreturn(*args, **kwargs):
|
def mockreturn(*args, **kwargs):
|
||||||
return None, [], {}, "", ""
|
return None, [], {}, "", "", None
|
||||||
|
|
||||||
# Not sure how to make app.router.get() return None, so use mock here.
|
# Not sure how to make app.router.get() return None, so use mock here.
|
||||||
monkeypatch.setattr(app.router, "get", mockreturn)
|
monkeypatch.setattr(app.router, "get", mockreturn)
|
||||||
@@ -258,7 +259,7 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
|
|||||||
|
|
||||||
|
|
||||||
def test_app_name_required():
|
def test_app_name_required():
|
||||||
with pytest.deprecated_call():
|
with pytest.raises(SanicException):
|
||||||
Sanic()
|
Sanic()
|
||||||
|
|
||||||
|
|
||||||
@@ -274,14 +275,50 @@ def test_app_has_test_mode_sync():
|
|||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
# @pytest.mark.asyncio
|
def test_app_registry():
|
||||||
# async def test_app_has_test_mode_async():
|
instance = Sanic("test")
|
||||||
# app = Sanic("test")
|
assert Sanic._app_registry["test"] is instance
|
||||||
|
|
||||||
# @app.get("/")
|
|
||||||
# async def handler(request):
|
|
||||||
# assert request.app.test_mode
|
|
||||||
# return text("test")
|
|
||||||
|
|
||||||
# _, response = await app.asgi_client.get("/")
|
def test_app_registry_wrong_type():
|
||||||
# assert response.status == 200
|
with pytest.raises(SanicException):
|
||||||
|
Sanic.register_app(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_registry_name_reuse():
|
||||||
|
Sanic("test")
|
||||||
|
Sanic.test_mode = False
|
||||||
|
with pytest.raises(SanicException):
|
||||||
|
Sanic("test")
|
||||||
|
Sanic.test_mode = True
|
||||||
|
Sanic("test")
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_registry_retrieval():
|
||||||
|
instance = Sanic("test")
|
||||||
|
assert Sanic.get_app("test") is instance
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_app_does_not_exist():
|
||||||
|
with pytest.raises(SanicException):
|
||||||
|
Sanic.get_app("does-not-exist")
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_app_does_not_exist_force_create():
|
||||||
|
assert isinstance(
|
||||||
|
Sanic.get_app("does-not-exist", force_create=True), Sanic
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_no_registry():
|
||||||
|
Sanic("no-register", register=False)
|
||||||
|
with pytest.raises(SanicException):
|
||||||
|
Sanic.get_app("no-register")
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_no_registry_env():
|
||||||
|
environ["SANIC_REGISTER"] = "False"
|
||||||
|
Sanic("no-register")
|
||||||
|
with pytest.raises(SanicException):
|
||||||
|
Sanic.get_app("no-register")
|
||||||
|
del environ["SANIC_REGISTER"]
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from collections import deque, namedtuple
|
from collections import deque, namedtuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -82,14 +79,6 @@ def test_listeners_triggered(app):
|
|||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
server.run()
|
server.run()
|
||||||
|
|
||||||
all_tasks = (
|
|
||||||
asyncio.Task.all_tasks()
|
|
||||||
if sys.version_info < (3, 7)
|
|
||||||
else asyncio.all_tasks(asyncio.get_event_loop())
|
|
||||||
)
|
|
||||||
for task in all_tasks:
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
assert before_server_start
|
assert before_server_start
|
||||||
assert after_server_start
|
assert after_server_start
|
||||||
assert before_server_stop
|
assert before_server_stop
|
||||||
@@ -132,14 +121,6 @@ def test_listeners_triggered_async(app):
|
|||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
server.run()
|
server.run()
|
||||||
|
|
||||||
all_tasks = (
|
|
||||||
asyncio.Task.all_tasks()
|
|
||||||
if sys.version_info < (3, 7)
|
|
||||||
else asyncio.all_tasks(asyncio.get_event_loop())
|
|
||||||
)
|
|
||||||
for task in all_tasks:
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
assert before_server_start
|
assert before_server_start
|
||||||
assert after_server_start
|
assert after_server_start
|
||||||
assert before_server_stop
|
assert before_server_stop
|
||||||
|
|||||||
@@ -736,6 +736,37 @@ def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
|
|||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("file_name", ["test.file"])
|
||||||
|
def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
|
||||||
|
current_file = inspect.getfile(inspect.currentframe())
|
||||||
|
with open(current_file, "rb") as file:
|
||||||
|
file.read()
|
||||||
|
|
||||||
|
triggered = False
|
||||||
|
|
||||||
|
bp = Blueprint(name="test_mw", url_prefix="")
|
||||||
|
|
||||||
|
@bp.middleware("request")
|
||||||
|
def bp_mw1(request):
|
||||||
|
nonlocal triggered
|
||||||
|
triggered = True
|
||||||
|
|
||||||
|
bp.static(
|
||||||
|
"/test.file",
|
||||||
|
get_file_path(static_file_directory, file_name),
|
||||||
|
strict_slashes=True,
|
||||||
|
name="static",
|
||||||
|
)
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
|
||||||
|
uri = app.url_for("test_mw.static")
|
||||||
|
assert uri == "/test.file"
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/test.file")
|
||||||
|
assert triggered is True
|
||||||
|
|
||||||
|
|
||||||
def test_route_handler_add(app: Sanic):
|
def test_route_handler_add(app: Sanic):
|
||||||
view = CompositionView()
|
view = CompositionView()
|
||||||
|
|
||||||
@@ -794,21 +825,6 @@ def test_duplicate_blueprint(app):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("debug", [True, False, None])
|
|
||||||
def test_register_blueprint(app, debug):
|
|
||||||
bp = Blueprint("bp")
|
|
||||||
|
|
||||||
app.debug = debug
|
|
||||||
with pytest.warns(DeprecationWarning) as record:
|
|
||||||
app.register_blueprint(bp)
|
|
||||||
|
|
||||||
assert record[0].message.args[0] == (
|
|
||||||
"Use of register_blueprint will be deprecated in "
|
|
||||||
"version 1.0. Please use the blueprint method"
|
|
||||||
" instead"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_strict_slashes_behavior_adoption(app):
|
def test_strict_slashes_behavior_adoption(app):
|
||||||
app.strict_slashes = True
|
app.strict_slashes = True
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from sanic.utils import load_module_from_file_location
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def loaded_module_from_file_location():
|
def loaded_module_from_file_location():
|
||||||
return load_module_from_file_location(
|
return load_module_from_file_location(
|
||||||
str(Path(__file__).parent / "static/app_test_config.py")
|
str(Path(__file__).parent / "static" / "app_test_config.py")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -23,7 +23,10 @@ def test_load_module_from_file_location(loaded_module_from_file_location):
|
|||||||
def test_loaded_module_from_file_location_name(
|
def test_loaded_module_from_file_location_name(
|
||||||
loaded_module_from_file_location,
|
loaded_module_from_file_location,
|
||||||
):
|
):
|
||||||
assert loaded_module_from_file_location.__name__ == "app_test_config"
|
name = loaded_module_from_file_location.__name__
|
||||||
|
if "C:\\" in name:
|
||||||
|
name = name.split("\\")[-1]
|
||||||
|
assert name == "app_test_config"
|
||||||
|
|
||||||
|
|
||||||
def test_load_module_from_file_location_with_non_existing_env_variable():
|
def test_load_module_from_file_location_with_non_existing_env_variable():
|
||||||
|
|||||||
@@ -290,6 +290,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("/")
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ def test_response_body_not_a_string(app):
|
|||||||
return text(random_num)
|
return text(random_num)
|
||||||
|
|
||||||
request, response = app.test_client.get("/hello")
|
request, response = app.test_client.get("/hello")
|
||||||
assert response.text == str(random_num)
|
assert response.status == 500
|
||||||
|
assert b"Internal Server Error" in response.body
|
||||||
|
|
||||||
|
|
||||||
async def sample_streaming_fn(response):
|
async def sample_streaming_fn(response):
|
||||||
@@ -235,6 +236,12 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
|
|||||||
assert response.text == "foo,bar"
|
assert response.text == "foo,bar"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_chunked_streaming_returns_correct_content_asgi(streaming_app):
|
||||||
|
request, response = await streaming_app.asgi_client.get("/")
|
||||||
|
assert response.text == "foo,bar"
|
||||||
|
|
||||||
|
|
||||||
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
|
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
|
||||||
request, response = non_chunked_streaming_app.test_client.get("/")
|
request, response = non_chunked_streaming_app.test_client.get("/")
|
||||||
assert "Transfer-Encoding" not in response.headers
|
assert "Transfer-Encoding" not in response.headers
|
||||||
@@ -242,6 +249,16 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
|
|||||||
assert response.headers["Content-Length"] == "7"
|
assert response.headers["Content-Length"] == "7"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_non_chunked_streaming_adds_correct_headers_asgi(
|
||||||
|
non_chunked_streaming_app,
|
||||||
|
):
|
||||||
|
request, response = await non_chunked_streaming_app.asgi_client.get("/")
|
||||||
|
assert "Transfer-Encoding" not in response.headers
|
||||||
|
assert response.headers["Content-Type"] == "text/csv"
|
||||||
|
assert response.headers["Content-Length"] == "7"
|
||||||
|
|
||||||
|
|
||||||
def test_non_chunked_streaming_returns_correct_content(
|
def test_non_chunked_streaming_returns_correct_content(
|
||||||
non_chunked_streaming_app,
|
non_chunked_streaming_app,
|
||||||
):
|
):
|
||||||
@@ -608,17 +625,3 @@ def test_empty_response(app):
|
|||||||
request, response = app.test_client.get("/test")
|
request, response = app.test_client.get("/test")
|
||||||
assert response.content_type is None
|
assert response.content_type is None
|
||||||
assert response.body == b""
|
assert response.body == b""
|
||||||
|
|
||||||
|
|
||||||
def test_response_body_bytes_deprecated(app):
|
|
||||||
with warnings.catch_warnings(record=True) as w:
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
|
|
||||||
HTTPResponse(body_bytes=b"bytes")
|
|
||||||
|
|
||||||
assert len(w) == 1
|
|
||||||
assert issubclass(w[0].category, DeprecationWarning)
|
|
||||||
assert (
|
|
||||||
"Parameter `body_bytes` is deprecated, use `body` instead"
|
|
||||||
in str(w[0].message)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sanic.app import Sanic
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def static_file_directory():
|
def static_file_directory():
|
||||||
@@ -15,6 +19,22 @@ def static_file_directory():
|
|||||||
return static_directory
|
return static_directory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def double_dotted_directory_file(static_file_directory: str):
|
||||||
|
"""Generate double dotted directory and its files"""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
raise Exception("Windows doesn't support double dotted directories")
|
||||||
|
|
||||||
|
file_path = Path(static_file_directory) / "dotted.." / "dot.txt"
|
||||||
|
double_dotted_dir = file_path.parent
|
||||||
|
Path.mkdir(double_dotted_dir, exist_ok=True)
|
||||||
|
with open(file_path, "w") as f:
|
||||||
|
f.write("DOT\n")
|
||||||
|
yield file_path
|
||||||
|
Path.unlink(file_path)
|
||||||
|
Path.rmdir(double_dotted_dir)
|
||||||
|
|
||||||
|
|
||||||
def get_file_path(static_file_directory, file_name):
|
def get_file_path(static_file_directory, file_name):
|
||||||
return os.path.join(static_file_directory, file_name)
|
return os.path.join(static_file_directory, file_name)
|
||||||
|
|
||||||
@@ -374,3 +394,43 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.platform == "win32",
|
||||||
|
reason="Windows does not support double dotted directories",
|
||||||
|
)
|
||||||
|
def test_dotted_dir_ok(
|
||||||
|
app: Sanic, static_file_directory: str, double_dotted_directory_file: Path
|
||||||
|
):
|
||||||
|
app.static("/foo", static_file_directory)
|
||||||
|
dot_relative_path = str(
|
||||||
|
double_dotted_directory_file.relative_to(static_file_directory)
|
||||||
|
)
|
||||||
|
_, response = app.test_client.get("/foo/" + dot_relative_path)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.body == b"DOT\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_breakout(app: Sanic, static_file_directory: str):
|
||||||
|
app.static("/foo", static_file_directory)
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/foo/..%2Ffake/server.py")
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/foo/..%2Fstatic/test.file")
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.platform != "win32", reason="Block backslash on Windows only"
|
||||||
|
)
|
||||||
|
def test_double_backslash_prohibited_on_win32(
|
||||||
|
app: Sanic, static_file_directory: str
|
||||||
|
):
|
||||||
|
app.static("/foo", static_file_directory)
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/foo/static/..\\static/test.file")
|
||||||
|
assert response.status == 404
|
||||||
|
_, response = app.test_client.get("/foo/static\\../static/test.file")
|
||||||
|
assert response.status == 404
|
||||||
|
|||||||
@@ -348,3 +348,13 @@ def test_methodview_naming(methodview_app):
|
|||||||
|
|
||||||
assert viewone_url == "/view_one"
|
assert viewone_url == "/view_one"
|
||||||
assert viewtwo_url == "/view_two"
|
assert viewtwo_url == "/view_two"
|
||||||
|
|
||||||
|
|
||||||
|
def test_url_for_with_websocket_handlers(app):
|
||||||
|
# Test for a specific bugfix in GH-2021
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def my_handler(request, ws):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert app.url_for("my_handler") == "/ws"
|
||||||
|
assert app.url_for("websocket_handler_my_handler") == "/ws"
|
||||||
|
|||||||
18
tox.ini
18
tox.ini
@@ -1,26 +1,26 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py36, py37, py38, pyNightly, {py36,py37,py38,pyNightly}-no-ext, lint, check, security, docs
|
envlist = py36, py37, py38, py39, pyNightly, {py36,py37,py38,py39,pyNightly}-no-ext, lint, check, security, docs
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop = True
|
usedevelop = True
|
||||||
setenv =
|
setenv =
|
||||||
{py36,py37,py38,pyNightly}-no-ext: SANIC_NO_UJSON=1
|
{py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UJSON=1
|
||||||
{py36,py37,py38,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
|
{py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
|
||||||
deps =
|
deps =
|
||||||
coverage
|
coverage==5.3
|
||||||
pytest==5.2.1
|
pytest==5.2.1
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest-sanic
|
pytest-sanic
|
||||||
pytest-sugar
|
pytest-sugar
|
||||||
pytest-benchmark
|
pytest-benchmark
|
||||||
pytest-dependency
|
pytest-dependency
|
||||||
httpcore==0.3.0
|
httpcore==0.11.*
|
||||||
httpx==0.15.4
|
httpx==0.15.4
|
||||||
chardet<=2.3.0
|
multidict>=5.0,<6.0
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
gunicorn
|
gunicorn==20.0.4
|
||||||
uvicorn
|
uvicorn
|
||||||
websockets>=8.1,<9.0
|
websockets>=8.1,<=9.1
|
||||||
commands =
|
commands =
|
||||||
pytest {posargs:tests --cov sanic}
|
pytest {posargs:tests --cov sanic}
|
||||||
- coverage combine --append
|
- coverage combine --append
|
||||||
@@ -76,7 +76,7 @@ deps =
|
|||||||
recommonmark>=0.5.0
|
recommonmark>=0.5.0
|
||||||
docutils
|
docutils
|
||||||
pygments
|
pygments
|
||||||
gunicorn
|
gunicorn==20.0.4
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
make docs-test
|
make docs-test
|
||||||
|
|||||||
Reference in New Issue
Block a user