Compare commits

...

105 Commits

Author SHA1 Message Date
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
59 changed files with 1886 additions and 634 deletions

View File

@@ -17,6 +17,12 @@ environment:
PYTHON_VERSION: "3.8.x"
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%"
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

View File

@@ -31,6 +31,16 @@ matrix:
dist: xenial
sudo: true
name: "Python 3.8 without Extensions"
- env: TOX_ENV=py39
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 with Extensions"
- env: TOX_ENV=py39-no-ext
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 without Extensions"
- env: TOX_ENV=type-checking
python: 3.6
name: "Python 3.6 Type checks"
@@ -40,6 +50,10 @@ matrix:
- env: TOX_ENV=type-checking
python: 3.8
name: "Python 3.8 Type checks"
- env: TOX_ENV=type-checking
python: 3.9
dist: bionic
name: "Python 3.9 Type checks"
- env: TOX_ENV=lint
python: 3.6
name: "Python 3.6 Linter checks"
@@ -61,23 +75,28 @@ matrix:
dist: xenial
sudo: true
name: "Python 3.8 Bandit security scan"
- env: TOX_ENV=security
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 Bandit security scan"
- env: TOX_ENV=docs
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Documentation tests"
- env: TOX_ENV=pyNightly
python: 'nightly'
python: "nightly"
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: 'nightly'
python: "nightly"
name: "Python nightly without Extensions"
allow_failures:
- env: TOX_ENV=pyNightly
python: 'nightly'
python: "nightly"
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: 'nightly'
python: "nightly"
name: "Python nightly without Extensions"
install:
- pip install -U tox

View File

@@ -1,9 +1,202 @@
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
@@ -14,7 +207,7 @@ Version 20.6.1
Features
********
*
`#1760 <https://github.com/huge-success/sanic/pull/1760>`_
Add version parameter to websocket routes
@@ -25,7 +218,7 @@ Features
*
`#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
********
@@ -45,7 +238,7 @@ Bugfixes
*
`#1848 <https://github.com/huge-success/sanic/pull/1848>`_
Reverse named_response_middlware execution order, to match normal response middleware execution order
*
`#1853 <https://github.com/huge-success/sanic/pull/1853>`_
Fix pickle error when attempting to pickle an application which contains websocket routes
@@ -97,28 +290,28 @@ Version 20.3.0
Features
********
*
*
`#1762 <https://github.com/huge-success/sanic/pull/1762>`_
Add ``srv.start_serving()`` and ``srv.serve_forever()`` to ``AsyncioServer``
*
*
`#1767 <https://github.com/huge-success/sanic/pull/1767>`_
Make Sanic usable on ``hypercorn -k trio myweb.app``
*
*
`#1768 <https://github.com/huge-success/sanic/pull/1768>`_
No tracebacks on normal errors and prettier error pages
*
*
`#1769 <https://github.com/huge-success/sanic/pull/1769>`_
Code cleanup in file responses
*
*
`#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
*
*
`#1798 <https://github.com/huge-success/sanic/pull/1798>`_
Allow multiple workers on MacOS with Python 3.8
@@ -129,19 +322,19 @@ Features
Bugfixes
********
*
*
`#1748 <https://github.com/huge-success/sanic/pull/1748>`_
Remove loop argument in ``asyncio.Event`` in Python 3.8
*
*
`#1764 <https://github.com/huge-success/sanic/pull/1764>`_
Allow route decorators to stack up again
*
*
`#1789 <https://github.com/huge-success/sanic/pull/1789>`_
Fix tests using hosts yielding incorrect ``url_for``
*
*
`#1808 <https://github.com/huge-success/sanic/pull/1808>`_
Fix Ctrl+C and tests on Windows
@@ -155,7 +348,7 @@ Deprecations and Removals
*
`#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.
*
`#1807 <https://github.com/huge-success/sanic/pull/1807>`_
Remove server config args that can be read directly from app
@@ -178,22 +371,22 @@ Dependencies
Developer infrastructure
************************
*
*
`#1833 <https://github.com/huge-success/sanic/pull/1833>`_
Resolve broken documentation builds
Improved Documentation
**********************
*
*
`#1755 <https://github.com/huge-success/sanic/pull/1755>`_
Usage of ``response.empty()``
*
*
`#1778 <https://github.com/huge-success/sanic/pull/1778>`_
Update README
*
*
`#1783 <https://github.com/huge-success/sanic/pull/1783>`_
Fix typo
@@ -220,7 +413,7 @@ Improved Documentation
*
`#1834 <https://github.com/huge-success/sanic/pull/1834>`_
Order of listeners
Version 19.12.0
===============
@@ -286,16 +479,16 @@ Version 19.6.2
Features
********
*
*
`#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>`_
*
*
`#1475 <https://github.com/huge-success/sanic/pull/1475>`_
Added ASGI support (Beta)
*
*
`#1436 <https://github.com/huge-success/sanic/pull/1436>`_
Add Configure support from object string
@@ -303,19 +496,19 @@ Features
Bugfixes
********
*
*
`#1587 <https://github.com/huge-success/sanic/pull/1587>`_
Add missing handle for Expect header.
*
*
`#1560 <https://github.com/huge-success/sanic/pull/1560>`_
Allow to disable Transfer-Encoding: chunked.
*
*
`#1558 <https://github.com/huge-success/sanic/pull/1558>`_
Fix graceful shutdown.
*
*
`#1594 <https://github.com/huge-success/sanic/pull/1594>`_
Strict Slashes behavior fix
@@ -326,11 +519,11 @@ Deprecations and Removals
`#1544 <https://github.com/huge-success/sanic/pull/1544>`_
Drop dependency on distutil
*
*
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_
Drop support for Python 3.5
*
*
`#1568 <https://github.com/huge-success/sanic/pull/1568>`_
Deprecate route removal.
@@ -347,39 +540,39 @@ Version 19.3
Features
********
*
*
`#1497 <https://github.com/huge-success/sanic/pull/1497>`_
Add support for zero-length and RFC 5987 encoded filename for
multipart/form-data requests.
*
*
`#1484 <https://github.com/huge-success/sanic/pull/1484>`_
The type of ``expires`` attribute of ``sanic.cookies.Cookie`` is now
enforced to be of type ``datetime``.
*
*
`#1482 <https://github.com/huge-success/sanic/pull/1482>`_
Add support for the ``stream`` parameter of ``sanic.Sanic.add_route()``
available to ``sanic.Blueprint.add_route()``.
*
*
`#1481 <https://github.com/huge-success/sanic/pull/1481>`_
Accept negative values for route parameters with type ``int`` or ``number``.
*
*
`#1476 <https://github.com/huge-success/sanic/pull/1476>`_
Deprecated the use of ``sanic.request.Request.raw_args`` - it has a
fundamental flaw in which is drops repeated query string parameters.
Added ``sanic.request.Request.query_args`` as a replacement for the
original use-case.
*
*
`#1472 <https://github.com/huge-success/sanic/pull/1472>`_
Remove an unwanted ``None`` check in Request class ``repr`` implementation.
This changes the default ``repr`` of a Request from ``<Request>`` to
``<Request: None />``
*
*
`#1470 <https://github.com/huge-success/sanic/pull/1470>`_
Added 2 new parameters to ``sanic.app.Sanic.create_server``\ :
@@ -390,21 +583,21 @@ Features
This is a breaking change.
*
*
`#1499 <https://github.com/huge-success/sanic/pull/1499>`_
Added a set of test cases that test and benchmark route resolution.
*
*
`#1457 <https://github.com/huge-success/sanic/pull/1457>`_
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``.
*
*
`#1445 <https://github.com/huge-success/sanic/pull/1445>`_
Added the ``endpoint`` attribute to an incoming ``request``\ , containing the
name of the handler function.
*
*
`#1423 <https://github.com/huge-success/sanic/pull/1423>`_
Improved request streaming. ``request.stream`` is now a bounded-size buffer
instead of an unbounded queue. Callers must now call
@@ -417,7 +610,7 @@ Bugfixes
********
*
*
`#1502 <https://github.com/huge-success/sanic/pull/1502>`_
Sanic was prefetching ``time.time()`` and updating it once per second to
avoid excessive ``time.time()`` calls. The implementation was observed to
@@ -425,25 +618,25 @@ Bugfixes
to negligible, so this has been removed. Fixes
`#1500 <https://github.com/huge-success/sanic/pull/1500>`_
*
*
`#1501 <https://github.com/huge-success/sanic/pull/1501>`_
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
in ``init0/mod1.py`` with ``debug`` enabled and imports another module in
``init0``.
*
*
`#1376 <https://github.com/huge-success/sanic/pull/1376>`_
Allow sanic test client to bind to a random port by specifying
``port=None`` when constructing a ``SanicTestClient``
*
*
`#1399 <https://github.com/huge-success/sanic/pull/1399>`_
Added the ability to specify middleware on a blueprint group, so that all
routes produced from the blueprints in the group have the middleware
applied.
*
*
`#1442 <https://github.com/huge-success/sanic/pull/1442>`_
Allow the the use the ``SANIC_ACCESS_LOG`` environment variable to
enable/disable the access log when not explicitly passed to ``app.run()``.
@@ -485,7 +678,7 @@ Version 18.12
18.12.0
*******
*
*
Changes:
@@ -503,7 +696,7 @@ Version 18.12
* Deprecate Handler.log
* Pinned httptools requirement to version 0.0.10+
*
*
Fixes:

