Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d758f7c6df | ||
|
|
aa7f2759a6 | ||
|
|
9b9dd67797 | ||
|
|
3f73bc075a | ||
|
|
56989a017b | ||
|
|
ada5918bc8 | ||
|
|
4efd450b32 | ||
|
|
d2670664ba | ||
|
|
fa7405fe9c | ||
|
|
33297f48a5 | ||
|
|
06297a1918 | ||
|
|
aa0874b6d8 | ||
|
|
822ced6294 | ||
|
|
1a59614f79 | ||
|
|
f2d528e52a | ||
|
|
f7adc5f84c | ||
|
|
e955e833c4 | ||
|
|
096c44b910 | ||
|
|
efb9a42045 | ||
|
|
296cda7801 | ||
|
|
90b9d73244 | ||
|
|
c8b0e7f2a7 | ||
|
|
6ce88ab5a4 | ||
|
|
e13ab805df | ||
|
|
e58ea8c7b4 | ||
|
|
dd5bac61cb | ||
|
|
6270b27a97 | ||
|
|
f89ba1d39f | ||
|
|
8b5d137d8f | ||
|
|
2629fab649 | ||
|
|
92cd10c6a8 | ||
|
|
cc3edb90dc | ||
|
|
c60ba81984 | ||
|
|
ece3cdaa2e | ||
|
|
4cb40f2042 | ||
|
|
0e9f350982 | ||
|
|
cf439f01f8 | ||
|
|
f1f1b8a630 | ||
|
|
d4d1df03c9 | ||
|
|
92b73a6f4f | ||
|
|
b63c06c75a | ||
|
|
3e3bce422e | ||
|
|
e3a27c2cc4 | ||
|
|
f13f451084 | ||
|
|
df0e3de911 | ||
|
|
8466be8728 | ||
|
|
5cf2144b3f | ||
|
|
7c182f63c8 | ||
|
|
056180782c | ||
|
|
ff0d5870e9 | ||
|
|
b70176f8c7 | ||
|
|
e3655b525d | ||
|
|
e63d0091af | ||
|
|
7b0af2d80d | ||
|
|
7d79a86d4d | ||
|
|
ba46aff069 | ||
|
|
7a65471ba5 | ||
|
|
c7c46da975 | ||
|
|
c708e8425f | ||
|
|
905c51bef0 | ||
|
|
bd87098b7e | ||
|
|
5f486cc25f | ||
|
|
f79fb72a33 | ||
|
|
0505aa2dda | ||
|
|
485ff32e42 | ||
|
|
5ead67972f | ||
|
|
9c860dbff3 | ||
|
|
a20ad99638 | ||
|
|
8ef7bf8e7b | ||
|
|
0d5be1969a | ||
|
|
d06ea9bfc3 | ||
|
|
57e79882e1 | ||
|
|
20d1ab60c7 | ||
|
|
277c2ce2d2 | ||
|
|
34e51f01d1 | ||
|
|
f4b4e3a58c | ||
|
|
def2e033c8 | ||
|
|
dfec18278b | ||
|
|
cd5bdecda3 | ||
|
|
9b6217ba41 | ||
|
|
272f6e195d | ||
|
|
aa9bf04dfe | ||
|
|
9ae6dfb6d2 | ||
|
|
619bb79a2f | ||
|
|
0cad831eca | ||
|
|
f15a7fb588 | ||
|
|
1bdf9ca057 | ||
|
|
c8c370b784 | ||
|
|
63182f55f7 | ||
|
|
41759248e2 | ||
|
|
3149d5a66d | ||
|
|
8b13597099 | ||
|
|
36032cc26e | ||
|
|
4cb107aedc | ||
|
|
176f8d1981 | ||
|
|
9a26030bd5 | ||
|
|
6778f4d9e0 | ||
|
|
fd61b9e3e2 | ||
|
|
298d5cdf24 | ||
|
|
1bf1c9d006 | ||
|
|
7dc62be5cf | ||
|
|
be580a6a5b | ||
|
|
8ce519668b | ||
|
|
801258c46a | ||
|
|
32a1db3622 | ||
|
|
ed1f3daacc | ||
|
|
b7d74c82ba | ||
|
|
c3b31a6fb0 | ||
|
|
f4c55bbc07 | ||
|
|
a16842f7bc | ||
|
|
439a38664f | ||
|
|
5cc12fd945 | ||
|
|
fe116fff5a | ||
|
|
06aaaf4727 | ||
|
|
6deb9b49b2 | ||
|
|
d59e92d3e5 | ||
|
|
cc83c1f0cf | ||
|
|
1fe7306af8 | ||
|
|
c796d73fc3 | ||
|
|
eb93f884f3 | ||
|
|
3673feb256 | ||
|
|
7c9c783e9d | ||
|
|
74a4b9efaa | ||
|
|
4466e8cce1 | ||
|
|
b689037984 | ||
|
|
db1ba21d88 | ||
|
|
50d270ef7c | ||
|
|
d1a578b555 | ||
|
|
76e9859cf8 | ||
|
|
add9d363c5 | ||
|
|
1498baab0f | ||
|
|
df7f63d45d | ||
|
|
f7425126a1 | ||
|
|
790047e450 | ||
|
|
9198b5b0be | ||
|
|
d534acb79d | ||
|
|
d100f54551 | ||
|
|
7a9e100b0f | ||
|
|
fafe23d7c2 | ||
|
|
9a08bdae4a | ||
|
|
bcc11fa7fe | ||
|
|
7d0c0fdf7c | ||
|
|
0e33d46ead | ||
|
|
efbacc17cf | ||
|
|
bd6dbd9090 | ||
|
|
076cf51fb2 | ||
|
|
f8a6af1e28 | ||
|
|
96912f436d | ||
|
|
f0e162442f | ||
|
|
04b8dd989f | ||
|
|
5851c8bd91 | ||
|
|
78efcf93f8 | ||
|
|
bb35bc3898 | ||
|
|
f38783bdef | ||
|
|
d8f9986089 | ||
|
|
3e616b599a | ||
|
|
d38fc17191 | ||
|
|
7ae0eb0dc3 | ||
|
|
c578974246 | ||
|
|
fec81ffe73 | ||
|
|
a7dd73c657 | ||
|
|
f770e16f6d | ||
|
|
c1222175b3 | ||
|
|
7928b9b3a2 |
32
.appveyor.yml
Normal file
32
.appveyor.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: "{branch}.{build}"
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- TOXENV: py35-no-ext
|
||||
PYTHON: "C:\\Python35-x64"
|
||||
PYTHON_VERSION: "3.5.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
- TOXENV: py36-no-ext
|
||||
PYTHON: "C:\\Python36-x64"
|
||||
PYTHON_VERSION: "3.6.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
- TOXENV: py37-no-ext
|
||||
PYTHON: "C:\\Python37-x64"
|
||||
PYTHON_VERSION: "3.7.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||
|
||||
install:
|
||||
- pip install tox
|
||||
|
||||
build: off
|
||||
|
||||
test_script: tox
|
||||
|
||||
notifications:
|
||||
- provider: Email
|
||||
on_build_success: false
|
||||
on_build_status_changed: false
|
||||
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks.
|
||||
|
||||
|
||||
**Code snippet**
|
||||
Relevant source code, make sure to remove what is not necessary.
|
||||
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 0.8.3]
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for Sanic
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context or sample code about the feature request here.
|
||||
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
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
|
||||
10
.travis.yml
10
.travis.yml
@@ -21,17 +21,21 @@ matrix:
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- env: TOX_ENV=flake8
|
||||
- env: TOX_ENV=lint
|
||||
python: 3.6
|
||||
- env: TOX_ENV=check
|
||||
python: 3.6
|
||||
install: pip install -U tox
|
||||
install:
|
||||
- pip install -U tox
|
||||
- pip install codecov
|
||||
script: travis_retry tox -e $TOX_ENV
|
||||
after_success:
|
||||
- codecov
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
password:
|
||||
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
|
||||
secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
|
||||
on:
|
||||
tags: true
|
||||
distributions: "sdist bdist_wheel"
|
||||
|
||||
87
CHANGELOG.md
87
CHANGELOG.md
@@ -1,3 +1,80 @@
|
||||
Version 0.8
|
||||
-----------
|
||||
0.8.3
|
||||
- Changes:
|
||||
- Ownership changed to org 'huge-success'
|
||||
|
||||
0.8.0
|
||||
- Changes:
|
||||
- Add Server-Sent Events extension (Innokenty Lebedev)
|
||||
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
|
||||
- Sanitize URL before redirection (aveao)
|
||||
- Add url_bytes to request (johndoe46)
|
||||
- py37 support for travisci (yunstanford)
|
||||
- Auto reloader support for OSX (garyo)
|
||||
- Add UUID route support (Volodymyr Maksymiv)
|
||||
- Add pausable response streams (Ashley Sommer)
|
||||
- Add weakref to request slots (vopankov)
|
||||
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
|
||||
- Allow streaming handlers in add_route (kinware)
|
||||
- use travis_retry for tox (Raphael Deem)
|
||||
- update aiohttp version for test client (yunstanford)
|
||||
- add redirect import for clarity (yingshaoxo)
|
||||
- Update HTTP Entity headers (Arnulfo Solís)
|
||||
- Add register_listener method (Stephan Fitzpatrick)
|
||||
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
|
||||
- Content-length header on 204/304 responses (Arnulfo Solís)
|
||||
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
|
||||
- Update development status from pre-alpha to beta (Maksim Anisenkov)
|
||||
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
|
||||
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
||||
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
|
||||
- Add support for blueprint groups and nesting (Elias Tarhini)
|
||||
- Remove uvloop for windows setup (Aleksandr Kurlov)
|
||||
- Auto Reload (Yaser Amari)
|
||||
- Documentation updates/fixups (multiple contributors)
|
||||
|
||||
- Fixes:
|
||||
- Fix: auto_reload in Linux (Ashley Sommer)
|
||||
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
|
||||
- Fix: disable auto_reload by default on windows (abuckenheimer)
|
||||
- Fix (1143): Turn off access log with gunicorn (hqy)
|
||||
- Fix (1268): Support status code for file response (Cosmo Borsky)
|
||||
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
|
||||
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
|
||||
- Fix (1242): Responses for CI header (yunstanford)
|
||||
- Fix (1237): add version constraint for websockets (yunstanford)
|
||||
- Fix (1231): memory leak - always release resource (Phillip Xu)
|
||||
- Fix (1221): make request truthy if transport exists (Raphael Deem)
|
||||
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
|
||||
- Fix try_everything examples (PyManiacGR, kot83)
|
||||
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
|
||||
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
|
||||
- Fix: raw requires bytes-like object (cloudship)
|
||||
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
|
||||
- Fix: Bug in multipart/form-data parser (DirkGuijt)
|
||||
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
|
||||
- Fix: Parameter check (Howie Hu)
|
||||
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
|
||||
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
|
||||
- Fix: single quote in readme.rst (Cosven)
|
||||
- Fix: method typos (Dmitry Dygalo)
|
||||
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
|
||||
- Fix (1042): Exception Handling (Raphael Deem)
|
||||
- Fix: Chinese URIs (Howie Hu)
|
||||
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
|
||||
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
|
||||
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
|
||||
- Fix (1065): allow add_task after server starts (Raphael Deem)
|
||||
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
|
||||
- Fix (1062): inject the app in add_task method (Raphael Deem)
|
||||
- Fix: update environment.yml for readthedocs (Eli Uriegas)
|
||||
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
|
||||
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
|
||||
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
|
||||
|
||||
Note: Changelog was unmaintained between 0.1 and 0.7
|
||||
|
||||
Version 0.1
|
||||
-----------
|
||||
- 0.1.7
|
||||
@@ -5,18 +82,18 @@ Version 0.1
|
||||
- 0.1.6
|
||||
- Static files
|
||||
- Lazy Cookie Loading
|
||||
- 0.1.5
|
||||
- 0.1.5
|
||||
- Cookies
|
||||
- Blueprint listeners and ordering
|
||||
- Faster Router
|
||||
- Fix: Incomplete file reads on medium+ sized post requests
|
||||
- Breaking: after_start and before_stop now pass sanic as their first argument
|
||||
- 0.1.4
|
||||
- 0.1.4
|
||||
- Multiprocessing
|
||||
- 0.1.3
|
||||
- Blueprint support
|
||||
- Faster Response processing
|
||||
- 0.1.1 - 0.1.2
|
||||
- 0.1.1 - 0.1.2
|
||||
- Struggling to update pypi via CI
|
||||
- 0.1.0
|
||||
- Released to public
|
||||
- 0.1.0
|
||||
- Released to public
|
||||
|
||||
35
README.rst
35
README.rst
@@ -1,15 +1,15 @@
|
||||
Sanic
|
||||
=====
|
||||
|
||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
|
||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |AppVeyor Build Status| |Documentation| |Codecov| |PyPI| |PyPI version| |Code style black|
|
||||
|
||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
|
||||
|
||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
Sanic is developed `on GitHub <https://github.com/huge-success/sanic/>`_. We also have `a community discussion board <https://community.sanicframework.org/>`_. Contributions are welcome!
|
||||
|
||||
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/channelcat/sanic/issues/396>`_ that we use to track those projects!
|
||||
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/huge-success/sanic/issues/396>`_ that we use to track those projects!
|
||||
|
||||
Hello World Example
|
||||
-------------------
|
||||
@@ -47,33 +47,36 @@ Documentation
|
||||
|
||||
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
|
||||
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master
|
||||
:target: https://travis-ci.org/channelcat/sanic
|
||||
.. |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
|
||||
.. |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
|
||||
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
|
||||
Questions and Discussion
|
||||
------------------------
|
||||
|
||||
`Ask a question or join the conversation <https://community.sanicframework.org/>`_.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
`Non-Core examples <https://github.com/channelcat/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
|
||||
`Non-Core examples <https://github.com/huge-success/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
|
||||
|
||||
`Extensions <https://github.com/channelcat/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
|
||||
`Extensions <https://github.com/huge-success/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
|
||||
|
||||
`Projects <https://github.com/channelcat/sanic/wiki/Projects/>`_. Sanic in production use.
|
||||
`Projects <https://github.com/huge-success/sanic/wiki/Projects/>`_. Sanic in production use.
|
||||
|
||||
|
||||
TODO
|
||||
----
|
||||
* http2
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
* No wheels for uvloop and httptools on Windows :(
|
||||
|
||||
Final Thoughts
|
||||
--------------
|
||||
|
||||
|
||||
0
docs/_static/.gitkeep
vendored
Normal file
0
docs/_static/.gitkeep
vendored
Normal file
@@ -21,8 +21,11 @@ Guides
|
||||
sanic/streaming
|
||||
sanic/class_based_views
|
||||
sanic/custom_protocol
|
||||
sanic/sockets
|
||||
sanic/ssl
|
||||
sanic/logging
|
||||
sanic/versioning
|
||||
sanic/debug_mode
|
||||
sanic/testing
|
||||
sanic/deploying
|
||||
sanic/extensions
|
||||
|
||||
@@ -48,7 +48,7 @@ by that blueprint. In this example, the registered routes in the `app.router`
|
||||
will look like:
|
||||
|
||||
```python
|
||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
|
||||
```
|
||||
|
||||
## Blueprint groups and nesting
|
||||
@@ -87,7 +87,7 @@ from sanic import Blueprint
|
||||
from .static import static
|
||||
from .authors import authors
|
||||
|
||||
content = Blueprint.group(assets, authors, url_prefix='/content')
|
||||
content = Blueprint.group(static, authors, url_prefix='/content')
|
||||
```
|
||||
```python
|
||||
# api/info.py
|
||||
@@ -254,5 +254,3 @@ async def root(request):
|
||||
async def post_handler(request, post_id):
|
||||
return text('Post {} in Blueprint V1'.format(post_id))
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -85,13 +85,15 @@ DB_USER = 'appuser'
|
||||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | --------- | --------------------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
||||
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
|
||||
| Variable | Default | Description |
|
||||
| ------------------------- | --------- | ------------------------------------------------------ |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
||||
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
|
||||
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long take to force close non-idle connection (sec) |
|
||||
| ACCESS_LOG | True | Disable or enable access log |
|
||||
|
||||
### The different Timeout variables:
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ See it's that simple!
|
||||
## Pull requests!
|
||||
|
||||
So the pull request approval rules are pretty simple:
|
||||
1. All pull requests must pass unit tests
|
||||
* All pull requests must be reviewed and approved by at least
|
||||
* All pull requests must pass unit tests
|
||||
* All pull requests must be reviewed and approved by at least
|
||||
one current collaborator on the project
|
||||
* All pull requests must pass flake8 checks
|
||||
* If you decide to remove/change anything from any common interface
|
||||
|
||||
@@ -34,6 +34,6 @@ def authorized():
|
||||
@app.route("/")
|
||||
@authorized()
|
||||
async def test(request):
|
||||
return json({status: 'authorized'})
|
||||
return json({'status': 'authorized'})
|
||||
```
|
||||
|
||||
|
||||
@@ -47,6 +47,36 @@ async def ignore_404s(request, exception):
|
||||
return text("Yep, I totally found the page: {}".format(request.url))
|
||||
```
|
||||
|
||||
You can also add an exception handler as such:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
async def server_error_handler(request, exception):
|
||||
return text("Oops, server error", status=500)
|
||||
|
||||
app = Sanic()
|
||||
app.error_handler.add(Exception, server_error_handler)
|
||||
```
|
||||
|
||||
In some cases, you might want want to add some more error handling
|
||||
functionality to what is provided by default. In that case, you
|
||||
can subclass Sanic's default error handler as such:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.handlers import ErrorHandler
|
||||
|
||||
class CustomErrorHandler(ErrorHandler):
|
||||
def default(self, request, exception):
|
||||
''' handles errors that have no error handlers assigned '''
|
||||
# You custom error handling logic...
|
||||
return super().default(request, exception)
|
||||
|
||||
app = Sanic()
|
||||
app.error_handler = CustomErrorHandler()
|
||||
```
|
||||
|
||||
## Useful exceptions
|
||||
|
||||
Some of the most useful exceptions are presented below:
|
||||
|
||||
@@ -8,6 +8,7 @@ A list of Sanic extensions created by the community.
|
||||
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic.
|
||||
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for Sanic
|
||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
||||
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
|
||||
@@ -31,4 +32,5 @@ A list of Sanic extensions created by the community.
|
||||
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
|
||||
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
|
||||
- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.
|
||||
- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
|
||||
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.
|
||||
|
||||
@@ -4,8 +4,13 @@ Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
|
||||
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
|
||||
syntax, so earlier versions of python won't work.
|
||||
|
||||
1. Install Sanic: `python3 -m pip install sanic`
|
||||
2. Create a file called `main.py` with the following code:
|
||||
## 1. Install Sanic
|
||||
|
||||
```
|
||||
python3 -m pip install sanic
|
||||
```
|
||||
|
||||
## 2. Create a file called `main.py`
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
@@ -20,9 +25,16 @@ syntax, so earlier versions of python won't work.
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
3. Run the server: `python3 main.py`
|
||||
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||
the message *Hello world!*.
|
||||
|
||||
## 3. Run the server
|
||||
|
||||
```
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## 4. Check your browser
|
||||
|
||||
Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||
the message *Hello world!*.
|
||||
|
||||
You now have a working Sanic server!
|
||||
|
||||
@@ -9,17 +9,32 @@ A simple example using default settings would be like this:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.log import logger
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('test')
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
return response.text('Hello World!')
|
||||
logger.info('Here is your log')
|
||||
return text('Hello World!')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, access_log=True)
|
||||
```
|
||||
|
||||
After the server is running, you can see some messages looks like:
|
||||
```
|
||||
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
|
||||
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
|
||||
```
|
||||
|
||||
You can send a request to server and it will print the log messages:
|
||||
```
|
||||
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
|
||||
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
|
||||
```
|
||||
|
||||
To use your own logging config, simply use `logging.config.dictConfig`, or
|
||||
pass `log_config` when you initialize `Sanic` app:
|
||||
|
||||
@@ -49,7 +64,7 @@ By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS
|
||||
|
||||
There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
|
||||
|
||||
- root:<br>
|
||||
- sanic.root:<br>
|
||||
Used to log internal messages.
|
||||
|
||||
- sanic.error:<br>
|
||||
|
||||
@@ -17,7 +17,7 @@ string representing its type: `'request'` or `'response'`.
|
||||
|
||||
The simplest middleware doesn't modify the request or response at all:
|
||||
|
||||
```python
|
||||
```
|
||||
@app.middleware('request')
|
||||
async def print_on_request(request):
|
||||
print("I print when a request is received by the server")
|
||||
@@ -33,7 +33,7 @@ Middleware can modify the request or response parameter it is given, *as long
|
||||
as it does not return it*. The following example shows a practical use-case for
|
||||
this.
|
||||
|
||||
```python
|
||||
```
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.middleware('response')
|
||||
@@ -60,7 +60,7 @@ and the response will be returned. If this occurs to a request before the
|
||||
relevant user route handler is reached, the handler will never be called.
|
||||
Returning a response will also prevent any further middleware from running.
|
||||
|
||||
```python
|
||||
```
|
||||
@app.middleware('request')
|
||||
async def halt_request(request):
|
||||
return text('I halted the request')
|
||||
@@ -79,11 +79,11 @@ If you want to execute startup/teardown code as your server starts or closes, yo
|
||||
- `before_server_stop`
|
||||
- `after_server_stop`
|
||||
|
||||
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
|
||||
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
```
|
||||
@app.listener('before_server_start')
|
||||
async def setup_db(app, loop):
|
||||
app.db = await db_setup()
|
||||
@@ -101,16 +101,16 @@ async def close_db(app, loop):
|
||||
await app.db.close()
|
||||
```
|
||||
|
||||
It's also possible to register a listener using the `register_listener` method.
|
||||
It's also possible to register a listener using the `register_listener` method.
|
||||
This may be useful if you define your listeners in another module besides
|
||||
the one you instantiate your app in.
|
||||
|
||||
```python
|
||||
```
|
||||
app = Sanic()
|
||||
|
||||
|
||||
async def setup_db(app, loop):
|
||||
app.db = await db_setup()
|
||||
|
||||
|
||||
app.register_listener(setup_db, 'before_server_start')
|
||||
|
||||
```
|
||||
@@ -118,7 +118,7 @@ app.register_listener(setup_db, 'before_server_start')
|
||||
If you want to schedule a background task to run after the loop has started,
|
||||
Sanic provides the `add_task` method to easily do so.
|
||||
|
||||
```python
|
||||
```
|
||||
async def notify_server_started_after_five_seconds():
|
||||
await asyncio.sleep(5)
|
||||
print('Server successfully started!')
|
||||
@@ -128,7 +128,7 @@ app.add_task(notify_server_started_after_five_seconds())
|
||||
|
||||
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
|
||||
|
||||
```python
|
||||
```
|
||||
async def notify_server_started_after_five_seconds(app):
|
||||
await asyncio.sleep(5)
|
||||
print(app.name)
|
||||
@@ -138,7 +138,7 @@ app.add_task(notify_server_started_after_five_seconds)
|
||||
|
||||
Or you can pass the app explicitly for the same effect:
|
||||
|
||||
```python
|
||||
```
|
||||
async def notify_server_started_after_five_seconds(app):
|
||||
await asyncio.sleep(5)
|
||||
print(app.name)
|
||||
|
||||
66
docs/sanic/sockets.rst
Normal file
66
docs/sanic/sockets.rst
Normal file
@@ -0,0 +1,66 @@
|
||||
Sockets
|
||||
=======
|
||||
|
||||
Sanic can use the python
|
||||
`socket module <https://docs.python.org/3/library/socket.html>`_ to accommodate
|
||||
non IPv4 sockets.
|
||||
|
||||
IPv6 example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
import socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
sock.bind(('::', 7777))
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(sock=sock)
|
||||
|
||||
to test IPv6 ``curl -g -6 "http://[::1]:7777/"``
|
||||
|
||||
|
||||
UNIX socket example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import signal
|
||||
import sys
|
||||
import socket
|
||||
import os
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
server_socket = '/tmp/sanic.sock'
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.bind(server_socket)
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print('Exiting')
|
||||
os.unlink(server_socket)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(sock=sock)
|
||||
|
||||
to test UNIX: ``curl -v --unix-socket /tmp/sanic.sock http://localhost/hello``
|
||||
@@ -1,7 +1,7 @@
|
||||
# Static Files
|
||||
|
||||
Static files and directories, such as an image file, are served by Sanic when
|
||||
registered with the `app.static` method. The method takes an endpoint URL and a
|
||||
registered with the `app.static()` method. The method takes an endpoint URL and a
|
||||
filename. The file specified will then be accessible via the given endpoint.
|
||||
|
||||
```python
|
||||
@@ -43,3 +43,41 @@ app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
> **Note:** Sanic does not provide directory index when you serve a static directory.
|
||||
|
||||
## Virtual Host
|
||||
|
||||
The `app.static()` method also support **virtual host**. You can serve your static files with spefic **virtual host** with `host` argument. For example:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.static('/static', './static')
|
||||
app.static('/example_static', './example_static', host='www.example.com')
|
||||
```
|
||||
|
||||
## Streaming Large File
|
||||
|
||||
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
|
||||
|
||||
Here is an example:
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
|
||||
```
|
||||
|
||||
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
|
||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
|
||||
```
|
||||
|
||||
@@ -43,6 +43,7 @@ and ``recv`` methods to send and receive data respectively.
|
||||
You could setup your own WebSocket configuration through ``app.config``, like
|
||||
|
||||
.. code:: python
|
||||
|
||||
app.config.WEBSOCKET_MAX_SIZE = 2 ** 20
|
||||
app.config.WEBSOCKET_MAX_QUEUE = 32
|
||||
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
|
||||
|
||||
@@ -12,9 +12,10 @@ dependencies:
|
||||
- zlib=1.2.8=0
|
||||
- pip:
|
||||
- uvloop>=0.5.3
|
||||
- httptools>=0.0.9
|
||||
- httptools>=0.0.10
|
||||
- ujson>=1.35
|
||||
- aiofiles>=0.3.0
|
||||
- websockets>=3.2
|
||||
- websockets>=6.0
|
||||
- sphinxcontrib-asyncio>=0.2.0
|
||||
- multidict>=4.0,<5.0
|
||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
||||
|
||||
2
pyproject.toml
Normal file
2
pyproject.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
@@ -3,7 +3,7 @@ aiohttp>=2.3.0,<=3.2.1
|
||||
chardet<=2.3.0
|
||||
beautifulsoup4
|
||||
coverage
|
||||
httptools
|
||||
httptools>=0.0.10
|
||||
flake8
|
||||
pytest==3.3.2
|
||||
tox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
aiofiles
|
||||
httptools
|
||||
httptools>=0.0.10
|
||||
ujson; sys_platform != "win32" and implementation_name == "cpython"
|
||||
uvloop; sys_platform != "win32" and implementation_name == "cpython"
|
||||
websockets>=5.0,<6.0
|
||||
websockets>=6.0,<7.0
|
||||
multidict>=4.0,<5.0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
__version__ = '0.8.1'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
__version__ = "18.12.0"
|
||||
|
||||
__all__ = ["Sanic", "Blueprint"]
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
from argparse import ArgumentParser
|
||||
from importlib import import_module
|
||||
|
||||
from sanic.log import logger
|
||||
from sanic.app import Sanic
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
if __name__ == "__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('--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('--debug', dest='debug', action="store_true")
|
||||
parser.add_argument('module')
|
||||
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(
|
||||
"--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("--debug", dest="debug", action="store_true")
|
||||
parser.add_argument("module")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
@@ -25,20 +28,29 @@ if __name__ == "__main__":
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
if not isinstance(app, Sanic):
|
||||
raise ValueError("Module is not a Sanic app, it is a {}. "
|
||||
"Perhaps you meant {}.app?"
|
||||
.format(type(app).__name__, args.module))
|
||||
raise ValueError(
|
||||
"Module is not a Sanic app, it is a {}. "
|
||||
"Perhaps you meant {}.app?".format(
|
||||
type(app).__name__, args.module
|
||||
)
|
||||
)
|
||||
if args.cert is not None or args.key is not None:
|
||||
ssl = {'cert': args.cert, 'key': args.key}
|
||||
ssl = {"cert": args.cert, "key": args.key}
|
||||
else:
|
||||
ssl = None
|
||||
|
||||
app.run(host=args.host, port=args.port,
|
||||
workers=args.workers, debug=args.debug, ssl=ssl)
|
||||
app.run(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
workers=args.workers,
|
||||
debug=args.debug,
|
||||
ssl=ssl,
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(e.name))
|
||||
except ValueError as e:
|
||||
logger.error("{}".format(e))
|
||||
logger.error(
|
||||
"No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app".format(e.name)
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception("Failed to run app")
|
||||
|
||||
689
sanic/app.py
689
sanic/app.py
File diff suppressed because it is too large
Load Diff
@@ -3,21 +3,41 @@ from collections import defaultdict, namedtuple
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
FutureRoute = namedtuple('Route',
|
||||
['handler', 'uri', 'methods', 'host',
|
||||
'strict_slashes', 'stream', 'version', 'name'])
|
||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
||||
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
||||
FutureStatic = namedtuple('Route',
|
||||
['uri', 'file_or_directory', 'args', 'kwargs'])
|
||||
|
||||
FutureRoute = namedtuple(
|
||||
"FutureRoute",
|
||||
[
|
||||
"handler",
|
||||
"uri",
|
||||
"methods",
|
||||
"host",
|
||||
"strict_slashes",
|
||||
"stream",
|
||||
"version",
|
||||
"name",
|
||||
],
|
||||
)
|
||||
FutureListener = namedtuple(
|
||||
"FutureListener", ["handler", "uri", "methods", "host"]
|
||||
)
|
||||
FutureMiddleware = namedtuple(
|
||||
"FutureMiddleware", ["middleware", "args", "kwargs"]
|
||||
)
|
||||
FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"])
|
||||
FutureStatic = namedtuple(
|
||||
"FutureStatic", ["uri", "file_or_directory", "args", "kwargs"]
|
||||
)
|
||||
|
||||
|
||||
class Blueprint:
|
||||
def __init__(self, name,
|
||||
url_prefix=None,
|
||||
host=None, version=None,
|
||||
strict_slashes=False):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
url_prefix=None,
|
||||
host=None,
|
||||
version=None,
|
||||
strict_slashes=False,
|
||||
):
|
||||
"""Create a new blueprint
|
||||
|
||||
:param name: unique name of the blueprint
|
||||
@@ -38,13 +58,14 @@ class Blueprint:
|
||||
self.strict_slashes = strict_slashes
|
||||
|
||||
@staticmethod
|
||||
def group(*blueprints, url_prefix=''):
|
||||
def group(*blueprints, url_prefix=""):
|
||||
"""Create a list of blueprints, optionally
|
||||
grouping them under a general URL prefix.
|
||||
|
||||
:param blueprints: blueprints to be registered as a group
|
||||
:param url_prefix: URL route to be prepended to all sub-prefixes
|
||||
"""
|
||||
|
||||
def chain(nested):
|
||||
"""itertools.chain() but leaves strings untouched"""
|
||||
for i in nested:
|
||||
@@ -52,8 +73,11 @@ class Blueprint:
|
||||
yield from chain(i)
|
||||
else:
|
||||
yield i
|
||||
|
||||
bps = []
|
||||
for bp in chain(blueprints):
|
||||
if bp.url_prefix is None:
|
||||
bp.url_prefix = ""
|
||||
bp.url_prefix = url_prefix + bp.url_prefix
|
||||
bps.append(bp)
|
||||
return bps
|
||||
@@ -61,7 +85,7 @@ class Blueprint:
|
||||
def register(self, app, options):
|
||||
"""Register the blueprint to the sanic app."""
|
||||
|
||||
url_prefix = options.get('url_prefix', self.url_prefix)
|
||||
url_prefix = options.get("url_prefix", self.url_prefix)
|
||||
|
||||
# Routes
|
||||
for future in self.routes:
|
||||
@@ -73,14 +97,15 @@ class Blueprint:
|
||||
|
||||
version = future.version or self.version
|
||||
|
||||
app.route(uri=uri[1:] if uri.startswith('//') else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
app.route(
|
||||
uri=uri[1:] if uri.startswith("//") else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
for future in self.websocket_routes:
|
||||
# attach the blueprint name to the handler so that it can be
|
||||
@@ -88,18 +113,19 @@ class Blueprint:
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.websocket(uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
app.websocket(
|
||||
uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
# Middleware
|
||||
for future in self.middlewares:
|
||||
if future.args or future.kwargs:
|
||||
app.register_middleware(future.middleware,
|
||||
*future.args,
|
||||
**future.kwargs)
|
||||
app.register_middleware(
|
||||
future.middleware, *future.args, **future.kwargs
|
||||
)
|
||||
else:
|
||||
app.register_middleware(future.middleware)
|
||||
|
||||
@@ -111,16 +137,25 @@ class Blueprint:
|
||||
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)
|
||||
app.static(
|
||||
uri, future.file_or_directory, *future.args, **future.kwargs
|
||||
)
|
||||
|
||||
# Event listeners
|
||||
for event, listeners in self.listeners.items():
|
||||
for listener in listeners:
|
||||
app.listener(event)(listener)
|
||||
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=None, stream=False, version=None, name=None):
|
||||
def route(
|
||||
self,
|
||||
uri,
|
||||
methods=frozenset({"GET"}),
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""Create a blueprint route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
@@ -131,14 +166,30 @@ class Blueprint:
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(
|
||||
handler, uri, methods, host, strict_slashes, stream, version,
|
||||
name)
|
||||
handler,
|
||||
uri,
|
||||
methods,
|
||||
host,
|
||||
strict_slashes,
|
||||
stream,
|
||||
version,
|
||||
name,
|
||||
)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=None, version=None, name=None):
|
||||
def add_route(
|
||||
self,
|
||||
handler,
|
||||
uri,
|
||||
methods=frozenset({"GET"}),
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""Create a blueprint route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
@@ -152,7 +203,7 @@ class Blueprint:
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, 'view_class'):
|
||||
if hasattr(handler, "view_class"):
|
||||
methods = set()
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
@@ -166,13 +217,19 @@ class Blueprint:
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)(handler)
|
||||
self.route(
|
||||
uri=uri,
|
||||
methods=methods,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)(handler)
|
||||
return handler
|
||||
|
||||
def websocket(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
def websocket(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
"""Create a blueprint websocket route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
@@ -181,14 +238,17 @@ class Blueprint:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(handler, uri, [], host, strict_slashes,
|
||||
False, version, name)
|
||||
route = FutureRoute(
|
||||
handler, uri, [], host, strict_slashes, False, version, name
|
||||
)
|
||||
self.websocket_routes.append(route)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None, version=None,
|
||||
name=None):
|
||||
def add_websocket_route(
|
||||
self, handler, uri, host=None, version=None, name=None
|
||||
):
|
||||
"""Create a blueprint websocket route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
@@ -204,13 +264,16 @@ class Blueprint:
|
||||
|
||||
:param event: Event to listen to.
|
||||
"""
|
||||
|
||||
def decorator(listener):
|
||||
self.listeners[event].append(listener)
|
||||
return listener
|
||||
|
||||
return decorator
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""Create a blueprint middleware from a decorated function."""
|
||||
|
||||
def register_middleware(_middleware):
|
||||
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
||||
self.middlewares.append(future_middleware)
|
||||
@@ -226,10 +289,12 @@ class Blueprint:
|
||||
|
||||
def exception(self, *args, **kwargs):
|
||||
"""Create a blueprint exception from a decorated function."""
|
||||
|
||||
def decorator(handler):
|
||||
exception = FutureException(handler, args, kwargs)
|
||||
self.exceptions.append(exception)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||
@@ -238,12 +303,12 @@ class Blueprint:
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
name = kwargs.pop('name', 'static')
|
||||
if not name.startswith(self.name + '.'):
|
||||
name = '{}.{}'.format(self.name, name)
|
||||
name = kwargs.pop("name", "static")
|
||||
if not name.startswith(self.name + "."):
|
||||
name = "{}.{}".format(self.name, name)
|
||||
kwargs.update(name=name)
|
||||
|
||||
strict_slashes = kwargs.get('strict_slashes')
|
||||
strict_slashes = kwargs.get("strict_slashes")
|
||||
if strict_slashes is None and self.strict_slashes is not None:
|
||||
kwargs.update(strict_slashes=self.strict_slashes)
|
||||
|
||||
@@ -251,44 +316,107 @@ class Blueprint:
|
||||
self.statics.append(static)
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["GET"], host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
def get(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["GET"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def post(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["POST"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
def post(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["POST"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def put(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["PUT"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
def put(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["PUT"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def head(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
def head(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["HEAD"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def options(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
def options(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["OPTIONS"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def patch(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
def patch(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["PATCH"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def delete(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
def delete(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["DELETE"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import os
|
||||
import types
|
||||
|
||||
from sanic.exceptions import PyFileError
|
||||
|
||||
SANIC_PREFIX = 'SANIC_'
|
||||
|
||||
SANIC_PREFIX = "SANIC_"
|
||||
|
||||
|
||||
class Config(dict):
|
||||
@@ -63,9 +65,10 @@ class Config(dict):
|
||||
"""
|
||||
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)
|
||||
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):
|
||||
@@ -74,15 +77,20 @@ class Config(dict):
|
||||
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
module = types.ModuleType('config')
|
||||
module = types.ModuleType("config")
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec(compile(config_file.read(), filename, 'exec'),
|
||||
module.__dict__)
|
||||
exec(
|
||||
compile(config_file.read(), filename, "exec"),
|
||||
module.__dict__,
|
||||
)
|
||||
except IOError as e:
|
||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
||||
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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE')
|
||||
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
import string
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# SimpleCookie
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -8,18 +9,16 @@ import string
|
||||
# Straight up copied this section of dark magic from SimpleCookie
|
||||
|
||||
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
|
||||
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
|
||||
_UnescapedChars = _LegalChars + " ()/<=>?@[]{}"
|
||||
|
||||
_Translator = {n: '\\%03o' % n
|
||||
for n in set(range(256)) - set(map(ord, _UnescapedChars))}
|
||||
_Translator.update({
|
||||
ord('"'): '\\"',
|
||||
ord('\\'): '\\\\',
|
||||
})
|
||||
_Translator = {
|
||||
n: "\\%03o" % n for n in set(range(256)) - set(map(ord, _UnescapedChars))
|
||||
}
|
||||
_Translator.update({ord('"'): '\\"', ord("\\"): "\\\\"})
|
||||
|
||||
|
||||
def _quote(str):
|
||||
"""Quote a string for use in a cookie header.
|
||||
r"""Quote a string for use in a cookie header.
|
||||
If the string does not need to be double-quoted, then just return the
|
||||
string. Otherwise, surround the string in doublequotes and quote
|
||||
(with a \) special characters.
|
||||
@@ -30,7 +29,7 @@ def _quote(str):
|
||||
return '"' + str.translate(_Translator) + '"'
|
||||
|
||||
|
||||
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||
_is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# Custom SimpleCookie
|
||||
@@ -53,7 +52,7 @@ class CookieJar(dict):
|
||||
# If this cookie doesn't exist, add it to the header keys
|
||||
if not self.cookie_headers.get(key):
|
||||
cookie = Cookie(key, value)
|
||||
cookie['path'] = '/'
|
||||
cookie["path"] = "/"
|
||||
self.cookie_headers[key] = self.header_key
|
||||
self.headers.add(self.header_key, cookie)
|
||||
return super().__setitem__(key, cookie)
|
||||
@@ -62,8 +61,8 @@ class CookieJar(dict):
|
||||
|
||||
def __delitem__(self, key):
|
||||
if key not in self.cookie_headers:
|
||||
self[key] = ''
|
||||
self[key]['max-age'] = 0
|
||||
self[key] = ""
|
||||
self[key]["max-age"] = 0
|
||||
else:
|
||||
cookie_header = self.cookie_headers[key]
|
||||
# remove it from header
|
||||
@@ -77,6 +76,7 @@ class CookieJar(dict):
|
||||
|
||||
class Cookie(dict):
|
||||
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
|
||||
|
||||
_keys = {
|
||||
"expires": "expires",
|
||||
"path": "Path",
|
||||
@@ -88,7 +88,7 @@ class Cookie(dict):
|
||||
"version": "Version",
|
||||
"samesite": "SameSite",
|
||||
}
|
||||
_flags = {'secure', 'httponly'}
|
||||
_flags = {"secure", "httponly"}
|
||||
|
||||
def __init__(self, key, value):
|
||||
if key in self._keys:
|
||||
@@ -106,24 +106,27 @@ class Cookie(dict):
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def encode(self, encoding):
|
||||
output = ['%s=%s' % (self.key, _quote(self.value))]
|
||||
output = ["%s=%s" % (self.key, _quote(self.value))]
|
||||
for key, value in self.items():
|
||||
if key == 'max-age':
|
||||
if key == "max-age":
|
||||
try:
|
||||
output.append('%s=%d' % (self._keys[key], value))
|
||||
output.append("%s=%d" % (self._keys[key], value))
|
||||
except TypeError:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
elif key == 'expires':
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
elif key == "expires":
|
||||
try:
|
||||
output.append('%s=%s' % (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT")
|
||||
))
|
||||
output.append(
|
||||
"%s=%s"
|
||||
% (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT"),
|
||||
)
|
||||
)
|
||||
except AttributeError:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
elif key in self._flags and self[key]:
|
||||
output.append(self._keys[key])
|
||||
else:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
|
||||
return "; ".join(output).encode(encoding)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from sanic.http import STATUS_CODES
|
||||
from sanic.helpers import STATUS_CODES
|
||||
|
||||
TRACEBACK_STYLE = '''
|
||||
|
||||
TRACEBACK_STYLE = """
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
@@ -61,9 +62,9 @@ TRACEBACK_STYLE = '''
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
'''
|
||||
"""
|
||||
|
||||
TRACEBACK_WRAPPER_HTML = '''
|
||||
TRACEBACK_WRAPPER_HTML = """
|
||||
<html>
|
||||
<head>
|
||||
{style}
|
||||
@@ -78,27 +79,27 @@ TRACEBACK_WRAPPER_HTML = '''
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
"""
|
||||
|
||||
TRACEBACK_WRAPPER_INNER_HTML = '''
|
||||
TRACEBACK_WRAPPER_INNER_HTML = """
|
||||
<h1>{exc_name}</h1>
|
||||
<h3><code>{exc_value}</code></h3>
|
||||
<div class="tb-wrapper">
|
||||
<p class="tb-header">Traceback (most recent call last):</p>
|
||||
{frame_html}
|
||||
</div>
|
||||
'''
|
||||
"""
|
||||
|
||||
TRACEBACK_BORDER = '''
|
||||
TRACEBACK_BORDER = """
|
||||
<div class="tb-border">
|
||||
<b><i>
|
||||
The above exception was the direct cause of the
|
||||
following exception:
|
||||
</i></b>
|
||||
</div>
|
||||
'''
|
||||
"""
|
||||
|
||||
TRACEBACK_LINE_HTML = '''
|
||||
TRACEBACK_LINE_HTML = """
|
||||
<div class="frame-line">
|
||||
<p class="frame-descriptor">
|
||||
File {0.filename}, line <i>{0.lineno}</i>,
|
||||
@@ -106,15 +107,15 @@ TRACEBACK_LINE_HTML = '''
|
||||
</p>
|
||||
<p class="frame-code"><code>{0.line}</code></p>
|
||||
</div>
|
||||
'''
|
||||
"""
|
||||
|
||||
INTERNAL_SERVER_ERROR_HTML = '''
|
||||
INTERNAL_SERVER_ERROR_HTML = """
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>
|
||||
The server encountered an internal error and cannot complete
|
||||
your request.
|
||||
</p>
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
_sanic_exceptions = {}
|
||||
@@ -124,15 +125,16 @@ def add_status_code(code):
|
||||
"""
|
||||
Decorator used for adding exceptions to _sanic_exceptions.
|
||||
"""
|
||||
|
||||
def class_decorator(cls):
|
||||
cls.status_code = code
|
||||
_sanic_exceptions[code] = cls
|
||||
return cls
|
||||
|
||||
return class_decorator
|
||||
|
||||
|
||||
class SanicException(Exception):
|
||||
|
||||
def __init__(self, message, status_code=None):
|
||||
super().__init__(message)
|
||||
|
||||
@@ -156,8 +158,8 @@ class MethodNotSupported(SanicException):
|
||||
super().__init__(message)
|
||||
self.headers = dict()
|
||||
self.headers["Allow"] = ", ".join(allowed_methods)
|
||||
if method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
|
||||
self.headers['Content-Length'] = 0
|
||||
if method in ["HEAD", "PATCH", "PUT", "DELETE"]:
|
||||
self.headers["Content-Length"] = 0
|
||||
|
||||
|
||||
@add_status_code(500)
|
||||
@@ -169,6 +171,7 @@ class ServerError(SanicException):
|
||||
class ServiceUnavailable(SanicException):
|
||||
"""The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -192,6 +195,7 @@ class RequestTimeout(SanicException):
|
||||
the connection. The socket connection has actually been lost - the Web
|
||||
server has 'timed out' on that particular socket connection.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -209,8 +213,8 @@ class ContentRangeError(SanicException):
|
||||
def __init__(self, message, content_range):
|
||||
super().__init__(message)
|
||||
self.headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
"Content-Range": "bytes */%s" % (content_range.total,)
|
||||
"Content-Type": "text/plain",
|
||||
"Content-Range": "bytes */%s" % (content_range.total,),
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +227,11 @@ class InvalidRangeType(ContentRangeError):
|
||||
pass
|
||||
|
||||
|
||||
class PyFileError(Exception):
|
||||
def __init__(self, file):
|
||||
super().__init__("could not execute config file %s", file)
|
||||
|
||||
|
||||
@add_status_code(401)
|
||||
class Unauthorized(SanicException):
|
||||
"""
|
||||
@@ -258,13 +267,14 @@ class Unauthorized(SanicException):
|
||||
scheme="Bearer",
|
||||
realm="Restricted Area")
|
||||
"""
|
||||
|
||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||
super().__init__(message, status_code)
|
||||
|
||||
# if auth-scheme is specified, set "WWW-Authenticate" header
|
||||
if scheme is not None:
|
||||
values = ['{!s}="{!s}"'.format(k, v) for k, v in kwargs.items()]
|
||||
challenge = ', '.join(values)
|
||||
challenge = ", ".join(values)
|
||||
|
||||
self.headers = {
|
||||
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
|
||||
@@ -283,6 +293,6 @@ def abort(status_code, message=None):
|
||||
if message is None:
|
||||
message = STATUS_CODES.get(status_code)
|
||||
# These are stored as bytes in the STATUS_CODES dict
|
||||
message = message.decode('utf8')
|
||||
message = message.decode("utf8")
|
||||
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
||||
raise sanic_exception(message=message, status_code=status_code)
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import sys
|
||||
from traceback import format_exc, extract_tb
|
||||
|
||||
from traceback import extract_tb, format_exc
|
||||
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
INTERNAL_SERVER_ERROR_HTML,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
TRACEBACK_BORDER,
|
||||
TRACEBACK_LINE_HTML,
|
||||
TRACEBACK_STYLE,
|
||||
TRACEBACK_WRAPPER_HTML,
|
||||
TRACEBACK_WRAPPER_INNER_HTML,
|
||||
TRACEBACK_BORDER)
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
)
|
||||
from sanic.log import logger
|
||||
from sanic.response import text, html
|
||||
from sanic.response import html, text
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
@@ -36,7 +38,8 @@ class ErrorHandler:
|
||||
return TRACEBACK_WRAPPER_INNER_HTML.format(
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
frame_html=''.join(frame_html))
|
||||
frame_html="".join(frame_html),
|
||||
)
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
@@ -51,13 +54,14 @@ class ErrorHandler:
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
|
||||
path=request.path)
|
||||
path=request.path,
|
||||
)
|
||||
|
||||
def add(self, exception, handler):
|
||||
self.handlers.append((exception, handler))
|
||||
|
||||
def lookup(self, exception):
|
||||
handler = self.cached_handlers.get(exception, self._missing)
|
||||
handler = self.cached_handlers.get(type(exception), self._missing)
|
||||
if handler is self._missing:
|
||||
for exception_class, handler in self.handlers:
|
||||
if isinstance(exception, exception_class):
|
||||
@@ -84,40 +88,45 @@ class ErrorHandler:
|
||||
response = self.default(request, exception)
|
||||
except Exception:
|
||||
self.log(format_exc())
|
||||
if self.debug:
|
||||
url = getattr(request, 'url', 'unknown')
|
||||
response_message = ('Exception raised in exception handler '
|
||||
'"%s" for uri: "%s"\n%s')
|
||||
logger.error(response_message,
|
||||
handler.__name__, url, format_exc())
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
url = "unknown"
|
||||
response_message = (
|
||||
"Exception raised in exception handler " '"%s" for uri: %s'
|
||||
)
|
||||
logger.exception(response_message, handler.__name__, url)
|
||||
|
||||
return text(response_message % (
|
||||
handler.__name__, url, format_exc()), 500)
|
||||
if self.debug:
|
||||
return text(response_message % (handler.__name__, url), 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return text("An error occurred while handling an error", 500)
|
||||
return response
|
||||
|
||||
def log(self, message, level='error'):
|
||||
def log(self, message, level="error"):
|
||||
"""
|
||||
Override this method in an ErrorHandler subclass to prevent
|
||||
logging exceptions.
|
||||
Deprecated, do not use.
|
||||
"""
|
||||
getattr(logger, level)(message)
|
||||
|
||||
def default(self, request, exception):
|
||||
self.log(format_exc())
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
url = "unknown"
|
||||
|
||||
response_message = "Exception occurred while handling uri: %s"
|
||||
logger.exception(response_message, url)
|
||||
|
||||
if issubclass(type(exception), SanicException):
|
||||
return text(
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500),
|
||||
headers=getattr(exception, 'headers', dict())
|
||||
"Error: {}".format(exception),
|
||||
status=getattr(exception, "status_code", 500),
|
||||
headers=getattr(exception, "headers", dict()),
|
||||
)
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
response_message = ('Exception occurred while handling uri: '
|
||||
'"%s"\n%s')
|
||||
logger.error(response_message, request.url, format_exc())
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
@@ -125,47 +134,54 @@ class ErrorHandler:
|
||||
|
||||
class ContentRangeHandler:
|
||||
"""Class responsible for parsing request header"""
|
||||
__slots__ = ('start', 'end', 'size', 'total', 'headers')
|
||||
|
||||
__slots__ = ("start", "end", "size", "total", "headers")
|
||||
|
||||
def __init__(self, request, stats):
|
||||
self.total = stats.st_size
|
||||
_range = request.headers.get('Range')
|
||||
_range = request.headers.get("Range")
|
||||
if _range is None:
|
||||
raise HeaderNotFound('Range Header Not Found')
|
||||
unit, _, value = tuple(map(str.strip, _range.partition('=')))
|
||||
if unit != 'bytes':
|
||||
raise HeaderNotFound("Range Header Not Found")
|
||||
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
||||
if unit != "bytes":
|
||||
raise InvalidRangeType(
|
||||
'%s is not a valid Range Type' % (unit,), self)
|
||||
start_b, _, end_b = tuple(map(str.strip, value.partition('-')))
|
||||
"%s is not a valid Range Type" % (unit,), self
|
||||
)
|
||||
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
|
||||
try:
|
||||
self.start = int(start_b) if start_b else None
|
||||
except ValueError:
|
||||
raise ContentRangeError(
|
||||
'\'%s\' is invalid for Content Range' % (start_b,), self)
|
||||
"'%s' is invalid for Content Range" % (start_b,), self
|
||||
)
|
||||
try:
|
||||
self.end = int(end_b) if end_b else None
|
||||
except ValueError:
|
||||
raise ContentRangeError(
|
||||
'\'%s\' is invalid for Content Range' % (end_b,), self)
|
||||
"'%s' is invalid for Content Range" % (end_b,), self
|
||||
)
|
||||
if self.end is None:
|
||||
if self.start is None:
|
||||
raise ContentRangeError(
|
||||
'Invalid for Content Range parameters', self)
|
||||
"Invalid for Content Range parameters", self
|
||||
)
|
||||
else:
|
||||
# this case represents `Content-Range: bytes 5-`
|
||||
self.end = self.total
|
||||
self.end = self.total - 1
|
||||
else:
|
||||
if self.start is None:
|
||||
# this case represents `Content-Range: bytes -5`
|
||||
self.start = self.total - self.end
|
||||
self.end = self.total
|
||||
self.end = self.total - 1
|
||||
if self.start >= self.end:
|
||||
raise ContentRangeError(
|
||||
'Invalid for Content Range parameters', self)
|
||||
self.size = self.end - self.start
|
||||
"Invalid for Content Range parameters", self
|
||||
)
|
||||
self.size = self.end - self.start + 1
|
||||
self.headers = {
|
||||
'Content-Range': "bytes %s-%s/%s" % (
|
||||
self.start, self.end, self.total)}
|
||||
"Content-Range": "bytes %s-%s/%s"
|
||||
% (self.start, self.end, self.total)
|
||||
}
|
||||
|
||||
def __bool__(self):
|
||||
return self.size > 0
|
||||
|
||||
133
sanic/helpers.py
Normal file
133
sanic/helpers.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Defines basics of HTTP standard."""
|
||||
|
||||
STATUS_CODES = {
|
||||
100: b"Continue",
|
||||
101: b"Switching Protocols",
|
||||
102: b"Processing",
|
||||
200: b"OK",
|
||||
201: b"Created",
|
||||
202: b"Accepted",
|
||||
203: b"Non-Authoritative Information",
|
||||
204: b"No Content",
|
||||
205: b"Reset Content",
|
||||
206: b"Partial Content",
|
||||
207: b"Multi-Status",
|
||||
208: b"Already Reported",
|
||||
226: b"IM Used",
|
||||
300: b"Multiple Choices",
|
||||
301: b"Moved Permanently",
|
||||
302: b"Found",
|
||||
303: b"See Other",
|
||||
304: b"Not Modified",
|
||||
305: b"Use Proxy",
|
||||
307: b"Temporary Redirect",
|
||||
308: b"Permanent Redirect",
|
||||
400: b"Bad Request",
|
||||
401: b"Unauthorized",
|
||||
402: b"Payment Required",
|
||||
403: b"Forbidden",
|
||||
404: b"Not Found",
|
||||
405: b"Method Not Allowed",
|
||||
406: b"Not Acceptable",
|
||||
407: b"Proxy Authentication Required",
|
||||
408: b"Request Timeout",
|
||||
409: b"Conflict",
|
||||
410: b"Gone",
|
||||
411: b"Length Required",
|
||||
412: b"Precondition Failed",
|
||||
413: b"Request Entity Too Large",
|
||||
414: b"Request-URI Too Long",
|
||||
415: b"Unsupported Media Type",
|
||||
416: b"Requested Range Not Satisfiable",
|
||||
417: b"Expectation Failed",
|
||||
418: b"I'm a teapot",
|
||||
422: b"Unprocessable Entity",
|
||||
423: b"Locked",
|
||||
424: b"Failed Dependency",
|
||||
426: b"Upgrade Required",
|
||||
428: b"Precondition Required",
|
||||
429: b"Too Many Requests",
|
||||
431: b"Request Header Fields Too Large",
|
||||
451: b"Unavailable For Legal Reasons",
|
||||
500: b"Internal Server Error",
|
||||
501: b"Not Implemented",
|
||||
502: b"Bad Gateway",
|
||||
503: b"Service Unavailable",
|
||||
504: b"Gateway Timeout",
|
||||
505: b"HTTP Version Not Supported",
|
||||
506: b"Variant Also Negotiates",
|
||||
507: b"Insufficient Storage",
|
||||
508: b"Loop Detected",
|
||||
510: b"Not Extended",
|
||||
511: b"Network Authentication Required",
|
||||
}
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-7.1
|
||||
_ENTITY_HEADERS = frozenset(
|
||||
[
|
||||
"allow",
|
||||
"content-encoding",
|
||||
"content-language",
|
||||
"content-length",
|
||||
"content-location",
|
||||
"content-md5",
|
||||
"content-range",
|
||||
"content-type",
|
||||
"expires",
|
||||
"last-modified",
|
||||
"extension-header",
|
||||
]
|
||||
)
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-13.5.1
|
||||
_HOP_BY_HOP_HEADERS = frozenset(
|
||||
[
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailers",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def has_message_body(status):
|
||||
"""
|
||||
According to the following RFC message body and length SHOULD NOT
|
||||
be included in responses status 1XX, 204 and 304.
|
||||
https://tools.ietf.org/html/rfc2616#section-4.4
|
||||
https://tools.ietf.org/html/rfc2616#section-4.3
|
||||
"""
|
||||
return status not in (204, 304) and not (100 <= status < 200)
|
||||
|
||||
|
||||
def is_entity_header(header):
|
||||
"""Checks if the given header is an Entity Header"""
|
||||
return header.lower() in _ENTITY_HEADERS
|
||||
|
||||
|
||||
def is_hop_by_hop_header(header):
|
||||
"""Checks if the given header is a Hop By Hop header"""
|
||||
return header.lower() in _HOP_BY_HOP_HEADERS
|
||||
|
||||
|
||||
def remove_entity_headers(headers, allowed=("content-location", "expires")):
|
||||
"""
|
||||
Removes all the entity headers present in the headers given.
|
||||
According to RFC 2616 Section 10.3.5,
|
||||
Content-Location and Expires are allowed as for the
|
||||
"strong cache validator".
|
||||
https://tools.ietf.org/html/rfc2616#section-10.3.5
|
||||
|
||||
returns the headers without the entity headers
|
||||
"""
|
||||
allowed = set([h.lower() for h in allowed])
|
||||
headers = {
|
||||
header: value
|
||||
for header, value in headers.items()
|
||||
if not is_entity_header(header) or header.lower() in allowed
|
||||
}
|
||||
return headers
|
||||
128
sanic/http.py
128
sanic/http.py
@@ -1,128 +0,0 @@
|
||||
"""Defines basics of HTTP standard."""
|
||||
|
||||
STATUS_CODES = {
|
||||
100: b'Continue',
|
||||
101: b'Switching Protocols',
|
||||
102: b'Processing',
|
||||
200: b'OK',
|
||||
201: b'Created',
|
||||
202: b'Accepted',
|
||||
203: b'Non-Authoritative Information',
|
||||
204: b'No Content',
|
||||
205: b'Reset Content',
|
||||
206: b'Partial Content',
|
||||
207: b'Multi-Status',
|
||||
208: b'Already Reported',
|
||||
226: b'IM Used',
|
||||
300: b'Multiple Choices',
|
||||
301: b'Moved Permanently',
|
||||
302: b'Found',
|
||||
303: b'See Other',
|
||||
304: b'Not Modified',
|
||||
305: b'Use Proxy',
|
||||
307: b'Temporary Redirect',
|
||||
308: b'Permanent Redirect',
|
||||
400: b'Bad Request',
|
||||
401: b'Unauthorized',
|
||||
402: b'Payment Required',
|
||||
403: b'Forbidden',
|
||||
404: b'Not Found',
|
||||
405: b'Method Not Allowed',
|
||||
406: b'Not Acceptable',
|
||||
407: b'Proxy Authentication Required',
|
||||
408: b'Request Timeout',
|
||||
409: b'Conflict',
|
||||
410: b'Gone',
|
||||
411: b'Length Required',
|
||||
412: b'Precondition Failed',
|
||||
413: b'Request Entity Too Large',
|
||||
414: b'Request-URI Too Long',
|
||||
415: b'Unsupported Media Type',
|
||||
416: b'Requested Range Not Satisfiable',
|
||||
417: b'Expectation Failed',
|
||||
418: b'I\'m a teapot',
|
||||
422: b'Unprocessable Entity',
|
||||
423: b'Locked',
|
||||
424: b'Failed Dependency',
|
||||
426: b'Upgrade Required',
|
||||
428: b'Precondition Required',
|
||||
429: b'Too Many Requests',
|
||||
431: b'Request Header Fields Too Large',
|
||||
451: b'Unavailable For Legal Reasons',
|
||||
500: b'Internal Server Error',
|
||||
501: b'Not Implemented',
|
||||
502: b'Bad Gateway',
|
||||
503: b'Service Unavailable',
|
||||
504: b'Gateway Timeout',
|
||||
505: b'HTTP Version Not Supported',
|
||||
506: b'Variant Also Negotiates',
|
||||
507: b'Insufficient Storage',
|
||||
508: b'Loop Detected',
|
||||
510: b'Not Extended',
|
||||
511: b'Network Authentication Required'
|
||||
}
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-7.1
|
||||
_ENTITY_HEADERS = frozenset([
|
||||
'allow',
|
||||
'content-encoding',
|
||||
'content-language',
|
||||
'content-length',
|
||||
'content-location',
|
||||
'content-md5',
|
||||
'content-range',
|
||||
'content-type',
|
||||
'expires',
|
||||
'last-modified',
|
||||
'extension-header'
|
||||
])
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-13.5.1
|
||||
_HOP_BY_HOP_HEADERS = frozenset([
|
||||
'connection',
|
||||
'keep-alive',
|
||||
'proxy-authenticate',
|
||||
'proxy-authorization',
|
||||
'te',
|
||||
'trailers',
|
||||
'transfer-encoding',
|
||||
'upgrade'
|
||||
])
|
||||
|
||||
|
||||
def has_message_body(status):
|
||||
"""
|
||||
According to the following RFC message body and length SHOULD NOT
|
||||
be included in responses status 1XX, 204 and 304.
|
||||
https://tools.ietf.org/html/rfc2616#section-4.4
|
||||
https://tools.ietf.org/html/rfc2616#section-4.3
|
||||
"""
|
||||
return status not in (204, 304) and not (100 <= status < 200)
|
||||
|
||||
|
||||
def is_entity_header(header):
|
||||
"""Checks if the given header is an Entity Header"""
|
||||
return header.lower() in _ENTITY_HEADERS
|
||||
|
||||
|
||||
def is_hop_by_hop_header(header):
|
||||
"""Checks if the given header is a Hop By Hop header"""
|
||||
return header.lower() in _HOP_BY_HOP_HEADERS
|
||||
|
||||
|
||||
def remove_entity_headers(headers,
|
||||
allowed=('content-location', 'expires')):
|
||||
"""
|
||||
Removes all the entity headers present in the headers given.
|
||||
According to RFC 2616 Section 10.3.5,
|
||||
Content-Location and Expires are allowed as for the
|
||||
"strong cache validator".
|
||||
https://tools.ietf.org/html/rfc2616#section-10.3.5
|
||||
|
||||
returns the headers without the entity headers
|
||||
"""
|
||||
allowed = set([h.lower() for h in allowed])
|
||||
headers = {header: value for header, value in headers.items()
|
||||
if not is_entity_header(header)
|
||||
and header.lower() not in allowed}
|
||||
return headers
|
||||
35
sanic/log.py
35
sanic/log.py
@@ -5,59 +5,54 @@ import sys
|
||||
LOGGING_CONFIG_DEFAULTS = dict(
|
||||
version=1,
|
||||
disable_existing_loggers=False,
|
||||
|
||||
loggers={
|
||||
"root": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"]
|
||||
},
|
||||
"sanic.root": {"level": "INFO", "handlers": ["console"]},
|
||||
"sanic.error": {
|
||||
"level": "INFO",
|
||||
"handlers": ["error_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.error"
|
||||
"qualname": "sanic.error",
|
||||
},
|
||||
|
||||
"sanic.access": {
|
||||
"level": "INFO",
|
||||
"handlers": ["access_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.access"
|
||||
}
|
||||
"qualname": "sanic.access",
|
||||
},
|
||||
},
|
||||
handlers={
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stdout
|
||||
"stream": sys.stdout,
|
||||
},
|
||||
"error_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stderr
|
||||
"stream": sys.stderr,
|
||||
},
|
||||
"access_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "access",
|
||||
"stream": sys.stdout
|
||||
"stream": sys.stdout,
|
||||
},
|
||||
},
|
||||
formatters={
|
||||
"generic": {
|
||||
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
"class": "logging.Formatter"
|
||||
"class": "logging.Formatter",
|
||||
},
|
||||
"access": {
|
||||
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " +
|
||||
"%(request)s %(message)s %(status)d %(byte)d",
|
||||
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
|
||||
+ "%(request)s %(message)s %(status)d %(byte)d",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
"class": "logging.Formatter"
|
||||
"class": "logging.Formatter",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger('root')
|
||||
error_logger = logging.getLogger('sanic.error')
|
||||
access_logger = logging.getLogger('sanic.access')
|
||||
logger = logging.getLogger("sanic.root")
|
||||
error_logger = logging.getLogger("sanic.error")
|
||||
access_logger = logging.getLogger("sanic.access")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import subprocess
|
||||
from time import sleep
|
||||
import sys
|
||||
|
||||
from multiprocessing import Process
|
||||
from time import sleep
|
||||
|
||||
|
||||
def _iter_module_files():
|
||||
@@ -18,7 +19,7 @@ def _iter_module_files():
|
||||
for module in list(sys.modules.values()):
|
||||
if module is None:
|
||||
continue
|
||||
filename = getattr(module, '__file__', None)
|
||||
filename = getattr(module, "__file__", None)
|
||||
if filename:
|
||||
old = None
|
||||
while not os.path.isfile(filename):
|
||||
@@ -27,7 +28,7 @@ def _iter_module_files():
|
||||
if filename == old:
|
||||
break
|
||||
else:
|
||||
if filename[-4:] in ('.pyc', '.pyo'):
|
||||
if filename[-4:] in (".pyc", ".pyo"):
|
||||
filename = filename[:-1]
|
||||
yield filename
|
||||
|
||||
@@ -45,11 +46,13 @@ def restart_with_reloader():
|
||||
"""
|
||||
args = _get_args_for_reloading()
|
||||
new_environ = os.environ.copy()
|
||||
new_environ['SANIC_SERVER_RUNNING'] = 'true'
|
||||
cmd = ' '.join(args)
|
||||
new_environ["SANIC_SERVER_RUNNING"] = "true"
|
||||
cmd = " ".join(args)
|
||||
worker_process = Process(
|
||||
target=subprocess.call, args=(cmd,),
|
||||
kwargs=dict(shell=True, env=new_environ))
|
||||
target=subprocess.call,
|
||||
args=(cmd,),
|
||||
kwargs=dict(shell=True, env=new_environ),
|
||||
)
|
||||
worker_process.start()
|
||||
return worker_process
|
||||
|
||||
@@ -67,8 +70,10 @@ def kill_process_children_unix(pid):
|
||||
children_list_pid = children_list_file.read().split()
|
||||
|
||||
for child_pid in children_list_pid:
|
||||
children_proc_path = "/proc/%s/task/%s/children" % \
|
||||
(child_pid, child_pid)
|
||||
children_proc_path = "/proc/%s/task/%s/children" % (
|
||||
child_pid,
|
||||
child_pid,
|
||||
)
|
||||
if not os.path.isfile(children_proc_path):
|
||||
continue
|
||||
with open(children_proc_path) as children_list_file_2:
|
||||
@@ -90,7 +95,7 @@ def kill_process_children_osx(pid):
|
||||
:param pid: PID of parent process (process ID)
|
||||
:return: Nothing
|
||||
"""
|
||||
subprocess.run(['pkill', '-P', str(pid)])
|
||||
subprocess.run(["pkill", "-P", str(pid)])
|
||||
|
||||
|
||||
def kill_process_children(pid):
|
||||
@@ -99,12 +104,12 @@ def kill_process_children(pid):
|
||||
:param pid: PID of parent process (process ID)
|
||||
:return: Nothing
|
||||
"""
|
||||
if sys.platform == 'darwin':
|
||||
if sys.platform == "darwin":
|
||||
kill_process_children_osx(pid)
|
||||
elif sys.platform == 'linux':
|
||||
elif sys.platform == "linux":
|
||||
kill_process_children_unix(pid)
|
||||
else:
|
||||
pass # should signal error here
|
||||
pass # should signal error here
|
||||
|
||||
|
||||
def kill_program_completly(proc):
|
||||
@@ -127,9 +132,11 @@ def watchdog(sleep_interval):
|
||||
mtimes = {}
|
||||
worker_process = restart_with_reloader()
|
||||
signal.signal(
|
||||
signal.SIGTERM, lambda *args: kill_program_completly(worker_process))
|
||||
signal.SIGTERM, lambda *args: kill_program_completly(worker_process)
|
||||
)
|
||||
signal.signal(
|
||||
signal.SIGINT, lambda *args: kill_program_completly(worker_process))
|
||||
signal.SIGINT, lambda *args: kill_program_completly(worker_process)
|
||||
)
|
||||
while True:
|
||||
for filename in _iter_module_files():
|
||||
try:
|
||||
|
||||
199
sanic/request.py
199
sanic/request.py
@@ -1,24 +1,29 @@
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from cgi import parse_header
|
||||
from collections import namedtuple
|
||||
from http.cookies import SimpleCookie
|
||||
from httptools import parse_url
|
||||
from urllib.parse import parse_qs, urlunparse
|
||||
|
||||
from httptools import parse_url
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import error_logger, logger
|
||||
|
||||
|
||||
try:
|
||||
from ujson import loads as json_loads
|
||||
except ImportError:
|
||||
if sys.version_info[:2] == (3, 5):
|
||||
|
||||
def json_loads(data):
|
||||
# on Python 3.5 json.loads only supports str not bytes
|
||||
return json.loads(data.decode())
|
||||
|
||||
else:
|
||||
json_loads = json.loads
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import error_logger, logger
|
||||
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
@@ -44,11 +49,28 @@ class RequestParameters(dict):
|
||||
|
||||
class Request(dict):
|
||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||
|
||||
__slots__ = (
|
||||
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr',
|
||||
'_socket', '_port', '__weakref__', 'raw_url'
|
||||
"app",
|
||||
"headers",
|
||||
"version",
|
||||
"method",
|
||||
"_cookies",
|
||||
"transport",
|
||||
"body",
|
||||
"parsed_json",
|
||||
"parsed_args",
|
||||
"parsed_form",
|
||||
"parsed_files",
|
||||
"_ip",
|
||||
"_parsed_url",
|
||||
"uri_template",
|
||||
"stream",
|
||||
"_remote_addr",
|
||||
"_socket",
|
||||
"_port",
|
||||
"__weakref__",
|
||||
"raw_url",
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
@@ -63,7 +85,7 @@ class Request(dict):
|
||||
self.transport = transport
|
||||
|
||||
# Init but do not inhale
|
||||
self.body = []
|
||||
self.body_init()
|
||||
self.parsed_json = None
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
@@ -74,16 +96,25 @@ class Request(dict):
|
||||
|
||||
def __repr__(self):
|
||||
if self.method is None or not self.path:
|
||||
return '<{0}>'.format(self.__class__.__name__)
|
||||
return '<{0}: {1} {2}>'.format(self.__class__.__name__,
|
||||
self.method,
|
||||
self.path)
|
||||
return "<{0}>".format(self.__class__.__name__)
|
||||
return "<{0}: {1} {2}>".format(
|
||||
self.__class__.__name__, self.method, self.path
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
if self.transport:
|
||||
return True
|
||||
return False
|
||||
|
||||
def body_init(self):
|
||||
self.body = []
|
||||
|
||||
def body_push(self, data):
|
||||
self.body.append(data)
|
||||
|
||||
def body_finish(self):
|
||||
self.body = b"".join(self.body)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
if self.parsed_json is None:
|
||||
@@ -107,8 +138,8 @@ class Request(dict):
|
||||
|
||||
:return: token related to request
|
||||
"""
|
||||
prefixes = ('Bearer', 'Token')
|
||||
auth_header = self.headers.get('Authorization')
|
||||
prefixes = ("Bearer", "Token")
|
||||
auth_header = self.headers.get("Authorization")
|
||||
|
||||
if auth_header is not None:
|
||||
for prefix in prefixes:
|
||||
@@ -123,17 +154,20 @@ class Request(dict):
|
||||
self.parsed_form = RequestParameters()
|
||||
self.parsed_files = RequestParameters()
|
||||
content_type = self.headers.get(
|
||||
'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
|
||||
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
|
||||
)
|
||||
content_type, parameters = parse_header(content_type)
|
||||
try:
|
||||
if content_type == 'application/x-www-form-urlencoded':
|
||||
if content_type == "application/x-www-form-urlencoded":
|
||||
self.parsed_form = RequestParameters(
|
||||
parse_qs(self.body.decode('utf-8')))
|
||||
elif content_type == 'multipart/form-data':
|
||||
parse_qs(self.body.decode("utf-8"))
|
||||
)
|
||||
elif content_type == "multipart/form-data":
|
||||
# TODO: Stream this instead of reading to/from memory
|
||||
boundary = parameters['boundary'].encode('utf-8')
|
||||
self.parsed_form, self.parsed_files = (
|
||||
parse_multipart_form(self.body, boundary))
|
||||
boundary = parameters["boundary"].encode("utf-8")
|
||||
self.parsed_form, self.parsed_files = parse_multipart_form(
|
||||
self.body, boundary
|
||||
)
|
||||
except Exception:
|
||||
error_logger.exception("Failed when parsing form")
|
||||
|
||||
@@ -151,7 +185,8 @@ class Request(dict):
|
||||
if self.parsed_args is None:
|
||||
if self.query_string:
|
||||
self.parsed_args = RequestParameters(
|
||||
parse_qs(self.query_string))
|
||||
parse_qs(self.query_string)
|
||||
)
|
||||
else:
|
||||
self.parsed_args = RequestParameters()
|
||||
return self.parsed_args
|
||||
@@ -163,47 +198,42 @@ class Request(dict):
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
cookie = self.headers.get('Cookie')
|
||||
cookie = self.headers.get("Cookie")
|
||||
if cookie is not None:
|
||||
cookies = SimpleCookie()
|
||||
cookies.load(cookie)
|
||||
self._cookies = {name: cookie.value
|
||||
for name, cookie in cookies.items()}
|
||||
self._cookies = {
|
||||
name: cookie.value for name, cookie in cookies.items()
|
||||
}
|
||||
else:
|
||||
self._cookies = {}
|
||||
return self._cookies
|
||||
|
||||
@property
|
||||
def ip(self):
|
||||
if not hasattr(self, '_socket'):
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._ip
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
if not hasattr(self, '_socket'):
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._port
|
||||
|
||||
@property
|
||||
def socket(self):
|
||||
if not hasattr(self, '_socket'):
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._socket
|
||||
|
||||
def _get_address(self):
|
||||
sock = self.transport.get_extra_info('socket')
|
||||
|
||||
if sock.family == socket.AF_INET:
|
||||
self._socket = (self.transport.get_extra_info('peername') or
|
||||
(None, None))
|
||||
self._ip, self._port = self._socket
|
||||
elif sock.family == socket.AF_INET6:
|
||||
self._socket = (self.transport.get_extra_info('peername') or
|
||||
(None, None, None, None))
|
||||
self._ip, self._port, *_ = self._socket
|
||||
else:
|
||||
self._ip, self._port = (None, None)
|
||||
self._socket = self.transport.get_extra_info("peername") or (
|
||||
None,
|
||||
None,
|
||||
)
|
||||
self._ip = self._socket[0]
|
||||
self._port = self._socket[1]
|
||||
|
||||
@property
|
||||
def remote_addr(self):
|
||||
@@ -211,29 +241,31 @@ class Request(dict):
|
||||
|
||||
:return: original client ip.
|
||||
"""
|
||||
if not hasattr(self, '_remote_addr'):
|
||||
forwarded_for = self.headers.get('X-Forwarded-For', '').split(',')
|
||||
if not hasattr(self, "_remote_addr"):
|
||||
forwarded_for = self.headers.get("X-Forwarded-For", "").split(",")
|
||||
remote_addrs = [
|
||||
addr for addr in [
|
||||
addr.strip() for addr in forwarded_for
|
||||
] if addr
|
||||
]
|
||||
addr
|
||||
for addr in [addr.strip() for addr in forwarded_for]
|
||||
if addr
|
||||
]
|
||||
if len(remote_addrs) > 0:
|
||||
self._remote_addr = remote_addrs[0]
|
||||
else:
|
||||
self._remote_addr = ''
|
||||
self._remote_addr = ""
|
||||
return self._remote_addr
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
if self.app.websocket_enabled \
|
||||
and self.headers.get('upgrade') == 'websocket':
|
||||
scheme = 'ws'
|
||||
if (
|
||||
self.app.websocket_enabled
|
||||
and self.headers.get("upgrade") == "websocket"
|
||||
):
|
||||
scheme = "ws"
|
||||
else:
|
||||
scheme = 'http'
|
||||
scheme = "http"
|
||||
|
||||
if self.transport.get_extra_info('sslcontext'):
|
||||
scheme += 's'
|
||||
if self.transport.get_extra_info("sslcontext"):
|
||||
scheme += "s"
|
||||
|
||||
return scheme
|
||||
|
||||
@@ -241,11 +273,11 @@ class Request(dict):
|
||||
def host(self):
|
||||
# it appears that httptools doesn't return the host
|
||||
# so pull it from the headers
|
||||
return self.headers.get('Host', '')
|
||||
return self.headers.get("Host", "")
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return self.headers.get('Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
|
||||
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def match_info(self):
|
||||
@@ -254,27 +286,23 @@ class Request(dict):
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._parsed_url.path.decode('utf-8')
|
||||
return self._parsed_url.path.decode("utf-8")
|
||||
|
||||
@property
|
||||
def query_string(self):
|
||||
if self._parsed_url.query:
|
||||
return self._parsed_url.query.decode('utf-8')
|
||||
return self._parsed_url.query.decode("utf-8")
|
||||
else:
|
||||
return ''
|
||||
return ""
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return urlunparse((
|
||||
self.scheme,
|
||||
self.host,
|
||||
self.path,
|
||||
None,
|
||||
self.query_string,
|
||||
None))
|
||||
return urlunparse(
|
||||
(self.scheme, self.host, self.path, None, self.query_string, None)
|
||||
)
|
||||
|
||||
|
||||
File = namedtuple('File', ['type', 'body', 'name'])
|
||||
File = namedtuple("File", ["type", "body", "name"])
|
||||
|
||||
|
||||
def parse_multipart_form(body, boundary):
|
||||
@@ -290,37 +318,38 @@ def parse_multipart_form(body, boundary):
|
||||
form_parts = body.split(boundary)
|
||||
for form_part in form_parts[1:-1]:
|
||||
file_name = None
|
||||
content_type = 'text/plain'
|
||||
content_charset = 'utf-8'
|
||||
content_type = "text/plain"
|
||||
content_charset = "utf-8"
|
||||
field_name = None
|
||||
line_index = 2
|
||||
line_end_index = 0
|
||||
while not line_end_index == -1:
|
||||
line_end_index = form_part.find(b'\r\n', line_index)
|
||||
form_line = form_part[line_index:line_end_index].decode('utf-8')
|
||||
line_end_index = form_part.find(b"\r\n", line_index)
|
||||
form_line = form_part[line_index:line_end_index].decode("utf-8")
|
||||
line_index = line_end_index + 2
|
||||
|
||||
if not form_line:
|
||||
break
|
||||
|
||||
colon_index = form_line.index(':')
|
||||
colon_index = form_line.index(":")
|
||||
form_header_field = form_line[0:colon_index].lower()
|
||||
form_header_value, form_parameters = parse_header(
|
||||
form_line[colon_index + 2:])
|
||||
form_line[colon_index + 2 :]
|
||||
)
|
||||
|
||||
if form_header_field == 'content-disposition':
|
||||
file_name = form_parameters.get('filename')
|
||||
field_name = form_parameters.get('name')
|
||||
elif form_header_field == 'content-type':
|
||||
if form_header_field == "content-disposition":
|
||||
file_name = form_parameters.get("filename")
|
||||
field_name = form_parameters.get("name")
|
||||
elif form_header_field == "content-type":
|
||||
content_type = form_header_value
|
||||
content_charset = form_parameters.get('charset', 'utf-8')
|
||||
content_charset = form_parameters.get("charset", "utf-8")
|
||||
|
||||
if field_name:
|
||||
post_data = form_part[line_index:-4]
|
||||
if file_name:
|
||||
form_file = File(type=content_type,
|
||||
name=file_name,
|
||||
body=post_data)
|
||||
form_file = File(
|
||||
type=content_type, name=file_name, body=post_data
|
||||
)
|
||||
if field_name in files:
|
||||
files[field_name].append(form_file)
|
||||
else:
|
||||
@@ -332,7 +361,9 @@ def parse_multipart_form(body, boundary):
|
||||
else:
|
||||
fields[field_name] = [value]
|
||||
else:
|
||||
logger.debug('Form-data field does not have a \'name\' parameter \
|
||||
in the Content-Disposition header')
|
||||
logger.debug(
|
||||
"Form-data field does not have a 'name' parameter "
|
||||
"in the Content-Disposition header"
|
||||
)
|
||||
|
||||
return fields, files
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
from functools import partial
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except BaseException:
|
||||
from json import dumps as json_dumps
|
||||
|
||||
from aiofiles import open as open_async
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from sanic import http
|
||||
from sanic.cookies import CookieJar
|
||||
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
|
||||
|
||||
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except BaseException:
|
||||
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.
|
||||
json_dumps = partial(dumps, separators=(",", ":"))
|
||||
|
||||
|
||||
class BaseHTTPResponse:
|
||||
@@ -24,16 +30,18 @@ class BaseHTTPResponse:
|
||||
return str(data).encode()
|
||||
|
||||
def _parse_headers(self):
|
||||
headers = b''
|
||||
headers = b""
|
||||
for name, value in self.headers.items():
|
||||
try:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (
|
||||
name.encode(), value.encode('utf-8')))
|
||||
headers += b"%b: %b\r\n" % (
|
||||
name.encode(),
|
||||
value.encode("utf-8"),
|
||||
)
|
||||
except AttributeError:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (
|
||||
str(name).encode(), str(value).encode('utf-8')))
|
||||
headers += b"%b: %b\r\n" % (
|
||||
str(name).encode(),
|
||||
str(value).encode("utf-8"),
|
||||
)
|
||||
|
||||
return headers
|
||||
|
||||
@@ -46,12 +54,17 @@ class BaseHTTPResponse:
|
||||
|
||||
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = (
|
||||
'protocol', 'streaming_fn', 'status',
|
||||
'content_type', 'headers', '_cookies'
|
||||
"protocol",
|
||||
"streaming_fn",
|
||||
"status",
|
||||
"content_type",
|
||||
"headers",
|
||||
"_cookies",
|
||||
)
|
||||
|
||||
def __init__(self, streaming_fn, status=200, headers=None,
|
||||
content_type='text/plain'):
|
||||
def __init__(
|
||||
self, streaming_fn, status=200, headers=None, content_type="text/plain"
|
||||
):
|
||||
self.content_type = content_type
|
||||
self.streaming_fn = streaming_fn
|
||||
self.status = status
|
||||
@@ -66,61 +79,69 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
if type(data) != bytes:
|
||||
data = self._encode_body(data)
|
||||
|
||||
self.protocol.push_data(
|
||||
b"%x\r\n%b\r\n" % (len(data), data))
|
||||
self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
||||
await self.protocol.drain()
|
||||
|
||||
async def stream(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None
|
||||
):
|
||||
"""Streams headers, runs the `streaming_fn` callback that writes
|
||||
content to the response body, then finalizes the response body.
|
||||
"""
|
||||
headers = self.get_headers(
|
||||
version, keep_alive=keep_alive,
|
||||
keep_alive_timeout=keep_alive_timeout)
|
||||
version,
|
||||
keep_alive=keep_alive,
|
||||
keep_alive_timeout=keep_alive_timeout,
|
||||
)
|
||||
self.protocol.push_data(headers)
|
||||
await self.protocol.drain()
|
||||
await self.streaming_fn(self)
|
||||
self.protocol.push_data(b'0\r\n\r\n')
|
||||
self.protocol.push_data(b"0\r\n\r\n")
|
||||
# no need to await drain here after this write, because it is the
|
||||
# very last thing we write and nothing needs to wait for it.
|
||||
|
||||
def get_headers(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None
|
||||
):
|
||||
# This is all returned in a kind-of funky way
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
timeout_header = b""
|
||||
if keep_alive and keep_alive_timeout is not None:
|
||||
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
|
||||
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
|
||||
|
||||
self.headers['Transfer-Encoding'] = 'chunked'
|
||||
self.headers.pop('Content-Length', None)
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
self.headers["Transfer-Encoding"] = "chunked"
|
||||
self.headers.pop("Content-Length", None)
|
||||
self.headers["Content-Type"] = self.headers.get(
|
||||
"Content-Type", self.content_type
|
||||
)
|
||||
|
||||
headers = self._parse_headers()
|
||||
|
||||
if self.status is 200:
|
||||
status = b'OK'
|
||||
if self.status == 200:
|
||||
status = b"OK"
|
||||
else:
|
||||
status = http.STATUS_CODES.get(self.status)
|
||||
status = STATUS_CODES.get(self.status)
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'%b'
|
||||
b'%b\r\n') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
timeout_header,
|
||||
headers
|
||||
)
|
||||
return (b"HTTP/%b %d %b\r\n" b"%b" b"%b\r\n") % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
timeout_header,
|
||||
headers,
|
||||
)
|
||||
|
||||
|
||||
class HTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||
__slots__ = ("body", "status", "content_type", "headers", "_cookies")
|
||||
|
||||
def __init__(self, body=None, status=200, headers=None,
|
||||
content_type='text/plain', body_bytes=b''):
|
||||
def __init__(
|
||||
self,
|
||||
body=None,
|
||||
status=200,
|
||||
headers=None,
|
||||
content_type="text/plain",
|
||||
body_bytes=b"",
|
||||
):
|
||||
self.content_type = content_type
|
||||
|
||||
if body is not None:
|
||||
@@ -132,46 +153,45 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
self.headers = CIMultiDict(headers or {})
|
||||
self._cookies = None
|
||||
|
||||
def output(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
# This is all returned in a kind-of funky way
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
timeout_header = b""
|
||||
if keep_alive and keep_alive_timeout is not None:
|
||||
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
|
||||
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
|
||||
|
||||
body = b''
|
||||
if http.has_message_body(self.status):
|
||||
body = b""
|
||||
if has_message_body(self.status):
|
||||
body = self.body
|
||||
self.headers['Content-Length'] = self.headers.get(
|
||||
'Content-Length', len(self.body))
|
||||
self.headers["Content-Length"] = self.headers.get(
|
||||
"Content-Length", len(self.body)
|
||||
)
|
||||
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
self.headers["Content-Type"] = self.headers.get(
|
||||
"Content-Type", self.content_type
|
||||
)
|
||||
|
||||
if self.status in (304, 412):
|
||||
self.headers = http.remove_entity_headers(self.headers)
|
||||
self.headers = remove_entity_headers(self.headers)
|
||||
|
||||
headers = self._parse_headers()
|
||||
|
||||
if self.status is 200:
|
||||
status = b'OK'
|
||||
if self.status == 200:
|
||||
status = b"OK"
|
||||
else:
|
||||
status = http.STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
|
||||
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'Connection: %b\r\n'
|
||||
b'%b'
|
||||
b'%b\r\n'
|
||||
b'%b') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
b'keep-alive' if keep_alive else b'close',
|
||||
timeout_header,
|
||||
headers,
|
||||
body
|
||||
)
|
||||
return (
|
||||
b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b"
|
||||
) % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
b"keep-alive" if keep_alive else b"close",
|
||||
timeout_header,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
@@ -180,9 +200,14 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
return self._cookies
|
||||
|
||||
|
||||
def json(body, status=200, headers=None,
|
||||
content_type="application/json", dumps=json_dumps,
|
||||
**kwargs):
|
||||
def json(
|
||||
body,
|
||||
status=200,
|
||||
headers=None,
|
||||
content_type="application/json",
|
||||
dumps=json_dumps,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Returns response object with body in json format.
|
||||
|
||||
@@ -191,12 +216,17 @@ def json(body, status=200, headers=None,
|
||||
:param headers: Custom Headers.
|
||||
:param kwargs: Remaining arguments that are passed to the json encoder.
|
||||
"""
|
||||
return HTTPResponse(dumps(body, **kwargs), headers=headers,
|
||||
status=status, content_type=content_type)
|
||||
return HTTPResponse(
|
||||
dumps(body, **kwargs),
|
||||
headers=headers,
|
||||
status=status,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
def text(body, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
def text(
|
||||
body, status=200, headers=None, content_type="text/plain; charset=utf-8"
|
||||
):
|
||||
"""
|
||||
Returns response object with body in text format.
|
||||
|
||||
@@ -206,12 +236,13 @@ def text(body, status=200, headers=None,
|
||||
:param content_type: the content type (string) of the response
|
||||
"""
|
||||
return HTTPResponse(
|
||||
body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
body, status=status, headers=headers, content_type=content_type
|
||||
)
|
||||
|
||||
|
||||
def raw(body, status=200, headers=None,
|
||||
content_type="application/octet-stream"):
|
||||
def raw(
|
||||
body, status=200, headers=None, content_type="application/octet-stream"
|
||||
):
|
||||
"""
|
||||
Returns response object without encoding the body.
|
||||
|
||||
@@ -220,8 +251,12 @@ def raw(body, status=200, headers=None,
|
||||
:param headers: Custom Headers.
|
||||
:param content_type: the content type (string) of the response.
|
||||
"""
|
||||
return HTTPResponse(body_bytes=body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
return HTTPResponse(
|
||||
body_bytes=body,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
def html(body, status=200, headers=None):
|
||||
@@ -232,12 +267,22 @@ def html(body, status=200, headers=None):
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/html; charset=utf-8")
|
||||
return HTTPResponse(
|
||||
body,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type="text/html; charset=utf-8",
|
||||
)
|
||||
|
||||
|
||||
async def file(location, status=200, mime_type=None, headers=None,
|
||||
filename=None, _range=None):
|
||||
async def file(
|
||||
location,
|
||||
status=200,
|
||||
mime_type=None,
|
||||
headers=None,
|
||||
filename=None,
|
||||
_range=None,
|
||||
):
|
||||
"""Return a response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
@@ -249,28 +294,41 @@ async def file(location, status=200, mime_type=None, headers=None,
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(filename))
|
||||
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
|
||||
)
|
||||
filename = filename or path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
async with open_async(location, mode="rb") as _file:
|
||||
if _range:
|
||||
await _file.seek(_range.start)
|
||||
out_stream = await _file.read(_range.size)
|
||||
headers['Content-Range'] = 'bytes %s-%s/%s' % (
|
||||
_range.start, _range.end, _range.total)
|
||||
headers["Content-Range"] = "bytes %s-%s/%s" % (
|
||||
_range.start,
|
||||
_range.end,
|
||||
_range.total,
|
||||
)
|
||||
status = 206
|
||||
else:
|
||||
out_stream = await _file.read()
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
return HTTPResponse(status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream)
|
||||
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
|
||||
return HTTPResponse(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream,
|
||||
)
|
||||
|
||||
|
||||
async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
|
||||
headers=None, filename=None, _range=None):
|
||||
async def file_stream(
|
||||
location,
|
||||
status=200,
|
||||
chunk_size=4096,
|
||||
mime_type=None,
|
||||
headers=None,
|
||||
filename=None,
|
||||
_range=None,
|
||||
):
|
||||
"""Return a streaming response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
@@ -283,11 +341,11 @@ async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(filename))
|
||||
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
|
||||
)
|
||||
filename = filename or path.split(location)[-1]
|
||||
|
||||
_file = await open_async(location, mode='rb')
|
||||
_file = await open_async(location, mode="rb")
|
||||
|
||||
async def _streaming_fn(response):
|
||||
nonlocal _file, chunk_size
|
||||
@@ -312,19 +370,28 @@ async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
|
||||
await _file.close()
|
||||
return # Returning from this fn closes the stream
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
|
||||
if _range:
|
||||
headers['Content-Range'] = 'bytes %s-%s/%s' % (
|
||||
_range.start, _range.end, _range.total)
|
||||
return StreamingHTTPResponse(streaming_fn=_streaming_fn,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type)
|
||||
headers["Content-Range"] = "bytes %s-%s/%s" % (
|
||||
_range.start,
|
||||
_range.end,
|
||||
_range.total,
|
||||
)
|
||||
status = 206
|
||||
return StreamingHTTPResponse(
|
||||
streaming_fn=_streaming_fn,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
)
|
||||
|
||||
|
||||
def stream(
|
||||
streaming_fn, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
streaming_fn,
|
||||
status=200,
|
||||
headers=None,
|
||||
content_type="text/plain; charset=utf-8",
|
||||
):
|
||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||
|
||||
@@ -344,15 +411,13 @@ def stream(
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return StreamingHTTPResponse(
|
||||
streaming_fn,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
status=status
|
||||
streaming_fn, headers=headers, content_type=content_type, status=status
|
||||
)
|
||||
|
||||
|
||||
def redirect(to, headers=None, status=302,
|
||||
content_type="text/html; charset=utf-8"):
|
||||
def redirect(
|
||||
to, headers=None, status=302, content_type="text/html; charset=utf-8"
|
||||
):
|
||||
"""Abort execution and cause a 302 redirect (by default).
|
||||
|
||||
:param to: path or fully qualified URL to redirect to
|
||||
@@ -364,12 +429,11 @@ def redirect(to, headers=None, status=302,
|
||||
headers = headers or {}
|
||||
|
||||
# URL Quote the URL before redirecting
|
||||
safe_to = quote_plus(to, safe=":/#?&=@[]!$&'()*+,;")
|
||||
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
|
||||
|
||||
# According to RFC 7231, a relative URI is now permitted.
|
||||
headers['Location'] = safe_to
|
||||
headers["Location"] = safe_to
|
||||
|
||||
return HTTPResponse(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type)
|
||||
status=status, headers=headers, content_type=content_type
|
||||
)
|
||||
|
||||
197
sanic/router.py
197
sanic/router.py
@@ -1,33 +1,38 @@
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections.abc import Iterable
|
||||
from functools import lru_cache
|
||||
from urllib.parse import unquote
|
||||
|
||||
from sanic.exceptions import NotFound, MethodNotSupported
|
||||
from sanic.exceptions import MethodNotSupported, NotFound
|
||||
from sanic.views import CompositionView
|
||||
|
||||
|
||||
Route = namedtuple(
|
||||
'Route',
|
||||
['handler', 'methods', 'pattern', 'parameters', 'name', 'uri'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
"Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
|
||||
)
|
||||
Parameter = namedtuple("Parameter", ["name", "cast"])
|
||||
|
||||
REGEX_TYPES = {
|
||||
'string': (str, r'[^/]+'),
|
||||
'int': (int, r'\d+'),
|
||||
'number': (float, r'[0-9\\.]+'),
|
||||
'alpha': (str, r'[A-Za-z]+'),
|
||||
'path': (str, r'[^/].*?'),
|
||||
'uuid': (uuid.UUID, r'[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-'
|
||||
r'[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}')
|
||||
"string": (str, r"[^/]+"),
|
||||
"int": (int, r"\d+"),
|
||||
"number": (float, r"[0-9\\.]+"),
|
||||
"alpha": (str, r"[A-Za-z]+"),
|
||||
"path": (str, r"[^/].*?"),
|
||||
"uuid": (
|
||||
uuid.UUID,
|
||||
r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-"
|
||||
r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}",
|
||||
),
|
||||
}
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
|
||||
|
||||
def url_hash(url):
|
||||
return url.count('/')
|
||||
return url.count("/")
|
||||
|
||||
|
||||
class RouteExists(Exception):
|
||||
@@ -38,6 +43,10 @@ class RouteDoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ParameterNameConflicts(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Router:
|
||||
"""Router supports basic routing with parameters and method checks
|
||||
|
||||
@@ -64,10 +73,11 @@ class Router:
|
||||
also be passed in as the type. The argument given to the function will
|
||||
always be a string, independent of the type.
|
||||
"""
|
||||
|
||||
routes_static = None
|
||||
routes_dynamic = None
|
||||
routes_always_check = None
|
||||
parameter_pattern = re.compile(r'<(.+?)>')
|
||||
parameter_pattern = re.compile(r"<(.+?)>")
|
||||
|
||||
def __init__(self):
|
||||
self.routes_all = {}
|
||||
@@ -94,9 +104,9 @@ class Router:
|
||||
"""
|
||||
# We could receive NAME or NAME:PATTERN
|
||||
name = parameter_string
|
||||
pattern = 'string'
|
||||
if ':' in parameter_string:
|
||||
name, pattern = parameter_string.split(':', 1)
|
||||
pattern = "string"
|
||||
if ":" in parameter_string:
|
||||
name, pattern = parameter_string.split(":", 1)
|
||||
if not name:
|
||||
raise ValueError(
|
||||
"Invalid parameter syntax: {}".format(parameter_string)
|
||||
@@ -108,8 +118,16 @@ class Router:
|
||||
|
||||
return name, _type, pattern
|
||||
|
||||
def add(self, uri, methods, handler, host=None, strict_slashes=False,
|
||||
version=None, name=None):
|
||||
def add(
|
||||
self,
|
||||
uri,
|
||||
methods,
|
||||
handler,
|
||||
host=None,
|
||||
strict_slashes=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: path to match
|
||||
@@ -123,8 +141,8 @@ class Router:
|
||||
:return: Nothing
|
||||
"""
|
||||
if version is not None:
|
||||
version = re.escape(str(version).strip('/').lstrip('v'))
|
||||
uri = "/".join(["/v{}".format(version), uri.lstrip('/')])
|
||||
version = re.escape(str(version).strip("/").lstrip("v"))
|
||||
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host, name)
|
||||
|
||||
@@ -139,28 +157,26 @@ class Router:
|
||||
return
|
||||
|
||||
# Add versions with and without trailing /
|
||||
slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
|
||||
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
|
||||
unslashed_methods = self.routes_all.get(uri[:-1], frozenset({}))
|
||||
if isinstance(methods, Iterable):
|
||||
_slash_is_missing = all(method in slashed_methods for
|
||||
method in methods)
|
||||
_without_slash_is_missing = all(method in unslashed_methods for
|
||||
method in methods)
|
||||
_slash_is_missing = all(
|
||||
method in slashed_methods for method in methods
|
||||
)
|
||||
_without_slash_is_missing = all(
|
||||
method in unslashed_methods for method in methods
|
||||
)
|
||||
else:
|
||||
_slash_is_missing = methods in slashed_methods
|
||||
_without_slash_is_missing = methods in unslashed_methods
|
||||
|
||||
slash_is_missing = (
|
||||
not uri[-1] == '/' and not _slash_is_missing
|
||||
)
|
||||
slash_is_missing = not uri[-1] == "/" and not _slash_is_missing
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == '/' and not
|
||||
_without_slash_is_missing and not
|
||||
uri == '/'
|
||||
uri[-1] == "/" and not _without_slash_is_missing and not uri == "/"
|
||||
)
|
||||
# add version with trailing slash
|
||||
if slash_is_missing:
|
||||
self._add(uri + '/', methods, handler, host, name)
|
||||
self._add(uri + "/", methods, handler, host, name)
|
||||
# add version without trailing slash
|
||||
elif without_slash_is_missing:
|
||||
self._add(uri[:-1], methods, handler, host, name)
|
||||
@@ -183,8 +199,10 @@ class Router:
|
||||
|
||||
else:
|
||||
if not isinstance(host, Iterable):
|
||||
raise ValueError("Expected either string or Iterable of "
|
||||
"host strings, not {!r}".format(host))
|
||||
raise ValueError(
|
||||
"Expected either string or Iterable of "
|
||||
"host strings, not {!r}".format(host)
|
||||
)
|
||||
|
||||
for host_ in host:
|
||||
self.add(uri, methods, handler, host_, name)
|
||||
@@ -195,40 +213,48 @@ class Router:
|
||||
methods = frozenset(methods)
|
||||
|
||||
parameters = []
|
||||
parameter_names = set()
|
||||
properties = {"unhashable": None}
|
||||
|
||||
def add_parameter(match):
|
||||
name = match.group(1)
|
||||
name, _type, pattern = self.parse_parameter_string(name)
|
||||
|
||||
parameter = Parameter(
|
||||
name=name, cast=_type)
|
||||
if name in parameter_names:
|
||||
raise ParameterNameConflicts(
|
||||
"Multiple parameter named <{name}> "
|
||||
"in route uri {uri}".format(name=name, uri=uri)
|
||||
)
|
||||
parameter_names.add(name)
|
||||
|
||||
parameter = Parameter(name=name, cast=_type)
|
||||
parameters.append(parameter)
|
||||
|
||||
# Mark the whole route as unhashable if it has the hash key in it
|
||||
if re.search(r'(^|[^^]){1}/', pattern):
|
||||
properties['unhashable'] = True
|
||||
if re.search(r"(^|[^^]){1}/", pattern):
|
||||
properties["unhashable"] = True
|
||||
# Mark the route as unhashable if it matches the hash key
|
||||
elif re.search(r'/', pattern):
|
||||
properties['unhashable'] = True
|
||||
elif re.search(r"/", pattern):
|
||||
properties["unhashable"] = True
|
||||
|
||||
return '({})'.format(pattern)
|
||||
return "({})".format(pattern)
|
||||
|
||||
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
|
||||
pattern = re.compile(r'^{}$'.format(pattern_string))
|
||||
pattern = re.compile(r"^{}$".format(pattern_string))
|
||||
|
||||
def merge_route(route, methods, handler):
|
||||
# merge to the existing route when possible.
|
||||
if not route.methods or not methods:
|
||||
# method-unspecified routes are not mergeable.
|
||||
raise RouteExists(
|
||||
"Route already registered: {}".format(uri))
|
||||
raise RouteExists("Route already registered: {}".format(uri))
|
||||
elif route.methods.intersection(methods):
|
||||
# already existing method is not overloadable.
|
||||
duplicated = methods.intersection(route.methods)
|
||||
raise RouteExists(
|
||||
"Route already registered: {} [{}]".format(
|
||||
uri, ','.join(list(duplicated))))
|
||||
uri, ",".join(list(duplicated))
|
||||
)
|
||||
)
|
||||
if isinstance(route.handler, CompositionView):
|
||||
view = route.handler
|
||||
else:
|
||||
@@ -236,19 +262,22 @@ class Router:
|
||||
view.add(route.methods, route.handler)
|
||||
view.add(methods, handler)
|
||||
route = route._replace(
|
||||
handler=view, methods=methods.union(route.methods))
|
||||
handler=view, methods=methods.union(route.methods)
|
||||
)
|
||||
return route
|
||||
|
||||
if parameters:
|
||||
# TODO: This is too complex, we need to reduce the complexity
|
||||
if properties['unhashable']:
|
||||
if properties["unhashable"]:
|
||||
routes_to_check = self.routes_always_check
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check, parameters)
|
||||
pattern, routes_to_check, parameters
|
||||
)
|
||||
else:
|
||||
routes_to_check = self.routes_dynamic[url_hash(uri)]
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check, parameters)
|
||||
pattern, routes_to_check, parameters
|
||||
)
|
||||
if ndx != -1:
|
||||
# Pop the ndx of the route, no dups of the same route
|
||||
routes_to_check.pop(ndx)
|
||||
@@ -259,35 +288,41 @@ class Router:
|
||||
# if available
|
||||
# special prefix for static files
|
||||
is_static = False
|
||||
if name and name.startswith('_static_'):
|
||||
if name and name.startswith("_static_"):
|
||||
is_static = True
|
||||
name = name.split('_static_', 1)[-1]
|
||||
name = name.split("_static_", 1)[-1]
|
||||
|
||||
if hasattr(handler, '__blueprintname__'):
|
||||
handler_name = '{}.{}'.format(
|
||||
handler.__blueprintname__, name or handler.__name__)
|
||||
if hasattr(handler, "__blueprintname__"):
|
||||
handler_name = "{}.{}".format(
|
||||
handler.__blueprintname__, name or handler.__name__
|
||||
)
|
||||
else:
|
||||
handler_name = name or getattr(handler, '__name__', None)
|
||||
handler_name = name or getattr(handler, "__name__", None)
|
||||
|
||||
if route:
|
||||
route = merge_route(route, methods, handler)
|
||||
else:
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters, name=handler_name, uri=uri)
|
||||
handler=handler,
|
||||
methods=methods,
|
||||
pattern=pattern,
|
||||
parameters=parameters,
|
||||
name=handler_name,
|
||||
uri=uri,
|
||||
)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if is_static:
|
||||
pair = self.routes_static_files.get(handler_name)
|
||||
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
|
||||
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
|
||||
self.routes_static_files[handler_name] = (uri, route)
|
||||
|
||||
else:
|
||||
pair = self.routes_names.get(handler_name)
|
||||
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
|
||||
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
|
||||
self.routes_names[handler_name] = (uri, route)
|
||||
|
||||
if properties['unhashable']:
|
||||
if properties["unhashable"]:
|
||||
self.routes_always_check.append(route)
|
||||
elif parameters:
|
||||
self.routes_dynamic[url_hash(uri)].append(route)
|
||||
@@ -322,8 +357,10 @@ class Router:
|
||||
|
||||
if route in self.routes_always_check:
|
||||
self.routes_always_check.remove(route)
|
||||
elif url_hash(uri) in self.routes_dynamic \
|
||||
and route in self.routes_dynamic[url_hash(uri)]:
|
||||
elif (
|
||||
url_hash(uri) in self.routes_dynamic
|
||||
and route in self.routes_dynamic[url_hash(uri)]
|
||||
):
|
||||
self.routes_dynamic[url_hash(uri)].remove(route)
|
||||
else:
|
||||
self.routes_static.pop(uri)
|
||||
@@ -342,7 +379,7 @@ class Router:
|
||||
if not view_name:
|
||||
return (None, None)
|
||||
|
||||
if view_name == 'static' or view_name.endswith('.static'):
|
||||
if view_name == "static" or view_name.endswith(".static"):
|
||||
return self.routes_static_files.get(name, (None, None))
|
||||
|
||||
return self.routes_names.get(view_name, (None, None))
|
||||
@@ -356,14 +393,15 @@ class Router:
|
||||
"""
|
||||
# No virtual hosts specified; default behavior
|
||||
if not self.hosts:
|
||||
return self._get(request.path, request.method, '')
|
||||
return self._get(request.path, request.method, "")
|
||||
# virtual hosts specified; try to match route to the host header
|
||||
try:
|
||||
return self._get(request.path, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
return self._get(
|
||||
request.path, request.method, request.headers.get("Host", "")
|
||||
)
|
||||
# try default hosts
|
||||
except NotFound:
|
||||
return self._get(request.path, request.method, '')
|
||||
return self._get(request.path, request.method, "")
|
||||
|
||||
def get_supported_methods(self, url):
|
||||
"""Get a list of supported methods for a url and optional host.
|
||||
@@ -373,7 +411,7 @@ class Router:
|
||||
"""
|
||||
route = self.routes_all.get(url)
|
||||
# if methods are None then this logic will prevent an error
|
||||
return getattr(route, 'methods', None) or frozenset()
|
||||
return getattr(route, "methods", None) or frozenset()
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(self, url, method, host):
|
||||
@@ -388,9 +426,10 @@ class Router:
|
||||
# Check against known static routes
|
||||
route = self.routes_static.get(url)
|
||||
method_not_supported = MethodNotSupported(
|
||||
'Method {} not allowed for URL {}'.format(method, url),
|
||||
"Method {} not allowed for URL {}".format(method, url),
|
||||
method=method,
|
||||
allowed_methods=self.get_supported_methods(url))
|
||||
allowed_methods=self.get_supported_methods(url),
|
||||
)
|
||||
if route:
|
||||
if route.methods and method not in route.methods:
|
||||
raise method_not_supported
|
||||
@@ -416,13 +455,14 @@ class Router:
|
||||
# Route was found but the methods didn't match
|
||||
if route_found:
|
||||
raise method_not_supported
|
||||
raise NotFound('Requested URL {} not found'.format(url))
|
||||
raise NotFound("Requested URL {} not found".format(url))
|
||||
|
||||
kwargs = {p.name: p.cast(value)
|
||||
for value, p
|
||||
in zip(match.groups(1), route.parameters)}
|
||||
kwargs = {
|
||||
p.name: p.cast(value)
|
||||
for value, p in zip(match.groups(1), route.parameters)
|
||||
}
|
||||
route_handler = route.handler
|
||||
if hasattr(route_handler, 'handlers'):
|
||||
if hasattr(route_handler, "handlers"):
|
||||
route_handler = route_handler.handlers[method]
|
||||
return route_handler, [], kwargs, route.uri
|
||||
|
||||
@@ -435,7 +475,8 @@ class Router:
|
||||
handler = self.get(request)[0]
|
||||
except (NotFound, MethodNotSupported):
|
||||
return False
|
||||
if (hasattr(handler, 'view_class') and
|
||||
hasattr(handler.view_class, request.method.lower())):
|
||||
if hasattr(handler, "view_class") and hasattr(
|
||||
handler.view_class, request.method.lower()
|
||||
):
|
||||
handler = getattr(handler.view_class, request.method.lower())
|
||||
return hasattr(handler, 'is_stream')
|
||||
return hasattr(handler, "is_stream")
|
||||
|
||||
415
sanic/server.py
415
sanic/server.py
@@ -1,40 +1,38 @@
|
||||
import asyncio
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from multiprocessing import Process
|
||||
from signal import (
|
||||
SIGTERM, SIGINT, SIG_IGN,
|
||||
signal as signal_func,
|
||||
Signals
|
||||
)
|
||||
from socket import (
|
||||
socket,
|
||||
SOL_SOCKET,
|
||||
SO_REUSEADDR,
|
||||
)
|
||||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||
from signal import signal as signal_func
|
||||
from socket import SO_REUSEADDR, SOL_SOCKET, socket
|
||||
from time import time
|
||||
|
||||
from httptools import HttpRequestParser
|
||||
from httptools.parser.errors import HttpParserError
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from sanic.exceptions import (
|
||||
InvalidUsage,
|
||||
PayloadTooLarge,
|
||||
RequestTimeout,
|
||||
ServerError,
|
||||
ServiceUnavailable,
|
||||
)
|
||||
from sanic.log import access_logger, logger
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse
|
||||
|
||||
|
||||
try:
|
||||
import uvloop
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from sanic.log import logger, access_logger
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.request import Request
|
||||
from sanic.exceptions import (
|
||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError,
|
||||
ServiceUnavailable)
|
||||
|
||||
current_time = None
|
||||
|
||||
|
||||
class Signal:
|
||||
stopped = False
|
||||
@@ -43,27 +41,65 @@ class Signal:
|
||||
class HttpProtocol(asyncio.Protocol):
|
||||
__slots__ = (
|
||||
# event loop, connection
|
||||
'loop', 'transport', 'connections', 'signal',
|
||||
"loop",
|
||||
"transport",
|
||||
"connections",
|
||||
"signal",
|
||||
# request params
|
||||
'parser', 'request', 'url', 'headers',
|
||||
"parser",
|
||||
"request",
|
||||
"url",
|
||||
"headers",
|
||||
# request config
|
||||
'request_handler', 'request_timeout', 'response_timeout',
|
||||
'keep_alive_timeout', 'request_max_size', 'request_class',
|
||||
'is_request_stream', 'router',
|
||||
"request_handler",
|
||||
"request_timeout",
|
||||
"response_timeout",
|
||||
"keep_alive_timeout",
|
||||
"request_max_size",
|
||||
"request_class",
|
||||
"is_request_stream",
|
||||
"router",
|
||||
"error_handler",
|
||||
# enable or disable access log purpose
|
||||
'access_log',
|
||||
"access_log",
|
||||
# connection management
|
||||
'_total_request_size', '_request_timeout_handler',
|
||||
'_response_timeout_handler', '_keep_alive_timeout_handler',
|
||||
'_last_request_time', '_last_response_time', '_is_stream_handler',
|
||||
'_not_paused')
|
||||
"_total_request_size",
|
||||
"_request_timeout_handler",
|
||||
"_response_timeout_handler",
|
||||
"_keep_alive_timeout_handler",
|
||||
"_last_request_time",
|
||||
"_last_response_time",
|
||||
"_is_stream_handler",
|
||||
"_not_paused",
|
||||
"_request_handler_task",
|
||||
"_request_stream_task",
|
||||
"_keep_alive",
|
||||
"_header_fragment",
|
||||
"state",
|
||||
"_debug",
|
||||
)
|
||||
|
||||
def __init__(self, *, loop, request_handler, error_handler,
|
||||
signal=Signal(), connections=set(), request_timeout=60,
|
||||
response_timeout=60, keep_alive_timeout=5,
|
||||
request_max_size=None, request_class=None, access_log=True,
|
||||
keep_alive=True, is_request_stream=False, router=None,
|
||||
state=None, debug=False, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
loop,
|
||||
request_handler,
|
||||
error_handler,
|
||||
signal=Signal(),
|
||||
connections=set(),
|
||||
request_timeout=60,
|
||||
response_timeout=60,
|
||||
keep_alive_timeout=5,
|
||||
request_max_size=None,
|
||||
request_class=None,
|
||||
access_log=True,
|
||||
keep_alive=True,
|
||||
is_request_stream=False,
|
||||
router=None,
|
||||
state=None,
|
||||
debug=False,
|
||||
**kwargs
|
||||
):
|
||||
self.loop = loop
|
||||
self.transport = None
|
||||
self.request = None
|
||||
@@ -93,19 +129,20 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._request_handler_task = None
|
||||
self._request_stream_task = None
|
||||
self._keep_alive = keep_alive
|
||||
self._header_fragment = b''
|
||||
self._header_fragment = b""
|
||||
self.state = state if state else {}
|
||||
if 'requests_count' not in self.state:
|
||||
self.state['requests_count'] = 0
|
||||
if "requests_count" not in self.state:
|
||||
self.state["requests_count"] = 0
|
||||
self._debug = debug
|
||||
self._not_paused.set()
|
||||
|
||||
@property
|
||||
def keep_alive(self):
|
||||
return (
|
||||
self._keep_alive and
|
||||
not self.signal.stopped and
|
||||
self.parser.should_keep_alive())
|
||||
self._keep_alive
|
||||
and not self.signal.stopped
|
||||
and self.parser.should_keep_alive()
|
||||
)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Connection
|
||||
@@ -114,12 +151,17 @@ class HttpProtocol(asyncio.Protocol):
|
||||
def connection_made(self, transport):
|
||||
self.connections.add(self)
|
||||
self._request_timeout_handler = self.loop.call_later(
|
||||
self.request_timeout, self.request_timeout_callback)
|
||||
self.request_timeout, self.request_timeout_callback
|
||||
)
|
||||
self.transport = transport
|
||||
self._last_request_time = current_time
|
||||
self._last_request_time = time()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.connections.discard(self)
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
if self._request_timeout_handler:
|
||||
self._request_timeout_handler.cancel()
|
||||
if self._response_timeout_handler:
|
||||
@@ -138,55 +180,46 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# exactly what this timeout is checking for.
|
||||
# Check if elapsed time since request initiated exceeds our
|
||||
# configured maximum request timeout value
|
||||
time_elapsed = current_time - self._last_request_time
|
||||
time_elapsed = time() - self._last_request_time
|
||||
if time_elapsed < self.request_timeout:
|
||||
time_left = self.request_timeout - time_elapsed
|
||||
self._request_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.request_timeout_callback)
|
||||
self._request_timeout_handler = self.loop.call_later(
|
||||
time_left, self.request_timeout_callback
|
||||
)
|
||||
else:
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
try:
|
||||
raise RequestTimeout('Request Timeout')
|
||||
except RequestTimeout as exception:
|
||||
self.write_error(exception)
|
||||
self.write_error(RequestTimeout("Request Timeout"))
|
||||
|
||||
def response_timeout_callback(self):
|
||||
# Check if elapsed time since response was initiated exceeds our
|
||||
# configured maximum request timeout value
|
||||
time_elapsed = current_time - self._last_request_time
|
||||
time_elapsed = time() - self._last_request_time
|
||||
if time_elapsed < self.response_timeout:
|
||||
time_left = self.response_timeout - time_elapsed
|
||||
self._response_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.response_timeout_callback)
|
||||
self._response_timeout_handler = self.loop.call_later(
|
||||
time_left, self.response_timeout_callback
|
||||
)
|
||||
else:
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
try:
|
||||
raise ServiceUnavailable('Response Timeout')
|
||||
except ServiceUnavailable as exception:
|
||||
self.write_error(exception)
|
||||
self.write_error(ServiceUnavailable("Response Timeout"))
|
||||
|
||||
def keep_alive_timeout_callback(self):
|
||||
# Check if elapsed time since last response exceeds our configured
|
||||
# maximum keep alive timeout value
|
||||
time_elapsed = current_time - self._last_response_time
|
||||
time_elapsed = time() - self._last_response_time
|
||||
if time_elapsed < self.keep_alive_timeout:
|
||||
time_left = self.keep_alive_timeout - time_elapsed
|
||||
self._keep_alive_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
time_left, self.keep_alive_timeout_callback
|
||||
)
|
||||
else:
|
||||
logger.debug('KeepAlive Timeout. Closing connection.')
|
||||
logger.debug("KeepAlive Timeout. Closing connection.")
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
|
||||
@@ -199,8 +232,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# memory limits
|
||||
self._total_request_size += len(data)
|
||||
if self._total_request_size > self.request_max_size:
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
self.write_error(PayloadTooLarge("Payload Too Large"))
|
||||
|
||||
# Create parser if this is the first time we're receiving data
|
||||
if self.parser is None:
|
||||
@@ -209,17 +241,16 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.parser = HttpRequestParser(self)
|
||||
|
||||
# requests count
|
||||
self.state['requests_count'] = self.state['requests_count'] + 1
|
||||
self.state["requests_count"] = self.state["requests_count"] + 1
|
||||
|
||||
# Parse request chunk or close connection
|
||||
try:
|
||||
self.parser.feed_data(data)
|
||||
except HttpParserError:
|
||||
message = 'Bad Request'
|
||||
message = "Bad Request"
|
||||
if self._debug:
|
||||
message += '\n' + traceback.format_exc()
|
||||
exception = InvalidUsage(message)
|
||||
self.write_error(exception)
|
||||
message += "\n" + traceback.format_exc()
|
||||
self.write_error(InvalidUsage(message))
|
||||
|
||||
def on_url(self, url):
|
||||
if not self.url:
|
||||
@@ -231,18 +262,20 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._header_fragment += name
|
||||
|
||||
if value is not None:
|
||||
if self._header_fragment == b'Content-Length' \
|
||||
and int(value) > self.request_max_size:
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
if (
|
||||
self._header_fragment == b"Content-Length"
|
||||
and int(value) > self.request_max_size
|
||||
):
|
||||
self.write_error(PayloadTooLarge("Payload Too Large"))
|
||||
try:
|
||||
value = value.decode()
|
||||
except UnicodeDecodeError:
|
||||
value = value.decode('latin_1')
|
||||
value = value.decode("latin_1")
|
||||
self.headers.append(
|
||||
(self._header_fragment.decode().casefold(), value))
|
||||
(self._header_fragment.decode().casefold(), value)
|
||||
)
|
||||
|
||||
self._header_fragment = b''
|
||||
self._header_fragment = b""
|
||||
|
||||
def on_headers_complete(self):
|
||||
self.request = self.request_class(
|
||||
@@ -250,7 +283,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
headers=CIMultiDict(self.headers),
|
||||
version=self.parser.get_http_version(),
|
||||
method=self.parser.get_method().decode(),
|
||||
transport=self.transport
|
||||
transport=self.transport,
|
||||
)
|
||||
# Remove any existing KeepAlive handler here,
|
||||
# It will be recreated if required on the new request.
|
||||
@@ -259,7 +292,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._keep_alive_timeout_handler = None
|
||||
if self.is_request_stream:
|
||||
self._is_stream_handler = self.router.is_stream_handler(
|
||||
self.request)
|
||||
self.request
|
||||
)
|
||||
if self._is_stream_handler:
|
||||
self.request.stream = asyncio.Queue()
|
||||
self.execute_request_handler()
|
||||
@@ -267,9 +301,10 @@ class HttpProtocol(asyncio.Protocol):
|
||||
def on_body(self, body):
|
||||
if self.is_request_stream and self._is_stream_handler:
|
||||
self._request_stream_task = self.loop.create_task(
|
||||
self.request.stream.put(body))
|
||||
self.request.stream.put(body)
|
||||
)
|
||||
return
|
||||
self.request.body.append(body)
|
||||
self.request.body_push(body)
|
||||
|
||||
def on_message_complete(self):
|
||||
# Entire request (headers and whole body) is received.
|
||||
@@ -279,47 +314,49 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._request_timeout_handler = None
|
||||
if self.is_request_stream and self._is_stream_handler:
|
||||
self._request_stream_task = self.loop.create_task(
|
||||
self.request.stream.put(None))
|
||||
self.request.stream.put(None)
|
||||
)
|
||||
return
|
||||
self.request.body = b''.join(self.request.body)
|
||||
self.request.body_finish()
|
||||
self.execute_request_handler()
|
||||
|
||||
def execute_request_handler(self):
|
||||
self._response_timeout_handler = self.loop.call_later(
|
||||
self.response_timeout, self.response_timeout_callback)
|
||||
self._last_request_time = current_time
|
||||
self.response_timeout, self.response_timeout_callback
|
||||
)
|
||||
self._last_request_time = time()
|
||||
self._request_handler_task = self.loop.create_task(
|
||||
self.request_handler(
|
||||
self.request,
|
||||
self.write_response,
|
||||
self.stream_response))
|
||||
self.request, self.write_response, self.stream_response
|
||||
)
|
||||
)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Responding
|
||||
# -------------------------------------------- #
|
||||
def log_response(self, response):
|
||||
if self.access_log:
|
||||
extra = {
|
||||
'status': getattr(response, 'status', 0),
|
||||
}
|
||||
extra = {"status": getattr(response, "status", 0)}
|
||||
|
||||
if isinstance(response, HTTPResponse):
|
||||
extra['byte'] = len(response.body)
|
||||
extra["byte"] = len(response.body)
|
||||
else:
|
||||
extra['byte'] = -1
|
||||
extra["byte"] = -1
|
||||
|
||||
extra['host'] = 'UNKNOWN'
|
||||
extra["host"] = "UNKNOWN"
|
||||
if self.request is not None:
|
||||
if self.request.ip:
|
||||
extra['host'] = '{0}:{1}'.format(self.request.ip,
|
||||
self.request.port)
|
||||
extra["host"] = "{0}:{1}".format(
|
||||
self.request.ip, self.request.port
|
||||
)
|
||||
|
||||
extra['request'] = '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
extra["request"] = "{0} {1}".format(
|
||||
self.request.method, self.request.url
|
||||
)
|
||||
else:
|
||||
extra['request'] = 'nil'
|
||||
extra["request"] = "nil"
|
||||
|
||||
access_logger.info('', extra=extra)
|
||||
access_logger.info("", extra=extra)
|
||||
|
||||
def write_response(self, response):
|
||||
"""
|
||||
@@ -332,32 +369,38 @@ class HttpProtocol(asyncio.Protocol):
|
||||
keep_alive = self.keep_alive
|
||||
self.transport.write(
|
||||
response.output(
|
||||
self.request.version, keep_alive,
|
||||
self.keep_alive_timeout))
|
||||
self.request.version, keep_alive, self.keep_alive_timeout
|
||||
)
|
||||
)
|
||||
self.log_response(response)
|
||||
except AttributeError:
|
||||
logger.error('Invalid response object for url %s, '
|
||||
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
logger.error(
|
||||
"Invalid response object for url %s, "
|
||||
"Expected Type: HTTPResponse, Actual Type: %s",
|
||||
self.url,
|
||||
type(response),
|
||||
)
|
||||
self.write_error(ServerError("Invalid response type"))
|
||||
except RuntimeError:
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
logger.error(
|
||||
"Connection lost before response written @ %s",
|
||||
self.request.ip,
|
||||
)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
repr(e)))
|
||||
"Writing response failed, connection closed {}".format(repr(e))
|
||||
)
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._last_response_time = current_time
|
||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||
)
|
||||
self._last_response_time = time()
|
||||
self.cleanup()
|
||||
|
||||
async def drain(self):
|
||||
@@ -380,31 +423,37 @@ class HttpProtocol(asyncio.Protocol):
|
||||
keep_alive = self.keep_alive
|
||||
response.protocol = self
|
||||
await response.stream(
|
||||
self.request.version, keep_alive, self.keep_alive_timeout)
|
||||
self.request.version, keep_alive, self.keep_alive_timeout
|
||||
)
|
||||
self.log_response(response)
|
||||
except AttributeError:
|
||||
logger.error('Invalid response object for url %s, '
|
||||
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
logger.error(
|
||||
"Invalid response object for url %s, "
|
||||
"Expected Type: HTTPResponse, Actual Type: %s",
|
||||
self.url,
|
||||
type(response),
|
||||
)
|
||||
self.write_error(ServerError("Invalid response type"))
|
||||
except RuntimeError:
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
logger.error(
|
||||
"Connection lost before response written @ %s",
|
||||
self.request.ip,
|
||||
)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
repr(e)))
|
||||
"Writing response failed, connection closed {}".format(repr(e))
|
||||
)
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._last_response_time = current_time
|
||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||
)
|
||||
self._last_response_time = time()
|
||||
self.cleanup()
|
||||
|
||||
def write_error(self, exception):
|
||||
@@ -416,35 +465,39 @@ class HttpProtocol(asyncio.Protocol):
|
||||
response = None
|
||||
try:
|
||||
response = self.error_handler.response(self.request, exception)
|
||||
version = self.request.version if self.request else '1.1'
|
||||
version = self.request.version if self.request else "1.1"
|
||||
self.transport.write(response.output(version))
|
||||
except RuntimeError:
|
||||
if self._debug:
|
||||
logger.error('Connection lost before error written @ %s',
|
||||
self.request.ip if self.request else 'Unknown')
|
||||
logger.error(
|
||||
"Connection lost before error written @ %s",
|
||||
self.request.ip if self.request else "Unknown",
|
||||
)
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing error failed, connection closed {}".format(
|
||||
repr(e)), from_error=True
|
||||
"Writing error failed, connection closed {}".format(repr(e)),
|
||||
from_error=True,
|
||||
)
|
||||
finally:
|
||||
if self.parser and (self.keep_alive
|
||||
or getattr(response, 'status', 0) == 408):
|
||||
if self.parser and (
|
||||
self.keep_alive or getattr(response, "status", 0) == 408
|
||||
):
|
||||
self.log_response(response)
|
||||
try:
|
||||
self.transport.close()
|
||||
except AttributeError as e:
|
||||
logger.debug('Connection lost before server could close it.')
|
||||
except AttributeError:
|
||||
logger.debug("Connection lost before server could close it.")
|
||||
|
||||
def bail_out(self, message, from_error=False):
|
||||
if from_error or self.transport.is_closing():
|
||||
logger.error("Transport closed @ %s and exception "
|
||||
"experienced during error handling",
|
||||
self.transport.get_extra_info('peername'))
|
||||
logger.debug('Exception:\n%s', traceback.format_exc())
|
||||
logger.error(
|
||||
"Transport closed @ %s and exception "
|
||||
"experienced during error handling",
|
||||
self.transport.get_extra_info("peername"),
|
||||
)
|
||||
logger.debug("Exception:", exc_info=True)
|
||||
else:
|
||||
exception = ServerError(message)
|
||||
self.write_error(exception)
|
||||
self.write_error(ServerError(message))
|
||||
logger.error(message)
|
||||
|
||||
def cleanup(self):
|
||||
@@ -503,17 +556,43 @@ def trigger_events(events, loop):
|
||||
loop.run_until_complete(result)
|
||||
|
||||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, response_timeout=60, keep_alive_timeout=5,
|
||||
ssl=None, sock=None, request_max_size=None, reuse_port=False,
|
||||
loop=None, protocol=HttpProtocol, backlog=100,
|
||||
register_sys_signals=True, run_multiple=False, run_async=False,
|
||||
connections=None, signal=Signal(), request_class=None,
|
||||
access_log=True, keep_alive=True, is_request_stream=False,
|
||||
router=None, websocket_max_size=None, websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16, websocket_write_limit=2 ** 16,
|
||||
state=None, graceful_shutdown_timeout=15.0):
|
||||
def serve(
|
||||
host,
|
||||
port,
|
||||
request_handler,
|
||||
error_handler,
|
||||
before_start=None,
|
||||
after_start=None,
|
||||
before_stop=None,
|
||||
after_stop=None,
|
||||
debug=False,
|
||||
request_timeout=60,
|
||||
response_timeout=60,
|
||||
keep_alive_timeout=5,
|
||||
ssl=None,
|
||||
sock=None,
|
||||
request_max_size=None,
|
||||
reuse_port=False,
|
||||
loop=None,
|
||||
protocol=HttpProtocol,
|
||||
backlog=100,
|
||||
register_sys_signals=True,
|
||||
run_multiple=False,
|
||||
run_async=False,
|
||||
connections=None,
|
||||
signal=Signal(),
|
||||
request_class=None,
|
||||
access_log=True,
|
||||
keep_alive=True,
|
||||
is_request_stream=False,
|
||||
router=None,
|
||||
websocket_max_size=None,
|
||||
websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16,
|
||||
websocket_write_limit=2 ** 16,
|
||||
state=None,
|
||||
graceful_shutdown_timeout=15.0,
|
||||
):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
@@ -554,6 +633,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
quarter of the high-water limit.
|
||||
:param is_request_stream: disable/enable Request.stream
|
||||
:param router: Router object
|
||||
:param graceful_shutdown_timeout: How long take to Force close non-idle
|
||||
connection
|
||||
:return: Nothing
|
||||
"""
|
||||
if not run_async:
|
||||
@@ -596,7 +677,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
ssl=ssl,
|
||||
reuse_port=reuse_port,
|
||||
sock=sock,
|
||||
backlog=backlog
|
||||
backlog=backlog,
|
||||
)
|
||||
|
||||
# Instead of pulling time at the end of every request,
|
||||
@@ -627,11 +708,13 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
try:
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
except NotImplementedError:
|
||||
logger.warning('Sanic tried to use loop.add_signal_handler '
|
||||
'but it is not implemented on this platform.')
|
||||
logger.warning(
|
||||
"Sanic tried to use loop.add_signal_handler "
|
||||
"but it is not implemented on this platform."
|
||||
)
|
||||
pid = os.getpid()
|
||||
try:
|
||||
logger.info('Starting worker [%s]', pid)
|
||||
logger.info("Starting worker [%s]", pid)
|
||||
loop.run_forever()
|
||||
finally:
|
||||
logger.info("Stopping worker [%s]", pid)
|
||||
@@ -662,9 +745,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
coros = []
|
||||
for conn in connections:
|
||||
if hasattr(conn, "websocket") and conn.websocket:
|
||||
coros.append(
|
||||
conn.websocket.close_connection()
|
||||
)
|
||||
coros.append(conn.websocket.close_connection())
|
||||
else:
|
||||
conn.close()
|
||||
|
||||
@@ -685,18 +766,18 @@ def serve_multiple(server_settings, workers):
|
||||
:param stop_event: if provided, is used as a stop signal
|
||||
:return:
|
||||
"""
|
||||
server_settings['reuse_port'] = True
|
||||
server_settings['run_multiple'] = True
|
||||
server_settings["reuse_port"] = True
|
||||
server_settings["run_multiple"] = True
|
||||
|
||||
# Handling when custom socket is not provided.
|
||||
if server_settings.get('sock') is None:
|
||||
if server_settings.get("sock") is None:
|
||||
sock = socket()
|
||||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
sock.bind((server_settings['host'], server_settings['port']))
|
||||
sock.bind((server_settings["host"], server_settings["port"]))
|
||||
sock.set_inheritable(True)
|
||||
server_settings['sock'] = sock
|
||||
server_settings['host'] = None
|
||||
server_settings['port'] = None
|
||||
server_settings["sock"] = sock
|
||||
server_settings["host"] = None
|
||||
server_settings["port"] = None
|
||||
|
||||
def sig_handler(signal, frame):
|
||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||
@@ -720,4 +801,4 @@ def serve_multiple(server_settings, workers):
|
||||
# the above processes will block this until they're stopped
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
server_settings.get('sock').close()
|
||||
server_settings.get("sock").close()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from re import sub
|
||||
from time import strftime, gmtime
|
||||
from time import gmtime, strftime
|
||||
from urllib.parse import unquote
|
||||
|
||||
from aiofiles.os import stat
|
||||
@@ -13,13 +13,22 @@ from sanic.exceptions import (
|
||||
InvalidUsage,
|
||||
)
|
||||
from sanic.handlers import ContentRangeHandler
|
||||
from sanic.response import file, file_stream, HTTPResponse
|
||||
from sanic.response import HTTPResponse, file, file_stream
|
||||
|
||||
|
||||
def register(app, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range,
|
||||
stream_large_files, name='static', host=None,
|
||||
strict_slashes=None, content_type=None):
|
||||
def register(
|
||||
app,
|
||||
uri,
|
||||
file_or_directory,
|
||||
pattern,
|
||||
use_modified_since,
|
||||
use_content_range,
|
||||
stream_large_files,
|
||||
name="static",
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
content_type=None,
|
||||
):
|
||||
# TODO: Though sanic is not a file server, I feel like we should at least
|
||||
# make a good effort here. Modified-since is nice, but we could
|
||||
# also look into etags, expires, and caching
|
||||
@@ -46,12 +55,12 @@ def register(app, uri, file_or_directory, pattern,
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
if not path.isfile(file_or_directory):
|
||||
uri += '<file_uri:' + pattern + '>'
|
||||
uri += "<file_uri:" + pattern + ">"
|
||||
|
||||
async def _handler(request, file_uri=None):
|
||||
# Using this to determine if the URL is trying to break out of the path
|
||||
# served. os.path.realpath seems to be very slow
|
||||
if file_uri and '../' in file_uri:
|
||||
if file_uri and "../" in file_uri:
|
||||
raise InvalidUsage("Invalid URL")
|
||||
# Merge served directory and requested file if provided
|
||||
# Strip all / that in the beginning of the URL to help prevent python
|
||||
@@ -59,15 +68,16 @@ def register(app, uri, file_or_directory, pattern,
|
||||
root_path = file_path = file_or_directory
|
||||
if file_uri:
|
||||
file_path = path.join(
|
||||
file_or_directory, sub('^[/]*', '', file_uri))
|
||||
file_or_directory, sub("^[/]*", "", file_uri)
|
||||
)
|
||||
|
||||
# URL decode the path sent by the browser otherwise we won't be able to
|
||||
# match filenames which got encoded (filenames with spaces etc)
|
||||
file_path = path.abspath(unquote(file_path))
|
||||
if not file_path.startswith(path.abspath(unquote(root_path))):
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
raise FileNotFound(
|
||||
"File not found", path=file_or_directory, relative_url=file_uri
|
||||
)
|
||||
try:
|
||||
headers = {}
|
||||
# Check if the client has been sent this file before
|
||||
@@ -76,33 +86,35 @@ def register(app, uri, file_or_directory, pattern,
|
||||
if use_modified_since:
|
||||
stats = await stat(file_path)
|
||||
modified_since = strftime(
|
||||
'%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
|
||||
if request.headers.get('If-Modified-Since') == modified_since:
|
||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
||||
)
|
||||
if request.headers.get("If-Modified-Since") == modified_since:
|
||||
return HTTPResponse(status=304)
|
||||
headers['Last-Modified'] = modified_since
|
||||
headers["Last-Modified"] = modified_since
|
||||
_range = None
|
||||
if use_content_range:
|
||||
_range = None
|
||||
if not stats:
|
||||
stats = await stat(file_path)
|
||||
headers['Accept-Ranges'] = 'bytes'
|
||||
headers['Content-Length'] = str(stats.st_size)
|
||||
if request.method != 'HEAD':
|
||||
headers["Accept-Ranges"] = "bytes"
|
||||
headers["Content-Length"] = str(stats.st_size)
|
||||
if request.method != "HEAD":
|
||||
try:
|
||||
_range = ContentRangeHandler(request, stats)
|
||||
except HeaderNotFound:
|
||||
pass
|
||||
else:
|
||||
del headers['Content-Length']
|
||||
del headers["Content-Length"]
|
||||
for key, value in _range.headers.items():
|
||||
headers[key] = value
|
||||
headers['Content-Type'] = content_type \
|
||||
or guess_type(file_path)[0] or 'text/plain'
|
||||
if request.method == 'HEAD':
|
||||
headers["Content-Type"] = (
|
||||
content_type or guess_type(file_path)[0] or "text/plain"
|
||||
)
|
||||
if request.method == "HEAD":
|
||||
return HTTPResponse(headers=headers)
|
||||
else:
|
||||
if stream_large_files:
|
||||
if isinstance(stream_large_files, int):
|
||||
if type(stream_large_files) == int:
|
||||
threshold = stream_large_files
|
||||
else:
|
||||
threshold = 1024 * 1024
|
||||
@@ -110,19 +122,25 @@ def register(app, uri, file_or_directory, pattern,
|
||||
if not stats:
|
||||
stats = await stat(file_path)
|
||||
if stats.st_size >= threshold:
|
||||
return await file_stream(file_path, headers=headers,
|
||||
_range=_range)
|
||||
return await file_stream(
|
||||
file_path, headers=headers, _range=_range
|
||||
)
|
||||
return await file(file_path, headers=headers, _range=_range)
|
||||
except ContentRangeError:
|
||||
raise
|
||||
except Exception:
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
raise FileNotFound(
|
||||
"File not found", path=file_or_directory, relative_url=file_uri
|
||||
)
|
||||
|
||||
# special prefix for static files
|
||||
if not name.startswith('_static_'):
|
||||
name = '_static_{}'.format(name)
|
||||
if not name.startswith("_static_"):
|
||||
name = "_static_{}".format(name)
|
||||
|
||||
app.route(uri, methods=['GET', 'HEAD'], name=name, host=host,
|
||||
strict_slashes=strict_slashes)(_handler)
|
||||
app.route(
|
||||
uri,
|
||||
methods=["GET", "HEAD"],
|
||||
name=name,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
)(_handler)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import traceback
|
||||
from json import JSONDecodeError
|
||||
from sanic.log import logger
|
||||
|
||||
from sanic.exceptions import MethodNotSupported
|
||||
from sanic.log import logger
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 42101
|
||||
|
||||
|
||||
@@ -16,70 +16,83 @@ class SanicTestClient:
|
||||
|
||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
||||
import aiohttp
|
||||
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
|
||||
|
||||
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
|
||||
url = uri
|
||||
else:
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=self.port, uri=uri)
|
||||
url = "http://{host}:{port}{uri}".format(
|
||||
host=HOST, port=self.port, uri=uri
|
||||
)
|
||||
|
||||
logger.info(url)
|
||||
conn = aiohttp.TCPConnector(verify_ssl=False)
|
||||
conn = aiohttp.TCPConnector(ssl=False)
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, connector=conn) as session:
|
||||
async with getattr(
|
||||
session, method.lower())(url, *args, **kwargs) as response:
|
||||
cookies=cookies, connector=conn
|
||||
) as session:
|
||||
async with getattr(session, method.lower())(
|
||||
url, *args, **kwargs
|
||||
) as response:
|
||||
try:
|
||||
response.text = await response.text()
|
||||
except UnicodeDecodeError as e:
|
||||
except UnicodeDecodeError:
|
||||
response.text = None
|
||||
|
||||
try:
|
||||
response.json = await response.json()
|
||||
except (JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError):
|
||||
except (
|
||||
JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError,
|
||||
):
|
||||
response.json = None
|
||||
|
||||
response.body = await response.read()
|
||||
return response
|
||||
|
||||
def _sanic_endpoint_test(
|
||||
self, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={"auto_reload": False},
|
||||
*request_args, **request_kwargs):
|
||||
self,
|
||||
method="get",
|
||||
uri="/",
|
||||
gather_request=True,
|
||||
debug=False,
|
||||
server_kwargs={"auto_reload": False},
|
||||
*request_args,
|
||||
**request_kwargs
|
||||
):
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
|
||||
if gather_request:
|
||||
|
||||
def _collect_request(request):
|
||||
if results[0] is None:
|
||||
results[0] = request
|
||||
|
||||
self.app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
@self.app.exception(MethodNotSupported)
|
||||
async def error_handler(request, exception):
|
||||
if request.method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
|
||||
if request.method in ["HEAD", "PATCH", "PUT", "DELETE"]:
|
||||
return text(
|
||||
'', exception.status_code, headers=exception.headers
|
||||
"", exception.status_code, headers=exception.headers
|
||||
)
|
||||
else:
|
||||
return self.app.error_handler.default(request, exception)
|
||||
|
||||
@self.app.listener('after_server_start')
|
||||
@self.app.listener("after_server_start")
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
response = await self._local_request(
|
||||
method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
method, uri, *request_args, **request_kwargs
|
||||
)
|
||||
results[-1] = response
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
logger.exception("Exception")
|
||||
exceptions.append(e)
|
||||
self.app.stop()
|
||||
|
||||
self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
|
||||
self.app.listeners['after_server_start'].pop()
|
||||
self.app.listeners["after_server_start"].pop()
|
||||
|
||||
if exceptions:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
@@ -91,31 +104,34 @@ class SanicTestClient:
|
||||
except BaseException:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
results
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except BaseException:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
"Request object expected, got ({})".format(results)
|
||||
)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('get', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("get", *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('post', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("post", *args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('put', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("put", *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('delete', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("delete", *args, **kwargs)
|
||||
|
||||
def patch(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('patch', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("patch", *args, **kwargs)
|
||||
|
||||
def options(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('options', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("options", *args, **kwargs)
|
||||
|
||||
def head(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('head', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("head", *args, **kwargs)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import InvalidUsage
|
||||
|
||||
|
||||
class HTTPMethodView:
|
||||
@@ -48,6 +48,7 @@ class HTTPMethodView:
|
||||
"""Return view function for use with the routing system, that
|
||||
dispatches request to appropriate handler method.
|
||||
"""
|
||||
|
||||
def view(*args, **kwargs):
|
||||
self = view.view_class(*class_args, **class_kwargs)
|
||||
return self.dispatch_request(*args, **kwargs)
|
||||
@@ -94,11 +95,13 @@ class CompositionView:
|
||||
for method in methods:
|
||||
if method not in HTTP_METHODS:
|
||||
raise InvalidUsage(
|
||||
'{} is not a valid HTTP method.'.format(method))
|
||||
"{} is not a valid HTTP method.".format(method)
|
||||
)
|
||||
|
||||
if method in self.handlers:
|
||||
raise InvalidUsage(
|
||||
'Method {} is already registered.'.format(method))
|
||||
"Method {} is already registered.".format(method)
|
||||
)
|
||||
self.handlers[method] = handler
|
||||
|
||||
def __call__(self, request, *args, **kwargs):
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
from httptools import HttpParserUpgrade
|
||||
from websockets import ConnectionClosed # noqa
|
||||
from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.server import HttpProtocol
|
||||
from httptools import HttpParserUpgrade
|
||||
from websockets import handshake, WebSocketCommonProtocol, InvalidHandshake
|
||||
from websockets import ConnectionClosed # noqa
|
||||
|
||||
|
||||
class WebSocketProtocol(HttpProtocol):
|
||||
def __init__(self, *args, websocket_timeout=10,
|
||||
websocket_max_size=None,
|
||||
websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16,
|
||||
websocket_write_limit=2 ** 16, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
websocket_timeout=10,
|
||||
websocket_max_size=None,
|
||||
websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16,
|
||||
websocket_write_limit=2 ** 16,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.websocket = None
|
||||
self.websocket_timeout = websocket_timeout
|
||||
@@ -57,36 +63,32 @@ class WebSocketProtocol(HttpProtocol):
|
||||
|
||||
async def websocket_handshake(self, request, subprotocols=None):
|
||||
# let the websockets package do the handshake with the client
|
||||
headers = []
|
||||
|
||||
def get_header(k):
|
||||
return request.headers.get(k, '')
|
||||
|
||||
def set_header(k, v):
|
||||
headers.append((k, v))
|
||||
headers = {}
|
||||
|
||||
try:
|
||||
key = handshake.check_request(get_header)
|
||||
handshake.build_response(set_header, key)
|
||||
key = handshake.check_request(request.headers)
|
||||
handshake.build_response(headers, key)
|
||||
except InvalidHandshake:
|
||||
raise InvalidUsage('Invalid websocket request')
|
||||
raise InvalidUsage("Invalid websocket request")
|
||||
|
||||
subprotocol = None
|
||||
if subprotocols and 'Sec-Websocket-Protocol' in request.headers:
|
||||
if subprotocols and "Sec-Websocket-Protocol" in request.headers:
|
||||
# select a subprotocol
|
||||
client_subprotocols = [p.strip() for p in request.headers[
|
||||
'Sec-Websocket-Protocol'].split(',')]
|
||||
client_subprotocols = [
|
||||
p.strip()
|
||||
for p in request.headers["Sec-Websocket-Protocol"].split(",")
|
||||
]
|
||||
for p in client_subprotocols:
|
||||
if p in subprotocols:
|
||||
subprotocol = p
|
||||
set_header('Sec-Websocket-Protocol', subprotocol)
|
||||
headers["Sec-Websocket-Protocol"] = subprotocol
|
||||
break
|
||||
|
||||
# write the 101 response back to the client
|
||||
rv = b'HTTP/1.1 101 Switching Protocols\r\n'
|
||||
for k, v in headers:
|
||||
rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n'
|
||||
rv += b'\r\n'
|
||||
rv = b"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
for k, v in headers.items():
|
||||
rv += k.encode("utf-8") + b": " + v.encode("utf-8") + b"\r\n"
|
||||
rv += b"\r\n"
|
||||
request.transport.write(rv)
|
||||
|
||||
# hook up the websocket protocol
|
||||
@@ -95,7 +97,7 @@ class WebSocketProtocol(HttpProtocol):
|
||||
max_size=self.websocket_max_size,
|
||||
max_queue=self.websocket_max_queue,
|
||||
read_limit=self.websocket_read_limit,
|
||||
write_limit=self.websocket_write_limit
|
||||
write_limit=self.websocket_write_limit,
|
||||
)
|
||||
self.websocket.subprotocol = subprotocol
|
||||
self.websocket.connection_made(request.transport)
|
||||
|
||||
103
sanic/worker.py
103
sanic/worker.py
@@ -1,10 +1,16 @@
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import gunicorn.workers.base as base
|
||||
|
||||
from sanic.server import HttpProtocol, Signal, serve, trigger_events
|
||||
from sanic.websocket import WebSocketProtocol
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
@@ -12,13 +18,10 @@ except ImportError:
|
||||
|
||||
try:
|
||||
import uvloop
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
import gunicorn.workers.base as base
|
||||
|
||||
from sanic.server import trigger_events, serve, HttpProtocol, Signal
|
||||
from sanic.websocket import WebSocketProtocol
|
||||
|
||||
|
||||
class GunicornWorker(base.Worker):
|
||||
@@ -50,36 +53,43 @@ class GunicornWorker(base.Worker):
|
||||
def run(self):
|
||||
is_debug = self.log.loglevel == logging.DEBUG
|
||||
protocol = (
|
||||
self.websocket_protocol if self.app.callable.websocket_enabled
|
||||
else self.http_protocol)
|
||||
self.websocket_protocol
|
||||
if self.app.callable.websocket_enabled
|
||||
else self.http_protocol
|
||||
)
|
||||
self._server_settings = self.app.callable._helper(
|
||||
loop=self.loop,
|
||||
debug=is_debug,
|
||||
protocol=protocol,
|
||||
ssl=self.ssl_context,
|
||||
run_async=True)
|
||||
self._server_settings['signal'] = self.signal
|
||||
self._server_settings.pop('sock')
|
||||
trigger_events(self._server_settings.get('before_start', []),
|
||||
self.loop)
|
||||
self._server_settings['before_start'] = ()
|
||||
run_async=True,
|
||||
)
|
||||
self._server_settings["signal"] = self.signal
|
||||
self._server_settings.pop("sock")
|
||||
trigger_events(
|
||||
self._server_settings.get("before_start", []), self.loop
|
||||
)
|
||||
self._server_settings["before_start"] = ()
|
||||
|
||||
self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
|
||||
try:
|
||||
self.loop.run_until_complete(self._runner)
|
||||
self.app.callable.is_running = True
|
||||
trigger_events(self._server_settings.get('after_start', []),
|
||||
self.loop)
|
||||
trigger_events(
|
||||
self._server_settings.get("after_start", []), self.loop
|
||||
)
|
||||
self.loop.run_until_complete(self._check_alive())
|
||||
trigger_events(self._server_settings.get('before_stop', []),
|
||||
self.loop)
|
||||
trigger_events(
|
||||
self._server_settings.get("before_stop", []), self.loop
|
||||
)
|
||||
self.loop.run_until_complete(self.close())
|
||||
except BaseException:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
try:
|
||||
trigger_events(self._server_settings.get('after_stop', []),
|
||||
self.loop)
|
||||
trigger_events(
|
||||
self._server_settings.get("after_stop", []), self.loop
|
||||
)
|
||||
except BaseException:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
@@ -90,8 +100,11 @@ class GunicornWorker(base.Worker):
|
||||
async def close(self):
|
||||
if self.servers:
|
||||
# stop accepting connections
|
||||
self.log.info("Stopping server: %s, connections: %s",
|
||||
self.pid, len(self.connections))
|
||||
self.log.info(
|
||||
"Stopping server: %s, connections: %s",
|
||||
self.pid,
|
||||
len(self.connections),
|
||||
)
|
||||
for server in self.servers:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
@@ -105,8 +118,9 @@ class GunicornWorker(base.Worker):
|
||||
# gracefully shutdown timeout
|
||||
start_shutdown = 0
|
||||
graceful_shutdown_timeout = self.cfg.graceful_timeout
|
||||
while self.connections and \
|
||||
(start_shutdown < graceful_shutdown_timeout):
|
||||
while self.connections and (
|
||||
start_shutdown < graceful_shutdown_timeout
|
||||
):
|
||||
await asyncio.sleep(0.1)
|
||||
start_shutdown = start_shutdown + 0.1
|
||||
|
||||
@@ -115,9 +129,7 @@ class GunicornWorker(base.Worker):
|
||||
coros = []
|
||||
for conn in self.connections:
|
||||
if hasattr(conn, "websocket") and conn.websocket:
|
||||
coros.append(
|
||||
conn.websocket.close_connection()
|
||||
)
|
||||
coros.append(conn.websocket.close_connection())
|
||||
else:
|
||||
conn.close()
|
||||
_shutdown = asyncio.gather(*coros, loop=self.loop)
|
||||
@@ -148,8 +160,9 @@ class GunicornWorker(base.Worker):
|
||||
)
|
||||
if self.max_requests and req_count > self.max_requests:
|
||||
self.alive = False
|
||||
self.log.info("Max requests exceeded, shutting down: %s",
|
||||
self)
|
||||
self.log.info(
|
||||
"Max requests exceeded, shutting down: %s", self
|
||||
)
|
||||
elif pid == os.getpid() and self.ppid != os.getppid():
|
||||
self.alive = False
|
||||
self.log.info("Parent changed, shutting down: %s", self)
|
||||
@@ -175,23 +188,29 @@ class GunicornWorker(base.Worker):
|
||||
def init_signals(self):
|
||||
# Set up signals through the event loop API.
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit,
|
||||
signal.SIGQUIT, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGQUIT, self.handle_quit, signal.SIGQUIT, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit,
|
||||
signal.SIGTERM, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGTERM, self.handle_exit, signal.SIGTERM, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGINT, self.handle_quit,
|
||||
signal.SIGINT, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGINT, self.handle_quit, signal.SIGINT, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch,
|
||||
signal.SIGWINCH, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGWINCH, self.handle_winch, signal.SIGWINCH, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1,
|
||||
signal.SIGUSR1, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGUSR1, self.handle_usr1, signal.SIGUSR1, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort,
|
||||
signal.SIGABRT, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGABRT, self.handle_abort, signal.SIGABRT, None
|
||||
)
|
||||
|
||||
# Don't let SIGTERM and SIGUSR1 disturb active requests
|
||||
# by interrupting system calls
|
||||
|
||||
17
setup.cfg
Normal file
17
setup.cfg
Normal file
@@ -0,0 +1,17 @@
|
||||
[flake8]
|
||||
# https://github.com/ambv/black#slices
|
||||
# https://github.com/ambv/black#line-breaks--binary-operators
|
||||
ignore = E203, W503
|
||||
|
||||
|
||||
[isort]
|
||||
atomic=true
|
||||
default_section = THIRDPARTY
|
||||
include_trailing_comma = true
|
||||
known_first_party = sanic
|
||||
known_third_party = pytest
|
||||
line_length = 79
|
||||
lines_after_imports = 2
|
||||
lines_between_types = 1
|
||||
multi_line_output = 3
|
||||
not_skip = __init__.py
|
||||
7
setup.py
7
setup.py
@@ -21,7 +21,7 @@ def open_local(paths, mode='r', encoding='utf8'):
|
||||
|
||||
with open_local(['sanic', '__init__.py'], encoding='latin1') as fp:
|
||||
try:
|
||||
version = re.findall(r"^__version__ = '([^']+)'\r?$",
|
||||
version = re.findall(r"^__version__ = \"([^']+)\"\r?$",
|
||||
fp.read(), re.M)[0]
|
||||
except IndexError:
|
||||
raise RuntimeError('Unable to determine version.')
|
||||
@@ -48,6 +48,7 @@ setup_kwargs = {
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -56,11 +57,11 @@ ujson = 'ujson>=1.35' + env_dependency
|
||||
uvloop = 'uvloop>=0.5.3' + env_dependency
|
||||
|
||||
requirements = [
|
||||
'httptools>=0.0.9',
|
||||
'httptools>=0.0.10',
|
||||
uvloop,
|
||||
ujson,
|
||||
'aiofiles>=0.3.0',
|
||||
'websockets>=5.0,<6.0',
|
||||
'websockets>=6.0,<7.0',
|
||||
'multidict>=4.0,<5.0',
|
||||
]
|
||||
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
||||
|
||||
12
tests/conftest.py
Normal file
12
tests/conftest.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
if sys.platform in ['win32', 'cygwin']:
|
||||
collect_ignore = ["test_worker.py"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(request):
|
||||
return Sanic(request.node.name)
|
||||
150
tests/test_app.py
Normal file
150
tests/test_app.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
def test_app_loop_running(app):
|
||||
|
||||
@app.get('/test')
|
||||
async def handler(request):
|
||||
assert isinstance(app.loop, asyncio.AbstractEventLoop)
|
||||
return text('pass')
|
||||
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.text == 'pass'
|
||||
|
||||
|
||||
def test_app_loop_not_running(app):
|
||||
with pytest.raises(SanicException) as excinfo:
|
||||
app.loop
|
||||
|
||||
assert str(excinfo.value) == (
|
||||
'Loop can only be retrieved after the app has started '
|
||||
'running. Not supported with `create_server` function'
|
||||
)
|
||||
|
||||
|
||||
def test_app_run_raise_type_error(app):
|
||||
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
app.run(loop='loop')
|
||||
|
||||
assert str(excinfo.value) == (
|
||||
'loop is not a valid argument. To use an existing loop, '
|
||||
'change to create_server().\nSee more: '
|
||||
'https://sanic.readthedocs.io/en/latest/sanic/deploying.html'
|
||||
'#asynchronous-support'
|
||||
)
|
||||
|
||||
|
||||
def test_app_route_raise_value_error(app):
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
@app.route('/test')
|
||||
async def handler():
|
||||
return text('test')
|
||||
|
||||
assert str(excinfo.value) == 'Required parameter `request` missing in the handler() route?'
|
||||
|
||||
|
||||
def test_app_handle_request_handler_is_none(app, monkeypatch):
|
||||
|
||||
def mockreturn(*args, **kwargs):
|
||||
return None, [], {}, ''
|
||||
|
||||
# Not sure how to make app.router.get() return None, so use mock here.
|
||||
monkeypatch.setattr(app.router, 'get', mockreturn)
|
||||
|
||||
@app.get('/test')
|
||||
def handler(request):
|
||||
return text('test')
|
||||
|
||||
request, response = app.test_client.get('/test')
|
||||
|
||||
assert response.text == 'Error: \'None\' was returned while requesting a handler from the router'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('websocket_enabled', [True, False])
|
||||
@pytest.mark.parametrize('enable', [True, False])
|
||||
def test_app_enable_websocket(app, websocket_enabled, enable):
|
||||
app.websocket_enabled = websocket_enabled
|
||||
app.enable_websocket(enable=enable)
|
||||
|
||||
assert app.websocket_enabled == enable
|
||||
|
||||
@app.websocket('/ws')
|
||||
async def handler(request, ws):
|
||||
await ws.send('test')
|
||||
|
||||
assert app.websocket_enabled == True
|
||||
|
||||
|
||||
def test_handle_request_with_nested_exception(app, monkeypatch):
|
||||
|
||||
err_msg = 'Mock Exception'
|
||||
|
||||
# Not sure how to raise an exception in app.error_handler.response(), use mock here
|
||||
def mock_error_handler_response(*args, **kwargs):
|
||||
raise Exception(err_msg)
|
||||
|
||||
monkeypatch.setattr(app.error_handler, 'response', mock_error_handler_response)
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
raise Exception
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.status == 500
|
||||
assert response.text == 'An error occurred while handling an error'
|
||||
|
||||
|
||||
def test_handle_request_with_nested_exception_debug(app, monkeypatch):
|
||||
|
||||
err_msg = 'Mock Exception'
|
||||
|
||||
# Not sure how to raise an exception in app.error_handler.response(), use mock here
|
||||
def mock_error_handler_response(*args, **kwargs):
|
||||
raise Exception(err_msg)
|
||||
|
||||
monkeypatch.setattr(app.error_handler, 'response', mock_error_handler_response)
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
raise Exception
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/', debug=True)
|
||||
assert response.status == 500
|
||||
assert response.text.startswith(
|
||||
'Error while handling error: {}\nStack: Traceback (most recent call last):\n'.format(err_msg)
|
||||
)
|
||||
|
||||
|
||||
def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
|
||||
|
||||
# Not sure how to raise an exception in app.error_handler.response(), use mock here
|
||||
def mock_error_handler_response(*args, **kwargs):
|
||||
raise SanicException('Mock SanicException')
|
||||
|
||||
monkeypatch.setattr(app.error_handler, 'response', mock_error_handler_response)
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
raise Exception
|
||||
return text('OK')
|
||||
|
||||
caplog.set_level(logging.ERROR, logger="sanic.root")
|
||||
with caplog.at_level(logging.ERROR):
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.status == 500
|
||||
assert response.text == 'Error: Mock SanicException'
|
||||
assert caplog.record_tuples[0] == (
|
||||
'sanic.root',
|
||||
logging.ERROR,
|
||||
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'"
|
||||
)
|
||||
@@ -1,10 +1,9 @@
|
||||
import asyncio
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def test_bad_request_response():
|
||||
app = Sanic('test_bad_request_response')
|
||||
def test_bad_request_response(app):
|
||||
lines = []
|
||||
|
||||
@app.listener('after_server_start')
|
||||
async def _request(sanic, loop):
|
||||
connect = asyncio.open_connection('127.0.0.1', 42101)
|
||||
|
||||
@@ -1,30 +1,43 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import json, text
|
||||
from sanic.exceptions import NotFound, ServerError, InvalidUsage
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import NotFound, ServerError, InvalidUsage
|
||||
from sanic.request import Request
|
||||
from sanic.response import text, json
|
||||
from sanic.views import CompositionView
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_directory():
|
||||
"""The static directory to serve"""
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
current_directory = os.path.dirname(os.path.abspath(current_file))
|
||||
static_directory = os.path.join(current_directory, 'static')
|
||||
return static_directory
|
||||
|
||||
|
||||
def get_file_path(static_file_directory, file_name):
|
||||
return os.path.join(static_file_directory, file_name)
|
||||
|
||||
|
||||
def get_file_content(static_file_directory, file_name):
|
||||
"""The content of the static file to check"""
|
||||
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_versioned_routes_get(method):
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
def test_versioned_routes_get(app, method):
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
method = method.lower()
|
||||
@@ -36,7 +49,7 @@ def test_versioned_routes_get(method):
|
||||
return text('OK')
|
||||
else:
|
||||
print(func)
|
||||
raise
|
||||
raise Exception("{} is not callable".format(func))
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
@@ -46,8 +59,7 @@ def test_versioned_routes_get(method):
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_bp():
|
||||
app = Sanic('test_text')
|
||||
def test_bp(app):
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
@bp.route('/')
|
||||
@@ -60,23 +72,23 @@ def test_bp():
|
||||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
def test_bp_strict_slash():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
|
||||
def test_bp_strict_slash(app):
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
@bp.get('/get', strict_slashes=True)
|
||||
def handler(request):
|
||||
def get_handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.post('/post/', strict_slashes=True)
|
||||
def handler(request):
|
||||
def post_handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
assert response.json == None
|
||||
assert response.json is None
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.status == 404
|
||||
@@ -87,16 +99,16 @@ def test_bp_strict_slash():
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.status == 404
|
||||
|
||||
def test_bp_strict_slash_default_value():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
|
||||
def test_bp_strict_slash_default_value(app):
|
||||
bp = Blueprint('test_text', strict_slashes=True)
|
||||
|
||||
@bp.get('/get')
|
||||
def handler(request):
|
||||
def get_handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.post('/post/')
|
||||
def handler(request):
|
||||
def post_handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
@@ -107,16 +119,16 @@ def test_bp_strict_slash_default_value():
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.status == 404
|
||||
|
||||
def test_bp_strict_slash_without_passing_default_value():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
|
||||
def test_bp_strict_slash_without_passing_default_value(app):
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
@bp.get('/get')
|
||||
def handler(request):
|
||||
def get_handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.post('/post/')
|
||||
def handler(request):
|
||||
def post_handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
@@ -127,16 +139,16 @@ def test_bp_strict_slash_without_passing_default_value():
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_bp_strict_slash_default_value_can_be_overwritten():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
|
||||
def test_bp_strict_slash_default_value_can_be_overwritten(app):
|
||||
bp = Blueprint('test_text', strict_slashes=True)
|
||||
|
||||
@bp.get('/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
def get_handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.post('/post/', strict_slashes=False)
|
||||
def handler(request):
|
||||
def post_handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
@@ -147,8 +159,8 @@ def test_bp_strict_slash_default_value_can_be_overwritten():
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_bp_with_url_prefix():
|
||||
app = Sanic('test_text')
|
||||
|
||||
def test_bp_with_url_prefix(app):
|
||||
bp = Blueprint('test_text', url_prefix='/test1')
|
||||
|
||||
@bp.route('/')
|
||||
@@ -161,8 +173,7 @@ def test_bp_with_url_prefix():
|
||||
assert response.text == 'Hello'
|
||||
|
||||
|
||||
def test_several_bp_with_url_prefix():
|
||||
app = Sanic('test_text')
|
||||
def test_several_bp_with_url_prefix(app):
|
||||
bp = Blueprint('test_text', url_prefix='/test1')
|
||||
bp2 = Blueprint('test_text2', url_prefix='/test2')
|
||||
|
||||
@@ -182,16 +193,16 @@ def test_several_bp_with_url_prefix():
|
||||
request, response = app.test_client.get('/test2/')
|
||||
assert response.text == 'Hello2'
|
||||
|
||||
def test_bp_with_host():
|
||||
app = Sanic('test_bp_host')
|
||||
|
||||
def test_bp_with_host(app):
|
||||
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
|
||||
|
||||
@bp.route('/')
|
||||
def handler(request):
|
||||
def handler1(request):
|
||||
return text('Hello')
|
||||
|
||||
@bp.route('/', host="sub.example.com")
|
||||
def handler(request):
|
||||
def handler2(request):
|
||||
return text('Hello subdomain!')
|
||||
|
||||
app.blueprint(bp)
|
||||
@@ -209,8 +220,7 @@ def test_bp_with_host():
|
||||
assert response.text == 'Hello subdomain!'
|
||||
|
||||
|
||||
def test_several_bp_with_host():
|
||||
app = Sanic('test_text')
|
||||
def test_several_bp_with_host(app):
|
||||
bp = Blueprint('test_text',
|
||||
url_prefix='/test',
|
||||
host="example.com")
|
||||
@@ -223,14 +233,13 @@ def test_several_bp_with_host():
|
||||
return text('Hello')
|
||||
|
||||
@bp2.route('/')
|
||||
def handler2(request):
|
||||
def handler1(request):
|
||||
return text('Hello2')
|
||||
|
||||
@bp2.route('/other/')
|
||||
def handler2(request):
|
||||
return text('Hello3')
|
||||
|
||||
|
||||
app.blueprint(bp)
|
||||
app.blueprint(bp2)
|
||||
|
||||
@@ -253,8 +262,8 @@ def test_several_bp_with_host():
|
||||
headers=headers)
|
||||
assert response.text == 'Hello3'
|
||||
|
||||
def test_bp_middleware():
|
||||
app = Sanic('test_middleware')
|
||||
|
||||
def test_bp_middleware(app):
|
||||
blueprint = Blueprint('test_middleware')
|
||||
|
||||
@blueprint.middleware('response')
|
||||
@@ -272,8 +281,8 @@ def test_bp_middleware():
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_bp_exception_handler():
|
||||
app = Sanic('test_middleware')
|
||||
|
||||
def test_bp_exception_handler(app):
|
||||
blueprint = Blueprint('test_middleware')
|
||||
|
||||
@blueprint.route('/1')
|
||||
@@ -297,7 +306,6 @@ def test_bp_exception_handler():
|
||||
request, response = app.test_client.get('/1')
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
request, response = app.test_client.get('/2')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
@@ -305,8 +313,8 @@ def test_bp_exception_handler():
|
||||
request, response = app.test_client.get('/3')
|
||||
assert response.status == 200
|
||||
|
||||
def test_bp_listeners():
|
||||
app = Sanic('test_middleware')
|
||||
|
||||
def test_bp_listeners(app):
|
||||
blueprint = Blueprint('test_middleware')
|
||||
|
||||
order = []
|
||||
@@ -339,14 +347,14 @@ def test_bp_listeners():
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert order == [1,2,3,4,5,6]
|
||||
assert order == [1, 2, 3, 4, 5, 6]
|
||||
|
||||
def test_bp_static():
|
||||
|
||||
def test_bp_static(app):
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
with open(current_file, 'rb') as file:
|
||||
current_file_contents = file.read()
|
||||
|
||||
app = Sanic('test_static')
|
||||
blueprint = Blueprint('test_static')
|
||||
|
||||
blueprint.static('/testing.file', current_file)
|
||||
@@ -357,14 +365,14 @@ def test_bp_static():
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.html'])
|
||||
def test_bp_static_content_type(file_name):
|
||||
def test_bp_static_content_type(app, file_name):
|
||||
# This is done here, since no other test loads a file here
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
current_directory = os.path.dirname(os.path.abspath(current_file))
|
||||
static_directory = os.path.join(current_directory, 'static')
|
||||
|
||||
app = Sanic('test_static')
|
||||
blueprint = Blueprint('test_static')
|
||||
blueprint.static(
|
||||
'/testing.file',
|
||||
@@ -379,8 +387,8 @@ def test_bp_static_content_type(file_name):
|
||||
assert response.body == get_file_content(static_directory, file_name)
|
||||
assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
|
||||
|
||||
def test_bp_shorthand():
|
||||
app = Sanic('test_shorhand_routes')
|
||||
|
||||
def test_bp_shorthand(app):
|
||||
blueprint = Blueprint('test_shorhand_routes')
|
||||
ev = asyncio.Event()
|
||||
|
||||
@@ -390,37 +398,37 @@ def test_bp_shorthand():
|
||||
return text('OK')
|
||||
|
||||
@blueprint.put('/put')
|
||||
def handler(request):
|
||||
def put_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.post('/post')
|
||||
def handler(request):
|
||||
def post_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.head('/head')
|
||||
def handler(request):
|
||||
def head_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.options('/options')
|
||||
def handler(request):
|
||||
def options_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.patch('/patch')
|
||||
def handler(request):
|
||||
def patch_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.delete('/delete')
|
||||
def handler(request):
|
||||
def delete_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.websocket('/ws')
|
||||
async def handler(request, ws):
|
||||
@blueprint.websocket('/ws/', strict_slashes=True)
|
||||
async def websocket_handler(request, ws):
|
||||
assert request.stream is None
|
||||
ev.set()
|
||||
|
||||
@@ -470,7 +478,7 @@ def test_bp_shorthand():
|
||||
request, response = app.test_client.get('/delete')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
request, response = app.test_client.get('/ws/', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
@@ -478,25 +486,24 @@ def test_bp_shorthand():
|
||||
assert response.status == 101
|
||||
assert ev.is_set()
|
||||
|
||||
def test_bp_group():
|
||||
app = Sanic('test_nested_bp_groups')
|
||||
|
||||
def test_bp_group(app):
|
||||
deep_0 = Blueprint('deep_0', url_prefix='/deep')
|
||||
deep_1 = Blueprint('deep_1', url_prefix = '/deep1')
|
||||
deep_1 = Blueprint('deep_1', url_prefix='/deep1')
|
||||
|
||||
@deep_0.route('/')
|
||||
def handler(request):
|
||||
return text('D0_OK')
|
||||
|
||||
@deep_1.route('/bottom')
|
||||
def handler(request):
|
||||
def bottom_handler(request):
|
||||
return text('D1B_OK')
|
||||
|
||||
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
|
||||
mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
|
||||
|
||||
@mid_1.route('/')
|
||||
def handler(request):
|
||||
def handler1(request):
|
||||
return text('M1_OK')
|
||||
|
||||
top = Blueprint.group(mid_0, mid_1)
|
||||
@@ -504,7 +511,7 @@ def test_bp_group():
|
||||
app.blueprint(top)
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
def handler2(request):
|
||||
return text('TOP_OK')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
@@ -518,3 +525,182 @@ def test_bp_group():
|
||||
|
||||
request, response = app.test_client.get('/mid/deep1/bottom')
|
||||
assert response.text == 'D1B_OK'
|
||||
|
||||
|
||||
def test_bp_group_with_default_url_prefix(app):
|
||||
from sanic.response import json
|
||||
bp_resources = Blueprint('bp_resources')
|
||||
|
||||
@bp_resources.get('/')
|
||||
def list_resources_handler(request):
|
||||
resource = {}
|
||||
return json([resource])
|
||||
|
||||
bp_resource = Blueprint('bp_resource', url_prefix='/<resource_id>')
|
||||
|
||||
@bp_resource.get('/')
|
||||
def get_resource_hander(request, resource_id):
|
||||
resource = {'resource_id': resource_id}
|
||||
return json(resource)
|
||||
|
||||
bp_resources_group = Blueprint.group(bp_resources, bp_resource,
|
||||
url_prefix='/resources')
|
||||
bp_api_v1 = Blueprint('bp_api_v1')
|
||||
|
||||
@bp_api_v1.get('/info')
|
||||
def api_v1_info(request):
|
||||
return text('api_version: v1')
|
||||
|
||||
bp_api_v1_group = Blueprint.group(bp_api_v1, bp_resources_group,
|
||||
url_prefix='/v1')
|
||||
bp_api_group = Blueprint.group(bp_api_v1_group, url_prefix='/api')
|
||||
app.blueprint(bp_api_group)
|
||||
|
||||
request, response = app.test_client.get('/api/v1/info')
|
||||
assert response.text == 'api_version: v1'
|
||||
|
||||
request, response = app.test_client.get('/api/v1/resources')
|
||||
assert response.json == [{}]
|
||||
|
||||
from uuid import uuid4
|
||||
resource_id = str(uuid4())
|
||||
request, response = app.test_client.get(
|
||||
'/api/v1/resources/{0}'.format(resource_id))
|
||||
assert response.json == {'resource_id': resource_id}
|
||||
|
||||
|
||||
def test_blueprint_middleware_with_args(app: Sanic):
|
||||
bp = Blueprint(name="with_args_bp", url_prefix="/wa")
|
||||
|
||||
@bp.middleware
|
||||
def middleware_with_no_tag(request: Request):
|
||||
if request.headers.get("content-type") == "application/json":
|
||||
request.headers["accepts"] = "plain/text"
|
||||
else:
|
||||
request.headers["accepts"] = "application/json"
|
||||
|
||||
@bp.route("/")
|
||||
def default_route(request):
|
||||
if request.headers.get("accepts") == "application/json":
|
||||
return json({"test": "value"})
|
||||
else:
|
||||
return text("value")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
_, response = app.test_client.get("/wa", headers={"content-type": "application/json"})
|
||||
assert response.text == "value"
|
||||
|
||||
_, response = app.test_client.get("/wa", headers={"content-type": "plain/text"})
|
||||
assert response.json.get("test") == "value"
|
||||
d = {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name',
|
||||
['test.file'])
|
||||
def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
with open(current_file, 'rb') as file:
|
||||
current_file_contents = file.read()
|
||||
|
||||
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
|
||||
|
||||
bp.static(
|
||||
"/test.file/",
|
||||
get_file_path(static_file_directory, file_name),
|
||||
name="static.testing",
|
||||
strict_slashes=True)
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
uri = app.url_for('static', name='static.testing')
|
||||
assert uri == "/static/test.file"
|
||||
|
||||
_, response = app.test_client.get("/static/test.file")
|
||||
assert response.status == 404
|
||||
|
||||
_, response = app.test_client.get("/static/test.file/")
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_route_handler_add(app: Sanic):
|
||||
view = CompositionView()
|
||||
|
||||
async def get_handler(request):
|
||||
return json({
|
||||
"response": "OK"
|
||||
})
|
||||
|
||||
view.add(["GET"], get_handler, stream=False)
|
||||
|
||||
async def default_handler(request):
|
||||
return text("OK")
|
||||
|
||||
bp = Blueprint(name="handler", url_prefix="/handler")
|
||||
bp.add_route(
|
||||
default_handler,
|
||||
uri="/default/",
|
||||
strict_slashes=True)
|
||||
|
||||
bp.add_route(view, uri="/view", name="test")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
_, response = app.test_client.get("/handler/default/")
|
||||
assert response.text == "OK"
|
||||
|
||||
_, response = app.test_client.get("/handler/view")
|
||||
assert response.json["response"] == "OK"
|
||||
|
||||
|
||||
def test_websocket_route(app: Sanic):
|
||||
event = asyncio.Event()
|
||||
|
||||
async def websocket_handler(request, ws):
|
||||
assert ws.subprotocol is None
|
||||
event.set()
|
||||
|
||||
bp = Blueprint(name="handler", url_prefix="/ws")
|
||||
bp.add_websocket_route(websocket_handler, "/test", name="test")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
_, response = app.test_client.get("/ws/test", headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13'
|
||||
})
|
||||
assert response.status == 101
|
||||
assert event.is_set()
|
||||
|
||||
|
||||
def test_duplicate_blueprint(app):
|
||||
bp_name = 'bp'
|
||||
bp = Blueprint(bp_name)
|
||||
bp1 = Blueprint(bp_name)
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
with pytest.raises(AssertionError) as excinfo:
|
||||
app.blueprint(bp1)
|
||||
|
||||
assert str(excinfo.value) == (
|
||||
'A blueprint with the name "{}" is already registered. '
|
||||
'Blueprint names must be unique.'
|
||||
).format(bp_name)
|
||||
|
||||
|
||||
@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"
|
||||
)
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
from tempfile import TemporaryDirectory
|
||||
from textwrap import dedent
|
||||
import pytest
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import PyFileError
|
||||
|
||||
|
||||
def test_load_from_object():
|
||||
app = Sanic('test_load_from_object')
|
||||
@contextmanager
|
||||
def temp_path():
|
||||
""" a simple cross platform replacement for NamedTemporaryFile """
|
||||
with TemporaryDirectory() as td:
|
||||
yield Path(td, 'file')
|
||||
|
||||
|
||||
def test_load_from_object(app):
|
||||
class Config:
|
||||
not_for_config = 'should not be used'
|
||||
CONFIG_VALUE = 'should be used'
|
||||
@@ -16,36 +26,52 @@ def test_load_from_object():
|
||||
assert app.config.CONFIG_VALUE == 'should be used'
|
||||
assert 'not_for_config' not in app.config
|
||||
|
||||
|
||||
def test_auto_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic()
|
||||
assert app.config.TEST_ANSWER == 42
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
|
||||
def test_dont_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic(load_env=False)
|
||||
assert getattr(app.config, 'TEST_ANSWER', None) == None
|
||||
assert getattr(app.config, 'TEST_ANSWER', None) is None
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
|
||||
def test_load_env_prefix():
|
||||
environ["MYAPP_TEST_ANSWER"] = "42"
|
||||
app = Sanic(load_env='MYAPP_')
|
||||
assert app.config.TEST_ANSWER == 42
|
||||
del environ["MYAPP_TEST_ANSWER"]
|
||||
|
||||
def test_load_from_file():
|
||||
app = Sanic('test_load_from_file')
|
||||
config = b"""
|
||||
VALUE = 'some value'
|
||||
condition = 1 == 1
|
||||
if condition:
|
||||
CONDITIONAL = 'should be set'
|
||||
"""
|
||||
with NamedTemporaryFile() as config_file:
|
||||
config_file.write(config)
|
||||
config_file.seek(0)
|
||||
app.config.from_pyfile(config_file.name)
|
||||
|
||||
def test_load_env_prefix_float_values():
|
||||
environ["MYAPP_TEST_ROI"] = "2.3"
|
||||
app = Sanic(load_env="MYAPP_")
|
||||
assert app.config.TEST_ROI == 2.3
|
||||
del environ["MYAPP_TEST_ROI"]
|
||||
|
||||
|
||||
def test_load_env_prefix_string_value():
|
||||
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
|
||||
app = Sanic(load_env="MYAPP_")
|
||||
assert app.config.TEST_TOKEN == "somerandomtesttoken"
|
||||
del environ["MYAPP_TEST_TOKEN"]
|
||||
|
||||
|
||||
def test_load_from_file(app):
|
||||
config = dedent("""
|
||||
VALUE = 'some value'
|
||||
condition = 1 == 1
|
||||
if condition:
|
||||
CONDITIONAL = 'should be set'
|
||||
""")
|
||||
with temp_path() as config_path:
|
||||
config_path.write_text(config)
|
||||
app.config.from_pyfile(str(config_path))
|
||||
assert 'VALUE' in app.config
|
||||
assert app.config.VALUE == 'some value'
|
||||
assert 'CONDITIONAL' in app.config
|
||||
@@ -53,33 +79,41 @@ if condition:
|
||||
assert 'condition' not in app.config
|
||||
|
||||
|
||||
def test_load_from_missing_file():
|
||||
app = Sanic('test_load_from_missing_file')
|
||||
def test_load_from_missing_file(app):
|
||||
with pytest.raises(IOError):
|
||||
app.config.from_pyfile('non-existent file')
|
||||
|
||||
|
||||
def test_load_from_envvar():
|
||||
app = Sanic('test_load_from_envvar')
|
||||
config = b"VALUE = 'some value'"
|
||||
with NamedTemporaryFile() as config_file:
|
||||
config_file.write(config)
|
||||
config_file.seek(0)
|
||||
environ['APP_CONFIG'] = config_file.name
|
||||
def test_load_from_envvar(app):
|
||||
config = "VALUE = 'some value'"
|
||||
with temp_path() as config_path:
|
||||
config_path.write_text(config)
|
||||
environ['APP_CONFIG'] = str(config_path)
|
||||
app.config.from_envvar('APP_CONFIG')
|
||||
assert 'VALUE' in app.config
|
||||
assert app.config.VALUE == 'some value'
|
||||
|
||||
|
||||
def test_load_from_missing_envvar():
|
||||
app = Sanic('test_load_from_missing_envvar')
|
||||
with pytest.raises(RuntimeError):
|
||||
def test_load_from_missing_envvar(app):
|
||||
with pytest.raises(RuntimeError) as e:
|
||||
app.config.from_envvar('non-existent variable')
|
||||
assert str(e.value) == ("The environment variable 'non-existent "
|
||||
"variable' is not set and thus configuration "
|
||||
"could not be loaded.")
|
||||
|
||||
|
||||
def test_overwrite_exisiting_config():
|
||||
app = Sanic('test_overwrite_exisiting_config')
|
||||
def test_load_config_from_file_invalid_syntax(app):
|
||||
config = "VALUE = some value"
|
||||
with temp_path() as config_path:
|
||||
config_path.write_text(config)
|
||||
|
||||
with pytest.raises(PyFileError):
|
||||
app.config.from_pyfile(config_path)
|
||||
|
||||
|
||||
def test_overwrite_exisiting_config(app):
|
||||
app.config.DEFAULT = 1
|
||||
|
||||
class Config:
|
||||
DEFAULT = 2
|
||||
|
||||
@@ -87,7 +121,17 @@ def test_overwrite_exisiting_config():
|
||||
assert app.config.DEFAULT == 2
|
||||
|
||||
|
||||
def test_missing_config():
|
||||
app = Sanic('test_missing_config')
|
||||
with pytest.raises(AttributeError):
|
||||
def test_overwrite_exisiting_config_ignore_lowercase(app):
|
||||
app.config.default = 1
|
||||
|
||||
class Config:
|
||||
default = 2
|
||||
|
||||
app.config.from_object(Config)
|
||||
assert app.config.default == 1
|
||||
|
||||
|
||||
def test_missing_config(app):
|
||||
with pytest.raises(AttributeError) as e:
|
||||
app.config.NON_EXISTENT
|
||||
assert str(e.value) == ("Config has no 'NON_EXISTENT'")
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
from datetime import datetime, timedelta
|
||||
from http.cookies import SimpleCookie
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.response import text
|
||||
import pytest
|
||||
|
||||
from sanic.cookies import Cookie
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_cookies():
|
||||
app = Sanic('test_text')
|
||||
def test_cookies(app):
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
@@ -30,8 +28,7 @@ def test_cookies():
|
||||
(False, False),
|
||||
(True, True),
|
||||
])
|
||||
def test_false_cookies_encoded(httponly, expected):
|
||||
app = Sanic('test_text')
|
||||
def test_false_cookies_encoded(app, httponly, expected):
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
@@ -49,8 +46,7 @@ def test_false_cookies_encoded(httponly, expected):
|
||||
(False, False),
|
||||
(True, True),
|
||||
])
|
||||
def test_false_cookies(httponly, expected):
|
||||
app = Sanic('test_text')
|
||||
def test_false_cookies(app, httponly, expected):
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
@@ -65,8 +61,8 @@ def test_false_cookies(httponly, expected):
|
||||
|
||||
assert ('HttpOnly' in response_cookies['right_back'].output()) == expected
|
||||
|
||||
def test_http2_cookies():
|
||||
app = Sanic('test_http2_cookies')
|
||||
|
||||
def test_http2_cookies(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -78,15 +74,16 @@ def test_http2_cookies():
|
||||
|
||||
assert response.text == 'Cookies are: working!'
|
||||
|
||||
def test_cookie_options():
|
||||
app = Sanic('test_text')
|
||||
|
||||
def test_cookie_options(app):
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
response = text("OK")
|
||||
response.cookies['test'] = 'at you'
|
||||
response.cookies['test']['httponly'] = True
|
||||
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
|
||||
response.cookies['test']['expires'] = (datetime.now() +
|
||||
timedelta(seconds=10))
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
@@ -94,10 +91,10 @@ def test_cookie_options():
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
assert response_cookies['test'].value == 'at you'
|
||||
assert response_cookies['test']['httponly'] == True
|
||||
assert response_cookies['test']['httponly'] is True
|
||||
|
||||
def test_cookie_deletion():
|
||||
app = Sanic('test_text')
|
||||
|
||||
def test_cookie_deletion(app):
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
@@ -113,4 +110,82 @@ def test_cookie_deletion():
|
||||
|
||||
assert int(response_cookies['i_want_to_die']['max-age']) == 0
|
||||
with pytest.raises(KeyError):
|
||||
hold_my_beer = response.cookies['i_never_existed']
|
||||
response.cookies['i_never_existed']
|
||||
|
||||
|
||||
def test_cookie_reserved_cookie():
|
||||
with pytest.raises(expected_exception=KeyError) as e:
|
||||
Cookie("domain", "testdomain.com")
|
||||
assert e.message == "Cookie name is a reserved word"
|
||||
|
||||
|
||||
def test_cookie_illegal_key_format():
|
||||
with pytest.raises(expected_exception=KeyError) as e:
|
||||
Cookie("testå", "test")
|
||||
assert e.message == "Cookie key contains illegal characters"
|
||||
|
||||
|
||||
def test_cookie_set_unknown_property():
|
||||
c = Cookie("test_cookie", "value")
|
||||
with pytest.raises(expected_exception=KeyError) as e:
|
||||
c["invalid"] = "value"
|
||||
assert e.message == "Unknown cookie property"
|
||||
|
||||
|
||||
def test_cookie_set_same_key(app):
|
||||
|
||||
cookies = {'test': 'wait'}
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
response = text('pass')
|
||||
response.cookies['test'] = 'modified'
|
||||
response.cookies['test'] = 'pass'
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get('/', cookies=cookies)
|
||||
assert response.status == 200
|
||||
assert response.cookies['test'].value == 'pass'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('max_age', ['0', 30, '30'])
|
||||
def test_cookie_max_age(app, max_age):
|
||||
cookies = {'test': 'wait'}
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
response = text('pass')
|
||||
response.cookies['test'] = 'pass'
|
||||
response.cookies['test']['max-age'] = max_age
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get('/', cookies=cookies)
|
||||
assert response.status == 200
|
||||
|
||||
assert response.cookies['test'].value == 'pass'
|
||||
assert response.cookies['test']['max-age'] == str(max_age)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('expires', [
|
||||
datetime.now() + timedelta(seconds=60),
|
||||
'Fri, 21-Dec-2018 15:30:00 GMT'
|
||||
])
|
||||
def test_cookie_expires(app, expires):
|
||||
cookies = {'test': 'wait'}
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
response = text('pass')
|
||||
response.cookies['test'] = 'pass'
|
||||
response.cookies['test']['expires'] = expires
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get('/', cookies=cookies)
|
||||
assert response.status == 200
|
||||
|
||||
assert response.cookies['test'].value == 'pass'
|
||||
|
||||
if isinstance(expires, datetime):
|
||||
expires = expires.strftime("%a, %d-%b-%Y %T GMT")
|
||||
|
||||
assert response.cookies['test']['expires'] == expires
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from threading import Event
|
||||
import asyncio
|
||||
from queue import Queue
|
||||
|
||||
|
||||
def test_create_task():
|
||||
def test_create_task(app):
|
||||
e = Event()
|
||||
|
||||
async def coro():
|
||||
await asyncio.sleep(0.05)
|
||||
e.set()
|
||||
|
||||
app = Sanic('test_create_task')
|
||||
app.add_task(coro)
|
||||
|
||||
@app.route('/early')
|
||||
@@ -30,8 +28,8 @@ def test_create_task():
|
||||
request, response = app.test_client.get('/late')
|
||||
assert response.body == b'True'
|
||||
|
||||
def test_create_task_with_app_arg():
|
||||
app = Sanic('test_add_task')
|
||||
|
||||
def test_create_task_with_app_arg(app):
|
||||
q = Queue()
|
||||
|
||||
@app.route('/')
|
||||
@@ -44,4 +42,4 @@ def test_create_task_with_app_arg():
|
||||
app.add_task(coro)
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert q.get() == 'test_add_task'
|
||||
assert q.get() == 'test_create_task_with_app_arg'
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
from sanic import Sanic
|
||||
from sanic.server import HttpProtocol
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('test_custom_porotocol')
|
||||
|
||||
|
||||
class CustomHttpProtocol(HttpProtocol):
|
||||
|
||||
@@ -16,12 +13,12 @@ class CustomHttpProtocol(HttpProtocol):
|
||||
self.transport.close()
|
||||
|
||||
|
||||
@app.route('/1')
|
||||
async def handler_1(request):
|
||||
return 'OK'
|
||||
def test_use_custom_protocol(app):
|
||||
|
||||
@app.route('/1')
|
||||
async def handler_1(request):
|
||||
return 'OK'
|
||||
|
||||
def test_use_custom_protocol():
|
||||
server_kwargs = {
|
||||
'protocol': CustomHttpProtocol
|
||||
}
|
||||
|
||||
53
tests/test_custom_request.py
Normal file
53
tests/test_custom_request.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from io import BytesIO
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.request import Request
|
||||
from sanic.response import json_dumps, text
|
||||
|
||||
|
||||
class CustomRequest(Request):
|
||||
__slots__ = ("body_buffer",)
|
||||
|
||||
def body_init(self):
|
||||
self.body_buffer = BytesIO()
|
||||
|
||||
def body_push(self, data):
|
||||
self.body_buffer.write(data)
|
||||
|
||||
def body_finish(self):
|
||||
self.body = self.body_buffer.getvalue()
|
||||
self.body_buffer.close()
|
||||
|
||||
|
||||
def test_custom_request():
|
||||
app = Sanic(request_class=CustomRequest)
|
||||
|
||||
@app.route("/post", methods=["POST"])
|
||||
async def post_handler(request):
|
||||
return text("OK")
|
||||
|
||||
@app.route("/get")
|
||||
async def get_handler(request):
|
||||
return text("OK")
|
||||
|
||||
payload = {"test": "OK"}
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
request, response = app.test_client.post(
|
||||
"/post", data=json_dumps(payload), headers=headers
|
||||
)
|
||||
|
||||
assert isinstance(request.body_buffer, BytesIO)
|
||||
assert request.body_buffer.closed
|
||||
assert request.body == b'{"test":"OK"}'
|
||||
assert request.json.get("test") == "OK"
|
||||
assert response.text == "OK"
|
||||
assert response.status == 200
|
||||
|
||||
request, response = app.test_client.get("/get")
|
||||
|
||||
assert isinstance(request.body_buffer, BytesIO)
|
||||
assert request.body_buffer.closed
|
||||
assert request.body == b""
|
||||
assert response.text == "OK"
|
||||
assert response.status == 200
|
||||
@@ -1,4 +1,3 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.router import RouteExists
|
||||
import pytest
|
||||
@@ -10,8 +9,7 @@ import pytest
|
||||
("put", "text", "OK2 test"),
|
||||
("delete", "status", 405),
|
||||
])
|
||||
def test_overload_dynamic_routes(method, attr, expected):
|
||||
app = Sanic('test_dynamic_route')
|
||||
def test_overload_dynamic_routes(app, method, attr, expected):
|
||||
|
||||
@app.route('/overload/<param>', methods=['GET'])
|
||||
async def handler1(request, param):
|
||||
@@ -25,8 +23,7 @@ def test_overload_dynamic_routes(method, attr, expected):
|
||||
assert getattr(response, attr) == expected
|
||||
|
||||
|
||||
def test_overload_dynamic_routes_exist():
|
||||
app = Sanic('test_dynamic_route')
|
||||
def test_overload_dynamic_routes_exist(app):
|
||||
|
||||
@app.route('/overload/<param>', methods=['GET'])
|
||||
async def handler1(request, param):
|
||||
|
||||
@@ -66,6 +66,10 @@ def exception_app():
|
||||
abort(500)
|
||||
return text("OK")
|
||||
|
||||
@app.route('/abort/message')
|
||||
def handler_abort_message(request):
|
||||
abort(500, message='Abort')
|
||||
|
||||
@app.route('/divide_by_zero')
|
||||
def handle_unhandled_exception(request):
|
||||
1 / 0
|
||||
@@ -81,8 +85,7 @@ def exception_app():
|
||||
return app
|
||||
|
||||
|
||||
def test_catch_exception_list():
|
||||
app = Sanic('exception_list')
|
||||
def test_catch_exception_list(app):
|
||||
|
||||
@app.exception([SanicExceptionTestException, NotFound])
|
||||
def exception_list(request, exception):
|
||||
@@ -202,3 +205,7 @@ def test_abort(exception_app):
|
||||
|
||||
request, response = exception_app.test_client.get('/abort')
|
||||
assert response.status == 500
|
||||
|
||||
request, response = exception_app.test_client.get('/abort/message')
|
||||
assert response.status == 500
|
||||
assert response.text == 'Error: Abort'
|
||||
|
||||
@@ -131,7 +131,7 @@ def test_exception_handler_lookup():
|
||||
|
||||
try:
|
||||
ModuleNotFoundError
|
||||
except:
|
||||
except Exception:
|
||||
class ModuleNotFoundError(ImportError):
|
||||
pass
|
||||
|
||||
|
||||
74
tests/test_helpers.py
Normal file
74
tests/test_helpers.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from sanic import helpers
|
||||
|
||||
|
||||
def test_has_message_body():
|
||||
tests = (
|
||||
(100, False),
|
||||
(102, False),
|
||||
(204, False),
|
||||
(200, True),
|
||||
(304, False),
|
||||
(400, True),
|
||||
)
|
||||
for status_code, expected in tests:
|
||||
assert helpers.has_message_body(status_code) is expected
|
||||
|
||||
|
||||
def test_is_entity_header():
|
||||
tests = (
|
||||
("allow", True),
|
||||
("extension-header", True),
|
||||
("", False),
|
||||
("test", False),
|
||||
)
|
||||
for header, expected in tests:
|
||||
assert helpers.is_entity_header(header) is expected
|
||||
|
||||
|
||||
def test_is_hop_by_hop_header():
|
||||
tests = (
|
||||
("connection", True),
|
||||
("upgrade", True),
|
||||
("", False),
|
||||
("test", False),
|
||||
)
|
||||
for header, expected in tests:
|
||||
assert helpers.is_hop_by_hop_header(header) is expected
|
||||
|
||||
|
||||
def test_remove_entity_headers():
|
||||
tests = (
|
||||
(
|
||||
{},
|
||||
{}
|
||||
),
|
||||
(
|
||||
{
|
||||
"Allow": "GET, POST, HEAD",
|
||||
},
|
||||
{}
|
||||
),
|
||||
(
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"Expires": "Wed, 21 Oct 2015 07:28:00 GMT",
|
||||
"Foo": "Bar"
|
||||
},
|
||||
{
|
||||
"Expires": "Wed, 21 Oct 2015 07:28:00 GMT",
|
||||
"Foo": "Bar"
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
"Allow": "GET, POST, HEAD",
|
||||
"Content-Location": "/test"
|
||||
},
|
||||
{
|
||||
"Content-Location": "/test"
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
for header, expected in tests:
|
||||
assert helpers.remove_entity_headers(header) == expected
|
||||
@@ -9,60 +9,22 @@ import aiohttp
|
||||
from aiohttp import TCPConnector
|
||||
from sanic.testing import SanicTestClient, HOST, PORT
|
||||
|
||||
try:
|
||||
try:
|
||||
import packaging # direct use
|
||||
except ImportError:
|
||||
# setuptools v39.0 and above.
|
||||
try:
|
||||
from setuptools.extern import packaging
|
||||
except ImportError:
|
||||
# Before setuptools v39.0
|
||||
from pkg_resources.extern import packaging
|
||||
version = packaging.version
|
||||
except ImportError:
|
||||
raise RuntimeError("The 'packaging' library is missing.")
|
||||
|
||||
aiohttp_version = version.parse(aiohttp.__version__)
|
||||
|
||||
class ReuseableTCPConnector(TCPConnector):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
|
||||
self.old_proto = None
|
||||
|
||||
if aiohttp_version >= version.parse('3.3.0'):
|
||||
async def connect(self, req, traces, timeout):
|
||||
new_conn = await super(ReuseableTCPConnector, self)\
|
||||
.connect(req, traces, timeout)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!")
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
elif aiohttp_version >= version.parse('3.0.0'):
|
||||
async def connect(self, req, traces=None):
|
||||
new_conn = await super(ReuseableTCPConnector, self)\
|
||||
.connect(req, traces=traces)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!")
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
else:
|
||||
async def connect(self, req):
|
||||
new_conn = await super(ReuseableTCPConnector, self)\
|
||||
.connect(req)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!")
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
async def connect(self, req, *args, **kwargs):
|
||||
new_conn = await super(ReuseableTCPConnector, self)\
|
||||
.connect(req, *args, **kwargs)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!")
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
|
||||
|
||||
class ReuseableSanicTestClient(SanicTestClient):
|
||||
@@ -147,14 +109,14 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
|
||||
@@ -178,10 +140,11 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
if self._tcp_connector:
|
||||
conn = self._tcp_connector
|
||||
else:
|
||||
conn = ReuseableTCPConnector(verify_ssl=False,
|
||||
loop=self._loop,
|
||||
keepalive_timeout=
|
||||
request_keepalive)
|
||||
conn = ReuseableTCPConnector(
|
||||
ssl=False,
|
||||
loop=self._loop,
|
||||
keepalive_timeout=request_keepalive
|
||||
)
|
||||
self._tcp_connector = conn
|
||||
session = aiohttp.ClientSession(cookies=cookies,
|
||||
connector=conn,
|
||||
|
||||
@@ -11,6 +11,7 @@ import sanic
|
||||
from sanic.response import text
|
||||
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
||||
from sanic import Sanic
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
logging_format = '''module: %(module)s; \
|
||||
@@ -23,7 +24,7 @@ def reset_logging():
|
||||
reload(logging)
|
||||
|
||||
|
||||
def test_log():
|
||||
def test_log(app):
|
||||
log_stream = StringIO()
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
@@ -33,7 +34,6 @@ def test_log():
|
||||
stream=log_stream
|
||||
)
|
||||
log = logging.getLogger()
|
||||
app = Sanic('test_logging')
|
||||
rand_string = str(uuid.uuid4())
|
||||
|
||||
@app.route('/')
|
||||
@@ -47,10 +47,10 @@ def test_log():
|
||||
|
||||
|
||||
def test_logging_defaults():
|
||||
reset_logging()
|
||||
# reset_logging()
|
||||
app = Sanic("test_logging")
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic.root').handlers]:
|
||||
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
|
||||
@@ -61,7 +61,7 @@ def test_logging_defaults():
|
||||
|
||||
|
||||
def test_logging_pass_customer_logconfig():
|
||||
reset_logging()
|
||||
# reset_logging()
|
||||
|
||||
modified_config = LOGGING_CONFIG_DEFAULTS
|
||||
modified_config['formatters']['generic']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
|
||||
@@ -69,7 +69,7 @@ def test_logging_pass_customer_logconfig():
|
||||
|
||||
app = Sanic("test_logging", log_config=modified_config)
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic.root').handlers]:
|
||||
assert fmt._fmt == modified_config['formatters']['generic']['format']
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
|
||||
@@ -80,11 +80,10 @@ def test_logging_pass_customer_logconfig():
|
||||
|
||||
|
||||
@pytest.mark.parametrize('debug', (True, False, ))
|
||||
def test_log_connection_lost(debug, monkeypatch):
|
||||
def test_log_connection_lost(app, debug, monkeypatch):
|
||||
""" Should not log Connection lost exception on non debug """
|
||||
app = Sanic('connection_lost')
|
||||
stream = StringIO()
|
||||
root = logging.getLogger('root')
|
||||
root = logging.getLogger('sanic.root')
|
||||
root.addHandler(logging.StreamHandler(stream))
|
||||
monkeypatch.setattr(sanic.server, 'logger', root)
|
||||
|
||||
@@ -104,3 +103,33 @@ def test_log_connection_lost(debug, monkeypatch):
|
||||
assert 'Connection lost before response written @' in log
|
||||
else:
|
||||
assert 'Connection lost before response written @' not in log
|
||||
|
||||
|
||||
def test_logger(caplog):
|
||||
rand_string = str(uuid.uuid4())
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.get('/')
|
||||
def log_info(request):
|
||||
logger.info(rand_string)
|
||||
return text('hello')
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert caplog.record_tuples[0] == ('sanic.root', logging.INFO, 'Goin\' Fast @ http://127.0.0.1:42101')
|
||||
assert caplog.record_tuples[1] == ('sanic.root', logging.INFO, 'http://127.0.0.1:42101/')
|
||||
assert caplog.record_tuples[2] == ('sanic.root', logging.INFO, rand_string)
|
||||
assert caplog.record_tuples[-1] == ('sanic.root', logging.INFO, 'Server Stopped')
|
||||
|
||||
|
||||
def test_logging_modified_root_logger_config():
|
||||
# reset_logging()
|
||||
|
||||
modified_config = LOGGING_CONFIG_DEFAULTS
|
||||
modified_config['loggers']['sanic.root']['level'] = 'DEBUG'
|
||||
|
||||
app = Sanic("test_logging", log_config=modified_config)
|
||||
|
||||
assert logging.getLogger('sanic.root').getEffectiveLevel() == logging.DEBUG
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.request import Request
|
||||
from sanic.response import json, text, HTTPResponse
|
||||
from sanic.exceptions import NotFound
|
||||
import logging
|
||||
from asyncio import CancelledError
|
||||
|
||||
from sanic.exceptions import NotFound
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, text
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_middleware_request():
|
||||
app = Sanic('test_middleware_request')
|
||||
|
||||
def test_middleware_request(app):
|
||||
results = []
|
||||
|
||||
@app.middleware
|
||||
async def handler(request):
|
||||
async def handler1(request):
|
||||
results.append(request)
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
async def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
@@ -28,13 +26,11 @@ def test_middleware_request():
|
||||
assert type(results[0]) is Request
|
||||
|
||||
|
||||
def test_middleware_response():
|
||||
app = Sanic('test_middleware_response')
|
||||
|
||||
def test_middleware_response(app):
|
||||
results = []
|
||||
|
||||
@app.middleware('request')
|
||||
async def process_response(request):
|
||||
async def process_request(request):
|
||||
results.append(request)
|
||||
|
||||
@app.middleware('response')
|
||||
@@ -54,8 +50,7 @@ def test_middleware_response():
|
||||
assert isinstance(results[2], HTTPResponse)
|
||||
|
||||
|
||||
def test_middleware_response_exception():
|
||||
app = Sanic('test_middleware_response_exception')
|
||||
def test_middleware_response_exception(app):
|
||||
result = {'status_code': None}
|
||||
|
||||
@app.middleware('response')
|
||||
@@ -75,8 +70,53 @@ def test_middleware_response_exception():
|
||||
assert response.text == 'OK'
|
||||
assert result['status_code'] == 404
|
||||
|
||||
def test_middleware_override_request():
|
||||
app = Sanic('test_middleware_override_request')
|
||||
|
||||
def test_middleware_response_raise_cancelled_error(app, caplog):
|
||||
|
||||
@app.middleware('response')
|
||||
async def process_response(request, response):
|
||||
raise CancelledError('CancelledError at response middleware')
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
caplog.set_level(logging.ERROR, logger="sanic.root")
|
||||
with caplog.at_level(logging.ERROR):
|
||||
reqrequest, response = app.test_client.get('/')
|
||||
|
||||
assert response.status == 503
|
||||
assert caplog.record_tuples[0] == (
|
||||
'sanic.root',
|
||||
logging.ERROR,
|
||||
'Exception occurred while handling uri: \'http://127.0.0.1:42101/\''
|
||||
)
|
||||
|
||||
|
||||
def test_middleware_response_raise_exception(app, caplog):
|
||||
|
||||
@app.middleware('response')
|
||||
async def process_response(request, response):
|
||||
raise Exception('Exception at response middleware')
|
||||
|
||||
caplog.set_level(logging.ERROR, logger="sanic.root")
|
||||
with caplog.at_level(logging.ERROR):
|
||||
reqrequest, response = app.test_client.get('/')
|
||||
|
||||
assert response.status == 404
|
||||
assert caplog.record_tuples[0] == (
|
||||
'sanic.root',
|
||||
logging.ERROR,
|
||||
'Exception occurred while handling uri: \'http://127.0.0.1:42101/\''
|
||||
)
|
||||
assert caplog.record_tuples[1] == (
|
||||
'sanic.error',
|
||||
logging.ERROR,
|
||||
'Exception occurred in one of response middleware handlers'
|
||||
)
|
||||
|
||||
|
||||
def test_middleware_override_request(app):
|
||||
|
||||
@app.middleware
|
||||
async def halt_request(request):
|
||||
@@ -92,8 +132,7 @@ def test_middleware_override_request():
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_middleware_override_response():
|
||||
app = Sanic('test_middleware_override_response')
|
||||
def test_middleware_override_response(app):
|
||||
|
||||
@app.middleware('response')
|
||||
async def process_response(request, response):
|
||||
@@ -109,10 +148,7 @@ def test_middleware_override_response():
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
|
||||
def test_middleware_order():
|
||||
app = Sanic('test_middleware_order')
|
||||
|
||||
def test_middleware_order(app):
|
||||
order = []
|
||||
|
||||
@app.middleware('request')
|
||||
@@ -146,4 +182,4 @@ def test_middleware_order():
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.status == 200
|
||||
assert order == [1,2,3,4,5,6]
|
||||
assert order == [1, 2, 3, 4, 5, 6]
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import multiprocessing
|
||||
import random
|
||||
import signal
|
||||
import pickle
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.testing import HOST, PORT
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
def test_multiprocessing():
|
||||
@pytest.mark.skipif(
|
||||
not hasattr(signal, 'SIGALRM'),
|
||||
reason='SIGALRM is not implemented for this platform, we have to come '
|
||||
'up with another timeout strategy to test these'
|
||||
)
|
||||
def test_multiprocessing(app):
|
||||
"""Tests that the number of children we produce is correct"""
|
||||
# Selects a number at random so we can spot check
|
||||
num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1))
|
||||
app = Sanic('test_multiprocessing')
|
||||
num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1))
|
||||
process_list = set()
|
||||
|
||||
def stop_on_alarm(*args):
|
||||
@@ -23,3 +29,58 @@ def test_multiprocessing():
|
||||
app.run(HOST, PORT, workers=num_workers)
|
||||
|
||||
assert len(process_list) == num_workers
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not hasattr(signal, 'SIGALRM'),
|
||||
reason='SIGALRM is not implemented for this platform',
|
||||
)
|
||||
def test_multiprocessing_with_blueprint(app):
|
||||
from sanic import Blueprint
|
||||
# Selects a number at random so we can spot check
|
||||
num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1))
|
||||
process_list = set()
|
||||
|
||||
def stop_on_alarm(*args):
|
||||
for process in multiprocessing.active_children():
|
||||
process_list.add(process.pid)
|
||||
process.terminate()
|
||||
|
||||
signal.signal(signal.SIGALRM, stop_on_alarm)
|
||||
signal.alarm(3)
|
||||
|
||||
bp = Blueprint('test_text')
|
||||
app.blueprint(bp)
|
||||
app.run(HOST, PORT, workers=num_workers)
|
||||
|
||||
assert len(process_list) == num_workers
|
||||
|
||||
|
||||
# this function must be outside a test function so that it can be
|
||||
# able to be pickled (local functions cannot be pickled).
|
||||
def handler(request):
|
||||
return text('Hello')
|
||||
|
||||
# Muliprocessing on Windows requires app to be able to be pickled
|
||||
@pytest.mark.parametrize('protocol', [3, 4])
|
||||
def test_pickle_app(app, protocol):
|
||||
app.route('/')(handler)
|
||||
p_app = pickle.dumps(app, protocol=protocol)
|
||||
up_p_app = pickle.loads(p_app)
|
||||
assert up_p_app
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'Hello'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('protocol', [3, 4])
|
||||
def test_pickle_app_with_bp(app, protocol):
|
||||
from sanic import Blueprint
|
||||
bp = Blueprint('test_text')
|
||||
bp.route('/')(handler)
|
||||
app.blueprint(bp)
|
||||
p_app = pickle.dumps(app, protocol=protocol)
|
||||
up_p_app = pickle.loads(p_app)
|
||||
assert up_p_app
|
||||
request, response = app.test_client.get('/')
|
||||
assert app.is_request_stream is False
|
||||
assert response.text == 'Hello'
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import URLBuildError
|
||||
@@ -16,8 +15,7 @@ from sanic.constants import HTTP_METHODS
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_versioned_named_routes_get(method):
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
def test_versioned_named_routes_get(app, method):
|
||||
bp = Blueprint('test_bp', url_prefix='/bp')
|
||||
|
||||
method = method.lower()
|
||||
@@ -57,8 +55,7 @@ def test_versioned_named_routes_get(method):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_default_routes_get():
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
def test_shorthand_default_routes_get(app):
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
@@ -68,8 +65,7 @@ def test_shorthand_default_routes_get():
|
||||
assert app.url_for('handler') == '/get'
|
||||
|
||||
|
||||
def test_shorthand_named_routes_get():
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
def test_shorthand_named_routes_get(app):
|
||||
bp = Blueprint('test_bp', url_prefix='/bp')
|
||||
|
||||
@app.get('/get', name='route_get')
|
||||
@@ -93,8 +89,7 @@ def test_shorthand_named_routes_get():
|
||||
app.url_for('test_bp.handler2')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_post():
|
||||
app = Sanic('test_shorhand_routes_post')
|
||||
def test_shorthand_named_routes_post(app):
|
||||
|
||||
@app.post('/post', name='route_name')
|
||||
def handler(request):
|
||||
@@ -106,8 +101,7 @@ def test_shorthand_named_routes_post():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_put():
|
||||
app = Sanic('test_shorhand_routes_put')
|
||||
def test_shorthand_named_routes_put(app):
|
||||
|
||||
@app.put('/put', name='route_put')
|
||||
def handler(request):
|
||||
@@ -121,8 +115,7 @@ def test_shorthand_named_routes_put():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_delete():
|
||||
app = Sanic('test_shorhand_routes_delete')
|
||||
def test_shorthand_named_routes_delete(app):
|
||||
|
||||
@app.delete('/delete', name='route_delete')
|
||||
def handler(request):
|
||||
@@ -136,8 +129,7 @@ def test_shorthand_named_routes_delete():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_patch():
|
||||
app = Sanic('test_shorhand_routes_patch')
|
||||
def test_shorthand_named_routes_patch(app):
|
||||
|
||||
@app.patch('/patch', name='route_patch')
|
||||
def handler(request):
|
||||
@@ -151,8 +143,7 @@ def test_shorthand_named_routes_patch():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_head():
|
||||
app = Sanic('test_shorhand_routes_head')
|
||||
def test_shorthand_named_routes_head(app):
|
||||
|
||||
@app.head('/head', name='route_head')
|
||||
def handler(request):
|
||||
@@ -166,8 +157,7 @@ def test_shorthand_named_routes_head():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_options():
|
||||
app = Sanic('test_shorhand_routes_options')
|
||||
def test_shorthand_named_routes_options(app):
|
||||
|
||||
@app.options('/options', name='route_options')
|
||||
def handler(request):
|
||||
@@ -181,8 +171,7 @@ def test_shorthand_named_routes_options():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_named_static_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
def test_named_static_routes(app):
|
||||
|
||||
@app.route('/test', name='route_test')
|
||||
async def handler1(request):
|
||||
@@ -205,9 +194,7 @@ def test_named_static_routes():
|
||||
app.url_for('handler2')
|
||||
|
||||
|
||||
def test_named_dynamic_route():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
def test_named_dynamic_route(app):
|
||||
results = []
|
||||
|
||||
@app.route('/folder/<name>', name='route_dynamic')
|
||||
@@ -221,8 +208,7 @@ def test_named_dynamic_route():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_dynamic_named_route_regex():
|
||||
app = Sanic('test_dynamic_route_regex')
|
||||
def test_dynamic_named_route_regex(app):
|
||||
|
||||
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>', name='route_re')
|
||||
async def handler(request, folder_id):
|
||||
@@ -235,8 +221,7 @@ def test_dynamic_named_route_regex():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_dynamic_named_route_path():
|
||||
app = Sanic('test_dynamic_route_path')
|
||||
def test_dynamic_named_route_path(app):
|
||||
|
||||
@app.route('/<path:path>/info', name='route_dynamic_path')
|
||||
async def handler(request, path):
|
||||
@@ -249,8 +234,7 @@ def test_dynamic_named_route_path():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_dynamic_named_route_unhashable():
|
||||
app = Sanic('test_dynamic_route_unhashable')
|
||||
def test_dynamic_named_route_unhashable(app):
|
||||
|
||||
@app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/',
|
||||
name='route_unhashable')
|
||||
@@ -265,8 +249,7 @@ def test_dynamic_named_route_unhashable():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_websocket_named_route():
|
||||
app = Sanic('test_websocket_route')
|
||||
def test_websocket_named_route(app):
|
||||
ev = asyncio.Event()
|
||||
|
||||
@app.websocket('/ws', name='route_ws')
|
||||
@@ -280,8 +263,7 @@ def test_websocket_named_route():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_websocket_named_route_with_subprotocols():
|
||||
app = Sanic('test_websocket_route')
|
||||
def test_websocket_named_route_with_subprotocols(app):
|
||||
results = []
|
||||
|
||||
@app.websocket('/ws', subprotocols=['foo', 'bar'], name='route_ws')
|
||||
@@ -294,8 +276,7 @@ def test_websocket_named_route_with_subprotocols():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_static_add_named_route():
|
||||
app = Sanic('test_static_add_route')
|
||||
def test_static_add_named_route(app):
|
||||
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
@@ -319,9 +300,7 @@ def test_static_add_named_route():
|
||||
app.url_for('handler2')
|
||||
|
||||
|
||||
def test_dynamic_add_named_route():
|
||||
app = Sanic('test_dynamic_add_route')
|
||||
|
||||
def test_dynamic_add_named_route(app):
|
||||
results = []
|
||||
|
||||
async def handler(request, name):
|
||||
@@ -335,8 +314,7 @@ def test_dynamic_add_named_route():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_dynamic_add_named_route_unhashable():
|
||||
app = Sanic('test_dynamic_add_route_unhashable')
|
||||
def test_dynamic_add_named_route_unhashable(app):
|
||||
|
||||
async def handler(request, unhashable):
|
||||
return text('OK')
|
||||
@@ -351,15 +329,14 @@ def test_dynamic_add_named_route_unhashable():
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_overload_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
def test_overload_routes(app):
|
||||
|
||||
@app.route('/overload', methods=['GET'], name='route_first')
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
|
||||
@app.route('/overload', methods=['POST', 'PUT'], name='route_second')
|
||||
async def handler1(request):
|
||||
async def handler2(request):
|
||||
return text('OK2')
|
||||
|
||||
request, response = app.test_client.get(app.url_for('route_first'))
|
||||
|
||||
@@ -1,49 +1,45 @@
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import PayloadTooLarge
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
def test_payload_too_large_from_error_handler():
|
||||
data_received_app = Sanic('data_received')
|
||||
data_received_app.config.REQUEST_MAX_SIZE = 1
|
||||
def test_payload_too_large_from_error_handler(app):
|
||||
app.config.REQUEST_MAX_SIZE = 1
|
||||
|
||||
@data_received_app.route('/1')
|
||||
@app.route('/1')
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
|
||||
@data_received_app.exception(PayloadTooLarge)
|
||||
@app.exception(PayloadTooLarge)
|
||||
def handler_exception(request, exception):
|
||||
return text('Payload Too Large from error_handler.', 413)
|
||||
|
||||
response = data_received_app.test_client.get('/1', gather_request=False)
|
||||
response = app.test_client.get('/1', gather_request=False)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Payload Too Large from error_handler.'
|
||||
|
||||
|
||||
def test_payload_too_large_at_data_received_default():
|
||||
data_received_default_app = Sanic('data_received_default')
|
||||
data_received_default_app.config.REQUEST_MAX_SIZE = 1
|
||||
def test_payload_too_large_at_data_received_default(app):
|
||||
app.config.REQUEST_MAX_SIZE = 1
|
||||
|
||||
@data_received_default_app.route('/1')
|
||||
@app.route('/1')
|
||||
async def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
response = data_received_default_app.test_client.get(
|
||||
response = app.test_client.get(
|
||||
'/1', gather_request=False)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Error: Payload Too Large'
|
||||
|
||||
|
||||
def test_payload_too_large_at_on_header_default():
|
||||
on_header_default_app = Sanic('on_header')
|
||||
on_header_default_app.config.REQUEST_MAX_SIZE = 500
|
||||
def test_payload_too_large_at_on_header_default(app):
|
||||
app.config.REQUEST_MAX_SIZE = 500
|
||||
|
||||
@on_header_default_app.post('/1')
|
||||
@app.post('/1')
|
||||
async def handler3(request):
|
||||
return text('OK')
|
||||
|
||||
data = 'a' * 1000
|
||||
response = on_header_default_app.test_client.post(
|
||||
response = app.test_client.post(
|
||||
'/1', gather_request=False, data=data)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Error: Payload Too Large'
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import pytest
|
||||
from urllib.parse import quote
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text, redirect
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def redirect_app():
|
||||
app = Sanic('test_redirection')
|
||||
def redirect_app(app):
|
||||
|
||||
@app.route('/redirect_init')
|
||||
async def redirect_init(request):
|
||||
@@ -21,15 +20,15 @@ def redirect_app():
|
||||
return text('OK')
|
||||
|
||||
@app.route('/1')
|
||||
def handler(request):
|
||||
def handler1(request):
|
||||
return redirect('/2')
|
||||
|
||||
@app.route('/2')
|
||||
def handler(request):
|
||||
def handler2(request):
|
||||
return redirect('/3')
|
||||
|
||||
@app.route('/3')
|
||||
def handler(request):
|
||||
def handler3(request):
|
||||
return text('OK')
|
||||
|
||||
@app.route('/redirect_with_header_injection')
|
||||
@@ -109,3 +108,25 @@ def test_redirect_with_header_injection(redirect_app):
|
||||
assert response.status == 302
|
||||
assert "test-header" not in response.headers
|
||||
assert not response.text.startswith('test-body')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_str", ["sanic-test", "sanictest", "sanic test"])
|
||||
async def test_redirect_with_params(app, test_client, test_str):
|
||||
|
||||
@app.route("/api/v1/test/<test>/")
|
||||
async def init_handler(request, test):
|
||||
assert test == test_str
|
||||
return redirect("/api/v2/test/{}/".format(quote(test)))
|
||||
|
||||
@app.route("/api/v2/test/<test>/")
|
||||
async def target_handler(request, test):
|
||||
assert test == test_str
|
||||
return text("OK")
|
||||
|
||||
test_cli = await test_client(app)
|
||||
|
||||
response = await test_cli.get("/api/v1/test/{}/".format(quote(test_str)))
|
||||
assert response.status == 200
|
||||
|
||||
txt = await response.text()
|
||||
assert txt == "OK"
|
||||
|
||||
72
tests/test_request_cancel.py
Normal file
72
tests/test_request_cancel.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
|
||||
from sanic.response import text, stream
|
||||
|
||||
|
||||
async def test_request_cancel_when_connection_lost(loop, app, test_client):
|
||||
app.still_serving_cancelled_request = False
|
||||
|
||||
@app.get('/')
|
||||
async def handler(request):
|
||||
await asyncio.sleep(1.0)
|
||||
# at this point client is already disconnected
|
||||
app.still_serving_cancelled_request = True
|
||||
return text('OK')
|
||||
|
||||
test_cli = await test_client(app)
|
||||
|
||||
# schedule client call
|
||||
task = loop.create_task(test_cli.get('/'))
|
||||
loop.call_later(0.01, task)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# cancelling request and closing connection after 0.5 sec
|
||||
task.cancel()
|
||||
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
# Wait for server and check if it's still serving the cancelled request
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
assert app.still_serving_cancelled_request is False
|
||||
|
||||
|
||||
async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
|
||||
app.still_serving_cancelled_request = False
|
||||
|
||||
@app.post('/post/<id>', stream=True)
|
||||
async def post(request, id):
|
||||
assert isinstance(request.stream, asyncio.Queue)
|
||||
|
||||
async def streaming(response):
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
await response.write(body.decode('utf-8'))
|
||||
|
||||
await asyncio.sleep(1.0)
|
||||
# at this point client is already disconnected
|
||||
app.still_serving_cancelled_request = True
|
||||
|
||||
return stream(streaming)
|
||||
|
||||
test_cli = await test_client(app)
|
||||
|
||||
# schedule client call
|
||||
task = loop.create_task(test_cli.post('/post/1'))
|
||||
loop.call_later(0.01, task)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# cancelling request and closing connection after 0.5 sec
|
||||
task.cancel()
|
||||
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
# Wait for server and check if it's still serving the cancelled request
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
assert app.still_serving_cancelled_request is False
|
||||
@@ -1,6 +1,5 @@
|
||||
import random
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
try:
|
||||
@@ -9,8 +8,7 @@ except ImportError:
|
||||
from json import loads
|
||||
|
||||
|
||||
def test_storage():
|
||||
app = Sanic('test_text')
|
||||
def test_storage(app):
|
||||
|
||||
@app.middleware('request')
|
||||
def store(request):
|
||||
@@ -20,7 +18,10 @@ def test_storage():
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
return json({'user': request.get('user'), 'sidekick': request.get('sidekick')})
|
||||
return json({
|
||||
'user': request.get('user'),
|
||||
'sidekick': request.get('sidekick')
|
||||
})
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
@@ -29,8 +30,7 @@ def test_storage():
|
||||
assert response_json.get('sidekick') is None
|
||||
|
||||
|
||||
def test_app_injection():
|
||||
app = Sanic('test_app_injection')
|
||||
def test_app_injection(app):
|
||||
expected = random.choice(range(0, 100))
|
||||
|
||||
@app.listener('after_server_start')
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.views import CompositionView
|
||||
from sanic.views import HTTPMethodView
|
||||
@@ -9,11 +8,9 @@ from sanic.response import stream, text
|
||||
data = "abc" * 100000
|
||||
|
||||
|
||||
def test_request_stream_method_view():
|
||||
def test_request_stream_method_view(app):
|
||||
'''for self.is_request_stream = True'''
|
||||
|
||||
app = Sanic('test_request_stream_method_view')
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
@@ -44,11 +41,9 @@ def test_request_stream_method_view():
|
||||
assert response.text == data
|
||||
|
||||
|
||||
def test_request_stream_app():
|
||||
def test_request_stream_app(app):
|
||||
'''for self.is_request_stream = True and decorators'''
|
||||
|
||||
app = Sanic('test_request_stream_app')
|
||||
|
||||
@app.get('/get')
|
||||
async def get(request):
|
||||
assert request.stream is None
|
||||
@@ -163,11 +158,9 @@ def test_request_stream_app():
|
||||
assert response.text == data
|
||||
|
||||
|
||||
def test_request_stream_handle_exception():
|
||||
def test_request_stream_handle_exception(app):
|
||||
'''for handling exceptions properly'''
|
||||
|
||||
app = Sanic('test_request_stream_exception')
|
||||
|
||||
@app.post('/post/<id>', stream=True)
|
||||
async def post(request, id):
|
||||
assert isinstance(request.stream, asyncio.Queue)
|
||||
@@ -188,13 +181,12 @@ def test_request_stream_handle_exception():
|
||||
# 405
|
||||
request, response = app.test_client.get('/post/random_id', data=data)
|
||||
assert response.status == 405
|
||||
assert response.text == 'Error: Method GET not allowed for URL /post/random_id'
|
||||
assert response.text == 'Error: Method GET not allowed for URL' \
|
||||
' /post/random_id'
|
||||
|
||||
|
||||
def test_request_stream_blueprint():
|
||||
def test_request_stream_blueprint(app):
|
||||
'''for self.is_request_stream = True'''
|
||||
|
||||
app = Sanic('test_request_stream_blueprint')
|
||||
bp = Blueprint('test_blueprint_request_stream_blueprint')
|
||||
|
||||
@app.get('/get')
|
||||
@@ -313,11 +305,9 @@ def test_request_stream_blueprint():
|
||||
assert response.text == data
|
||||
|
||||
|
||||
def test_request_stream_composition_view():
|
||||
def test_request_stream_composition_view(app):
|
||||
'''for self.is_request_stream = True'''
|
||||
|
||||
app = Sanic('test_request_stream__composition_view')
|
||||
|
||||
def get_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
@@ -348,11 +338,9 @@ def test_request_stream_composition_view():
|
||||
assert response.text == data
|
||||
|
||||
|
||||
def test_request_stream():
|
||||
def test_request_stream(app):
|
||||
'''test for complex application'''
|
||||
|
||||
bp = Blueprint('test_blueprint_request_stream')
|
||||
app = Sanic('test_request_stream')
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ import asyncio
|
||||
from sanic.response import text
|
||||
from sanic.config import Config
|
||||
import aiohttp
|
||||
from aiohttp import TCPConnector, ClientResponse
|
||||
from sanic.testing import SanicTestClient, HOST, PORT
|
||||
from aiohttp import TCPConnector
|
||||
from sanic.testing import SanicTestClient, HOST
|
||||
|
||||
try:
|
||||
try:
|
||||
import packaging # direct use
|
||||
except ImportError:
|
||||
# direct use
|
||||
import packaging
|
||||
version = packaging.version
|
||||
except (ImportError, AttributeError):
|
||||
# setuptools v39.0 and above.
|
||||
try:
|
||||
from setuptools.extern import packaging
|
||||
@@ -22,8 +24,10 @@ try:
|
||||
except ImportError:
|
||||
raise RuntimeError("The 'packaging' library is missing.")
|
||||
|
||||
|
||||
aiohttp_version = version.parse(aiohttp.__version__)
|
||||
|
||||
|
||||
class DelayableTCPConnector(TCPConnector):
|
||||
|
||||
class RequestContextManager(object):
|
||||
@@ -56,8 +60,7 @@ class DelayableTCPConnector(TCPConnector):
|
||||
if aiohttp_version >= version.parse("3.3.0"):
|
||||
ret = await self.orig_start(connection)
|
||||
else:
|
||||
ret = await self.orig_start(connection,
|
||||
read_until_eof)
|
||||
ret = await self.orig_start(connection, read_until_eof)
|
||||
except Exception as e:
|
||||
raise e
|
||||
return ret
|
||||
@@ -71,57 +74,43 @@ class DelayableTCPConnector(TCPConnector):
|
||||
async def delayed_send(self, *args, **kwargs):
|
||||
req = self.req
|
||||
if self.delay and self.delay > 0:
|
||||
#sync_sleep(self.delay)
|
||||
# sync_sleep(self.delay)
|
||||
await asyncio.sleep(self.delay)
|
||||
t = req.loop.time()
|
||||
print("sending at {}".format(t), flush=True)
|
||||
conn = next(iter(args)) # first arg is connection
|
||||
next(iter(args)) # first arg is connection
|
||||
|
||||
if aiohttp_version >= version.parse("3.1.0"):
|
||||
try:
|
||||
delayed_resp = await self.orig_send(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if aiohttp_version >= version.parse("3.3.0"):
|
||||
return aiohttp.ClientResponse(req.method, req.url,
|
||||
writer=None,
|
||||
continue100=None,
|
||||
timer=None,
|
||||
request_info=None,
|
||||
traces=[],
|
||||
loop=req.loop,
|
||||
session=None)
|
||||
else:
|
||||
return aiohttp.ClientResponse(req.method, req.url,
|
||||
writer=None,
|
||||
continue100=None,
|
||||
timer=None,
|
||||
request_info=None,
|
||||
auto_decompress=None,
|
||||
traces=[],
|
||||
loop=req.loop,
|
||||
session=None)
|
||||
else:
|
||||
try:
|
||||
delayed_resp = self.orig_send(*args, **kwargs)
|
||||
except Exception as e:
|
||||
try:
|
||||
return await self.orig_send(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if aiohttp_version < version.parse("3.1.0"):
|
||||
return aiohttp.ClientResponse(req.method, req.url)
|
||||
return delayed_resp
|
||||
kw = dict(
|
||||
writer=None,
|
||||
continue100=None,
|
||||
timer=None,
|
||||
request_info=None,
|
||||
traces=[],
|
||||
loop=req.loop,
|
||||
session=None
|
||||
)
|
||||
if aiohttp_version < version.parse("3.3.0"):
|
||||
kw['auto_decompress'] = None
|
||||
return aiohttp.ClientResponse(req.method, req.url, **kw)
|
||||
|
||||
def _send(self, *args, **kwargs):
|
||||
gen = self.delayed_send(*args, **kwargs)
|
||||
task = self.req.loop.create_task(gen)
|
||||
self.send_task = task
|
||||
self._acting_as = task
|
||||
return self
|
||||
|
||||
if aiohttp_version >= version.parse("3.1.0"):
|
||||
# aiohttp changed the request.send method to async
|
||||
async def send(self, *args, **kwargs):
|
||||
gen = self.delayed_send(*args, **kwargs)
|
||||
task = self.req.loop.create_task(gen)
|
||||
self.send_task = task
|
||||
self._acting_as = task
|
||||
return self
|
||||
return self._send(*args, **kwargs)
|
||||
else:
|
||||
def send(self, *args, **kwargs):
|
||||
gen = self.delayed_send(*args, **kwargs)
|
||||
task = self.req.loop.create_task(gen)
|
||||
self.send_task = task
|
||||
self._acting_as = task
|
||||
return self
|
||||
send = _send
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
_post_connect_delay = kwargs.pop('post_connect_delay', 0)
|
||||
@@ -130,45 +119,18 @@ class DelayableTCPConnector(TCPConnector):
|
||||
self._post_connect_delay = _post_connect_delay
|
||||
self._pre_request_delay = _pre_request_delay
|
||||
|
||||
if aiohttp_version >= version.parse("3.3.0"):
|
||||
async def connect(self, req, traces, timeout):
|
||||
d_req = DelayableTCPConnector.\
|
||||
RequestContextManager(req, self._pre_request_delay)
|
||||
conn = await super(DelayableTCPConnector, self).\
|
||||
connect(req, traces, timeout)
|
||||
if self._post_connect_delay and self._post_connect_delay > 0:
|
||||
await asyncio.sleep(self._post_connect_delay,
|
||||
loop=self._loop)
|
||||
req.send = d_req.send
|
||||
t = req.loop.time()
|
||||
print("Connected at {}".format(t), flush=True)
|
||||
return conn
|
||||
elif aiohttp_version >= version.parse("3.0.0"):
|
||||
async def connect(self, req, traces=None):
|
||||
d_req = DelayableTCPConnector.\
|
||||
RequestContextManager(req, self._pre_request_delay)
|
||||
conn = await super(DelayableTCPConnector, self).\
|
||||
connect(req, traces=traces)
|
||||
if self._post_connect_delay and self._post_connect_delay > 0:
|
||||
await asyncio.sleep(self._post_connect_delay,
|
||||
loop=self._loop)
|
||||
req.send = d_req.send
|
||||
t = req.loop.time()
|
||||
print("Connected at {}".format(t), flush=True)
|
||||
return conn
|
||||
else:
|
||||
|
||||
async def connect(self, req):
|
||||
d_req = DelayableTCPConnector.\
|
||||
RequestContextManager(req, self._pre_request_delay)
|
||||
conn = await super(DelayableTCPConnector, self).connect(req)
|
||||
if self._post_connect_delay and self._post_connect_delay > 0:
|
||||
await asyncio.sleep(self._post_connect_delay,
|
||||
loop=self._loop)
|
||||
req.send = d_req.send
|
||||
t = req.loop.time()
|
||||
print("Connected at {}".format(t), flush=True)
|
||||
return conn
|
||||
async def connect(self, req, *args, **kwargs):
|
||||
d_req = DelayableTCPConnector.\
|
||||
RequestContextManager(req, self._pre_request_delay)
|
||||
conn = await super(DelayableTCPConnector, self).\
|
||||
connect(req, *args, **kwargs)
|
||||
if self._post_connect_delay and self._post_connect_delay > 0:
|
||||
await asyncio.sleep(self._post_connect_delay,
|
||||
loop=self._loop)
|
||||
req.send = d_req.send
|
||||
t = req.loop.time()
|
||||
print("Connected at {}".format(t), flush=True)
|
||||
return conn
|
||||
|
||||
|
||||
class DelayableSanicTestClient(SanicTestClient):
|
||||
@@ -187,7 +149,7 @@ class DelayableSanicTestClient(SanicTestClient):
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=self.port, uri=uri)
|
||||
conn = DelayableTCPConnector(pre_request_delay=self._request_delay,
|
||||
verify_ssl=False, loop=self._loop)
|
||||
ssl=False, loop=self._loop)
|
||||
async with aiohttp.ClientSession(cookies=cookies, connector=conn,
|
||||
loop=self._loop) as session:
|
||||
# Insert a delay after creating the connection
|
||||
@@ -211,7 +173,7 @@ class DelayableSanicTestClient(SanicTestClient):
|
||||
return response
|
||||
|
||||
|
||||
Config.REQUEST_TIMEOUT = 2
|
||||
Config.REQUEST_TIMEOUT = 0.6
|
||||
request_timeout_default_app = Sanic('test_request_timeout_default')
|
||||
request_no_timeout_app = Sanic('test_request_no_timeout')
|
||||
|
||||
@@ -226,15 +188,36 @@ async def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
@request_timeout_default_app.websocket('/ws1')
|
||||
async def ws_handler1(request, ws):
|
||||
await ws.send('OK')
|
||||
|
||||
|
||||
def test_default_server_error_request_timeout():
|
||||
client = DelayableSanicTestClient(request_timeout_default_app, None, 3)
|
||||
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
|
||||
request, response = client.get('/1')
|
||||
assert response.status == 408
|
||||
assert response.text == 'Error: Request Timeout'
|
||||
|
||||
|
||||
def test_default_server_error_request_dont_timeout():
|
||||
client = DelayableSanicTestClient(request_no_timeout_app, None, 1)
|
||||
client = DelayableSanicTestClient(request_no_timeout_app, None, 0.2)
|
||||
request, response = client.get('/1')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_default_server_error_websocket_request_timeout():
|
||||
|
||||
headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13'
|
||||
}
|
||||
|
||||
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
|
||||
request, response = client.get('/ws1', headers=headers)
|
||||
|
||||
assert response.status == 408
|
||||
assert response.text == 'Error: Request Timeout'
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
from json import loads as json_loads, dumps as json_dumps
|
||||
from urllib.parse import urlparse
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
from json import dumps as json_dumps
|
||||
from json import loads as json_loads
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.response import json, text
|
||||
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE
|
||||
from sanic.response import json, text
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_sync():
|
||||
app = Sanic('test_text')
|
||||
def test_sync(app):
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
@@ -27,8 +26,8 @@ def test_sync():
|
||||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
def test_remote_address():
|
||||
app = Sanic('test_text')
|
||||
|
||||
def test_remote_address(app):
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
@@ -38,8 +37,8 @@ def test_remote_address():
|
||||
|
||||
assert response.text == '127.0.0.1'
|
||||
|
||||
def test_text():
|
||||
app = Sanic('test_text')
|
||||
|
||||
def test_text(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -50,8 +49,7 @@ def test_text():
|
||||
assert response.text == 'Hello'
|
||||
|
||||
|
||||
def test_headers():
|
||||
app = Sanic('test_text')
|
||||
def test_headers(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -63,8 +61,7 @@ def test_headers():
|
||||
assert response.headers.get('spam') == 'great'
|
||||
|
||||
|
||||
def test_non_str_headers():
|
||||
app = Sanic('test_text')
|
||||
def test_non_str_headers(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -75,8 +72,8 @@ def test_non_str_headers():
|
||||
|
||||
assert response.headers.get('answer') == '42'
|
||||
|
||||
def test_invalid_response():
|
||||
app = Sanic('test_invalid_response')
|
||||
|
||||
def test_invalid_response(app):
|
||||
|
||||
@app.exception(ServerError)
|
||||
def handler_exception(request, exception):
|
||||
@@ -91,8 +88,7 @@ def test_invalid_response():
|
||||
assert response.text == "Internal Server Error."
|
||||
|
||||
|
||||
def test_json():
|
||||
app = Sanic('test_json')
|
||||
def test_json(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -102,11 +98,10 @@ def test_json():
|
||||
|
||||
results = json_loads(response.text)
|
||||
|
||||
assert results.get('test') == True
|
||||
assert results.get('test') is True
|
||||
|
||||
|
||||
def test_empty_json():
|
||||
app = Sanic('test_json')
|
||||
def test_empty_json(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -118,8 +113,7 @@ def test_empty_json():
|
||||
assert response.text == 'null'
|
||||
|
||||
|
||||
def test_invalid_json():
|
||||
app = Sanic('test_json')
|
||||
def test_invalid_json(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -131,8 +125,7 @@ def test_invalid_json():
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
def test_query_string():
|
||||
app = Sanic('test_query_string')
|
||||
def test_query_string(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -145,8 +138,7 @@ def test_query_string():
|
||||
assert request.args.get('test2') == 'false'
|
||||
|
||||
|
||||
def test_uri_template():
|
||||
app = Sanic('test_uri_template')
|
||||
def test_uri_template(app):
|
||||
|
||||
@app.route('/foo/<id:int>/bar/<name:[A-z]+>')
|
||||
async def handler(request):
|
||||
@@ -156,8 +148,7 @@ def test_uri_template():
|
||||
assert request.uri_template == '/foo/<id:int>/bar/<name:[A-z]+>'
|
||||
|
||||
|
||||
def test_token():
|
||||
app = Sanic('test_post_token')
|
||||
def test_token(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -204,8 +195,7 @@ def test_token():
|
||||
assert request.token is None
|
||||
|
||||
|
||||
def test_content_type():
|
||||
app = Sanic('test_content_type')
|
||||
def test_content_type(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -223,8 +213,7 @@ def test_content_type():
|
||||
assert response.text == 'application/json'
|
||||
|
||||
|
||||
def test_remote_addr():
|
||||
app = Sanic('test_content_type')
|
||||
def test_remote_addr(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -249,8 +238,7 @@ def test_remote_addr():
|
||||
assert response.text == '127.0.0.1'
|
||||
|
||||
|
||||
def test_match_info():
|
||||
app = Sanic('test_match_info')
|
||||
def test_match_info(app):
|
||||
|
||||
@app.route('/api/v1/user/<user_id>/')
|
||||
async def handler(request, user_id):
|
||||
@@ -266,8 +254,7 @@ def test_match_info():
|
||||
# POST
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_post_json():
|
||||
app = Sanic('test_post_json')
|
||||
def test_post_json(app):
|
||||
|
||||
@app.route('/', methods=['POST'])
|
||||
async def handler(request):
|
||||
@@ -280,11 +267,11 @@ def test_post_json():
|
||||
'/', data=json_dumps(payload), headers=headers)
|
||||
|
||||
assert request.json.get('test') == 'OK'
|
||||
assert request.json.get('test') == 'OK' # for request.parsed_json
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_post_form_urlencoded():
|
||||
app = Sanic('test_post_form_urlencoded')
|
||||
def test_post_form_urlencoded(app):
|
||||
|
||||
@app.route('/', methods=['POST'])
|
||||
async def handler(request):
|
||||
@@ -293,26 +280,27 @@ def test_post_form_urlencoded():
|
||||
payload = 'test=OK'
|
||||
headers = {'content-type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
request, response = app.test_client.post('/', data=payload, headers=headers)
|
||||
request, response = app.test_client.post('/', data=payload,
|
||||
headers=headers)
|
||||
|
||||
assert request.form.get('test') == 'OK'
|
||||
assert request.form.get('test') == 'OK' # For request.parsed_form
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'payload', [
|
||||
'------sanic\r\n' \
|
||||
'Content-Disposition: form-data; name="test"\r\n' \
|
||||
'\r\n' \
|
||||
'OK\r\n' \
|
||||
'------sanic\r\n'
|
||||
'Content-Disposition: form-data; name="test"\r\n'
|
||||
'\r\n'
|
||||
'OK\r\n'
|
||||
'------sanic--\r\n',
|
||||
'------sanic\r\n' \
|
||||
'content-disposition: form-data; name="test"\r\n' \
|
||||
'\r\n' \
|
||||
'OK\r\n' \
|
||||
'------sanic\r\n'
|
||||
'content-disposition: form-data; name="test"\r\n'
|
||||
'\r\n'
|
||||
'OK\r\n'
|
||||
'------sanic--\r\n',
|
||||
])
|
||||
def test_post_form_multipart_form_data(payload):
|
||||
app = Sanic('test_post_form_multipart_form_data')
|
||||
def test_post_form_multipart_form_data(app, payload):
|
||||
|
||||
@app.route('/', methods=['POST'])
|
||||
async def handler(request):
|
||||
@@ -331,8 +319,7 @@ def test_post_form_multipart_form_data(payload):
|
||||
('/bar/baz', '', 'http://{}:{}/bar/baz'),
|
||||
('/moo/boo', 'arg1=val1', 'http://{}:{}/moo/boo?arg1=val1')
|
||||
])
|
||||
def test_url_attributes_no_ssl(path, query, expected_url):
|
||||
app = Sanic('test_url_attrs_no_ssl')
|
||||
def test_url_attributes_no_ssl(app, path, query, expected_url):
|
||||
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
@@ -356,9 +343,7 @@ def test_url_attributes_no_ssl(path, query, expected_url):
|
||||
('/bar/baz', '', 'https://{}:{}/bar/baz'),
|
||||
('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1')
|
||||
])
|
||||
def test_url_attributes_with_ssl(path, query, expected_url):
|
||||
app = Sanic('test_url_attrs_with_ssl')
|
||||
|
||||
def test_url_attributes_with_ssl_context(app, path, query, expected_url):
|
||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(
|
||||
@@ -381,3 +366,342 @@ def test_url_attributes_with_ssl(path, query, expected_url):
|
||||
assert parsed.path == request.path
|
||||
assert parsed.query == request.query_string
|
||||
assert parsed.netloc == request.host
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path,query,expected_url', [
|
||||
('/foo', '', 'https://{}:{}/foo'),
|
||||
('/bar/baz', '', 'https://{}:{}/bar/baz'),
|
||||
('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1')
|
||||
])
|
||||
def test_url_attributes_with_ssl_dict(app, path, query, expected_url):
|
||||
|
||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
ssl_cert = os.path.join(current_dir, 'certs/selfsigned.cert')
|
||||
ssl_key = os.path.join(current_dir, 'certs/selfsigned.key')
|
||||
|
||||
ssl_dict = {
|
||||
'cert': ssl_cert,
|
||||
'key': ssl_key
|
||||
}
|
||||
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, path)
|
||||
|
||||
request, response = app.test_client.get(
|
||||
'https://{}:{}'.format(HOST, PORT) + path + '?{}'.format(query),
|
||||
server_kwargs={'ssl': ssl_dict})
|
||||
assert request.url == expected_url.format(HOST, PORT)
|
||||
|
||||
parsed = urlparse(request.url)
|
||||
|
||||
assert parsed.scheme == request.scheme
|
||||
assert parsed.path == request.path
|
||||
assert parsed.query == request.query_string
|
||||
assert parsed.netloc == request.host
|
||||
|
||||
|
||||
def test_invalid_ssl_dict(app):
|
||||
|
||||
@app.get('/test')
|
||||
async def handler(request):
|
||||
return text('ssl test')
|
||||
|
||||
ssl_dict = {
|
||||
'cert': None,
|
||||
'key': None
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
request, response = app.test_client.get('/test', server_kwargs={'ssl': ssl_dict})
|
||||
|
||||
assert str(excinfo.value) == 'SSLContext or certificate and key required.'
|
||||
|
||||
|
||||
def test_form_with_multiple_values(app):
|
||||
|
||||
@app.route('/', methods=['POST'])
|
||||
async def handler(request):
|
||||
return text("OK")
|
||||
|
||||
payload="selectedItems=v1&selectedItems=v2&selectedItems=v3"
|
||||
|
||||
headers = {'content-type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
request, response = app.test_client.post('/', data=payload,
|
||||
headers=headers)
|
||||
|
||||
assert request.form.getlist("selectedItems") == ["v1", "v2", "v3"]
|
||||
|
||||
|
||||
def test_request_string_representation(app):
|
||||
@app.route('/', methods=["GET"])
|
||||
async def get(request):
|
||||
return text("OK")
|
||||
|
||||
request, _ = app.test_client.get("/")
|
||||
assert repr(request) == '<Request: GET />'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'payload', [
|
||||
'------sanic\r\n'
|
||||
'Content-Disposition: form-data; filename="filename"; name="test"\r\n'
|
||||
'\r\n'
|
||||
'OK\r\n'
|
||||
'------sanic--\r\n',
|
||||
'------sanic\r\n'
|
||||
'content-disposition: form-data; filename="filename"; name="test"\r\n'
|
||||
'\r\n'
|
||||
'content-type: application/json; {"field": "value"}\r\n'
|
||||
'------sanic--\r\n',
|
||||
])
|
||||
def test_request_multipart_files(app, payload):
|
||||
@app.route("/", methods=["POST"])
|
||||
async def post(request):
|
||||
return text("OK")
|
||||
|
||||
headers = {'content-type': 'multipart/form-data; boundary=----sanic'}
|
||||
|
||||
request, _ = app.test_client.post(data=payload, headers=headers)
|
||||
assert request.files.get('test').name == "filename"
|
||||
|
||||
|
||||
def test_request_multipart_file_with_json_content_type(app):
|
||||
@app.route("/", methods=["POST"])
|
||||
async def post(request):
|
||||
return text("OK")
|
||||
|
||||
payload = (
|
||||
'------sanic\r\n'
|
||||
'Content-Disposition: form-data; name="file"; filename="test.json"\r\n'
|
||||
'Content-Type: application/json\r\n'
|
||||
'Content-Length: 0'
|
||||
'\r\n'
|
||||
'\r\n'
|
||||
'------sanic--'
|
||||
)
|
||||
|
||||
headers = {'content-type': 'multipart/form-data; boundary=------sanic'}
|
||||
|
||||
request, _ = app.test_client.post(data=payload, headers=headers)
|
||||
assert request.files.get('file').type == 'application/json'
|
||||
|
||||
|
||||
def test_request_multipart_file_without_field_name(app, caplog):
|
||||
|
||||
@app.route("/", methods=["POST"])
|
||||
async def post(request):
|
||||
return text("OK")
|
||||
|
||||
payload = (
|
||||
'------sanic\r\nContent-Disposition: form-data; filename="test.json"'
|
||||
'\r\nContent-Type: application/json\r\n\r\n\r\n------sanic--'
|
||||
)
|
||||
|
||||
headers = {'content-type': 'multipart/form-data; boundary=------sanic'}
|
||||
|
||||
request, _ = app.test_client.post(data=payload, headers=headers, debug=True)
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
request.form
|
||||
|
||||
assert caplog.record_tuples[-1] == ('sanic.root', logging.DEBUG,
|
||||
"Form-data field does not have a 'name' parameter "
|
||||
"in the Content-Disposition header"
|
||||
)
|
||||
|
||||
|
||||
def test_request_multipart_file_duplicate_filed_name(app):
|
||||
|
||||
@app.route("/", methods=["POST"])
|
||||
async def post(request):
|
||||
return text("OK")
|
||||
|
||||
payload = (
|
||||
'--e73ffaa8b1b2472b8ec848de833cb05b\r\n'
|
||||
'Content-Disposition: form-data; name="file"\r\n'
|
||||
'Content-Type: application/octet-stream\r\n'
|
||||
'Content-Length: 15\r\n'
|
||||
'\r\n'
|
||||
'{"test":"json"}\r\n'
|
||||
'--e73ffaa8b1b2472b8ec848de833cb05b\r\n'
|
||||
'Content-Disposition: form-data; name="file"\r\n'
|
||||
'Content-Type: application/octet-stream\r\n'
|
||||
'Content-Length: 15\r\n'
|
||||
'\r\n'
|
||||
'{"test":"json2"}\r\n'
|
||||
'--e73ffaa8b1b2472b8ec848de833cb05b--\r\n'
|
||||
)
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'multipart/form-data; boundary=e73ffaa8b1b2472b8ec848de833cb05b'
|
||||
}
|
||||
|
||||
request, _ = app.test_client.post(data=payload, headers=headers, debug=True)
|
||||
assert request.form.getlist('file') == ['{"test":"json"}', '{"test":"json2"}']
|
||||
|
||||
|
||||
def test_request_multipart_with_multiple_files_and_type(app):
|
||||
@app.route("/", methods=["POST"])
|
||||
async def post(request):
|
||||
return text("OK")
|
||||
|
||||
payload = '------sanic\r\nContent-Disposition: form-data; name="file"; filename="test.json"' \
|
||||
'\r\nContent-Type: application/json\r\n\r\n\r\n' \
|
||||
'------sanic\r\nContent-Disposition: form-data; name="file"; filename="some_file.pdf"\r\n' \
|
||||
'Content-Type: application/pdf\r\n\r\n\r\n------sanic--'
|
||||
headers = {'content-type': 'multipart/form-data; boundary=------sanic'}
|
||||
|
||||
request, _ = app.test_client.post(data=payload, headers=headers)
|
||||
assert len(request.files.getlist('file')) == 2
|
||||
assert request.files.getlist('file')[0].type == 'application/json'
|
||||
assert request.files.getlist('file')[1].type == 'application/pdf'
|
||||
|
||||
|
||||
def test_request_repr(app):
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
return text('pass')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert repr(request) == '<Request: GET />'
|
||||
|
||||
request.method = None
|
||||
assert repr(request) == '<Request>'
|
||||
|
||||
|
||||
def test_request_bool(app):
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
return text('pass')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert bool(request)
|
||||
|
||||
request.transport = False
|
||||
assert not bool(request)
|
||||
|
||||
|
||||
def test_request_parsing_form_failed(app, caplog):
|
||||
|
||||
@app.route('/', methods=['POST'])
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
payload = 'test=OK'
|
||||
headers = {'content-type': 'multipart/form-data'}
|
||||
|
||||
request, response = app.test_client.post('/', data=payload, headers=headers)
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
request.form
|
||||
|
||||
assert caplog.record_tuples[-1] == ('sanic.error', logging.ERROR, 'Failed when parsing form')
|
||||
|
||||
|
||||
def test_request_args_no_query_string(app):
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
return text('pass')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert request.args == {}
|
||||
|
||||
|
||||
def test_request_raw_args(app):
|
||||
|
||||
params = {'test': 'OK'}
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
return text('pass')
|
||||
|
||||
request, response = app.test_client.get('/', params=params)
|
||||
|
||||
assert request.raw_args == params
|
||||
|
||||
|
||||
def test_request_cookies(app):
|
||||
|
||||
cookies = {'test': 'OK'}
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/', cookies=cookies)
|
||||
|
||||
assert request.cookies == cookies
|
||||
assert request.cookies == cookies # For request._cookies
|
||||
|
||||
|
||||
def test_request_cookies_without_cookies(app):
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert request.cookies == {}
|
||||
|
||||
|
||||
def test_request_port(app):
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
port = request.port
|
||||
assert isinstance(port, int)
|
||||
|
||||
delattr(request, '_socket')
|
||||
delattr(request, '_port')
|
||||
|
||||
port = request.port
|
||||
assert isinstance(port, int)
|
||||
assert hasattr(request, '_socket')
|
||||
assert hasattr(request, '_port')
|
||||
|
||||
|
||||
def test_request_socket(app):
|
||||
|
||||
@app.get('/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
socket = request.socket
|
||||
assert isinstance(socket, tuple)
|
||||
|
||||
ip = socket[0]
|
||||
port = socket[1]
|
||||
|
||||
assert ip == request.ip
|
||||
assert port == request.port
|
||||
|
||||
delattr(request, '_socket')
|
||||
|
||||
socket = request.socket
|
||||
assert isinstance(socket, tuple)
|
||||
assert hasattr(request, '_socket')
|
||||
|
||||
|
||||
def test_request_form_invalid_content_type(app):
|
||||
|
||||
@app.route("/", methods=["POST"])
|
||||
async def post(request):
|
||||
return text("OK")
|
||||
|
||||
request, response = app.test_client.post('/', json={'test': 'OK'})
|
||||
|
||||
assert request.form == {}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
from aiofiles import os as async_os
|
||||
from collections import namedtuple
|
||||
from mimetypes import guess_type
|
||||
from random import choice
|
||||
from unittest.mock import MagicMock
|
||||
from urllib.parse import unquote
|
||||
|
||||
import pytest
|
||||
from random import choice
|
||||
from aiofiles import os as async_os
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json
|
||||
from sanic.response import (HTTPResponse, StreamingHTTPResponse, file,
|
||||
file_stream, json, raw, stream, text)
|
||||
from sanic.server import HttpProtocol
|
||||
from sanic.testing import HOST, PORT
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
JSON_DATA = {'ok': True}
|
||||
|
||||
|
||||
def test_response_body_not_a_string():
|
||||
def test_response_body_not_a_string(app):
|
||||
"""Test when a response body sent from the application is not a string"""
|
||||
app = Sanic('response_body_not_a_string')
|
||||
random_num = choice(range(1000))
|
||||
|
||||
@app.route('/hello')
|
||||
@@ -36,12 +36,9 @@ async def sample_streaming_fn(response):
|
||||
await response.write('bar')
|
||||
|
||||
|
||||
|
||||
def test_method_not_allowed():
|
||||
app = Sanic('method_not_allowed')
|
||||
|
||||
def test_method_not_allowed(app):
|
||||
@app.get('/')
|
||||
async def test(request):
|
||||
async def test_get(request):
|
||||
return response.json({'hello': 'world'})
|
||||
|
||||
request, response = app.test_client.head('/')
|
||||
@@ -50,24 +47,23 @@ def test_method_not_allowed():
|
||||
request, response = app.test_client.post('/')
|
||||
assert response.headers['Allow'] == 'GET'
|
||||
|
||||
|
||||
@app.post('/')
|
||||
async def test(request):
|
||||
async def test_post(request):
|
||||
return response.json({'hello': 'world'})
|
||||
|
||||
request, response = app.test_client.head('/')
|
||||
assert response.status == 405
|
||||
assert set(response.headers['Allow'].split(', ')) == set(['GET', 'POST'])
|
||||
assert set(response.headers['Allow'].split(', ')) == {'GET', 'POST'}
|
||||
assert response.headers['Content-Length'] == '0'
|
||||
|
||||
request, response = app.test_client.patch('/')
|
||||
assert response.status == 405
|
||||
assert set(response.headers['Allow'].split(', ')) == set(['GET', 'POST'])
|
||||
assert set(response.headers['Allow'].split(', ')) == {'GET', 'POST'}
|
||||
assert response.headers['Content-Length'] == '0'
|
||||
|
||||
|
||||
def test_response_header():
|
||||
app = Sanic('test_response_header')
|
||||
def test_response_header(app):
|
||||
|
||||
@app.get('/')
|
||||
async def test(request):
|
||||
return json({
|
||||
@@ -79,15 +75,61 @@ def test_response_header():
|
||||
request, response = app.test_client.get('/')
|
||||
assert dict(response.headers) == {
|
||||
'Connection': 'keep-alive',
|
||||
'Keep-Alive': '2',
|
||||
'Keep-Alive': str(app.config.KEEP_ALIVE_TIMEOUT),
|
||||
'Content-Length': '11',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
|
||||
def test_response_content_length(app):
|
||||
@app.get("/response_with_space")
|
||||
async def response_with_space(request):
|
||||
return json({
|
||||
"message": "Data",
|
||||
"details": "Some Details"
|
||||
}, headers={
|
||||
'CONTENT-TYPE': 'application/json'
|
||||
})
|
||||
|
||||
@app.get("/response_without_space")
|
||||
async def response_without_space(request):
|
||||
return json({
|
||||
"message":"Data",
|
||||
"details":"Some Details"
|
||||
}, headers={
|
||||
'CONTENT-TYPE': 'application/json'
|
||||
})
|
||||
|
||||
_, response = app.test_client.get("/response_with_space")
|
||||
content_length_for_response_with_space = response.headers.get("Content-Length")
|
||||
|
||||
_, response = app.test_client.get("/response_without_space")
|
||||
content_length_for_response_without_space = response.headers.get("Content-Length")
|
||||
|
||||
assert content_length_for_response_with_space == content_length_for_response_without_space
|
||||
|
||||
assert content_length_for_response_with_space == '43'
|
||||
|
||||
|
||||
def test_response_content_length_with_different_data_types(app):
|
||||
@app.get("/")
|
||||
async def get_data_with_different_types(request):
|
||||
# Indentation issues in the Response is intentional. Please do not fix
|
||||
return json({
|
||||
'bool': True,
|
||||
'none': None,
|
||||
'string':'string',
|
||||
'number': -1},
|
||||
headers={
|
||||
'CONTENT-TYPE': 'application/json'
|
||||
})
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
assert response.headers.get("Content-Length") == '55'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_app():
|
||||
app = Sanic('json')
|
||||
def json_app(app):
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
@@ -145,8 +187,7 @@ def test_no_content(json_app):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def streaming_app():
|
||||
app = Sanic('streaming')
|
||||
def streaming_app(app):
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
@@ -223,6 +264,29 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
|
||||
streaming_app.run(host=HOST, port=PORT)
|
||||
|
||||
|
||||
def test_stream_response_with_cookies(app):
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
response = stream(sample_streaming_fn, content_type='text/csv')
|
||||
response.cookies['test'] = 'modified'
|
||||
response.cookies['test'] = 'pass'
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.cookies['test'].value == 'pass'
|
||||
|
||||
|
||||
def test_stream_response_without_cookies(app):
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return stream(sample_streaming_fn, content_type='text/csv')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.cookies == {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def static_file_directory():
|
||||
"""The static directory to serve"""
|
||||
@@ -238,10 +302,10 @@ def get_file_content(static_file_directory, file_name):
|
||||
return file.read()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
|
||||
@pytest.mark.parametrize('file_name',
|
||||
['test.file', 'decode me.txt', 'python.png'])
|
||||
@pytest.mark.parametrize('status', [200, 401])
|
||||
def test_file_response(file_name, static_file_directory, status):
|
||||
app = Sanic('test_file_helper')
|
||||
def test_file_response(app, file_name, static_file_directory, status):
|
||||
|
||||
@app.route('/files/<filename>', methods=['GET'])
|
||||
def file_route(request, filename):
|
||||
@@ -256,10 +320,15 @@ def test_file_response(file_name, static_file_directory, status):
|
||||
assert 'Content-Disposition' not in response.headers
|
||||
|
||||
|
||||
@pytest.mark.parametrize('source,dest', [
|
||||
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
|
||||
def test_file_response_custom_filename(source, dest, static_file_directory):
|
||||
app = Sanic('test_file_helper')
|
||||
@pytest.mark.parametrize(
|
||||
'source,dest',
|
||||
[
|
||||
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'),
|
||||
('python.png', 'logo.png')
|
||||
]
|
||||
)
|
||||
def test_file_response_custom_filename(app, source, dest,
|
||||
static_file_directory):
|
||||
|
||||
@app.route('/files/<filename>', methods=['GET'])
|
||||
def file_route(request, filename):
|
||||
@@ -270,12 +339,12 @@ def test_file_response_custom_filename(source, dest, static_file_directory):
|
||||
request, response = app.test_client.get('/files/{}'.format(source))
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, source)
|
||||
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
|
||||
assert response.headers['Content-Disposition'] == \
|
||||
'attachment; filename="{}"'.format(dest)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_file_head_response(file_name, static_file_directory):
|
||||
app = Sanic('test_file_helper')
|
||||
def test_file_head_response(app, file_name, static_file_directory):
|
||||
|
||||
@app.route('/files/<filename>', methods=['GET', 'HEAD'])
|
||||
async def file_route(request, filename):
|
||||
@@ -302,9 +371,9 @@ def test_file_head_response(file_name, static_file_directory):
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_file_stream_response(file_name, static_file_directory):
|
||||
app = Sanic('test_file_helper')
|
||||
@pytest.mark.parametrize('file_name',
|
||||
['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_file_stream_response(app, file_name, static_file_directory):
|
||||
|
||||
@app.route('/files/<filename>', methods=['GET'])
|
||||
def file_route(request, filename):
|
||||
@@ -319,10 +388,15 @@ def test_file_stream_response(file_name, static_file_directory):
|
||||
assert 'Content-Disposition' not in response.headers
|
||||
|
||||
|
||||
@pytest.mark.parametrize('source,dest', [
|
||||
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
|
||||
def test_file_stream_response_custom_filename(source, dest, static_file_directory):
|
||||
app = Sanic('test_file_helper')
|
||||
@pytest.mark.parametrize(
|
||||
'source,dest',
|
||||
[
|
||||
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'),
|
||||
('python.png', 'logo.png')
|
||||
]
|
||||
)
|
||||
def test_file_stream_response_custom_filename(app, source, dest,
|
||||
static_file_directory):
|
||||
|
||||
@app.route('/files/<filename>', methods=['GET'])
|
||||
def file_route(request, filename):
|
||||
@@ -333,12 +407,12 @@ def test_file_stream_response_custom_filename(source, dest, static_file_director
|
||||
request, response = app.test_client.get('/files/{}'.format(source))
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, source)
|
||||
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
|
||||
assert response.headers['Content-Disposition'] == \
|
||||
'attachment; filename="{}"'.format(dest)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_file_stream_head_response(file_name, static_file_directory):
|
||||
app = Sanic('test_file_helper')
|
||||
def test_file_stream_head_response(app, file_name, static_file_directory):
|
||||
|
||||
@app.route('/files/<filename>', methods=['GET', 'HEAD'])
|
||||
async def file_route(request, filename):
|
||||
@@ -355,8 +429,10 @@ def test_file_stream_head_response(file_name, static_file_directory):
|
||||
headers=headers,
|
||||
content_type=guess_type(file_path)[0] or 'text/plain')
|
||||
else:
|
||||
return file_stream(file_path, chunk_size=32, headers=headers,
|
||||
mime_type=guess_type(file_path)[0] or 'text/plain')
|
||||
return file_stream(
|
||||
file_path, chunk_size=32, headers=headers,
|
||||
mime_type=guess_type(file_path)[0] or 'text/plain'
|
||||
)
|
||||
|
||||
request, response = app.test_client.head('/files/{}'.format(file_name))
|
||||
assert response.status == 200
|
||||
@@ -369,3 +445,42 @@ def test_file_stream_head_response(file_name, static_file_directory):
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name',
|
||||
['test.file', 'decode me.txt', 'python.png'])
|
||||
@pytest.mark.parametrize('size,start,end', [
|
||||
(1024, 0, 1024),
|
||||
(4096, 1024, 8192),
|
||||
])
|
||||
def test_file_stream_response_range(app, file_name, static_file_directory, size, start, end):
|
||||
|
||||
Range = namedtuple('Range', ['size', 'start', 'end', 'total'])
|
||||
total = len(get_file_content(static_file_directory, file_name))
|
||||
range = Range(size=size, start=start, end=end, total=total)
|
||||
|
||||
@app.route('/files/<filename>', methods=['GET'])
|
||||
def file_route(request, filename):
|
||||
file_path = os.path.join(static_file_directory, filename)
|
||||
file_path = os.path.abspath(unquote(file_path))
|
||||
return file_stream(
|
||||
file_path,
|
||||
chunk_size=32,
|
||||
mime_type=guess_type(file_path)[0] or 'text/plain',
|
||||
_range=range)
|
||||
|
||||
request, response = app.test_client.get('/files/{}'.format(file_name))
|
||||
assert response.status == 206
|
||||
assert 'Content-Range' in response.headers
|
||||
assert response.headers['Content-Range'] == 'bytes {}-{}/{}'.format(range.start, range.end, range.total)
|
||||
|
||||
|
||||
def test_raw_response(app):
|
||||
|
||||
@app.get('/test')
|
||||
def handler(request):
|
||||
return raw(b'raw_response')
|
||||
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.content_type == 'application/octet-stream'
|
||||
assert response.body == b'raw_response'
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text, json
|
||||
from sanic.router import RouteExists, RouteDoesNotExist
|
||||
from sanic.constants import HTTP_METHODS
|
||||
|
||||
from sanic.response import json, text
|
||||
from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# UTF-8
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_versioned_routes_get(method):
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
|
||||
def test_versioned_routes_get(app, method):
|
||||
method = method.lower()
|
||||
|
||||
func = getattr(app, method)
|
||||
@@ -32,8 +30,7 @@ def test_versioned_routes_get(method):
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_shorthand_routes_get():
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
def test_shorthand_routes_get(app):
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
@@ -46,8 +43,7 @@ def test_shorthand_routes_get():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_shorthand_routes_multiple():
|
||||
app = Sanic('test_shorthand_routes_multiple')
|
||||
def test_shorthand_routes_multiple(app):
|
||||
|
||||
@app.get('/get')
|
||||
def get_handler(request):
|
||||
@@ -65,16 +61,15 @@ def test_shorthand_routes_multiple():
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_route_strict_slash():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
def test_route_strict_slash(app):
|
||||
|
||||
@app.get('/get', strict_slashes=True)
|
||||
def handler(request):
|
||||
def handler1(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@app.post('/post/', strict_slashes=True)
|
||||
def handler(request):
|
||||
def handler2(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@@ -93,9 +88,8 @@ def test_route_strict_slash():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_route_invalid_parameter_syntax():
|
||||
def test_route_invalid_parameter_syntax(app):
|
||||
with pytest.raises(ValueError):
|
||||
app = Sanic('test_route_invalid_param_syntax')
|
||||
|
||||
@app.get('/get/<:string>', strict_slashes=True)
|
||||
def handler(request):
|
||||
@@ -115,8 +109,7 @@ def test_route_strict_slash_default_value():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_route_strict_slash_without_passing_default_value():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
def test_route_strict_slash_without_passing_default_value(app):
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
@@ -137,15 +130,14 @@ def test_route_strict_slash_default_value_can_be_overwritten():
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_route_slashes_overload():
|
||||
app = Sanic('test_route_slashes_overload')
|
||||
def test_route_slashes_overload(app):
|
||||
|
||||
@app.get('/hello/')
|
||||
def handler(request):
|
||||
def handler_get(request):
|
||||
return text('OK')
|
||||
|
||||
@app.post('/hello/')
|
||||
def handler(request):
|
||||
def handler_post(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/hello')
|
||||
@@ -161,8 +153,7 @@ def test_route_slashes_overload():
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_route_optional_slash():
|
||||
app = Sanic('test_route_optional_slash')
|
||||
def test_route_optional_slash(app):
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
@@ -174,43 +165,43 @@ def test_route_optional_slash():
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_route_strict_slashes_set_to_false_and_host_is_a_list():
|
||||
#Part of regression test for issue #1120
|
||||
app = Sanic('test_route_strict_slashes_set_to_false_and_host_is_a_list')
|
||||
|
||||
site1 = 'localhost:{}'.format(app.test_client.port)
|
||||
def test_route_strict_slashes_set_to_false_and_host_is_a_list(app):
|
||||
# Part of regression test for issue #1120
|
||||
|
||||
#before fix, this raises a RouteExists error
|
||||
site1 = '127.0.0.1:{}'.format(app.test_client.port)
|
||||
|
||||
# before fix, this raises a RouteExists error
|
||||
@app.get('/get', host=[site1, 'site2.com'], strict_slashes=False)
|
||||
def handler(request):
|
||||
def get_handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('http://' + site1 + '/get')
|
||||
assert response.text == 'OK'
|
||||
|
||||
@app.post('/post', host=[site1, 'site2.com'], strict_slashes=False)
|
||||
def handler(request):
|
||||
def post_handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.post('http://' + site1 +'/post')
|
||||
request, response = app.test_client.post('http://' + site1 + '/post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
@app.put('/put', host=[site1, 'site2.com'], strict_slashes=False)
|
||||
def handler(request):
|
||||
def put_handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.put('http://' + site1 +'/put')
|
||||
request, response = app.test_client.put('http://' + site1 + '/put')
|
||||
assert response.text == 'OK'
|
||||
|
||||
@app.delete('/delete', host=[site1, 'site2.com'], strict_slashes=False)
|
||||
def handler(request):
|
||||
def delete_handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.delete('http://' + site1 +'/delete')
|
||||
request, response = app.test_client.delete('http://' + site1 + '/delete')
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_shorthand_routes_post():
|
||||
app = Sanic('test_shorhand_routes_post')
|
||||
|
||||
def test_shorthand_routes_post(app):
|
||||
|
||||
@app.post('/post')
|
||||
def handler(request):
|
||||
@@ -223,8 +214,7 @@ def test_shorthand_routes_post():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_shorthand_routes_put():
|
||||
app = Sanic('test_shorhand_routes_put')
|
||||
def test_shorthand_routes_put(app):
|
||||
|
||||
@app.put('/put')
|
||||
def handler(request):
|
||||
@@ -240,8 +230,7 @@ def test_shorthand_routes_put():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_shorthand_routes_delete():
|
||||
app = Sanic('test_shorhand_routes_delete')
|
||||
def test_shorthand_routes_delete(app):
|
||||
|
||||
@app.delete('/delete')
|
||||
def handler(request):
|
||||
@@ -257,8 +246,7 @@ def test_shorthand_routes_delete():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_shorthand_routes_patch():
|
||||
app = Sanic('test_shorhand_routes_patch')
|
||||
def test_shorthand_routes_patch(app):
|
||||
|
||||
@app.patch('/patch')
|
||||
def handler(request):
|
||||
@@ -274,8 +262,7 @@ def test_shorthand_routes_patch():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_shorthand_routes_head():
|
||||
app = Sanic('test_shorhand_routes_head')
|
||||
def test_shorthand_routes_head(app):
|
||||
|
||||
@app.head('/head')
|
||||
def handler(request):
|
||||
@@ -291,8 +278,7 @@ def test_shorthand_routes_head():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_shorthand_routes_options():
|
||||
app = Sanic('test_shorhand_routes_options')
|
||||
def test_shorthand_routes_options(app):
|
||||
|
||||
@app.options('/options')
|
||||
def handler(request):
|
||||
@@ -308,8 +294,7 @@ def test_shorthand_routes_options():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_static_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
def test_static_routes(app):
|
||||
|
||||
@app.route('/test')
|
||||
async def handler1(request):
|
||||
@@ -326,9 +311,7 @@ def test_static_routes():
|
||||
assert response.text == 'OK2'
|
||||
|
||||
|
||||
def test_dynamic_route():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
def test_dynamic_route(app):
|
||||
results = []
|
||||
|
||||
@app.route('/folder/<name>')
|
||||
@@ -342,9 +325,7 @@ def test_dynamic_route():
|
||||
assert results[0] == 'test123'
|
||||
|
||||
|
||||
def test_dynamic_route_string():
|
||||
app = Sanic('test_dynamic_route_string')
|
||||
|
||||
def test_dynamic_route_string(app):
|
||||
results = []
|
||||
|
||||
@app.route('/folder/<name:string>')
|
||||
@@ -363,9 +344,7 @@ def test_dynamic_route_string():
|
||||
assert results[1] == 'favicon.ico'
|
||||
|
||||
|
||||
def test_dynamic_route_int():
|
||||
app = Sanic('test_dynamic_route_int')
|
||||
|
||||
def test_dynamic_route_int(app):
|
||||
results = []
|
||||
|
||||
@app.route('/folder/<folder_id:int>')
|
||||
@@ -381,9 +360,7 @@ def test_dynamic_route_int():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_dynamic_route_number():
|
||||
app = Sanic('test_dynamic_route_number')
|
||||
|
||||
def test_dynamic_route_number(app):
|
||||
results = []
|
||||
|
||||
@app.route('/weight/<weight:number>')
|
||||
@@ -402,8 +379,7 @@ def test_dynamic_route_number():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_dynamic_route_regex():
|
||||
app = Sanic('test_dynamic_route_regex')
|
||||
def test_dynamic_route_regex(app):
|
||||
|
||||
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
|
||||
async def handler(request, folder_id):
|
||||
@@ -422,9 +398,8 @@ def test_dynamic_route_regex():
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_dynamic_route_uuid():
|
||||
def test_dynamic_route_uuid(app):
|
||||
import uuid
|
||||
app = Sanic('test_dynamic_route_uuid')
|
||||
|
||||
results = []
|
||||
|
||||
@@ -433,7 +408,8 @@ def test_dynamic_route_uuid():
|
||||
results.append(unique_id)
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/quirky/123e4567-e89b-12d3-a456-426655440000')
|
||||
url = '/quirky/123e4567-e89b-12d3-a456-426655440000'
|
||||
request, response = app.test_client.get(url)
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is uuid.UUID
|
||||
|
||||
@@ -444,8 +420,7 @@ def test_dynamic_route_uuid():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_dynamic_route_path():
|
||||
app = Sanic('test_dynamic_route_path')
|
||||
def test_dynamic_route_path(app):
|
||||
|
||||
@app.route('/<path:path>/info')
|
||||
async def handler(request, path):
|
||||
@@ -468,8 +443,7 @@ def test_dynamic_route_path():
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_dynamic_route_unhashable():
|
||||
app = Sanic('test_dynamic_route_unhashable')
|
||||
def test_dynamic_route_unhashable(app):
|
||||
|
||||
@app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||
async def handler(request, unhashable):
|
||||
@@ -488,12 +462,13 @@ def test_dynamic_route_unhashable():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_websocket_route():
|
||||
app = Sanic('test_websocket_route')
|
||||
@pytest.mark.parametrize('url', ['/ws', 'ws'])
|
||||
def test_websocket_route(app, url):
|
||||
ev = asyncio.Event()
|
||||
|
||||
@app.websocket('/ws')
|
||||
@app.websocket(url)
|
||||
async def handler(request, ws):
|
||||
assert request.scheme == 'ws'
|
||||
assert ws.subprotocol is None
|
||||
ev.set()
|
||||
|
||||
@@ -506,8 +481,7 @@ def test_websocket_route():
|
||||
assert ev.is_set()
|
||||
|
||||
|
||||
def test_websocket_route_with_subprotocols():
|
||||
app = Sanic('test_websocket_route')
|
||||
def test_websocket_route_with_subprotocols(app):
|
||||
results = []
|
||||
|
||||
@app.websocket('/ws', subprotocols=['foo', 'bar'])
|
||||
@@ -548,8 +522,25 @@ def test_websocket_route_with_subprotocols():
|
||||
assert results == ['bar', 'bar', None, None]
|
||||
|
||||
|
||||
def test_route_duplicate():
|
||||
app = Sanic('test_route_duplicate')
|
||||
@pytest.mark.parametrize('strict_slashes', [True, False, None])
|
||||
def test_add_webscoket_route(app, strict_slashes):
|
||||
ev = asyncio.Event()
|
||||
|
||||
async def handler(request, ws):
|
||||
assert ws.subprotocol is None
|
||||
ev.set()
|
||||
|
||||
app.add_websocket_route(handler, '/ws', strict_slashes=strict_slashes)
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13'})
|
||||
assert response.status == 101
|
||||
assert ev.is_set()
|
||||
|
||||
|
||||
def test_route_duplicate(app):
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/test')
|
||||
@@ -562,16 +553,15 @@ def test_route_duplicate():
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/test/<dynamic>/')
|
||||
async def handler1(request, dynamic):
|
||||
async def handler3(request, dynamic):
|
||||
pass
|
||||
|
||||
@app.route('/test/<dynamic>/')
|
||||
async def handler2(request, dynamic):
|
||||
async def handler4(request, dynamic):
|
||||
pass
|
||||
|
||||
|
||||
def test_method_not_allowed():
|
||||
app = Sanic('test_method_not_allowed')
|
||||
def test_method_not_allowed(app):
|
||||
|
||||
@app.route('/test', methods=['GET'])
|
||||
async def handler(request):
|
||||
@@ -584,8 +574,8 @@ def test_method_not_allowed():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_static_add_route():
|
||||
app = Sanic('test_static_add_route')
|
||||
@pytest.mark.parametrize('strict_slashes', [True, False, None])
|
||||
def test_static_add_route(app, strict_slashes):
|
||||
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
@@ -593,8 +583,8 @@ def test_static_add_route():
|
||||
async def handler2(request):
|
||||
return text('OK2')
|
||||
|
||||
app.add_route(handler1, '/test')
|
||||
app.add_route(handler2, '/test2')
|
||||
app.add_route(handler1, '/test', strict_slashes=strict_slashes)
|
||||
app.add_route(handler2, '/test2', strict_slashes=strict_slashes)
|
||||
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.text == 'OK1'
|
||||
@@ -603,8 +593,7 @@ def test_static_add_route():
|
||||
assert response.text == 'OK2'
|
||||
|
||||
|
||||
def test_dynamic_add_route():
|
||||
app = Sanic('test_dynamic_add_route')
|
||||
def test_dynamic_add_route(app):
|
||||
|
||||
results = []
|
||||
|
||||
@@ -619,8 +608,7 @@ def test_dynamic_add_route():
|
||||
assert results[0] == 'test123'
|
||||
|
||||
|
||||
def test_dynamic_add_route_string():
|
||||
app = Sanic('test_dynamic_add_route_string')
|
||||
def test_dynamic_add_route_string(app):
|
||||
|
||||
results = []
|
||||
|
||||
@@ -640,9 +628,7 @@ def test_dynamic_add_route_string():
|
||||
assert results[1] == 'favicon.ico'
|
||||
|
||||
|
||||
def test_dynamic_add_route_int():
|
||||
app = Sanic('test_dynamic_add_route_int')
|
||||
|
||||
def test_dynamic_add_route_int(app):
|
||||
results = []
|
||||
|
||||
async def handler(request, folder_id):
|
||||
@@ -659,9 +645,7 @@ def test_dynamic_add_route_int():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_dynamic_add_route_number():
|
||||
app = Sanic('test_dynamic_add_route_number')
|
||||
|
||||
def test_dynamic_add_route_number(app):
|
||||
results = []
|
||||
|
||||
async def handler(request, weight):
|
||||
@@ -681,8 +665,7 @@ def test_dynamic_add_route_number():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_dynamic_add_route_regex():
|
||||
app = Sanic('test_dynamic_route_int')
|
||||
def test_dynamic_add_route_regex(app):
|
||||
|
||||
async def handler(request, folder_id):
|
||||
return text('OK')
|
||||
@@ -702,8 +685,7 @@ def test_dynamic_add_route_regex():
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_dynamic_add_route_unhashable():
|
||||
app = Sanic('test_dynamic_add_route_unhashable')
|
||||
def test_dynamic_add_route_unhashable(app):
|
||||
|
||||
async def handler(request, unhashable):
|
||||
return text('OK')
|
||||
@@ -723,8 +705,7 @@ def test_dynamic_add_route_unhashable():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_add_route_duplicate():
|
||||
app = Sanic('test_add_route_duplicate')
|
||||
def test_add_route_duplicate(app):
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
async def handler1(request):
|
||||
@@ -747,8 +728,7 @@ def test_add_route_duplicate():
|
||||
app.add_route(handler2, '/test/<dynamic>/')
|
||||
|
||||
|
||||
def test_add_route_method_not_allowed():
|
||||
app = Sanic('test_add_route_method_not_allowed')
|
||||
def test_add_route_method_not_allowed(app):
|
||||
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
@@ -762,8 +742,7 @@ def test_add_route_method_not_allowed():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_remove_static_route():
|
||||
app = Sanic('test_remove_static_route')
|
||||
def test_remove_static_route(app):
|
||||
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
@@ -790,8 +769,7 @@ def test_remove_static_route():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_remove_dynamic_route():
|
||||
app = Sanic('test_remove_dynamic_route')
|
||||
def test_remove_dynamic_route(app):
|
||||
|
||||
async def handler(request, name):
|
||||
return text('OK')
|
||||
@@ -806,15 +784,16 @@ def test_remove_dynamic_route():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_remove_inexistent_route():
|
||||
app = Sanic('test_remove_inexistent_route')
|
||||
def test_remove_inexistent_route(app):
|
||||
|
||||
with pytest.raises(RouteDoesNotExist):
|
||||
app.remove_route('/test')
|
||||
uri = '/test'
|
||||
with pytest.raises(RouteDoesNotExist) as excinfo:
|
||||
app.remove_route(uri)
|
||||
|
||||
assert str(excinfo.value) == 'Route was not registered: {}'.format(uri)
|
||||
|
||||
|
||||
def test_removing_slash():
|
||||
app = Sanic(__name__)
|
||||
def test_removing_slash(app):
|
||||
|
||||
@app.get('/rest/<resource>')
|
||||
def get(_):
|
||||
@@ -827,8 +806,7 @@ def test_removing_slash():
|
||||
assert len(app.router.routes_all.keys()) == 2
|
||||
|
||||
|
||||
def test_remove_unhashable_route():
|
||||
app = Sanic('test_remove_unhashable_route')
|
||||
def test_remove_unhashable_route(app):
|
||||
|
||||
async def handler(request, unhashable):
|
||||
return text('OK')
|
||||
@@ -856,8 +834,7 @@ def test_remove_unhashable_route():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_remove_route_without_clean_cache():
|
||||
app = Sanic('test_remove_static_route')
|
||||
def test_remove_route_without_clean_cache(app):
|
||||
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
@@ -884,8 +861,7 @@ def test_remove_route_without_clean_cache():
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_overload_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
def test_overload_routes(app):
|
||||
|
||||
@app.route('/overload', methods=['GET'])
|
||||
async def handler1(request):
|
||||
@@ -913,8 +889,7 @@ def test_overload_routes():
|
||||
return text('Duplicated')
|
||||
|
||||
|
||||
def test_unmergeable_overload_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
def test_unmergeable_overload_routes(app):
|
||||
|
||||
@app.route('/overload_whole', methods=None)
|
||||
async def handler1(request):
|
||||
@@ -932,12 +907,12 @@ def test_unmergeable_overload_routes():
|
||||
assert response.text == 'OK1'
|
||||
|
||||
@app.route('/overload_part', methods=['GET'])
|
||||
async def handler1(request):
|
||||
async def handler3(request):
|
||||
return text('OK1')
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/overload_part')
|
||||
async def handler2(request):
|
||||
async def handler4(request):
|
||||
return text('Duplicated')
|
||||
|
||||
request, response = app.test_client.get('/overload_part')
|
||||
@@ -947,8 +922,7 @@ def test_unmergeable_overload_routes():
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_unicode_routes():
|
||||
app = Sanic('test_unicode_routes')
|
||||
def test_unicode_routes(app):
|
||||
|
||||
@app.get('/你好')
|
||||
def handler1(request):
|
||||
@@ -965,8 +939,7 @@ def test_unicode_routes():
|
||||
assert response.text == 'OK2 你好'
|
||||
|
||||
|
||||
def test_uri_with_different_method_and_different_params():
|
||||
app = Sanic('test_uri')
|
||||
def test_uri_with_different_method_and_different_params(app):
|
||||
|
||||
@app.route('/ads/<ad_id>', methods=['GET'])
|
||||
async def ad_get(request, ad_id):
|
||||
@@ -987,3 +960,24 @@ def test_uri_with_different_method_and_different_params():
|
||||
assert response.json == {
|
||||
'action': 'post'
|
||||
}
|
||||
|
||||
|
||||
def test_route_raise_ParameterNameConflicts(app):
|
||||
with pytest.raises(ParameterNameConflicts):
|
||||
@app.get('/api/v1/<user>/<user>/')
|
||||
def handler(request, user):
|
||||
return text('OK')
|
||||
|
||||
|
||||
def test_route_invalid_host(app):
|
||||
|
||||
host = 321
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
@app.get('/test', host=host)
|
||||
def handler(request):
|
||||
return text('pass')
|
||||
|
||||
assert str(excinfo.value) == (
|
||||
"Expected either string or Iterable of "
|
||||
"host strings, not {!r}"
|
||||
).format(host)
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
from io import StringIO
|
||||
from random import choice
|
||||
from string import ascii_letters
|
||||
import signal
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
AVAILABLE_LISTENERS = [
|
||||
@@ -15,6 +11,12 @@ AVAILABLE_LISTENERS = [
|
||||
'after_server_stop'
|
||||
]
|
||||
|
||||
skipif_no_alarm = pytest.mark.skipif(
|
||||
not hasattr(signal, 'SIGALRM'),
|
||||
reason='SIGALRM is not implemented for this platform, we have to come '
|
||||
'up with another timeout strategy to test these'
|
||||
)
|
||||
|
||||
|
||||
def create_listener(listener_name, in_list):
|
||||
async def _listener(app, loop):
|
||||
@@ -36,55 +38,49 @@ def start_stop_app(random_name_app, **run_kwargs):
|
||||
pass
|
||||
|
||||
|
||||
@skipif_no_alarm
|
||||
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
|
||||
def test_single_listener(listener_name):
|
||||
def test_single_listener(app, listener_name):
|
||||
"""Test that listeners on their own work"""
|
||||
random_name_app = Sanic(''.join(
|
||||
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||
output = list()
|
||||
output = []
|
||||
# Register listener
|
||||
random_name_app.listener(listener_name)(
|
||||
app.listener(listener_name)(
|
||||
create_listener(listener_name, output))
|
||||
start_stop_app(random_name_app)
|
||||
assert random_name_app.name + listener_name == output.pop()
|
||||
start_stop_app(app)
|
||||
assert app.name + listener_name == output.pop()
|
||||
|
||||
|
||||
@skipif_no_alarm
|
||||
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
|
||||
def test_register_listener(listener_name):
|
||||
def test_register_listener(app, listener_name):
|
||||
"""
|
||||
Test that listeners on their own work with
|
||||
app.register_listener method
|
||||
"""
|
||||
random_name_app = Sanic(''.join(
|
||||
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||
output = list()
|
||||
output = []
|
||||
# Register listener
|
||||
listener = create_listener(listener_name, output)
|
||||
random_name_app.register_listener(listener,
|
||||
event=listener_name)
|
||||
start_stop_app(random_name_app)
|
||||
assert random_name_app.name + listener_name == output.pop()
|
||||
app.register_listener(listener, event=listener_name)
|
||||
start_stop_app(app)
|
||||
assert app.name + listener_name == output.pop()
|
||||
|
||||
|
||||
def test_all_listeners():
|
||||
random_name_app = Sanic(''.join(
|
||||
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||
output = list()
|
||||
@skipif_no_alarm
|
||||
def test_all_listeners(app):
|
||||
output = []
|
||||
for listener_name in AVAILABLE_LISTENERS:
|
||||
listener = create_listener(listener_name, output)
|
||||
random_name_app.listener(listener_name)(listener)
|
||||
start_stop_app(random_name_app)
|
||||
app.listener(listener_name)(listener)
|
||||
start_stop_app(app)
|
||||
for listener_name in AVAILABLE_LISTENERS:
|
||||
assert random_name_app.name + listener_name == output.pop()
|
||||
assert app.name + listener_name == output.pop()
|
||||
|
||||
|
||||
async def test_trigger_before_events_create_server():
|
||||
async def test_trigger_before_events_create_server(app):
|
||||
|
||||
class MySanicDb:
|
||||
pass
|
||||
|
||||
app = Sanic("test_sanic_app")
|
||||
|
||||
@app.listener('before_server_start')
|
||||
async def init_db(app, loop):
|
||||
app.db = MySanicDb()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.testing import HOST, PORT
|
||||
from unittest.mock import MagicMock
|
||||
@@ -12,15 +11,17 @@ async def stop(app, loop):
|
||||
|
||||
calledq = Queue()
|
||||
|
||||
|
||||
def set_loop(app, loop):
|
||||
loop.add_signal_handler = MagicMock()
|
||||
|
||||
|
||||
def after(app, loop):
|
||||
calledq.put(loop.add_signal_handler.called)
|
||||
|
||||
def test_register_system_signals():
|
||||
|
||||
def test_register_system_signals(app):
|
||||
"""Test if sanic register system signals"""
|
||||
app = Sanic('test_register_system_signals')
|
||||
|
||||
@app.route('/hello')
|
||||
async def hello_route(request):
|
||||
@@ -31,12 +32,11 @@ def test_register_system_signals():
|
||||
app.listener('after_server_stop')(after)
|
||||
|
||||
app.run(HOST, PORT)
|
||||
assert calledq.get() == True
|
||||
assert calledq.get() is True
|
||||
|
||||
|
||||
def test_dont_register_system_signals():
|
||||
def test_dont_register_system_signals(app):
|
||||
"""Test if sanic don't register system signals"""
|
||||
app = Sanic('test_register_system_signals')
|
||||
|
||||
@app.route('/hello')
|
||||
async def hello_route(request):
|
||||
@@ -47,4 +47,4 @@ def test_dont_register_system_signals():
|
||||
app.listener('after_server_stop')(after)
|
||||
|
||||
app.run(HOST, PORT, register_sys_signals=False)
|
||||
assert calledq.get() == False
|
||||
assert calledq.get() is False
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import inspect
|
||||
import os
|
||||
from time import gmtime, strftime
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_directory():
|
||||
@@ -25,9 +24,42 @@ def get_file_content(static_file_directory, file_name):
|
||||
return file.read()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_static_file(static_file_directory, file_name):
|
||||
app = Sanic('test_static')
|
||||
@pytest.fixture(scope='module')
|
||||
def large_file(static_file_directory):
|
||||
large_file_path = os.path.join(static_file_directory, 'large.file')
|
||||
|
||||
size = 2 * 1024 * 1024
|
||||
with open(large_file_path, 'w') as f:
|
||||
f.write('a' * size)
|
||||
|
||||
yield large_file_path
|
||||
|
||||
os.remove(large_file_path)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope='module')
|
||||
def symlink(static_file_directory):
|
||||
src = os.path.abspath(os.path.join(os.path.dirname(static_file_directory), 'conftest.py'))
|
||||
symlink = 'symlink'
|
||||
dist = os.path.join(static_file_directory, symlink)
|
||||
os.symlink(src, dist)
|
||||
yield symlink
|
||||
os.remove(dist)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope='module')
|
||||
def hard_link(static_file_directory):
|
||||
src = os.path.abspath(os.path.join(os.path.dirname(static_file_directory), 'conftest.py'))
|
||||
hard_link = 'hard_link'
|
||||
dist = os.path.join(static_file_directory, hard_link)
|
||||
os.link(src, dist)
|
||||
yield hard_link
|
||||
os.remove(dist)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name',
|
||||
['test.file', 'decode me.txt', 'python.png', 'symlink', 'hard_link'])
|
||||
def test_static_file(app, static_file_directory, file_name):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name))
|
||||
|
||||
@@ -37,8 +69,7 @@ def test_static_file(static_file_directory, file_name):
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.html'])
|
||||
def test_static_file_content_type(static_file_directory, file_name):
|
||||
app = Sanic('test_static')
|
||||
def test_static_file_content_type(app, static_file_directory, file_name):
|
||||
app.static(
|
||||
'/testing.file',
|
||||
get_file_path(static_file_directory, file_name),
|
||||
@@ -51,11 +82,9 @@ def test_static_file_content_type(static_file_directory, file_name):
|
||||
assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'symlink', 'hard_link'])
|
||||
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
|
||||
def test_static_directory(file_name, base_uri, static_file_directory):
|
||||
|
||||
app = Sanic('test_static')
|
||||
def test_static_directory(app, file_name, base_uri, static_file_directory):
|
||||
app.static(base_uri, static_file_directory)
|
||||
|
||||
request, response = app.test_client.get(
|
||||
@@ -65,8 +94,7 @@ def test_static_directory(file_name, base_uri, static_file_directory):
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_head_request(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_head_request(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -81,8 +109,7 @@ def test_static_head_request(file_name, static_file_directory):
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_correct(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_correct(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -91,19 +118,18 @@ def test_static_content_range_correct(file_name, static_file_directory):
|
||||
'Range': 'bytes=12-19'
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.status == 206
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[12:19]
|
||||
static_file_directory, file_name))[12:20]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_front(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_front(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -112,7 +138,7 @@ def test_static_content_range_front(file_name, static_file_directory):
|
||||
'Range': 'bytes=12-'
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.status == 206
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
@@ -123,8 +149,7 @@ def test_static_content_range_front(file_name, static_file_directory):
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_back(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_back(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -133,7 +158,7 @@ def test_static_content_range_back(file_name, static_file_directory):
|
||||
'Range': 'bytes=-12'
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.status == 206
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
@@ -143,26 +168,28 @@ def test_static_content_range_back(file_name, static_file_directory):
|
||||
assert response.body == static_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('use_modified_since', [True, False])
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_empty(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_empty(app, file_name, static_file_directory, use_modified_since):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
'/testing.file',
|
||||
get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True,
|
||||
use_modified_since=use_modified_since
|
||||
)
|
||||
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' not in response.headers
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(get_file_content(static_file_directory, file_name))
|
||||
assert int(response.headers['Content-Length']) == \
|
||||
len(get_file_content(static_file_directory, file_name))
|
||||
assert response.body == bytes(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_error(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_error(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -178,9 +205,72 @@ def test_static_content_range_error(file_name, static_file_directory):
|
||||
len(get_file_content(static_file_directory, file_name)),)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_static_file_specified_host(static_file_directory, file_name):
|
||||
app = Sanic('test_static')
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_invalid_unit(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
unit = 'bit'
|
||||
headers = {
|
||||
'Range': '{}=1-0'.format(unit)
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
|
||||
assert response.status == 416
|
||||
assert response.text == "Error: {} is not a valid Range Type".format(unit)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_invalid_start(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
start = 'start'
|
||||
headers = {
|
||||
'Range': 'bytes={}-0'.format(start)
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
|
||||
assert response.status == 416
|
||||
assert response.text == "Error: '{}' is invalid for Content Range".format(start)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_invalid_end(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
end = 'end'
|
||||
headers = {
|
||||
'Range': 'bytes=1-{}'.format(end)
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
|
||||
assert response.status == 416
|
||||
assert response.text == "Error: '{}' is invalid for Content Range".format(end)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_invalid_parameters(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
headers = {
|
||||
'Range': 'bytes=-'
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
|
||||
assert response.status == 416
|
||||
assert response.text == "Error: Invalid for Content Range parameters"
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name',
|
||||
['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_static_file_specified_host(app, static_file_directory, file_name):
|
||||
app.static(
|
||||
'/testing.file',
|
||||
get_file_path(static_file_directory, file_name),
|
||||
@@ -193,3 +283,73 @@ def test_static_file_specified_host(static_file_directory, file_name):
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@pytest.mark.parametrize('use_modified_since', [True, False])
|
||||
@pytest.mark.parametrize('stream_large_files', [True, 1024])
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'large.file'])
|
||||
def test_static_stream_large_file(app, static_file_directory, file_name, use_modified_since, stream_large_files, large_file):
|
||||
app.static(
|
||||
'/testing.file',
|
||||
get_file_path(static_file_directory, file_name),
|
||||
use_modified_since=use_modified_since,
|
||||
stream_large_files=stream_large_files
|
||||
)
|
||||
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_use_modified_since(app, static_file_directory, file_name):
|
||||
|
||||
file_stat = os.stat(get_file_path(static_file_directory, file_name))
|
||||
modified_since = strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime(file_stat.st_mtime))
|
||||
|
||||
app.static(
|
||||
'/testing.file',
|
||||
get_file_path(static_file_directory, file_name),
|
||||
use_modified_since=True
|
||||
)
|
||||
|
||||
request, response = app.test_client.get(
|
||||
'/testing.file', headers={'If-Modified-Since': modified_since})
|
||||
|
||||
assert response.status == 304
|
||||
|
||||
|
||||
def test_file_not_found(app, static_file_directory):
|
||||
app.static('/static', static_file_directory)
|
||||
|
||||
request, response = app.test_client.get('/static/not_found')
|
||||
|
||||
assert response.status == 404
|
||||
assert response.text == 'Error: File not found'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('static_name', ['_static_name', 'static'])
|
||||
@pytest.mark.parametrize('file_name', ['test.html'])
|
||||
def test_static_name(app, static_file_directory, static_name, file_name):
|
||||
app.static('/static', static_file_directory, name=static_name)
|
||||
|
||||
request, response = app.test_client.get('/static/{}'.format(file_name))
|
||||
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name',
|
||||
['test.file'])
|
||||
def test_static_remove_route(app, static_file_directory, file_name):
|
||||
app.static(
|
||||
'/testing.file',
|
||||
get_file_path(static_file_directory, file_name)
|
||||
)
|
||||
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
assert response.status == 200
|
||||
|
||||
app.remove_route('/testing.file')
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
assert response.status == 404
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import pytest as pytest
|
||||
from urllib.parse import urlsplit, parse_qsl
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.blueprints import Blueprint
|
||||
@@ -14,12 +13,16 @@ URL_FOR_ARGS1 = dict(arg1=['v1', 'v2'])
|
||||
URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2'
|
||||
URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
|
||||
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
|
||||
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
|
||||
_server='{}:{}'.format(test_host, test_port), _external=True)
|
||||
URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
|
||||
URL_FOR_ARGS3 = dict(
|
||||
arg1='v1', _anchor='anchor', _scheme='http',
|
||||
_server='{}:{}'.format(test_host, test_port), _external=True
|
||||
)
|
||||
URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host,
|
||||
test_port)
|
||||
URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True,
|
||||
_server='http://{}:{}'.format(test_host, test_port))
|
||||
URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
|
||||
URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host,
|
||||
test_port)
|
||||
|
||||
|
||||
def _generate_handlers_from_names(app, l):
|
||||
@@ -30,8 +33,7 @@ def _generate_handlers_from_names(app, l):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_app():
|
||||
app = Sanic('simple_app')
|
||||
def simple_app(app):
|
||||
handler_names = list(string.ascii_letters)
|
||||
|
||||
_generate_handlers_from_names(app, handler_names)
|
||||
@@ -54,8 +56,7 @@ def test_simple_url_for_getting(simple_app):
|
||||
(URL_FOR_ARGS2, URL_FOR_VALUE2),
|
||||
(URL_FOR_ARGS3, URL_FOR_VALUE3),
|
||||
(URL_FOR_ARGS4, URL_FOR_VALUE4)])
|
||||
def test_simple_url_for_getting_with_more_params(args, url):
|
||||
app = Sanic('more_url_build')
|
||||
def test_simple_url_for_getting_with_more_params(app, args, url):
|
||||
|
||||
@app.route('/myurl')
|
||||
def passes(request):
|
||||
@@ -67,8 +68,26 @@ def test_simple_url_for_getting_with_more_params(args, url):
|
||||
assert response.text == 'this should pass'
|
||||
|
||||
|
||||
def test_fails_if_endpoint_not_found():
|
||||
app = Sanic('fail_url_build')
|
||||
def test_url_for_with_server_name(app):
|
||||
|
||||
server_name = '{}:{}'.format(test_host, test_port)
|
||||
app.config.update({
|
||||
'SERVER_NAME': server_name
|
||||
})
|
||||
path = '/myurl'
|
||||
|
||||
@app.route(path)
|
||||
def passes(request):
|
||||
return text('this should pass')
|
||||
|
||||
url = 'http://{}{}'.format(server_name, path)
|
||||
assert url == app.url_for('passes', _server=None, _external=True)
|
||||
request, response = app.test_client.get(url)
|
||||
assert response.status == 200
|
||||
assert response.text == 'this should pass'
|
||||
|
||||
|
||||
def test_fails_if_endpoint_not_found(app):
|
||||
|
||||
@app.route('/fail')
|
||||
def fail(request):
|
||||
@@ -80,14 +99,12 @@ def test_fails_if_endpoint_not_found():
|
||||
assert str(e.value) == 'Endpoint with name `passes` was not found'
|
||||
|
||||
|
||||
def test_fails_url_build_if_param_not_passed():
|
||||
def test_fails_url_build_if_param_not_passed(app):
|
||||
url = '/'
|
||||
|
||||
for letter in string.ascii_letters:
|
||||
url += '<{}>/'.format(letter)
|
||||
|
||||
app = Sanic('fail_url_build')
|
||||
|
||||
@app.route(url)
|
||||
def fail(request):
|
||||
return text('this should fail')
|
||||
@@ -103,8 +120,7 @@ def test_fails_url_build_if_param_not_passed():
|
||||
assert 'Required parameter `Z` was not passed to url_for' in str(e.value)
|
||||
|
||||
|
||||
def test_fails_url_build_if_params_not_passed():
|
||||
app = Sanic('fail_url_build')
|
||||
def test_fails_url_build_if_params_not_passed(app):
|
||||
|
||||
@app.route('/fail')
|
||||
def fail(request):
|
||||
@@ -126,8 +142,7 @@ PASSING_KWARGS = {
|
||||
EXPECTED_BUILT_URL = '/4/woof/ba/normal/1.001'
|
||||
|
||||
|
||||
def test_fails_with_int_message():
|
||||
app = Sanic('fail_url_build')
|
||||
def test_fails_with_int_message(app):
|
||||
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def fail(request):
|
||||
@@ -140,13 +155,12 @@ def test_fails_with_int_message():
|
||||
app.url_for('fail', **failing_kwargs)
|
||||
|
||||
expected_error = (
|
||||
'Value "not_int" for parameter `foo` '
|
||||
'does not match pattern for type `int`: \d+')
|
||||
r'Value "not_int" for parameter `foo` '
|
||||
r'does not match pattern for type `int`: \d+')
|
||||
assert str(e.value) == expected_error
|
||||
|
||||
|
||||
def test_fails_with_two_letter_string_message():
|
||||
app = Sanic('fail_url_build')
|
||||
def test_fails_with_two_letter_string_message(app):
|
||||
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def fail(request):
|
||||
@@ -165,8 +179,7 @@ def test_fails_with_two_letter_string_message():
|
||||
assert str(e.value) == expected_error
|
||||
|
||||
|
||||
def test_fails_with_number_message():
|
||||
app = Sanic('fail_url_build')
|
||||
def test_fails_with_number_message(app):
|
||||
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def fail(request):
|
||||
@@ -185,8 +198,7 @@ def test_fails_with_number_message():
|
||||
assert str(e.value) == expected_error
|
||||
|
||||
|
||||
def test_adds_other_supplied_values_as_query_string():
|
||||
app = Sanic('passes')
|
||||
def test_adds_other_supplied_values_as_query_string(app):
|
||||
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def passes(request):
|
||||
@@ -205,8 +217,7 @@ def test_adds_other_supplied_values_as_query_string():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def blueprint_app():
|
||||
app = Sanic('blueprints')
|
||||
def blueprint_app(app):
|
||||
|
||||
first_print = Blueprint('first', url_prefix='/first')
|
||||
second_print = Blueprint('second', url_prefix='/second')
|
||||
@@ -252,8 +263,7 @@ def test_blueprints_work_with_params(blueprint_app):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def methodview_app():
|
||||
app = Sanic('methodview')
|
||||
def methodview_app(app):
|
||||
|
||||
class ViewOne(HTTPMethodView):
|
||||
def get(self, request):
|
||||
|
||||
@@ -3,7 +3,6 @@ import os
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
|
||||
@@ -26,9 +25,9 @@ def get_file_content(static_file_directory, file_name):
|
||||
return file.read()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_static_file(static_file_directory, file_name):
|
||||
app = Sanic('test_static')
|
||||
@pytest.mark.parametrize('file_name',
|
||||
['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_static_file(app, static_file_directory, file_name):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name))
|
||||
app.static(
|
||||
@@ -102,9 +101,7 @@ def test_static_file(static_file_directory, file_name):
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
|
||||
def test_static_directory(file_name, base_uri, static_file_directory):
|
||||
|
||||
app = Sanic('test_static')
|
||||
def test_static_directory(app, file_name, base_uri, static_file_directory):
|
||||
app.static(base_uri, static_file_directory)
|
||||
base_uri2 = base_uri + '/2'
|
||||
app.static(base_uri2, static_file_directory, name='uploads')
|
||||
@@ -156,10 +153,8 @@ def test_static_directory(file_name, base_uri, static_file_directory):
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_head_request(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_head_request(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -198,8 +193,7 @@ def test_static_head_request(file_name, static_file_directory):
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_correct(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_correct(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -218,11 +212,11 @@ def test_static_content_range_correct(file_name, static_file_directory):
|
||||
assert uri == app.url_for('static', name='static', filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.status == 206
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[12:19]
|
||||
static_file_directory, file_name))[12:20]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
@@ -239,19 +233,18 @@ def test_static_content_range_correct(file_name, static_file_directory):
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.status == 206
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[12:19]
|
||||
static_file_directory, file_name))[12:20]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_front(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_front(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -270,7 +263,7 @@ def test_static_content_range_front(file_name, static_file_directory):
|
||||
assert uri == app.url_for('static', name='static', filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.status == 206
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
@@ -291,7 +284,7 @@ def test_static_content_range_front(file_name, static_file_directory):
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.status == 206
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
@@ -302,8 +295,7 @@ def test_static_content_range_front(file_name, static_file_directory):
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_back(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_back(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -322,7 +314,7 @@ def test_static_content_range_back(file_name, static_file_directory):
|
||||
assert uri == app.url_for('static', name='static', filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.status == 206
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
@@ -343,7 +335,7 @@ def test_static_content_range_back(file_name, static_file_directory):
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.status == 206
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
@@ -354,8 +346,7 @@ def test_static_content_range_back(file_name, static_file_directory):
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_empty(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_empty(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
@@ -374,8 +365,8 @@ def test_static_content_range_empty(file_name, static_file_directory):
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' not in response.headers
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(get_file_content(static_file_directory, file_name))
|
||||
assert int(response.headers['Content-Length']) == \
|
||||
len(get_file_content(static_file_directory, file_name))
|
||||
assert response.body == bytes(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
@@ -394,15 +385,14 @@ def test_static_content_range_empty(file_name, static_file_directory):
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' not in response.headers
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(get_file_content(static_file_directory, file_name))
|
||||
assert int(response.headers['Content-Length']) == \
|
||||
len(get_file_content(static_file_directory, file_name))
|
||||
assert response.body == bytes(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_error(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
def test_static_content_range_error(app, file_name, static_file_directory):
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from json import dumps as json_dumps
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# UTF-8
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_utf8_query_string():
|
||||
app = Sanic('test_utf8_query_string')
|
||||
def test_utf8_query_string(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -18,8 +16,7 @@ def test_utf8_query_string():
|
||||
assert request.args.get('utf8') == '✓'
|
||||
|
||||
|
||||
def test_utf8_response():
|
||||
app = Sanic('test_utf8_response')
|
||||
def test_utf8_response(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -29,8 +26,7 @@ def test_utf8_response():
|
||||
assert response.text == '✓'
|
||||
|
||||
|
||||
def skip_test_utf8_route():
|
||||
app = Sanic('skip_test_utf8_route')
|
||||
def skip_test_utf8_route(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
@@ -41,8 +37,7 @@ def skip_test_utf8_route():
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_utf8_post_json():
|
||||
app = Sanic('test_utf8_post_json')
|
||||
def test_utf8_post_json(app):
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
def test_vhosts():
|
||||
app = Sanic('test_vhosts')
|
||||
def test_vhosts(app):
|
||||
|
||||
@app.route('/', host="example.com")
|
||||
async def handler(request):
|
||||
async def handler1(request):
|
||||
return text("You're at example.com!")
|
||||
|
||||
@app.route('/', host="subdomain.example.com")
|
||||
async def handler(request):
|
||||
async def handler2(request):
|
||||
return text("You're at subdomain.example.com!")
|
||||
|
||||
headers = {"Host": "example.com"}
|
||||
@@ -22,8 +20,7 @@ def test_vhosts():
|
||||
assert response.text == "You're at subdomain.example.com!"
|
||||
|
||||
|
||||
def test_vhosts_with_list():
|
||||
app = Sanic('test_vhosts')
|
||||
def test_vhosts_with_list(app):
|
||||
|
||||
@app.route('/', host=["hello.com", "world.com"])
|
||||
async def handler(request):
|
||||
@@ -37,15 +34,15 @@ def test_vhosts_with_list():
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
assert response.text == "Hello, world!"
|
||||
|
||||
def test_vhosts_with_defaults():
|
||||
app = Sanic('test_vhosts')
|
||||
|
||||
def test_vhosts_with_defaults(app):
|
||||
|
||||
@app.route('/', host="hello.com")
|
||||
async def handler(request):
|
||||
async def handler1(request):
|
||||
return text("Hello, world!")
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
async def handler2(request):
|
||||
return text("default")
|
||||
|
||||
headers = {"Host": "hello.com"}
|
||||
@@ -54,3 +51,18 @@ def test_vhosts_with_defaults():
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == "default"
|
||||
|
||||
|
||||
def test_remove_vhost_route(app):
|
||||
|
||||
@app.route('/', host="example.com")
|
||||
async def handler1(request):
|
||||
return text("You're at example.com!")
|
||||
|
||||
headers = {"Host": "example.com"}
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
assert response.status == 200
|
||||
|
||||
app.remove_route('/', host="example.com")
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
assert response.status == 404
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import pytest as pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.response import text, HTTPResponse
|
||||
from sanic.views import HTTPMethodView, CompositionView
|
||||
@@ -10,8 +9,7 @@ from sanic.constants import HTTP_METHODS
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_methods(method):
|
||||
app = Sanic('test_methods')
|
||||
def test_methods(app, method):
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
@@ -44,8 +42,7 @@ def test_methods(method):
|
||||
assert response.headers['method'] == method
|
||||
|
||||
|
||||
def test_unexisting_methods():
|
||||
app = Sanic('test_unexisting_methods')
|
||||
def test_unexisting_methods(app):
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
@@ -59,8 +56,7 @@ def test_unexisting_methods():
|
||||
assert response.text == 'Error: Method POST not allowed for URL /'
|
||||
|
||||
|
||||
def test_argument_methods():
|
||||
app = Sanic('test_argument_methods')
|
||||
def test_argument_methods(app):
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
@@ -74,8 +70,7 @@ def test_argument_methods():
|
||||
assert response.text == 'I am get method with test123'
|
||||
|
||||
|
||||
def test_with_bp():
|
||||
app = Sanic('test_with_bp')
|
||||
def test_with_bp(app):
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
@@ -93,8 +88,7 @@ def test_with_bp():
|
||||
assert response.text == 'I am get method'
|
||||
|
||||
|
||||
def test_with_bp_with_url_prefix():
|
||||
app = Sanic('test_with_bp_with_url_prefix')
|
||||
def test_with_bp_with_url_prefix(app):
|
||||
bp = Blueprint('test_text', url_prefix='/test1')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
@@ -110,8 +104,7 @@ def test_with_bp_with_url_prefix():
|
||||
assert response.text == 'I am get method'
|
||||
|
||||
|
||||
def test_with_middleware():
|
||||
app = Sanic('test_with_middleware')
|
||||
def test_with_middleware(app):
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
@@ -132,13 +125,11 @@ def test_with_middleware():
|
||||
assert type(results[0]) is Request
|
||||
|
||||
|
||||
def test_with_middleware_response():
|
||||
app = Sanic('test_with_middleware_response')
|
||||
|
||||
def test_with_middleware_response(app):
|
||||
results = []
|
||||
|
||||
@app.middleware('request')
|
||||
async def process_response(request):
|
||||
async def process_request(request):
|
||||
results.append(request)
|
||||
|
||||
@app.middleware('response')
|
||||
@@ -161,8 +152,7 @@ def test_with_middleware_response():
|
||||
assert isinstance(results[2], HTTPResponse)
|
||||
|
||||
|
||||
def test_with_custom_class_methods():
|
||||
app = Sanic('test_with_custom_class_methods')
|
||||
def test_with_custom_class_methods(app):
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
global_var = 0
|
||||
@@ -172,16 +162,15 @@ def test_with_custom_class_methods():
|
||||
|
||||
def get(self, request):
|
||||
self._iternal_method()
|
||||
return text('I am get method and global var is {}'.format(self.global_var))
|
||||
return text('I am get method and global var '
|
||||
'is {}'.format(self.global_var))
|
||||
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'I am get method and global var is 10'
|
||||
|
||||
|
||||
def test_with_decorator():
|
||||
app = Sanic('test_with_decorator')
|
||||
|
||||
def test_with_decorator(app):
|
||||
results = []
|
||||
|
||||
def stupid_decorator(view):
|
||||
@@ -227,9 +216,7 @@ def test_composition_view_rejects_duplicate_methods():
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_composition_view_runs_methods_as_expected(method):
|
||||
app = Sanic('test_composition_view')
|
||||
|
||||
def test_composition_view_runs_methods_as_expected(app, method):
|
||||
view = CompositionView()
|
||||
|
||||
def first(request):
|
||||
@@ -245,15 +232,19 @@ def test_composition_view_runs_methods_as_expected(method):
|
||||
request, response = getattr(app.test_client, method.lower())('/')
|
||||
assert response.text == 'first method'
|
||||
|
||||
response = view(request)
|
||||
assert response.body.decode() == 'first method'
|
||||
|
||||
if method in ['DELETE', 'PATCH']:
|
||||
request, response = getattr(app.test_client, method.lower())('/')
|
||||
assert response.text == 'second method'
|
||||
|
||||
response = view(request)
|
||||
assert response.body.decode() == 'second method'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_composition_view_rejects_invalid_methods(method):
|
||||
app = Sanic('test_composition_view')
|
||||
|
||||
def test_composition_view_rejects_invalid_methods(app, method):
|
||||
view = CompositionView()
|
||||
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
|
||||
|
||||
|
||||
@@ -7,13 +7,17 @@ from unittest import mock
|
||||
from sanic.worker import GunicornWorker
|
||||
from sanic.app import Sanic
|
||||
import asyncio
|
||||
import logging
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def gunicorn_worker():
|
||||
command = 'gunicorn --bind 127.0.0.1:1337 --worker-class sanic.worker.GunicornWorker examples.simple_server:app'
|
||||
command = (
|
||||
'gunicorn '
|
||||
'--bind 127.0.0.1:1337 '
|
||||
'--worker-class sanic.worker.GunicornWorker '
|
||||
'examples.simple_server:app'
|
||||
)
|
||||
worker = subprocess.Popen(shlex.split(command))
|
||||
time.sleep(3)
|
||||
yield
|
||||
@@ -96,10 +100,11 @@ def test_run_max_requests_exceeded(worker):
|
||||
_runner = asyncio.ensure_future(worker._check_alive(), loop=loop)
|
||||
loop.run_until_complete(_runner)
|
||||
|
||||
assert worker.alive == False
|
||||
assert not worker.alive
|
||||
worker.notify.assert_called_with()
|
||||
worker.log.info.assert_called_with("Max requests exceeded, shutting down: %s",
|
||||
worker)
|
||||
worker.log.info.assert_called_with("Max requests exceeded, shutting "
|
||||
"down: %s", worker)
|
||||
|
||||
|
||||
def test_worker_close(worker):
|
||||
loop = asyncio.new_event_loop()
|
||||
@@ -113,14 +118,15 @@ def test_worker_close(worker):
|
||||
conn = mock.Mock()
|
||||
conn.websocket = mock.Mock()
|
||||
conn.websocket.close_connection = mock.Mock(
|
||||
wraps=asyncio.coroutine(lambda *a, **kw: None)
|
||||
)
|
||||
wraps=asyncio.coroutine(lambda *a, **kw: None)
|
||||
)
|
||||
worker.connections = set([conn])
|
||||
worker.log = mock.Mock()
|
||||
worker.loop = loop
|
||||
server = mock.Mock()
|
||||
server.close = mock.Mock(wraps=lambda *a, **kw: None)
|
||||
server.wait_closed = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None))
|
||||
server.wait_closed = mock.Mock(wraps=asyncio.coroutine(
|
||||
lambda *a, **kw: None))
|
||||
worker.servers = {
|
||||
server: {"requests_count": 14},
|
||||
}
|
||||
@@ -130,6 +136,6 @@ def test_worker_close(worker):
|
||||
_close = asyncio.ensure_future(worker.close(), loop=loop)
|
||||
loop.run_until_complete(_close)
|
||||
|
||||
assert worker.signal.stopped == True
|
||||
assert conn.websocket.close_connection.called == True
|
||||
assert worker.signal.stopped
|
||||
assert conn.websocket.close_connection.called
|
||||
assert len(worker.servers) == 0
|
||||
|
||||
11
tox.ini
11
tox.ini
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = py35, py36, py37, {py35,py36,py37}-no-ext, flake8, check
|
||||
envlist = py35, py36, py37, {py35,py36,py37}-no-ext, lint, check
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
@@ -8,7 +8,7 @@ setenv =
|
||||
{py35,py36,py37}-no-ext: SANIC_NO_UVLOOP=1
|
||||
deps =
|
||||
coverage
|
||||
pytest==3.3.2
|
||||
pytest>=3.6
|
||||
pytest-cov
|
||||
pytest-sanic
|
||||
pytest-sugar
|
||||
@@ -20,13 +20,18 @@ commands =
|
||||
pytest tests --cov sanic --cov-report= {posargs}
|
||||
- coverage combine --append
|
||||
coverage report -m
|
||||
coverage html -i
|
||||
|
||||
[testenv:flake8]
|
||||
[testenv:lint]
|
||||
deps =
|
||||
flake8
|
||||
black
|
||||
isort
|
||||
|
||||
commands =
|
||||
flake8 sanic
|
||||
black --check --verbose sanic
|
||||
isort --check-only --recursive sanic
|
||||
|
||||
[testenv:check]
|
||||
deps =
|
||||
|
||||
Reference in New Issue
Block a user