Compare commits

...

118 Commits

Author SHA1 Message Date
Stephen Sadowski
6e55e73da1 fix: websocket dependency for websockets 9.1 security fix (#2366)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-01-16 20:42:38 +02:00
Adam Hopkins
89d942451f Merge branch 'pr2129' into 20.12LTS 2021-10-03 01:19:53 +03:00
Adam Hopkins
4d6205e6fe Bump version 2021-10-03 01:05:08 +03:00
Thomas Grainger
1684b0b986 remove reference to yanked packages 2021-07-02 10:02:17 +01:00
Thomas Grainger
4f5faa4a3c unpin uvloop 2021-05-04 18:14:14 +01:00
Arthur Goldberg
cbb77b536a fix issue where request.args.pop removed parameters inconsistently (#2112)
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-04-22 12:49:08 +03:00
Adam Hopkins
35c76253bf Bump version 20.12.3 (#2062) 2021-03-21 09:48:44 +02:00
laggardkernel
8d86c3c598 Remove unnecessary prefix from websocket handler name (#2021)
Remove the websocket prefix "websocket_handler_" introduced in
761eef7. Add a backward support for url_for() calling with this prefix
in param "view_name".
2021-03-14 20:33:07 +02:00
Adam Hopkins
97635111af Align setup.py 2021-02-16 09:33:48 +02:00
Adam Hopkins
7f3fe40cd4 Bump version 2021-02-16 08:50:35 +02:00
Adam Hopkins
ea34bcd849 Merge branch '20.12LTS' of github.com:sanic-org/sanic into 20.12LTS 2021-02-16 08:43:15 +02:00
Adam Hopkins
05f758583b Merge pull request #2029 from ashleysommer/tox_requires_2012
Fix tox requirements conflicts for 20.12LTS
2021-02-16 08:37:59 +02:00
Ashley Sommer
760c74a293 Merge remote-tracking branch 'origin/20.12LTS' into tox_requires_2012 2021-02-16 10:31:42 +10:00
Ashley Sommer
9def46beb8 Remove old chardet requirement, add our real multidict requirement 2021-02-16 10:03:40 +10:00
Ashley Sommer
04be8e95a5 Merge pull request #2026 from sanic-org/fix-uvloop-2012
Fix uvloop version for 20.12LTS
2021-02-16 09:23:57 +10:00
Adam Hopkins
78ced20fc7 fix uvloop version 2021-02-15 14:30:57 +02:00
Adam Hopkins
c3003413d3 Bump to version 20.12.1 2021-01-05 18:26:47 +02:00
Adam Hopkins
fe3fdc5d83 #1993 Disable registry (#1994)
* Bump to v20.12 (#1987)

* Bump to v20.12

* Update Changelog

* Add disable app registry

* squash

* Create FUNDING.yml (#1995)
2021-01-05 17:00:25 +02:00
Adam Hopkins
b66fb6f9e8 Merge branch 'master' into 20.12LTS 2020-12-28 23:21:46 +02:00
Adam Hopkins
bf6175fb20 Update Changelog 2020-12-28 23:18:19 +02:00
Tomasz Drożdż
7475897a03 Making static route more verbose if file not found (#1945)
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2020-12-28 23:17:32 +02:00
Adam Hopkins
58ca887be4 Bump to v20.12 2020-12-28 23:11:29 +02:00
Adam Hopkins
449bc417a3 App registry (#1979)
* Add app level registry

* Add documentation for app registry

* Remove unused import

* Add force_create keyword to Sanic.get_app

* Add force_commit to docs
2020-12-28 22:47:31 +02:00
Adam Hopkins
262f89f2b6 Merge pull request #1984 from markgras/patch-1
Fix typo in routing.rst
2020-12-25 08:29:21 +02:00
Adam Hopkins
38337446cf Merge branch 'master' into patch-1 2020-12-25 06:48:42 +02:00
Adam Hopkins
ac1331ea4c Merge pull request #1983 from sinabeuro/dev-typo_doc_r0
Remove duplicate contents in document
2020-12-25 06:48:26 +02:00
Adam Hopkins
2b947e831f Merge branch 'master' into dev-typo_doc_r0 2020-12-25 01:35:22 +02:00
Adam Hopkins
112715eb80 Merge pull request #1986 from huge-success/tox-reqs
Update reqs to get tox running
2020-12-25 01:34:45 +02:00
Adam Hopkins
ea9cf365bc Turn off Appyveyor 3.9 2020-12-24 22:09:51 +02:00
Adam Hopkins
b9b3b4051a Update reqs to get tox running 2020-12-24 21:56:35 +02:00
Adam Hopkins
ecb6db29e6 Merge branch 'master' into dev-typo_doc_r0 2020-12-24 21:00:20 +02:00
Adam Hopkins
6515dde64b Merge pull request #1981 from huge-success/deprecation-cleanup
Cleanup and remove some deprecated code
2020-12-24 19:48:58 +02:00
Mark Grassi
01d2a2aa3c Fix typo in routing.rst
This fixes a small typo in the routing docs.
2020-12-20 16:08:51 -05:00
sinabeuro
39e12accb8 Remove duplicate contents in document
Since the contents of line 61 and line 75 of the 'testing' document are
duplicated, the content of line 61 is removed for context.

Signed-off-by: sinabeuro <ican312@hanmail.net>
2020-12-18 12:25:24 +09:00
Adam Hopkins
39fe6ea5b1 Cleanup and remove some deprecated code 2020-12-14 09:23:13 +02:00
Adam Hopkins
fc4b7df088 Merge pull request #1961 from huge-success/py39
Update testing for Python 3.9
2020-12-10 09:25:12 +02:00
Adam Hopkins
35f28f7a64 Merge branch 'master' into py39 2020-12-09 11:52:49 +02:00
Adam Hopkins
614be40438 Name endpoints at startup (#1972)
* Name endpoints at startup

* Beautify

* Fix reformatting
2020-11-29 23:26:12 +02:00
Adam Hopkins
bde0428d0c Update README.rst (#1973)
Change `.org` to `.com` for transition in Travis.
2020-11-23 02:02:33 +02:00
Trevor Bekolay
63567c2ae4 Add py.typed file (#1970) 2020-11-19 11:18:25 +02:00
Ashley Sommer
ec10f337b6 Merge pull request #1969 from all2ham/remove-upper-bound-multidict-pin
loosen pin on multidict, add higher upper bound to multidict requirement
2020-11-18 15:25:18 +10:00
allandialpad
d0f0e73e96 remove upper bound for multidict 2020-11-17 11:18:18 -05:00
7
b4fe2c8a6b bump up aiofile version constraint (#1967) 2020-11-06 08:32:04 +02:00
Adam Hopkins
33da0771d1 Merge pull request #1965 from ashleysommer/asgs_chunk_length
Fix Chunked Transport-Encoding in ASGI streaming response
2020-11-05 09:02:18 +02:00
Adam Hopkins
75994cd915 Fixes for linting and type hints 2020-11-05 08:49:55 +02:00
Ashley Sommer
c0839afdde Fix Chunked Transport-Encoding in ASGI streaming response
In ASGI-mode, don't do sanic-side response chunk encoding, leave that to the ASGI-response-transport
Don't set content-length when using chunked-encoding in ASGI mode, this is incompatible with ASGI Chunked Transport-Encoding.
2020-11-05 15:27:01 +10:00
Ashley Sommer
5961da3f57 Merge pull request #1960 from huge-success/release-notes-19.12.3-20.9.1
Update changelog for 19.12.3 and 20.9.1
2020-10-26 11:43:53 +10:00
Ashley Sommer
41f1809351 Merge branch 'master' into release-notes-19.12.3-20.9.1 2020-10-26 09:58:19 +10:00
Ashley Sommer
5fbdcb62e4 Merge pull request #1962 from huge-success/cli-upgrade
Sanic CLI upgrade
2020-10-26 08:31:45 +10:00
Ashley Sommer
677b83e9f8 Merge branch 'master' into release-notes-19.12.3-20.9.1 2020-10-26 08:21:44 +10:00
Adam Hopkins
6a5c8becac Merge branch 'master' into cli-upgrade 2020-10-25 22:46:37 +02:00
Adam Hopkins
fd23b99d60 Merge pull request #1951 from tomaszdrozdz/Improving-documentation
Improving documentation.
2020-10-25 22:45:37 +02:00
Adam Hopkins
634b586df3 Merge branch 'master' into Improving-documentation 2020-10-25 21:32:54 +02:00
Ashley Sommer
4ca3e98082 Add pytest-dependency requirement to tests_require list in setup.py (#1955)
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2020-10-25 21:31:34 +02:00
Adam Hopkins
d18a776964 squash 2020-10-25 21:22:19 +02:00
Adam Hopkins
d6b4d7d265 Add bionic in travis and change path in appveyor 2020-10-25 21:01:31 +02:00
Adam Hopkins
33ee4c21b3 Add BASE_LOGO to sanic cli 2020-10-25 20:45:06 +02:00
Adam Hopkins
a026cd7195 add --access-logs flag to sanic cli 2020-10-25 20:36:22 +02:00
Adam Hopkins
7b1bce8d90 Add some help messages and a user friendly cli experience 2020-10-25 20:21:09 +02:00
Adam Hopkins
217a7c5161 Small changes to sanic-cli to make it more user friendly 2020-10-25 20:09:42 +02:00
Adam Hopkins
2949e3422d Add 3.9 to appveyor 2020-10-25 15:37:48 +02:00
Adam Hopkins
16ea99b0c0 Update testing for Python 3.9 2020-10-25 15:21:48 +02:00
Adam Hopkins
19b84ce9f0 Update changelog for 19.12.3 and 20.9.1 2020-10-25 15:11:39 +02:00
Adam Hopkins
e5aed4c067 Ignore writing headers when in ASGI mode (#1957)
* Ignore writing headers when in ASGI mode for streaming responses

* Move asgi set on streaming until after response type check

* Adds multidict==5.0.0 to pass tests

* Bump version to 20.9.1
2020-10-25 15:01:53 +02:00
Ashley Sommer
9e048bc0c3 Merge pull request #1956 from huge-success/fix-load-module-test
Fix load module test
2020-10-25 22:09:24 +10:00
Adam Hopkins
5d7b0735ce Merge branch 'master' into fix-load-module-test 2020-10-25 08:27:25 +02:00
Adam Hopkins
12521cd5b4 Merge branch 'master' into Improving-documentation 2020-10-25 00:02:32 +03:00
Adam Hopkins
7dbd3eb5e8 Update multidict version 2020-10-24 23:49:55 +03:00
Adam Hopkins
96364aacc0 squash 2020-10-24 23:42:38 +03:00
Adam Hopkins
fc18f86964 Resolve broken test in appveyor 2020-10-24 23:03:25 +03:00
Ashley Sommer
fb3d368a78 Add ability for app.static() to return the routes it created. (#1954)
This allows blueprint registration to add the bp's static routes to its list of own routes. So now blueprint middlewares will apply to a blueprint's static file routes.
Fixes #1953
2020-10-24 22:57:02 +03:00
tomaszdrozdz
f41435fae3 Improving documentation. 2020-10-19 10:12:20 +02:00
Adam Hopkins
5928c50057 Version 20.9 (#1940) 2020-09-30 17:30:21 +03:00
Tomasz Drożdż
1de4bcef55 Update config (#1903)
* New aproach for uploading sanic app config.

* Update config.rst

Co-authored-by: tigerthelion <bjt.thompson@gmail.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2020-09-30 16:44:09 +03:00
Adam Hopkins
7b7559309d Add issue config.yml (#1936)
* Add issue config.yml

* Update SECURITY.md
2020-09-30 15:38:08 +03:00
Adam Hopkins
066df2c142 Add text and json fallback error handlers (#1937)
* Add text and json fallback error handlers

* Add tests and auto-detect error fallback type
2020-09-30 15:11:27 +03:00
Adam Hopkins
0c4a9b1dce Merge pull request #1909 from brooklet/master
fix websocket ping variables issues
2020-09-29 01:08:04 +03:00
Adam Hopkins
65a7060d3b Merge branch 'master' into master 2020-09-29 00:41:22 +03:00
Adam Hopkins
3483e7b061 Fix linting issues 2020-09-29 00:40:24 +03:00
Adam Hopkins
13094e02bc Revert check for websocket protocol to use hasattr 2020-09-29 00:24:00 +03:00
Ashley Sommer
ed777e9d5b Merge pull request #1935 from huge-success/httpx-upgrade
Upgrade httpx
2020-09-28 09:06:37 +10:00
Adam Hopkins
8ad80a282a Merge branch 'master' into httpx-upgrade 2020-09-27 11:20:07 +03:00
Adam Hopkins
0b7eb49839 Merge pull request #1924 from tomaszdrozdz/strict_markers_for_pytest
Adding --strict-markers for pytest
2020-09-27 11:18:24 +03:00
Adam Hopkins
de3b40c2e6 Merge branch 'master' into strict_markers_for_pytest 2020-09-27 10:57:31 +03:00
Adam Hopkins
efa0aaf2c2 Add asyncio markers to tox.ini 2020-09-27 10:46:51 +03:00
Adam Hopkins
bd4e1cdc1e squash 2020-09-27 10:27:12 +03:00
Adam Hopkins
eb8df1fc18 Upgrade httpx 2020-09-27 02:58:36 +03:00
tomaszdrozdz
9a8e49751d Adding --strict-markers for pytest 2020-09-08 13:08:49 +02:00
raphaelauv
58e15134fd Add explicit ASGI compliance to the README (#1922) 2020-09-02 23:22:02 +03:00
Adam Hopkins
875be11ae5 Update README.rst (#1917) 2020-08-27 10:28:56 +03:00
Andrew Scott
3f7c9ea3f5 feat: fixes exception due to unread bytes in stream (#1897)
* feat: fixes exception due to unread bytes in stream

* feat: additonal unit tests to cover changes

* fix: automated changes by `make fix-import`

* fix: additonal changes by `make fix-import`

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2020-08-27 10:22:02 +03:00
brook
33aa4daac8 fixed the problem that the websocket ping_timeout and ping_interval parameter settings did not take effect 2020-08-13 14:39:55 +08:00
Shawn Hill
58e4087d4b Add websocket ping variables (#1906)
* Add config params for websocket ping_timeout & ping_interval

* Include changelog

* Pass websocket config values to WebSocketProtocol init, test

* Linting

* Improve docs

Co-authored-by: shawnhill <shawn.hill@equipmentshare.com>
2020-08-07 06:37:59 +03:00
Ashley Sommer
0072fd1573 Add an additional component to the request_data context test. This checks if items stored a request.ctx are able to be accessed from a response-middleware after a response is issued. (#1888)
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2020-07-29 14:25:31 +03:00
Lee Tat Wai David
5d5ed10a45 Websocket subprotocol (#1887)
* Added fix to include subprotocols from scope

* Added unit test to validate fix

* Changes by black

* Made changes to WebsocketConnection protocol

* Linter changes

* Added unit tests

* Fixing bugs in linting due to isort import checks

* Reverting compat import changes

* Fixing linter errors in compat.py

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2020-07-29 14:09:26 +03:00
Ashley Sommer
5ee8ee7b04 Merge pull request #1894 from huge-success/test_mode
add a test_mode boolean variable to sanic `app` which is set to True when using Sanic TestClient or ASGIClient, and False all other times.
2020-07-15 22:46:23 +10:00
Adam Hopkins
521ae7f60e squash 2020-07-14 10:41:28 +03:00
Adam Hopkins
27c8c12420 squash 2020-07-14 10:30:48 +03:00
Adam Hopkins
3d1f100781 squash 2020-07-14 10:30:01 +03:00
Adam Hopkins
16d36fc17f squash 2020-07-14 10:25:56 +03:00
Adam Hopkins
eddb5bad91 squash 2020-07-14 10:25:30 +03:00
Adam Hopkins
23e1b5ee3f squash 2020-07-14 10:23:31 +03:00
Adam Hopkins
9e053bef19 squash 2020-07-14 10:13:30 +03:00
Adam Hopkins
cf234fca15 squash this 2020-07-13 23:59:45 +03:00
Adam Hopkins
050a563e1d Add documentation on test mode 2020-07-09 14:57:42 +03:00
Adam Hopkins
c347ff742e Add app.test_mode which is set on testing calls 2020-07-09 14:52:58 +03:00
Adam Hopkins
db1c819fe1 Merge branch 'master' of github.com:huge-success/sanic 2020-07-09 14:24:06 +03:00
Egor
9f2818ee29 Remove version section (#1893) 2020-07-09 07:17:50 +03:00
Adam Hopkins
26aa6d23c7 Fix imports and isort to remove from Makefile deprecated options (#1891)
* Version

* Version 20.6.1

* Fix imports and isort to remove from Makefile deprecated options

* duplicate the mypy ignore hint across both lines

after splitting the `from trio import ...` statement onto two lines, need to duplicate the mypy ignore hint across both lines to keep mypy from complaining

Co-authored-by: Ashley Sommer <ashleysommer@gmail.com>
2020-07-07 16:13:03 +03:00
Adam Hopkins
ec7e894eb3 Merge branch 'master' of github.com:huge-success/sanic 2020-07-07 08:46:01 +03:00
Ashley Sommer
71a08382d6 Adjust isort options and invocation to work on isort 5.0.0 (#1890)
isort 5.0.0 removed command line option `recursive` and removed config option `not_skip`.
2020-07-07 08:43:33 +03:00
Adam Hopkins
09224f8676 Merge branch 'master' of github.com:huge-success/sanic 2020-06-29 15:19:32 +03:00
Adam Hopkins
008b8ac394 V2.6.3 changelog (#1886)
* Version

* Version 20.6.1

* v2.6.3 changelog and version
2020-06-29 15:16:06 +03:00
Adam Hopkins
a357add14e Merge branch 'master' of github.com:huge-success/sanic 2020-06-29 14:55:52 +03:00
Adam Hopkins
0cfd7b528b V20.6.2 changelog (#1885)
* Version

* Version 20.6.1

* CHANGELOG for v20.6.2
2020-06-29 14:54:44 +03:00
Adam Hopkins
9ba4fe05df Merge branch 'master' of github.com:huge-success/sanic 2020-06-29 14:54:02 +03:00
Ashley Sommer
35786b4b74 Revert change to multiprocessing mode (#1884)
Revert change to multiprocessing mode accidentally included in https://github.com/huge-success/sanic/pull/1853

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2020-06-29 14:51:30 +03:00
Ashley Sommer
c7430d805a Revert change to multiprocessing mode (#1884)
Revert change to multiprocessing mode accidentally included in https://github.com/huge-success/sanic/pull/1853

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2020-06-29 13:51:55 +03:00
76 changed files with 2494 additions and 667 deletions

View File

@@ -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
View 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']

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Questions and Help
url: https://community.sanicframework.org/c/questions-and-help
about: Do you need help with Sanic? Ask your questions here.

View File

@@ -1,13 +0,0 @@
---
name: Help wanted
about: Do you need help? Try community.sanicframework.org
---
*DELETE ALL BEFORE POSTING*
*Post your HELP WANTED questions on [the community forum](https://community.sanicframework.org/)*.
Checkout the community forum before posting any question here.
We prefer if you put these kinds of questions here:
https://community.sanicframework.org/c/questions-and-help

40
.github/workflows/codeql-analysis.yml vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"

View File

@@ -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

View File

@@ -1,9 +1,223 @@
Version 20.12.5
===============
Bugfixes
********
*
`#2366 <https://github.com/sanic-org/sanic/pull/2366>`_
websocket dependency for websockets 9.1 security fix
Version 20.12.0
===============
Features
********
*
`#1945 <https://github.com/huge-success/sanic/pull/1945>`_
Static route more verbose if file not found
*
`#1954 <https://github.com/huge-success/sanic/pull/1954>`_
Fix static routes registration on a blueprint
*
`#1961 <https://github.com/huge-success/sanic/pull/1961>`_
Add Python 3.9 support
*
`#1962 <https://github.com/huge-success/sanic/pull/1962>`_
Sanic CLI upgrade
*
`#1967 <https://github.com/huge-success/sanic/pull/1967>`_
Update aiofile version requirements
*
`#1969 <https://github.com/huge-success/sanic/pull/1969>`_
Update multidict version requirements
*
`#1970 <https://github.com/huge-success/sanic/pull/1970>`_
Add py.typed file
*
`#1972 <https://github.com/huge-success/sanic/pull/1972>`_
Speed optimization in request handler
*
`#1979 <https://github.com/huge-success/sanic/pull/1979>`_
Add app registry and Sanic class level app retrieval
Bugfixes
********
*
`#1965 <https://github.com/huge-success/sanic/pull/1965>`_
Fix Chunked Transport-Encoding in ASGI streaming response
Deprecations and Removals
*************************
*
`#1981 <https://github.com/huge-success/sanic/pull/1981>`_
Cleanup and remove deprecated code
Developer infrastructure
************************
*
`#1956 <https://github.com/huge-success/sanic/pull/1956>`_
Fix load module test
*
`#1973 <https://github.com/huge-success/sanic/pull/1973>`_
Transition Travis from .org to .com
*
`#1986 <https://github.com/huge-success/sanic/pull/1986>`_
Update tox requirements
Improved Documentation
**********************
*
`#1951 <https://github.com/huge-success/sanic/pull/1951>`_
Documentation improvements
*
`#1983 <https://github.com/huge-success/sanic/pull/1983>`_
Remove duplicate contents in testing.rst
*
`#1984 <https://github.com/huge-success/sanic/pull/1984>`_
Fix typo in routing.rst
Version 20.9.1
===============
Bugfixes
********
*
`#1954 <https://github.com/huge-success/sanic/pull/1954>`_
Fix static route registration on blueprints
*
`#1957 <https://github.com/huge-success/sanic/pull/1957>`_
Removes duplicate headers in ASGI streaming body
Version 19.12.3
===============
Bugfixes
********
*
`#1959 <https://github.com/huge-success/sanic/pull/1959>`_
Removes duplicate headers in ASGI streaming body
Version 20.9.0
===============
Features
********
*
`#1887 <https://github.com/huge-success/sanic/pull/1887>`_
Pass subprotocols in websockets (both sanic server and ASGI)
*
`#1894 <https://github.com/huge-success/sanic/pull/1894>`_
Automatically set ``test_mode`` flag on app instance
*
`#1903 <https://github.com/huge-success/sanic/pull/1903>`_
Add new unified method for updating app values
*
`#1906 <https://github.com/huge-success/sanic/pull/1906>`_,
`#1909 <https://github.com/huge-success/sanic/pull/1909>`_
Adds WEBSOCKET_PING_TIMEOUT and WEBSOCKET_PING_INTERVAL configuration values
*
`#1935 <https://github.com/huge-success/sanic/pull/1935>`_
httpx version dependency updated, it is slated for removal as a dependency in v20.12
*
`#1937 <https://github.com/huge-success/sanic/pull/1937>`_
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
Bugfixes
********
*
`#1897 <https://github.com/huge-success/sanic/pull/1897>`_
Resolves exception from unread bytes in stream
Deprecations and Removals
*************************
*
`#1903 <https://github.com/huge-success/sanic/pull/1903>`_
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
Developer infrastructure
************************
*
`#1890 <https://github.com/huge-success/sanic/pull/1890>`_,
`#1891 <https://github.com/huge-success/sanic/pull/1891>`_
Update isort calls to be compatible with new API
*
`#1893 <https://github.com/huge-success/sanic/pull/1893>`_
Remove version section from setup.cfg
*
`#1924 <https://github.com/huge-success/sanic/pull/1924>`_
Adding --strict-markers for pytest
Improved Documentation
**********************
*
`#1922 <https://github.com/huge-success/sanic/pull/1922>`_
Add explicit ASGI compliance to the README
Version 20.6.3
===============
Bugfixes
********
*
`#1884 <https://github.com/huge-success/sanic/pull/1884>`_
Revert change to multiprocessing mode
Version 20.6.2
===============
Features
********
*
`#1641 <https://github.com/huge-success/sanic/pull/1641>`_
Socket binding implemented properly for IPv6 and UNIX sockets
Version 20.6.1 Version 20.6.1
=============== ===============
Features Features
******** ********
* *
`#1760 <https://github.com/huge-success/sanic/pull/1760>`_ `#1760 <https://github.com/huge-success/sanic/pull/1760>`_
Add version parameter to websocket routes Add version parameter to websocket routes
@@ -14,7 +228,7 @@ Features
* *
`#1880 <https://github.com/huge-success/sanic/pull/1880>`_ `#1880 <https://github.com/huge-success/sanic/pull/1880>`_
Add handler names for websockets for url_for usage Add handler names for websockets for url_for usage
Bugfixes Bugfixes
******** ********
@@ -34,7 +248,7 @@ Bugfixes
* *
`#1848 <https://github.com/huge-success/sanic/pull/1848>`_ `#1848 <https://github.com/huge-success/sanic/pull/1848>`_
Reverse named_response_middlware execution order, to match normal response middleware execution order Reverse named_response_middlware execution order, to match normal response middleware execution order
* *
`#1853 <https://github.com/huge-success/sanic/pull/1853>`_ `#1853 <https://github.com/huge-success/sanic/pull/1853>`_
Fix pickle error when attempting to pickle an application which contains websocket routes Fix pickle error when attempting to pickle an application which contains websocket routes
@@ -73,34 +287,41 @@ Improved Documentation
`#1865 <https://github.com/huge-success/sanic/pull/1865>`_ `#1865 <https://github.com/huge-success/sanic/pull/1865>`_
Fixing rst format issue that was hiding documentation Fixing rst format issue that was hiding documentation
Version 20.6.0
===============
*Released, but unintentionally ommitting PR #1880, so was replaced by 20.6.1*
Version 20.3.0 Version 20.3.0
=============== ===============
Features Features
******** ********
* *
`#1762 <https://github.com/huge-success/sanic/pull/1762>`_ `#1762 <https://github.com/huge-success/sanic/pull/1762>`_
Add ``srv.start_serving()`` and ``srv.serve_forever()`` to ``AsyncioServer`` Add ``srv.start_serving()`` and ``srv.serve_forever()`` to ``AsyncioServer``
* *
`#1767 <https://github.com/huge-success/sanic/pull/1767>`_ `#1767 <https://github.com/huge-success/sanic/pull/1767>`_
Make Sanic usable on ``hypercorn -k trio myweb.app`` Make Sanic usable on ``hypercorn -k trio myweb.app``
* *
`#1768 <https://github.com/huge-success/sanic/pull/1768>`_ `#1768 <https://github.com/huge-success/sanic/pull/1768>`_
No tracebacks on normal errors and prettier error pages No tracebacks on normal errors and prettier error pages
* *
`#1769 <https://github.com/huge-success/sanic/pull/1769>`_ `#1769 <https://github.com/huge-success/sanic/pull/1769>`_
Code cleanup in file responses Code cleanup in file responses
* *
`#1793 <https://github.com/huge-success/sanic/pull/1793>`_ and `#1793 <https://github.com/huge-success/sanic/pull/1793>`_ and
`#1819 <https://github.com/huge-success/sanic/pull/1819>`_ `#1819 <https://github.com/huge-success/sanic/pull/1819>`_
Upgrade ``str.format()`` to f-strings Upgrade ``str.format()`` to f-strings
* *
`#1798 <https://github.com/huge-success/sanic/pull/1798>`_ `#1798 <https://github.com/huge-success/sanic/pull/1798>`_
Allow multiple workers on MacOS with Python 3.8 Allow multiple workers on MacOS with Python 3.8
@@ -111,19 +332,19 @@ Features
Bugfixes Bugfixes
******** ********
* *
`#1748 <https://github.com/huge-success/sanic/pull/1748>`_ `#1748 <https://github.com/huge-success/sanic/pull/1748>`_
Remove loop argument in ``asyncio.Event`` in Python 3.8 Remove loop argument in ``asyncio.Event`` in Python 3.8
* *
`#1764 <https://github.com/huge-success/sanic/pull/1764>`_ `#1764 <https://github.com/huge-success/sanic/pull/1764>`_
Allow route decorators to stack up again Allow route decorators to stack up again
* *
`#1789 <https://github.com/huge-success/sanic/pull/1789>`_ `#1789 <https://github.com/huge-success/sanic/pull/1789>`_
Fix tests using hosts yielding incorrect ``url_for`` Fix tests using hosts yielding incorrect ``url_for``
* *
`#1808 <https://github.com/huge-success/sanic/pull/1808>`_ `#1808 <https://github.com/huge-success/sanic/pull/1808>`_
Fix Ctrl+C and tests on Windows Fix Ctrl+C and tests on Windows
@@ -137,7 +358,7 @@ Deprecations and Removals
* *
`#1801 <https://github.com/huge-success/sanic/pull/1801>`_ `#1801 <https://github.com/huge-success/sanic/pull/1801>`_
Complete deprecation from `#1666 <https://github.com/huge-success/sanic/pull/1666>`_ of dictionary context on ``request`` objects. Complete deprecation from `#1666 <https://github.com/huge-success/sanic/pull/1666>`_ of dictionary context on ``request`` objects.
* *
`#1807 <https://github.com/huge-success/sanic/pull/1807>`_ `#1807 <https://github.com/huge-success/sanic/pull/1807>`_
Remove server config args that can be read directly from app Remove server config args that can be read directly from app
@@ -160,22 +381,22 @@ Dependencies
Developer infrastructure Developer infrastructure
************************ ************************
* *
`#1833 <https://github.com/huge-success/sanic/pull/1833>`_ `#1833 <https://github.com/huge-success/sanic/pull/1833>`_
Resolve broken documentation builds Resolve broken documentation builds
Improved Documentation Improved Documentation
********************** **********************
* *
`#1755 <https://github.com/huge-success/sanic/pull/1755>`_ `#1755 <https://github.com/huge-success/sanic/pull/1755>`_
Usage of ``response.empty()`` Usage of ``response.empty()``
* *
`#1778 <https://github.com/huge-success/sanic/pull/1778>`_ `#1778 <https://github.com/huge-success/sanic/pull/1778>`_
Update README Update README
* *
`#1783 <https://github.com/huge-success/sanic/pull/1783>`_ `#1783 <https://github.com/huge-success/sanic/pull/1783>`_
Fix typo Fix typo
@@ -202,7 +423,7 @@ Improved Documentation
* *
`#1834 <https://github.com/huge-success/sanic/pull/1834>`_ `#1834 <https://github.com/huge-success/sanic/pull/1834>`_
Order of listeners Order of listeners
Version 19.12.0 Version 19.12.0
=============== ===============
@@ -268,16 +489,16 @@ Version 19.6.2
Features Features
******** ********
* *
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_ `#1562 <https://github.com/huge-success/sanic/pull/1562>`_
Remove ``aiohttp`` dependencey and create new ``SanicTestClient`` based upon Remove ``aiohttp`` dependency and create new ``SanicTestClient`` based upon
`requests-async <https://github.com/encode/requests-async>`_ `requests-async <https://github.com/encode/requests-async>`_
* *
`#1475 <https://github.com/huge-success/sanic/pull/1475>`_ `#1475 <https://github.com/huge-success/sanic/pull/1475>`_
Added ASGI support (Beta) Added ASGI support (Beta)
* *
`#1436 <https://github.com/huge-success/sanic/pull/1436>`_ `#1436 <https://github.com/huge-success/sanic/pull/1436>`_
Add Configure support from object string Add Configure support from object string
@@ -285,19 +506,19 @@ Features
Bugfixes Bugfixes
******** ********
* *
`#1587 <https://github.com/huge-success/sanic/pull/1587>`_ `#1587 <https://github.com/huge-success/sanic/pull/1587>`_
Add missing handle for Expect header. Add missing handle for Expect header.
* *
`#1560 <https://github.com/huge-success/sanic/pull/1560>`_ `#1560 <https://github.com/huge-success/sanic/pull/1560>`_
Allow to disable Transfer-Encoding: chunked. Allow to disable Transfer-Encoding: chunked.
* *
`#1558 <https://github.com/huge-success/sanic/pull/1558>`_ `#1558 <https://github.com/huge-success/sanic/pull/1558>`_
Fix graceful shutdown. Fix graceful shutdown.
* *
`#1594 <https://github.com/huge-success/sanic/pull/1594>`_ `#1594 <https://github.com/huge-success/sanic/pull/1594>`_
Strict Slashes behavior fix Strict Slashes behavior fix
@@ -308,11 +529,11 @@ Deprecations and Removals
`#1544 <https://github.com/huge-success/sanic/pull/1544>`_ `#1544 <https://github.com/huge-success/sanic/pull/1544>`_
Drop dependency on distutil Drop dependency on distutil
* *
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_ `#1562 <https://github.com/huge-success/sanic/pull/1562>`_
Drop support for Python 3.5 Drop support for Python 3.5
* *
`#1568 <https://github.com/huge-success/sanic/pull/1568>`_ `#1568 <https://github.com/huge-success/sanic/pull/1568>`_
Deprecate route removal. Deprecate route removal.
@@ -329,39 +550,39 @@ Version 19.3
Features Features
******** ********
* *
`#1497 <https://github.com/huge-success/sanic/pull/1497>`_ `#1497 <https://github.com/huge-success/sanic/pull/1497>`_
Add support for zero-length and RFC 5987 encoded filename for Add support for zero-length and RFC 5987 encoded filename for
multipart/form-data requests. multipart/form-data requests.
* *
`#1484 <https://github.com/huge-success/sanic/pull/1484>`_ `#1484 <https://github.com/huge-success/sanic/pull/1484>`_
The type of ``expires`` attribute of ``sanic.cookies.Cookie`` is now The type of ``expires`` attribute of ``sanic.cookies.Cookie`` is now
enforced to be of type ``datetime``. enforced to be of type ``datetime``.
* *
`#1482 <https://github.com/huge-success/sanic/pull/1482>`_ `#1482 <https://github.com/huge-success/sanic/pull/1482>`_
Add support for the ``stream`` parameter of ``sanic.Sanic.add_route()`` Add support for the ``stream`` parameter of ``sanic.Sanic.add_route()``
available to ``sanic.Blueprint.add_route()``. available to ``sanic.Blueprint.add_route()``.
* *
`#1481 <https://github.com/huge-success/sanic/pull/1481>`_ `#1481 <https://github.com/huge-success/sanic/pull/1481>`_
Accept negative values for route parameters with type ``int`` or ``number``. Accept negative values for route parameters with type ``int`` or ``number``.
* *
`#1476 <https://github.com/huge-success/sanic/pull/1476>`_ `#1476 <https://github.com/huge-success/sanic/pull/1476>`_
Deprecated the use of ``sanic.request.Request.raw_args`` - it has a Deprecated the use of ``sanic.request.Request.raw_args`` - it has a
fundamental flaw in which is drops repeated query string parameters. fundamental flaw in which is drops repeated query string parameters.
Added ``sanic.request.Request.query_args`` as a replacement for the Added ``sanic.request.Request.query_args`` as a replacement for the
original use-case. original use-case.
* *
`#1472 <https://github.com/huge-success/sanic/pull/1472>`_ `#1472 <https://github.com/huge-success/sanic/pull/1472>`_
Remove an unwanted ``None`` check in Request class ``repr`` implementation. Remove an unwanted ``None`` check in Request class ``repr`` implementation.
This changes the default ``repr`` of a Request from ``<Request>`` to This changes the default ``repr`` of a Request from ``<Request>`` to
``<Request: None />`` ``<Request: None />``
* *
`#1470 <https://github.com/huge-success/sanic/pull/1470>`_ `#1470 <https://github.com/huge-success/sanic/pull/1470>`_
Added 2 new parameters to ``sanic.app.Sanic.create_server``\ : Added 2 new parameters to ``sanic.app.Sanic.create_server``\ :
@@ -372,21 +593,21 @@ Features
This is a breaking change. This is a breaking change.
* *
`#1499 <https://github.com/huge-success/sanic/pull/1499>`_ `#1499 <https://github.com/huge-success/sanic/pull/1499>`_
Added a set of test cases that test and benchmark route resolution. Added a set of test cases that test and benchmark route resolution.
* *
`#1457 <https://github.com/huge-success/sanic/pull/1457>`_ `#1457 <https://github.com/huge-success/sanic/pull/1457>`_
The type of the ``"max-age"`` value in a ``sanic.cookies.Cookie`` is now The type of the ``"max-age"`` value in a ``sanic.cookies.Cookie`` is now
enforced to be an integer. Non-integer values are replaced with ``0``. enforced to be an integer. Non-integer values are replaced with ``0``.
* *
`#1445 <https://github.com/huge-success/sanic/pull/1445>`_ `#1445 <https://github.com/huge-success/sanic/pull/1445>`_
Added the ``endpoint`` attribute to an incoming ``request``\ , containing the Added the ``endpoint`` attribute to an incoming ``request``\ , containing the
name of the handler function. name of the handler function.
* *
`#1423 <https://github.com/huge-success/sanic/pull/1423>`_ `#1423 <https://github.com/huge-success/sanic/pull/1423>`_
Improved request streaming. ``request.stream`` is now a bounded-size buffer Improved request streaming. ``request.stream`` is now a bounded-size buffer
instead of an unbounded queue. Callers must now call instead of an unbounded queue. Callers must now call
@@ -399,7 +620,7 @@ Bugfixes
******** ********
* *
`#1502 <https://github.com/huge-success/sanic/pull/1502>`_ `#1502 <https://github.com/huge-success/sanic/pull/1502>`_
Sanic was prefetching ``time.time()`` and updating it once per second to Sanic was prefetching ``time.time()`` and updating it once per second to
avoid excessive ``time.time()`` calls. The implementation was observed to avoid excessive ``time.time()`` calls. The implementation was observed to
@@ -407,25 +628,25 @@ Bugfixes
to negligible, so this has been removed. Fixes to negligible, so this has been removed. Fixes
`#1500 <https://github.com/huge-success/sanic/pull/1500>`_ `#1500 <https://github.com/huge-success/sanic/pull/1500>`_
* *
`#1501 <https://github.com/huge-success/sanic/pull/1501>`_ `#1501 <https://github.com/huge-success/sanic/pull/1501>`_
Fix a bug in the auto-reloader when the process was launched as a module Fix a bug in the auto-reloader when the process was launched as a module
i.e. ``python -m init0.mod1`` where the sanic server is started i.e. ``python -m init0.mod1`` where the sanic server is started
in ``init0/mod1.py`` with ``debug`` enabled and imports another module in in ``init0/mod1.py`` with ``debug`` enabled and imports another module in
``init0``. ``init0``.
* *
`#1376 <https://github.com/huge-success/sanic/pull/1376>`_ `#1376 <https://github.com/huge-success/sanic/pull/1376>`_
Allow sanic test client to bind to a random port by specifying Allow sanic test client to bind to a random port by specifying
``port=None`` when constructing a ``SanicTestClient`` ``port=None`` when constructing a ``SanicTestClient``
* *
`#1399 <https://github.com/huge-success/sanic/pull/1399>`_ `#1399 <https://github.com/huge-success/sanic/pull/1399>`_
Added the ability to specify middleware on a blueprint group, so that all Added the ability to specify middleware on a blueprint group, so that all
routes produced from the blueprints in the group have the middleware routes produced from the blueprints in the group have the middleware
applied. applied.
* *
`#1442 <https://github.com/huge-success/sanic/pull/1442>`_ `#1442 <https://github.com/huge-success/sanic/pull/1442>`_
Allow the the use the ``SANIC_ACCESS_LOG`` environment variable to Allow the the use the ``SANIC_ACCESS_LOG`` environment variable to
enable/disable the access log when not explicitly passed to ``app.run()``. enable/disable the access log when not explicitly passed to ``app.run()``.
@@ -467,7 +688,7 @@ Version 18.12
18.12.0 18.12.0
******* *******
* *
Changes: Changes:
@@ -485,7 +706,7 @@ Version 18.12
* Deprecate Handler.log * Deprecate Handler.log
* Pinned httptools requirement to version 0.0.10+ * Pinned httptools requirement to version 0.0.10+
* *
Fixes: Fixes:

View File

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

View File

@@ -26,8 +26,8 @@ Sanic | Build fast. Run fast.
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg .. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/huge-success/sanic :target: https://codecov.io/gh/huge-success/sanic
.. |Build Status| image:: https://travis-ci.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
@@ -58,6 +58,8 @@ Sanic | Build fast. Run fast.
Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-asgi>`_.
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_. `Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
The project is maintained by the community, for the community. **Contributions are welcome!** The project is maintained by the community, for the community. **Contributions are welcome!**
@@ -104,7 +106,7 @@ Hello World Example
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000) app.run(host='0.0.0.0', port=8000)
Sanic can now be easily run using ``python3 hello.py``. Sanic can now be easily run using ``sanic hello.app``.
.. code:: .. code::

View File

@@ -4,19 +4,27 @@
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release. Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
| Version | LTS | Supported | | Version | LTS | Supported |
| ------- | ------------------ | ------------------ | | ------- | ------------- | ------------------ |
| 19.6.0 | | :white_check_mark: | | 20.9 | | :heavy_check_mark: |
| 19.3.1 | | :heavy_check_mark: | | 20.6 | | :x: |
| 18.12.0 | :heavy_check_mark: | :heavy_check_mark: | | 20.3 | | :x: |
| 0.8.3 | | :x: | | 19.12 | until 2021-12 | :white_check_mark: |
| 0.7.0 | | :x: | | 19.9 | | :x: |
| 0.6.0 | | :x: | | 19.6 | | :x: |
| 0.5.4 | | :x: | | 19.3 | | :x: |
| 0.4.1 | | :x: | | 18.12 | until 2020-12 | :white_check_mark: |
| 0.3.1 | | :x: | | 0.8.3 | | :x: |
| 0.2.0 | | :x: | | 0.7.0 | | :x: |
| 0.1.9 | | :x: | | 0.6.0 | | :x: |
| 0.5.4 | | :x: |
| 0.4.1 | | :x: |
| 0.3.1 | | :x: |
| 0.2.0 | | :x: |
| 0.1.9 | | :x: |
:white_check_mark: = security/bug fixes
:heavy_check_mark: = full support
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@@ -0,0 +1 @@
Remove [version] section.

View File

@@ -0,0 +1,3 @@
Adds WEBSOCKET_PING_TIMEOUT and WEBSOCKET_PING_INTERVAL configuration values
Allows setting the ping_interval and ping_timeout arguments when initializing `WebSocketCommonProtocol`.

1
changelogs/1970.misc.rst Normal file
View File

@@ -0,0 +1 @@
Adds py.typed file to expose type information to other packages.

View File

@@ -12,9 +12,9 @@ Sanic holds the configuration in the `config` attribute of the application objec
app = Sanic('myapp') app = Sanic('myapp')
app.config.DB_NAME = 'appdb' app.config.DB_NAME = 'appdb'
app.config.DB_USER = 'appuser' app.config['DB_USER'] = 'appuser'
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once: Since the config object has a type that inherits from dictionary, you can use its ``update`` method in order to set several values at once:
.. code-block:: python .. code-block:: python
@@ -45,11 +45,92 @@ Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable
.. code-block:: python .. code-block:: python
app = Sanic(__name__, load_env=False) app = Sanic(__name__, load_env=False)
From file, dict, or any object (having __dict__ attribute).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can store app configurations in: (1) a Python file, (2) a dictionary, or (3) in some other type of custom object.
In order to load configuration from ove of those, you can use ``app.upload_config()``.
**1) From file**
Let's say you have ``my_config.py`` file that looks like this:
.. code-block:: python
# my_config.py
A = 1
B = 2
Loading config from this file is as easy as:
.. code-block:: python
app.update_config("/path/to/my_config.py")
You can also use environment variables in the path name here.
Let's say you have an environment variable like this:
.. code-block:: shell
$ export my_path="/path/to"
Then you can use it like this:
.. code-block:: python
app.update_config("${my_path}/my_config.py")
.. note::
Just remember that you have to provide environment variables in the format ${environment_variable} and that $environment_variable is not expanded (is treated as "plain" text).
**2) From dict**
You can also set your app config by providing a ``dict``:
.. code-block:: python
d = {"A": 1, "B": 2}
app.update_config(d)
**3) From _any_ object**
App config can be taken from an object. Internally, it uses ``__dict__`` to retrieve keys and values.
For example, pass the class:
.. code-block:: python
class C:
A = 1
B = 2
app.update_config(C)
or, it can be instantiated:
.. code-block:: python
c = C()
app.update_config(c)
- From an object (having __dict__ attribute)
From an Object From an Object
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
.. note::
Deprecated, will be removed in version 21.3.
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module: If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
.. code-block:: python .. code-block:: python
@@ -71,6 +152,10 @@ You could use a class or any other object as well.
From a File From a File
~~~~~~~~~~~ ~~~~~~~~~~~
.. note::
Deprecated, will be removed in version 21.3.
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file: Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
.. code-block:: python .. code-block:: python
@@ -98,7 +183,7 @@ The config files are regular Python files which are executed in order to load th
Builtin Configuration Values Builtin Configuration Values
---------------------------- ----------------------------
Out of the box there are just a few predefined values which can be overwritten when creating the application. Out of the box there are just a few predefined values which can be overwritten when creating the application. Note that websocket configuration values will have no impact if running in ASGI mode.
+---------------------------+-------------------+-----------------------------------------------------------------------------+ +---------------------------+-------------------+-----------------------------------------------------------------------------+
| Variable | Default | Description | | Variable | Default | Description |
@@ -123,6 +208,10 @@ Out of the box there are just a few predefined values which can be overwritten w
+---------------------------+-------------------+-----------------------------------------------------------------------------+ +---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_WRITE_LIMIT | 2^16 | High-water limit of the buffer for outgoing bytes | | WEBSOCKET_WRITE_LIMIT | 2^16 | High-water limit of the buffer for outgoing bytes |
+---------------------------+-------------------+-----------------------------------------------------------------------------+ +---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_PING_INTERVAL | 20 | A Ping frame is sent every ping_interval seconds. |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_PING_TIMEOUT | 20 | Connection is closed when Pong is not received after ping_timeout seconds |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) | | GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+ +---------------------------+-------------------+-----------------------------------------------------------------------------+
| ACCESS_LOG | True | Disable or enable access log | | ACCESS_LOG | True | Disable or enable access log |

View File

@@ -60,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)

View File

@@ -133,7 +133,7 @@ which allows the handler function to work with any of the HTTP methods in the li
async def get_handler(request): async def get_handler(request):
return text('GET request - {}'.format(request.args)) return text('GET request - {}'.format(request.args))
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is 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

View File

@@ -88,5 +88,5 @@ When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `
app = Sanic(__name__) app = Sanic(__name__)
chunk_size = 1024 * 1024 * 8 # Set chunk size to 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)

View File

@@ -58,6 +58,32 @@ More information about
the available arguments to `httpx` can be found the available arguments to `httpx` can be found
[in the documentation for `httpx <https://www.encode.io/httpx/>`_. [in the documentation for `httpx <https://www.encode.io/httpx/>`_.
.. code-block:: python
@pytest.mark.asyncio
async def test_index_returns_200():
request, response = await app.asgi_client.put('/')
assert response.status == 200
.. note::
Whenever one of the test clients run, you can test your app instance to determine if it is in testing mode:
`app.test_mode`.
Additionally, Sanic has an asynchronous testing client. The difference is that the async client will not stand up an
instance of your application, but will instead reach inside it using ASGI. All listeners and middleware are still
executed.
.. code-block:: python
@pytest.mark.asyncio
async def test_index_returns_200():
request, response = await app.asgi_client.put('/')
assert response.status == 200
.. note::
Whenever one of the test clients run, you can test your app instance to determine if it is in testing mode:
`app.test_mode`.
Using a random port Using a random port
------------------- -------------------

View File

@@ -51,5 +51,9 @@ You could setup your own WebSocket configuration through ``app.config``, like
app.config.WEBSOCKET_MAX_QUEUE = 32 app.config.WEBSOCKET_MAX_QUEUE = 32
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16 app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
app.config.WEBSOCKET_WRITE_LIMIT = 2 ** 16 app.config.WEBSOCKET_WRITE_LIMIT = 2 ** 16
app.config.WEBSOCKET_PING_INTERVAL = 20
app.config.WEBSOCKET_PING_TIMEOUT = 20
These settings will have no impact if running in ASGI mode.
Find more in ``Configuration`` section. Find more in ``Configuration`` section.

View File

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

View File

@@ -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,9 +85,12 @@ 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)
module_parts = args.module.split(".") if ":" in args.module:
module_name = ".".join(module_parts[:-1]) module_name, app_name = args.module.rsplit(":", 1)
app_name = module_parts[-1] else:
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
module = import_module(module_name) module = import_module(module_name)
app = getattr(module, app_name, None) app = getattr(module, app_name, None)
@@ -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:

View File

@@ -1 +1 @@
__version__ = "20.6.1" __version__ = "20.12.5"

View File

@@ -2,17 +2,17 @@ import logging
import logging.config import logging.config
import os import os
import re import re
import warnings
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from collections import defaultdict, deque from collections import defaultdict, deque
from functools import partial from functools import partial
from inspect import 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 +38,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,19 +51,15 @@ 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:
@@ -68,7 +67,7 @@ class Sanic:
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)
@@ -91,6 +90,12 @@ class Sanic:
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
if register is not None:
self.config.REGISTER = register
if self.config.REGISTER:
self.__class__.register_app(self)
@property @property
def loop(self): def loop(self):
"""Synonymous with asyncio.get_event_loop(). """Synonymous with asyncio.get_event_loop().
@@ -490,9 +495,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,
@@ -675,9 +678,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,
@@ -712,28 +716,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.
@@ -764,6 +746,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"
@@ -898,7 +898,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
@@ -920,16 +922,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)
@@ -1030,7 +1024,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,
@@ -1060,9 +1053,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)
@@ -1090,13 +1080,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
@@ -1153,7 +1136,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,
@@ -1186,9 +1168,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
@@ -1208,13 +1187,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
@@ -1296,7 +1268,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,
@@ -1311,13 +1282,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. "
@@ -1451,3 +1415,42 @@ class Sanic:
self.asgi = True self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send) asgi_app = await ASGIApp.create(self, scope, receive, send)
await asgi_app() await asgi_app()
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
# -------------------------------------------------------------------- #
# Configuration
# -------------------------------------------------------------------- #
def update_config(self, config: Union[bytes, str, dict, Any]):
"""Update app.config.
Please refer to config.py::Config.update_config for documentation."""
self.config.update_config(config)
# -------------------------------------------------------------------- #
# Class methods
# -------------------------------------------------------------------- #
@classmethod
def register_app(cls, app: "Sanic") -> None:
"""Register a Sanic instance"""
if not isinstance(app, cls):
raise SanicException("Registered app must be an instance of Sanic")
name = app.name
if name in cls._app_registry and not cls.test_mode:
raise SanicException(f'Sanic app name "{name}" already in use.')
cls._app_registry[name] = app
@classmethod
def get_app(cls, name: str, *, force_create: bool = False) -> "Sanic":
"""Retrieve an instantiated Sanic instance"""
try:
return cls._app_registry[name]
except KeyError:
if force_create:
return cls(name)
raise SanicException(f'Sanic app name "{name}" not found.')

View File

@@ -98,7 +98,9 @@ class MockTransport:
def create_websocket_connection( def create_websocket_connection(
self, send: ASGISend, receive: ASGIReceive self, send: ASGISend, receive: ASGIReceive
) -> WebSocketConnection: ) -> WebSocketConnection:
self._websocket_connection = WebSocketConnection(send, receive) self._websocket_connection = WebSocketConnection(
send, receive, self.scope.get("subprotocols", [])
)
return self._websocket_connection return self._websocket_connection
def add_task(self) -> None: def add_task(self) -> None:
@@ -310,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(
@@ -348,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:

View File

@@ -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:

View File

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

View File

@@ -1,8 +1,15 @@
import os from os import environ
import types from typing import Any, Union
from sanic.exceptions import PyFileError # NOTE(tomaszdrozdz): remove in version: 21.3
from sanic.helpers import import_string # We replace from_envvar(), from_object(), from_pyfile() config object methods
# with one simpler update_config() method.
# We also replace "loading module from file code" in from_pyfile()
# in a favour of load_module_from_file_location().
# Please see pull request: 1903
# and issue: 1895
from .deprecated import from_envvar, from_object, from_pyfile # noqa
from .utils import load_module_from_file_location, str_to_bool
SANIC_PREFIX = "SANIC_" SANIC_PREFIX = "SANIC_"
@@ -24,12 +31,16 @@ DEFAULT_CONFIG = {
"WEBSOCKET_MAX_QUEUE": 32, "WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16, "WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16, "WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"WEBSOCKET_PING_TIMEOUT": 20,
"WEBSOCKET_PING_INTERVAL": 20,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True, "ACCESS_LOG": True,
"FORWARDED_SECRET": None, "FORWARDED_SECRET": None,
"REAL_IP_HEADER": None, "REAL_IP_HEADER": None,
"PROXIES_COUNT": None, "PROXIES_COUNT": None,
"FORWARDED_FOR_HEADER": "X-Forwarded-For", "FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FALLBACK_ERROR_FORMAT": "html",
"REGISTER": True,
} }
@@ -56,76 +67,23 @@ class Config(dict):
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
self[attr] = value self[attr] = value
def from_envvar(self, variable_name): # NOTE(tomaszdrozdz): remove in version: 21.3
"""Load a configuration from an environment variable pointing to # We replace from_envvar(), from_object(), from_pyfile() config object
a configuration file. # methods with one simpler update_config() method.
# We also replace "loading module from file code" in from_pyfile()
:param variable_name: name of the environment variable # in a favour of load_module_from_file_location().
:return: bool. ``True`` if able to load config, ``False`` otherwise. # Please see pull request: 1903
""" # and issue: 1895
config_file = os.environ.get(variable_name) from_envvar = from_envvar
if not config_file: from_pyfile = from_pyfile
raise RuntimeError( from_object = from_object
"The environment variable %r is not set and "
"thus configuration could not be loaded." % variable_name
)
return self.from_pyfile(config_file)
def from_pyfile(self, filename):
"""Update the values in the config from a Python file.
Only the uppercase variables in that module are stored in the config.
:param filename: an absolute path to the config file
"""
module = types.ModuleType("config")
module.__file__ = filename
try:
with open(filename) as config_file:
exec( # nosec
compile(config_file.read(), filename, "exec"),
module.__dict__,
)
except IOError as e:
e.strerror = "Unable to load configuration file (%s)" % e.strerror
raise
except Exception as e:
raise PyFileError(filename) from e
self.from_object(module)
return True
def from_object(self, obj):
"""Update the values from the given object.
Objects are usually either modules or classes.
Just the uppercase variables in that object are stored in the config.
Example usage::
from yourapplication import default_config
app.config.from_object(default_config)
or also:
app.config.from_object('myproject.config.MyConfigClass')
You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
package because the package might be installed system wide.
:param obj: an object holding the configuration
"""
if isinstance(obj, str):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
def load_environment_vars(self, prefix=SANIC_PREFIX): def load_environment_vars(self, prefix=SANIC_PREFIX):
""" """
Looks for prefixed environment variables and applies Looks for prefixed environment variables and applies
them to the configuration if present. them to the configuration if present.
""" """
for k, v in os.environ.items(): for k, v in environ.items():
if k.startswith(prefix): if k.startswith(prefix):
_, config_key = k.split(prefix, 1) _, config_key = k.split(prefix, 1)
try: try:
@@ -135,23 +93,47 @@ class Config(dict):
self[config_key] = float(v) self[config_key] = float(v)
except ValueError: except ValueError:
try: try:
self[config_key] = strtobool(v) self[config_key] = str_to_bool(v)
except ValueError: except ValueError:
self[config_key] = v self[config_key] = v
def update_config(self, config: Union[bytes, str, dict, Any]):
"""Update app.config.
def strtobool(val): Note:: only upper case settings are considered.
"""
This function was borrowed from distutils.utils. While distutils
is part of stdlib, it feels odd to use distutils in main application code.
The function was modified to walk its talk and actually return bool You can upload app config by providing path to py file
and not int. holding settings.
"""
val = val.lower() # /some/py/file
if val in ("y", "yes", "t", "true", "on", "1"): A = 1
return True B = 2
elif val in ("n", "no", "f", "false", "off", "0"):
return False config.update_config("${some}/py/file")
else:
raise ValueError("invalid truth value %r" % (val,)) Yes you can put environment variable here, but they must be provided
in format: ${some_env_var}, and mark that $some_env_var is treated
as plain string.
You can upload app config by providing dict holding settings.
d = {"A": 1, "B": 2}
config.update_config(d)
You can upload app config by providing any object holding settings,
but in such case config.__dict__ will be used as dict holding settings.
class C:
A = 1
B = 2
config.update_config(C)"""
if isinstance(config, (bytes, str)):
config = load_module_from_file_location(location=config)
if not isinstance(config, dict):
config = config.__dict__
config = dict(filter(lambda i: i[0].isupper(), config.items()))
self.update(config)

106
sanic/deprecated.py Normal file
View File

@@ -0,0 +1,106 @@
# NOTE(tomaszdrozdz): remove in version: 21.3
# We replace from_envvar(), from_object(), from_pyfile() config object methods
# with one simpler update_config() method.
# We also replace "loading module from file code" in from_pyfile()
# in a favour of load_module_from_file_location().
# Please see pull request: 1903
# and issue: 1895
import types
from os import environ
from typing import Any
from warnings import warn
from sanic.exceptions import PyFileError
from sanic.helpers import import_string
def from_envvar(self, variable_name: str) -> bool:
"""Load a configuration from an environment variable pointing to
a configuration file.
:param variable_name: name of the environment variable
:return: bool. ``True`` if able to load config, ``False`` otherwise.
"""
warn(
"Using `from_envvar` method is deprecated and will be removed in "
"v21.3, use `app.update_config` method instead.",
DeprecationWarning,
stacklevel=2,
)
config_file = environ.get(variable_name)
if not config_file:
raise RuntimeError(
f"The environment variable {variable_name} is not set and "
f"thus configuration could not be loaded."
)
return self.from_pyfile(config_file)
def from_pyfile(self, filename: str) -> bool:
"""Update the values in the config from a Python file.
Only the uppercase variables in that module are stored in the config.
:param filename: an absolute path to the config file
"""
warn(
"Using `from_pyfile` method is deprecated and will be removed in "
"v21.3, use `app.update_config` method instead.",
DeprecationWarning,
stacklevel=2,
)
module = types.ModuleType("config")
module.__file__ = filename
try:
with open(filename) as config_file:
exec( # nosec
compile(config_file.read(), filename, "exec"),
module.__dict__,
)
except IOError as e:
e.strerror = "Unable to load configuration file (e.strerror)"
raise
except Exception as e:
raise PyFileError(filename) from e
self.from_object(module)
return True
def from_object(self, obj: Any) -> None:
"""Update the values from the given object.
Objects are usually either modules or classes.
Just the uppercase variables in that object are stored in the config.
Example usage::
from yourapplication import default_config
app.config.from_object(default_config)
or also:
app.config.from_object('myproject.config.MyConfigClass')
You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
package because the package might be installed system wide.
:param obj: an object holding the configuration
"""
warn(
"Using `from_object` method is deprecated and will be removed in "
"v21.3, use `app.update_config` method instead.",
DeprecationWarning,
stacklevel=2,
)
if isinstance(obj, str):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

View File

@@ -1,13 +1,283 @@
import sys import sys
import typing as t
from functools import partial
from traceback import extract_tb from traceback import extract_tb
from sanic.exceptions import SanicException from sanic.exceptions import InvalidUsage, SanicException
from sanic.helpers import STATUS_CODES from sanic.helpers import STATUS_CODES
from sanic.response import html from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
# Here, There Be Dragons (custom HTML formatting to follow) try:
from ujson import dumps
dumps = partial(dumps, escape_forward_slashes=False)
except ImportError: # noqa
from json import dumps # type: ignore
FALLBACK_TEXT = (
"The server encountered an internal error and "
"cannot complete your request."
)
FALLBACK_STATUS = 500
class BaseRenderer:
def __init__(self, request, exception, debug):
self.request = request
self.exception = exception
self.debug = debug
@property
def headers(self):
if isinstance(self.exception, SanicException):
return getattr(self.exception, "headers", {})
return {}
@property
def status(self):
if isinstance(self.exception, SanicException):
return getattr(self.exception, "status_code", FALLBACK_STATUS)
return FALLBACK_STATUS
@property
def text(self):
if self.debug or isinstance(self.exception, SanicException):
return str(self.exception)
return FALLBACK_TEXT
@property
def title(self):
status_text = STATUS_CODES.get(self.status, b"Error Occurred").decode()
return f"{self.status}{status_text}"
def render(self):
output = (
self.full
if self.debug and not getattr(self.exception, "quiet", False)
else self.minimal
)
return output()
def minimal(self): # noqa
raise NotImplementedError
def full(self): # noqa
raise NotImplementedError
class HTMLRenderer(BaseRenderer):
TRACEBACK_STYLE = """
html { font-family: sans-serif }
h2 { color: #888; }
.tb-wrapper p { margin: 0 }
.frame-border { margin: 1rem }
.frame-line > * { padding: 0.3rem 0.6rem }
.frame-line { margin-bottom: 0.3rem }
.frame-code { font-size: 16px; padding-left: 4ch }
.tb-wrapper { border: 1px solid #eee }
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
.frame-descriptor { background: #e2eafb; font-size: 14px }
"""
TRACEBACK_WRAPPER_HTML = (
"<div class=tb-header>{exc_name}: {exc_value}</div>"
"<div class=tb-wrapper>{frame_html}</div>"
)
TRACEBACK_BORDER = (
"<div class=frame-border>"
"The above exception was the direct cause of the following exception:"
"</div>"
)
TRACEBACK_LINE_HTML = (
"<div class=frame-line>"
"<p class=frame-descriptor>"
"File {0.filename}, line <i>{0.lineno}</i>, "
"in <code><b>{0.name}</b></code>"
"<p class=frame-code><code>{0.line}</code>"
"</div>"
)
OUTPUT_HTML = (
"<!DOCTYPE html><html lang=en>"
"<meta charset=UTF-8><title>{title}</title>\n"
"<style>{style}</style>\n"
"<h1>{title}</h1><p>{text}\n"
"{body}"
)
def full(self):
return html(
self.OUTPUT_HTML.format(
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(),
),
status=self.status,
)
def minimal(self):
return html(
self.OUTPUT_HTML.format(
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body="",
),
status=self.status,
headers=self.headers,
)
@property
def text(self):
return escape(super().text)
@property
def title(self):
return escape(f"⚠️ {super().title}")
def _generate_body(self):
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines = [
f"<h2>Traceback of {appname} (most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
"</div>",
]
return "\n".join(lines)
def _format_exc(self, exc):
frames = extract_tb(exc.__traceback__)
frame_html = "".join(
self.TRACEBACK_LINE_HTML.format(frame) for frame in frames
)
return self.TRACEBACK_WRAPPER_HTML.format(
exc_name=escape(exc.__class__.__name__),
exc_value=escape(exc),
frame_html=frame_html,
)
class TextRenderer(BaseRenderer):
OUTPUT_TEXT = "{title}\n{bar}\n{text}\n\n{body}"
SPACER = " "
def full(self):
return text(
self.OUTPUT_TEXT.format(
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body=self._generate_body(),
),
status=self.status,
)
def minimal(self):
return text(
self.OUTPUT_TEXT.format(
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body="",
),
status=self.status,
headers=self.headers,
)
@property
def title(self):
return f"⚠️ {super().title}"
def _generate_body(self):
_, exc_value, __ = sys.exc_info()
exceptions = []
# traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
lines = [
f"{self.exception.__class__.__name__}: {self.exception} while "
f"handling path {self.request.path}",
f"Traceback of {self.request.app.name} (most recent call last):\n",
]
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
return "\n".join(lines + exceptions[::-1])
def _format_exc(self, exc):
frames = "\n\n".join(
[
f"{self.SPACER * 2}File {frame.filename}, "
f"line {frame.lineno}, in "
f"{frame.name}\n{self.SPACER * 2}{frame.line}"
for frame in extract_tb(exc.__traceback__)
]
)
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
class JSONRenderer(BaseRenderer):
def full(self):
output = self._generate_output(full=True)
return json(output, status=self.status, dumps=dumps)
def minimal(self):
output = self._generate_output(full=False)
return json(output, status=self.status, dumps=dumps)
def _generate_output(self, *, full):
output = {
"description": self.title,
"status": self.status,
"message": self.text,
}
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(
{
"type": exc_value.__class__.__name__,
"exception": str(exc_value),
"frames": [
{
"file": frame.filename,
"line": frame.lineno,
"name": frame.name,
"src": frame.line,
}
for frame in extract_tb(exc_value.__traceback__)
],
}
)
exc_value = exc_value.__cause__
output["path"] = self.request.path
output["args"] = self.request.args
output["exceptions"] = exceptions[::-1]
return output
@property
def title(self):
return STATUS_CODES.get(self.status, b"Error Occurred").decode()
def escape(text): def escape(text):
@@ -15,103 +285,46 @@ def escape(text):
return f"{text}".replace("&", "&amp;").replace("<", "&lt;") return f"{text}".replace("&", "&amp;").replace("<", "&lt;")
def exception_response(request, exception, debug): RENDERERS_BY_CONFIG = {
status = 500 "html": HTMLRenderer,
text = ( "json": JSONRenderer,
"The server encountered an internal error " "text": TextRenderer,
"and cannot complete your request." }
)
headers = {} RENDERERS_BY_CONTENT_TYPE = {
if isinstance(exception, SanicException): "multipart/form-data": HTMLRenderer,
text = f"{exception}" "application/json": JSONRenderer,
status = getattr(exception, "status_code", status) "text/plain": TextRenderer,
headers = getattr(exception, "headers", headers) }
elif debug:
text = f"{exception}"
status_text = STATUS_CODES.get(status, b"Error Occurred").decode()
title = escape(f"{status}{status_text}")
text = escape(text)
if debug and not getattr(exception, "quiet", False):
return html(
f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>"
f"<style>{TRACEBACK_STYLE}</style>\n"
f"<h1>⚠️ {title}</h1><p>{text}\n"
f"{_render_traceback_html(request, exception)}",
status=status,
)
# Keeping it minimal with trailing newline for pretty curl/console output
return html(
f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>"
"<style>html { font-family: sans-serif }</style>\n"
f"<h1>⚠️ {title}</h1><p>{text}\n",
status=status,
headers=headers,
)
def _render_exception(exception): def exception_response(
frames = extract_tb(exception.__traceback__) request: Request,
frame_html = "".join(TRACEBACK_LINE_HTML.format(frame) for frame in frames) exception: Exception,
return TRACEBACK_WRAPPER_HTML.format( debug: bool,
exc_name=escape(exception.__class__.__name__), renderer: t.Type[t.Optional[BaseRenderer]] = None,
exc_value=escape(exception), ) -> HTTPResponse:
frame_html=frame_html, """Render a response for the default FALLBACK exception handler"""
)
if not renderer:
renderer = HTMLRenderer
def _render_traceback_html(request, exception): if request:
exc_type, exc_value, tb = sys.exc_info() if request.app.config.FALLBACK_ERROR_FORMAT == "auto":
exceptions = [] try:
while exc_value: renderer = JSONRenderer if request.json else HTMLRenderer
exceptions.append(_render_exception(exc_value)) except InvalidUsage:
exc_value = exc_value.__cause__ renderer = HTMLRenderer
traceback_html = TRACEBACK_BORDER.join(reversed(exceptions)) content_type, *_ = request.headers.get(
appname = escape(request.app.name) "content-type", ""
name = escape(exception.__class__.__name__) ).split(";")
value = escape(exception) renderer = RENDERERS_BY_CONTENT_TYPE.get(
path = escape(request.path) content_type, renderer
return ( )
f"<h2>Traceback of {appname} (most recent call last):</h2>" else:
f"{traceback_html}" render_format = request.app.config.FALLBACK_ERROR_FORMAT
"<div class=summary><p>" renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)
f"<b>{name}: {value}</b> while handling path <code>{path}</code>"
)
renderer = t.cast(t.Type[BaseRenderer], renderer)
TRACEBACK_STYLE = """ return renderer(request, exception, debug).render()
html { font-family: sans-serif }
h2 { color: #888; }
.tb-wrapper p { margin: 0 }
.frame-border { margin: 1rem }
.frame-line > * { padding: 0.3rem 0.6rem }
.frame-line { margin-bottom: 0.3rem }
.frame-code { font-size: 16px; padding-left: 4ch }
.tb-wrapper { border: 1px solid #eee }
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
.frame-descriptor { background: #e2eafb; font-size: 14px }
"""
TRACEBACK_WRAPPER_HTML = (
"<div class=tb-header>{exc_name}: {exc_value}</div>"
"<div class=tb-wrapper>{frame_html}</div>"
)
TRACEBACK_BORDER = (
"<div class=frame-border>"
"The above exception was the direct cause of the following exception:"
"</div>"
)
TRACEBACK_LINE_HTML = (
"<div class=frame-line>"
"<p class=frame-descriptor>"
"File {0.filename}, line <i>{0.lineno}</i>, "
"in <code><b>{0.name}</b></code>"
"<p class=frame-code><code>{0.line}</code>"
"</div>"
)

View File

@@ -169,14 +169,18 @@ class Unauthorized(SanicException):
} }
class LoadFileException(SanicException):
pass
def abort(status_code, message=None): def abort(status_code, message=None):
""" """
Raise an exception based on SanicException. Returns the HTTP response Raise an exception based on SanicException. Returns the HTTP response
message appropriate for the given status code, unless provided. message appropriate for the given status code, unless provided.
:param status_code: The HTTP status code to return. :param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages :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
View File

View File

@@ -50,7 +50,7 @@ class StreamBuffer:
self._queue = asyncio.Queue(buffer_size) self._queue = asyncio.Queue(buffer_size)
async def read(self): async def read(self):
""" Stop reading when gets None """ """Stop reading when gets None"""
payload = await self._queue.get() payload = await self._queue.get()
self._queue.task_done() self._queue.task_done()
return payload return payload
@@ -136,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)

View File

@@ -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
@@ -14,15 +12,20 @@ from sanic.helpers import has_message_body, remove_entity_headers
try: try:
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
except ImportError: except ImportError:
from json import dumps
# This is done in order to ensure that the JSON response is # This is done in order to ensure that the JSON response is
# kept consistent across both ujson and inbuilt json usage. # kept consistent across both ujson and inbuilt json usage.
from json import dumps
json_dumps = partial(dumps, separators=(",", ":")) json_dumps = partial(dumps, separators=(",", ":"))
class BaseHTTPResponse: class BaseHTTPResponse:
def __init__(self):
self.asgi = False
def _encode_body(self, data): def _encode_body(self, data):
if data is None:
return b""
return data.encode() if hasattr(data, "encode") else data return data.encode() if hasattr(data, "encode") else data
def _parse_headers(self): def _parse_headers(self):
@@ -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,13 +116,14 @@ class StreamingHTTPResponse(BaseHTTPResponse):
""" """
if version != "1.1": if version != "1.1":
self.chunked = False self.chunked = False
headers = self.get_headers( if not getattr(self, "asgi", False):
version, headers = self.get_headers(
keep_alive=keep_alive, version,
keep_alive_timeout=keep_alive_timeout, keep_alive=keep_alive,
) keep_alive_timeout=keep_alive_timeout,
await self.protocol.push_data(headers) )
await self.protocol.drain() await self.protocol.push_data(headers)
await self.protocol.drain()
await self.streaming_fn(self) await self.streaming_fn(self)
if self.chunked: if self.chunked:
await self.protocol.push_data(b"0\r\n\r\n") await self.protocol.push_data(b"0\r\n\r\n")
@@ -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
) )
@@ -249,7 +242,10 @@ def raw(
:param content_type: the content type (string) of the response. :param content_type: the content type (string) of the response.
""" """
return HTTPResponse( return HTTPResponse(
body=body, status=status, headers=headers, content_type=content_type, body=body,
status=status,
headers=headers,
content_type=content_type,
) )

View File

@@ -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,10 +464,11 @@ class Router:
route_handler = route.handler route_handler = route.handler
if hasattr(route_handler, "handlers"): if hasattr(route_handler, "handlers"):
route_handler = route_handler.handlers[method] route_handler = route_handler.handlers[method]
return route_handler, [], kwargs, route.uri, route.name
return route_handler, [], kwargs, route.uri, route.name, route.endpoint
def is_stream_handler(self, request): def is_stream_handler(self, request):
""" Handler for request is stream or not. """Handler for request is stream or not.
:param request: Request object :param request: Request object
:return: bool :return: bool
""" """

View File

@@ -14,11 +14,13 @@ from ipaddress import ip_address
from signal import SIG_IGN, SIGINT, SIGTERM, Signals from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func from signal import signal as signal_func
from time import time from time import time
from typing import Dict, Type, Union
from httptools import HttpRequestParser # type: ignore from httptools import HttpRequestParser # type: ignore
from httptools.parser.errors import HttpParserError # type: ignore from httptools.parser.errors import HttpParserError # type: ignore
from sanic.compat import Header, ctrlc_workaround_for_windows from sanic.compat import Header, ctrlc_workaround_for_windows
from sanic.config import Config
from sanic.exceptions import ( from sanic.exceptions import (
HeaderExpectationFailed, HeaderExpectationFailed,
InvalidUsage, InvalidUsage,
@@ -416,12 +418,13 @@ class HttpProtocol(asyncio.Protocol):
async def stream_append(self): async def stream_append(self):
while self._body_chunks: while self._body_chunks:
body = self._body_chunks.popleft() body = self._body_chunks.popleft()
if self.request.stream.is_full(): if self.request:
self.transport.pause_reading() if self.request.stream.is_full():
await self.request.stream.put(body) self.transport.pause_reading()
self.transport.resume_reading() await self.request.stream.put(body)
else: self.transport.resume_reading()
await self.request.stream.put(body) else:
await self.request.stream.put(body)
def on_message_complete(self): def on_message_complete(self):
# Entire request (headers and whole body) is received. # Entire request (headers and whole body) is received.
@@ -844,6 +847,7 @@ def serve(
app.asgi = False app.asgi = False
connections = connections if connections is not None else set() connections = connections if connections is not None else set()
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
server = partial( server = partial(
protocol, protocol,
loop=loop, loop=loop,
@@ -852,6 +856,7 @@ def serve(
app=app, app=app,
state=state, state=state,
unix=unix, unix=unix,
**protocol_kwargs,
) )
asyncio_server_kwargs = ( asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {} asyncio_server_kwargs if asyncio_server_kwargs else {}
@@ -948,6 +953,21 @@ def serve(
remove_unix_socket(unix) remove_unix_socket(unix)
def _build_protocol_kwargs(
protocol: Type[HttpProtocol], config: Config
) -> Dict[str, Union[int, float]]:
if hasattr(protocol, "websocket_handshake"):
return {
"websocket_max_size": config.WEBSOCKET_MAX_SIZE,
"websocket_max_queue": config.WEBSOCKET_MAX_QUEUE,
"websocket_read_limit": config.WEBSOCKET_READ_LIMIT,
"websocket_write_limit": config.WEBSOCKET_WRITE_LIMIT,
"websocket_ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
"websocket_ping_interval": config.WEBSOCKET_PING_INTERVAL,
}
return {}
def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket: def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
"""Create TCP server socket. """Create TCP server socket.
:param host: IPv4, IPv6 or hostname may be specified :param host: IPv4, IPv6 or hostname may be specified
@@ -1063,7 +1083,7 @@ def serve_multiple(server_settings, workers):
signal_func(SIGINT, lambda s, f: sig_handler(s, f)) signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
mp = multiprocessing.get_context("spawn") mp = multiprocessing.get_context("fork")
for _ in range(workers): for _ in range(workers):
process = mp.Process(target=serve, kwargs=server_settings) process = mp.Process(target=serve, kwargs=server_settings)

View File

@@ -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
@@ -40,6 +41,10 @@ async def _static_request_handler(
# match filenames which got encoded (filenames with spaces etc) # match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path)) file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))): if not file_path.startswith(path.abspath(unquote(root_path))):
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={file_uri}"
)
raise FileNotFound( raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri "File not found", path=file_or_directory, relative_url=file_uri
) )
@@ -94,6 +99,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 +143,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 +166,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

View File

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

99
sanic/utils.py Normal file
View File

@@ -0,0 +1,99 @@
from importlib.util import module_from_spec, spec_from_file_location
from os import environ as os_environ
from re import findall as re_findall
from typing import Union
from .exceptions import LoadFileException
def str_to_bool(val: str) -> bool:
"""Takes string and tries to turn it into bool as human would do.
If val is in case insensitive (
"y", "yes", "yep", "yup", "t",
"true", "on", "enable", "enabled", "1"
) returns True.
If val is in case insensitive (
"n", "no", "f", "false", "off", "disable", "disabled", "0"
) returns False.
Else Raise ValueError."""
val = val.lower()
if val in {
"y",
"yes",
"yep",
"yup",
"t",
"true",
"on",
"enable",
"enabled",
"1",
}:
return True
elif val in {"n", "no", "f", "false", "off", "disable", "disabled", "0"}:
return False
else:
raise ValueError(f"Invalid truth value {val}")
def load_module_from_file_location(
location: Union[bytes, str], encoding: str = "utf8", *args, **kwargs
):
"""Returns loaded module provided as a file path.
:param args:
Coresponds to importlib.util.spec_from_file_location location
parameters,but with this differences:
- It has to be of a string or bytes type.
- You can also use here environment variables
in format ${some_env_var}.
Mark that $some_env_var will not be resolved as environment variable.
:encoding:
If location parameter is of a bytes type, then use this encoding
to decode it into string.
:param args:
Coresponds to the rest of importlib.util.spec_from_file_location
parameters.
:param kwargs:
Coresponds to the rest of importlib.util.spec_from_file_location
parameters.
For example You can:
some_module = load_module_from_file_location(
"some_module_name",
"/some/path/${some_env_var}"
)
"""
# 1) Parse location.
if isinstance(location, bytes):
location = location.decode(encoding)
# A) Check if location contains any environment variables
# in format ${some_env_var}.
env_vars_in_location = set(re_findall(r"\${(.+?)}", location))
# B) Check these variables exists in environment.
not_defined_env_vars = env_vars_in_location.difference(os_environ.keys())
if not_defined_env_vars:
raise LoadFileException(
"The following environment variables are not set: "
f"{', '.join(not_defined_env_vars)}"
)
# C) Substitute them in location.
for env_var in env_vars_in_location:
location = location.replace("${" + env_var + "}", os_environ[env_var])
# 2) Load and return module.
name = location.split("/")[-1].split(".")[
0
] # get just the file name without path and .py extension
_mod_spec = spec_from_file_location(name, location, *args, **kwargs)
module = module_from_spec(_mod_spec)
_mod_spec.loader.exec_module(module) # type: ignore
return module

View File

@@ -90,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:

View File

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

View File

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

View File

@@ -11,11 +11,3 @@ line_length = 79
lines_after_imports = 2 lines_after_imports = 2
lines_between_types = 1 lines_between_types = 1
multi_line_output = 3 multi_line_output = 3
not_skip = __init__.py
[version]
current_version = 19.12.0
files = sanic/__version__.py
current_version_pattern = __version__ = "{current_version}"
new_version_pattern = __version__ = "{new_version}"

View File

@@ -5,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.11.1", "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 = [

View File

@@ -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:

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ import logging
import sys import sys
from inspect import isawaitable from inspect import isawaitable
from os import environ
from unittest.mock import patch
import pytest import pytest
@@ -116,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)
@@ -125,7 +127,7 @@ def test_app_handle_request_handler_is_none(app, monkeypatch):
def handler(request): def handler(request):
return text("test") return text("test")
request, response = app.test_client.get("/test") _, response = app.test_client.get("/test")
assert ( assert (
"'None' was returned while requesting a handler from the router" "'None' was returned while requesting a handler from the router"
@@ -148,6 +150,43 @@ def test_app_enable_websocket(app, websocket_enabled, enable):
assert app.websocket_enabled == True assert app.websocket_enabled == True
@patch("sanic.app.WebSocketProtocol")
def test_app_websocket_parameters(websocket_protocol_mock, app):
app.config.WEBSOCKET_MAX_SIZE = 44
app.config.WEBSOCKET_MAX_QUEUE = 45
app.config.WEBSOCKET_READ_LIMIT = 46
app.config.WEBSOCKET_WRITE_LIMIT = 47
app.config.WEBSOCKET_PING_TIMEOUT = 48
app.config.WEBSOCKET_PING_INTERVAL = 50
@app.websocket("/ws")
async def handler(request, ws):
await ws.send("test")
try:
# This will fail because WebSocketProtocol is mocked and only the call kwargs matter
app.test_client.get("/ws")
except:
pass
websocket_protocol_call_args = websocket_protocol_mock.call_args
ws_kwargs = websocket_protocol_call_args[1]
assert ws_kwargs["websocket_max_size"] == app.config.WEBSOCKET_MAX_SIZE
assert ws_kwargs["websocket_max_queue"] == app.config.WEBSOCKET_MAX_QUEUE
assert ws_kwargs["websocket_read_limit"] == app.config.WEBSOCKET_READ_LIMIT
assert (
ws_kwargs["websocket_write_limit"] == app.config.WEBSOCKET_WRITE_LIMIT
)
assert (
ws_kwargs["websocket_ping_timeout"]
== app.config.WEBSOCKET_PING_TIMEOUT
)
assert (
ws_kwargs["websocket_ping_interval"]
== app.config.WEBSOCKET_PING_INTERVAL
)
def test_handle_request_with_nested_exception(app, monkeypatch): def test_handle_request_with_nested_exception(app, monkeypatch):
err_msg = "Mock Exception" err_msg = "Mock Exception"
@@ -220,5 +259,66 @@ 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()
def test_app_has_test_mode_sync():
app = Sanic("test")
@app.get("/")
def handler(request):
assert request.app.test_mode
return text("test")
_, response = app.test_client.get("/")
assert response.status == 200
def test_app_registry():
instance = Sanic("test")
assert Sanic._app_registry["test"] is instance
def test_app_registry_wrong_type():
with pytest.raises(SanicException):
Sanic.register_app(1)
def test_app_registry_name_reuse():
Sanic("test")
Sanic.test_mode = False
with pytest.raises(SanicException):
Sanic("test")
Sanic.test_mode = True
Sanic("test")
def test_app_registry_retrieval():
instance = Sanic("test")
assert Sanic.get_app("test") is instance
def test_get_app_does_not_exist():
with pytest.raises(SanicException):
Sanic.get_app("does-not-exist")
def test_get_app_does_not_exist_force_create():
assert isinstance(
Sanic.get_app("does-not-exist", force_create=True), Sanic
)
def test_app_no_registry():
Sanic("no-register", register=False)
with pytest.raises(SanicException):
Sanic.get_app("no-register")
def test_app_no_registry_env():
environ["SANIC_REGISTER"] = "False"
Sanic("no-register")
with pytest.raises(SanicException):
Sanic.get_app("no-register")
del environ["SANIC_REGISTER"]

View File

@@ -1,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
@@ -208,6 +189,53 @@ async def test_websocket_receive(send, receive, message_stack):
assert text == msg["text"] assert text == msg["text"]
@pytest.mark.asyncio
async def test_websocket_accept_with_no_subprotocols(
send, receive, message_stack
):
ws = WebSocketConnection(send, receive)
await ws.accept()
assert len(message_stack) == 1
message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == ""
assert "bytes" not in message
@pytest.mark.asyncio
async def test_websocket_accept_with_subprotocol(send, receive, message_stack):
subprotocols = ["graphql-ws"]
ws = WebSocketConnection(send, receive, subprotocols)
await ws.accept()
assert len(message_stack) == 1
message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == "graphql-ws"
assert "bytes" not in message
@pytest.mark.asyncio
async def test_websocket_accept_with_multiple_subprotocols(
send, receive, message_stack
):
subprotocols = ["graphql-ws", "hello", "world"]
ws = WebSocketConnection(send, receive, subprotocols)
await ws.accept()
assert len(message_stack) == 1
message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == "graphql-ws,hello,world"
assert "bytes" not in message
def test_improper_websocket_connection(transport, send, receive): def test_improper_websocket_connection(transport, send, receive):
with pytest.raises(InvalidUsage): with pytest.raises(InvalidUsage):
transport.get_websocket_connection() transport.get_websocket_connection()

View File

@@ -736,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

View File

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

86
tests/test_errorpages.py Normal file
View File

@@ -0,0 +1,86 @@
import pytest
from sanic import Sanic
from sanic.errorpages import exception_response
from sanic.exceptions import NotFound
from sanic.request import Request
from sanic.response import HTTPResponse
@pytest.fixture
def app():
app = Sanic("error_page_testing")
@app.route("/error", methods=["GET", "POST"])
def err(request):
raise Exception("something went wrong")
return app
@pytest.fixture
def fake_request(app):
return Request(b"/foobar", {}, "1.1", "GET", None, app)
@pytest.mark.parametrize(
"fallback,content_type, exception, status",
(
(None, "text/html; charset=utf-8", Exception, 500),
("html", "text/html; charset=utf-8", Exception, 500),
("auto", "text/html; charset=utf-8", Exception, 500),
("text", "text/plain; charset=utf-8", Exception, 500),
("json", "application/json", Exception, 500),
(None, "text/html; charset=utf-8", NotFound, 404),
("html", "text/html; charset=utf-8", NotFound, 404),
("auto", "text/html; charset=utf-8", NotFound, 404),
("text", "text/plain; charset=utf-8", NotFound, 404),
("json", "application/json", NotFound, 404),
),
)
def test_should_return_html_valid_setting(
fake_request, fallback, content_type, exception, status
):
if fallback:
fake_request.app.config.FALLBACK_ERROR_FORMAT = fallback
try:
raise exception("bad stuff")
except Exception as e:
response = exception_response(fake_request, e, True)
assert isinstance(response, HTTPResponse)
assert response.status == status
assert response.content_type == content_type
def test_auto_fallback_with_data(app):
app.config.FALLBACK_ERROR_FORMAT = "auto"
_, response = app.test_client.get("/error")
assert response.status == 500
assert response.content_type == "text/html; charset=utf-8"
_, response = app.test_client.post("/error", json={"foo": "bar"})
assert response.status == 500
assert response.content_type == "application/json"
_, response = app.test_client.post("/error", data={"foo": "bar"})
assert response.status == 500
assert response.content_type == "text/html; charset=utf-8"
def test_auto_fallback_with_content_type(app):
app.config.FALLBACK_ERROR_FORMAT = "auto"
_, response = app.test_client.get(
"/error", headers={"content-type": "application/json"}
)
assert response.status == 500
assert response.content_type == "application/json"
_, response = app.test_client.get(
"/error", headers={"content-type": "text/plain"}
)
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"

View File

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

View File

@@ -0,0 +1,38 @@
from pathlib import Path
from types import ModuleType
import pytest
from sanic.exceptions import LoadFileException
from sanic.utils import load_module_from_file_location
@pytest.fixture
def loaded_module_from_file_location():
return load_module_from_file_location(
str(Path(__file__).parent / "static" / "app_test_config.py")
)
@pytest.mark.dependency(name="test_load_module_from_file_location")
def test_load_module_from_file_location(loaded_module_from_file_location):
assert isinstance(loaded_module_from_file_location, ModuleType)
@pytest.mark.dependency(depends=["test_load_module_from_file_location"])
def test_loaded_module_from_file_location_name(
loaded_module_from_file_location,
):
name = loaded_module_from_file_location.__name__
if "C:\\" in name:
name = name.split("\\")[-1]
assert name == "app_test_config"
def test_load_module_from_file_location_with_non_existing_env_variable():
with pytest.raises(
LoadFileException,
match="The following environment variables are not set: MuuMilk",
):
load_module_from_file_location("${MuuMilk}")

View File

@@ -102,7 +102,7 @@ def test_logging_pass_customer_logconfig():
@pytest.mark.parametrize("debug", (True, False)) @pytest.mark.parametrize("debug", (True, False))
def test_log_connection_lost(app, debug, monkeypatch): def test_log_connection_lost(app, debug, monkeypatch):
""" Should not log Connection lost exception on non debug """ """Should not log Connection lost exception on non debug"""
stream = StringIO() stream = StringIO()
root = logging.getLogger("sanic.root") root = logging.getLogger("sanic.root")
root.addHandler(logging.StreamHandler(stream)) root.addHandler(logging.StreamHandler(stream))

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,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)
)

View File

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

View File

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

View File

@@ -348,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"

30
tox.ini
View File

@@ -1,25 +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
httpcore==0.3.0
httpx==0.11.1
chardet<=2.3.0
beautifulsoup4
gunicorn
pytest-benchmark pytest-benchmark
pytest-dependency
httpcore==0.11.*
httpx==0.15.4
multidict>=5.0,<6.0
beautifulsoup4
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
@@ -30,13 +31,13 @@ commands =
deps = deps =
flake8 flake8
black black
isort isort>=5.0.0
bandit bandit
commands = commands =
flake8 sanic flake8 sanic
black --config ./.black.toml --check --verbose sanic/ black --config ./.black.toml --check --verbose sanic/
isort --check-only --recursive sanic isort --check-only sanic
[testenv:type-checking] [testenv:type-checking]
deps = deps =
@@ -55,6 +56,9 @@ commands =
[pytest] [pytest]
filterwarnings = filterwarnings =
ignore:.*async with lock.* instead:DeprecationWarning ignore:.*async with lock.* instead:DeprecationWarning
addopts = --strict-markers
markers =
asyncio
[testenv:security] [testenv:security]
deps = deps =
@@ -72,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