View File

@@ -71,7 +71,7 @@ black:
black --config ./.black.toml sanic tests
fix-import: black
isort -rc sanic tests
isort sanic tests
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
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/huge-success/sanic
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
:target: https://travis-ci.org/huge-success/sanic
.. |Build Status| image:: https://travis-ci.com/huge-success/sanic.svg?branch=master
:target: https://travis-ci.com/huge-success/sanic
.. |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
.. |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 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/>`_.
The project is maintained by the community, for the community. **Contributions are welcome!**
@@ -104,7 +106,7 @@ Hello World Example
if __name__ == '__main__':
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::

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.
| Version | LTS | Supported |
| ------- | ------------------ | ------------------ |
| 19.6.0 | | :white_check_mark: |
| 19.3.1 | | :heavy_check_mark: |
| 18.12.0 | :heavy_check_mark: | :heavy_check_mark: |
| 0.8.3 | | :x: |
| 0.7.0 | | :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: |
| Version | LTS | Supported |
| ------- | ------------- | ------------------ |
| 20.9 | | :heavy_check_mark: |
| 20.6 | | :x: |
| 20.3 | | :x: |
| 19.12 | until 2021-12 | :white_check_mark: |
| 19.9 | | :x: |
| 19.6 | | :x: |
| 19.3 | | :x: |
| 18.12 | until 2020-12 | :white_check_mark: |
| 0.8.3 | | :x: |
| 0.7.0 | | :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

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.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
@@ -45,11 +45,92 @@ Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable
.. 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
~~~~~~~~~~~~~~
.. 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:
.. code-block:: python
@@ -71,6 +152,10 @@ You could use a class or any other object as well.
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:
.. 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
----------------------------
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 |
@@ -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_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) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| 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!*.
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):
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

View File

@@ -88,5 +88,5 @@ When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `
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)

