Compare commits
	
		
			161 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 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 | ||
|   | 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 | ||||
|   | ||||
							
								
								
									
										23
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								README.rst
									
									
									
									
									
								
							| @@ -1,13 +1,13 @@ | ||||
| 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/huge-success/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/huge-success/sanic/issues/396>`_ that we use to track those projects! | ||||
|  | ||||
| @@ -47,15 +47,26 @@ 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 | ||||
| .. |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 | ||||
| -------- | ||||
| @@ -66,14 +77,6 @@ Examples | ||||
| `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.2' | ||||
|  | ||||
| __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' | ||||
|             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' | ||||
|             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") | ||||
|   | ||||
							
								
								
									
										398
									
								
								sanic/server.py
									
									
									
									
									
								
							
							
						
						
									
										398
									
								
								sanic/server.py
									
									
									
									
									
								
							| @@ -1,37 +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 | ||||
|  | ||||
| @@ -43,27 +44,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 +132,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 +154,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 | ||||
|  | ||||
|     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: | ||||
| @@ -141,19 +186,15 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         time_elapsed = current_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 | ||||
| @@ -161,19 +202,15 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         time_elapsed = current_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 | ||||
| @@ -181,12 +218,11 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         time_elapsed = current_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 +235,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 +244,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 +265,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 +286,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 +295,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 +304,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 +317,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.response_timeout, self.response_timeout_callback | ||||
|         ) | ||||
|         self._last_request_time = current_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,31 +372,37 @@ 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.keep_alive_timeout, self.keep_alive_timeout_callback | ||||
|                 ) | ||||
|                 self._last_response_time = current_time | ||||
|                 self.cleanup() | ||||
|  | ||||
| @@ -380,30 +426,36 @@ 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.keep_alive_timeout, self.keep_alive_timeout_callback | ||||
|                 ) | ||||
|                 self._last_response_time = current_time | ||||
|                 self.cleanup() | ||||
|  | ||||
| @@ -416,35 +468,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 +559,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 +636,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 +680,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 +711,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 +748,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 +769,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 +804,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) | ||||
							
								
								
									
										149
									
								
								tests/test_app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								tests/test_app.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| 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') | ||||
|  | ||||
|     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( | ||||
|                     verify_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,51 @@ 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') | ||||
|  | ||||
|     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') | ||||
|  | ||||
|     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 +130,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 +146,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 +180,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): | ||||
| @@ -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): | ||||
| @@ -145,8 +160,7 @@ def test_fails_with_int_message(): | ||||
|     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 | ||||
|   | ||||
							
								
								
									
										9
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								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 | ||||
| @@ -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