Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97635111af | ||
|
|
7f3fe40cd4 | ||
|
|
ea34bcd849 | ||
|
|
05f758583b | ||
|
|
760c74a293 | ||
|
|
9def46beb8 | ||
|
|
04be8e95a5 | ||
|
|
78ced20fc7 | ||
|
|
c3003413d3 | ||
|
|
fe3fdc5d83 | ||
|
|
b66fb6f9e8 | ||
|
|
bf6175fb20 | ||
|
|
7475897a03 | ||
|
|
58ca887be4 | ||
|
|
449bc417a3 | ||
|
|
262f89f2b6 | ||
|
|
38337446cf | ||
|
|
ac1331ea4c | ||
|
|
2b947e831f | ||
|
|
112715eb80 | ||
|
|
ea9cf365bc | ||
|
|
b9b3b4051a | ||
|
|
ecb6db29e6 | ||
|
|
6515dde64b | ||
|
|
01d2a2aa3c | ||
|
|
39e12accb8 | ||
|
|
39fe6ea5b1 | ||
|
|
fc4b7df088 | ||
|
|
35f28f7a64 | ||
|
|
614be40438 | ||
|
|
bde0428d0c | ||
|
|
63567c2ae4 | ||
|
|
ec10f337b6 | ||
|
|
d0f0e73e96 | ||
|
|
b4fe2c8a6b | ||
|
|
33da0771d1 | ||
|
|
75994cd915 | ||
|
|
c0839afdde | ||
|
|
5961da3f57 | ||
|
|
41f1809351 | ||
|
|
5fbdcb62e4 | ||
|
|
677b83e9f8 | ||
|
|
6a5c8becac | ||
|
|
fd23b99d60 | ||
|
|
634b586df3 | ||
|
|
4ca3e98082 | ||
|
|
d18a776964 | ||
|
|
d6b4d7d265 | ||
|
|
33ee4c21b3 | ||
|
|
a026cd7195 | ||
|
|
7b1bce8d90 | ||
|
|
217a7c5161 | ||
|
|
2949e3422d | ||
|
|
16ea99b0c0 | ||
|
|
19b84ce9f0 | ||
|
|
e5aed4c067 | ||
|
|
9e048bc0c3 | ||
|
|
5d7b0735ce | ||
|
|
12521cd5b4 | ||
|
|
7dbd3eb5e8 | ||
|
|
96364aacc0 | ||
|
|
fc18f86964 | ||
|
|
fb3d368a78 | ||
|
|
f41435fae3 | ||
|
|
5928c50057 | ||
|
|
1de4bcef55 | ||
|
|
7b7559309d | ||
|
|
066df2c142 | ||
|
|
0c4a9b1dce | ||
|
|
65a7060d3b | ||
|
|
3483e7b061 | ||
|
|
13094e02bc | ||
|
|
ed777e9d5b | ||
|
|
8ad80a282a | ||
|
|
0b7eb49839 | ||
|
|
de3b40c2e6 | ||
|
|
efa0aaf2c2 | ||
|
|
bd4e1cdc1e | ||
|
|
eb8df1fc18 | ||
|
|
9a8e49751d | ||
|
|
58e15134fd | ||
|
|
875be11ae5 | ||
|
|
3f7c9ea3f5 | ||
|
|
33aa4daac8 | ||
|
|
58e4087d4b | ||
|
|
0072fd1573 | ||
|
|
5d5ed10a45 | ||
|
|
5ee8ee7b04 | ||
|
|
521ae7f60e | ||
|
|
27c8c12420 | ||
|
|
3d1f100781 | ||
|
|
16d36fc17f | ||
|
|
eddb5bad91 | ||
|
|
23e1b5ee3f | ||
|
|
9e053bef19 | ||
|
|
cf234fca15 | ||
|
|
050a563e1d | ||
|
|
c347ff742e | ||
|
|
db1c819fe1 | ||
|
|
9f2818ee29 | ||
|
|
26aa6d23c7 | ||
|
|
ec7e894eb3 | ||
|
|
71a08382d6 | ||
|
|
09224f8676 | ||
|
|
008b8ac394 |
@@ -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
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: sanic-org # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
||||
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
@@ -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
|
||||
27
.travis.yml
27
.travis.yml
@@ -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
|
||||
|
||||
291
CHANGELOG.rst
291
CHANGELOG.rst
@@ -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:
|
||||
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -71,7 +71,7 @@ black:
|
||||
black --config ./.black.toml sanic tests
|
||||
|
||||
fix-import: black
|
||||
isort -rc sanic tests
|
||||
isort sanic tests
|
||||
|
||||
|
||||
docs-clean:
|
||||
|
||||
@@ -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::
|
||||
|
||||
|
||||
34
SECURITY.md
34
SECURITY.md
@@ -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
|
||||
|
||||
|
||||
1
changelogs/1892.removal.rst
Normal file
1
changelogs/1892.removal.rst
Normal file
@@ -0,0 +1 @@
|
||||
Remove [version] section.
|
||||
3
changelogs/1904.feature.rst
Normal file
3
changelogs/1904.feature.rst
Normal 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
1
changelogs/1970.misc.rst
Normal file
@@ -0,0 +1 @@
|
||||
Adds py.typed file to expose type information to other packages.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
-------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "20.6.2"
|
||||
__version__ = "20.12.2"
|
||||
|
||||
136
sanic/app.py
136
sanic/app.py
@@ -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.')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
148
sanic/config.py
148
sanic/config.py
@@ -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
106
sanic/deprecated.py
Normal 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)
|
||||
@@ -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("&", "&").replace("<", "<")
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -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
0
sanic/py.typed
Normal 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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
99
sanic/utils.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
30
setup.py
30
setup.py
@@ -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 = [
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
1
tests/static/app_test_config.py
Normal file
1
tests/static/app_test_config.py
Normal file
@@ -0,0 +1 @@
|
||||
TEST_SETTING_VALUE = 1
|
||||
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
86
tests/test_errorpages.py
Normal 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"
|
||||
@@ -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()
|
||||
|
||||
38
tests/test_load_module_from_file_location.py
Normal file
38
tests/test_load_module_from_file_location.py
Normal 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}")
|
||||
@@ -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'",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
36
tests/test_update_config.py
Normal file
36
tests/test_update_config.py
Normal 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
28
tox.ini
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user