View File

@@ -58,6 +58,32 @@ More information about
the available arguments to `httpx` can be found
[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
-------------------

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_READ_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.

View File

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

View File

@@ -1,28 +1,83 @@
import os
import sys
from argparse import ArgumentParser
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from importlib import import_module
from typing import Any, Dict, Optional
from sanic import __version__
from sanic.app import Sanic
from sanic.config import BASE_LOGO
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():
parser = ArgumentParser(prog="sanic")
parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
parser.add_argument("--port", dest="port", type=int, default=8000)
parser.add_argument("--unix", dest="unix", type=str, default="")
parser = SanicArgumentParser(
prog="sanic",
description=BASE_LOGO,
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(
"--cert", dest="cert", type=str, help="location of certificate for SSL"
)
parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL."
)
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("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()
try:
@@ -30,9 +85,12 @@ def main():
if module_path not in sys.path:
sys.path.append(module_path)
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
if ":" in args.module:
module_name, app_name = args.module.rsplit(":", 1)
else:
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
module = import_module(module_name)
app = getattr(module, app_name, None)
@@ -57,6 +115,7 @@ def main():
unix=args.unix,
workers=args.workers,
debug=args.debug,
access_log=args.access_log,
ssl=ssl,
)
except ImportError as e:

View File

@@ -1 +1 @@
__version__ = "20.6.2"
__version__ = "20.12.2"

View File

@@ -2,12 +2,11 @@ import logging
import logging.config
import os
import re
import warnings
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from collections import defaultdict, deque
from functools import partial
from inspect import getmodulename, isawaitable, signature, stack
from inspect import isawaitable, signature
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc
@@ -38,6 +37,9 @@ from sanic.websocket import ConnectionClosed, WebSocketProtocol
class Sanic:
_app_registry: Dict[str, "Sanic"] = {}
test_mode = False
def __init__(
self,
name=None,
@@ -48,19 +50,15 @@ class Sanic:
strict_slashes=False,
log_config=None,
configure_logging=True,
register=None,
):
# Get name from previous stack frame
if name is None:
warnings.warn(
"Sanic(name=None) is deprecated and None value support "
"for `name` will be removed in the next release. "
raise SanicException(
"Sanic instance cannot be unnamed. "
"Please use Sanic(name='your_application_name') instead.",
DeprecationWarning,
stacklevel=2,
)
frame_records = stack()[1]
name = getmodulename(frame_records[1])
# logging
if configure_logging:
@@ -68,7 +66,7 @@ class Sanic:
self.name = name
self.asgi = False
self.router = router or Router()
self.router = router or Router(self)
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env)
@@ -91,6 +89,12 @@ class Sanic:
# Register alternative method names
self.go_fast = self.run
if register is not None:
self.config.REGISTER = register
if self.config.REGISTER:
self.__class__.register_app(self)
@property
def loop(self):
"""Synonymous with asyncio.get_event_loop().
@@ -675,9 +679,10 @@ class Sanic:
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
: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,
uri,
file_or_directory,
@@ -712,28 +717,6 @@ class Sanic:
self._blueprint_order.append(blueprint)
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):
r"""Build a URL based on a view name and the values provided.
@@ -898,7 +881,9 @@ class Sanic:
name = None
try:
# 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
@@ -920,16 +905,8 @@ class Sanic:
"handler from the router"
)
)
else:
if not getattr(handler, "__blueprintname__", False):
request.endpoint = self._build_endpoint_name(
handler.__name__
)
else:
request.endpoint = self._build_endpoint_name(
getattr(handler, "__blueprintname__", ""),
handler.__name__,
)
request.endpoint = endpoint
# Run response handler
response = handler(request, *args, **kwargs)
@@ -1030,7 +1007,6 @@ class Sanic:
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
backlog: int = 100,
stop_event: Any = None,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
@@ -1060,9 +1036,6 @@ class Sanic:
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
: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
:type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server)
@@ -1090,13 +1063,6 @@ class Sanic:
protocol = (
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 not None:
self.config.ACCESS_LOG = access_log
@@ -1153,7 +1119,6 @@ class Sanic:
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
stop_event: Any = None,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
return_asyncio_server=False,
@@ -1186,9 +1151,6 @@ class Sanic:
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
: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)
:type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need
@@ -1208,13 +1170,6 @@ class Sanic:
protocol = (
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 not None:
self.config.ACCESS_LOG = access_log
@@ -1296,7 +1251,6 @@ class Sanic:
loop=None,
protocol=HttpProtocol,
backlog=100,
stop_event=None,
register_sys_signals=True,
run_async=False,
auto_reload=False,
@@ -1311,13 +1265,6 @@ class Sanic:
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
context.load_cert_chain(cert, keyfile=key)
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:
raise ValueError(
"PROXIES_COUNT cannot be negative. "
@@ -1451,3 +1398,42 @@ class Sanic:
self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send)
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(
self, send: ASGISend, receive: ASGIReceive
) -> WebSocketConnection:
self._websocket_connection = WebSocketConnection(send, receive)
self._websocket_connection = WebSocketConnection(
send, receive, self.scope.get("subprotocols", [])
)
return self._websocket_connection
def add_task(self) -> None:
@@ -310,13 +312,19 @@ class ASGIApp:
callback = None if self.ws else self.stream_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.
"""
headers: List[Tuple[bytes, bytes]] = []
cookies: Dict[str, str] = {}
content_length: List[str] = []
try:
content_length = response.headers.popall("content-length", [])
cookies = {
v.key: v
for _, v in list(
@@ -348,11 +356,23 @@ class ASGIApp:
if name not in (b"Set-Cookie",)
]
if "content-length" not in response.headers and not isinstance(
response, StreamingHTTPResponse
):
response.asgi = True
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 += [
(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:

View File

@@ -143,7 +143,18 @@ class Blueprint:
if _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]
# Middleware
for future in self.middlewares:
if future.args or future.kwargs:
@@ -160,14 +171,6 @@ class Blueprint:
for future in self.exceptions:
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
for event, listeners in self.listeners.items():
for listener in listeners:

View File

@@ -14,7 +14,8 @@ class Header(CIMultiDict):
use_trio = argv[0].endswith("hypercorn") and "trio" in argv
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):
return Path(path).stat()

View File

@@ -1,8 +1,15 @@
import os
import types
from os import environ
from typing import Any, Union
from sanic.exceptions import PyFileError
from sanic.helpers import import_string
# 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
from .deprecated import from_envvar, from_object, from_pyfile # noqa
from .utils import load_module_from_file_location, str_to_bool
SANIC_PREFIX = "SANIC_"
@@ -24,12 +31,16 @@ DEFAULT_CONFIG = {
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"WEBSOCKET_PING_TIMEOUT": 20,
"WEBSOCKET_PING_INTERVAL": 20,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True,
"FORWARDED_SECRET": None,
"REAL_IP_HEADER": None,
"PROXIES_COUNT": None,
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FALLBACK_ERROR_FORMAT": "html",
"REGISTER": True,
}
@@ -56,76 +67,23 @@ class Config(dict):
def __setattr__(self, attr, value):
self[attr] = value
def from_envvar(self, variable_name):
"""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.
"""
config_file = os.environ.get(variable_name)
if not config_file:
raise RuntimeError(
"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)
# 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
from_envvar = from_envvar
from_pyfile = from_pyfile
from_object = from_object
def load_environment_vars(self, prefix=SANIC_PREFIX):
"""
Looks for prefixed environment variables and applies
them to the configuration if present.
"""
for k, v in os.environ.items():
for k, v in environ.items():
if k.startswith(prefix):
_, config_key = k.split(prefix, 1)
try:
@@ -135,23 +93,47 @@ class Config(dict):
self[config_key] = float(v)
except ValueError:
try:
self[config_key] = strtobool(v)
self[config_key] = str_to_bool(v)
except ValueError:
self[config_key] = v
def update_config(self, config: Union[bytes, str, dict, Any]):
"""Update app.config.
def strtobool(val):
"""
This function was borrowed from distutils.utils. While distutils
is part of stdlib, it feels odd to use distutils in main application code.
Note:: only upper case settings are considered.
The function was modified to walk its talk and actually return bool
and not int.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return True
elif val in ("n", "no", "f", "false", "off", "0"):
return False
else:
raise ValueError("invalid truth value %r" % (val,))
You can upload app config by providing path to py file
holding settings.
# /some/py/file
A = 1
B = 2
config.update_config("${some}/py/file")
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 typing as t
from functools import partial
from traceback import extract_tb
from sanic.exceptions import SanicException
from sanic.exceptions import InvalidUsage, SanicException
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):
@@ -15,103 +285,46 @@ def escape(text):
return f"{text}".replace("&", "&amp;").replace("<", "&lt;")
def exception_response(request, exception, debug):
status = 500
text = (
"The server encountered an internal error "
"and cannot complete your request."
)
RENDERERS_BY_CONFIG = {
"html": HTMLRenderer,
"json": JSONRenderer,
"text": TextRenderer,
}
headers = {}
if isinstance(exception, SanicException):
text = f"{exception}"
status = getattr(exception, "status_code", status)
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,
)
RENDERERS_BY_CONTENT_TYPE = {
"multipart/form-data": HTMLRenderer,
"application/json": JSONRenderer,
"text/plain": TextRenderer,
}
def _render_exception(exception):
frames = extract_tb(exception.__traceback__)
frame_html = "".join(TRACEBACK_LINE_HTML.format(frame) for frame in frames)
return TRACEBACK_WRAPPER_HTML.format(
exc_name=escape(exception.__class__.__name__),
exc_value=escape(exception),
frame_html=frame_html,
)
def exception_response(
request: Request,
exception: Exception,
debug: bool,
renderer: t.Type[t.Optional[BaseRenderer]] = None,
) -> HTTPResponse:
"""Render a response for the default FALLBACK exception handler"""
if not renderer:
renderer = HTMLRenderer
def _render_traceback_html(request, exception):
exc_type, exc_value, tb = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(_render_exception(exc_value))
exc_value = exc_value.__cause__
if request:
if request.app.config.FALLBACK_ERROR_FORMAT == "auto":
try:
renderer = JSONRenderer if request.json else HTMLRenderer
except InvalidUsage:
renderer = HTMLRenderer
traceback_html = TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(request.app.name)
name = escape(exception.__class__.__name__)
value = escape(exception)
path = escape(request.path)
return (
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>"
)
content_type, *_ = request.headers.get(
"content-type", ""
).split(";")
renderer = RENDERERS_BY_CONTENT_TYPE.get(
content_type, renderer
)
else:
render_format = request.app.config.FALLBACK_ERROR_FORMAT
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)
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>"
)
renderer = t.cast(t.Type[BaseRenderer], renderer)
return renderer(request, exception, debug).render()

View File

@@ -169,14 +169,18 @@ class Unauthorized(SanicException):
}
class LoadFileException(SanicException):
pass
def abort(status_code, message=None):
"""
Raise an exception based on SanicException. Returns the HTTP response
message appropriate for the given status code, unless provided.
:param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages
in response.py for the given status code.
:param message: The HTTP response body. Defaults to the messages in
STATUS_CODES from sanic.helpers for the given status code.
"""
if message is None:
message = STATUS_CODES.get(status_code)

0
sanic/py.typed Normal file
View File

View File

@@ -136,15 +136,18 @@ class Request:
return f"<{class_name}: {self.method} {self.path}>"
def body_init(self):
""".. deprecated:: 20.3"""
""".. deprecated:: 20.3
To be removed in 21.3"""
self.body = []
def body_push(self, data):
""".. deprecated:: 20.3"""
""".. deprecated:: 20.3
To be removed in 21.3"""
self.body.append(data)
def body_finish(self):
""".. deprecated:: 20.3"""
""".. deprecated:: 20.3
To be removed in 21.3"""
self.body = b"".join(self.body)
async def receive_body(self):

View File

@@ -1,5 +1,3 @@
import warnings
from functools import partial
from mimetypes import guess_type
from os import path
@@ -14,15 +12,20 @@ from sanic.helpers import has_message_body, remove_entity_headers
try:
from ujson import dumps as json_dumps
except ImportError:
from json import dumps
# This is done in order to ensure that the JSON response is
# kept consistent across both ujson and inbuilt json usage.
from json import dumps
json_dumps = partial(dumps, separators=(",", ":"))
class BaseHTTPResponse:
def __init__(self):
self.asgi = False
def _encode_body(self, data):
if data is None:
return b""
return data.encode() if hasattr(data, "encode") else data
def _parse_headers(self):
@@ -42,7 +45,7 @@ class BaseHTTPResponse:
body=b"",
):
""".. 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
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",
chunked=True,
):
super().__init__()
self.content_type = content_type
self.streaming_fn = streaming_fn
self.status = status
@@ -95,6 +100,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
"""
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:
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
else:
@@ -109,13 +116,14 @@ class StreamingHTTPResponse(BaseHTTPResponse):
"""
if version != "1.1":
self.chunked = False
headers = self.get_headers(
version,
keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout,
)
await self.protocol.push_data(headers)
await self.protocol.drain()
if not getattr(self, "asgi", False):
headers = self.get_headers(
version,
keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout,
)
await self.protocol.push_data(headers)
await self.protocol.drain()
await self.streaming_fn(self)
if self.chunked:
await self.protocol.push_data(b"0\r\n\r\n")
@@ -141,20 +149,15 @@ class HTTPResponse(BaseHTTPResponse):
status=200,
headers=None,
content_type=None,
body_bytes=b"",
):
super().__init__()
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.headers = Header(headers or {})
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):
body = b""
if has_message_body(self.status):
@@ -218,20 +221,10 @@ def text(
:param content_type: the content type (string) of the response
"""
if not isinstance(body, str):
warnings.warn(
"Types other than str will be deprecated in future versions for"
f" response.text, got type {type(body).__name__})",
DeprecationWarning,
raise TypeError(
f"Bad body type. Expected str, got {type(body).__name__})"
)
# 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(
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.
"""
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", ["handler", "methods", "pattern", "parameters", "name", "uri"]
"Route",
[
"handler",
"methods",
"pattern",
"parameters",
"name",
"uri",
"endpoint",
],
)
Parameter = namedtuple("Parameter", ["name", "cast"])
@@ -79,7 +88,8 @@ class Router:
routes_always_check = None
parameter_pattern = re.compile(r"<(.+?)>")
def __init__(self):
def __init__(self, app):
self.app = app
self.routes_all = {}
self.routes_names = {}
self.routes_static_files = {}
@@ -299,11 +309,15 @@ class Router:
handler_name = f"{bp_name}.{name or handler.__name__}"
else:
handler_name = name or getattr(handler, "__name__", None)
handler_name = name or getattr(
handler, "__name__", handler.__class__.__name__
)
if route:
route = merge_route(route, methods, handler)
else:
endpoint = self.app._build_endpoint_name(handler_name)
route = Route(
handler=handler,
methods=methods,
@@ -311,6 +325,7 @@ class Router:
parameters=parameters,
name=handler_name,
uri=uri,
endpoint=endpoint,
)
self.routes_all[uri] = route
@@ -449,10 +464,11 @@ class Router:
route_handler = route.handler
if hasattr(route_handler, "handlers"):
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):
""" Handler for request is stream or not.
"""Handler for request is stream or not.
:param request: Request object
:return: bool
"""

View File

@@ -14,11 +14,13 @@ from ipaddress import ip_address
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from time import time
from typing import Dict, Type, Union
from httptools import HttpRequestParser # type: ignore
from httptools.parser.errors import HttpParserError # type: ignore
from sanic.compat import Header, ctrlc_workaround_for_windows
from sanic.config import Config
from sanic.exceptions import (
HeaderExpectationFailed,
InvalidUsage,
@@ -416,12 +418,13 @@ class HttpProtocol(asyncio.Protocol):
async def stream_append(self):
while self._body_chunks:
body = self._body_chunks.popleft()
if self.request.stream.is_full():
self.transport.pause_reading()
await self.request.stream.put(body)
self.transport.resume_reading()
else:
await self.request.stream.put(body)
if self.request:
if self.request.stream.is_full():
self.transport.pause_reading()
await self.request.stream.put(body)
self.transport.resume_reading()
else:
await self.request.stream.put(body)
def on_message_complete(self):
# Entire request (headers and whole body) is received.
@@ -844,6 +847,7 @@ def serve(
app.asgi = False
connections = connections if connections is not None else set()
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
server = partial(
protocol,
loop=loop,
@@ -852,6 +856,7 @@ def serve(
app=app,
state=state,
unix=unix,
**protocol_kwargs,
)
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
@@ -948,6 +953,21 @@ def serve(
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:
"""Create TCP server socket.
:param host: IPv4, IPv6 or hostname may be specified

View File

@@ -13,6 +13,7 @@ from sanic.exceptions import (
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler
from sanic.log import error_logger
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)
file_path = path.abspath(unquote(file_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(
"File not found", path=file_or_directory, relative_url=file_uri
)
@@ -94,6 +99,10 @@ async def _static_request_handler(
except ContentRangeError:
raise
except Exception:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={file_uri}"
)
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
@@ -134,6 +143,8 @@ def register(
threshold size to switch to file_stream()
:param name: user defined name used for url_for
: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,
# serve from the folder
@@ -155,10 +166,11 @@ def register(
)
)
app.route(
_routes, _ = app.route(
uri,
methods=["GET", "HEAD"],
name=name,
host=host,
strict_slashes=strict_slashes,
)(_handler)
return _routes

View File

@@ -11,6 +11,8 @@ from sanic.response import text
ASGI_HOST = "mockserver"
ASGI_PORT = 1234
ASGI_BASE_URL = f"http://{ASGI_HOST}:{ASGI_PORT}"
HOST = "127.0.0.1"
PORT = None
@@ -22,6 +24,14 @@ class SanicTestClient:
self.port = port
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):
return httpx.AsyncClient(verify=False)
@@ -95,7 +105,9 @@ class SanicTestClient:
if self.port:
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
else:
@@ -185,30 +197,33 @@ async def app_call_with_return(self, scope, receive, send):
return await asgi_app()
class SanicASGIDispatch(httpx.ASGIDispatch):
pass
class SanicASGITestClient(httpx.AsyncClient):
def __init__(
self,
app,
base_url: str = f"http://{ASGI_HOST}",
base_url: str = ASGI_BASE_URL,
suppress_exceptions: bool = False,
) -> None:
app.__class__.__call__ = app_call_with_return
app.asgi = True
self.app = app
dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT or 0))
super().__init__(dispatch=dispatch, base_url=base_url)
transport = httpx.ASGITransport(app=app, client=(ASGI_HOST, ASGI_PORT))
super().__init__(transport=transport, base_url=base_url)
self.last_request = None
def _collect_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)
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):
self.handlers = {}
self.name = self.__class__.__name__
def add(self, methods, handler, stream=False):
if stream:

View File

@@ -3,6 +3,7 @@ from typing import (
Awaitable,
Callable,
Dict,
List,
MutableMapping,
Optional,
Union,
@@ -34,6 +35,8 @@ class WebSocketProtocol(HttpProtocol):
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16,
websocket_ping_interval=20,
websocket_ping_timeout=20,
**kwargs
):
super().__init__(*args, **kwargs)
@@ -44,6 +47,8 @@ class WebSocketProtocol(HttpProtocol):
self.websocket_max_queue = websocket_max_queue
self.websocket_read_limit = websocket_read_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
def request_timeout_callback(self):
@@ -118,6 +123,8 @@ class WebSocketProtocol(HttpProtocol):
max_queue=self.websocket_max_queue,
read_limit=self.websocket_read_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
self.websocket.is_client = False
@@ -137,9 +144,11 @@ class WebSocketConnection:
self,
send: Callable[[ASIMessage], Awaitable[None]],
receive: Callable[[], Awaitable[ASIMessage]],
subprotocols: Optional[List[str]] = None,
) -> None:
self._send = send
self._receive = receive
self.subprotocols = subprotocols or []
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
@@ -164,7 +173,14 @@ class WebSocketConnection:
receive = recv
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:
pass

View File

@@ -5,7 +5,7 @@ import signal
import sys
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.websocket import WebSocketProtocol
@@ -174,7 +174,7 @@ class GunicornWorker(base.Worker):
@staticmethod
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.
"""
ctx = ssl.SSLContext(cfg.ssl_version)

View File

@@ -11,11 +11,3 @@ line_length = 79
lines_after_imports = 2
lines_between_types = 1
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 re
import sys
from distutils.util import strtobool
from setuptools import setup
@@ -24,6 +25,7 @@ class PyTest(TestCommand):
def run_tests(self):
import shlex
import pytest
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:
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:
raise RuntimeError("Unable to determine version.")
@@ -53,10 +57,12 @@ setup_kwargs = {
"author": "Sanic Community",
"author_email": "admhpkns@gmail.com",
"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,
"packages": ["sanic"],
"package_data": {"sanic": ["py.typed"]},
"platforms": "any",
"python_requires": ">=3.6",
"classifiers": [
@@ -66,36 +72,40 @@ setup_kwargs = {
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
"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
uvloop = "uvloop>=0.5.3" + env_dependency
uvloop = "uvloop>=0.5.3,<0.15.0" + env_dependency
requirements = [
"httptools>=0.0.10",
uvloop,
ujson,
"aiofiles>=0.3.0",
"aiofiles>=0.6.0",
"websockets>=8.1,<9.0",
"multidict>=4.0,<5.0",
"httpx==0.11.1",
"multidict>=5.0,<6.0",
"httpx==0.15.4",
]
tests_require = [
"pytest==5.2.1",
"multidict>=4.0,<5.0",
"gunicorn",
"multidict>=5.0,<6.0",
"gunicorn==20.0.4",
"pytest-cov",
"httpcore==0.3.0",
"httpcore==0.11.*",
"beautifulsoup4",
uvloop,
ujson,
"pytest-sanic",
"pytest-sugar",
"pytest-benchmark",
"pytest-dependency",
]
docs_require = [

View File

@@ -11,6 +11,7 @@ from sanic.router import RouteExists, Router
random.seed("Pack my box with five dozen liquor jugs.")
Sanic.test_mode = True
if sys.platform in ["win32", "cygwin"]:
collect_ignore = ["test_worker.py"]
@@ -95,10 +96,10 @@ class RouteStringGenerator:
@pytest.fixture(scope="function")
def sanic_router():
def sanic_router(app):
# noinspection PyProtectedMember
def _setup(route_details: tuple) -> (Router, tuple):
router = Router()
router = Router(app)
added_router = []
for method, route in route_details:
try:

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ import logging
import sys
from inspect import isawaitable
from os import environ
from unittest.mock import patch
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 mockreturn(*args, **kwargs):
return None, [], {}, "", ""
return None, [], {}, "", "", None
# Not sure how to make app.router.get() return None, so use mock here.
monkeypatch.setattr(app.router, "get", mockreturn)
@@ -125,7 +127,7 @@ def test_app_handle_request_handler_is_none(app, monkeypatch):
def handler(request):
return text("test")
request, response = app.test_client.get("/test")
_, response = app.test_client.get("/test")
assert (
"'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
@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):
err_msg = "Mock Exception"
@@ -220,5 +259,66 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
def test_app_name_required():
with pytest.deprecated_call():
with pytest.raises(SanicException):
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

@@ -208,6 +208,53 @@ async def test_websocket_receive(send, receive, message_stack):
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):
with pytest.raises(InvalidUsage):
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
@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):
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):
app.strict_slashes = True

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 json import JSONDecodeError
import httpcore
import httpx
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}
old_conn = None
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
def verify(self):
return self.ssl.verify
class ReusableSanicConnectionPool(httpcore.AsyncConnectionPool):
last_reused_connection = None
@property
def trust_env(self):
return self.ssl.trust_env
@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
async def _get_connection_from_pool(self, *args, **kwargs):
conn = await super()._get_connection_from_pool(*args, **kwargs)
self.__class__.last_reused_connection = conn
return conn
class ResusableSanicSession(httpx.AsyncClient):
def __init__(self, *args, **kwargs) -> None:
dispatch = ReusableSanicConnectionPool()
super().__init__(dispatch=dispatch, *args, **kwargs)
transport = ReusableSanicConnectionPool()
super().__init__(transport=transport, *args, **kwargs)
class ReuseableSanicTestClient(SanicTestClient):
@@ -244,8 +204,8 @@ async def handler3(request):
def test_keep_alive_timeout_reuse():
"""If the server keep-alive timeout and client keep-alive timeout are
both longer than the delay, the client _and_ server will successfully
reuse the existing connection."""
both longer than the delay, the client _and_ server will successfully
reuse the existing connection."""
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
@@ -258,6 +218,7 @@ def test_keep_alive_timeout_reuse():
request, response = client.get("/1")
assert response.status == 200
assert response.text == "OK"
assert ReusableSanicConnectionPool.last_reused_connection
finally:
client.kill_server()
@@ -270,20 +231,15 @@ def test_keep_alive_client_timeout():
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
headers = {"Connection": "keep-alive"}
try:
request, response = client.get(
"/1", headers=headers, request_keepalive=1
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(2))
exception = None
request, response = client.get("/1", request_keepalive=1)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert "got a new connection" in exception.args[0]
request, response = client.get(
"/1", headers=headers, request_keepalive=1
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(2))
exception = None
request, response = client.get("/1", request_keepalive=1)
assert ReusableSanicConnectionPool.last_reused_connection is None
finally:
client.kill_server()
@@ -298,22 +254,14 @@ def test_keep_alive_server_timeout():
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
headers = {"Connection": "keep-alive"}
try:
request, response = client.get(
"/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]
request, response = client.get(
"/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)
assert ReusableSanicConnectionPool.last_reused_connection is None
finally:
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

@@ -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("/")
assert response.json == {
"user": "sanic",
@@ -41,6 +58,8 @@ def test_custom_context(app):
"has_session": True,
"has_missing": False,
"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
from sanic.blueprints import Blueprint
from sanic.exceptions import HeaderExpectationFailed
from sanic.request import StreamBuffer
from sanic.response import json, stream, text
from sanic.server import HttpProtocol
from sanic.views import CompositionView, HTTPMethodView
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
@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):
"""for self.is_request_stream = True"""
bp = Blueprint("test_blueprint_request_stream_blueprint")

View File

@@ -1,64 +1,54 @@
import asyncio
from typing import cast
import httpcore
import httpx
from httpcore._async.base import (
AsyncByteStream,
AsyncHTTPTransport,
ConnectionState,
NewConnectionRequired,
)
from httpcore._async.connection import AsyncHTTPConnection
from httpcore._async.connection_pool import ResponseByteStream
from httpcore._exceptions import LocalProtocolError, UnsupportedProtocol
from httpcore._types import TimeoutDict
from httpcore._utils import url_to_origin
from sanic import Sanic
from sanic.response import text
from sanic.testing import SanicTestClient
class DelayableHTTPConnection(httpx.dispatch.connection.HTTPConnection):
def __init__(self, *args, **kwargs):
self._request_delay = None
if "request_delay" in 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)
class DelayableHTTPConnection(httpcore._async.connection.AsyncHTTPConnection):
async def arequest(self, *args, **kwargs):
await asyncio.sleep(2)
return await super().arequest(*args, **kwargs)
async def _open_socket(self, *args, **kwargs):
retval = await super()._open_socket(*args, **kwargs)
if self._request_delay:
await asyncio.sleep(self._request_delay)
response = await self.connection.send(request, timeout=timeout)
return response
return retval
class DelayableSanicConnectionPool(
httpx.dispatch.connection_pool.ConnectionPool
):
class DelayableSanicConnectionPool(httpcore.AsyncConnectionPool):
def __init__(self, request_delay=None, *args, **kwargs):
self._request_delay = request_delay
super().__init__(*args, **kwargs)
async def acquire_connection(self, origin, timeout=None):
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)
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
async def _add_to_pool(self, connection, timeout):
connection.__class__ = DelayableHTTPConnection
connection._request_delay = self._request_delay
await super()._add_to_pool(connection, timeout)
class DelayableSanicSession(httpx.AsyncClient):
def __init__(self, request_delay=None, *args, **kwargs) -> None:
dispatch = DelayableSanicConnectionPool(request_delay=request_delay)
super().__init__(dispatch=dispatch, *args, **kwargs)
transport = DelayableSanicConnectionPool(request_delay=request_delay)
super().__init__(transport=transport, *args, **kwargs)
class DelayableSanicTestClient(SanicTestClient):

View File

@@ -12,7 +12,14 @@ from sanic import Blueprint, Sanic
from sanic.exceptions import ServerError
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters
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("/")
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):
@@ -573,7 +583,7 @@ async def test_standard_forwarded_asgi(app):
assert response.json() == {"for": "127.0.0.2", "proto": "ws"}
assert request.remote_addr == "127.0.0.2"
assert request.scheme == "ws"
assert request.server_port == 80
assert request.server_port == ASGI_PORT
app.config.FORWARDED_SECRET = "mySecret"
request, response = await app.asgi_client.get("/", headers=headers)
@@ -1044,9 +1054,9 @@ def test_url_attributes_no_ssl(app, path, query, expected_url):
@pytest.mark.parametrize(
"path,query,expected_url",
[
("/foo", "", "http://{}/foo"),
("/bar/baz", "", "http://{}/bar/baz"),
("/moo/boo", "arg1=val1", "http://{}/moo/boo?arg1=val1"),
("/foo", "", "{}/foo"),
("/bar/baz", "", "{}/bar/baz"),
("/moo/boo", "arg1=val1", "{}/moo/boo?arg1=val1"),
],
)
@pytest.mark.asyncio
@@ -1057,7 +1067,7 @@ async def test_url_attributes_no_ssl_asgi(app, path, query, expected_url):
app.add_route(handler, path)
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)
@@ -1955,10 +1965,7 @@ def test_server_name_and_url_for(app):
app.config.SERVER_NAME = "my-server" # This means default port
assert app.url_for("handler", _external=True) == "http://my-server/foo"
request, response = app.test_client.get("/foo")
assert (
request.url_for("handler")
== f"http://my-server/foo"
)
assert request.url_for("handler") == f"http://my-server/foo"
app.config.SERVER_NAME = "https://my-server/path"
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)
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):
@@ -235,6 +236,12 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
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):
request, response = non_chunked_streaming_app.test_client.get("/")
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"
@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(
non_chunked_streaming_app,
):
@@ -608,17 +625,3 @@ def test_empty_response(app):
request, response = app.test_client.get("/test")
assert response.content_type is None
assert response.body == b""
def test_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 sys
import httpcore
import httpx
import pytest
@@ -139,8 +140,9 @@ def test_unix_connection():
@app.listener("after_server_start")
async def client(app, loop):
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
try:
async with httpx.AsyncClient(uds=SOCKPATH) as client:
async with httpx.AsyncClient(transport=transport) as client:
r = await client.get("http://myhost.invalid/")
assert r.status_code == 200
assert r.text == os.path.abspath(SOCKPATH)
@@ -179,8 +181,9 @@ async def test_zero_downtime():
from time import monotonic as current_time
async def client():
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
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")
assert r.status_code == 200
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

28
tox.ini
View File

@@ -1,23 +1,24 @@
[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]
usedevelop = True
setenv =
{py36,py37,py38,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_UJSON=1
{py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
deps =
coverage
coverage==5.3
pytest==5.2.1
pytest-cov
pytest-sanic
pytest-sugar
httpcore==0.3.0
httpx==0.11.1
chardet<=2.3.0
beautifulsoup4
gunicorn
pytest-benchmark
pytest-dependency
httpcore==0.11.*
httpx==0.15.4
multidict>=5.0,<6.0
beautifulsoup4
gunicorn==20.0.4
uvicorn
websockets>=8.1,<9.0
commands =
@@ -30,13 +31,13 @@ commands =
deps =
flake8
black
isort
isort>=5.0.0
bandit
commands =
flake8 sanic
black --config ./.black.toml --check --verbose sanic/
isort --check-only --recursive sanic
isort --check-only sanic
[testenv:type-checking]
deps =
@@ -55,6 +56,9 @@ commands =
[pytest]
filterwarnings =
ignore:.*async with lock.* instead:DeprecationWarning
addopts = --strict-markers
markers =
asyncio
[testenv:security]
deps =
@@ -72,7 +76,7 @@ deps =
recommonmark>=0.5.0
docutils
pygments
gunicorn
gunicorn==20.0.4
commands =
make docs-test