Compare commits
	
		
			11 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 9b24fbb2f3 | ||
|   | 468f4ac7f1 | ||
|   | be1ca93a23 | ||
|   | 662c7c7f62 | ||
|   | 3e4bec7f2c | ||
|   | df4970a73d | ||
|   | c5070bd449 | ||
|   | eb3d0a3f87 | ||
|   | c09129ec63 | ||
|   | 2a44a27236 | ||
|   | bb9ff7cec1 | 
| @@ -12,11 +12,6 @@ environment: | |||||||
|       PYTHON_VERSION: "3.7.x" |       PYTHON_VERSION: "3.7.x" | ||||||
|       PYTHON_ARCH: "64" |       PYTHON_ARCH: "64" | ||||||
|  |  | ||||||
|     - TOXENV: py38-no-ext |  | ||||||
|       PYTHON: "C:\\Python38-x64" |  | ||||||
|       PYTHON_VERSION: "3.8.x" |  | ||||||
|       PYTHON_ARCH: "64" |  | ||||||
|  |  | ||||||
| init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" | init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" | ||||||
|  |  | ||||||
| install: | install: | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -21,46 +21,23 @@ matrix: | |||||||
|       dist: xenial |       dist: xenial | ||||||
|       sudo: true |       sudo: true | ||||||
|       name: "Python 3.7 without Extensions" |       name: "Python 3.7 without Extensions" | ||||||
|     - env: TOX_ENV=py38 |  | ||||||
|       python: 3.8 |  | ||||||
|       dist: xenial |  | ||||||
|       sudo: true |  | ||||||
|       name: "Python 3.8 with Extensions" |  | ||||||
|     - env: TOX_ENV=py38-no-ext |  | ||||||
|       python: 3.8 |  | ||||||
|       dist: xenial |  | ||||||
|       sudo: true |  | ||||||
|       name: "Python 3.8 without Extensions" |  | ||||||
|     - env: TOX_ENV=type-checking |     - env: TOX_ENV=type-checking | ||||||
|       python: 3.6 |       python: 3.6 | ||||||
|       name: "Python 3.6 Type checks" |       name: "Python 3.6 Type checks" | ||||||
|     - env: TOX_ENV=type-checking |     - env: TOX_ENV=type-checking | ||||||
|       python: 3.7 |       python: 3.7 | ||||||
|       name: "Python 3.7 Type checks" |       name: "Python 3.7 Type checks" | ||||||
|     - env: TOX_ENV=type-checking |  | ||||||
|       python: 3.8 |  | ||||||
|       name: "Python 3.8 Type checks" |  | ||||||
|     - env: TOX_ENV=lint |     - env: TOX_ENV=lint | ||||||
|       python: 3.6 |       python: 3.6 | ||||||
|       name: "Python 3.6 Linter checks" |       name: "Python 3.6 Linter checks" | ||||||
|     - env: TOX_ENV=check |     - env: TOX_ENV=check | ||||||
|       python: 3.6 |       python: 3.6 | ||||||
|       name: "Python 3.6 Package checks" |       name: "Python 3.6 Package checks" | ||||||
|     - env: TOX_ENV=security |  | ||||||
|       python: 3.6 |  | ||||||
|       dist: xenial |  | ||||||
|       sudo: true |  | ||||||
|       name: "Python 3.6 Bandit security scan" |  | ||||||
|     - env: TOX_ENV=security |     - env: TOX_ENV=security | ||||||
|       python: 3.7 |       python: 3.7 | ||||||
|       dist: xenial |       dist: xenial | ||||||
|       sudo: true |       sudo: true | ||||||
|       name: "Python 3.7 Bandit security scan" |       name: "Python 3.7 Bandit security scan" | ||||||
|     - env: TOX_ENV=security |  | ||||||
|       python: 3.8 |  | ||||||
|       dist: xenial |  | ||||||
|       sudo: true |  | ||||||
|       name: "Python 3.8 Bandit security scan" |  | ||||||
|     - env: TOX_ENV=docs |     - env: TOX_ENV=docs | ||||||
|       python: 3.7 |       python: 3.7 | ||||||
|       dist: xenial |       dist: xenial | ||||||
| @@ -71,14 +48,14 @@ matrix: | |||||||
|       name: "Python nightly with Extensions" |       name: "Python nightly with Extensions" | ||||||
|     - env: TOX_ENV=pyNightly-no-ext |     - env: TOX_ENV=pyNightly-no-ext | ||||||
|       python: 'nightly' |       python: 'nightly' | ||||||
|       name: "Python nightly without Extensions" |       name: "Python nightly Extensions" | ||||||
|   allow_failures: |   allow_failures: | ||||||
|     - env: TOX_ENV=pyNightly |     - env: TOX_ENV=pyNightly | ||||||
|       python: 'nightly' |       python: 'nightly' | ||||||
|       name: "Python nightly with Extensions" |       name: "Python nightly with Extensions" | ||||||
|     - env: TOX_ENV=pyNightly-no-ext |     - env: TOX_ENV=pyNightly-no-ext | ||||||
|       python: 'nightly' |       python: 'nightly' | ||||||
|       name: "Python nightly without Extensions" |       name: "Python nightly Extensions" | ||||||
| install: | install: | ||||||
|   - pip install -U tox |   - pip install -U tox | ||||||
|   - pip install codecov |   - pip install codecov | ||||||
|   | |||||||
							
								
								
									
										237
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							
							
						
						
									
										237
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							| @@ -1,238 +1,3 @@ | |||||||
| Version 20.6.3 |  | ||||||
| =============== |  | ||||||
|  |  | ||||||
| Bugfixes |  | ||||||
| ******** |  | ||||||
|    |  | ||||||
|   * |  | ||||||
|     `#1884 <https://github.com/huge-success/sanic/pull/1884>`_ |  | ||||||
|     Revert change to multiprocessing mode |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Version 20.6.2 |  | ||||||
| =============== |  | ||||||
|  |  | ||||||
| Features |  | ||||||
| ******** |  | ||||||
|    |  | ||||||
|   * |  | ||||||
|     `#1641 <https://github.com/huge-success/sanic/pull/1641>`_ |  | ||||||
|     Socket binding implemented properly for IPv6 and UNIX sockets |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Version 20.6.1 |  | ||||||
| =============== |  | ||||||
|  |  | ||||||
| Features |  | ||||||
| ******** |  | ||||||
|    |  | ||||||
|   * |  | ||||||
|     `#1760 <https://github.com/huge-success/sanic/pull/1760>`_ |  | ||||||
|     Add version parameter to websocket routes |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1866 <https://github.com/huge-success/sanic/pull/1866>`_ |  | ||||||
|     Add ``sanic`` as an entry point command |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1880 <https://github.com/huge-success/sanic/pull/1880>`_ |  | ||||||
|     Add handler names for websockets for url_for usage  |  | ||||||
|  |  | ||||||
| Bugfixes |  | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1776 <https://github.com/huge-success/sanic/pull/1776>`_ |  | ||||||
|     Bug fix for host parameter issue with lists |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1842 <https://github.com/huge-success/sanic/pull/1842>`_ |  | ||||||
|     Fix static _handler pickling error |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1827 <https://github.com/huge-success/sanic/pull/1827>`_ |  | ||||||
|     Fix reloader on OSX py38 and Windows |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1848 <https://github.com/huge-success/sanic/pull/1848>`_ |  | ||||||
|     Reverse named_response_middlware execution order, to match normal response middleware execution order |  | ||||||
|    |  | ||||||
|   * |  | ||||||
|     `#1853 <https://github.com/huge-success/sanic/pull/1853>`_ |  | ||||||
|     Fix pickle error when attempting to pickle an application which contains websocket routes |  | ||||||
|  |  | ||||||
| Deprecations and Removals |  | ||||||
| ************************* |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1739 <https://github.com/huge-success/sanic/pull/1739>`_ |  | ||||||
|     Deprecate body_bytes to merge into body |  | ||||||
|  |  | ||||||
| Developer infrastructure |  | ||||||
| ************************ |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1852 <https://github.com/huge-success/sanic/pull/1852>`_ |  | ||||||
|     Fix naming of CI test env on Python nightlies |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1857 <https://github.com/huge-success/sanic/pull/1857>`_ |  | ||||||
|     Adjust websockets version to setup.py |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1869 <https://github.com/huge-success/sanic/pull/1869>`_ |  | ||||||
|     Wrap run()'s "protocol" type annotation in Optional[] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Improved Documentation |  | ||||||
| ********************** |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1846 <https://github.com/huge-success/sanic/pull/1846>`_ |  | ||||||
|     Update docs to clarify response middleware execution order |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1865 <https://github.com/huge-success/sanic/pull/1865>`_ |  | ||||||
|     Fixing rst format issue that was hiding documentation |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Version 20.6.0 |  | ||||||
| =============== |  | ||||||
|  |  | ||||||
| *Released, but unintentionally ommitting PR #1880, so was replaced by 20.6.1* |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Version 20.3.0 |  | ||||||
| =============== |  | ||||||
|  |  | ||||||
| Features |  | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1762 <https://github.com/huge-success/sanic/pull/1762>`_ |  | ||||||
|     Add ``srv.start_serving()`` and ``srv.serve_forever()`` to ``AsyncioServer`` |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1767 <https://github.com/huge-success/sanic/pull/1767>`_ |  | ||||||
|     Make Sanic usable on ``hypercorn -k trio myweb.app`` |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1768 <https://github.com/huge-success/sanic/pull/1768>`_ |  | ||||||
|     No tracebacks on normal errors and prettier error pages |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1769 <https://github.com/huge-success/sanic/pull/1769>`_ |  | ||||||
|     Code cleanup in file responses |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1793 <https://github.com/huge-success/sanic/pull/1793>`_ and |  | ||||||
|     `#1819 <https://github.com/huge-success/sanic/pull/1819>`_  |  | ||||||
|     Upgrade ``str.format()`` to f-strings |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1798 <https://github.com/huge-success/sanic/pull/1798>`_ |  | ||||||
|     Allow multiple workers on MacOS with Python 3.8 |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1820 <https://github.com/huge-success/sanic/pull/1820>`_ |  | ||||||
|     Do not set content-type and content-length headers in exceptions |  | ||||||
|  |  | ||||||
| Bugfixes |  | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1748 <https://github.com/huge-success/sanic/pull/1748>`_ |  | ||||||
|     Remove loop argument in ``asyncio.Event`` in Python 3.8 |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1764 <https://github.com/huge-success/sanic/pull/1764>`_ |  | ||||||
|     Allow route decorators to stack up again |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1789 <https://github.com/huge-success/sanic/pull/1789>`_ |  | ||||||
|     Fix tests using hosts yielding incorrect ``url_for`` |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1808 <https://github.com/huge-success/sanic/pull/1808>`_ |  | ||||||
|      Fix Ctrl+C and tests on Windows |  | ||||||
|  |  | ||||||
| Deprecations and Removals |  | ||||||
| ************************* |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1800 <https://github.com/huge-success/sanic/pull/1800>`_ |  | ||||||
|     Begin deprecation in way of first-class streaming, removal of ``body_init``, ``body_push``, and ``body_finish`` |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1801 <https://github.com/huge-success/sanic/pull/1801>`_ |  | ||||||
|     Complete deprecation from `#1666 <https://github.com/huge-success/sanic/pull/1666>`_ of dictionary context on ``request`` objects. |  | ||||||
|      |  | ||||||
|   * |  | ||||||
|     `#1807 <https://github.com/huge-success/sanic/pull/1807>`_ |  | ||||||
|     Remove server config args that can be read directly from app |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1818 <https://github.com/huge-success/sanic/pull/1818>`_ |  | ||||||
|     Complete deprecation of ``app.remove_route`` and ``request.raw_args`` |  | ||||||
|  |  | ||||||
| Dependencies |  | ||||||
| ************ |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1794 <https://github.com/huge-success/sanic/pull/1794>`_ |  | ||||||
|     Bump ``httpx`` to 0.11.1 |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1806 <https://github.com/huge-success/sanic/pull/1806>`_ |  | ||||||
|     Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation) |  | ||||||
|  |  | ||||||
| Developer infrastructure |  | ||||||
| ************************ |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1833 <https://github.com/huge-success/sanic/pull/1833>`_ |  | ||||||
|     Resolve broken documentation builds |  | ||||||
|  |  | ||||||
| Improved Documentation |  | ||||||
| ********************** |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1755 <https://github.com/huge-success/sanic/pull/1755>`_ |  | ||||||
|     Usage of ``response.empty()`` |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1778 <https://github.com/huge-success/sanic/pull/1778>`_ |  | ||||||
|     Update README |  | ||||||
|  |  | ||||||
|   *  |  | ||||||
|     `#1783 <https://github.com/huge-success/sanic/pull/1783>`_ |  | ||||||
|     Fix typo |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1784 <https://github.com/huge-success/sanic/pull/1784>`_ |  | ||||||
|     Corrected changelog for docs move of MD to RST (`#1691 <https://github.com/huge-success/sanic/pull/1691>`_) |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1803 <https://github.com/huge-success/sanic/pull/1803>`_ |  | ||||||
|     Update config docs to match DEFAULT_CONFIG |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1814 <https://github.com/huge-success/sanic/pull/1814>`_ |  | ||||||
|     Update getting_started.rst |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1821 <https://github.com/huge-success/sanic/pull/1821>`_ |  | ||||||
|     Update to deployment |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1822 <https://github.com/huge-success/sanic/pull/1822>`_ |  | ||||||
|     Update docs with changes done in 20.3 |  | ||||||
|  |  | ||||||
|   * |  | ||||||
|     `#1834 <https://github.com/huge-success/sanic/pull/1834>`_ |  | ||||||
|     Order of listeners |  | ||||||
|      |  | ||||||
|  |  | ||||||
| Version 19.12.0 | Version 19.12.0 | ||||||
| =============== | =============== | ||||||
|  |  | ||||||
| @@ -259,7 +24,7 @@ Bugfixes | |||||||
| Improved Documentation | Improved Documentation | ||||||
| ********************** | ********************** | ||||||
|  |  | ||||||
| - Move docs from MD to RST | - Move docs from RST to MD | ||||||
|  |  | ||||||
|   Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in |   Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in | ||||||
|   the future to update documentation. (`#1691 <https://github.com/huge-success/sanic/issues/1691>`__) |   the future to update documentation. (`#1691 <https://github.com/huge-success/sanic/issues/1691>`__) | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @@ -71,7 +71,7 @@ black: | |||||||
| 	black --config ./.black.toml sanic tests | 	black --config ./.black.toml sanic tests | ||||||
|  |  | ||||||
| fix-import: black | fix-import: black | ||||||
| 	isort -rc sanic tests | 	isort sanic tests | ||||||
|  |  | ||||||
|  |  | ||||||
| docs-clean: | docs-clean: | ||||||
|   | |||||||
| @@ -83,9 +83,6 @@ Installation | |||||||
|   If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to |   If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to | ||||||
|   use ``sanic`` with ``ujson`` dependency. |   use ``sanic`` with ``ujson`` dependency. | ||||||
|  |  | ||||||
| .. note:: |  | ||||||
|  |  | ||||||
|   Windows support is currently "experimental" and on a best-effort basis. Multiple workers are also not currently supported on Windows (see `Issue #1517 <https://github.com/huge-success/sanic/issues/1517>`_), but setting ``workers=1`` should launch the server successfully. |  | ||||||
|  |  | ||||||
| Hello World Example | Hello World Example | ||||||
| ------------------- | ------------------- | ||||||
|   | |||||||
| @@ -28,7 +28,6 @@ Guides | |||||||
|    sanic/debug_mode |    sanic/debug_mode | ||||||
|    sanic/testing |    sanic/testing | ||||||
|    sanic/deploying |    sanic/deploying | ||||||
|    sanic/nginx |  | ||||||
|    sanic/extensions |    sanic/extensions | ||||||
|    sanic/examples |    sanic/examples | ||||||
|    sanic/changelog |    sanic/changelog | ||||||
|   | |||||||
| @@ -28,15 +28,14 @@ using all these methods would look like the following. | |||||||
|     from sanic.views import HTTPMethodView |     from sanic.views import HTTPMethodView | ||||||
|     from sanic.response import text |     from sanic.response import text | ||||||
|  |  | ||||||
|     app = Sanic("class_views_example") |     app = Sanic('some_name') | ||||||
|  |  | ||||||
|     class SimpleView(HTTPMethodView): |     class SimpleView(HTTPMethodView): | ||||||
|  |  | ||||||
|       def get(self, request): |       def get(self, request): | ||||||
|           return text('I am get method') |           return text('I am get method') | ||||||
|  |  | ||||||
|       # You can also use async syntax |       def post(self, request): | ||||||
|       async def post(self, request): |  | ||||||
|           return text('I am post method') |           return text('I am post method') | ||||||
|  |  | ||||||
|       def put(self, request): |       def put(self, request): | ||||||
| @@ -50,6 +49,22 @@ using all these methods would look like the following. | |||||||
|  |  | ||||||
|     app.add_route(SimpleView.as_view(), '/') |     app.add_route(SimpleView.as_view(), '/') | ||||||
|  |  | ||||||
|  | You can also use `async` syntax. | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     from sanic import Sanic | ||||||
|  |     from sanic.views import HTTPMethodView | ||||||
|  |     from sanic.response import text | ||||||
|  |  | ||||||
|  |     app = Sanic('some_name') | ||||||
|  |  | ||||||
|  |     class SimpleAsyncView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |       async def get(self, request): | ||||||
|  |           return text('I am async get method') | ||||||
|  |  | ||||||
|  |     app.add_route(SimpleAsyncView.as_view(), '/') | ||||||
|  |  | ||||||
| URL parameters | URL parameters | ||||||
| -------------- | -------------- | ||||||
| @@ -139,7 +154,7 @@ lambda: | |||||||
|     from sanic.views import CompositionView |     from sanic.views import CompositionView | ||||||
|     from sanic.response import text |     from sanic.response import text | ||||||
|  |  | ||||||
|     app = Sanic("composition_example") |     app = Sanic(__name__) | ||||||
|  |  | ||||||
|     def get_handler(request): |     def get_handler(request): | ||||||
|         return text('I am a get method') |         return text('I am a get method') | ||||||
|   | |||||||
| @@ -39,13 +39,13 @@ Any variables defined with the `SANIC_` prefix will be applied to the sanic conf | |||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
|  |  | ||||||
|     app = Sanic(__name__, load_env='MYAPP_') |     app = Sanic(load_env='MYAPP_') | ||||||
|  |  | ||||||
| Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead: | Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead: | ||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
|  |  | ||||||
|     app = Sanic(__name__, load_env=False) |     app = Sanic(load_env=False) | ||||||
|  |  | ||||||
| From an Object | From an Object | ||||||
| ~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~ | ||||||
| @@ -115,25 +115,15 @@ Out of the box there are just a few predefined values which can be overwritten w | |||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ | +---------------------------+-------------------+-----------------------------------------------------------------------------+ | ||||||
| | KEEP_ALIVE_TIMEOUT        | 5                 | How long to hold a TCP connection open (sec)                                | | | KEEP_ALIVE_TIMEOUT        | 5                 | How long to hold a TCP connection open (sec)                                | | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ | +---------------------------+-------------------+-----------------------------------------------------------------------------+ | ||||||
| | WEBSOCKET_MAX_SIZE        | 2^20              | Maximum size for incoming messages (bytes)                                  | |  | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ |  | ||||||
| | WEBSOCKET_MAX_QUEUE       | 32                | Maximum length of the queue that holds incoming messages                    | |  | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ |  | ||||||
| | WEBSOCKET_READ_LIMIT      | 2^16              | High-water limit of the buffer for incoming bytes                           | |  | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ |  | ||||||
| | WEBSOCKET_WRITE_LIMIT     | 2^16              | High-water limit of the buffer for outgoing bytes                           | |  | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ |  | ||||||
| | GRACEFUL_SHUTDOWN_TIMEOUT | 15.0              | How long to wait to force close non-idle connection (sec)                   | | | GRACEFUL_SHUTDOWN_TIMEOUT | 15.0              | How long to wait to force close non-idle connection (sec)                   | | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ | +---------------------------+-------------------+-----------------------------------------------------------------------------+ | ||||||
| | ACCESS_LOG                | True              | Disable or enable access log                                                | | | ACCESS_LOG                | True              | Disable or enable access log                                                | | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ | +---------------------------+-------------------+-----------------------------------------------------------------------------+ | ||||||
| | FORWARDED_SECRET          | None              | Used to securely identify a specific proxy server (see below)               | | | PROXIES_COUNT             | -1                | The number of proxy servers in front of the app (e.g. nginx; see below)     | | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ |  | ||||||
| | PROXIES_COUNT             | None              | The number of proxy servers in front of the app (e.g. nginx; see below)     | |  | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ | +---------------------------+-------------------+-----------------------------------------------------------------------------+ | ||||||
| | FORWARDED_FOR_HEADER      | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip | | | FORWARDED_FOR_HEADER      | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip | | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ | +---------------------------+-------------------+-----------------------------------------------------------------------------+ | ||||||
| | REAL_IP_HEADER            |  None             | The name of "X-Real-IP" HTTP header that contains real client ip            | | | REAL_IP_HEADER            | "X-Real-IP"       | The name of "X-Real-IP" HTTP header that contains real client ip            | | ||||||
| +---------------------------+-------------------+-----------------------------------------------------------------------------+ | +---------------------------+-------------------+-----------------------------------------------------------------------------+ | ||||||
|  |  | ||||||
| The different Timeout variables: | The different Timeout variables: | ||||||
| @@ -238,7 +228,9 @@ Proxy config if using ... | |||||||
| * a proxy that supports `forwarded`: set `FORWARDED_SECRET` to the value that the proxy inserts in the header | * a proxy that supports `forwarded`: set `FORWARDED_SECRET` to the value that the proxy inserts in the header | ||||||
|     * Apache Traffic Server: `CONFIG proxy.config.http.insert_forwarded STRING for|proto|host|by=_secret` |     * Apache Traffic Server: `CONFIG proxy.config.http.insert_forwarded STRING for|proto|host|by=_secret` | ||||||
|     * NGHTTPX: `nghttpx --add-forwarded=for,proto,host,by --forwarded-for=ip --forwarded-by=_secret` |     * NGHTTPX: `nghttpx --add-forwarded=for,proto,host,by --forwarded-for=ip --forwarded-by=_secret` | ||||||
|     * NGINX: :ref:`nginx`. |     * NGINX: after `the official instructions <https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/>`_, add anywhere in your config: | ||||||
|  |  | ||||||
|  | ..        proxy_set_header Forwarded "$proxy_add_forwarded;by=\"_$server_name\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\";secret=_secret"; | ||||||
|  |  | ||||||
| * a custom header with client IP: set `REAL_IP_HEADER` to the name of that header | * a custom header with client IP: set `REAL_IP_HEADER` to the name of that header | ||||||
| * `x-forwarded-for`: set `PROXIES_COUNT` to `1` for a single proxy, or a greater number to allow Sanic to select the correct IP | * `x-forwarded-for`: set `PROXIES_COUNT` to `1` for a single proxy, or a greater number to allow Sanic to select the correct IP | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ and the Automatic Reloader will be activated. | |||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|     from sanic.response import json |     from sanic.response import json | ||||||
|  |  | ||||||
|     app = Sanic(__name__) |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.route('/') |     @app.route('/') | ||||||
|     async def hello_world(request): |     async def hello_world(request): | ||||||
| @@ -43,7 +43,7 @@ the ``auto_reload`` argument will activate or deactivate the Automatic Reloader. | |||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|     from sanic.response import json |     from sanic.response import json | ||||||
|  |  | ||||||
|     app = Sanic(__name__) |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.route('/') |     @app.route('/') | ||||||
|     async def hello_world(request): |     async def hello_world(request): | ||||||
|   | |||||||
| @@ -1,12 +1,9 @@ | |||||||
| Deploying | Deploying | ||||||
| ========= | ========= | ||||||
|  |  | ||||||
| Sanic has three serving options: the inbuilt webserver, | Deploying Sanic is very simple using one of three options: the inbuilt webserver, | ||||||
| an `ASGI webserver <https://asgi.readthedocs.io/en/latest/implementations.html>`_, or `gunicorn`. | an `ASGI webserver <https://asgi.readthedocs.io/en/latest/implementations.html>`_, or `gunicorn`. | ||||||
|  | It is also very common to place Sanic behind a reverse proxy, like `nginx`. | ||||||
| Sanic's own webserver is the fastest option, and it can be securely run on |  | ||||||
| the Internet. Still, it is also very common to place Sanic behind a reverse |  | ||||||
| proxy, as shown in :ref:`nginx`. |  | ||||||
|  |  | ||||||
| Running via Sanic webserver | Running via Sanic webserver | ||||||
| --------------------------- | --------------------------- | ||||||
| @@ -16,7 +13,6 @@ keyword arguments: | |||||||
|  |  | ||||||
| - `host` *(default `"127.0.0.1"`)*: Address to host the server on. | - `host` *(default `"127.0.0.1"`)*: Address to host the server on. | ||||||
| - `port` *(default `8000`)*: Port to host the server on. | - `port` *(default `8000`)*: Port to host the server on. | ||||||
| - `unix` *(default `None`)*: Unix socket name to host the server on (instead of TCP). |  | ||||||
| - `debug` *(default `False`)*: Enables debug output (slows server). | - `debug` *(default `False`)*: Enables debug output (slows server). | ||||||
| - `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s). | - `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s). | ||||||
| - `sock` *(default `None`)*: Socket for the server to accept connections from. | - `sock` *(default `None`)*: Socket for the server to accept connections from. | ||||||
| @@ -51,15 +47,7 @@ If you like using command line arguments, you can launch a Sanic webserver by | |||||||
| executing the module. For example, if you initialized Sanic as `app` in a file | executing the module. For example, if you initialized Sanic as `app` in a file | ||||||
| named `server.py`, you could run the server like so: | named `server.py`, you could run the server like so: | ||||||
|  |  | ||||||
| :: | .. python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4 | ||||||
|  |  | ||||||
|     sanic server.app --host=0.0.0.0 --port=1337 --workers=4 |  | ||||||
|  |  | ||||||
| It can also be called directly as a module. |  | ||||||
|  |  | ||||||
| :: |  | ||||||
|  |  | ||||||
|     python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4 |  | ||||||
|  |  | ||||||
| With this way of running sanic, it is not necessary to invoke `app.run` in your | With this way of running sanic, it is not necessary to invoke `app.run` in your | ||||||
| Python file. If you do, make sure you wrap it so that it only executes when | Python file. If you do, make sure you wrap it so that it only executes when | ||||||
| @@ -97,11 +85,7 @@ before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and sh | |||||||
| run consecutively and not actually around the server process beginning and ending (since that | run consecutively and not actually around the server process beginning and ending (since that | ||||||
| is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and | is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and | ||||||
| `before_server_stop`. | `before_server_stop`. | ||||||
|  | 3. ASGI mode is still in "beta" as of Sanic v19.6. | ||||||
| Sanic has experimental support for running on `Trio <https://trio.readthedocs.io/en/stable/>`_ with:: |  | ||||||
|  |  | ||||||
|     hypercorn -k trio myapp:app |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Running via Gunicorn | Running via Gunicorn | ||||||
| -------------------- | -------------------- | ||||||
| @@ -126,6 +110,28 @@ See the `Gunicorn Docs <http://docs.gunicorn.org/en/latest/settings.html#max-req | |||||||
| Other deployment considerations | Other deployment considerations | ||||||
| ------------------------------- | ------------------------------- | ||||||
|  |  | ||||||
|  | Running behind a reverse proxy | ||||||
|  | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|  |  | ||||||
|  | Sanic can be used with a reverse proxy (e.g. nginx). There's a simple example of nginx configuration: | ||||||
|  |  | ||||||
|  |  | ||||||
|  | :: | ||||||
|  |  | ||||||
|  |     server { | ||||||
|  |       listen 80; | ||||||
|  |       server_name example.org; | ||||||
|  |       location / { | ||||||
|  |         proxy_pass http://127.0.0.1:8000; | ||||||
|  |         proxy_set_header Host $host; | ||||||
|  |         proxy_set_header X-Real-IP $remote_addr; | ||||||
|  |         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | If you want to get real client ip, you should configure `X-Real-IP` and `X-Forwarded-For` HTTP headers and set `app.config.PROXIES_COUNT` to `1`; see the configuration page for more information. | ||||||
|  |  | ||||||
| Disable debug logging for performance | Disable debug logging for performance | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ A simple sanic application with a single ``async`` method with ``text`` and ``js | |||||||
| Simple App with ``Sanic Views`` | Simple App with ``Sanic Views`` | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|  |  | ||||||
| Showcasing the simple mechanism of using :class:`sanic.views.HTTPMethodView` as well as a way to extend the same | Showcasing the simple mechanism of using :class:`sanic.viewes.HTTPMethodView` as well as a way to extend the same | ||||||
| into providing a custom ``async`` behavior for ``view``. | into providing a custom ``async`` behavior for ``view``. | ||||||
|  |  | ||||||
| .. literalinclude:: ../../examples/simple_async_view.py | .. literalinclude:: ../../examples/simple_async_view.py | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ You can also add an exception handler as such: | |||||||
|     async def server_error_handler(request, exception): |     async def server_error_handler(request, exception): | ||||||
|         return text("Oops, server error", status=500) |         return text("Oops, server error", status=500) | ||||||
|  |  | ||||||
|     app = Sanic("error_handler_example") |     app = Sanic() | ||||||
|     app.error_handler.add(Exception, server_error_handler) |     app.error_handler.add(Exception, server_error_handler) | ||||||
|  |  | ||||||
| In some cases, you might want to add some more error handling | In some cases, you might want to add some more error handling | ||||||
| @@ -77,7 +77,7 @@ can subclass Sanic's default error handler as such: | |||||||
|             # You custom error handling logic... |             # You custom error handling logic... | ||||||
|             return super().default(request, exception) |             return super().default(request, exception) | ||||||
|  |  | ||||||
|     app = Sanic("custom_error_handler_example") |     app = Sanic() | ||||||
|     app.error_handler = CustomErrorHandler() |     app.error_handler = CustomErrorHandler() | ||||||
|  |  | ||||||
| Useful exceptions | Useful exceptions | ||||||
|   | |||||||
| @@ -8,19 +8,19 @@ syntax, so earlier versions of python won't work. | |||||||
| 1. Install Sanic | 1. Install Sanic | ||||||
| ---------------- | ---------------- | ||||||
|  |  | ||||||
| If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency. | >   If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency. | ||||||
|  |  | ||||||
| .. code-block:: bash | .. code-block:: bash | ||||||
|  |  | ||||||
|     pip3 install sanic |     pip3 install sanic | ||||||
|  |  | ||||||
| To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables | To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables | ||||||
| using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` ( with`X` = `UVLOOP`/`UJSON`) | using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`) | ||||||
| to true will stop that features installation. | to true will stop that features installation. | ||||||
|  |  | ||||||
| .. code-block:: bash | .. code-block:: bash | ||||||
|  |  | ||||||
|     SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install --no-binary :all: sanic |     SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic | ||||||
|  |  | ||||||
| You can also install Sanic from `conda-forge <https://anaconda.org/conda-forge/sanic>`_ | You can also install Sanic from `conda-forge <https://anaconda.org/conda-forge/sanic>`_ | ||||||
|  |  | ||||||
| @@ -37,7 +37,7 @@ You can also install Sanic from `conda-forge <https://anaconda.org/conda-forge/s | |||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|     from sanic.response import json |     from sanic.response import json | ||||||
|  |  | ||||||
|     app = Sanic("hello_example") |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     async def test(request): |     async def test(request): | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ Sanic aspires to be simple | |||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|     from sanic.response import json |     from sanic.response import json | ||||||
|  |  | ||||||
|     app = Sanic("App Name") |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     async def test(request): |     async def test(request): | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ A simple example using default settings would be like this: | |||||||
|    from sanic.log import logger |    from sanic.log import logger | ||||||
|    from sanic.response import text |    from sanic.response import text | ||||||
|  |  | ||||||
|    app = Sanic('logging_example') |    app = Sanic('test') | ||||||
|  |  | ||||||
|    @app.route('/') |    @app.route('/') | ||||||
|    async def test(request): |    async def test(request): | ||||||
| @@ -47,7 +47,7 @@ initialize ``Sanic`` app: | |||||||
|  |  | ||||||
| .. code:: python | .. code:: python | ||||||
|  |  | ||||||
|    app = Sanic('logging_example', log_config=LOGGING_CONFIG) |    app = Sanic('test', log_config=LOGGING_CONFIG) | ||||||
|  |  | ||||||
| And to close logging, simply assign access_log=False: | And to close logging, simply assign access_log=False: | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,8 +14,8 @@ There are two types of middleware: request and response. Both are declared | |||||||
| using the `@app.middleware` decorator, with the decorator's parameter being a | using the `@app.middleware` decorator, with the decorator's parameter being a | ||||||
| string representing its type: `'request'` or `'response'`. | string representing its type: `'request'` or `'response'`. | ||||||
|  |  | ||||||
| * Request middleware receives only the `request` as an argument and are executed in the order they were added. | * Request middleware receives only the `request` as argument. | ||||||
| * Response middleware receives both the `request` and `response` and are executed in *reverse* order. | * Response middleware receives both the `request` and `response`. | ||||||
|  |  | ||||||
| The simplest middleware doesn't modify the request or response at all: | The simplest middleware doesn't modify the request or response at all: | ||||||
|  |  | ||||||
| @@ -64,12 +64,12 @@ this. | |||||||
|  |  | ||||||
|     app.run(host="0.0.0.0", port=8000) |     app.run(host="0.0.0.0", port=8000) | ||||||
|  |  | ||||||
| The three middlewares are executed in the following order: | The three middlewares are executed in order: | ||||||
|  |  | ||||||
| 1. The first request middleware **add_key** adds a new key `foo` into request context. | 1. The first request middleware **add_key** adds a new key `foo` into request context. | ||||||
| 2. Request is routed to handler **index**, which gets the key from context and returns a text response. | 2. Request is routed to handler **index**, which gets the key from context and returns a text response. | ||||||
| 3. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks. | 3. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server* | ||||||
| 4. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server* | 4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks. | ||||||
|  |  | ||||||
| Responding early | Responding early | ||||||
| ---------------- | ---------------- | ||||||
| @@ -132,24 +132,13 @@ For example: | |||||||
|     async def close_db(app, loop): |     async def close_db(app, loop): | ||||||
|         await app.db.close() |         await app.db.close() | ||||||
|  |  | ||||||
| Note: |  | ||||||
|  |  | ||||||
| The listeners are deconstructed in the reverse order of being constructed. |  | ||||||
|  |  | ||||||
| For example: |  | ||||||
|  |  | ||||||
| If the first listener in before_server_start handler setups a database connection, |  | ||||||
| ones registered after it can rely on that connection being alive both when they are started |  | ||||||
| and stopped, because stopping is done in reverse order, and the database connection is |  | ||||||
| torn down last. |  | ||||||
|  |  | ||||||
| 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 | This may be useful if you define your listeners in another module besides | ||||||
| the one you instantiate your app in. | the one you instantiate your app in. | ||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
|  |  | ||||||
|     app = Sanic(__name__) |     app = Sanic() | ||||||
|  |  | ||||||
|     async def setup_db(app, loop): |     async def setup_db(app, loop): | ||||||
|         app.db = await db_setup() |         app.db = await db_setup() | ||||||
|   | |||||||
| @@ -1,222 +0,0 @@ | |||||||
|  |  | ||||||
| .. _nginx: |  | ||||||
|  |  | ||||||
| Nginx Deployment |  | ||||||
| ================ |  | ||||||
|  |  | ||||||
| Introduction |  | ||||||
| ~~~~~~~~~~~~ |  | ||||||
|  |  | ||||||
| Although Sanic can be run directly on Internet, it may be useful to use a proxy |  | ||||||
| server such as Nginx in front of it. This is particularly useful for running |  | ||||||
| multiple virtual hosts on the same IP, serving NodeJS or other services beside |  | ||||||
| a single Sanic app, and it also allows for efficient serving of static files. |  | ||||||
| SSL and HTTP/2 are also easily implemented on such proxy. |  | ||||||
|  |  | ||||||
| We are setting the Sanic app to serve only locally at `127.0.0.1:8000`, while the |  | ||||||
| Nginx installation is responsible for providing the service to public Internet |  | ||||||
| on domain `example.com`. Static files will be served from `/var/www/`. |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Proxied Sanic app |  | ||||||
| ~~~~~~~~~~~~~~~~~ |  | ||||||
|  |  | ||||||
| The app needs to be setup with a secret key used to identify a trusted proxy, |  | ||||||
| so that real client IP and other information can be identified. This protects |  | ||||||
| against anyone on the Internet sending fake headers to spoof their IP addresses |  | ||||||
| and other details. Choose any random string and configure it both on the app |  | ||||||
| and in Nginx config. |  | ||||||
|  |  | ||||||
| .. code-block:: python |  | ||||||
|  |  | ||||||
|     from sanic import Sanic |  | ||||||
|     from sanic.response import text |  | ||||||
|  |  | ||||||
|     app = Sanic("proxied_example") |  | ||||||
|     app.config.FORWARDED_SECRET = "YOUR SECRET" |  | ||||||
|  |  | ||||||
|     @app.get("/") |  | ||||||
|     def index(request): |  | ||||||
|         # This should display external (public) addresses: |  | ||||||
|         return text( |  | ||||||
|             f"{request.remote_addr} connected to {request.url_for('index')}\n" |  | ||||||
|             f"Forwarded: {request.forwarded}\n" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     if __name__ == '__main__': |  | ||||||
|         app.run(host='127.0.0.1', port=8000, workers=8, access_log=False) |  | ||||||
|  |  | ||||||
| Since this is going to be a system service, save your code to |  | ||||||
| `/srv/sanicexample/sanicexample.py`. |  | ||||||
|  |  | ||||||
| For testing, run your app in a terminal. |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Nginx configuration |  | ||||||
| ~~~~~~~~~~~~~~~~~~~ |  | ||||||
|  |  | ||||||
| Quite much configuration is required to allow fast transparent proxying, but |  | ||||||
| for the most part these don't need to be modified, so bear with me. |  | ||||||
|  |  | ||||||
| Upstream servers need to be configured in a separate `upstream` block to enable |  | ||||||
| HTTP keep-alive, which can drastically improve performance, so we use this |  | ||||||
| instead of directly providing an upstream address in `proxy_pass` directive. In |  | ||||||
| this example, the upstream section is named by `server_name`, i.e. the public |  | ||||||
| domain name, which then also gets passed to Sanic in the `Host` header. You may |  | ||||||
| change the naming as you see fit. Multiple servers may also be provided for |  | ||||||
| load balancing and failover. |  | ||||||
|  |  | ||||||
| Change the two occurrences of `example.com` to your true domain name, and |  | ||||||
| instead of `YOUR SECRET` use the secret you chose for your app. |  | ||||||
|  |  | ||||||
| :: |  | ||||||
|  |  | ||||||
|     upstream example.com { |  | ||||||
|       keepalive 100; |  | ||||||
|       server 127.0.0.1:8000; |  | ||||||
|       #server unix:/tmp/sanic.sock; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     server { |  | ||||||
|       server_name example.com; |  | ||||||
|       listen 443 ssl http2 default_server; |  | ||||||
|       listen [::]:443 ssl http2 default_server; |  | ||||||
|       # Serve static files if found, otherwise proxy to Sanic |  | ||||||
|       location / { |  | ||||||
|         root /var/www; |  | ||||||
|         try_files $uri @sanic; |  | ||||||
|       } |  | ||||||
|       location @sanic { |  | ||||||
|         proxy_pass http://$server_name; |  | ||||||
|         # Allow fast streaming HTTP/1.1 pipes (keep-alive, unbuffered) |  | ||||||
|         proxy_http_version 1.1; |  | ||||||
|         proxy_request_buffering off; |  | ||||||
|         proxy_buffering off; |  | ||||||
|         # Proxy forwarding (password configured in app.config.FORWARDED_SECRET) |  | ||||||
|         proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\""; |  | ||||||
|         # Allow websockets |  | ||||||
|         proxy_set_header connection "upgrade"; |  | ||||||
|         proxy_set_header upgrade $http_upgrade; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| To avoid cookie visibility issues and inconsistent addresses on search engines, |  | ||||||
| it is a good idea to redirect all visitors to one true domain, always using |  | ||||||
| HTTPS: |  | ||||||
|  |  | ||||||
| :: |  | ||||||
|  |  | ||||||
|     # Redirect all HTTP to HTTPS with no-WWW |  | ||||||
|     server { |  | ||||||
|       listen 80 default_server; |  | ||||||
|       listen [::]:80 default_server; |  | ||||||
|       server_name ~^(?:www\.)?(.*)$; |  | ||||||
|       return 301 https://$1$request_uri; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     # Redirect WWW to no-WWW |  | ||||||
|     server { |  | ||||||
|       listen 443 ssl http2; |  | ||||||
|       listen [::]:443 ssl http2; |  | ||||||
|       server_name ~^www\.(.*)$; |  | ||||||
|       return 301 $scheme://$1$request_uri; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| The above config sections may be placed in `/etc/nginx/sites-available/default` |  | ||||||
| or in other site configs (be sure to symlink them to `sites-enabled` if you |  | ||||||
| create new ones). |  | ||||||
|  |  | ||||||
| Make sure that your SSL certificates are configured in the main config, or |  | ||||||
| add the `ssl_certificate` and `ssl_certificate_key` directives to each |  | ||||||
| `server` section that listens on SSL. |  | ||||||
|  |  | ||||||
| Additionally, copy&paste all of this into `nginx/conf.d/forwarded.conf`: |  | ||||||
|  |  | ||||||
| :: |  | ||||||
|  |  | ||||||
|     # RFC 7239 Forwarded header for Nginx proxy_pass |  | ||||||
|  |  | ||||||
|     # Add within your server or location block: |  | ||||||
|     #    proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\""; |  | ||||||
|  |  | ||||||
|     # Configure your upstream web server to identify this proxy by that password |  | ||||||
|     # because otherwise anyone on the Internet could spoof these headers and fake |  | ||||||
|     # their real IP address and other information to your service. |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Provide the full proxy chain in $proxy_forwarded |  | ||||||
|     map $proxy_add_forwarded $proxy_forwarded { |  | ||||||
|       default "$proxy_add_forwarded;by=\"_$hostname\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\""; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     # The following mappings are based on |  | ||||||
|     # https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/ |  | ||||||
|  |  | ||||||
|     map $remote_addr $proxy_forwarded_elem { |  | ||||||
|       # IPv4 addresses can be sent as-is |  | ||||||
|       ~^[0-9.]+$          "for=$remote_addr"; |  | ||||||
|  |  | ||||||
|       # IPv6 addresses need to be bracketed and quoted |  | ||||||
|       ~^[0-9A-Fa-f:.]+$   "for=\"[$remote_addr]\""; |  | ||||||
|  |  | ||||||
|       # Unix domain socket names cannot be represented in RFC 7239 syntax |  | ||||||
|       default             "for=unknown"; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     map $http_forwarded $proxy_add_forwarded { |  | ||||||
|       # If the incoming Forwarded header is syntactically valid, append to it |  | ||||||
|       "~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem"; |  | ||||||
|  |  | ||||||
|       # Otherwise, replace it |  | ||||||
|       default "$proxy_forwarded_elem"; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| For installs that don't use `conf.d` and `sites-available`, all of the above |  | ||||||
| configs may also be placed inside the `http` section of the main `nginx.conf`. |  | ||||||
|  |  | ||||||
| Reload Nginx config after changes: |  | ||||||
|  |  | ||||||
| :: |  | ||||||
|  |  | ||||||
|     sudo nginx -s reload |  | ||||||
|  |  | ||||||
| Now you should be able to connect your app on `https://example.com/`. Any 404 |  | ||||||
| errors and such will be handled by Sanic's error pages, and whenever a static |  | ||||||
| file is present at a given path, it will be served by Nginx. |  | ||||||
|  |  | ||||||
|  |  | ||||||
| SSL certificates |  | ||||||
| ~~~~~~~~~~~~~~~~ |  | ||||||
|  |  | ||||||
| If you haven't already configured valid certificates on your server, now is a |  | ||||||
| good time to do so. Install `certbot` and `python3-certbot-nginx`, then run |  | ||||||
|  |  | ||||||
| :: |  | ||||||
|  |  | ||||||
|     certbot --nginx -d example.com -d www.example.com |  | ||||||
|  |  | ||||||
| `<https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/>`_ |  | ||||||
|  |  | ||||||
| Running as a service |  | ||||||
| ~~~~~~~~~~~~~~~~~~~~ |  | ||||||
|  |  | ||||||
| This part is for Linux distributions based on `systemd`. Create a unit file |  | ||||||
| `/etc/systemd/system/sanicexample.service`:: |  | ||||||
|  |  | ||||||
|     [Unit] |  | ||||||
|     Description=Sanic Example |  | ||||||
|  |  | ||||||
|     [Service] |  | ||||||
|     User=nobody |  | ||||||
|     WorkingDirectory=/srv/sanicexample |  | ||||||
|     ExecStart=/usr/bin/env python3 sanicexample.py |  | ||||||
|     Restart=always |  | ||||||
|  |  | ||||||
|     [Install] |  | ||||||
|     WantedBy=multi-user.target |  | ||||||
|  |  | ||||||
| Then reload service files, start your service and enable it on boot:: |  | ||||||
|  |  | ||||||
|     sudo systemctl daemon-reload |  | ||||||
|     sudo systemctl start sanicexample |  | ||||||
|     sudo systemctl enable sanicexample |  | ||||||
| @@ -56,6 +56,7 @@ The difference between Request.args and Request.query_args for the queryset `?ke | |||||||
|           "url": request.url, |           "url": request.url, | ||||||
|           "query_string": request.query_string, |           "query_string": request.query_string, | ||||||
|           "args": request.args, |           "args": request.args, | ||||||
|  |           "raw_args": request.raw_args, | ||||||
|           "query_args": request.query_args, |           "query_args": request.query_args, | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
| @@ -71,9 +72,12 @@ The difference between Request.args and Request.query_args for the queryset `?ke | |||||||
|         "url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3", |         "url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3", | ||||||
|         "query_string":"key1=value1&key2=value2&key1=value3", |         "query_string":"key1=value1&key2=value2&key1=value3", | ||||||
|         "args":{"key1":["value1","value3"],"key2":["value2"]}, |         "args":{"key1":["value1","value3"],"key2":["value2"]}, | ||||||
|  |         "raw_args":{"key1":"value1","key2":"value2"}, | ||||||
|         "query_args":[["key1","value1"],["key2","value2"],["key1","value3"]] |         "query_args":[["key1","value1"],["key2","value2"],["key1","value3"]] | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | - `raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions. | ||||||
|  |  | ||||||
| - `files` (dictionary of `File` objects) - List of files that have a name, body, and type | - `files` (dictionary of `File` objects) - List of files that have a name, body, and type | ||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
| @@ -224,7 +228,7 @@ The key difference when using this object is the distinction between the `get` a | |||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|     from sanic.response import json |     from sanic.response import json | ||||||
|  |  | ||||||
|     app = Sanic(__name__) |     app = Sanic(name="example") | ||||||
|  |  | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     def get_handler(request): |     def get_handler(request): | ||||||
| @@ -243,7 +247,7 @@ route will return "hello". | |||||||
|     from sanic.response import text |     from sanic.response import text | ||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|  |  | ||||||
|     app = Sanic(__name__) |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.get("/") |     @app.get("/") | ||||||
|     def hello(request): |     def hello(request): | ||||||
|   | |||||||
| @@ -107,19 +107,6 @@ Response without encoding the body | |||||||
|     def handle_request(request): |     def handle_request(request): | ||||||
|         return response.raw(b'raw data') |         return response.raw(b'raw data') | ||||||
|  |  | ||||||
| Empty |  | ||||||
| -------------- |  | ||||||
|  |  | ||||||
| For responding with an empty message as defined by `RFC 2616 <https://tools.ietf.org/search/rfc2616#section-7.2.1>`_ |  | ||||||
|  |  | ||||||
| .. code-block:: python |  | ||||||
|  |  | ||||||
|     from sanic import response |  | ||||||
|  |  | ||||||
|     @app.route('/empty') |  | ||||||
|     async def handle_request(request): |  | ||||||
|         return response.empty() |  | ||||||
|  |  | ||||||
| Modify headers or status | Modify headers or status | ||||||
| ------------------------ | ------------------------ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -406,7 +406,7 @@ Build URL for static files | |||||||
| ========================== | ========================== | ||||||
|  |  | ||||||
| Sanic supports using `url_for` method to build static file urls. In case if the static url | Sanic supports using `url_for` method to build static file urls. In case if the static url | ||||||
| is pointing to a directory, `filename` parameter to the `url_for` can be ignored. | is pointing to a directory, `filename` parameter to the `url_for` can be ignored.   q | ||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ IPv6 example: | |||||||
|     sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) |     sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) | ||||||
|     sock.bind(('::', 7777)) |     sock.bind(('::', 7777)) | ||||||
|  |  | ||||||
|     app = Sanic("ipv6_example") |     app = Sanic() | ||||||
|  |  | ||||||
|  |  | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
| @@ -46,7 +46,7 @@ UNIX socket example: | |||||||
|     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||||||
|     sock.bind(server_socket) |     sock.bind(server_socket) | ||||||
|  |  | ||||||
|     app = Sanic("unix_socket_example") |     app = Sanic() | ||||||
|  |  | ||||||
|  |  | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ Sanic allows you to get request data by stream, as below. When the request ends, | |||||||
|     from sanic.response import stream, text |     from sanic.response import stream, text | ||||||
|  |  | ||||||
|     bp = Blueprint('blueprint_request_stream') |     bp = Blueprint('blueprint_request_stream') | ||||||
|     app = Sanic(__name__) |     app = Sanic('request_stream') | ||||||
|  |  | ||||||
|  |  | ||||||
|     class SimpleView(HTTPMethodView): |     class SimpleView(HTTPMethodView): | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ To setup a WebSocket: | |||||||
|     from sanic.response import json |     from sanic.response import json | ||||||
|     from sanic.websocket import WebSocketProtocol |     from sanic.websocket import WebSocketProtocol | ||||||
|  |  | ||||||
|     app = Sanic("websocket_example") |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.websocket('/feed') |     @app.websocket('/feed') | ||||||
|     async def feed(request, ws): |     async def feed(request, ws): | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								environment.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								environment.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | name: py36 | ||||||
|  | dependencies: | ||||||
|  | - pip=18.1=py36_0 | ||||||
|  | - python=3.6=0 | ||||||
|  | - setuptools=40.4.3=py36_0 | ||||||
|  | - pip: | ||||||
|  |   - httptools>=0.0.10 | ||||||
|  |   - uvloop>=0.5.3 | ||||||
|  |   - ujson>=1.35 | ||||||
|  |   - aiofiles>=0.3.0 | ||||||
|  |   - websockets>=6.0,<7.0 | ||||||
|  |   - multidict>=4.0,<5.0 | ||||||
|  |   - sphinx==1.8.3 | ||||||
|  |   - sphinx_rtd_theme==0.4.2 | ||||||
|  |   - recommonmark==0.5.0 | ||||||
|  |   - httpx==0.9.3 | ||||||
|  |   - sphinxcontrib-asyncio>=0.2.0 | ||||||
|  |   - docutils==0.14 | ||||||
|  |   - pygments==2.3.1 | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| from sanic import Sanic, Blueprint |  | ||||||
| from sanic.response import text |  | ||||||
| ''' |  | ||||||
| Demonstrates that blueprint request middleware are executed in the order they  |  | ||||||
| are added. And blueprint response middleware are executed in _reverse_ order. |  | ||||||
| On a valid request, it should print "1 2 3 6 5 4" to terminal |  | ||||||
| ''' |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) |  | ||||||
|  |  | ||||||
| bp = Blueprint("bp_"+__name__) |  | ||||||
|  |  | ||||||
| @bp.middleware('request') |  | ||||||
| def request_middleware_1(request): |  | ||||||
|     print('1') |  | ||||||
|  |  | ||||||
| @bp.middleware('request') |  | ||||||
| def request_middleware_2(request): |  | ||||||
|     print('2') |  | ||||||
|  |  | ||||||
| @bp.middleware('request') |  | ||||||
| def request_middleware_3(request): |  | ||||||
|     print('3') |  | ||||||
|  |  | ||||||
| @bp.middleware('response') |  | ||||||
| def resp_middleware_4(request, response): |  | ||||||
|     print('4') |  | ||||||
|  |  | ||||||
| @bp.middleware('response') |  | ||||||
| def resp_middleware_5(request, response): |  | ||||||
|     print('5') |  | ||||||
|  |  | ||||||
| @bp.middleware('response') |  | ||||||
| def resp_middleware_6(request, response): |  | ||||||
|     print('6') |  | ||||||
|  |  | ||||||
| @bp.route('/') |  | ||||||
| def pop_handler(request): |  | ||||||
|     return text('hello world') |  | ||||||
|  |  | ||||||
| app.blueprint(bp, url_prefix='/bp') |  | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False) |  | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| from asyncio import sleep |  | ||||||
|  |  | ||||||
| from sanic import Sanic, response |  | ||||||
|  |  | ||||||
| app = Sanic(__name__, strict_slashes=True) |  | ||||||
|  |  | ||||||
| @app.get("/") |  | ||||||
| async def handler(request): |  | ||||||
|     return response.redirect("/sleep/3") |  | ||||||
|  |  | ||||||
| @app.get("/sleep/<t:number>") |  | ||||||
| async def handler2(request, t=0.3): |  | ||||||
|     await sleep(t) |  | ||||||
|     return response.text(f"Slept {t:.1f} seconds.\n") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': |  | ||||||
|     app.run(host="0.0.0.0", port=8000) |  | ||||||
| @@ -1,9 +1,2 @@ | |||||||
| version: 2 | conda: | ||||||
| python: |     file: environment.yml | ||||||
|    version: 3.8 |  | ||||||
|    install: |  | ||||||
|       - method: pip |  | ||||||
|         path: . |  | ||||||
|         extra_requirements: |  | ||||||
|             - docs |  | ||||||
|    system_packages: true |  | ||||||
| @@ -1,6 +1,3 @@ | |||||||
| import os |  | ||||||
| import sys |  | ||||||
|  |  | ||||||
| from argparse import ArgumentParser | from argparse import ArgumentParser | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
| from typing import Any, Dict, Optional | from typing import Any, Dict, Optional | ||||||
| @@ -9,11 +6,10 @@ from sanic.app import Sanic | |||||||
| from sanic.log import logger | from sanic.log import logger | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | if __name__ == "__main__": | ||||||
|     parser = ArgumentParser(prog="sanic") |     parser = ArgumentParser(prog="sanic") | ||||||
|     parser.add_argument("--host", dest="host", type=str, default="127.0.0.1") |     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("--port", dest="port", type=int, default=8000) | ||||||
|     parser.add_argument("--unix", dest="unix", type=str, default="") |  | ||||||
|     parser.add_argument( |     parser.add_argument( | ||||||
|         "--cert", dest="cert", type=str, help="location of certificate for SSL" |         "--cert", dest="cert", type=str, help="location of certificate for SSL" | ||||||
|     ) |     ) | ||||||
| @@ -26,22 +22,18 @@ def main(): | |||||||
|     args = parser.parse_args() |     args = parser.parse_args() | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         module_path = os.path.abspath(os.getcwd()) |  | ||||||
|         if module_path not in sys.path: |  | ||||||
|             sys.path.append(module_path) |  | ||||||
|  |  | ||||||
|         module_parts = args.module.split(".") |         module_parts = args.module.split(".") | ||||||
|         module_name = ".".join(module_parts[:-1]) |         module_name = ".".join(module_parts[:-1]) | ||||||
|         app_name = module_parts[-1] |         app_name = module_parts[-1] | ||||||
|  |  | ||||||
|         module = import_module(module_name) |         module = import_module(module_name) | ||||||
|         app = getattr(module, app_name, None) |         app = getattr(module, app_name, None) | ||||||
|         app_name = type(app).__name__ |  | ||||||
|  |  | ||||||
|         if not isinstance(app, Sanic): |         if not isinstance(app, Sanic): | ||||||
|             raise ValueError( |             raise ValueError( | ||||||
|                 f"Module is not a Sanic app, it is a {app_name}.  " |                 "Module is not a Sanic app, it is a {}.  " | ||||||
|                 f"Perhaps you meant {args.module}.app?" |                 "Perhaps you meant {}.app?".format( | ||||||
|  |                     type(app).__name__, args.module | ||||||
|  |                 ) | ||||||
|             ) |             ) | ||||||
|         if args.cert is not None or args.key is not None: |         if args.cert is not None or args.key is not None: | ||||||
|             ssl = { |             ssl = { | ||||||
| @@ -54,20 +46,15 @@ def main(): | |||||||
|         app.run( |         app.run( | ||||||
|             host=args.host, |             host=args.host, | ||||||
|             port=args.port, |             port=args.port, | ||||||
|             unix=args.unix, |  | ||||||
|             workers=args.workers, |             workers=args.workers, | ||||||
|             debug=args.debug, |             debug=args.debug, | ||||||
|             ssl=ssl, |             ssl=ssl, | ||||||
|         ) |         ) | ||||||
|     except ImportError as e: |     except ImportError as e: | ||||||
|         logger.error( |         logger.error( | ||||||
|             f"No module named {e.name} found.\n" |             "No module named {} found.\n" | ||||||
|             f"  Example File: project/sanic_server.py -> app\n" |             "  Example File: project/sanic_server.py -> app\n" | ||||||
|             f"  Example Module: project.sanic_server.app" |             "  Example Module: project.sanic_server.app".format(e.name) | ||||||
|         ) |         ) | ||||||
|     except ValueError: |     except ValueError: | ||||||
|         logger.exception("Failed to run app") |         logger.exception("Failed to run app") | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": |  | ||||||
|     main() |  | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "20.6.3" | __version__ = "19.12.4" | ||||||
|   | |||||||
							
								
								
									
										271
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										271
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -81,7 +81,6 @@ class Sanic: | |||||||
|         self.sock = None |         self.sock = None | ||||||
|         self.strict_slashes = strict_slashes |         self.strict_slashes = strict_slashes | ||||||
|         self.listeners = defaultdict(list) |         self.listeners = defaultdict(list) | ||||||
|         self.is_stopping = False |  | ||||||
|         self.is_running = False |         self.is_running = False | ||||||
|         self.is_request_stream = False |         self.is_request_stream = False | ||||||
|         self.websocket_enabled = False |         self.websocket_enabled = False | ||||||
| @@ -117,12 +116,24 @@ class Sanic: | |||||||
|         :param task: future, couroutine or awaitable |         :param task: future, couroutine or awaitable | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             loop = self.loop  # Will raise SanicError if loop is not started |             if callable(task): | ||||||
|             self._loop_add_task(task, self, loop) |                 try: | ||||||
|  |                     self.loop.create_task(task(self)) | ||||||
|  |                 except TypeError: | ||||||
|  |                     self.loop.create_task(task()) | ||||||
|  |             else: | ||||||
|  |                 self.loop.create_task(task) | ||||||
|         except SanicException: |         except SanicException: | ||||||
|             self.listener("before_server_start")( |  | ||||||
|                 partial(self._loop_add_task, task) |             @self.listener("before_server_start") | ||||||
|             ) |             def run(app, loop): | ||||||
|  |                 if callable(task): | ||||||
|  |                     try: | ||||||
|  |                         loop.create_task(task(self)) | ||||||
|  |                     except TypeError: | ||||||
|  |                         loop.create_task(task()) | ||||||
|  |                 else: | ||||||
|  |                     loop.create_task(task) | ||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def listener(self, event): |     def listener(self, event): | ||||||
| @@ -192,11 +203,9 @@ class Sanic: | |||||||
|             args = list(signature(handler).parameters.keys()) |             args = list(signature(handler).parameters.keys()) | ||||||
|  |  | ||||||
|             if not args: |             if not args: | ||||||
|                 handler_name = handler.__name__ |  | ||||||
|  |  | ||||||
|                 raise ValueError( |                 raise ValueError( | ||||||
|                     f"Required parameter `request` missing " |                     "Required parameter `request` missing " | ||||||
|                     f"in the {handler_name}() route?" |                     "in the {0}() route?".format(handler.__name__) | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|             if stream: |             if stream: | ||||||
| @@ -450,13 +459,7 @@ class Sanic: | |||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def websocket( |     def websocket( | ||||||
|         self, |         self, uri, host=None, strict_slashes=None, subprotocols=None, name=None | ||||||
|         uri, |  | ||||||
|         host=None, |  | ||||||
|         strict_slashes=None, |  | ||||||
|         subprotocols=None, |  | ||||||
|         version=None, |  | ||||||
|         name=None, |  | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Decorate a function to be registered as a websocket route |         Decorate a function to be registered as a websocket route | ||||||
| @@ -487,12 +490,47 @@ class Sanic: | |||||||
|                 routes, handler = handler |                 routes, handler = handler | ||||||
|             else: |             else: | ||||||
|                 routes = [] |                 routes = [] | ||||||
|             websocket_handler = partial( |  | ||||||
|                 self._websocket_handler, handler, subprotocols=subprotocols |             async def websocket_handler(request, *args, **kwargs): | ||||||
|  |                 request.app = self | ||||||
|  |                 if not getattr(handler, "__blueprintname__", False): | ||||||
|  |                     request.endpoint = handler.__name__ | ||||||
|  |                 else: | ||||||
|  |                     request.endpoint = ( | ||||||
|  |                         getattr(handler, "__blueprintname__", "") | ||||||
|  |                         + handler.__name__ | ||||||
|                     ) |                     ) | ||||||
|             websocket_handler.__name__ = ( |  | ||||||
|                 "websocket_handler_" + handler.__name__ |                     pass | ||||||
|  |  | ||||||
|  |                 if self.asgi: | ||||||
|  |                     ws = request.transport.get_websocket_connection() | ||||||
|  |                 else: | ||||||
|  |                     try: | ||||||
|  |                         protocol = request.transport.get_protocol() | ||||||
|  |                     except AttributeError: | ||||||
|  |                         # On Python3.5 the Transport classes in asyncio do not | ||||||
|  |                         # have a get_protocol() method as in uvloop | ||||||
|  |                         protocol = request.transport._protocol | ||||||
|  |                     protocol.app = self | ||||||
|  |  | ||||||
|  |                     ws = await protocol.websocket_handshake( | ||||||
|  |                         request, subprotocols | ||||||
|                     ) |                     ) | ||||||
|  |  | ||||||
|  |                 # schedule the application handler | ||||||
|  |                 # its future is kept in self.websocket_tasks in case it | ||||||
|  |                 # needs to be cancelled due to the server being stopped | ||||||
|  |                 fut = ensure_future(handler(request, ws, *args, **kwargs)) | ||||||
|  |                 self.websocket_tasks.add(fut) | ||||||
|  |                 try: | ||||||
|  |                     await fut | ||||||
|  |                 except (CancelledError, ConnectionClosed): | ||||||
|  |                     pass | ||||||
|  |                 finally: | ||||||
|  |                     self.websocket_tasks.remove(fut) | ||||||
|  |                 await ws.close() | ||||||
|  |  | ||||||
|             routes.extend( |             routes.extend( | ||||||
|                 self.router.add( |                 self.router.add( | ||||||
|                     uri=uri, |                     uri=uri, | ||||||
| @@ -500,7 +538,6 @@ class Sanic: | |||||||
|                     methods=frozenset({"GET"}), |                     methods=frozenset({"GET"}), | ||||||
|                     host=host, |                     host=host, | ||||||
|                     strict_slashes=strict_slashes, |                     strict_slashes=strict_slashes, | ||||||
|                     version=version, |  | ||||||
|                     name=name, |                     name=name, | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
| @@ -515,7 +552,6 @@ class Sanic: | |||||||
|         host=None, |         host=None, | ||||||
|         strict_slashes=None, |         strict_slashes=None, | ||||||
|         subprotocols=None, |         subprotocols=None, | ||||||
|         version=None, |  | ||||||
|         name=None, |         name=None, | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
| @@ -543,7 +579,6 @@ class Sanic: | |||||||
|             host=host, |             host=host, | ||||||
|             strict_slashes=strict_slashes, |             strict_slashes=strict_slashes, | ||||||
|             subprotocols=subprotocols, |             subprotocols=subprotocols, | ||||||
|             version=version, |  | ||||||
|             name=name, |             name=name, | ||||||
|         )(handler) |         )(handler) | ||||||
|  |  | ||||||
| @@ -556,10 +591,36 @@ class Sanic: | |||||||
|         if not self.websocket_enabled: |         if not self.websocket_enabled: | ||||||
|             # if the server is stopped, we want to cancel any ongoing |             # if the server is stopped, we want to cancel any ongoing | ||||||
|             # websocket tasks, to allow the server to exit promptly |             # websocket tasks, to allow the server to exit promptly | ||||||
|             self.listener("before_server_stop")(self._cancel_websocket_tasks) |             @self.listener("before_server_stop") | ||||||
|  |             def cancel_websocket_tasks(app, loop): | ||||||
|  |                 for task in self.websocket_tasks: | ||||||
|  |                     task.cancel() | ||||||
|  |  | ||||||
|         self.websocket_enabled = enable |         self.websocket_enabled = enable | ||||||
|  |  | ||||||
|  |     def remove_route(self, uri, clean_cache=True, host=None): | ||||||
|  |         """ | ||||||
|  |         This method provides the app user a mechanism by which an already | ||||||
|  |         existing route can be removed from the :class:`Sanic` object | ||||||
|  |  | ||||||
|  |         .. warning:: | ||||||
|  |             remove_route is deprecated in v19.06 and will be removed | ||||||
|  |             from future versions. | ||||||
|  |  | ||||||
|  |         :param uri: URL Path to be removed from the app | ||||||
|  |         :param clean_cache: Instruct sanic if it needs to clean up the LRU | ||||||
|  |             route cache | ||||||
|  |         :param host: IP address or FQDN specific to the host | ||||||
|  |         :return: None | ||||||
|  |         """ | ||||||
|  |         warnings.warn( | ||||||
|  |             "remove_route is deprecated and will be removed " | ||||||
|  |             "from future versions.", | ||||||
|  |             DeprecationWarning, | ||||||
|  |             stacklevel=2, | ||||||
|  |         ) | ||||||
|  |         self.router.remove(uri, clean_cache, host) | ||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def exception(self, *exceptions): |     def exception(self, *exceptions): | ||||||
|         """Decorate a function to be registered as a handler for exceptions |         """Decorate a function to be registered as a handler for exceptions | ||||||
| @@ -617,7 +678,7 @@ class Sanic: | |||||||
|                 if _rn not in self.named_response_middleware: |                 if _rn not in self.named_response_middleware: | ||||||
|                     self.named_response_middleware[_rn] = deque() |                     self.named_response_middleware[_rn] = deque() | ||||||
|                 if middleware not in self.named_response_middleware[_rn]: |                 if middleware not in self.named_response_middleware[_rn]: | ||||||
|                     self.named_response_middleware[_rn].appendleft(middleware) |                     self.named_response_middleware[_rn].append(middleware) | ||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def middleware(self, middleware_or_request): |     def middleware(self, middleware_or_request): | ||||||
| @@ -766,7 +827,7 @@ class Sanic: | |||||||
|         uri, route = self.router.find_route_by_view_name(view_name, **kw) |         uri, route = self.router.find_route_by_view_name(view_name, **kw) | ||||||
|         if not (uri and route): |         if not (uri and route): | ||||||
|             raise URLBuildError( |             raise URLBuildError( | ||||||
|                 f"Endpoint with name `{view_name}` was not found" |                 "Endpoint with name `{}` was not found".format(view_name) | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # If the route has host defined, split that off |         # If the route has host defined, split that off | ||||||
| @@ -788,7 +849,7 @@ class Sanic: | |||||||
|                 if filename.startswith("/"): |                 if filename.startswith("/"): | ||||||
|                     filename = filename[1:] |                     filename = filename[1:] | ||||||
|  |  | ||||||
|                 uri = f"{folder_}/{filename}" |                 uri = "{}/{}".format(folder_, filename) | ||||||
|  |  | ||||||
|         if uri != "/" and uri.endswith("/"): |         if uri != "/" and uri.endswith("/"): | ||||||
|             uri = uri[:-1] |             uri = uri[:-1] | ||||||
| @@ -824,7 +885,7 @@ class Sanic: | |||||||
|         for match in matched_params: |         for match in matched_params: | ||||||
|             name, _type, pattern = self.router.parse_parameter_string(match) |             name, _type, pattern = self.router.parse_parameter_string(match) | ||||||
|             # we only want to match against each individual parameter |             # we only want to match against each individual parameter | ||||||
|             specific_pattern = f"^{pattern}$" |             specific_pattern = "^{}$".format(pattern) | ||||||
|             supplied_param = None |             supplied_param = None | ||||||
|  |  | ||||||
|             if name in kwargs: |             if name in kwargs: | ||||||
| @@ -832,7 +893,9 @@ class Sanic: | |||||||
|                 del kwargs[name] |                 del kwargs[name] | ||||||
|             else: |             else: | ||||||
|                 raise URLBuildError( |                 raise URLBuildError( | ||||||
|                     f"Required parameter `{name}` was not passed to url_for" |                     "Required parameter `{}` was not passed to url_for".format( | ||||||
|  |                         name | ||||||
|  |                     ) | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|             supplied_param = str(supplied_param) |             supplied_param = str(supplied_param) | ||||||
| @@ -842,22 +905,23 @@ class Sanic: | |||||||
|  |  | ||||||
|             if not passes_pattern: |             if not passes_pattern: | ||||||
|                 if _type != str: |                 if _type != str: | ||||||
|                     type_name = _type.__name__ |  | ||||||
|  |  | ||||||
|                     msg = ( |                     msg = ( | ||||||
|                         f'Value "{supplied_param}" ' |                         'Value "{}" for parameter `{}` does not ' | ||||||
|                         f"for parameter `{name}` does not " |                         "match pattern for type `{}`: {}".format( | ||||||
|                         f"match pattern for type `{type_name}`: {pattern}" |                             supplied_param, name, _type.__name__, pattern | ||||||
|  |                         ) | ||||||
|                     ) |                     ) | ||||||
|                 else: |                 else: | ||||||
|                     msg = ( |                     msg = ( | ||||||
|                         f'Value "{supplied_param}" for parameter `{name}` ' |                         'Value "{}" for parameter `{}` ' | ||||||
|                         f"does not satisfy pattern {pattern}" |                         "does not satisfy pattern {}".format( | ||||||
|  |                             supplied_param, name, pattern | ||||||
|  |                         ) | ||||||
|                     ) |                     ) | ||||||
|                 raise URLBuildError(msg) |                 raise URLBuildError(msg) | ||||||
|  |  | ||||||
|             # replace the parameter in the URL with the supplied value |             # replace the parameter in the URL with the supplied value | ||||||
|             replacement_regex = f"(<{name}.*?>)" |             replacement_regex = "(<{}.*?>)".format(name) | ||||||
|  |  | ||||||
|             out = re.sub(replacement_regex, supplied_param, out) |             out = re.sub(replacement_regex, supplied_param, out) | ||||||
|  |  | ||||||
| @@ -958,8 +1022,9 @@ class Sanic: | |||||||
|                     ) |                     ) | ||||||
|                 elif self.debug: |                 elif self.debug: | ||||||
|                     response = HTTPResponse( |                     response = HTTPResponse( | ||||||
|                         f"Error while " |                         "Error while handling error: {}\nStack: {}".format( | ||||||
|                         f"handling error: {e}\nStack: {format_exc()}", |                             e, format_exc() | ||||||
|  |                         ), | ||||||
|                         status=500, |                         status=500, | ||||||
|                     ) |                     ) | ||||||
|                 else: |                 else: | ||||||
| @@ -1022,19 +1087,16 @@ class Sanic: | |||||||
|         self, |         self, | ||||||
|         host: Optional[str] = None, |         host: Optional[str] = None, | ||||||
|         port: Optional[int] = None, |         port: Optional[int] = None, | ||||||
|         *, |  | ||||||
|         debug: bool = False, |         debug: bool = False, | ||||||
|         auto_reload: Optional[bool] = None, |  | ||||||
|         ssl: Union[dict, SSLContext, None] = None, |         ssl: Union[dict, SSLContext, None] = None, | ||||||
|         sock: Optional[socket] = None, |         sock: Optional[socket] = None, | ||||||
|         workers: int = 1, |         workers: int = 1, | ||||||
|         protocol: Optional[Type[Protocol]] = None, |         protocol: Type[Protocol] = None, | ||||||
|         backlog: int = 100, |         backlog: int = 100, | ||||||
|         stop_event: Any = None, |         stop_event: Any = None, | ||||||
|         register_sys_signals: bool = True, |         register_sys_signals: bool = True, | ||||||
|         access_log: Optional[bool] = None, |         access_log: Optional[bool] = None, | ||||||
|         unix: Optional[str] = None, |         **kwargs: Any | ||||||
|         loop: None = None, |  | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Run the HTTP Server and listen until keyboard interrupt or term |         """Run the HTTP Server and listen until keyboard interrupt or term | ||||||
|         signal. On termination, drain connections before closing. |         signal. On termination, drain connections before closing. | ||||||
| @@ -1045,9 +1107,6 @@ class Sanic: | |||||||
|         :type port: int |         :type port: int | ||||||
|         :param debug: Enables debug output (slows server) |         :param debug: Enables debug output (slows server) | ||||||
|         :type debug: bool |         :type debug: bool | ||||||
|         :param auto_reload: Reload app whenever its source code is changed. |  | ||||||
|                             Enabled by default in debug mode. |  | ||||||
|         :type auto_relaod: bool |  | ||||||
|         :param ssl: SSLContext, or location of certificate and key |         :param ssl: SSLContext, or location of certificate and key | ||||||
|                     for SSL encryption of worker(s) |                     for SSL encryption of worker(s) | ||||||
|         :type ssl: SSLContext or dict |         :type ssl: SSLContext or dict | ||||||
| @@ -1067,11 +1126,9 @@ class Sanic: | |||||||
|         :type register_sys_signals: bool |         :type register_sys_signals: bool | ||||||
|         :param access_log: Enables writing access logs (slows server) |         :param access_log: Enables writing access logs (slows server) | ||||||
|         :type access_log: bool |         :type access_log: bool | ||||||
|         :param unix: Unix socket to listen on instead of TCP port |  | ||||||
|         :type unix: str |  | ||||||
|         :return: Nothing |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|         if loop is not None: |         if "loop" in kwargs: | ||||||
|             raise TypeError( |             raise TypeError( | ||||||
|                 "loop is not a valid argument. To use an existing loop, " |                 "loop is not a valid argument. To use an existing loop, " | ||||||
|                 "change to create_server().\nSee more: " |                 "change to create_server().\nSee more: " | ||||||
| @@ -1079,9 +1136,13 @@ class Sanic: | |||||||
|                 "#asynchronous-support" |                 "#asynchronous-support" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         if auto_reload or auto_reload is None and debug: |         # Default auto_reload to false | ||||||
|             if os.environ.get("SANIC_SERVER_RUNNING") != "true": |         auto_reload = False | ||||||
|                 return reloader_helpers.watchdog(1.0) |         # If debug is set, default it to true (unless on windows) | ||||||
|  |         if debug and os.name == "posix": | ||||||
|  |             auto_reload = True | ||||||
|  |         # Allow for overriding either of the defaults | ||||||
|  |         auto_reload = kwargs.get("auto_reload", auto_reload) | ||||||
|  |  | ||||||
|         if sock is None: |         if sock is None: | ||||||
|             host, port = host or "127.0.0.1", port or 8000 |             host, port = host or "127.0.0.1", port or 8000 | ||||||
| @@ -1107,7 +1168,6 @@ class Sanic: | |||||||
|             debug=debug, |             debug=debug, | ||||||
|             ssl=ssl, |             ssl=ssl, | ||||||
|             sock=sock, |             sock=sock, | ||||||
|             unix=unix, |  | ||||||
|             workers=workers, |             workers=workers, | ||||||
|             protocol=protocol, |             protocol=protocol, | ||||||
|             backlog=backlog, |             backlog=backlog, | ||||||
| @@ -1117,14 +1177,18 @@ class Sanic: | |||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             self.is_running = True |             self.is_running = True | ||||||
|             self.is_stopping = False |  | ||||||
|             if workers > 1 and os.name != "posix": |  | ||||||
|                 logger.warn( |  | ||||||
|                     f"Multiprocessing is currently not supported on {os.name}," |  | ||||||
|                     " using workers=1 instead" |  | ||||||
|                 ) |  | ||||||
|                 workers = 1 |  | ||||||
|             if workers == 1: |             if workers == 1: | ||||||
|  |                 if auto_reload and os.name != "posix": | ||||||
|  |                     # This condition must be removed after implementing | ||||||
|  |                     # auto reloader for other operating systems. | ||||||
|  |                     raise NotImplementedError | ||||||
|  |  | ||||||
|  |                 if ( | ||||||
|  |                     auto_reload | ||||||
|  |                     and os.environ.get("SANIC_SERVER_RUNNING") != "true" | ||||||
|  |                 ): | ||||||
|  |                     reloader_helpers.watchdog(2) | ||||||
|  |                 else: | ||||||
|                     serve(**server_settings) |                     serve(**server_settings) | ||||||
|             else: |             else: | ||||||
|                 serve_multiple(server_settings, workers) |                 serve_multiple(server_settings, workers) | ||||||
| @@ -1139,15 +1203,12 @@ class Sanic: | |||||||
|  |  | ||||||
|     def stop(self): |     def stop(self): | ||||||
|         """This kills the Sanic""" |         """This kills the Sanic""" | ||||||
|         if not self.is_stopping: |  | ||||||
|             self.is_stopping = True |  | ||||||
|         get_event_loop().stop() |         get_event_loop().stop() | ||||||
|  |  | ||||||
|     async def create_server( |     async def create_server( | ||||||
|         self, |         self, | ||||||
|         host: Optional[str] = None, |         host: Optional[str] = None, | ||||||
|         port: Optional[int] = None, |         port: Optional[int] = None, | ||||||
|         *, |  | ||||||
|         debug: bool = False, |         debug: bool = False, | ||||||
|         ssl: Union[dict, SSLContext, None] = None, |         ssl: Union[dict, SSLContext, None] = None, | ||||||
|         sock: Optional[socket] = None, |         sock: Optional[socket] = None, | ||||||
| @@ -1155,7 +1216,6 @@ class Sanic: | |||||||
|         backlog: int = 100, |         backlog: int = 100, | ||||||
|         stop_event: Any = None, |         stop_event: Any = None, | ||||||
|         access_log: Optional[bool] = None, |         access_log: Optional[bool] = None, | ||||||
|         unix: Optional[str] = None, |  | ||||||
|         return_asyncio_server=False, |         return_asyncio_server=False, | ||||||
|         asyncio_server_kwargs=None, |         asyncio_server_kwargs=None, | ||||||
|     ) -> Optional[AsyncioServer]: |     ) -> Optional[AsyncioServer]: | ||||||
| @@ -1225,7 +1285,6 @@ class Sanic: | |||||||
|             debug=debug, |             debug=debug, | ||||||
|             ssl=ssl, |             ssl=ssl, | ||||||
|             sock=sock, |             sock=sock, | ||||||
|             unix=unix, |  | ||||||
|             loop=get_event_loop(), |             loop=get_event_loop(), | ||||||
|             protocol=protocol, |             protocol=protocol, | ||||||
|             backlog=backlog, |             backlog=backlog, | ||||||
| @@ -1291,7 +1350,6 @@ class Sanic: | |||||||
|         debug=False, |         debug=False, | ||||||
|         ssl=None, |         ssl=None, | ||||||
|         sock=None, |         sock=None, | ||||||
|         unix=None, |  | ||||||
|         workers=1, |         workers=1, | ||||||
|         loop=None, |         loop=None, | ||||||
|         protocol=HttpProtocol, |         protocol=HttpProtocol, | ||||||
| @@ -1330,16 +1388,33 @@ class Sanic: | |||||||
|  |  | ||||||
|         server_settings = { |         server_settings = { | ||||||
|             "protocol": protocol, |             "protocol": protocol, | ||||||
|  |             "request_class": self.request_class, | ||||||
|  |             "is_request_stream": self.is_request_stream, | ||||||
|  |             "router": self.router, | ||||||
|             "host": host, |             "host": host, | ||||||
|             "port": port, |             "port": port, | ||||||
|             "sock": sock, |             "sock": sock, | ||||||
|             "unix": unix, |  | ||||||
|             "ssl": ssl, |             "ssl": ssl, | ||||||
|             "app": self, |             "app": self, | ||||||
|             "signal": Signal(), |             "signal": Signal(), | ||||||
|  |             "debug": debug, | ||||||
|  |             "request_handler": self.handle_request, | ||||||
|  |             "error_handler": self.error_handler, | ||||||
|  |             "request_timeout": self.config.REQUEST_TIMEOUT, | ||||||
|  |             "response_timeout": self.config.RESPONSE_TIMEOUT, | ||||||
|  |             "keep_alive_timeout": self.config.KEEP_ALIVE_TIMEOUT, | ||||||
|  |             "request_max_size": self.config.REQUEST_MAX_SIZE, | ||||||
|  |             "request_buffer_queue_size": self.config.REQUEST_BUFFER_QUEUE_SIZE, | ||||||
|  |             "keep_alive": self.config.KEEP_ALIVE, | ||||||
|             "loop": loop, |             "loop": loop, | ||||||
|             "register_sys_signals": register_sys_signals, |             "register_sys_signals": register_sys_signals, | ||||||
|             "backlog": backlog, |             "backlog": backlog, | ||||||
|  |             "access_log": self.config.ACCESS_LOG, | ||||||
|  |             "websocket_max_size": self.config.WEBSOCKET_MAX_SIZE, | ||||||
|  |             "websocket_max_queue": self.config.WEBSOCKET_MAX_QUEUE, | ||||||
|  |             "websocket_read_limit": self.config.WEBSOCKET_READ_LIMIT, | ||||||
|  |             "websocket_write_limit": self.config.WEBSOCKET_WRITE_LIMIT, | ||||||
|  |             "graceful_shutdown_timeout": self.config.GRACEFUL_SHUTDOWN_TIMEOUT, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         # -------------------------------------------- # |         # -------------------------------------------- # | ||||||
| @@ -1376,14 +1451,11 @@ class Sanic: | |||||||
|             server_settings["run_async"] = True |             server_settings["run_async"] = True | ||||||
|  |  | ||||||
|         # Serve |         # Serve | ||||||
|         if host and port: |         if host and port and os.environ.get("SANIC_SERVER_RUNNING") != "true": | ||||||
|             proto = "http" |             proto = "http" | ||||||
|             if ssl is not None: |             if ssl is not None: | ||||||
|                 proto = "https" |                 proto = "https" | ||||||
|             if unix: |             logger.info("Goin' Fast @ {}://{}:{}".format(proto, host, port)) | ||||||
|                 logger.info(f"Goin' Fast @ {unix} {proto}://...") |  | ||||||
|             else: |  | ||||||
|                 logger.info(f"Goin' Fast @ {proto}://{host}:{port}") |  | ||||||
|  |  | ||||||
|         return server_settings |         return server_settings | ||||||
|  |  | ||||||
| @@ -1391,55 +1463,6 @@ class Sanic: | |||||||
|         parts = [self.name, *parts] |         parts = [self.name, *parts] | ||||||
|         return ".".join(parts) |         return ".".join(parts) | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def _loop_add_task(cls, task, app, loop): |  | ||||||
|         if callable(task): |  | ||||||
|             try: |  | ||||||
|                 loop.create_task(task(app)) |  | ||||||
|             except TypeError: |  | ||||||
|                 loop.create_task(task()) |  | ||||||
|         else: |  | ||||||
|             loop.create_task(task) |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def _cancel_websocket_tasks(cls, app, loop): |  | ||||||
|         for task in app.websocket_tasks: |  | ||||||
|             task.cancel() |  | ||||||
|  |  | ||||||
|     async def _websocket_handler( |  | ||||||
|         self, handler, request, *args, subprotocols=None, **kwargs |  | ||||||
|     ): |  | ||||||
|         request.app = self |  | ||||||
|         if not getattr(handler, "__blueprintname__", False): |  | ||||||
|             request.endpoint = handler.__name__ |  | ||||||
|         else: |  | ||||||
|             request.endpoint = ( |  | ||||||
|                 getattr(handler, "__blueprintname__", "") + handler.__name__ |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|         if self.asgi: |  | ||||||
|             ws = request.transport.get_websocket_connection() |  | ||||||
|         else: |  | ||||||
|             protocol = request.transport.get_protocol() |  | ||||||
|             protocol.app = self |  | ||||||
|  |  | ||||||
|             ws = await protocol.websocket_handshake(request, subprotocols) |  | ||||||
|  |  | ||||||
|         # schedule the application handler |  | ||||||
|         # its future is kept in self.websocket_tasks in case it |  | ||||||
|         # needs to be cancelled due to the server being stopped |  | ||||||
|         fut = ensure_future(handler(request, ws, *args, **kwargs)) |  | ||||||
|         self.websocket_tasks.add(fut) |  | ||||||
|         try: |  | ||||||
|             await fut |  | ||||||
|         except (CancelledError, ConnectionClosed): |  | ||||||
|             pass |  | ||||||
|         finally: |  | ||||||
|             self.websocket_tasks.remove(fut) |  | ||||||
|         await ws.close() |  | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|     # ASGI |     # ASGI | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
| @@ -1451,3 +1474,5 @@ class Sanic: | |||||||
|         self.asgi = True |         self.asgi = True | ||||||
|         asgi_app = await ASGIApp.create(self, scope, receive, send) |         asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||||
|         await asgi_app() |         await asgi_app() | ||||||
|  |  | ||||||
|  |     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ from sanic.exceptions import InvalidUsage, ServerError | |||||||
| from sanic.log import logger | from sanic.log import logger | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import HTTPResponse, StreamingHTTPResponse | from sanic.response import HTTPResponse, StreamingHTTPResponse | ||||||
| from sanic.server import ConnInfo, StreamBuffer | from sanic.server import StreamBuffer | ||||||
| from sanic.websocket import WebSocketConnection | from sanic.websocket import WebSocketConnection | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -255,7 +255,6 @@ class ASGIApp: | |||||||
|                 instance.transport, |                 instance.transport, | ||||||
|                 sanic_app, |                 sanic_app, | ||||||
|             ) |             ) | ||||||
|             instance.request.conn_info = ConnInfo(instance.transport) |  | ||||||
|  |  | ||||||
|             if sanic_app.is_request_stream: |             if sanic_app.is_request_stream: | ||||||
|                 is_stream_handler = sanic_app.router.is_stream_handler( |                 is_stream_handler = sanic_app.router.is_stream_handler( | ||||||
| @@ -310,13 +309,17 @@ class ASGIApp: | |||||||
|         callback = None if self.ws else self.stream_callback |         callback = None if self.ws else self.stream_callback | ||||||
|         await handler(self.request, None, callback) |         await handler(self.request, None, callback) | ||||||
|  |  | ||||||
|  |     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||||
|  |  | ||||||
|     async def stream_callback(self, response: HTTPResponse) -> None: |     async def stream_callback(self, response: HTTPResponse) -> None: | ||||||
|         """ |         """ | ||||||
|         Write the response. |         Write the response. | ||||||
|         """ |         """ | ||||||
|         headers: List[Tuple[bytes, bytes]] = [] |         headers: List[Tuple[bytes, bytes]] = [] | ||||||
|         cookies: Dict[str, str] = {} |         cookies: Dict[str, str] = {} | ||||||
|  |         content_length: List[str] = [] | ||||||
|         try: |         try: | ||||||
|  |             content_length = response.headers.popall("content-length", []) | ||||||
|             cookies = { |             cookies = { | ||||||
|                 v.key: v |                 v.key: v | ||||||
|                 for _, v in list( |                 for _, v in list( | ||||||
| @@ -348,11 +351,23 @@ class ASGIApp: | |||||||
|                 if name not in (b"Set-Cookie",) |                 if name not in (b"Set-Cookie",) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|         if "content-length" not in response.headers and not isinstance( |         response.asgi = True | ||||||
|             response, StreamingHTTPResponse |         is_streaming = isinstance(response, StreamingHTTPResponse) | ||||||
|         ): |         if is_streaming and getattr(response, "chunked", False): | ||||||
|  |             # disable sanic chunking, this is done at the ASGI-server level | ||||||
|  |             setattr(response, "chunked", False) | ||||||
|  |             # content-length header is removed to signal to the ASGI-server | ||||||
|  |             # to use automatic-chunking if it supports it | ||||||
|  |         elif len(content_length) > 0: | ||||||
|             headers += [ |             headers += [ | ||||||
|                 (b"content-length", str(len(response.body)).encode("latin-1")) |                 (b"content-length", str(content_length[0]).encode("latin-1")) | ||||||
|  |             ] | ||||||
|  |         elif not is_streaming: | ||||||
|  |             headers += [ | ||||||
|  |                 ( | ||||||
|  |                     b"content-length", | ||||||
|  |                     str(len(getattr(response, "body", b""))).encode("latin-1"), | ||||||
|  |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|         if "content-type" not in response.headers: |         if "content-type" not in response.headers: | ||||||
|   | |||||||
| @@ -143,7 +143,7 @@ class Blueprint: | |||||||
|             if _routes: |             if _routes: | ||||||
|                 routes += _routes |                 routes += _routes | ||||||
|  |  | ||||||
|         route_names = [route.name for route in routes if route] |         route_names = [route.name for route in routes] | ||||||
|         # Middleware |         # Middleware | ||||||
|         for future in self.middlewares: |         for future in self.middlewares: | ||||||
|             if future.args or future.kwargs: |             if future.args or future.kwargs: | ||||||
| @@ -151,7 +151,7 @@ class Blueprint: | |||||||
|                     future.middleware, |                     future.middleware, | ||||||
|                     route_names, |                     route_names, | ||||||
|                     *future.args, |                     *future.args, | ||||||
|                     **future.kwargs, |                     **future.kwargs | ||||||
|                 ) |                 ) | ||||||
|             else: |             else: | ||||||
|                 app.register_named_middleware(future.middleware, route_names) |                 app.register_named_middleware(future.middleware, route_names) | ||||||
| @@ -283,13 +283,6 @@ class Blueprint: | |||||||
|             strict_slashes = self.strict_slashes |             strict_slashes = self.strict_slashes | ||||||
|  |  | ||||||
|         def decorator(handler): |         def decorator(handler): | ||||||
|             nonlocal uri |  | ||||||
|             nonlocal host |  | ||||||
|             nonlocal strict_slashes |  | ||||||
|             nonlocal version |  | ||||||
|             nonlocal name |  | ||||||
|  |  | ||||||
|             name = f"{self.name}.{name or handler.__name__}" |  | ||||||
|             route = FutureRoute( |             route = FutureRoute( | ||||||
|                 handler, uri, [], host, strict_slashes, False, version, name |                 handler, uri, [], host, strict_slashes, False, version, name | ||||||
|             ) |             ) | ||||||
| @@ -383,7 +376,7 @@ class Blueprint: | |||||||
|         """ |         """ | ||||||
|         name = kwargs.pop("name", "static") |         name = kwargs.pop("name", "static") | ||||||
|         if not name.startswith(self.name + "."): |         if not name.startswith(self.name + "."): | ||||||
|             name = f"{self.name}.{name}" |             name = "{}.{}".format(self.name, name) | ||||||
|         kwargs.update(name=name) |         kwargs.update(name=name) | ||||||
|  |  | ||||||
|         strict_slashes = kwargs.get("strict_slashes") |         strict_slashes = kwargs.get("strict_slashes") | ||||||
|   | |||||||
| @@ -1,52 +1,6 @@ | |||||||
| import asyncio |  | ||||||
| import signal |  | ||||||
|  |  | ||||||
| from sys import argv |  | ||||||
|  |  | ||||||
| from multidict import CIMultiDict  # type: ignore | from multidict import CIMultiDict  # type: ignore | ||||||
|  |  | ||||||
|  |  | ||||||
| class Header(CIMultiDict): | class Header(CIMultiDict): | ||||||
|     def get_all(self, key): |     def get_all(self, key): | ||||||
|         return self.getall(key, default=[]) |         return self.getall(key, default=[]) | ||||||
|  |  | ||||||
|  |  | ||||||
| use_trio = argv[0].endswith("hypercorn") and "trio" in argv |  | ||||||
|  |  | ||||||
| if use_trio: |  | ||||||
|     from trio import open_file as open_async, Path  # type: ignore |  | ||||||
|  |  | ||||||
|     def stat_async(path): |  | ||||||
|         return Path(path).stat() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| else: |  | ||||||
|     from aiofiles import open as aio_open  # type: ignore |  | ||||||
|     from aiofiles.os import stat as stat_async  # type: ignore  # noqa: F401 |  | ||||||
|  |  | ||||||
|     async def open_async(file, mode="r", **kwargs): |  | ||||||
|         return aio_open(file, mode, **kwargs) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def ctrlc_workaround_for_windows(app): |  | ||||||
|     async def stay_active(app): |  | ||||||
|         """Asyncio wakeups to allow receiving SIGINT in Python""" |  | ||||||
|         while not die: |  | ||||||
|             # If someone else stopped the app, just exit |  | ||||||
|             if app.is_stopping: |  | ||||||
|                 return |  | ||||||
|             # Windows Python blocks signal handlers while the event loop is |  | ||||||
|             # waiting for I/O. Frequent wakeups keep interrupts flowing. |  | ||||||
|             await asyncio.sleep(0.1) |  | ||||||
|         # Can't be called from signal handler, so call it from here |  | ||||||
|         app.stop() |  | ||||||
|  |  | ||||||
|     def ctrlc_handler(sig, frame): |  | ||||||
|         nonlocal die |  | ||||||
|         if die: |  | ||||||
|             raise KeyboardInterrupt("Non-graceful Ctrl+C") |  | ||||||
|         die = True |  | ||||||
|  |  | ||||||
|     die = False |  | ||||||
|     signal.signal(signal.SIGINT, ctrlc_handler) |  | ||||||
|     app.add_task(stay_active) |  | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ DEFAULT_CONFIG = { | |||||||
|     "RESPONSE_TIMEOUT": 60,  # 60 seconds |     "RESPONSE_TIMEOUT": 60,  # 60 seconds | ||||||
|     "KEEP_ALIVE": True, |     "KEEP_ALIVE": True, | ||||||
|     "KEEP_ALIVE_TIMEOUT": 5,  # 5 seconds |     "KEEP_ALIVE_TIMEOUT": 5,  # 5 seconds | ||||||
|     "WEBSOCKET_MAX_SIZE": 2 ** 20,  # 1 megabyte |     "WEBSOCKET_MAX_SIZE": 2 ** 20,  # 1 megabytes | ||||||
|     "WEBSOCKET_MAX_QUEUE": 32, |     "WEBSOCKET_MAX_QUEUE": 32, | ||||||
|     "WEBSOCKET_READ_LIMIT": 2 ** 16, |     "WEBSOCKET_READ_LIMIT": 2 ** 16, | ||||||
|     "WEBSOCKET_WRITE_LIMIT": 2 ** 16, |     "WEBSOCKET_WRITE_LIMIT": 2 ** 16, | ||||||
| @@ -51,7 +51,7 @@ class Config(dict): | |||||||
|         try: |         try: | ||||||
|             return self[attr] |             return self[attr] | ||||||
|         except KeyError as ke: |         except KeyError as ke: | ||||||
|             raise AttributeError(f"Config has no '{ke.args[0]}'") |             raise AttributeError("Config has no '{}'".format(ke.args[0])) | ||||||
|  |  | ||||||
|     def __setattr__(self, attr, value): |     def __setattr__(self, attr, value): | ||||||
|         self[attr] = value |         self[attr] = value | ||||||
|   | |||||||
| @@ -1,117 +0,0 @@ | |||||||
| import sys |  | ||||||
|  |  | ||||||
| from traceback import extract_tb |  | ||||||
|  |  | ||||||
| from sanic.exceptions import SanicException |  | ||||||
| from sanic.helpers import STATUS_CODES |  | ||||||
| from sanic.response import html |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # Here, There Be Dragons (custom HTML formatting to follow) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def escape(text): |  | ||||||
|     """Minimal HTML escaping, not for attribute values (unlike html.escape).""" |  | ||||||
|     return f"{text}".replace("&", "&").replace("<", "<") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def exception_response(request, exception, debug): |  | ||||||
|     status = 500 |  | ||||||
|     text = ( |  | ||||||
|         "The server encountered an internal error " |  | ||||||
|         "and cannot complete your request." |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     headers = {} |  | ||||||
|     if isinstance(exception, SanicException): |  | ||||||
|         text = f"{exception}" |  | ||||||
|         status = getattr(exception, "status_code", status) |  | ||||||
|         headers = getattr(exception, "headers", headers) |  | ||||||
|     elif debug: |  | ||||||
|         text = f"{exception}" |  | ||||||
|  |  | ||||||
|     status_text = STATUS_CODES.get(status, b"Error Occurred").decode() |  | ||||||
|     title = escape(f"{status} — {status_text}") |  | ||||||
|     text = escape(text) |  | ||||||
|  |  | ||||||
|     if debug and not getattr(exception, "quiet", False): |  | ||||||
|         return html( |  | ||||||
|             f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>" |  | ||||||
|             f"<style>{TRACEBACK_STYLE}</style>\n" |  | ||||||
|             f"<h1>⚠️ {title}</h1><p>{text}\n" |  | ||||||
|             f"{_render_traceback_html(request, exception)}", |  | ||||||
|             status=status, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     # Keeping it minimal with trailing newline for pretty curl/console output |  | ||||||
|     return html( |  | ||||||
|         f"<!DOCTYPE html><meta charset=UTF-8><title>{title}</title>" |  | ||||||
|         "<style>html { font-family: sans-serif }</style>\n" |  | ||||||
|         f"<h1>⚠️ {title}</h1><p>{text}\n", |  | ||||||
|         status=status, |  | ||||||
|         headers=headers, |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _render_exception(exception): |  | ||||||
|     frames = extract_tb(exception.__traceback__) |  | ||||||
|     frame_html = "".join(TRACEBACK_LINE_HTML.format(frame) for frame in frames) |  | ||||||
|     return TRACEBACK_WRAPPER_HTML.format( |  | ||||||
|         exc_name=escape(exception.__class__.__name__), |  | ||||||
|         exc_value=escape(exception), |  | ||||||
|         frame_html=frame_html, |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _render_traceback_html(request, exception): |  | ||||||
|     exc_type, exc_value, tb = sys.exc_info() |  | ||||||
|     exceptions = [] |  | ||||||
|     while exc_value: |  | ||||||
|         exceptions.append(_render_exception(exc_value)) |  | ||||||
|         exc_value = exc_value.__cause__ |  | ||||||
|  |  | ||||||
|     traceback_html = TRACEBACK_BORDER.join(reversed(exceptions)) |  | ||||||
|     appname = escape(request.app.name) |  | ||||||
|     name = escape(exception.__class__.__name__) |  | ||||||
|     value = escape(exception) |  | ||||||
|     path = escape(request.path) |  | ||||||
|     return ( |  | ||||||
|         f"<h2>Traceback of {appname} (most recent call last):</h2>" |  | ||||||
|         f"{traceback_html}" |  | ||||||
|         "<div class=summary><p>" |  | ||||||
|         f"<b>{name}: {value}</b> while handling path <code>{path}</code>" |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| TRACEBACK_STYLE = """ |  | ||||||
|     html { font-family: sans-serif } |  | ||||||
|     h2 { color: #888; } |  | ||||||
|     .tb-wrapper p { margin: 0 } |  | ||||||
|     .frame-border { margin: 1rem } |  | ||||||
|     .frame-line > * { padding: 0.3rem 0.6rem } |  | ||||||
|     .frame-line { margin-bottom: 0.3rem } |  | ||||||
|     .frame-code { font-size: 16px; padding-left: 4ch } |  | ||||||
|     .tb-wrapper { border: 1px solid #eee } |  | ||||||
|     .tb-header { background: #eee; padding: 0.3rem; font-weight: bold } |  | ||||||
|     .frame-descriptor { background: #e2eafb; font-size: 14px } |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| TRACEBACK_WRAPPER_HTML = ( |  | ||||||
|     "<div class=tb-header>{exc_name}: {exc_value}</div>" |  | ||||||
|     "<div class=tb-wrapper>{frame_html}</div>" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| TRACEBACK_BORDER = ( |  | ||||||
|     "<div class=frame-border>" |  | ||||||
|     "The above exception was the direct cause of the following exception:" |  | ||||||
|     "</div>" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| TRACEBACK_LINE_HTML = ( |  | ||||||
|     "<div class=frame-line>" |  | ||||||
|     "<p class=frame-descriptor>" |  | ||||||
|     "File {0.filename}, line <i>{0.lineno}</i>, " |  | ||||||
|     "in <code><b>{0.name}</b></code>" |  | ||||||
|     "<p class=frame-code><code>{0.line}</code>" |  | ||||||
|     "</div>" |  | ||||||
| ) |  | ||||||
| @@ -1,18 +1,133 @@ | |||||||
| from sanic.helpers import STATUS_CODES | from sanic.helpers import STATUS_CODES | ||||||
|  |  | ||||||
|  |  | ||||||
|  | TRACEBACK_STYLE = """ | ||||||
|  |     <style> | ||||||
|  |         body { | ||||||
|  |             padding: 20px; | ||||||
|  |             font-family: Arial, sans-serif; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         p { | ||||||
|  |             margin: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .summary { | ||||||
|  |             padding: 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         h1 { | ||||||
|  |             margin-bottom: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         h3 { | ||||||
|  |             margin-top: 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         h3 code { | ||||||
|  |             font-size: 24px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-line > * { | ||||||
|  |             padding: 5px 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-line { | ||||||
|  |             margin-bottom: 5px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-code { | ||||||
|  |             font-size: 16px; | ||||||
|  |             padding-left: 30px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .tb-wrapper { | ||||||
|  |             border: 1px solid #f3f3f3; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .tb-header { | ||||||
|  |             background-color: #f3f3f3; | ||||||
|  |             padding: 5px 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .tb-border { | ||||||
|  |             padding-top: 20px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-descriptor { | ||||||
|  |             background-color: #e2eafb; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-descriptor { | ||||||
|  |             font-size: 14px; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | TRACEBACK_WRAPPER_HTML = """ | ||||||
|  |     <html> | ||||||
|  |         <head> | ||||||
|  |             {style} | ||||||
|  |         </head> | ||||||
|  |         <body> | ||||||
|  |             {inner_html} | ||||||
|  |             <div class="summary"> | ||||||
|  |                 <p> | ||||||
|  |                 <b>{exc_name}: {exc_value}</b> | ||||||
|  |                     while handling path <code>{path}</code> | ||||||
|  |                 </p> | ||||||
|  |             </div> | ||||||
|  |         </body> | ||||||
|  |     </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 = """ | ||||||
|  |     <div class="tb-border"> | ||||||
|  |         <b><i> | ||||||
|  |             The above exception was the direct cause of the | ||||||
|  |             following exception: | ||||||
|  |         </i></b> | ||||||
|  |     </div> | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | TRACEBACK_LINE_HTML = """ | ||||||
|  |     <div class="frame-line"> | ||||||
|  |         <p class="frame-descriptor"> | ||||||
|  |             File {0.filename}, line <i>{0.lineno}</i>, | ||||||
|  |             in <code><b>{0.name}</b></code> | ||||||
|  |         </p> | ||||||
|  |         <p class="frame-code"><code>{0.line}</code></p> | ||||||
|  |     </div> | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | INTERNAL_SERVER_ERROR_HTML = """ | ||||||
|  |     <h1>Internal Server Error</h1> | ||||||
|  |     <p> | ||||||
|  |         The server encountered an internal error and cannot complete | ||||||
|  |         your request. | ||||||
|  |     </p> | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
| _sanic_exceptions = {} | _sanic_exceptions = {} | ||||||
|  |  | ||||||
|  |  | ||||||
| def add_status_code(code, quiet=None): | def add_status_code(code): | ||||||
|     """ |     """ | ||||||
|     Decorator used for adding exceptions to :class:`SanicException`. |     Decorator used for adding exceptions to :class:`SanicException`. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def class_decorator(cls): |     def class_decorator(cls): | ||||||
|         cls.status_code = code |         cls.status_code = code | ||||||
|         if quiet or quiet is None and code != 500: |  | ||||||
|             cls.quiet = True |  | ||||||
|         _sanic_exceptions[code] = cls |         _sanic_exceptions[code] = cls | ||||||
|         return cls |         return cls | ||||||
|  |  | ||||||
| @@ -20,16 +135,12 @@ def add_status_code(code, quiet=None): | |||||||
|  |  | ||||||
|  |  | ||||||
| class SanicException(Exception): | class SanicException(Exception): | ||||||
|     def __init__(self, message, status_code=None, quiet=None): |     def __init__(self, message, status_code=None): | ||||||
|         super().__init__(message) |         super().__init__(message) | ||||||
|  |  | ||||||
|         if status_code is not None: |         if status_code is not None: | ||||||
|             self.status_code = status_code |             self.status_code = status_code | ||||||
|  |  | ||||||
|         # quiet=None/False/True with None meaning choose by status |  | ||||||
|         if quiet or quiet is None and status_code not in (None, 500): |  | ||||||
|             self.quiet = True |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(404) | @add_status_code(404) | ||||||
| class NotFound(SanicException): | class NotFound(SanicException): | ||||||
| @@ -45,7 +156,10 @@ class InvalidUsage(SanicException): | |||||||
| class MethodNotSupported(SanicException): | class MethodNotSupported(SanicException): | ||||||
|     def __init__(self, message, method, allowed_methods): |     def __init__(self, message, method, allowed_methods): | ||||||
|         super().__init__(message) |         super().__init__(message) | ||||||
|         self.headers = {"Allow": ", ".join(allowed_methods)} |         self.headers = dict() | ||||||
|  |         self.headers["Allow"] = ", ".join(allowed_methods) | ||||||
|  |         if method in ["HEAD", "PATCH", "PUT", "DELETE"]: | ||||||
|  |             self.headers["Content-Length"] = 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(500) | @add_status_code(500) | ||||||
| @@ -98,7 +212,10 @@ class HeaderNotFound(InvalidUsage): | |||||||
| class ContentRangeError(SanicException): | class ContentRangeError(SanicException): | ||||||
|     def __init__(self, message, content_range): |     def __init__(self, message, content_range): | ||||||
|         super().__init__(message) |         super().__init__(message) | ||||||
|         self.headers = {"Content-Range": f"bytes */{content_range.total}"} |         self.headers = { | ||||||
|  |             "Content-Type": "text/plain", | ||||||
|  |             "Content-Range": "bytes */%s" % (content_range.total,), | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(417) | @add_status_code(417) | ||||||
| @@ -165,7 +282,7 @@ class Unauthorized(SanicException): | |||||||
|             challenge = ", ".join(values) |             challenge = ", ".join(values) | ||||||
|  |  | ||||||
|             self.headers = { |             self.headers = { | ||||||
|                 "WWW-Authenticate": f"{scheme} {challenge}".rstrip() |                 "WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip() | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,13 +1,21 @@ | |||||||
| from traceback import format_exc | import sys | ||||||
|  |  | ||||||
|  | from traceback import extract_tb, format_exc | ||||||
|  |  | ||||||
| from sanic.errorpages import exception_response |  | ||||||
| from sanic.exceptions import ( | from sanic.exceptions import ( | ||||||
|  |     INTERNAL_SERVER_ERROR_HTML, | ||||||
|  |     TRACEBACK_BORDER, | ||||||
|  |     TRACEBACK_LINE_HTML, | ||||||
|  |     TRACEBACK_STYLE, | ||||||
|  |     TRACEBACK_WRAPPER_HTML, | ||||||
|  |     TRACEBACK_WRAPPER_INNER_HTML, | ||||||
|     ContentRangeError, |     ContentRangeError, | ||||||
|     HeaderNotFound, |     HeaderNotFound, | ||||||
|     InvalidRangeType, |     InvalidRangeType, | ||||||
|  |     SanicException, | ||||||
| ) | ) | ||||||
| from sanic.log import logger | from sanic.log import logger | ||||||
| from sanic.response import text | from sanic.response import html, text | ||||||
|  |  | ||||||
|  |  | ||||||
| class ErrorHandler: | class ErrorHandler: | ||||||
| @@ -32,6 +40,35 @@ class ErrorHandler: | |||||||
|         self.cached_handlers = {} |         self.cached_handlers = {} | ||||||
|         self.debug = False |         self.debug = False | ||||||
|  |  | ||||||
|  |     def _render_exception(self, exception): | ||||||
|  |         frames = extract_tb(exception.__traceback__) | ||||||
|  |  | ||||||
|  |         frame_html = [] | ||||||
|  |         for frame in frames: | ||||||
|  |             frame_html.append(TRACEBACK_LINE_HTML.format(frame)) | ||||||
|  |  | ||||||
|  |         return TRACEBACK_WRAPPER_INNER_HTML.format( | ||||||
|  |             exc_name=exception.__class__.__name__, | ||||||
|  |             exc_value=exception, | ||||||
|  |             frame_html="".join(frame_html), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def _render_traceback_html(self, exception, request): | ||||||
|  |         exc_type, exc_value, tb = sys.exc_info() | ||||||
|  |         exceptions = [] | ||||||
|  |  | ||||||
|  |         while exc_value: | ||||||
|  |             exceptions.append(self._render_exception(exc_value)) | ||||||
|  |             exc_value = exc_value.__cause__ | ||||||
|  |  | ||||||
|  |         return TRACEBACK_WRAPPER_HTML.format( | ||||||
|  |             style=TRACEBACK_STYLE, | ||||||
|  |             exc_name=exception.__class__.__name__, | ||||||
|  |             exc_value=exception, | ||||||
|  |             inner_html=TRACEBACK_BORDER.join(reversed(exceptions)), | ||||||
|  |             path=request.path, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def add(self, exception, handler): |     def add(self, exception, handler): | ||||||
|         """ |         """ | ||||||
|         Add a new exception handler to an already existing handler object. |         Add a new exception handler to an already existing handler object. | ||||||
| @@ -129,17 +166,27 @@ class ErrorHandler: | |||||||
|             :class:`Exception` |             :class:`Exception` | ||||||
|         :return: |         :return: | ||||||
|         """ |         """ | ||||||
|         quiet = getattr(exception, "quiet", False) |         self.log(format_exc()) | ||||||
|         if quiet is False: |  | ||||||
|         try: |         try: | ||||||
|             url = repr(request.url) |             url = repr(request.url) | ||||||
|         except AttributeError: |         except AttributeError: | ||||||
|             url = "unknown" |             url = "unknown" | ||||||
|  |  | ||||||
|             self.log(format_exc()) |         response_message = "Exception occurred while handling uri: %s" | ||||||
|             logger.exception("Exception occurred while handling uri: %s", url) |         logger.exception(response_message, url) | ||||||
|  |  | ||||||
|         return exception_response(request, exception, self.debug) |         if issubclass(type(exception), SanicException): | ||||||
|  |             return text( | ||||||
|  |                 "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) | ||||||
|  |  | ||||||
|  |             return html(html_output, status=500) | ||||||
|  |         else: | ||||||
|  |             return html(INTERNAL_SERVER_ERROR_HTML, status=500) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ContentRangeHandler: | class ContentRangeHandler: | ||||||
|   | |||||||
| @@ -3,8 +3,6 @@ import re | |||||||
| from typing import Any, Dict, Iterable, List, Optional, Tuple, Union | from typing import Any, Dict, Iterable, List, Optional, Tuple, Union | ||||||
| from urllib.parse import unquote | from urllib.parse import unquote | ||||||
|  |  | ||||||
| from sanic.helpers import STATUS_CODES |  | ||||||
|  |  | ||||||
|  |  | ||||||
| HeaderIterable = Iterable[Tuple[str, Any]]  # Values convertible to str | HeaderIterable = Iterable[Tuple[str, Any]]  # Values convertible to str | ||||||
| Options = Dict[str, Union[int, str]]  # key=value fields in various headers | Options = Dict[str, Union[int, str]]  # key=value fields in various headers | ||||||
| @@ -182,19 +180,3 @@ def format_http1(headers: HeaderIterable) -> bytes: | |||||||
|     - Values are converted into strings if necessary. |     - Values are converted into strings if necessary. | ||||||
|     """ |     """ | ||||||
|     return "".join(f"{name}: {val}\r\n" for name, val in headers).encode() |     return "".join(f"{name}: {val}\r\n" for name, val in headers).encode() | ||||||
|  |  | ||||||
|  |  | ||||||
| def format_http1_response( |  | ||||||
|     status: int, headers: HeaderIterable, body=b"" |  | ||||||
| ) -> bytes: |  | ||||||
|     """Format a full HTTP/1.1 response. |  | ||||||
|  |  | ||||||
|     - If `body` is included, content-length must be specified in headers. |  | ||||||
|     """ |  | ||||||
|     headerbytes = format_http1(headers) |  | ||||||
|     return b"HTTP/1.1 %d %b\r\n%b\r\n%b" % ( |  | ||||||
|         status, |  | ||||||
|         STATUS_CODES.get(status, b"UNKNOWN"), |  | ||||||
|         headerbytes, |  | ||||||
|         body, |  | ||||||
|     ) |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import signal | |||||||
| import subprocess | import subprocess | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
|  | from multiprocessing import Process | ||||||
| from time import sleep | from time import sleep | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -34,26 +35,101 @@ def _iter_module_files(): | |||||||
|  |  | ||||||
| def _get_args_for_reloading(): | def _get_args_for_reloading(): | ||||||
|     """Returns the executable.""" |     """Returns the executable.""" | ||||||
|  |     rv = [sys.executable] | ||||||
|     main_module = sys.modules["__main__"] |     main_module = sys.modules["__main__"] | ||||||
|     mod_spec = getattr(main_module, "__spec__", None) |     mod_spec = getattr(main_module, "__spec__", None) | ||||||
|     if sys.argv[0] in ("", "-c"): |  | ||||||
|         raise RuntimeError( |  | ||||||
|             f"Autoreloader cannot work with argv[0]={sys.argv[0]!r}" |  | ||||||
|         ) |  | ||||||
|     if mod_spec: |     if mod_spec: | ||||||
|         # Parent exe was launched as a module rather than a script |         # Parent exe was launched as a module rather than a script | ||||||
|         return [sys.executable, "-m", mod_spec.name] + sys.argv[1:] |         rv.extend(["-m", mod_spec.name]) | ||||||
|     return [sys.executable] + sys.argv |         if len(sys.argv) > 1: | ||||||
|  |             rv.extend(sys.argv[1:]) | ||||||
|  |     else: | ||||||
|  |         rv.extend(sys.argv) | ||||||
|  |     return rv | ||||||
|  |  | ||||||
|  |  | ||||||
| def restart_with_reloader(): | def restart_with_reloader(): | ||||||
|     """Create a new process and a subprocess in it with the same arguments as |     """Create a new process and a subprocess in it with the same arguments as | ||||||
|     this one. |     this one. | ||||||
|     """ |     """ | ||||||
|     return subprocess.Popen( |     cwd = os.getcwd() | ||||||
|         _get_args_for_reloading(), |     args = _get_args_for_reloading() | ||||||
|         env={**os.environ, "SANIC_SERVER_RUNNING": "true"}, |     new_environ = os.environ.copy() | ||||||
|  |     new_environ["SANIC_SERVER_RUNNING"] = "true" | ||||||
|  |     cmd = " ".join(args) | ||||||
|  |     worker_process = Process( | ||||||
|  |         target=subprocess.call, | ||||||
|  |         args=(cmd,), | ||||||
|  |         kwargs={"cwd": cwd, "shell": True, "env": new_environ}, | ||||||
|     ) |     ) | ||||||
|  |     worker_process.start() | ||||||
|  |     return worker_process | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def kill_process_children_unix(pid): | ||||||
|  |     """Find and kill child processes of a process (maximum two level). | ||||||
|  |  | ||||||
|  |     :param pid: PID of parent process (process ID) | ||||||
|  |     :return: Nothing | ||||||
|  |     """ | ||||||
|  |     root_process_path = "/proc/{pid}/task/{pid}/children".format(pid=pid) | ||||||
|  |     if not os.path.isfile(root_process_path): | ||||||
|  |         return | ||||||
|  |     with open(root_process_path) as children_list_file: | ||||||
|  |         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, | ||||||
|  |         ) | ||||||
|  |         if not os.path.isfile(children_proc_path): | ||||||
|  |             continue | ||||||
|  |         with open(children_proc_path) as children_list_file_2: | ||||||
|  |             children_list_pid_2 = children_list_file_2.read().split() | ||||||
|  |         for _pid in children_list_pid_2: | ||||||
|  |             try: | ||||||
|  |                 os.kill(int(_pid), signal.SIGTERM) | ||||||
|  |             except ProcessLookupError: | ||||||
|  |                 continue | ||||||
|  |         try: | ||||||
|  |             os.kill(int(child_pid), signal.SIGTERM) | ||||||
|  |         except ProcessLookupError: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def kill_process_children_osx(pid): | ||||||
|  |     """Find and kill child processes of a process. | ||||||
|  |  | ||||||
|  |     :param pid: PID of parent process (process ID) | ||||||
|  |     :return: Nothing | ||||||
|  |     """ | ||||||
|  |     subprocess.run(["pkill", "-P", str(pid)]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def kill_process_children(pid): | ||||||
|  |     """Find and kill child processes of a process. | ||||||
|  |  | ||||||
|  |     :param pid: PID of parent process (process ID) | ||||||
|  |     :return: Nothing | ||||||
|  |     """ | ||||||
|  |     if sys.platform == "darwin": | ||||||
|  |         kill_process_children_osx(pid) | ||||||
|  |     elif sys.platform == "linux": | ||||||
|  |         kill_process_children_unix(pid) | ||||||
|  |     else: | ||||||
|  |         pass  # should signal error here | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def kill_program_completly(proc): | ||||||
|  |     """Kill worker and it's child processes and exit. | ||||||
|  |  | ||||||
|  |     :param proc: worker process (process ID) | ||||||
|  |     :return: Nothing | ||||||
|  |     """ | ||||||
|  |     kill_process_children(proc.pid) | ||||||
|  |     proc.terminate() | ||||||
|  |     os._exit(0) | ||||||
|  |  | ||||||
|  |  | ||||||
| def watchdog(sleep_interval): | def watchdog(sleep_interval): | ||||||
| @@ -62,21 +138,15 @@ def watchdog(sleep_interval): | |||||||
|     :param sleep_interval: interval in second. |     :param sleep_interval: interval in second. | ||||||
|     :return: Nothing |     :return: Nothing | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def interrupt_self(*args): |  | ||||||
|         raise KeyboardInterrupt |  | ||||||
|  |  | ||||||
|     mtimes = {} |     mtimes = {} | ||||||
|     signal.signal(signal.SIGTERM, interrupt_self) |  | ||||||
|     if os.name == "nt": |  | ||||||
|         signal.signal(signal.SIGBREAK, interrupt_self) |  | ||||||
|  |  | ||||||
|     worker_process = restart_with_reloader() |     worker_process = restart_with_reloader() | ||||||
|  |     signal.signal( | ||||||
|     try: |         signal.SIGTERM, lambda *args: kill_program_completly(worker_process) | ||||||
|  |     ) | ||||||
|  |     signal.signal( | ||||||
|  |         signal.SIGINT, lambda *args: kill_program_completly(worker_process) | ||||||
|  |     ) | ||||||
|     while True: |     while True: | ||||||
|             need_reload = False |  | ||||||
|  |  | ||||||
|         for filename in _iter_module_files(): |         for filename in _iter_module_files(): | ||||||
|             try: |             try: | ||||||
|                 mtime = os.stat(filename).st_mtime |                 mtime = os.stat(filename).st_mtime | ||||||
| @@ -86,18 +156,12 @@ def watchdog(sleep_interval): | |||||||
|             old_time = mtimes.get(filename) |             old_time = mtimes.get(filename) | ||||||
|             if old_time is None: |             if old_time is None: | ||||||
|                 mtimes[filename] = mtime |                 mtimes[filename] = mtime | ||||||
|  |                 continue | ||||||
|             elif mtime > old_time: |             elif mtime > old_time: | ||||||
|                     mtimes[filename] = mtime |                 kill_process_children(worker_process.pid) | ||||||
|                     need_reload = True |  | ||||||
|  |  | ||||||
|             if need_reload: |  | ||||||
|                 worker_process.terminate() |                 worker_process.terminate() | ||||||
|                 worker_process.wait() |  | ||||||
|                 worker_process = restart_with_reloader() |                 worker_process = restart_with_reloader() | ||||||
|  |                 mtimes[filename] = mtime | ||||||
|  |                 break | ||||||
|  |  | ||||||
|         sleep(sleep_interval) |         sleep(sleep_interval) | ||||||
|     except KeyboardInterrupt: |  | ||||||
|         pass |  | ||||||
|     finally: |  | ||||||
|         worker_process.terminate() |  | ||||||
|         worker_process.wait() |  | ||||||
|   | |||||||
							
								
								
									
										221
									
								
								sanic/request.py
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								sanic/request.py
									
									
									
									
									
								
							| @@ -1,5 +1,6 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import email.utils | import email.utils | ||||||
|  | import warnings | ||||||
|  |  | ||||||
| from collections import defaultdict, namedtuple | from collections import defaultdict, namedtuple | ||||||
| from http.cookies import SimpleCookie | from http.cookies import SimpleCookie | ||||||
| @@ -55,14 +56,6 @@ class StreamBuffer: | |||||||
|         self._queue.task_done() |         self._queue.task_done() | ||||||
|         return payload |         return payload | ||||||
|  |  | ||||||
|     async def __aiter__(self): |  | ||||||
|         """Support `async for data in request.stream`""" |  | ||||||
|         while True: |  | ||||||
|             data = await self.read() |  | ||||||
|             if not data: |  | ||||||
|                 break |  | ||||||
|             yield data |  | ||||||
|  |  | ||||||
|     async def put(self, payload): |     async def put(self, payload): | ||||||
|         await self._queue.put(payload) |         await self._queue.put(payload) | ||||||
|  |  | ||||||
| @@ -87,7 +80,6 @@ class Request: | |||||||
|         "_socket", |         "_socket", | ||||||
|         "app", |         "app", | ||||||
|         "body", |         "body", | ||||||
|         "conn_info", |  | ||||||
|         "ctx", |         "ctx", | ||||||
|         "endpoint", |         "endpoint", | ||||||
|         "headers", |         "headers", | ||||||
| @@ -118,7 +110,6 @@ class Request: | |||||||
|  |  | ||||||
|         # Init but do not inhale |         # Init but do not inhale | ||||||
|         self.body_init() |         self.body_init() | ||||||
|         self.conn_info = None |  | ||||||
|         self.ctx = SimpleNamespace() |         self.ctx = SimpleNamespace() | ||||||
|         self.parsed_forwarded = None |         self.parsed_forwarded = None | ||||||
|         self.parsed_json = None |         self.parsed_json = None | ||||||
| @@ -132,37 +123,44 @@ class Request: | |||||||
|         self.endpoint = None |         self.endpoint = None | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         class_name = self.__class__.__name__ |         return "<{0}: {1} {2}>".format( | ||||||
|         return f"<{class_name}: {self.method} {self.path}>" |             self.__class__.__name__, self.method, self.path | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def get(self, key, default=None): | ||||||
|  |         """.. deprecated:: 19.9 | ||||||
|  |         Custom context is now stored in `request.custom_context.yourkey`""" | ||||||
|  |         return self.ctx.__dict__.get(key, default) | ||||||
|  |  | ||||||
|  |     def __contains__(self, key): | ||||||
|  |         """.. deprecated:: 19.9 | ||||||
|  |         Custom context is now stored in `request.custom_context.yourkey`""" | ||||||
|  |         return key in self.ctx.__dict__ | ||||||
|  |  | ||||||
|  |     def __getitem__(self, key): | ||||||
|  |         """.. deprecated:: 19.9 | ||||||
|  |         Custom context is now stored in `request.custom_context.yourkey`""" | ||||||
|  |         return self.ctx.__dict__[key] | ||||||
|  |  | ||||||
|  |     def __delitem__(self, key): | ||||||
|  |         """.. deprecated:: 19.9 | ||||||
|  |         Custom context is now stored in `request.custom_context.yourkey`""" | ||||||
|  |         del self.ctx.__dict__[key] | ||||||
|  |  | ||||||
|  |     def __setitem__(self, key, value): | ||||||
|  |         """.. deprecated:: 19.9 | ||||||
|  |         Custom context is now stored in `request.custom_context.yourkey`""" | ||||||
|  |         setattr(self.ctx, key, value) | ||||||
|  |  | ||||||
|     def body_init(self): |     def body_init(self): | ||||||
|         """.. deprecated:: 20.3""" |  | ||||||
|         self.body = [] |         self.body = [] | ||||||
|  |  | ||||||
|     def body_push(self, data): |     def body_push(self, data): | ||||||
|         """.. deprecated:: 20.3""" |  | ||||||
|         self.body.append(data) |         self.body.append(data) | ||||||
|  |  | ||||||
|     def body_finish(self): |     def body_finish(self): | ||||||
|         """.. deprecated:: 20.3""" |  | ||||||
|         self.body = b"".join(self.body) |         self.body = b"".join(self.body) | ||||||
|  |  | ||||||
|     async def receive_body(self): |  | ||||||
|         """Receive request.body, if not already received. |  | ||||||
|  |  | ||||||
|         Streaming handlers may call this to receive the full body. |  | ||||||
|  |  | ||||||
|         This is added as a compatibility shim in Sanic 20.3 because future |  | ||||||
|         versions of Sanic will make all requests streaming and will use this |  | ||||||
|         function instead of the non-async body_init/push/finish functions. |  | ||||||
|  |  | ||||||
|         Please make an issue if your code depends on the old functionality and |  | ||||||
|         cannot be upgraded to the new API. |  | ||||||
|         """ |  | ||||||
|         if not self.stream: |  | ||||||
|             return |  | ||||||
|         self.body = b"".join([data async for data in self.stream]) |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def json(self): |     def json(self): | ||||||
|         if self.parsed_json is None: |         if self.parsed_json is None: | ||||||
| @@ -284,6 +282,18 @@ class Request: | |||||||
|  |  | ||||||
|     args = property(get_args) |     args = property(get_args) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def raw_args(self) -> dict: | ||||||
|  |         if self.app.debug:  # pragma: no cover | ||||||
|  |             warnings.simplefilter("default") | ||||||
|  |         warnings.warn( | ||||||
|  |             "Use of raw_args will be deprecated in " | ||||||
|  |             "the future versions. Please use args or query_args " | ||||||
|  |             "properties instead", | ||||||
|  |             DeprecationWarning, | ||||||
|  |         ) | ||||||
|  |         return {k: v[0] for k, v in self.args.items()} | ||||||
|  |  | ||||||
|     def get_query_args( |     def get_query_args( | ||||||
|         self, |         self, | ||||||
|         keep_blank_values: bool = False, |         keep_blank_values: bool = False, | ||||||
| @@ -351,55 +361,56 @@ class Request: | |||||||
|                 self._cookies = {} |                 self._cookies = {} | ||||||
|         return self._cookies |         return self._cookies | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def content_type(self): |  | ||||||
|         return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def match_info(self): |  | ||||||
|         """return matched info after resolving route""" |  | ||||||
|         return self.app.router.get(self)[2] |  | ||||||
|  |  | ||||||
|     # Transport properties (obtained from local interface only) |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def ip(self): |     def ip(self): | ||||||
|         """ |         """ | ||||||
|         :return: peer ip of the socket |         :return: peer ip of the socket | ||||||
|         """ |         """ | ||||||
|         return self.conn_info.client if self.conn_info else "" |         if not hasattr(self, "_socket"): | ||||||
|  |             self._get_address() | ||||||
|  |         return self._ip | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def port(self): |     def port(self): | ||||||
|         """ |         """ | ||||||
|         :return: peer port of the socket |         :return: peer port of the socket | ||||||
|         """ |         """ | ||||||
|         return self.conn_info.client_port if self.conn_info else 0 |         if not hasattr(self, "_socket"): | ||||||
|  |             self._get_address() | ||||||
|  |         return self._port | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def socket(self): |     def socket(self): | ||||||
|         return self.conn_info.peername if self.conn_info else (None, None) |         if not hasattr(self, "_socket"): | ||||||
|  |             self._get_address() | ||||||
|  |         return self._socket | ||||||
|  |  | ||||||
|  |     def _get_address(self): | ||||||
|  |         self._socket = self.transport.get_extra_info("peername") or ( | ||||||
|  |             None, | ||||||
|  |             None, | ||||||
|  |         ) | ||||||
|  |         self._ip = self._socket[0] | ||||||
|  |         self._port = self._socket[1] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def path(self) -> str: |     def server_name(self): | ||||||
|         """Path of the local HTTP request.""" |         """ | ||||||
|         return self._parsed_url.path.decode("utf-8") |         Attempt to get the server's external hostname in this order: | ||||||
|  |         `config.SERVER_NAME`, proxied or direct Host headers | ||||||
|  |         :func:`Request.host` | ||||||
|  |  | ||||||
|     # Proxy properties (using SERVER_NAME/forwarded/request/transport info) |         :return: the server name without port number | ||||||
|  |         :rtype: str | ||||||
|  |         """ | ||||||
|  |         server_name = self.app.config.get("SERVER_NAME") | ||||||
|  |         if server_name: | ||||||
|  |             host = server_name.split("//", 1)[-1].split("/", 1)[0] | ||||||
|  |             return parse_host(host)[0] | ||||||
|  |         return parse_host(self.host)[0] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def forwarded(self): |     def forwarded(self): | ||||||
|         """ |  | ||||||
|         Active proxy information obtained from request headers, as specified in |  | ||||||
|         Sanic configuration. |  | ||||||
|  |  | ||||||
|         Field names by, for, proto, host, port and path are normalized. |  | ||||||
|         - for and by IPv6 addresses are bracketed |  | ||||||
|         - port (int) is only set by port headers, not from host. |  | ||||||
|         - path is url-unencoded |  | ||||||
|  |  | ||||||
|         Additional values may be available from new style Forwarded headers. |  | ||||||
|         """ |  | ||||||
|         if self.parsed_forwarded is None: |         if self.parsed_forwarded is None: | ||||||
|             self.parsed_forwarded = ( |             self.parsed_forwarded = ( | ||||||
|                 parse_forwarded(self.headers, self.app.config) |                 parse_forwarded(self.headers, self.app.config) | ||||||
| @@ -409,30 +420,50 @@ class Request: | |||||||
|         return self.parsed_forwarded |         return self.parsed_forwarded | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def remote_addr(self) -> str: |     def server_port(self): | ||||||
|         """ |         """ | ||||||
|         Client IP address, if available. |         Attempt to get the server's external port number in this order: | ||||||
|         1. proxied remote address `self.forwarded['for']` |         `config.SERVER_NAME`, proxied or direct Host headers | ||||||
|         2. local remote address `self.ip` |         :func:`Request.host`, | ||||||
|         :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string |         actual port used by the transport layer socket. | ||||||
|  |         :return: server port | ||||||
|  |         :rtype: int | ||||||
|  |         """ | ||||||
|  |         if self.forwarded: | ||||||
|  |             return self.forwarded.get("port") or ( | ||||||
|  |                 80 if self.scheme in ("http", "ws") else 443 | ||||||
|  |             ) | ||||||
|  |         return ( | ||||||
|  |             parse_host(self.host)[1] | ||||||
|  |             or self.transport.get_extra_info("sockname")[1] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def remote_addr(self): | ||||||
|  |         """Attempt to return the original client ip based on `forwarded`, | ||||||
|  |         `x-forwarded-for` or `x-real-ip`. If HTTP headers are unavailable or | ||||||
|  |         untrusted, returns an empty string. | ||||||
|  |  | ||||||
|  |         :return: original client ip. | ||||||
|         """ |         """ | ||||||
|         if not hasattr(self, "_remote_addr"): |         if not hasattr(self, "_remote_addr"): | ||||||
|             self._remote_addr = self.forwarded.get("for", "")  # or self.ip |             self._remote_addr = self.forwarded.get("for", "") | ||||||
|         return self._remote_addr |         return self._remote_addr | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def scheme(self) -> str: |     def scheme(self): | ||||||
|         """ |         """ | ||||||
|         Determine request scheme. |         Attempt to get the request scheme. | ||||||
|         1. `config.SERVER_NAME` if in full URL format |         Seeking the value in this order: | ||||||
|         2. proxied proto/scheme |         `forwarded` header, `x-forwarded-proto` header, | ||||||
|         3. local connection protocol |         `x-scheme` header, the sanic app itself. | ||||||
|  |  | ||||||
|         :return: http|https|ws|wss or arbitrary value given by the headers. |         :return: http|https|ws|wss or arbitrary value given by the headers. | ||||||
|  |         :rtype: str | ||||||
|         """ |         """ | ||||||
|         if "//" in self.app.config.get("SERVER_NAME", ""): |         forwarded_proto = self.forwarded.get("proto") | ||||||
|             return self.app.config.SERVER_NAME.split("//")[0] |         if forwarded_proto: | ||||||
|         if "proto" in self.forwarded: |             return forwarded_proto | ||||||
|             return self.forwarded["proto"] |  | ||||||
|  |  | ||||||
|         if ( |         if ( | ||||||
|             self.app.websocket_enabled |             self.app.websocket_enabled | ||||||
| @@ -448,41 +479,25 @@ class Request: | |||||||
|         return scheme |         return scheme | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def host(self) -> str: |     def host(self): | ||||||
|         """ |         """ | ||||||
|         The currently effective server 'host' (hostname or hostname:port). |         :return: proxied or direct Host header. Hostname and port number may be | ||||||
|         1. `config.SERVER_NAME` overrides any client headers |           separated by sanic.headers.parse_host(request.host). | ||||||
|         2. proxied host of original request |  | ||||||
|         3. request host header |  | ||||||
|         hostname and port may be separated by |  | ||||||
|         `sanic.headers.parse_host(request.host)`. |  | ||||||
|         :return: the first matching host found, or empty string |  | ||||||
|         """ |         """ | ||||||
|         server_name = self.app.config.get("SERVER_NAME") |         return self.forwarded.get("host", self.headers.get("Host", "")) | ||||||
|         if server_name: |  | ||||||
|             return server_name.split("//", 1)[-1].split("/", 1)[0] |  | ||||||
|         return self.forwarded.get("host") or self.headers.get("host", "") |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def server_name(self) -> str: |     def content_type(self): | ||||||
|         """The hostname the client connected to, by `request.host`.""" |         return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE) | ||||||
|         return parse_host(self.host)[0] or "" |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def server_port(self) -> int: |     def match_info(self): | ||||||
|         """ |         """return matched info after resolving route""" | ||||||
|         The port the client connected to, by forwarded `port` or |         return self.app.router.get(self)[2] | ||||||
|         `request.host`. |  | ||||||
|  |  | ||||||
|         Default port is returned as 80 and 443 based on `request.scheme`. |  | ||||||
|         """ |  | ||||||
|         port = self.forwarded.get("port") or parse_host(self.host)[1] |  | ||||||
|         return port or (80 if self.scheme in ("http", "ws") else 443) |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def server_path(self) -> str: |     def path(self): | ||||||
|         """Full path of current URL. Uses proxied or local path.""" |         return self._parsed_url.path.decode("utf-8") | ||||||
|         return self.forwarded.get("path") or self.path |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def query_string(self): |     def query_string(self): | ||||||
| @@ -523,7 +538,7 @@ class Request: | |||||||
|         ): |         ): | ||||||
|             netloc = host |             netloc = host | ||||||
|         else: |         else: | ||||||
|             netloc = f"{host}:{port}" |             netloc = "{}:{}".format(host, port) | ||||||
|  |  | ||||||
|         return self.app.url_for( |         return self.app.url_for( | ||||||
|             view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs |             view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| import warnings |  | ||||||
|  |  | ||||||
| from functools import partial | from functools import partial | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path | ||||||
| from urllib.parse import quote_plus | from urllib.parse import quote_plus | ||||||
|  |  | ||||||
| from sanic.compat import Header, open_async | from aiofiles import open as open_async  # type: ignore | ||||||
|  |  | ||||||
|  | from sanic.compat import Header | ||||||
| from sanic.cookies import CookieJar | from sanic.cookies import CookieJar | ||||||
| from sanic.headers import format_http1, format_http1_response | from sanic.headers import format_http1 | ||||||
| from sanic.helpers import has_message_body, remove_entity_headers | from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers | ||||||
|  |  | ||||||
|  |  | ||||||
| try: | try: | ||||||
| @@ -22,8 +22,16 @@ except ImportError: | |||||||
|  |  | ||||||
|  |  | ||||||
| class BaseHTTPResponse: | class BaseHTTPResponse: | ||||||
|  |     def __init__(self): | ||||||
|  |         self.asgi = False | ||||||
|  |  | ||||||
|     def _encode_body(self, data): |     def _encode_body(self, data): | ||||||
|         return data.encode() if hasattr(data, "encode") else data |         try: | ||||||
|  |             # Try to encode it regularly | ||||||
|  |             return data.encode() | ||||||
|  |         except AttributeError: | ||||||
|  |             # Convert it to a str if you can't | ||||||
|  |             return str(data).encode() | ||||||
|  |  | ||||||
|     def _parse_headers(self): |     def _parse_headers(self): | ||||||
|         return format_http1(self.headers.items()) |         return format_http1(self.headers.items()) | ||||||
| @@ -34,32 +42,6 @@ class BaseHTTPResponse: | |||||||
|             self._cookies = CookieJar(self.headers) |             self._cookies = CookieJar(self.headers) | ||||||
|         return self._cookies |         return self._cookies | ||||||
|  |  | ||||||
|     def get_headers( |  | ||||||
|         self, |  | ||||||
|         version="1.1", |  | ||||||
|         keep_alive=False, |  | ||||||
|         keep_alive_timeout=None, |  | ||||||
|         body=b"", |  | ||||||
|     ): |  | ||||||
|         """.. deprecated:: 20.3: |  | ||||||
|            This function is not public API and will be removed.""" |  | ||||||
|  |  | ||||||
|         # self.headers get priority over content_type |  | ||||||
|         if self.content_type and "Content-Type" not in self.headers: |  | ||||||
|             self.headers["Content-Type"] = self.content_type |  | ||||||
|  |  | ||||||
|         if keep_alive: |  | ||||||
|             self.headers["Connection"] = "keep-alive" |  | ||||||
|             if keep_alive_timeout is not None: |  | ||||||
|                 self.headers["Keep-Alive"] = keep_alive_timeout |  | ||||||
|         else: |  | ||||||
|             self.headers["Connection"] = "close" |  | ||||||
|  |  | ||||||
|         if self.status in (304, 412): |  | ||||||
|             self.headers = remove_entity_headers(self.headers) |  | ||||||
|  |  | ||||||
|         return format_http1_response(self.status, self.headers.items(), body) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class StreamingHTTPResponse(BaseHTTPResponse): | class StreamingHTTPResponse(BaseHTTPResponse): | ||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
| @@ -77,9 +59,11 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|         streaming_fn, |         streaming_fn, | ||||||
|         status=200, |         status=200, | ||||||
|         headers=None, |         headers=None, | ||||||
|         content_type="text/plain; charset=utf-8", |         content_type="text/plain", | ||||||
|         chunked=True, |         chunked=True, | ||||||
|     ): |     ): | ||||||
|  |         super().__init__() | ||||||
|  |  | ||||||
|         self.content_type = content_type |         self.content_type = content_type | ||||||
|         self.streaming_fn = streaming_fn |         self.streaming_fn = streaming_fn | ||||||
|         self.status = status |         self.status = status | ||||||
| @@ -91,10 +75,13 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|     async def write(self, data): |     async def write(self, data): | ||||||
|         """Writes a chunk of data to the streaming response. |         """Writes a chunk of data to the streaming response. | ||||||
|  |  | ||||||
|         :param data: str or bytes-ish data to be written. |         :param data: bytes-ish data to be written. | ||||||
|         """ |         """ | ||||||
|  |         if type(data) != bytes: | ||||||
|             data = self._encode_body(data) |             data = self._encode_body(data) | ||||||
|  |  | ||||||
|  |         # `chunked` will always be False in ASGI-mode, even if the underlying | ||||||
|  |         # ASGI Transport implements Chunked transport. That does it itself. | ||||||
|         if self.chunked: |         if self.chunked: | ||||||
|             await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) |             await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) | ||||||
|         else: |         else: | ||||||
| @@ -114,6 +101,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|             keep_alive=keep_alive, |             keep_alive=keep_alive, | ||||||
|             keep_alive_timeout=keep_alive_timeout, |             keep_alive_timeout=keep_alive_timeout, | ||||||
|         ) |         ) | ||||||
|  |         if not getattr(self, "asgi", False): | ||||||
|             await self.protocol.push_data(headers) |             await self.protocol.push_data(headers) | ||||||
|             await self.protocol.drain() |             await self.protocol.drain() | ||||||
|         await self.streaming_fn(self) |         await self.streaming_fn(self) | ||||||
| @@ -125,11 +113,33 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|     def get_headers( |     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"" | ||||||
|  |         if keep_alive and keep_alive_timeout is not None: | ||||||
|  |             timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout | ||||||
|  |  | ||||||
|         if self.chunked and version == "1.1": |         if self.chunked and version == "1.1": | ||||||
|             self.headers["Transfer-Encoding"] = "chunked" |             self.headers["Transfer-Encoding"] = "chunked" | ||||||
|             self.headers.pop("Content-Length", None) |             self.headers.pop("Content-Length", None) | ||||||
|  |         self.headers["Content-Type"] = self.headers.get( | ||||||
|  |             "Content-Type", self.content_type | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         return super().get_headers(version, keep_alive, keep_alive_timeout) |         headers = self._parse_headers() | ||||||
|  |  | ||||||
|  |         if self.status == 200: | ||||||
|  |             status = b"OK" | ||||||
|  |         else: | ||||||
|  |             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, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class HTTPResponse(BaseHTTPResponse): | class HTTPResponse(BaseHTTPResponse): | ||||||
| @@ -143,19 +153,26 @@ class HTTPResponse(BaseHTTPResponse): | |||||||
|         content_type=None, |         content_type=None, | ||||||
|         body_bytes=b"", |         body_bytes=b"", | ||||||
|     ): |     ): | ||||||
|  |         super().__init__() | ||||||
|  |  | ||||||
|         self.content_type = content_type |         self.content_type = content_type | ||||||
|         self.body = body_bytes if body is None else self._encode_body(body) |  | ||||||
|  |         if body is not None: | ||||||
|  |             self.body = self._encode_body(body) | ||||||
|  |         else: | ||||||
|  |             self.body = body_bytes | ||||||
|  |  | ||||||
|         self.status = status |         self.status = status | ||||||
|         self.headers = Header(headers or {}) |         self.headers = Header(headers or {}) | ||||||
|         self._cookies = None |         self._cookies = None | ||||||
|  |  | ||||||
|         if body_bytes: |  | ||||||
|             warnings.warn( |  | ||||||
|                 "Parameter `body_bytes` is deprecated, use `body` instead", |  | ||||||
|                 DeprecationWarning, |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): |     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): | ||||||
|  |         # 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"" | ||||||
|  |         if keep_alive and keep_alive_timeout is not None: | ||||||
|  |             timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout | ||||||
|  |  | ||||||
|         body = b"" |         body = b"" | ||||||
|         if has_message_body(self.status): |         if has_message_body(self.status): | ||||||
|             body = self.body |             body = self.body | ||||||
| @@ -163,7 +180,31 @@ class HTTPResponse(BaseHTTPResponse): | |||||||
|                 "Content-Length", len(self.body) |                 "Content-Length", len(self.body) | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         return self.get_headers(version, keep_alive, keep_alive_timeout, body) |         # self.headers get priority over content_type | ||||||
|  |         if self.content_type and "Content-Type" not in self.headers: | ||||||
|  |             self.headers["Content-Type"] = self.content_type | ||||||
|  |  | ||||||
|  |         if self.status in (304, 412): | ||||||
|  |             self.headers = remove_entity_headers(self.headers) | ||||||
|  |  | ||||||
|  |         headers = self._parse_headers() | ||||||
|  |  | ||||||
|  |         if self.status == 200: | ||||||
|  |             status = b"OK" | ||||||
|  |         else: | ||||||
|  |             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, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def cookies(self): |     def cookies(self): | ||||||
| @@ -179,7 +220,7 @@ def empty(status=204, headers=None): | |||||||
|     :param status Response code. |     :param status Response code. | ||||||
|     :param headers Custom Headers. |     :param headers Custom Headers. | ||||||
|     """ |     """ | ||||||
|     return HTTPResponse(body=b"", status=status, headers=headers) |     return HTTPResponse(body_bytes=b"", status=status, headers=headers) | ||||||
|  |  | ||||||
|  |  | ||||||
| def json( | def json( | ||||||
| @@ -188,7 +229,7 @@ def json( | |||||||
|     headers=None, |     headers=None, | ||||||
|     content_type="application/json", |     content_type="application/json", | ||||||
|     dumps=json_dumps, |     dumps=json_dumps, | ||||||
|     **kwargs, |     **kwargs | ||||||
| ): | ): | ||||||
|     """ |     """ | ||||||
|     Returns response object with body in json format. |     Returns response object with body in json format. | ||||||
| @@ -217,21 +258,6 @@ def text( | |||||||
|     :param headers: Custom Headers. |     :param headers: Custom Headers. | ||||||
|     :param content_type: the content type (string) of the response |     :param content_type: the content type (string) of the response | ||||||
|     """ |     """ | ||||||
|     if not isinstance(body, str): |  | ||||||
|         warnings.warn( |  | ||||||
|             "Types other than str will be deprecated in future versions for" |  | ||||||
|             f" response.text, got type {type(body).__name__})", |  | ||||||
|             DeprecationWarning, |  | ||||||
|         ) |  | ||||||
|     # Type conversions are deprecated and quite b0rked but still supported for |  | ||||||
|     # text() until applications get fixed. This try-except should be removed. |  | ||||||
|     try: |  | ||||||
|         # Avoid repr(body).encode() b0rkage for body that is already encoded. |  | ||||||
|         # memoryview used only to test bytes-ishness. |  | ||||||
|         with memoryview(body): |  | ||||||
|             pass |  | ||||||
|     except TypeError: |  | ||||||
|         body = f"{body}"  # no-op if body is already str |  | ||||||
|     return HTTPResponse( |     return HTTPResponse( | ||||||
|         body, status=status, headers=headers, content_type=content_type |         body, status=status, headers=headers, content_type=content_type | ||||||
|     ) |     ) | ||||||
| @@ -249,7 +275,10 @@ def raw( | |||||||
|     :param content_type: the content type (string) of the response. |     :param content_type: the content type (string) of the response. | ||||||
|     """ |     """ | ||||||
|     return HTTPResponse( |     return HTTPResponse( | ||||||
|         body=body, status=status, headers=headers, content_type=content_type, |         body_bytes=body, | ||||||
|  |         status=status, | ||||||
|  |         headers=headers, | ||||||
|  |         content_type=content_type, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -257,14 +286,10 @@ def html(body, status=200, headers=None): | |||||||
|     """ |     """ | ||||||
|     Returns response object with body in html format. |     Returns response object with body in html format. | ||||||
|  |  | ||||||
|     :param body: str or bytes-ish, or an object with __html__ or _repr_html_. |     :param body: Response data to be encoded. | ||||||
|     :param status: Response code. |     :param status: Response code. | ||||||
|     :param headers: Custom Headers. |     :param headers: Custom Headers. | ||||||
|     """ |     """ | ||||||
|     if hasattr(body, "__html__"): |  | ||||||
|         body = body.__html__() |  | ||||||
|     elif hasattr(body, "_repr_html_"): |  | ||||||
|         body = body._repr_html_() |  | ||||||
|     return HTTPResponse( |     return HTTPResponse( | ||||||
|         body, |         body, | ||||||
|         status=status, |         status=status, | ||||||
| @@ -292,27 +317,29 @@ async def file( | |||||||
|     headers = headers or {} |     headers = headers or {} | ||||||
|     if filename: |     if filename: | ||||||
|         headers.setdefault( |         headers.setdefault( | ||||||
|             "Content-Disposition", f'attachment; filename="{filename}"' |             "Content-Disposition", 'attachment; filename="{}"'.format(filename) | ||||||
|         ) |         ) | ||||||
|     filename = filename or path.split(location)[-1] |     filename = filename or path.split(location)[-1] | ||||||
|  |  | ||||||
|     async with await open_async(location, mode="rb") as f: |     async with open_async(location, mode="rb") as _file: | ||||||
|         if _range: |         if _range: | ||||||
|             await f.seek(_range.start) |             await _file.seek(_range.start) | ||||||
|             out_stream = await f.read(_range.size) |             out_stream = await _file.read(_range.size) | ||||||
|             headers[ |             headers["Content-Range"] = "bytes %s-%s/%s" % ( | ||||||
|                 "Content-Range" |                 _range.start, | ||||||
|             ] = f"bytes {_range.start}-{_range.end}/{_range.total}" |                 _range.end, | ||||||
|  |                 _range.total, | ||||||
|  |             ) | ||||||
|             status = 206 |             status = 206 | ||||||
|         else: |         else: | ||||||
|             out_stream = await f.read() |             out_stream = await _file.read() | ||||||
|  |  | ||||||
|     mime_type = mime_type or guess_type(filename)[0] or "text/plain" |     mime_type = mime_type or guess_type(filename)[0] or "text/plain" | ||||||
|     return HTTPResponse( |     return HTTPResponse( | ||||||
|         body=out_stream, |  | ||||||
|         status=status, |         status=status, | ||||||
|         headers=headers, |         headers=headers, | ||||||
|         content_type=mime_type, |         content_type=mime_type, | ||||||
|  |         body_bytes=out_stream, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -339,36 +366,43 @@ async def file_stream( | |||||||
|     headers = headers or {} |     headers = headers or {} | ||||||
|     if filename: |     if filename: | ||||||
|         headers.setdefault( |         headers.setdefault( | ||||||
|             "Content-Disposition", f'attachment; filename="{filename}"' |             "Content-Disposition", 'attachment; filename="{}"'.format(filename) | ||||||
|         ) |         ) | ||||||
|     filename = filename or path.split(location)[-1] |     filename = filename or path.split(location)[-1] | ||||||
|     mime_type = mime_type or guess_type(filename)[0] or "text/plain" |  | ||||||
|     if _range: |  | ||||||
|         start = _range.start |  | ||||||
|         end = _range.end |  | ||||||
|         total = _range.total |  | ||||||
|  |  | ||||||
|         headers["Content-Range"] = f"bytes {start}-{end}/{total}" |     _file = await open_async(location, mode="rb") | ||||||
|         status = 206 |  | ||||||
|  |  | ||||||
|     async def _streaming_fn(response): |     async def _streaming_fn(response): | ||||||
|         async with await open_async(location, mode="rb") as f: |         nonlocal _file, chunk_size | ||||||
|  |         try: | ||||||
|             if _range: |             if _range: | ||||||
|                 await f.seek(_range.start) |                 chunk_size = min((_range.size, chunk_size)) | ||||||
|  |                 await _file.seek(_range.start) | ||||||
|                 to_send = _range.size |                 to_send = _range.size | ||||||
|                 while to_send > 0: |                 while to_send > 0: | ||||||
|                     content = await f.read(min((_range.size, chunk_size))) |                     content = await _file.read(chunk_size) | ||||||
|                     if len(content) < 1: |                     if len(content) < 1: | ||||||
|                         break |                         break | ||||||
|                     to_send -= len(content) |                     to_send -= len(content) | ||||||
|                     await response.write(content) |                     await response.write(content) | ||||||
|             else: |             else: | ||||||
|                 while True: |                 while True: | ||||||
|                     content = await f.read(chunk_size) |                     content = await _file.read(chunk_size) | ||||||
|                     if len(content) < 1: |                     if len(content) < 1: | ||||||
|                         break |                         break | ||||||
|                     await response.write(content) |                     await response.write(content) | ||||||
|  |         finally: | ||||||
|  |             await _file.close() | ||||||
|  |         return  # Returning from this fn closes the stream | ||||||
|  |  | ||||||
|  |     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, | ||||||
|  |         ) | ||||||
|  |         status = 206 | ||||||
|     return StreamingHTTPResponse( |     return StreamingHTTPResponse( | ||||||
|         streaming_fn=_streaming_fn, |         streaming_fn=_streaming_fn, | ||||||
|         status=status, |         status=status, | ||||||
|   | |||||||
| @@ -109,7 +109,7 @@ class Router: | |||||||
|             name, pattern = parameter_string.split(":", 1) |             name, pattern = parameter_string.split(":", 1) | ||||||
|             if not name: |             if not name: | ||||||
|                 raise ValueError( |                 raise ValueError( | ||||||
|                     f"Invalid parameter syntax: {parameter_string}" |                     "Invalid parameter syntax: {}".format(parameter_string) | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|         default = (str, pattern) |         default = (str, pattern) | ||||||
| @@ -143,7 +143,7 @@ class Router: | |||||||
|         routes = [] |         routes = [] | ||||||
|         if version is not None: |         if version is not None: | ||||||
|             version = re.escape(str(version).strip("/").lstrip("v")) |             version = re.escape(str(version).strip("/").lstrip("v")) | ||||||
|             uri = "/".join([f"/v{version}", uri.lstrip("/")]) |             uri = "/".join(["/v{}".format(version), uri.lstrip("/")]) | ||||||
|         # add regular version |         # add regular version | ||||||
|         routes.append(self._add(uri, methods, handler, host, name)) |         routes.append(self._add(uri, methods, handler, host, name)) | ||||||
|  |  | ||||||
| @@ -203,8 +203,8 @@ class Router: | |||||||
|             else: |             else: | ||||||
|                 if not isinstance(host, Iterable): |                 if not isinstance(host, Iterable): | ||||||
|                     raise ValueError( |                     raise ValueError( | ||||||
|                         f"Expected either string or Iterable of " |                         "Expected either string or Iterable of " | ||||||
|                         f"host strings, not {host!r}" |                         "host strings, not {!r}".format(host) | ||||||
|                     ) |                     ) | ||||||
|  |  | ||||||
|                 for host_ in host: |                 for host_ in host: | ||||||
| @@ -225,7 +225,8 @@ class Router: | |||||||
|  |  | ||||||
|             if name in parameter_names: |             if name in parameter_names: | ||||||
|                 raise ParameterNameConflicts( |                 raise ParameterNameConflicts( | ||||||
|                     f"Multiple parameter named <{name}> " f"in route uri {uri}" |                     "Multiple parameter named <{name}> " | ||||||
|  |                     "in route uri {uri}".format(name=name, uri=uri) | ||||||
|                 ) |                 ) | ||||||
|             parameter_names.add(name) |             parameter_names.add(name) | ||||||
|  |  | ||||||
| @@ -239,23 +240,23 @@ class Router: | |||||||
|             elif re.search(r"/", pattern): |             elif re.search(r"/", pattern): | ||||||
|                 properties["unhashable"] = True |                 properties["unhashable"] = True | ||||||
|  |  | ||||||
|             return f"({pattern})" |             return "({})".format(pattern) | ||||||
|  |  | ||||||
|         pattern_string = re.sub(self.parameter_pattern, add_parameter, uri) |         pattern_string = re.sub(self.parameter_pattern, add_parameter, uri) | ||||||
|         pattern = re.compile(fr"^{pattern_string}$") |         pattern = re.compile(r"^{}$".format(pattern_string)) | ||||||
|  |  | ||||||
|         def merge_route(route, methods, handler): |         def merge_route(route, methods, handler): | ||||||
|             # merge to the existing route when possible. |             # merge to the existing route when possible. | ||||||
|             if not route.methods or not methods: |             if not route.methods or not methods: | ||||||
|                 # method-unspecified routes are not mergeable. |                 # method-unspecified routes are not mergeable. | ||||||
|                 raise RouteExists(f"Route already registered: {uri}") |                 raise RouteExists("Route already registered: {}".format(uri)) | ||||||
|             elif route.methods.intersection(methods): |             elif route.methods.intersection(methods): | ||||||
|                 # already existing method is not overloadable. |                 # already existing method is not overloadable. | ||||||
|                 duplicated = methods.intersection(route.methods) |                 duplicated = methods.intersection(route.methods) | ||||||
|                 duplicated_methods = ",".join(list(duplicated)) |  | ||||||
|  |  | ||||||
|                 raise RouteExists( |                 raise RouteExists( | ||||||
|                     f"Route already registered: {uri} [{duplicated_methods}]" |                     "Route already registered: {} [{}]".format( | ||||||
|  |                         uri, ",".join(list(duplicated)) | ||||||
|  |                     ) | ||||||
|                 ) |                 ) | ||||||
|             if isinstance(route.handler, CompositionView): |             if isinstance(route.handler, CompositionView): | ||||||
|                 view = route.handler |                 view = route.handler | ||||||
| @@ -295,9 +296,9 @@ class Router: | |||||||
|             name = name.split("_static_", 1)[-1] |             name = name.split("_static_", 1)[-1] | ||||||
|  |  | ||||||
|         if hasattr(handler, "__blueprintname__"): |         if hasattr(handler, "__blueprintname__"): | ||||||
|             bp_name = handler.__blueprintname__ |             handler_name = "{}.{}".format( | ||||||
|  |                 handler.__blueprintname__, name or handler.__name__ | ||||||
|             handler_name = f"{bp_name}.{name or handler.__name__}" |             ) | ||||||
|         else: |         else: | ||||||
|             handler_name = name or getattr(handler, "__name__", None) |             handler_name = name or getattr(handler, "__name__", None) | ||||||
|  |  | ||||||
| @@ -351,6 +352,37 @@ class Router: | |||||||
|         else: |         else: | ||||||
|             return -1, None |             return -1, None | ||||||
|  |  | ||||||
|  |     def remove(self, uri, clean_cache=True, host=None): | ||||||
|  |         if host is not None: | ||||||
|  |             uri = host + uri | ||||||
|  |         try: | ||||||
|  |             route = self.routes_all.pop(uri) | ||||||
|  |             for handler_name, pairs in self.routes_names.items(): | ||||||
|  |                 if pairs[0] == uri: | ||||||
|  |                     self.routes_names.pop(handler_name) | ||||||
|  |                     break | ||||||
|  |  | ||||||
|  |             for handler_name, pairs in self.routes_static_files.items(): | ||||||
|  |                 if pairs[0] == uri: | ||||||
|  |                     self.routes_static_files.pop(handler_name) | ||||||
|  |                     break | ||||||
|  |  | ||||||
|  |         except KeyError: | ||||||
|  |             raise RouteDoesNotExist("Route was not registered: {}".format(uri)) | ||||||
|  |  | ||||||
|  |         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)] | ||||||
|  |         ): | ||||||
|  |             self.routes_dynamic[url_hash(uri)].remove(route) | ||||||
|  |         else: | ||||||
|  |             self.routes_static.pop(uri) | ||||||
|  |  | ||||||
|  |         if clean_cache: | ||||||
|  |             self._get.cache_clear() | ||||||
|  |  | ||||||
|     @lru_cache(maxsize=ROUTER_CACHE_SIZE) |     @lru_cache(maxsize=ROUTER_CACHE_SIZE) | ||||||
|     def find_route_by_view_name(self, view_name, name=None): |     def find_route_by_view_name(self, view_name, name=None): | ||||||
|         """Find a route in the router based on the specified view name. |         """Find a route in the router based on the specified view name. | ||||||
| @@ -410,7 +442,7 @@ class Router: | |||||||
|         # Check against known static routes |         # Check against known static routes | ||||||
|         route = self.routes_static.get(url) |         route = self.routes_static.get(url) | ||||||
|         method_not_supported = MethodNotSupported( |         method_not_supported = MethodNotSupported( | ||||||
|             f"Method {method} not allowed for URL {url}", |             "Method {} not allowed for URL {}".format(method, url), | ||||||
|             method=method, |             method=method, | ||||||
|             allowed_methods=self.get_supported_methods(url), |             allowed_methods=self.get_supported_methods(url), | ||||||
|         ) |         ) | ||||||
| @@ -440,7 +472,7 @@ class Router: | |||||||
|                     # Route was found but the methods didn't match |                     # Route was found but the methods didn't match | ||||||
|                     if route_found: |                     if route_found: | ||||||
|                         raise method_not_supported |                         raise method_not_supported | ||||||
|                     raise NotFound(f"Requested URL {url} not found") |                     raise NotFound("Requested URL {} not found".format(url)) | ||||||
|  |  | ||||||
|         kwargs = { |         kwargs = { | ||||||
|             p.name: p.cast(value) |             p.name: p.cast(value) | ||||||
| @@ -452,7 +484,7 @@ class Router: | |||||||
|         return route_handler, [], kwargs, route.uri, route.name |         return route_handler, [], kwargs, route.uri, route.name | ||||||
|  |  | ||||||
|     def is_stream_handler(self, request): |     def is_stream_handler(self, request): | ||||||
|         """ Handler for request is stream or not. |         """Handler for request is stream or not. | ||||||
|         :param request: Request object |         :param request: Request object | ||||||
|         :return: bool |         :return: bool | ||||||
|         """ |         """ | ||||||
|   | |||||||
							
								
								
									
										344
									
								
								sanic/server.py
									
									
									
									
									
								
							
							
						
						
									
										344
									
								
								sanic/server.py
									
									
									
									
									
								
							| @@ -1,24 +1,20 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import multiprocessing |  | ||||||
| import os | import os | ||||||
| import secrets |  | ||||||
| import socket |  | ||||||
| import stat |  | ||||||
| import sys |  | ||||||
| import traceback | import traceback | ||||||
|  |  | ||||||
| from collections import deque | from collections import deque | ||||||
| from functools import partial | from functools import partial | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from ipaddress import ip_address | from multiprocessing import Process | ||||||
| from signal import SIG_IGN, SIGINT, SIGTERM, Signals | from signal import SIG_IGN, SIGINT, SIGTERM, Signals | ||||||
| from signal import signal as signal_func | from signal import signal as signal_func | ||||||
|  | from socket import SO_REUSEADDR, SOL_SOCKET, socket | ||||||
| from time import time | from time import time | ||||||
|  |  | ||||||
| from httptools import HttpRequestParser  # type: ignore | from httptools import HttpRequestParser  # type: ignore | ||||||
| from httptools.parser.errors import HttpParserError  # type: ignore | from httptools.parser.errors import HttpParserError  # type: ignore | ||||||
|  |  | ||||||
| from sanic.compat import Header, ctrlc_workaround_for_windows | from sanic.compat import Header | ||||||
| from sanic.exceptions import ( | from sanic.exceptions import ( | ||||||
|     HeaderExpectationFailed, |     HeaderExpectationFailed, | ||||||
|     InvalidUsage, |     InvalidUsage, | ||||||
| @@ -40,48 +36,11 @@ try: | |||||||
| except ImportError: | except ImportError: | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
| OS_IS_WINDOWS = os.name == "nt" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Signal: | class Signal: | ||||||
|     stopped = False |     stopped = False | ||||||
|  |  | ||||||
|  |  | ||||||
| class ConnInfo: |  | ||||||
|     """Local and remote addresses and SSL status info.""" |  | ||||||
|  |  | ||||||
|     __slots__ = ( |  | ||||||
|         "sockname", |  | ||||||
|         "peername", |  | ||||||
|         "server", |  | ||||||
|         "server_port", |  | ||||||
|         "client", |  | ||||||
|         "client_port", |  | ||||||
|         "ssl", |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     def __init__(self, transport, unix=None): |  | ||||||
|         self.ssl = bool(transport.get_extra_info("sslcontext")) |  | ||||||
|         self.server = self.client = "" |  | ||||||
|         self.server_port = self.client_port = 0 |  | ||||||
|         self.peername = None |  | ||||||
|         self.sockname = addr = transport.get_extra_info("sockname") |  | ||||||
|         if isinstance(addr, str):  # UNIX socket |  | ||||||
|             self.server = unix or addr |  | ||||||
|             return |  | ||||||
|         # IPv4 (ip, port) or IPv6 (ip, port, flowinfo, scopeid) |  | ||||||
|         if isinstance(addr, tuple): |  | ||||||
|             self.server = addr[0] if len(addr) == 2 else f"[{addr[0]}]" |  | ||||||
|             self.server_port = addr[1] |  | ||||||
|             # self.server gets non-standard port appended |  | ||||||
|             if addr[1] != (443 if self.ssl else 80): |  | ||||||
|                 self.server = f"{self.server}:{addr[1]}" |  | ||||||
|         self.peername = addr = transport.get_extra_info("peername") |  | ||||||
|         if isinstance(addr, tuple): |  | ||||||
|             self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]" |  | ||||||
|             self.client_port = addr[1] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class HttpProtocol(asyncio.Protocol): | class HttpProtocol(asyncio.Protocol): | ||||||
|     """ |     """ | ||||||
|     This class provides a basic HTTP implementation of the sanic framework. |     This class provides a basic HTTP implementation of the sanic framework. | ||||||
| @@ -95,7 +54,6 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         "transport", |         "transport", | ||||||
|         "connections", |         "connections", | ||||||
|         "signal", |         "signal", | ||||||
|         "conn_info", |  | ||||||
|         # request params |         # request params | ||||||
|         "parser", |         "parser", | ||||||
|         "request", |         "request", | ||||||
| @@ -110,6 +68,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         "request_buffer_queue_size", |         "request_buffer_queue_size", | ||||||
|         "request_class", |         "request_class", | ||||||
|         "is_request_stream", |         "is_request_stream", | ||||||
|  |         "router", | ||||||
|         "error_handler", |         "error_handler", | ||||||
|         # enable or disable access log purpose |         # enable or disable access log purpose | ||||||
|         "access_log", |         "access_log", | ||||||
| @@ -127,7 +86,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         "_keep_alive", |         "_keep_alive", | ||||||
|         "_header_fragment", |         "_header_fragment", | ||||||
|         "state", |         "state", | ||||||
|         "_unix", |         "_debug", | ||||||
|         "_body_chunks", |         "_body_chunks", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @@ -136,38 +95,46 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         *, |         *, | ||||||
|         loop, |         loop, | ||||||
|         app, |         app, | ||||||
|  |         request_handler, | ||||||
|  |         error_handler, | ||||||
|         signal=Signal(), |         signal=Signal(), | ||||||
|         connections=None, |         connections=None, | ||||||
|  |         request_timeout=60, | ||||||
|  |         response_timeout=60, | ||||||
|  |         keep_alive_timeout=5, | ||||||
|  |         request_max_size=None, | ||||||
|  |         request_buffer_queue_size=100, | ||||||
|  |         request_class=None, | ||||||
|  |         access_log=True, | ||||||
|  |         keep_alive=True, | ||||||
|  |         is_request_stream=False, | ||||||
|  |         router=None, | ||||||
|         state=None, |         state=None, | ||||||
|         unix=None, |         debug=False, | ||||||
|         **kwargs, |         **kwargs | ||||||
|     ): |     ): | ||||||
|         asyncio.set_event_loop(loop) |  | ||||||
|         self.loop = loop |         self.loop = loop | ||||||
|         deprecated_loop = self.loop if sys.version_info < (3, 7) else None |  | ||||||
|         self.app = app |         self.app = app | ||||||
|         self.transport = None |         self.transport = None | ||||||
|         self.conn_info = None |  | ||||||
|         self.request = None |         self.request = None | ||||||
|         self.parser = None |         self.parser = None | ||||||
|         self.url = None |         self.url = None | ||||||
|         self.headers = None |         self.headers = None | ||||||
|  |         self.router = router | ||||||
|         self.signal = signal |         self.signal = signal | ||||||
|         self.access_log = self.app.config.ACCESS_LOG |         self.access_log = access_log | ||||||
|         self.connections = connections if connections is not None else set() |         self.connections = connections if connections is not None else set() | ||||||
|         self.request_handler = self.app.handle_request |         self.request_handler = request_handler | ||||||
|         self.error_handler = self.app.error_handler |         self.error_handler = error_handler | ||||||
|         self.request_timeout = self.app.config.REQUEST_TIMEOUT |         self.request_timeout = request_timeout | ||||||
|         self.request_buffer_queue_size = ( |         self.request_buffer_queue_size = request_buffer_queue_size | ||||||
|             self.app.config.REQUEST_BUFFER_QUEUE_SIZE |         self.response_timeout = response_timeout | ||||||
|         ) |         self.keep_alive_timeout = keep_alive_timeout | ||||||
|         self.response_timeout = self.app.config.RESPONSE_TIMEOUT |         self.request_max_size = request_max_size | ||||||
|         self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT |         self.request_class = request_class or Request | ||||||
|         self.request_max_size = self.app.config.REQUEST_MAX_SIZE |         self.is_request_stream = is_request_stream | ||||||
|         self.request_class = self.app.request_class or Request |  | ||||||
|         self.is_request_stream = self.app.is_request_stream |  | ||||||
|         self._is_stream_handler = False |         self._is_stream_handler = False | ||||||
|         self._not_paused = asyncio.Event(loop=deprecated_loop) |         self._not_paused = asyncio.Event(loop=loop) | ||||||
|         self._total_request_size = 0 |         self._total_request_size = 0 | ||||||
|         self._request_timeout_handler = None |         self._request_timeout_handler = None | ||||||
|         self._response_timeout_handler = None |         self._response_timeout_handler = None | ||||||
| @@ -176,12 +143,12 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self._last_response_time = None |         self._last_response_time = None | ||||||
|         self._request_handler_task = None |         self._request_handler_task = None | ||||||
|         self._request_stream_task = None |         self._request_stream_task = None | ||||||
|         self._keep_alive = self.app.config.KEEP_ALIVE |         self._keep_alive = keep_alive | ||||||
|         self._header_fragment = b"" |         self._header_fragment = b"" | ||||||
|         self.state = state if state else {} |         self.state = state if state else {} | ||||||
|         if "requests_count" not in self.state: |         if "requests_count" not in self.state: | ||||||
|             self.state["requests_count"] = 0 |             self.state["requests_count"] = 0 | ||||||
|         self._unix = unix |         self._debug = debug | ||||||
|         self._not_paused.set() |         self._not_paused.set() | ||||||
|         self._body_chunks = deque() |         self._body_chunks = deque() | ||||||
|  |  | ||||||
| @@ -210,7 +177,6 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             self.request_timeout, self.request_timeout_callback |             self.request_timeout, self.request_timeout_callback | ||||||
|         ) |         ) | ||||||
|         self.transport = transport |         self.transport = transport | ||||||
|         self.conn_info = ConnInfo(transport, unix=self._unix) |  | ||||||
|         self._last_request_time = time() |         self._last_request_time = time() | ||||||
|  |  | ||||||
|     def connection_lost(self, exc): |     def connection_lost(self, exc): | ||||||
| @@ -310,7 +276,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             self.parser.feed_data(data) |             self.parser.feed_data(data) | ||||||
|         except HttpParserError: |         except HttpParserError: | ||||||
|             message = "Bad Request" |             message = "Bad Request" | ||||||
|             if self.app.debug: |             if self._debug: | ||||||
|                 message += "\n" + traceback.format_exc() |                 message += "\n" + traceback.format_exc() | ||||||
|             self.write_error(InvalidUsage(message)) |             self.write_error(InvalidUsage(message)) | ||||||
|  |  | ||||||
| @@ -348,7 +314,6 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             transport=self.transport, |             transport=self.transport, | ||||||
|             app=self.app, |             app=self.app, | ||||||
|         ) |         ) | ||||||
|         self.request.conn_info = self.conn_info |  | ||||||
|         # Remove any existing KeepAlive handler here, |         # Remove any existing KeepAlive handler here, | ||||||
|         # It will be recreated if required on the new request. |         # It will be recreated if required on the new request. | ||||||
|         if self._keep_alive_timeout_handler: |         if self._keep_alive_timeout_handler: | ||||||
| @@ -359,7 +324,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             self.expect_handler() |             self.expect_handler() | ||||||
|  |  | ||||||
|         if self.is_request_stream: |         if self.is_request_stream: | ||||||
|             self._is_stream_handler = self.app.router.is_stream_handler( |             self._is_stream_handler = self.router.is_stream_handler( | ||||||
|                 self.request |                 self.request | ||||||
|             ) |             ) | ||||||
|             if self._is_stream_handler: |             if self._is_stream_handler: | ||||||
| @@ -378,7 +343,9 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|                 self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n") |                 self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n") | ||||||
|             else: |             else: | ||||||
|                 self.write_error( |                 self.write_error( | ||||||
|                     HeaderExpectationFailed(f"Unknown Expect: {expect}") |                     HeaderExpectationFailed( | ||||||
|  |                         "Unknown Expect: {expect}".format(expect=expect) | ||||||
|  |                     ) | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|     def on_body(self, body): |     def on_body(self, body): | ||||||
| @@ -485,9 +452,13 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             extra["host"] = "UNKNOWN" |             extra["host"] = "UNKNOWN" | ||||||
|             if self.request is not None: |             if self.request is not None: | ||||||
|                 if self.request.ip: |                 if self.request.ip: | ||||||
|                     extra["host"] = f"{self.request.ip}:{self.request.port}" |                     extra["host"] = "{0}:{1}".format( | ||||||
|  |                         self.request.ip, self.request.port | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|                 extra["request"] = f"{self.request.method} {self.request.url}" |                 extra["request"] = "{0} {1}".format( | ||||||
|  |                     self.request.method, self.request.url | ||||||
|  |                 ) | ||||||
|             else: |             else: | ||||||
|                 extra["request"] = "nil" |                 extra["request"] = "nil" | ||||||
|  |  | ||||||
| @@ -517,14 +488,16 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             ) |             ) | ||||||
|             self.write_error(ServerError("Invalid response type")) |             self.write_error(ServerError("Invalid response type")) | ||||||
|         except RuntimeError: |         except RuntimeError: | ||||||
|             if self.app.debug: |             if self._debug: | ||||||
|                 logger.error( |                 logger.error( | ||||||
|                     "Connection lost before response written @ %s", |                     "Connection lost before response written @ %s", | ||||||
|                     self.request.ip, |                     self.request.ip, | ||||||
|                 ) |                 ) | ||||||
|             keep_alive = False |             keep_alive = False | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             self.bail_out(f"Writing response failed, connection closed {e!r}") |             self.bail_out( | ||||||
|  |                 "Writing response failed, connection closed {}".format(repr(e)) | ||||||
|  |             ) | ||||||
|         finally: |         finally: | ||||||
|             if not keep_alive: |             if not keep_alive: | ||||||
|                 self.transport.close() |                 self.transport.close() | ||||||
| @@ -568,14 +541,16 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             ) |             ) | ||||||
|             self.write_error(ServerError("Invalid response type")) |             self.write_error(ServerError("Invalid response type")) | ||||||
|         except RuntimeError: |         except RuntimeError: | ||||||
|             if self.app.debug: |             if self._debug: | ||||||
|                 logger.error( |                 logger.error( | ||||||
|                     "Connection lost before response written @ %s", |                     "Connection lost before response written @ %s", | ||||||
|                     self.request.ip, |                     self.request.ip, | ||||||
|                 ) |                 ) | ||||||
|             keep_alive = False |             keep_alive = False | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             self.bail_out(f"Writing response failed, connection closed {e!r}") |             self.bail_out( | ||||||
|  |                 "Writing response failed, connection closed {}".format(repr(e)) | ||||||
|  |             ) | ||||||
|         finally: |         finally: | ||||||
|             if not keep_alive: |             if not keep_alive: | ||||||
|                 self.transport.close() |                 self.transport.close() | ||||||
| @@ -599,14 +574,14 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             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)) |             self.transport.write(response.output(version)) | ||||||
|         except RuntimeError: |         except RuntimeError: | ||||||
|             if self.app.debug: |             if self._debug: | ||||||
|                 logger.error( |                 logger.error( | ||||||
|                     "Connection lost before error written @ %s", |                     "Connection lost before error written @ %s", | ||||||
|                     self.request.ip if self.request else "Unknown", |                     self.request.ip if self.request else "Unknown", | ||||||
|                 ) |                 ) | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             self.bail_out( |             self.bail_out( | ||||||
|                 f"Writing error failed, connection closed {e!r}", |                 "Writing error failed, connection closed {}".format(repr(e)), | ||||||
|                 from_error=True, |                 from_error=True, | ||||||
|             ) |             ) | ||||||
|         finally: |         finally: | ||||||
| @@ -667,7 +642,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|  |  | ||||||
|         :return: boolean - True if closed, false if staying open |         :return: boolean - True if closed, false if staying open | ||||||
|         """ |         """ | ||||||
|         if not self.parser and self.transport is not None: |         if not self.parser: | ||||||
|             self.transport.close() |             self.transport.close() | ||||||
|             return True |             return True | ||||||
|         return False |         return False | ||||||
| @@ -789,13 +764,20 @@ def serve( | |||||||
|     host, |     host, | ||||||
|     port, |     port, | ||||||
|     app, |     app, | ||||||
|  |     request_handler, | ||||||
|  |     error_handler, | ||||||
|     before_start=None, |     before_start=None, | ||||||
|     after_start=None, |     after_start=None, | ||||||
|     before_stop=None, |     before_stop=None, | ||||||
|     after_stop=None, |     after_stop=None, | ||||||
|  |     debug=False, | ||||||
|  |     request_timeout=60, | ||||||
|  |     response_timeout=60, | ||||||
|  |     keep_alive_timeout=5, | ||||||
|     ssl=None, |     ssl=None, | ||||||
|     sock=None, |     sock=None, | ||||||
|     unix=None, |     request_max_size=None, | ||||||
|  |     request_buffer_queue_size=100, | ||||||
|     reuse_port=False, |     reuse_port=False, | ||||||
|     loop=None, |     loop=None, | ||||||
|     protocol=HttpProtocol, |     protocol=HttpProtocol, | ||||||
| @@ -805,13 +787,25 @@ def serve( | |||||||
|     run_async=False, |     run_async=False, | ||||||
|     connections=None, |     connections=None, | ||||||
|     signal=Signal(), |     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, |     state=None, | ||||||
|  |     graceful_shutdown_timeout=15.0, | ||||||
|     asyncio_server_kwargs=None, |     asyncio_server_kwargs=None, | ||||||
| ): | ): | ||||||
|     """Start asynchronous HTTP Server on an individual process. |     """Start asynchronous HTTP Server on an individual process. | ||||||
|  |  | ||||||
|     :param host: Address to host on |     :param host: Address to host on | ||||||
|     :param port: Port to host on |     :param port: Port to host on | ||||||
|  |     :param request_handler: Sanic request handler with middleware | ||||||
|  |     :param error_handler: Sanic error handler with middleware | ||||||
|     :param before_start: function to be executed before the server starts |     :param before_start: function to be executed before the server starts | ||||||
|                          listening. Takes arguments `app` instance and `loop` |                          listening. Takes arguments `app` instance and `loop` | ||||||
|     :param after_start: function to be executed after the server starts |     :param after_start: function to be executed after the server starts | ||||||
| @@ -822,13 +816,35 @@ def serve( | |||||||
|     :param after_stop: function to be executed when a stop signal is |     :param after_stop: function to be executed when a stop signal is | ||||||
|                        received after it is respected. Takes arguments |                        received after it is respected. Takes arguments | ||||||
|                        `app` instance and `loop` |                        `app` instance and `loop` | ||||||
|  |     :param debug: enables debug output (slows server) | ||||||
|  |     :param request_timeout: time in seconds | ||||||
|  |     :param response_timeout: time in seconds | ||||||
|  |     :param keep_alive_timeout: time in seconds | ||||||
|     :param ssl: SSLContext |     :param ssl: SSLContext | ||||||
|     :param sock: Socket for the server to accept connections from |     :param sock: Socket for the server to accept connections from | ||||||
|     :param unix: Unix socket to listen on instead of TCP port |     :param request_max_size: size in bytes, `None` for no limit | ||||||
|     :param reuse_port: `True` for multiple workers |     :param reuse_port: `True` for multiple workers | ||||||
|     :param loop: asyncio compatible event loop |     :param loop: asyncio compatible event loop | ||||||
|  |     :param protocol: subclass of asyncio protocol class | ||||||
|     :param run_async: bool: Do not create a new event loop for the server, |     :param run_async: bool: Do not create a new event loop for the server, | ||||||
|                       and return an AsyncServer object rather than running it |                       and return an AsyncServer object rather than running it | ||||||
|  |     :param request_class: Request class to use | ||||||
|  |     :param access_log: disable/enable access log | ||||||
|  |     :param websocket_max_size: enforces the maximum size for | ||||||
|  |                                incoming messages in bytes. | ||||||
|  |     :param websocket_max_queue: sets the maximum length of the queue | ||||||
|  |                                 that holds incoming messages. | ||||||
|  |     :param websocket_read_limit: sets the high-water limit of the buffer for | ||||||
|  |                                  incoming bytes, the low-water limit is half | ||||||
|  |                                  the high-water limit. | ||||||
|  |     :param websocket_write_limit: sets the high-water limit of the buffer for | ||||||
|  |                                   outgoing bytes, the low-water limit is a | ||||||
|  |                                   quarter of the high-water limit. | ||||||
|  |     :param is_request_stream: disable/enable Request.stream | ||||||
|  |     :param request_buffer_queue_size: streaming request buffer queue size | ||||||
|  |     :param router: Router object | ||||||
|  |     :param graceful_shutdown_timeout: How long take to Force close non-idle | ||||||
|  |                                       connection | ||||||
|     :param asyncio_server_kwargs: key-value args for asyncio/uvloop |     :param asyncio_server_kwargs: key-value args for asyncio/uvloop | ||||||
|                                   create_server method |                                   create_server method | ||||||
|     :return: Nothing |     :return: Nothing | ||||||
| @@ -838,8 +854,8 @@ def serve( | |||||||
|         loop = asyncio.new_event_loop() |         loop = asyncio.new_event_loop() | ||||||
|         asyncio.set_event_loop(loop) |         asyncio.set_event_loop(loop) | ||||||
|  |  | ||||||
|     if app.debug: |     if debug: | ||||||
|         loop.set_debug(app.debug) |         loop.set_debug(debug) | ||||||
|  |  | ||||||
|     app.asgi = False |     app.asgi = False | ||||||
|  |  | ||||||
| @@ -850,34 +866,47 @@ def serve( | |||||||
|         connections=connections, |         connections=connections, | ||||||
|         signal=signal, |         signal=signal, | ||||||
|         app=app, |         app=app, | ||||||
|  |         request_handler=request_handler, | ||||||
|  |         error_handler=error_handler, | ||||||
|  |         request_timeout=request_timeout, | ||||||
|  |         response_timeout=response_timeout, | ||||||
|  |         keep_alive_timeout=keep_alive_timeout, | ||||||
|  |         request_max_size=request_max_size, | ||||||
|  |         request_buffer_queue_size=request_buffer_queue_size, | ||||||
|  |         request_class=request_class, | ||||||
|  |         access_log=access_log, | ||||||
|  |         keep_alive=keep_alive, | ||||||
|  |         is_request_stream=is_request_stream, | ||||||
|  |         router=router, | ||||||
|  |         websocket_max_size=websocket_max_size, | ||||||
|  |         websocket_max_queue=websocket_max_queue, | ||||||
|  |         websocket_read_limit=websocket_read_limit, | ||||||
|  |         websocket_write_limit=websocket_write_limit, | ||||||
|         state=state, |         state=state, | ||||||
|         unix=unix, |         debug=debug, | ||||||
|     ) |     ) | ||||||
|     asyncio_server_kwargs = ( |     asyncio_server_kwargs = ( | ||||||
|         asyncio_server_kwargs if asyncio_server_kwargs else {} |         asyncio_server_kwargs if asyncio_server_kwargs else {} | ||||||
|     ) |     ) | ||||||
|     # UNIX sockets are always bound by us (to preserve semantics between modes) |  | ||||||
|     if unix: |  | ||||||
|         sock = bind_unix_socket(unix, backlog=backlog) |  | ||||||
|     server_coroutine = loop.create_server( |     server_coroutine = loop.create_server( | ||||||
|         server, |         server, | ||||||
|         None if sock else host, |         host, | ||||||
|         None if sock else port, |         port, | ||||||
|         ssl=ssl, |         ssl=ssl, | ||||||
|         reuse_port=reuse_port, |         reuse_port=reuse_port, | ||||||
|         sock=sock, |         sock=sock, | ||||||
|         backlog=backlog, |         backlog=backlog, | ||||||
|         **asyncio_server_kwargs, |         **asyncio_server_kwargs | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     if run_async: |     if run_async: | ||||||
|         return AsyncioServer( |         return AsyncioServer( | ||||||
|             loop=loop, |             loop, | ||||||
|             serve_coro=server_coroutine, |             server_coroutine, | ||||||
|             connections=connections, |             connections, | ||||||
|             after_start=after_start, |             after_start, | ||||||
|             before_stop=before_stop, |             before_stop, | ||||||
|             after_stop=after_stop, |             after_stop, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     trigger_events(before_start, loop) |     trigger_events(before_start, loop) | ||||||
| @@ -896,11 +925,15 @@ def serve( | |||||||
|  |  | ||||||
|     # Register signals for graceful termination |     # Register signals for graceful termination | ||||||
|     if register_sys_signals: |     if register_sys_signals: | ||||||
|         if OS_IS_WINDOWS: |         _singals = (SIGTERM,) if run_multiple else (SIGINT, SIGTERM) | ||||||
|             ctrlc_workaround_for_windows(app) |         for _signal in _singals: | ||||||
|         else: |             try: | ||||||
|             for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]: |                 loop.add_signal_handler(_signal, loop.stop) | ||||||
|                 loop.add_signal_handler(_signal, app.stop) |             except NotImplementedError: | ||||||
|  |                 logger.warning( | ||||||
|  |                     "Sanic tried to use loop.add_signal_handler " | ||||||
|  |                     "but it is not implemented on this platform." | ||||||
|  |                 ) | ||||||
|     pid = os.getpid() |     pid = os.getpid() | ||||||
|     try: |     try: | ||||||
|         logger.info("Starting worker [%s]", pid) |         logger.info("Starting worker [%s]", pid) | ||||||
| @@ -924,9 +957,8 @@ def serve( | |||||||
|         # We should provide graceful_shutdown_timeout, |         # We should provide graceful_shutdown_timeout, | ||||||
|         # instead of letting connection hangs forever. |         # instead of letting connection hangs forever. | ||||||
|         # Let's roughly calcucate time. |         # Let's roughly calcucate time. | ||||||
|         graceful = app.config.GRACEFUL_SHUTDOWN_TIMEOUT |  | ||||||
|         start_shutdown = 0 |         start_shutdown = 0 | ||||||
|         while connections and (start_shutdown < graceful): |         while connections and (start_shutdown < graceful_shutdown_timeout): | ||||||
|             loop.run_until_complete(asyncio.sleep(0.1)) |             loop.run_until_complete(asyncio.sleep(0.1)) | ||||||
|             start_shutdown = start_shutdown + 0.1 |             start_shutdown = start_shutdown + 0.1 | ||||||
|  |  | ||||||
| @@ -939,91 +971,12 @@ def serve( | |||||||
|             else: |             else: | ||||||
|                 conn.close() |                 conn.close() | ||||||
|  |  | ||||||
|         _shutdown = asyncio.gather(*coros) |         _shutdown = asyncio.gather(*coros, loop=loop) | ||||||
|         loop.run_until_complete(_shutdown) |         loop.run_until_complete(_shutdown) | ||||||
|  |  | ||||||
|         trigger_events(after_stop, loop) |         trigger_events(after_stop, loop) | ||||||
|  |  | ||||||
|         loop.close() |         loop.close() | ||||||
|         remove_unix_socket(unix) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket: |  | ||||||
|     """Create TCP server socket. |  | ||||||
|     :param host: IPv4, IPv6 or hostname may be specified |  | ||||||
|     :param port: TCP port number |  | ||||||
|     :param backlog: Maximum number of connections to queue |  | ||||||
|     :return: socket.socket object |  | ||||||
|     """ |  | ||||||
|     try:  # IP address: family must be specified for IPv6 at least |  | ||||||
|         ip = ip_address(host) |  | ||||||
|         host = str(ip) |  | ||||||
|         sock = socket.socket( |  | ||||||
|             socket.AF_INET6 if ip.version == 6 else socket.AF_INET |  | ||||||
|         ) |  | ||||||
|     except ValueError:  # Hostname, may become AF_INET or AF_INET6 |  | ||||||
|         sock = socket.socket() |  | ||||||
|     sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |  | ||||||
|     sock.bind((host, port)) |  | ||||||
|     sock.listen(backlog) |  | ||||||
|     return sock |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def bind_unix_socket(path: str, *, mode=0o666, backlog=100) -> socket.socket: |  | ||||||
|     """Create unix socket. |  | ||||||
|     :param path: filesystem path |  | ||||||
|     :param backlog: Maximum number of connections to queue |  | ||||||
|     :return: socket.socket object |  | ||||||
|     """ |  | ||||||
|     """Open or atomically replace existing socket with zero downtime.""" |  | ||||||
|     # Sanitise and pre-verify socket path |  | ||||||
|     path = os.path.abspath(path) |  | ||||||
|     folder = os.path.dirname(path) |  | ||||||
|     if not os.path.isdir(folder): |  | ||||||
|         raise FileNotFoundError(f"Socket folder does not exist: {folder}") |  | ||||||
|     try: |  | ||||||
|         if not stat.S_ISSOCK(os.stat(path, follow_symlinks=False).st_mode): |  | ||||||
|             raise FileExistsError(f"Existing file is not a socket: {path}") |  | ||||||
|     except FileNotFoundError: |  | ||||||
|         pass |  | ||||||
|     # Create new socket with a random temporary name |  | ||||||
|     tmp_path = f"{path}.{secrets.token_urlsafe()}" |  | ||||||
|     sock = socket.socket(socket.AF_UNIX) |  | ||||||
|     try: |  | ||||||
|         # Critical section begins (filename races) |  | ||||||
|         sock.bind(tmp_path) |  | ||||||
|         try: |  | ||||||
|             os.chmod(tmp_path, mode) |  | ||||||
|             # Start listening before rename to avoid connection failures |  | ||||||
|             sock.listen(backlog) |  | ||||||
|             os.rename(tmp_path, path) |  | ||||||
|         except:  # noqa: E722 |  | ||||||
|             try: |  | ||||||
|                 os.unlink(tmp_path) |  | ||||||
|             finally: |  | ||||||
|                 raise |  | ||||||
|     except:  # noqa: E722 |  | ||||||
|         try: |  | ||||||
|             sock.close() |  | ||||||
|         finally: |  | ||||||
|             raise |  | ||||||
|     return sock |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def remove_unix_socket(path: str) -> None: |  | ||||||
|     """Remove dead unix socket during server exit.""" |  | ||||||
|     if not path: |  | ||||||
|         return |  | ||||||
|     try: |  | ||||||
|         if stat.S_ISSOCK(os.stat(path, follow_symlinks=False).st_mode): |  | ||||||
|             # Is it actually dead (doesn't belong to a new server instance)? |  | ||||||
|             with socket.socket(socket.AF_UNIX) as testsock: |  | ||||||
|                 try: |  | ||||||
|                     testsock.connect(path) |  | ||||||
|                 except ConnectionRefusedError: |  | ||||||
|                     os.unlink(path) |  | ||||||
|     except FileNotFoundError: |  | ||||||
|         pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def serve_multiple(server_settings, workers): | def serve_multiple(server_settings, workers): | ||||||
| @@ -1038,17 +991,11 @@ def serve_multiple(server_settings, workers): | |||||||
|     server_settings["reuse_port"] = True |     server_settings["reuse_port"] = True | ||||||
|     server_settings["run_multiple"] = True |     server_settings["run_multiple"] = True | ||||||
|  |  | ||||||
|     # Create a listening socket or use the one in settings |     # Handling when custom socket is not provided. | ||||||
|     sock = server_settings.get("sock") |     if server_settings.get("sock") is None: | ||||||
|     unix = server_settings["unix"] |         sock = socket() | ||||||
|     backlog = server_settings["backlog"] |         sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) | ||||||
|     if unix: |         sock.bind((server_settings["host"], server_settings["port"])) | ||||||
|         sock = bind_unix_socket(unix, backlog=backlog) |  | ||||||
|         server_settings["unix"] = unix |  | ||||||
|     if sock is None: |  | ||||||
|         sock = bind_socket( |  | ||||||
|             server_settings["host"], server_settings["port"], backlog=backlog |  | ||||||
|         ) |  | ||||||
|         sock.set_inheritable(True) |         sock.set_inheritable(True) | ||||||
|         server_settings["sock"] = sock |         server_settings["sock"] = sock | ||||||
|         server_settings["host"] = None |         server_settings["host"] = None | ||||||
| @@ -1063,10 +1010,9 @@ def serve_multiple(server_settings, workers): | |||||||
|  |  | ||||||
|     signal_func(SIGINT, lambda s, f: sig_handler(s, f)) |     signal_func(SIGINT, lambda s, f: sig_handler(s, f)) | ||||||
|     signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) |     signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) | ||||||
|     mp = multiprocessing.get_context("fork") |  | ||||||
|  |  | ||||||
|     for _ in range(workers): |     for _ in range(workers): | ||||||
|         process = mp.Process(target=serve, kwargs=server_settings) |         process = Process(target=serve, kwargs=server_settings) | ||||||
|         process.daemon = True |         process.daemon = True | ||||||
|         process.start() |         process.start() | ||||||
|         processes.append(process) |         processes.append(process) | ||||||
| @@ -1077,6 +1023,4 @@ def serve_multiple(server_settings, workers): | |||||||
|     # the above processes will block this until they're stopped |     # the above processes will block this until they're stopped | ||||||
|     for process in processes: |     for process in processes: | ||||||
|         process.terminate() |         process.terminate() | ||||||
|  |     server_settings.get("sock").close() | ||||||
|     sock.close() |  | ||||||
|     remove_unix_socket(unix) |  | ||||||
|   | |||||||
							
								
								
									
										176
									
								
								sanic/static.py
									
									
									
									
									
								
							
							
						
						
									
										176
									
								
								sanic/static.py
									
									
									
									
									
								
							| @@ -1,11 +1,11 @@ | |||||||
| from functools import partial, wraps |  | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path | ||||||
| from re import sub | from re import sub | ||||||
| from time import gmtime, strftime | from time import gmtime, strftime | ||||||
| from urllib.parse import unquote | from urllib.parse import unquote | ||||||
|  |  | ||||||
| from sanic.compat import stat_async | from aiofiles.os import stat  # type: ignore | ||||||
|  |  | ||||||
| from sanic.exceptions import ( | from sanic.exceptions import ( | ||||||
|     ContentRangeError, |     ContentRangeError, | ||||||
|     FileNotFound, |     FileNotFound, | ||||||
| @@ -16,89 +16,6 @@ from sanic.handlers import ContentRangeHandler | |||||||
| from sanic.response import HTTPResponse, file, file_stream | from sanic.response import HTTPResponse, file, file_stream | ||||||
|  |  | ||||||
|  |  | ||||||
| async def _static_request_handler( |  | ||||||
|     file_or_directory, |  | ||||||
|     use_modified_since, |  | ||||||
|     use_content_range, |  | ||||||
|     stream_large_files, |  | ||||||
|     request, |  | ||||||
|     content_type=None, |  | ||||||
|     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: |  | ||||||
|         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 |  | ||||||
|     # from herping a derp and treating the uri as an absolute path |  | ||||||
|     root_path = file_path = file_or_directory |  | ||||||
|     if file_uri: |  | ||||||
|         file_path = path.join(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 |  | ||||||
|         ) |  | ||||||
|     try: |  | ||||||
|         headers = {} |  | ||||||
|         # Check if the client has been sent this file before |  | ||||||
|         # and it has not been modified since |  | ||||||
|         stats = None |  | ||||||
|         if use_modified_since: |  | ||||||
|             stats = await stat_async(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: |  | ||||||
|                 return HTTPResponse(status=304) |  | ||||||
|             headers["Last-Modified"] = modified_since |  | ||||||
|         _range = None |  | ||||||
|         if use_content_range: |  | ||||||
|             _range = None |  | ||||||
|             if not stats: |  | ||||||
|                 stats = await stat_async(file_path) |  | ||||||
|             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"] |  | ||||||
|                     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": |  | ||||||
|             return HTTPResponse(headers=headers) |  | ||||||
|         else: |  | ||||||
|             if stream_large_files: |  | ||||||
|                 if type(stream_large_files) == int: |  | ||||||
|                     threshold = stream_large_files |  | ||||||
|                 else: |  | ||||||
|                     threshold = 1024 * 1024 |  | ||||||
|  |  | ||||||
|                 if not stats: |  | ||||||
|                     stats = await stat_async(file_path) |  | ||||||
|                 if stats.st_size >= threshold: |  | ||||||
|                     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 |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def register( | def register( | ||||||
|     app, |     app, | ||||||
|     uri, |     uri, | ||||||
| @@ -140,20 +57,85 @@ def register( | |||||||
|     if not path.isfile(file_or_directory): |     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: | ||||||
|  |             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 | ||||||
|  |         # from herping a derp and treating the uri as an absolute path | ||||||
|  |         root_path = file_path = file_or_directory | ||||||
|  |         if file_uri: | ||||||
|  |             file_path = path.join( | ||||||
|  |                 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 | ||||||
|  |             ) | ||||||
|  |         try: | ||||||
|  |             headers = {} | ||||||
|  |             # Check if the client has been sent this file before | ||||||
|  |             # and it has not been modified since | ||||||
|  |             stats = None | ||||||
|  |             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: | ||||||
|  |                     return HTTPResponse(status=304) | ||||||
|  |                 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": | ||||||
|  |                     try: | ||||||
|  |                         _range = ContentRangeHandler(request, stats) | ||||||
|  |                     except HeaderNotFound: | ||||||
|  |                         pass | ||||||
|  |                     else: | ||||||
|  |                         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": | ||||||
|  |                 return HTTPResponse(headers=headers) | ||||||
|  |             else: | ||||||
|  |                 if stream_large_files: | ||||||
|  |                     if type(stream_large_files) == int: | ||||||
|  |                         threshold = stream_large_files | ||||||
|  |                     else: | ||||||
|  |                         threshold = 1024 * 1024 | ||||||
|  |  | ||||||
|  |                     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(file_path, headers=headers, _range=_range) | ||||||
|  |         except ContentRangeError: | ||||||
|  |             raise | ||||||
|  |         except Exception: | ||||||
|  |             raise FileNotFound( | ||||||
|  |                 "File not found", path=file_or_directory, relative_url=file_uri | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     # special prefix for static files |     # special prefix for static files | ||||||
|     if not name.startswith("_static_"): |     if not name.startswith("_static_"): | ||||||
|         name = f"_static_{name}" |         name = "_static_{}".format(name) | ||||||
|  |  | ||||||
|     _handler = wraps(_static_request_handler)( |  | ||||||
|         partial( |  | ||||||
|             _static_request_handler, |  | ||||||
|             file_or_directory, |  | ||||||
|             use_modified_since, |  | ||||||
|             use_content_range, |  | ||||||
|             stream_large_files, |  | ||||||
|             content_type=content_type, |  | ||||||
|         ) |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     app.route( |     app.route( | ||||||
|         uri, |         uri, | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ from sanic.response import text | |||||||
|  |  | ||||||
| ASGI_HOST = "mockserver" | ASGI_HOST = "mockserver" | ||||||
| HOST = "127.0.0.1" | HOST = "127.0.0.1" | ||||||
| PORT = None | PORT = 42101 | ||||||
|  |  | ||||||
|  |  | ||||||
| class SanicTestClient: | class SanicTestClient: | ||||||
| @@ -23,7 +23,7 @@ class SanicTestClient: | |||||||
|         self.host = host |         self.host = host | ||||||
|  |  | ||||||
|     def get_new_session(self): |     def get_new_session(self): | ||||||
|         return httpx.AsyncClient(verify=False) |         return httpx.Client() | ||||||
|  |  | ||||||
|     async def _local_request(self, method, url, *args, **kwargs): |     async def _local_request(self, method, url, *args, **kwargs): | ||||||
|         logger.info(url) |         logger.info(url) | ||||||
| @@ -38,22 +38,20 @@ class SanicTestClient: | |||||||
|  |  | ||||||
|                 try: |                 try: | ||||||
|                     response = await getattr(session, method.lower())( |                     response = await getattr(session, method.lower())( | ||||||
|                         url, *args, **kwargs |                         url, verify=False, *args, **kwargs | ||||||
|                     ) |                     ) | ||||||
|                 except NameError: |                 except NameError: | ||||||
|                     raise Exception(response.status_code) |                     raise Exception(response.status_code) | ||||||
|  |  | ||||||
|                 response.body = await response.aread() |  | ||||||
|                 response.status = response.status_code |  | ||||||
|                 response.content_type = response.headers.get("content-type") |  | ||||||
|  |  | ||||||
|                 # response can be decoded as json after response._content |  | ||||||
|                 # is set by response.aread() |  | ||||||
|                 try: |                 try: | ||||||
|                     response.json = response.json() |                     response.json = response.json() | ||||||
|                 except (JSONDecodeError, UnicodeDecodeError): |                 except (JSONDecodeError, UnicodeDecodeError): | ||||||
|                     response.json = None |                     response.json = None | ||||||
|  |  | ||||||
|  |                 response.body = await response.read() | ||||||
|  |                 response.status = response.status_code | ||||||
|  |                 response.content_type = response.headers.get("content-type") | ||||||
|  |  | ||||||
|                 if raw_cookies: |                 if raw_cookies: | ||||||
|                     response.raw_cookies = {} |                     response.raw_cookies = {} | ||||||
|  |  | ||||||
| @@ -95,7 +93,7 @@ class SanicTestClient: | |||||||
|  |  | ||||||
|         if self.port: |         if self.port: | ||||||
|             server_kwargs = dict( |             server_kwargs = dict( | ||||||
|                 host=host or self.host, port=self.port, **server_kwargs, |                 host=host or self.host, port=self.port, **server_kwargs | ||||||
|             ) |             ) | ||||||
|             host, port = host or self.host, self.port |             host, port = host or self.host, self.port | ||||||
|         else: |         else: | ||||||
| @@ -103,19 +101,17 @@ class SanicTestClient: | |||||||
|             sock.bind((host or self.host, 0)) |             sock.bind((host or self.host, 0)) | ||||||
|             server_kwargs = dict(sock=sock, **server_kwargs) |             server_kwargs = dict(sock=sock, **server_kwargs) | ||||||
|             host, port = sock.getsockname() |             host, port = sock.getsockname() | ||||||
|             self.port = port |  | ||||||
|  |  | ||||||
|         if uri.startswith( |         if uri.startswith( | ||||||
|             ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:") |             ("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:") | ||||||
|         ): |         ): | ||||||
|             url = uri |             url = uri | ||||||
|         else: |         else: | ||||||
|             uri = uri if uri.startswith("/") else f"/{uri}" |             uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri) | ||||||
|             scheme = "ws" if method == "websocket" else "http" |             scheme = "ws" if method == "websocket" else "http" | ||||||
|             url = f"{scheme}://{host}:{port}{uri}" |             url = "{scheme}://{host}:{port}{uri}".format( | ||||||
|         # Tests construct URLs using PORT = None, which means random port not |                 scheme=scheme, host=host, port=port, uri=uri | ||||||
|         # known until this function is called, so fix that here |             ) | ||||||
|         url = url.replace(":None/", f":{port}/") |  | ||||||
|  |  | ||||||
|         @self.app.listener("after_server_start") |         @self.app.listener("after_server_start") | ||||||
|         async def _collect_response(sanic, loop): |         async def _collect_response(sanic, loop): | ||||||
| @@ -133,7 +129,7 @@ class SanicTestClient: | |||||||
|         self.app.listeners["after_server_start"].pop() |         self.app.listeners["after_server_start"].pop() | ||||||
|  |  | ||||||
|         if exceptions: |         if exceptions: | ||||||
|             raise ValueError(f"Exception during request: {exceptions}") |             raise ValueError("Exception during request: {}".format(exceptions)) | ||||||
|  |  | ||||||
|         if gather_request: |         if gather_request: | ||||||
|             try: |             try: | ||||||
| @@ -141,13 +137,17 @@ class SanicTestClient: | |||||||
|                 return request, response |                 return request, response | ||||||
|             except BaseException:  # noqa |             except BaseException:  # noqa | ||||||
|                 raise ValueError( |                 raise ValueError( | ||||||
|                     f"Request and response object expected, got ({results})" |                     "Request and response object expected, got ({})".format( | ||||||
|  |                         results | ||||||
|  |                     ) | ||||||
|                 ) |                 ) | ||||||
|         else: |         else: | ||||||
|             try: |             try: | ||||||
|                 return results[-1] |                 return results[-1] | ||||||
|             except BaseException:  # noqa |             except BaseException:  # noqa | ||||||
|                 raise ValueError(f"Request object expected, got ({results})") |                 raise ValueError( | ||||||
|  |                     "Request object expected, got ({})".format(results) | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|     def get(self, *args, **kwargs): |     def get(self, *args, **kwargs): | ||||||
|         return self._sanic_endpoint_test("get", *args, **kwargs) |         return self._sanic_endpoint_test("get", *args, **kwargs) | ||||||
| @@ -185,15 +185,15 @@ async def app_call_with_return(self, scope, receive, send): | |||||||
|     return await asgi_app() |     return await asgi_app() | ||||||
|  |  | ||||||
|  |  | ||||||
| class SanicASGIDispatch(httpx.ASGIDispatch): | class SanicASGIDispatch(httpx.dispatch.ASGIDispatch): | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class SanicASGITestClient(httpx.AsyncClient): | class SanicASGITestClient(httpx.Client): | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         app, |         app, | ||||||
|         base_url: str = f"http://{ASGI_HOST}", |         base_url: str = "http://{}".format(ASGI_HOST), | ||||||
|         suppress_exceptions: bool = False, |         suppress_exceptions: bool = False, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         app.__class__.__call__ = app_call_with_return |         app.__class__.__call__ = app_call_with_return | ||||||
| @@ -201,7 +201,7 @@ class SanicASGITestClient(httpx.AsyncClient): | |||||||
|  |  | ||||||
|         self.app = app |         self.app = app | ||||||
|  |  | ||||||
|         dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT or 0)) |         dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT)) | ||||||
|         super().__init__(dispatch=dispatch, base_url=base_url) |         super().__init__(dispatch=dispatch, base_url=base_url) | ||||||
|  |  | ||||||
|         self.last_request = None |         self.last_request = None | ||||||
| @@ -224,7 +224,7 @@ class SanicASGITestClient(httpx.AsyncClient): | |||||||
|     async def websocket(self, uri, subprotocols=None, *args, **kwargs): |     async def websocket(self, uri, subprotocols=None, *args, **kwargs): | ||||||
|         scheme = "ws" |         scheme = "ws" | ||||||
|         path = uri |         path = uri | ||||||
|         root_path = f"{scheme}://{ASGI_HOST}" |         root_path = "{}://{}".format(scheme, ASGI_HOST) | ||||||
|  |  | ||||||
|         headers = kwargs.get("headers", {}) |         headers = kwargs.get("headers", {}) | ||||||
|         headers.setdefault("connection", "upgrade") |         headers.setdefault("connection", "upgrade") | ||||||
|   | |||||||
| @@ -96,10 +96,14 @@ class CompositionView: | |||||||
|             handler.is_stream = stream |             handler.is_stream = stream | ||||||
|         for method in methods: |         for method in methods: | ||||||
|             if method not in HTTP_METHODS: |             if method not in HTTP_METHODS: | ||||||
|                 raise InvalidUsage(f"{method} is not a valid HTTP method.") |                 raise InvalidUsage( | ||||||
|  |                     "{} is not a valid HTTP method.".format(method) | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|             if method in self.handlers: |             if method in self.handlers: | ||||||
|                 raise InvalidUsage(f"Method {method} is already registered.") |                 raise InvalidUsage( | ||||||
|  |                     "Method {} is already registered.".format(method) | ||||||
|  |                 ) | ||||||
|             self.handlers[method] = handler |             self.handlers[method] = handler | ||||||
|  |  | ||||||
|     def __call__(self, request, *args, **kwargs): |     def __call__(self, request, *args, **kwargs): | ||||||
|   | |||||||
| @@ -113,7 +113,7 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|  |  | ||||||
|         # hook up the websocket protocol |         # hook up the websocket protocol | ||||||
|         self.websocket = WebSocketCommonProtocol( |         self.websocket = WebSocketCommonProtocol( | ||||||
|             close_timeout=self.websocket_timeout, |             timeout=self.websocket_timeout, | ||||||
|             max_size=self.websocket_max_size, |             max_size=self.websocket_max_size, | ||||||
|             max_queue=self.websocket_max_queue, |             max_queue=self.websocket_max_queue, | ||||||
|             read_limit=self.websocket_read_limit, |             read_limit=self.websocket_read_limit, | ||||||
|   | |||||||
| @@ -174,7 +174,7 @@ class GunicornWorker(base.Worker): | |||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _create_ssl_context(cfg): |     def _create_ssl_context(cfg): | ||||||
|         """ Creates SSLContext instance for usage in asyncio.create_server. |         """Creates SSLContext instance for usage in asyncio.create_server. | ||||||
|         See ssl.SSLSocket.__init__ for more details. |         See ssl.SSLSocket.__init__ for more details. | ||||||
|         """ |         """ | ||||||
|         ctx = ssl.SSLContext(cfg.ssl_version) |         ctx = ssl.SSLContext(cfg.ssl_version) | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								setup.py
									
									
									
									
									
								
							| @@ -65,9 +65,7 @@ setup_kwargs = { | |||||||
|         "License :: OSI Approved :: MIT License", |         "License :: OSI Approved :: MIT License", | ||||||
|         "Programming Language :: Python :: 3.6", |         "Programming Language :: Python :: 3.6", | ||||||
|         "Programming Language :: Python :: 3.7", |         "Programming Language :: Python :: 3.7", | ||||||
|         "Programming Language :: Python :: 3.8", |  | ||||||
|     ], |     ], | ||||||
|     "entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]}, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | ||||||
| @@ -79,14 +77,14 @@ requirements = [ | |||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
|     "aiofiles>=0.3.0", |     "aiofiles>=0.3.0", | ||||||
|     "websockets>=8.1,<9.0", |     "websockets>=7.0,<9.0", | ||||||
|     "multidict>=4.0,<5.0", |     "multidict==5.0.0", | ||||||
|     "httpx==0.11.1", |     "httpx==0.9.3", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| tests_require = [ | tests_require = [ | ||||||
|     "pytest==5.2.1", |     "pytest==5.2.1", | ||||||
|     "multidict>=4.0,<5.0", |     "multidict==5.0.0", | ||||||
|     "gunicorn", |     "gunicorn", | ||||||
|     "pytest-cov", |     "pytest-cov", | ||||||
|     "httpcore==0.3.0", |     "httpcore==0.3.0", | ||||||
|   | |||||||
| @@ -103,7 +103,7 @@ def sanic_router(): | |||||||
|         for method, route in route_details: |         for method, route in route_details: | ||||||
|             try: |             try: | ||||||
|                 router._add( |                 router._add( | ||||||
|                     uri=f"/{route}", |                     uri="/{}".format(route), | ||||||
|                     methods=frozenset({method}), |                     methods=frozenset({method}), | ||||||
|                     host="localhost", |                     host="localhost", | ||||||
|                     handler=_handler, |                     handler=_handler, | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ main = WSGIApplication( | |||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     import sys |     import sys | ||||||
|  |  | ||||||
|     from wsgiref.simple_server import make_server |     from wsgiref.simple_server import make_server | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ from inspect import isawaitable | |||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic import Sanic |  | ||||||
| from sanic.exceptions import SanicException | from sanic.exceptions import SanicException | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  |  | ||||||
| @@ -49,7 +48,6 @@ def test_asyncio_server_no_start_serving(app): | |||||||
|     if not uvloop_installed(): |     if not uvloop_installed(): | ||||||
|         loop = asyncio.get_event_loop() |         loop = asyncio.get_event_loop() | ||||||
|         asyncio_srv_coro = app.create_server( |         asyncio_srv_coro = app.create_server( | ||||||
|             port=43123, |  | ||||||
|             return_asyncio_server=True, |             return_asyncio_server=True, | ||||||
|             asyncio_server_kwargs=dict(start_serving=False), |             asyncio_server_kwargs=dict(start_serving=False), | ||||||
|         ) |         ) | ||||||
| @@ -64,7 +62,6 @@ def test_asyncio_server_start_serving(app): | |||||||
|     if not uvloop_installed(): |     if not uvloop_installed(): | ||||||
|         loop = asyncio.get_event_loop() |         loop = asyncio.get_event_loop() | ||||||
|         asyncio_srv_coro = app.create_server( |         asyncio_srv_coro = app.create_server( | ||||||
|             port=43124, |  | ||||||
|             return_asyncio_server=True, |             return_asyncio_server=True, | ||||||
|             asyncio_server_kwargs=dict(start_serving=False), |             asyncio_server_kwargs=dict(start_serving=False), | ||||||
|         ) |         ) | ||||||
| @@ -72,8 +69,7 @@ def test_asyncio_server_start_serving(app): | |||||||
|         assert srv.is_serving() is False |         assert srv.is_serving() is False | ||||||
|         loop.run_until_complete(srv.start_serving()) |         loop.run_until_complete(srv.start_serving()) | ||||||
|         assert srv.is_serving() is True |         assert srv.is_serving() is True | ||||||
|         wait_close = srv.close() |         srv.close() | ||||||
|         loop.run_until_complete(wait_close) |  | ||||||
|         # Looks like we can't easily test `serve_forever()` |         # Looks like we can't easily test `serve_forever()` | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -128,8 +124,8 @@ def test_app_handle_request_handler_is_none(app, monkeypatch): | |||||||
|     request, response = app.test_client.get("/test") |     request, response = app.test_client.get("/test") | ||||||
|  |  | ||||||
|     assert ( |     assert ( | ||||||
|         "'None' was returned while requesting a handler from the router" |         response.text | ||||||
|         in response.text |         == "Error: 'None' was returned while requesting a handler from the router" | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -188,7 +184,9 @@ def test_handle_request_with_nested_exception_debug(app, monkeypatch): | |||||||
|     request, response = app.test_client.get("/", debug=True) |     request, response = app.test_client.get("/", debug=True) | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|     assert response.text.startswith( |     assert response.text.startswith( | ||||||
|         f"Error while handling error: {err_msg}\nStack: Traceback (most recent call last):\n" |         "Error while handling error: {}\nStack: Traceback (most recent call last):\n".format( | ||||||
|  |             err_msg | ||||||
|  |         ) | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -208,17 +206,10 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog): | |||||||
|  |  | ||||||
|     with caplog.at_level(logging.ERROR): |     with caplog.at_level(logging.ERROR): | ||||||
|         request, response = app.test_client.get("/") |         request, response = app.test_client.get("/") | ||||||
|     port = request.server_port |  | ||||||
|     assert port > 0 |  | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|     assert "Mock SanicException" in response.text |     assert response.text == "Error: Mock SanicException" | ||||||
|     assert ( |     assert ( | ||||||
|         "sanic.root", |         "sanic.root", | ||||||
|         logging.ERROR, |         logging.ERROR, | ||||||
|         f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'", |         "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", | ||||||
|     ) in caplog.record_tuples |     ) in caplog.record_tuples | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_name_required(): |  | ||||||
|     with pytest.deprecated_call(): |  | ||||||
|         Sanic() |  | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import sys |  | ||||||
|  |  | ||||||
| from collections import deque, namedtuple | from collections import deque, namedtuple | ||||||
|  |  | ||||||
| @@ -82,12 +81,7 @@ def test_listeners_triggered(app): | |||||||
|     with pytest.warns(UserWarning): |     with pytest.warns(UserWarning): | ||||||
|         server.run() |         server.run() | ||||||
|  |  | ||||||
|     all_tasks = ( |     for task in asyncio.Task.all_tasks(): | ||||||
|         asyncio.Task.all_tasks() |  | ||||||
|         if sys.version_info < (3, 7) |  | ||||||
|         else asyncio.all_tasks(asyncio.get_event_loop()) |  | ||||||
|     ) |  | ||||||
|     for task in all_tasks: |  | ||||||
|         task.cancel() |         task.cancel() | ||||||
|  |  | ||||||
|     assert before_server_start |     assert before_server_start | ||||||
| @@ -132,12 +126,7 @@ def test_listeners_triggered_async(app): | |||||||
|     with pytest.warns(UserWarning): |     with pytest.warns(UserWarning): | ||||||
|         server.run() |         server.run() | ||||||
|  |  | ||||||
|     all_tasks = ( |     for task in asyncio.Task.all_tasks(): | ||||||
|         asyncio.Task.all_tasks() |  | ||||||
|         if sys.version_info < (3, 7) |  | ||||||
|         else asyncio.all_tasks(asyncio.get_event_loop()) |  | ||||||
|     ) |  | ||||||
|     for task in all_tasks: |  | ||||||
|         task.cancel() |         task.cancel() | ||||||
|  |  | ||||||
|     assert before_server_start |     assert before_server_start | ||||||
| @@ -232,7 +221,7 @@ async def test_request_class_custom(): | |||||||
|     class MyCustomRequest(Request): |     class MyCustomRequest(Request): | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     app = Sanic(name=__name__, request_class=MyCustomRequest) |     app = Sanic(request_class=MyCustomRequest) | ||||||
|  |  | ||||||
|     @app.get("/custom") |     @app.get("/custom") | ||||||
|     def custom_request(request): |     def custom_request(request): | ||||||
|   | |||||||
| @@ -18,4 +18,4 @@ def test_bad_request_response(app): | |||||||
|  |  | ||||||
|     app.run(host="127.0.0.1", port=42101, debug=False) |     app.run(host="127.0.0.1", port=42101, debug=False) | ||||||
|     assert lines[0] == b"HTTP/1.1 400 Bad Request\r\n" |     assert lines[0] == b"HTTP/1.1 400 Bad Request\r\n" | ||||||
|     assert b"Bad Request" in lines[-1] |     assert lines[-1] == b"Error: Bad Request" | ||||||
|   | |||||||
| @@ -40,9 +40,9 @@ def test_bp_group_with_additional_route_params(app: Sanic): | |||||||
|     ) |     ) | ||||||
|     def blueprint_2_named_method(request: Request, param): |     def blueprint_2_named_method(request: Request, param): | ||||||
|         if request.method == "DELETE": |         if request.method == "DELETE": | ||||||
|             return text(f"DELETE_{param}") |             return text("DELETE_{}".format(param)) | ||||||
|         elif request.method == "PATCH": |         elif request.method == "PATCH": | ||||||
|             return text(f"PATCH_{param}") |             return text("PATCH_{}".format(param)) | ||||||
|  |  | ||||||
|     blueprint_group = Blueprint.group( |     blueprint_group = Blueprint.group( | ||||||
|         blueprint_1, blueprint_2, url_prefix="/api" |         blueprint_1, blueprint_2, url_prefix="/api" | ||||||
|   | |||||||
| @@ -46,19 +46,19 @@ def test_versioned_routes_get(app, method): | |||||||
|     func = getattr(bp, method) |     func = getattr(bp, method) | ||||||
|     if callable(func): |     if callable(func): | ||||||
|  |  | ||||||
|         @func(f"/{method}", version=1) |         @func("/{}".format(method), version=1) | ||||||
|         def handler(request): |         def handler(request): | ||||||
|             return text("OK") |             return text("OK") | ||||||
|  |  | ||||||
|     else: |     else: | ||||||
|         print(func) |         print(func) | ||||||
|         raise Exception(f"{func} is not callable") |         raise Exception("{} is not callable".format(func)) | ||||||
|  |  | ||||||
|     app.blueprint(bp) |     app.blueprint(bp) | ||||||
|  |  | ||||||
|     client_method = getattr(app.test_client, method) |     client_method = getattr(app.test_client, method) | ||||||
|  |  | ||||||
|     request, response = client_method(f"/v1/{method}") |     request, response = client_method("/v1/{}".format(method)) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -252,90 +252,8 @@ def test_several_bp_with_host(app): | |||||||
|     assert response.text == "Hello3" |     assert response.text == "Hello3" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_bp_with_host_list(app): |  | ||||||
|     bp = Blueprint( |  | ||||||
|         "test_bp_host", |  | ||||||
|         url_prefix="/test1", |  | ||||||
|         host=["example.com", "sub.example.com"], |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     @bp.route("/") |  | ||||||
|     def handler1(request): |  | ||||||
|         return text("Hello") |  | ||||||
|  |  | ||||||
|     @bp.route("/", host=["sub1.example.com"]) |  | ||||||
|     def handler2(request): |  | ||||||
|         return text("Hello subdomain!") |  | ||||||
|  |  | ||||||
|     app.blueprint(bp) |  | ||||||
|     headers = {"Host": "example.com"} |  | ||||||
|     request, response = app.test_client.get("/test1/", headers=headers) |  | ||||||
|     assert response.text == "Hello" |  | ||||||
|  |  | ||||||
|     headers = {"Host": "sub.example.com"} |  | ||||||
|     request, response = app.test_client.get("/test1/", headers=headers) |  | ||||||
|     assert response.text == "Hello" |  | ||||||
|  |  | ||||||
|     headers = {"Host": "sub1.example.com"} |  | ||||||
|     request, response = app.test_client.get("/test1/", headers=headers) |  | ||||||
|  |  | ||||||
|     assert response.text == "Hello subdomain!" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_several_bp_with_host_list(app): |  | ||||||
|     bp = Blueprint( |  | ||||||
|         "test_text", |  | ||||||
|         url_prefix="/test", |  | ||||||
|         host=["example.com", "sub.example.com"], |  | ||||||
|     ) |  | ||||||
|     bp2 = Blueprint( |  | ||||||
|         "test_text2", |  | ||||||
|         url_prefix="/test", |  | ||||||
|         host=["sub1.example.com", "sub2.example.com"], |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     @bp.route("/") |  | ||||||
|     def handler(request): |  | ||||||
|         return text("Hello") |  | ||||||
|  |  | ||||||
|     @bp2.route("/") |  | ||||||
|     def handler1(request): |  | ||||||
|         return text("Hello2") |  | ||||||
|  |  | ||||||
|     @bp2.route("/other/") |  | ||||||
|     def handler2(request): |  | ||||||
|         return text("Hello3") |  | ||||||
|  |  | ||||||
|     app.blueprint(bp) |  | ||||||
|     app.blueprint(bp2) |  | ||||||
|  |  | ||||||
|     assert bp.host == ["example.com", "sub.example.com"] |  | ||||||
|     headers = {"Host": "example.com"} |  | ||||||
|     request, response = app.test_client.get("/test/", headers=headers) |  | ||||||
|     assert response.text == "Hello" |  | ||||||
|  |  | ||||||
|     assert bp.host == ["example.com", "sub.example.com"] |  | ||||||
|     headers = {"Host": "sub.example.com"} |  | ||||||
|     request, response = app.test_client.get("/test/", headers=headers) |  | ||||||
|     assert response.text == "Hello" |  | ||||||
|  |  | ||||||
|     assert bp2.host == ["sub1.example.com", "sub2.example.com"] |  | ||||||
|     headers = {"Host": "sub1.example.com"} |  | ||||||
|     request, response = app.test_client.get("/test/", headers=headers) |  | ||||||
|     assert response.text == "Hello2" |  | ||||||
|     request, response = app.test_client.get("/test/other/", headers=headers) |  | ||||||
|     assert response.text == "Hello3" |  | ||||||
|  |  | ||||||
|     assert bp2.host == ["sub1.example.com", "sub2.example.com"] |  | ||||||
|     headers = {"Host": "sub2.example.com"} |  | ||||||
|     request, response = app.test_client.get("/test/", headers=headers) |  | ||||||
|     assert response.text == "Hello2" |  | ||||||
|     request, response = app.test_client.get("/test/other/", headers=headers) |  | ||||||
|     assert response.text == "Hello3" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_bp_middleware(app): | def test_bp_middleware(app): | ||||||
|     blueprint = Blueprint("test_bp_middleware") |     blueprint = Blueprint("test_middleware") | ||||||
|  |  | ||||||
|     @blueprint.middleware("response") |     @blueprint.middleware("response") | ||||||
|     async def process_response(request, response): |     async def process_response(request, response): | ||||||
| @@ -353,46 +271,6 @@ def test_bp_middleware(app): | |||||||
|     assert response.text == "FAIL" |     assert response.text == "FAIL" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_bp_middleware_order(app): |  | ||||||
|     blueprint = Blueprint("test_bp_middleware_order") |  | ||||||
|     order = list() |  | ||||||
|  |  | ||||||
|     @blueprint.middleware("request") |  | ||||||
|     def mw_1(request): |  | ||||||
|         order.append(1) |  | ||||||
|  |  | ||||||
|     @blueprint.middleware("request") |  | ||||||
|     def mw_2(request): |  | ||||||
|         order.append(2) |  | ||||||
|  |  | ||||||
|     @blueprint.middleware("request") |  | ||||||
|     def mw_3(request): |  | ||||||
|         order.append(3) |  | ||||||
|  |  | ||||||
|     @blueprint.middleware("response") |  | ||||||
|     def mw_4(request, response): |  | ||||||
|         order.append(6) |  | ||||||
|  |  | ||||||
|     @blueprint.middleware("response") |  | ||||||
|     def mw_5(request, response): |  | ||||||
|         order.append(5) |  | ||||||
|  |  | ||||||
|     @blueprint.middleware("response") |  | ||||||
|     def mw_6(request, response): |  | ||||||
|         order.append(4) |  | ||||||
|  |  | ||||||
|     @blueprint.route("/") |  | ||||||
|     def process_response(request): |  | ||||||
|         return text("OK") |  | ||||||
|  |  | ||||||
|     app.blueprint(blueprint) |  | ||||||
|     order.clear() |  | ||||||
|     request, response = app.test_client.get("/") |  | ||||||
|  |  | ||||||
|     assert response.status == 200 |  | ||||||
|     assert order == [1, 2, 3, 4, 5, 6] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_bp_exception_handler(app): | def test_bp_exception_handler(app): | ||||||
|     blueprint = Blueprint("test_middleware") |     blueprint = Blueprint("test_middleware") | ||||||
|  |  | ||||||
| @@ -675,7 +553,9 @@ def test_bp_group_with_default_url_prefix(app): | |||||||
|     from uuid import uuid4 |     from uuid import uuid4 | ||||||
|  |  | ||||||
|     resource_id = str(uuid4()) |     resource_id = str(uuid4()) | ||||||
|     request, response = app.test_client.get(f"/api/v1/resources/{resource_id}") |     request, response = app.test_client.get( | ||||||
|  |         "/api/v1/resources/{0}".format(resource_id) | ||||||
|  |     ) | ||||||
|     assert response.json == {"resource_id": resource_id} |     assert response.json == {"resource_id": resource_id} | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -789,9 +669,9 @@ def test_duplicate_blueprint(app): | |||||||
|         app.blueprint(bp1) |         app.blueprint(bp1) | ||||||
|  |  | ||||||
|     assert str(excinfo.value) == ( |     assert str(excinfo.value) == ( | ||||||
|         f'A blueprint with the name "{bp_name}" is already registered.  ' |         'A blueprint with the name "{}" is already registered.  ' | ||||||
|         "Blueprint names must be unique." |         "Blueprint names must be unique." | ||||||
|     ) |     ).format(bp_name) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("debug", [True, False, None]) | @pytest.mark.parametrize("debug", [True, False, None]) | ||||||
|   | |||||||
| @@ -44,42 +44,42 @@ def test_load_from_object_string_exception(app): | |||||||
|  |  | ||||||
| def test_auto_load_env(): | def test_auto_load_env(): | ||||||
|     environ["SANIC_TEST_ANSWER"] = "42" |     environ["SANIC_TEST_ANSWER"] = "42" | ||||||
|     app = Sanic(name=__name__) |     app = Sanic() | ||||||
|     assert app.config.TEST_ANSWER == 42 |     assert app.config.TEST_ANSWER == 42 | ||||||
|     del environ["SANIC_TEST_ANSWER"] |     del environ["SANIC_TEST_ANSWER"] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_auto_load_bool_env(): | def test_auto_load_bool_env(): | ||||||
|     environ["SANIC_TEST_ANSWER"] = "True" |     environ["SANIC_TEST_ANSWER"] = "True" | ||||||
|     app = Sanic(name=__name__) |     app = Sanic() | ||||||
|     assert app.config.TEST_ANSWER == True |     assert app.config.TEST_ANSWER == True | ||||||
|     del environ["SANIC_TEST_ANSWER"] |     del environ["SANIC_TEST_ANSWER"] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_dont_load_env(): | def test_dont_load_env(): | ||||||
|     environ["SANIC_TEST_ANSWER"] = "42" |     environ["SANIC_TEST_ANSWER"] = "42" | ||||||
|     app = Sanic(name=__name__, load_env=False) |     app = Sanic(load_env=False) | ||||||
|     assert getattr(app.config, "TEST_ANSWER", None) is None |     assert getattr(app.config, "TEST_ANSWER", None) is None | ||||||
|     del environ["SANIC_TEST_ANSWER"] |     del environ["SANIC_TEST_ANSWER"] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_load_env_prefix(): | def test_load_env_prefix(): | ||||||
|     environ["MYAPP_TEST_ANSWER"] = "42" |     environ["MYAPP_TEST_ANSWER"] = "42" | ||||||
|     app = Sanic(name=__name__, load_env="MYAPP_") |     app = Sanic(load_env="MYAPP_") | ||||||
|     assert app.config.TEST_ANSWER == 42 |     assert app.config.TEST_ANSWER == 42 | ||||||
|     del environ["MYAPP_TEST_ANSWER"] |     del environ["MYAPP_TEST_ANSWER"] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_load_env_prefix_float_values(): | def test_load_env_prefix_float_values(): | ||||||
|     environ["MYAPP_TEST_ROI"] = "2.3" |     environ["MYAPP_TEST_ROI"] = "2.3" | ||||||
|     app = Sanic(name=__name__, load_env="MYAPP_") |     app = Sanic(load_env="MYAPP_") | ||||||
|     assert app.config.TEST_ROI == 2.3 |     assert app.config.TEST_ROI == 2.3 | ||||||
|     del environ["MYAPP_TEST_ROI"] |     del environ["MYAPP_TEST_ROI"] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_load_env_prefix_string_value(): | def test_load_env_prefix_string_value(): | ||||||
|     environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken" |     environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken" | ||||||
|     app = Sanic(name=__name__, load_env="MYAPP_") |     app = Sanic(load_env="MYAPP_") | ||||||
|     assert app.config.TEST_TOKEN == "somerandomtesttoken" |     assert app.config.TEST_TOKEN == "somerandomtesttoken" | ||||||
|     del environ["MYAPP_TEST_TOKEN"] |     del environ["MYAPP_TEST_TOKEN"] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,8 +15,7 @@ from sanic.response import text | |||||||
| def test_cookies(app): | def test_cookies(app): | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     def handler(request): |     def handler(request): | ||||||
|         cookie_value = request.cookies["test"] |         response = text("Cookies are: {}".format(request.cookies["test"])) | ||||||
|         response = text(f"Cookies are: {cookie_value}") |  | ||||||
|         response.cookies["right_back"] = "at you" |         response.cookies["right_back"] = "at you" | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
| @@ -32,8 +31,7 @@ def test_cookies(app): | |||||||
| async def test_cookies_asgi(app): | async def test_cookies_asgi(app): | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     def handler(request): |     def handler(request): | ||||||
|         cookie_value = request.cookies["test"] |         response = text("Cookies are: {}".format(request.cookies["test"])) | ||||||
|         response = text(f"Cookies are: {cookie_value}") |  | ||||||
|         response.cookies["right_back"] = "at you" |         response.cookies["right_back"] = "at you" | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
| @@ -54,7 +52,7 @@ def test_false_cookies_encoded(app, httponly, expected): | |||||||
|         response = text("hello cookies") |         response = text("hello cookies") | ||||||
|         response.cookies["hello"] = "world" |         response.cookies["hello"] = "world" | ||||||
|         response.cookies["hello"]["httponly"] = httponly |         response.cookies["hello"]["httponly"] = httponly | ||||||
|         return text(response.cookies["hello"].encode("utf8").decode()) |         return text(response.cookies["hello"].encode("utf8")) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/") |     request, response = app.test_client.get("/") | ||||||
|  |  | ||||||
| @@ -80,8 +78,7 @@ def test_false_cookies(app, httponly, expected): | |||||||
| def test_http2_cookies(app): | def test_http2_cookies(app): | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     async def handler(request): |     async def handler(request): | ||||||
|         cookie_value = request.cookies["test"] |         response = text("Cookies are: {}".format(request.cookies["test"])) | ||||||
|         response = text(f"Cookies are: {cookie_value}") |  | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|     headers = {"cookie": "test=working!"} |     headers = {"cookie": "test=working!"} | ||||||
|   | |||||||
| @@ -17,12 +17,12 @@ def test_create_task(app): | |||||||
|  |  | ||||||
|     @app.route("/early") |     @app.route("/early") | ||||||
|     def not_set(request): |     def not_set(request): | ||||||
|         return text(str(e.is_set())) |         return text(e.is_set()) | ||||||
|  |  | ||||||
|     @app.route("/late") |     @app.route("/late") | ||||||
|     async def set(request): |     async def set(request): | ||||||
|         await asyncio.sleep(0.1) |         await asyncio.sleep(0.1) | ||||||
|         return text(str(e.is_set())) |         return text(e.is_set()) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/early") |     request, response = app.test_client.get("/early") | ||||||
|     assert response.body == b"False" |     assert response.body == b"False" | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ class CustomRequest(Request): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_custom_request(): | def test_custom_request(): | ||||||
|     app = Sanic(name=__name__, request_class=CustomRequest) |     app = Sanic(request_class=CustomRequest) | ||||||
|  |  | ||||||
|     @app.route("/post", methods=["POST"]) |     @app.route("/post", methods=["POST"]) | ||||||
|     async def post_handler(request): |     async def post_handler(request): | ||||||
|   | |||||||
| @@ -172,7 +172,7 @@ def test_handled_unhandled_exception(exception_app): | |||||||
|     request, response = exception_app.test_client.get("/divide_by_zero") |     request, response = exception_app.test_client.get("/divide_by_zero") | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|     soup = BeautifulSoup(response.body, "html.parser") |     soup = BeautifulSoup(response.body, "html.parser") | ||||||
|     assert "Internal Server Error" in soup.h1.text |     assert soup.h1.text == "Internal Server Error" | ||||||
|  |  | ||||||
|     message = " ".join(soup.p.text.split()) |     message = " ".join(soup.p.text.split()) | ||||||
|     assert message == ( |     assert message == ( | ||||||
| @@ -218,4 +218,4 @@ def test_abort(exception_app): | |||||||
|  |  | ||||||
|     request, response = exception_app.test_client.get("/abort/message") |     request, response = exception_app.test_client.get("/abort/message") | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|     assert "Abort" in response.text |     assert response.text == "Error: Abort" | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ def handler_6(request, arg): | |||||||
|     try: |     try: | ||||||
|         foo = 1 / arg |         foo = 1 / arg | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         raise e from ValueError(f"{arg}") |         raise e from ValueError("{}".format(arg)) | ||||||
|     return text(foo) |     return text(foo) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -86,7 +86,7 @@ def test_html_traceback_output_in_debug_mode(): | |||||||
|  |  | ||||||
|     summary_text = " ".join(soup.select(".summary")[0].text.split()) |     summary_text = " ".join(soup.select(".summary")[0].text.split()) | ||||||
|     assert ( |     assert ( | ||||||
|         "NameError: name 'bar' is not defined while handling path /4" |         "NameError: name 'bar' " "is not defined while handling path /4" | ||||||
|     ) == summary_text |     ) == summary_text | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -112,7 +112,7 @@ def test_chained_exception_handler(): | |||||||
|  |  | ||||||
|     summary_text = " ".join(soup.select(".summary")[0].text.split()) |     summary_text = " ".join(soup.select(".summary")[0].text.split()) | ||||||
|     assert ( |     assert ( | ||||||
|         "ZeroDivisionError: division by zero while handling path /6/0" |         "ZeroDivisionError: division by zero " "while handling path /6/0" | ||||||
|     ) == summary_text |     ) == summary_text | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,34 +7,17 @@ import httpx | |||||||
|  |  | ||||||
| from sanic import Sanic, server | from sanic import Sanic, server | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
| from sanic.testing import HOST, SanicTestClient | from sanic.testing import HOST, PORT, SanicTestClient | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | ||||||
|  |  | ||||||
| old_conn = None | old_conn = None | ||||||
| PORT = 42101  # test_keep_alive_timeout_reuse doesn't work with random port |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ReusableSanicConnectionPool( | class ReusableSanicConnectionPool( | ||||||
|     httpx.dispatch.connection_pool.ConnectionPool |     httpx.dispatch.connection_pool.ConnectionPool | ||||||
| ): | ): | ||||||
|     @property |  | ||||||
|     def cert(self): |  | ||||||
|         return self.ssl.cert |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def verify(self): |  | ||||||
|         return self.ssl.verify |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def trust_env(self): |  | ||||||
|         return self.ssl.trust_env |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def http2(self): |  | ||||||
|         return self.ssl.http2 |  | ||||||
|  |  | ||||||
|     async def acquire_connection(self, origin, timeout): |     async def acquire_connection(self, origin, timeout): | ||||||
|         global old_conn |         global old_conn | ||||||
|         connection = self.pop_connection(origin) |         connection = self.pop_connection(origin) | ||||||
| @@ -43,17 +26,14 @@ class ReusableSanicConnectionPool( | |||||||
|             pool_timeout = None if timeout is None else timeout.pool_timeout |             pool_timeout = None if timeout is None else timeout.pool_timeout | ||||||
|  |  | ||||||
|             await self.max_connections.acquire(timeout=pool_timeout) |             await self.max_connections.acquire(timeout=pool_timeout) | ||||||
|             ssl_config = httpx.config.SSLConfig( |  | ||||||
|                 cert=self.cert, |  | ||||||
|                 verify=self.verify, |  | ||||||
|                 trust_env=self.trust_env, |  | ||||||
|                 http2=self.http2, |  | ||||||
|             ) |  | ||||||
|             connection = httpx.dispatch.connection.HTTPConnection( |             connection = httpx.dispatch.connection.HTTPConnection( | ||||||
|                 origin, |                 origin, | ||||||
|                 ssl=ssl_config, |                 verify=self.verify, | ||||||
|  |                 cert=self.cert, | ||||||
|  |                 http2=self.http2, | ||||||
|                 backend=self.backend, |                 backend=self.backend, | ||||||
|                 release_func=self.release_connection, |                 release_func=self.release_connection, | ||||||
|  |                 trust_env=self.trust_env, | ||||||
|                 uds=self.uds, |                 uds=self.uds, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
| @@ -69,7 +49,7 @@ class ReusableSanicConnectionPool( | |||||||
|         return connection |         return connection | ||||||
|  |  | ||||||
|  |  | ||||||
| class ResusableSanicSession(httpx.AsyncClient): | class ResusableSanicSession(httpx.Client): | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         dispatch = ReusableSanicConnectionPool() |         dispatch = ReusableSanicConnectionPool() | ||||||
|         super().__init__(dispatch=dispatch, *args, **kwargs) |         super().__init__(dispatch=dispatch, *args, **kwargs) | ||||||
| @@ -117,9 +97,11 @@ class ReuseableSanicTestClient(SanicTestClient): | |||||||
|         ): |         ): | ||||||
|             url = uri |             url = uri | ||||||
|         else: |         else: | ||||||
|             uri = uri if uri.startswith("/") else f"/{uri}" |             uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri) | ||||||
|             scheme = "http" |             scheme = "http" | ||||||
|             url = f"{scheme}://{HOST}:{PORT}{uri}" |             url = "{scheme}://{host}:{port}{uri}".format( | ||||||
|  |                 scheme=scheme, host=HOST, port=PORT, uri=uri | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         @self.app.listener("after_server_start") |         @self.app.listener("after_server_start") | ||||||
|         async def _collect_response(loop): |         async def _collect_response(loop): | ||||||
| @@ -152,7 +134,7 @@ class ReuseableSanicTestClient(SanicTestClient): | |||||||
|         self.app.listeners["after_server_start"].pop() |         self.app.listeners["after_server_start"].pop() | ||||||
|  |  | ||||||
|         if exceptions: |         if exceptions: | ||||||
|             raise ValueError(f"Exception during request: {exceptions}") |             raise ValueError("Exception during request: {}".format(exceptions)) | ||||||
|  |  | ||||||
|         if gather_request: |         if gather_request: | ||||||
|             self.app.request_middleware.pop() |             self.app.request_middleware.pop() | ||||||
| @@ -161,13 +143,17 @@ class ReuseableSanicTestClient(SanicTestClient): | |||||||
|                 return request, response |                 return request, response | ||||||
|             except Exception: |             except Exception: | ||||||
|                 raise ValueError( |                 raise ValueError( | ||||||
|                     f"Request and response object expected, got ({results})" |                     "Request and response object expected, got ({})".format( | ||||||
|  |                         results | ||||||
|  |                     ) | ||||||
|                 ) |                 ) | ||||||
|         else: |         else: | ||||||
|             try: |             try: | ||||||
|                 return results[-1] |                 return results[-1] | ||||||
|             except Exception: |             except Exception: | ||||||
|                 raise ValueError(f"Request object expected, got ({results})") |                 raise ValueError( | ||||||
|  |                     "Request object expected, got ({})".format(results) | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|     def kill_server(self): |     def kill_server(self): | ||||||
|         try: |         try: | ||||||
| @@ -177,7 +163,7 @@ class ReuseableSanicTestClient(SanicTestClient): | |||||||
|                 self._server = None |                 self._server = None | ||||||
|  |  | ||||||
|             if self._session: |             if self._session: | ||||||
|                 self._loop.run_until_complete(self._session.aclose()) |                 self._loop.run_until_complete(self._session.close()) | ||||||
|                 self._session = None |                 self._session = None | ||||||
|  |  | ||||||
|         except Exception as e3: |         except Exception as e3: | ||||||
| @@ -196,7 +182,7 @@ class ReuseableSanicTestClient(SanicTestClient): | |||||||
|             self._session = self.get_new_session() |             self._session = self.get_new_session() | ||||||
|         try: |         try: | ||||||
|             response = await getattr(self._session, method.lower())( |             response = await getattr(self._session, method.lower())( | ||||||
|                 url, timeout=request_keepalive, *args, **kwargs |                 url, verify=False, timeout=request_keepalive, *args, **kwargs | ||||||
|             ) |             ) | ||||||
|         except NameError: |         except NameError: | ||||||
|             raise Exception(response.status_code) |             raise Exception(response.status_code) | ||||||
| @@ -206,7 +192,7 @@ class ReuseableSanicTestClient(SanicTestClient): | |||||||
|         except (JSONDecodeError, UnicodeDecodeError): |         except (JSONDecodeError, UnicodeDecodeError): | ||||||
|             response.json = None |             response.json = None | ||||||
|  |  | ||||||
|         response.body = await response.aread() |         response.body = await response.read() | ||||||
|         response.status = response.status_code |         response.status = response.status_code | ||||||
|         response.content_type = response.headers.get("content-type") |         response.content_type = response.headers.get("content-type") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import logging | import logging | ||||||
| import os |  | ||||||
| import uuid | import uuid | ||||||
|  |  | ||||||
| from importlib import reload | from importlib import reload | ||||||
| @@ -13,7 +12,6 @@ import sanic | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.log import LOGGING_CONFIG_DEFAULTS, logger | from sanic.log import LOGGING_CONFIG_DEFAULTS, logger | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
| from sanic.testing import SanicTestClient |  | ||||||
|  |  | ||||||
|  |  | ||||||
| logging_format = """module: %(module)s; \ | logging_format = """module: %(module)s; \ | ||||||
| @@ -129,7 +127,7 @@ def test_log_connection_lost(app, debug, monkeypatch): | |||||||
| def test_logger(caplog): | def test_logger(caplog): | ||||||
|     rand_string = str(uuid.uuid4()) |     rand_string = str(uuid.uuid4()) | ||||||
|  |  | ||||||
|     app = Sanic(name=__name__) |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.get("/") |     @app.get("/") | ||||||
|     def log_info(request): |     def log_info(request): | ||||||
| @@ -139,67 +137,15 @@ def test_logger(caplog): | |||||||
|     with caplog.at_level(logging.INFO): |     with caplog.at_level(logging.INFO): | ||||||
|         request, response = app.test_client.get("/") |         request, response = app.test_client.get("/") | ||||||
|  |  | ||||||
|     port = request.server_port |  | ||||||
|  |  | ||||||
|     # Note: testing with random port doesn't show the banner because it doesn't |  | ||||||
|     # define host and port. This test supports both modes. |  | ||||||
|     if caplog.record_tuples[0] == ( |  | ||||||
|         "sanic.root", |  | ||||||
|         logging.INFO, |  | ||||||
|         f"Goin' Fast @ http://127.0.0.1:{port}", |  | ||||||
|     ): |  | ||||||
|         caplog.record_tuples.pop(0) |  | ||||||
|  |  | ||||||
|     assert caplog.record_tuples[0] == ( |     assert caplog.record_tuples[0] == ( | ||||||
|         "sanic.root", |         "sanic.root", | ||||||
|         logging.INFO, |         logging.INFO, | ||||||
|         f"http://127.0.0.1:{port}/", |         "Goin' Fast @ http://127.0.0.1:42101", | ||||||
|     ) |  | ||||||
|     assert caplog.record_tuples[1] == ("sanic.root", logging.INFO, rand_string) |  | ||||||
|     assert caplog.record_tuples[-1] == ( |  | ||||||
|         "sanic.root", |  | ||||||
|         logging.INFO, |  | ||||||
|         "Server Stopped", |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_logger_static_and_secure(caplog): |  | ||||||
|     # Same as test_logger, except for more coverage: |  | ||||||
|     # - test_client initialised separately for static port |  | ||||||
|     # - using ssl |  | ||||||
|     rand_string = str(uuid.uuid4()) |  | ||||||
|  |  | ||||||
|     app = Sanic(name=__name__) |  | ||||||
|  |  | ||||||
|     @app.get("/") |  | ||||||
|     def log_info(request): |  | ||||||
|         logger.info(rand_string) |  | ||||||
|         return text("hello") |  | ||||||
|  |  | ||||||
|     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} |  | ||||||
|  |  | ||||||
|     test_client = SanicTestClient(app, port=42101) |  | ||||||
|     with caplog.at_level(logging.INFO): |  | ||||||
|         request, response = test_client.get( |  | ||||||
|             f"https://127.0.0.1:{test_client.port}/", |  | ||||||
|             server_kwargs=dict(ssl=ssl_dict), |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     port = test_client.port |  | ||||||
|  |  | ||||||
|     assert caplog.record_tuples[0] == ( |  | ||||||
|         "sanic.root", |  | ||||||
|         logging.INFO, |  | ||||||
|         f"Goin' Fast @ https://127.0.0.1:{port}", |  | ||||||
|     ) |     ) | ||||||
|     assert caplog.record_tuples[1] == ( |     assert caplog.record_tuples[1] == ( | ||||||
|         "sanic.root", |         "sanic.root", | ||||||
|         logging.INFO, |         logging.INFO, | ||||||
|         f"https://127.0.0.1:{port}/", |         "http://127.0.0.1:42101/", | ||||||
|     ) |     ) | ||||||
|     assert caplog.record_tuples[2] == ("sanic.root", logging.INFO, rand_string) |     assert caplog.record_tuples[2] == ("sanic.root", logging.INFO, rand_string) | ||||||
|     assert caplog.record_tuples[-1] == ( |     assert caplog.record_tuples[-1] == ( | ||||||
|   | |||||||
| @@ -49,10 +49,10 @@ def test_logo_false(app, caplog): | |||||||
|     loop.run_until_complete(_server.wait_closed()) |     loop.run_until_complete(_server.wait_closed()) | ||||||
|     app.stop() |     app.stop() | ||||||
|  |  | ||||||
|     banner, port = caplog.record_tuples[ROW][2].rsplit(":", 1) |  | ||||||
|     assert caplog.record_tuples[ROW][1] == logging.INFO |     assert caplog.record_tuples[ROW][1] == logging.INFO | ||||||
|     assert banner == "Goin' Fast @ http://127.0.0.1" |     assert caplog.record_tuples[ROW][ | ||||||
|     assert int(port) > 0 |         2 | ||||||
|  |     ] == "Goin' Fast @ http://127.0.0.1:{}".format(PORT) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_logo_true(app, caplog): | def test_logo_true(app, caplog): | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import logging | |||||||
|  |  | ||||||
| from asyncio import CancelledError | from asyncio import CancelledError | ||||||
|  |  | ||||||
| from sanic.exceptions import NotFound, SanicException | from sanic.exceptions import NotFound | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import HTTPResponse, text | from sanic.response import HTTPResponse, text | ||||||
|  |  | ||||||
| @@ -93,7 +93,7 @@ def test_middleware_response_raise_cancelled_error(app, caplog): | |||||||
|             "sanic.root", |             "sanic.root", | ||||||
|             logging.ERROR, |             logging.ERROR, | ||||||
|             "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", |             "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", | ||||||
|         ) not in caplog.record_tuples |         ) in caplog.record_tuples | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_middleware_response_raise_exception(app, caplog): | def test_middleware_response_raise_exception(app, caplog): | ||||||
| @@ -102,16 +102,14 @@ def test_middleware_response_raise_exception(app, caplog): | |||||||
|         raise Exception("Exception at response middleware") |         raise Exception("Exception at response middleware") | ||||||
|  |  | ||||||
|     with caplog.at_level(logging.ERROR): |     with caplog.at_level(logging.ERROR): | ||||||
|         reqrequest, response = app.test_client.get("/fail") |         reqrequest, response = app.test_client.get("/") | ||||||
|  |  | ||||||
|     assert response.status == 404 |     assert response.status == 404 | ||||||
|     # 404 errors are not logged |  | ||||||
|     assert ( |     assert ( | ||||||
|         "sanic.root", |         "sanic.root", | ||||||
|         logging.ERROR, |         logging.ERROR, | ||||||
|         "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", |         "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", | ||||||
|     ) not in caplog.record_tuples |     ) in caplog.record_tuples | ||||||
|     # Middleware exception ignored but logged |  | ||||||
|     assert ( |     assert ( | ||||||
|         "sanic.error", |         "sanic.error", | ||||||
|         logging.ERROR, |         logging.ERROR, | ||||||
|   | |||||||
| @@ -87,15 +87,3 @@ def test_pickle_app_with_bp(app, protocol): | |||||||
|     request, response = up_p_app.test_client.get("/") |     request, response = up_p_app.test_client.get("/") | ||||||
|     assert up_p_app.is_request_stream is False |     assert up_p_app.is_request_stream is False | ||||||
|     assert response.text == "Hello" |     assert response.text == "Hello" | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("protocol", [3, 4]) |  | ||||||
| def test_pickle_app_with_static(app, protocol): |  | ||||||
|     app.route("/")(handler) |  | ||||||
|     app.static("/static", "/tmp/static") |  | ||||||
|     p_app = pickle.dumps(app, protocol=protocol) |  | ||||||
|     del app |  | ||||||
|     up_p_app = pickle.loads(p_app) |  | ||||||
|     assert up_p_app |  | ||||||
|     request, response = up_p_app.test_client.get("/static/missing.txt") |  | ||||||
|     assert response.status == 404 |  | ||||||
|   | |||||||
| @@ -21,13 +21,13 @@ def test_versioned_named_routes_get(app, method): | |||||||
|     bp = Blueprint("test_bp", url_prefix="/bp") |     bp = Blueprint("test_bp", url_prefix="/bp") | ||||||
|  |  | ||||||
|     method = method.lower() |     method = method.lower() | ||||||
|     route_name = f"route_{method}" |     route_name = "route_{}".format(method) | ||||||
|     route_name2 = f"route2_{method}" |     route_name2 = "route2_{}".format(method) | ||||||
|  |  | ||||||
|     func = getattr(app, method) |     func = getattr(app, method) | ||||||
|     if callable(func): |     if callable(func): | ||||||
|  |  | ||||||
|         @func(f"/{method}", version=1, name=route_name) |         @func("/{}".format(method), version=1, name=route_name) | ||||||
|         def handler(request): |         def handler(request): | ||||||
|             return text("OK") |             return text("OK") | ||||||
|  |  | ||||||
| @@ -38,7 +38,7 @@ def test_versioned_named_routes_get(app, method): | |||||||
|     func = getattr(bp, method) |     func = getattr(bp, method) | ||||||
|     if callable(func): |     if callable(func): | ||||||
|  |  | ||||||
|         @func(f"/{method}", version=1, name=route_name2) |         @func("/{}".format(method), version=1, name=route_name2) | ||||||
|         def handler2(request): |         def handler2(request): | ||||||
|             return text("OK") |             return text("OK") | ||||||
|  |  | ||||||
| @@ -48,14 +48,14 @@ def test_versioned_named_routes_get(app, method): | |||||||
|  |  | ||||||
|     app.blueprint(bp) |     app.blueprint(bp) | ||||||
|  |  | ||||||
|     assert app.router.routes_all[f"/v1/{method}"].name == route_name |     assert app.router.routes_all["/v1/{}".format(method)].name == route_name | ||||||
|  |  | ||||||
|     route = app.router.routes_all[f"/v1/bp/{method}"] |     route = app.router.routes_all["/v1/bp/{}".format(method)] | ||||||
|     assert route.name == f"test_bp.{route_name2}" |     assert route.name == "test_bp.{}".format(route_name2) | ||||||
|  |  | ||||||
|     assert app.url_for(route_name) == f"/v1/{method}" |     assert app.url_for(route_name) == "/v1/{}".format(method) | ||||||
|     url = app.url_for(f"test_bp.{route_name2}") |     url = app.url_for("test_bp.{}".format(route_name2)) | ||||||
|     assert url == f"/v1/bp/{method}" |     assert url == "/v1/bp/{}".format(method) | ||||||
|     with pytest.raises(URLBuildError): |     with pytest.raises(URLBuildError): | ||||||
|         app.url_for("handler") |         app.url_for("handler") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ def test_payload_too_large_at_data_received_default(app): | |||||||
|  |  | ||||||
|     response = app.test_client.get("/1", gather_request=False) |     response = app.test_client.get("/1", gather_request=False) | ||||||
|     assert response.status == 413 |     assert response.status == 413 | ||||||
|     assert "Payload Too Large" in response.text |     assert response.text == "Error: Payload Too Large" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_payload_too_large_at_on_header_default(app): | def test_payload_too_large_at_on_header_default(app): | ||||||
| @@ -40,4 +40,4 @@ def test_payload_too_large_at_on_header_default(app): | |||||||
|     data = "a" * 1000 |     data = "a" * 1000 | ||||||
|     response = app.test_client.post("/1", gather_request=False, data=data) |     response = app.test_client.post("/1", gather_request=False, data=data) | ||||||
|     assert response.status == 413 |     assert response.status == 413 | ||||||
|     assert "Payload Too Large" in response.text |     assert response.text == "Error: Payload Too Large" | ||||||
|   | |||||||
| @@ -115,14 +115,14 @@ def test_redirect_with_params(app, test_str): | |||||||
|  |  | ||||||
|     @app.route("/api/v1/test/<test>/") |     @app.route("/api/v1/test/<test>/") | ||||||
|     async def init_handler(request, test): |     async def init_handler(request, test): | ||||||
|         return redirect(f"/api/v2/test/{use_in_uri}/") |         return redirect("/api/v2/test/{}/".format(use_in_uri)) | ||||||
|  |  | ||||||
|     @app.route("/api/v2/test/<test>/") |     @app.route("/api/v2/test/<test>/") | ||||||
|     async def target_handler(request, test): |     async def target_handler(request, test): | ||||||
|         assert test == test_str |         assert test == test_str | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     _, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/") |     _, response = app.test_client.get("/api/v1/test/{}/".format(use_in_uri)) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
|     assert response.content == b"OK" |     assert response.content == b"OK" | ||||||
|   | |||||||
| @@ -1,108 +0,0 @@ | |||||||
| import os |  | ||||||
| import secrets |  | ||||||
| import sys |  | ||||||
|  |  | ||||||
| from contextlib import suppress |  | ||||||
| from subprocess import PIPE, Popen, TimeoutExpired |  | ||||||
| from tempfile import TemporaryDirectory |  | ||||||
| from textwrap import dedent |  | ||||||
| from threading import Timer |  | ||||||
| from time import sleep |  | ||||||
|  |  | ||||||
| import pytest |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # We need to interrupt the autoreloader without killing it, so that the server gets terminated |  | ||||||
| # https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/ |  | ||||||
|  |  | ||||||
| try: |  | ||||||
|     from signal import CTRL_BREAK_EVENT |  | ||||||
|     from subprocess import CREATE_NEW_PROCESS_GROUP |  | ||||||
|  |  | ||||||
|     flags = CREATE_NEW_PROCESS_GROUP |  | ||||||
| except ImportError: |  | ||||||
|     flags = 0 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def terminate(proc): |  | ||||||
|     if flags: |  | ||||||
|         proc.send_signal(CTRL_BREAK_EVENT) |  | ||||||
|     else: |  | ||||||
|         proc.terminate() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def write_app(filename, **runargs): |  | ||||||
|     text = secrets.token_urlsafe() |  | ||||||
|     with open(filename, "w") as f: |  | ||||||
|         f.write( |  | ||||||
|             dedent( |  | ||||||
|                 f"""\ |  | ||||||
|             import os |  | ||||||
|             from sanic import Sanic |  | ||||||
|  |  | ||||||
|             app = Sanic(__name__) |  | ||||||
|  |  | ||||||
|             @app.listener("after_server_start") |  | ||||||
|             def complete(*args): |  | ||||||
|                 print("complete", os.getpid(), {text!r}) |  | ||||||
|  |  | ||||||
|             if __name__ == "__main__": |  | ||||||
|                 app.run(**{runargs!r}) |  | ||||||
|             """ |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|     return text |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def scanner(proc): |  | ||||||
|     for line in proc.stdout: |  | ||||||
|         line = line.decode().strip() |  | ||||||
|         print(">", line) |  | ||||||
|         if line.startswith("complete"): |  | ||||||
|             yield line |  | ||||||
|  |  | ||||||
|  |  | ||||||
| argv = dict( |  | ||||||
|     script=[sys.executable, "reloader.py"], |  | ||||||
|     module=[sys.executable, "-m", "reloader"], |  | ||||||
|     sanic=[ |  | ||||||
|         sys.executable, |  | ||||||
|         "-m", |  | ||||||
|         "sanic", |  | ||||||
|         "--port", |  | ||||||
|         "42104", |  | ||||||
|         "--debug", |  | ||||||
|         "reloader.app", |  | ||||||
|     ], |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( |  | ||||||
|     "runargs, mode", |  | ||||||
|     [ |  | ||||||
|         (dict(port=42102, auto_reload=True), "script"), |  | ||||||
|         (dict(port=42103, debug=True), "module"), |  | ||||||
|         (dict(), "sanic"), |  | ||||||
|     ], |  | ||||||
| ) |  | ||||||
| async def test_reloader_live(runargs, mode): |  | ||||||
|     with TemporaryDirectory() as tmpdir: |  | ||||||
|         filename = os.path.join(tmpdir, "reloader.py") |  | ||||||
|         text = write_app(filename, **runargs) |  | ||||||
|         proc = Popen(argv[mode], cwd=tmpdir, stdout=PIPE, creationflags=flags) |  | ||||||
|         try: |  | ||||||
|             timeout = Timer(5, terminate, [proc]) |  | ||||||
|             timeout.start() |  | ||||||
|             # Python apparently keeps using the old source sometimes if |  | ||||||
|             # we don't sleep before rewrite (pycache timestamp problem?) |  | ||||||
|             sleep(1) |  | ||||||
|             line = scanner(proc) |  | ||||||
|             assert text in next(line) |  | ||||||
|             # Edit source code and try again |  | ||||||
|             text = write_app(filename, **runargs) |  | ||||||
|             assert text in next(line) |  | ||||||
|         finally: |  | ||||||
|             timeout.cancel() |  | ||||||
|             terminate(proc) |  | ||||||
|             with suppress(TimeoutExpired): |  | ||||||
|                 proc.wait(timeout=3) |  | ||||||
| @@ -44,6 +44,44 @@ def test_custom_context(app): | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Remove this once the deprecated API is abolished. | ||||||
|  | def test_custom_context_old(app): | ||||||
|  |     @app.middleware("request") | ||||||
|  |     def store(request): | ||||||
|  |         try: | ||||||
|  |             request["foo"] | ||||||
|  |         except KeyError: | ||||||
|  |             pass | ||||||
|  |         request["user"] = "sanic" | ||||||
|  |         sidekick = request.get("sidekick", "tails")  # Item missing -> default | ||||||
|  |         request["sidekick"] = sidekick | ||||||
|  |         request["bar"] = request["sidekick"] | ||||||
|  |         del request["sidekick"] | ||||||
|  |  | ||||||
|  |     @app.route("/") | ||||||
|  |     def handler(request): | ||||||
|  |         return json( | ||||||
|  |             { | ||||||
|  |                 "user": request.get("user"), | ||||||
|  |                 "sidekick": request.get("sidekick"), | ||||||
|  |                 "has_bar": "bar" in request, | ||||||
|  |                 "has_sidekick": "sidekick" in request, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/") | ||||||
|  |  | ||||||
|  |     assert response.json == { | ||||||
|  |         "user": "sanic", | ||||||
|  |         "sidekick": None, | ||||||
|  |         "has_bar": True, | ||||||
|  |         "has_sidekick": False, | ||||||
|  |     } | ||||||
|  |     response_json = loads(response.text) | ||||||
|  |     assert response_json["user"] == "sanic" | ||||||
|  |     assert response_json.get("sidekick") is None | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_injection(app): | def test_app_injection(app): | ||||||
|     expected = random.choice(range(0, 100)) |     expected = random.choice(range(0, 100)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,12 +3,12 @@ import pytest | |||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
| from sanic.exceptions import HeaderExpectationFailed | from sanic.exceptions import HeaderExpectationFailed | ||||||
| from sanic.request import StreamBuffer | from sanic.request import StreamBuffer | ||||||
| from sanic.response import json, stream, text | from sanic.response import stream, text | ||||||
| from sanic.views import CompositionView, HTTPMethodView | from sanic.views import CompositionView, HTTPMethodView | ||||||
| from sanic.views import stream as stream_decorator | from sanic.views import stream as stream_decorator | ||||||
|  |  | ||||||
|  |  | ||||||
| data = "abc" * 1_000_000 | data = "abc" * 10000000 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_request_stream_method_view(app): | def test_request_stream_method_view(app): | ||||||
| @@ -329,12 +329,15 @@ def test_request_stream_handle_exception(app): | |||||||
|     # 404 |     # 404 | ||||||
|     request, response = app.test_client.post("/in_valid_post", data=data) |     request, response = app.test_client.post("/in_valid_post", data=data) | ||||||
|     assert response.status == 404 |     assert response.status == 404 | ||||||
|     assert "Requested URL /in_valid_post not found" in response.text |     assert response.text == "Error: Requested URL /in_valid_post not found" | ||||||
|  |  | ||||||
|     # 405 |     # 405 | ||||||
|     request, response = app.test_client.get("/post/random_id") |     request, response = app.test_client.get("/post/random_id") | ||||||
|     assert response.status == 405 |     assert response.status == 405 | ||||||
|     assert "Method GET not allowed for URL /post/random_id" in response.text |     assert ( | ||||||
|  |         response.text == "Error: Method GET not allowed for URL" | ||||||
|  |         " /post/random_id" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_request_stream_blueprint(app): | def test_request_stream_blueprint(app): | ||||||
| @@ -613,44 +616,3 @@ def test_request_stream(app): | |||||||
|     request, response = app.test_client.post("/bp_stream", data=data) |     request, response = app.test_client.post("/bp_stream", data=data) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.text == data |     assert response.text == data | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_streaming_new_api(app): |  | ||||||
|     @app.post("/non-stream") |  | ||||||
|     async def handler(request): |  | ||||||
|         assert request.body == b"x" |  | ||||||
|         await request.receive_body()  # This should do nothing |  | ||||||
|         assert request.body == b"x" |  | ||||||
|         return text("OK") |  | ||||||
|  |  | ||||||
|     @app.post("/1", stream=True) |  | ||||||
|     async def handler(request): |  | ||||||
|         assert request.stream |  | ||||||
|         assert not request.body |  | ||||||
|         await request.receive_body() |  | ||||||
|         return text(request.body.decode().upper()) |  | ||||||
|  |  | ||||||
|     @app.post("/2", stream=True) |  | ||||||
|     async def handler(request): |  | ||||||
|         ret = [] |  | ||||||
|         async for data in request.stream: |  | ||||||
|             # We should have no b"" or None, just proper chunks |  | ||||||
|             assert data |  | ||||||
|             assert isinstance(data, bytes) |  | ||||||
|             ret.append(data.decode("ASCII")) |  | ||||||
|         return json(ret) |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.post("/non-stream", data="x") |  | ||||||
|     assert response.status == 200 |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.post("/1", data="TEST data") |  | ||||||
|     assert request.body == b"TEST data" |  | ||||||
|     assert response.status == 200 |  | ||||||
|     assert response.text == "TEST DATA" |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.post("/2", data=data) |  | ||||||
|     assert response.status == 200 |  | ||||||
|     res = response.json |  | ||||||
|     assert isinstance(res, list) |  | ||||||
|     assert len(res) > 1 |  | ||||||
|     assert "".join(res) == data |  | ||||||
|   | |||||||
| @@ -14,15 +14,18 @@ class DelayableHTTPConnection(httpx.dispatch.connection.HTTPConnection): | |||||||
|             self._request_delay = kwargs.pop("request_delay") |             self._request_delay = kwargs.pop("request_delay") | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|     async def send(self, request, timeout=None): |     async def send(self, request, verify=None, cert=None, timeout=None): | ||||||
|  |         if self.h11_connection is None and self.h2_connection is None: | ||||||
|         if self.connection is None: |             await self.connect(verify=verify, cert=cert, timeout=timeout) | ||||||
|             self.connection = await self.connect(timeout=timeout) |  | ||||||
|  |  | ||||||
|         if self._request_delay: |         if self._request_delay: | ||||||
|             await asyncio.sleep(self._request_delay) |             await asyncio.sleep(self._request_delay) | ||||||
|  |  | ||||||
|         response = await self.connection.send(request, timeout=timeout) |         if self.h2_connection is not None: | ||||||
|  |             response = await self.h2_connection.send(request, timeout=timeout) | ||||||
|  |         else: | ||||||
|  |             assert self.h11_connection is not None | ||||||
|  |             response = await self.h11_connection.send(request, timeout=timeout) | ||||||
|  |  | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
| @@ -43,9 +46,12 @@ class DelayableSanicConnectionPool( | |||||||
|             await self.max_connections.acquire(timeout=pool_timeout) |             await self.max_connections.acquire(timeout=pool_timeout) | ||||||
|             connection = DelayableHTTPConnection( |             connection = DelayableHTTPConnection( | ||||||
|                 origin, |                 origin, | ||||||
|                 ssl=self.ssl, |                 verify=self.verify, | ||||||
|  |                 cert=self.cert, | ||||||
|  |                 http2=self.http2, | ||||||
|                 backend=self.backend, |                 backend=self.backend, | ||||||
|                 release_func=self.release_connection, |                 release_func=self.release_connection, | ||||||
|  |                 trust_env=self.trust_env, | ||||||
|                 uds=self.uds, |                 uds=self.uds, | ||||||
|                 request_delay=self._request_delay, |                 request_delay=self._request_delay, | ||||||
|             ) |             ) | ||||||
| @@ -55,7 +61,7 @@ class DelayableSanicConnectionPool( | |||||||
|         return connection |         return connection | ||||||
|  |  | ||||||
|  |  | ||||||
| class DelayableSanicSession(httpx.AsyncClient): | class DelayableSanicSession(httpx.Client): | ||||||
|     def __init__(self, request_delay=None, *args, **kwargs) -> None: |     def __init__(self, request_delay=None, *args, **kwargs) -> None: | ||||||
|         dispatch = DelayableSanicConnectionPool(request_delay=request_delay) |         dispatch = DelayableSanicConnectionPool(request_delay=request_delay) | ||||||
|         super().__init__(dispatch=dispatch, *args, **kwargs) |         super().__init__(dispatch=dispatch, *args, **kwargs) | ||||||
| @@ -96,7 +102,7 @@ def test_default_server_error_request_timeout(): | |||||||
|     client = DelayableSanicTestClient(request_timeout_default_app, 2) |     client = DelayableSanicTestClient(request_timeout_default_app, 2) | ||||||
|     request, response = client.get("/1") |     request, response = client.get("/1") | ||||||
|     assert response.status == 408 |     assert response.status == 408 | ||||||
|     assert "Request Timeout" in response.text |     assert response.text == "Error: Request Timeout" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_default_server_error_request_dont_timeout(): | def test_default_server_error_request_dont_timeout(): | ||||||
| @@ -119,4 +125,4 @@ def test_default_server_error_websocket_request_timeout(): | |||||||
|     request, response = client.get("/ws1", headers=headers) |     request, response = client.get("/ws1", headers=headers) | ||||||
|  |  | ||||||
|     assert response.status == 408 |     assert response.status == 408 | ||||||
|     assert "Request Timeout" in response.text |     assert response.text == "Error: Request Timeout" | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ import pytest | |||||||
| from sanic import Blueprint, Sanic | from sanic import Blueprint, Sanic | ||||||
| from sanic.exceptions import ServerError | from sanic.exceptions import ServerError | ||||||
| from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters | from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters | ||||||
| from sanic.response import html, json, text | from sanic.response import json, text | ||||||
| from sanic.testing import ASGI_HOST, HOST, PORT, SanicTestClient | from sanic.testing import ASGI_HOST, HOST, PORT | ||||||
|  |  | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
| @@ -44,7 +44,7 @@ async def test_sync_asgi(app): | |||||||
| def test_ip(app): | def test_ip(app): | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     def handler(request): |     def handler(request): | ||||||
|         return text(f"{request.ip}") |         return text("{}".format(request.ip)) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/") |     request, response = app.test_client.get("/") | ||||||
|  |  | ||||||
| @@ -55,7 +55,7 @@ def test_ip(app): | |||||||
| async def test_ip_asgi(app): | async def test_ip_asgi(app): | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     def handler(request): |     def handler(request): | ||||||
|         return text(f"{request.url}") |         return text("{}".format(request.url)) | ||||||
|  |  | ||||||
|     request, response = await app.asgi_client.get("/") |     request, response = await app.asgi_client.get("/") | ||||||
|  |  | ||||||
| @@ -72,41 +72,6 @@ def test_text(app): | |||||||
|     assert response.text == "Hello" |     assert response.text == "Hello" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_html(app): |  | ||||||
|     class Foo: |  | ||||||
|         def __html__(self): |  | ||||||
|             return "<h1>Foo</h1>" |  | ||||||
|  |  | ||||||
|         def _repr_html_(self): |  | ||||||
|             return "<h1>Foo object repr</h1>" |  | ||||||
|  |  | ||||||
|     class Bar: |  | ||||||
|         def _repr_html_(self): |  | ||||||
|             return "<h1>Bar object repr</h1>" |  | ||||||
|  |  | ||||||
|     @app.route("/") |  | ||||||
|     async def handler(request): |  | ||||||
|         return html("<h1>Hello</h1>") |  | ||||||
|  |  | ||||||
|     @app.route("/foo") |  | ||||||
|     async def handler(request): |  | ||||||
|         return html(Foo()) |  | ||||||
|  |  | ||||||
|     @app.route("/bar") |  | ||||||
|     async def handler(request): |  | ||||||
|         return html(Bar()) |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/") |  | ||||||
|     assert response.content_type == "text/html; charset=utf-8" |  | ||||||
|     assert response.text == "<h1>Hello</h1>" |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/foo") |  | ||||||
|     assert response.text == "<h1>Foo</h1>" |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/bar") |  | ||||||
|     assert response.text == "<h1>Bar object repr</h1>" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| async def test_text_asgi(app): | async def test_text_asgi(app): | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
| @@ -325,7 +290,7 @@ def test_token(app): | |||||||
|     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" |     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" | ||||||
|     headers = { |     headers = { | ||||||
|         "content-type": "application/json", |         "content-type": "application/json", | ||||||
|         "Authorization": f"{token}", |         "Authorization": "{}".format(token), | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/", headers=headers) |     request, response = app.test_client.get("/", headers=headers) | ||||||
| @@ -335,7 +300,7 @@ def test_token(app): | |||||||
|     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" |     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" | ||||||
|     headers = { |     headers = { | ||||||
|         "content-type": "application/json", |         "content-type": "application/json", | ||||||
|         "Authorization": f"Token {token}", |         "Authorization": "Token {}".format(token), | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/", headers=headers) |     request, response = app.test_client.get("/", headers=headers) | ||||||
| @@ -345,7 +310,7 @@ def test_token(app): | |||||||
|     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" |     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" | ||||||
|     headers = { |     headers = { | ||||||
|         "content-type": "application/json", |         "content-type": "application/json", | ||||||
|         "Authorization": f"Bearer {token}", |         "Authorization": "Bearer {}".format(token), | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/", headers=headers) |     request, response = app.test_client.get("/", headers=headers) | ||||||
| @@ -370,7 +335,7 @@ async def test_token_asgi(app): | |||||||
|     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" |     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" | ||||||
|     headers = { |     headers = { | ||||||
|         "content-type": "application/json", |         "content-type": "application/json", | ||||||
|         "Authorization": f"{token}", |         "Authorization": "{}".format(token), | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     request, response = await app.asgi_client.get("/", headers=headers) |     request, response = await app.asgi_client.get("/", headers=headers) | ||||||
| @@ -380,7 +345,7 @@ async def test_token_asgi(app): | |||||||
|     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" |     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" | ||||||
|     headers = { |     headers = { | ||||||
|         "content-type": "application/json", |         "content-type": "application/json", | ||||||
|         "Authorization": f"Token {token}", |         "Authorization": "Token {}".format(token), | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     request, response = await app.asgi_client.get("/", headers=headers) |     request, response = await app.asgi_client.get("/", headers=headers) | ||||||
| @@ -390,7 +355,7 @@ async def test_token_asgi(app): | |||||||
|     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" |     token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" | ||||||
|     headers = { |     headers = { | ||||||
|         "content-type": "application/json", |         "content-type": "application/json", | ||||||
|         "Authorization": f"Bearer {token}", |         "Authorization": "Bearer {}".format(token), | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     request, response = await app.asgi_client.get("/", headers=headers) |     request, response = await app.asgi_client.get("/", headers=headers) | ||||||
| @@ -454,13 +419,11 @@ def test_standard_forwarded(app): | |||||||
|         "X-Real-IP": "127.0.0.2", |         "X-Real-IP": "127.0.0.2", | ||||||
|         "X-Forwarded-For": "127.0.1.1", |         "X-Forwarded-For": "127.0.1.1", | ||||||
|         "X-Scheme": "ws", |         "X-Scheme": "ws", | ||||||
|         "Host": "local.site", |  | ||||||
|     } |     } | ||||||
|     request, response = app.test_client.get("/", headers=headers) |     request, response = app.test_client.get("/", headers=headers) | ||||||
|     assert response.json == {"for": "127.0.0.2", "proto": "ws"} |     assert response.json == {"for": "127.0.0.2", "proto": "ws"} | ||||||
|     assert request.remote_addr == "127.0.0.2" |     assert request.remote_addr == "127.0.0.2" | ||||||
|     assert request.scheme == "ws" |     assert request.scheme == "ws" | ||||||
|     assert request.server_name == "local.site" |  | ||||||
|     assert request.server_port == 80 |     assert request.server_port == 80 | ||||||
|  |  | ||||||
|     app.config.FORWARDED_SECRET = "mySecret" |     app.config.FORWARDED_SECRET = "mySecret" | ||||||
| @@ -1030,8 +993,8 @@ def test_url_attributes_no_ssl(app, path, query, expected_url): | |||||||
|  |  | ||||||
|     app.add_route(handler, path) |     app.add_route(handler, path) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(path + f"?{query}") |     request, response = app.test_client.get(path + "?{}".format(query)) | ||||||
|     assert request.url == expected_url.format(HOST, request.server_port) |     assert request.url == expected_url.format(HOST, PORT) | ||||||
|  |  | ||||||
|     parsed = urlparse(request.url) |     parsed = urlparse(request.url) | ||||||
|  |  | ||||||
| @@ -1056,7 +1019,7 @@ async def test_url_attributes_no_ssl_asgi(app, path, query, expected_url): | |||||||
|  |  | ||||||
|     app.add_route(handler, path) |     app.add_route(handler, path) | ||||||
|  |  | ||||||
|     request, response = await app.asgi_client.get(path + f"?{query}") |     request, response = await app.asgi_client.get(path + "?{}".format(query)) | ||||||
|     assert request.url == expected_url.format(ASGI_HOST) |     assert request.url == expected_url.format(ASGI_HOST) | ||||||
|  |  | ||||||
|     parsed = urlparse(request.url) |     parsed = urlparse(request.url) | ||||||
| @@ -1088,12 +1051,11 @@ def test_url_attributes_with_ssl_context(app, path, query, expected_url): | |||||||
|  |  | ||||||
|     app.add_route(handler, path) |     app.add_route(handler, path) | ||||||
|  |  | ||||||
|     port = app.test_client.port |  | ||||||
|     request, response = app.test_client.get( |     request, response = app.test_client.get( | ||||||
|         f"https://{HOST}:{PORT}" + path + f"?{query}", |         "https://{}:{}".format(HOST, PORT) + path + "?{}".format(query), | ||||||
|         server_kwargs={"ssl": context}, |         server_kwargs={"ssl": context}, | ||||||
|     ) |     ) | ||||||
|     assert request.url == expected_url.format(HOST, request.server_port) |     assert request.url == expected_url.format(HOST, PORT) | ||||||
|  |  | ||||||
|     parsed = urlparse(request.url) |     parsed = urlparse(request.url) | ||||||
|  |  | ||||||
| @@ -1125,10 +1087,10 @@ def test_url_attributes_with_ssl_dict(app, path, query, expected_url): | |||||||
|     app.add_route(handler, path) |     app.add_route(handler, path) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get( |     request, response = app.test_client.get( | ||||||
|         f"https://{HOST}:{PORT}" + path + f"?{query}", |         "https://{}:{}".format(HOST, PORT) + path + "?{}".format(query), | ||||||
|         server_kwargs={"ssl": ssl_dict}, |         server_kwargs={"ssl": ssl_dict}, | ||||||
|     ) |     ) | ||||||
|     assert request.url == expected_url.format(HOST, request.server_port) |     assert request.url == expected_url.format(HOST, PORT) | ||||||
|  |  | ||||||
|     parsed = urlparse(request.url) |     parsed = urlparse(request.url) | ||||||
|  |  | ||||||
| @@ -1609,6 +1571,33 @@ async def test_request_args_no_query_string_await(app): | |||||||
|     assert request.args == {} |     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 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_request_raw_args_asgi(app): | ||||||
|  |  | ||||||
|  |     params = {"test": "OK"} | ||||||
|  |  | ||||||
|  |     @app.get("/") | ||||||
|  |     def handler(request): | ||||||
|  |         return text("pass") | ||||||
|  |  | ||||||
|  |     request, response = await app.asgi_client.get("/", params=params) | ||||||
|  |  | ||||||
|  |     assert request.raw_args == params | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_request_query_args(app): | def test_request_query_args(app): | ||||||
|     # test multiple params with the same key |     # test multiple params with the same key | ||||||
|     params = [("test", "value1"), ("test", "value2")] |     params = [("test", "value1"), ("test", "value2")] | ||||||
| @@ -1809,17 +1798,13 @@ def test_request_port(app): | |||||||
|     port = request.port |     port = request.port | ||||||
|     assert isinstance(port, int) |     assert isinstance(port, int) | ||||||
|  |  | ||||||
|  |     delattr(request, "_socket") | ||||||
| @pytest.mark.asyncio |     delattr(request, "_port") | ||||||
| async def test_request_port_asgi(app): |  | ||||||
|     @app.get("/") |  | ||||||
|     def handler(request): |  | ||||||
|         return text("OK") |  | ||||||
|  |  | ||||||
|     request, response = await app.asgi_client.get("/") |  | ||||||
|  |  | ||||||
|     port = request.port |     port = request.port | ||||||
|     assert isinstance(port, int) |     assert isinstance(port, int) | ||||||
|  |     assert hasattr(request, "_socket") | ||||||
|  |     assert hasattr(request, "_port") | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_request_socket(app): | def test_request_socket(app): | ||||||
| @@ -1838,6 +1823,12 @@ def test_request_socket(app): | |||||||
|     assert ip == request.ip |     assert ip == request.ip | ||||||
|     assert port == request.port |     assert port == request.port | ||||||
|  |  | ||||||
|  |     delattr(request, "_socket") | ||||||
|  |  | ||||||
|  |     socket = request.socket | ||||||
|  |     assert isinstance(socket, tuple) | ||||||
|  |     assert hasattr(request, "_socket") | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_request_server_name(app): | def test_request_server_name(app): | ||||||
|     @app.get("/") |     @app.get("/") | ||||||
| @@ -1866,7 +1857,7 @@ def test_request_server_name_in_host_header(app): | |||||||
|     request, response = app.test_client.get( |     request, response = app.test_client.get( | ||||||
|         "/", headers={"Host": "mal_formed"} |         "/", headers={"Host": "mal_formed"} | ||||||
|     ) |     ) | ||||||
|     assert request.server_name == "" |     assert request.server_name == None  # For now (later maybe 127.0.0.1) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_request_server_name_forwarded(app): | def test_request_server_name_forwarded(app): | ||||||
| @@ -1891,9 +1882,8 @@ def test_request_server_port(app): | |||||||
|     def handler(request): |     def handler(request): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     test_client = SanicTestClient(app) |     request, response = app.test_client.get("/", headers={"Host": "my-server"}) | ||||||
|     request, response = test_client.get("/", headers={"Host": "my-server"}) |     assert request.server_port == app.test_client.port | ||||||
|     assert request.server_port == 80 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_request_server_port_in_host_header(app): | def test_request_server_port_in_host_header(app): | ||||||
| @@ -1914,9 +1904,6 @@ def test_request_server_port_in_host_header(app): | |||||||
|     request, response = app.test_client.get( |     request, response = app.test_client.get( | ||||||
|         "/", headers={"Host": "mal_formed:5555"} |         "/", headers={"Host": "mal_formed:5555"} | ||||||
|     ) |     ) | ||||||
|     if PORT is None: |  | ||||||
|         assert request.server_port != 5555 |  | ||||||
|     else: |  | ||||||
|     assert request.server_port == app.test_client.port |     assert request.server_port == app.test_client.port | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1952,12 +1939,12 @@ def test_server_name_and_url_for(app): | |||||||
|     def handler(request): |     def handler(request): | ||||||
|         return text("ok") |         return text("ok") | ||||||
|  |  | ||||||
|     app.config.SERVER_NAME = "my-server"  # This means default port |     app.config.SERVER_NAME = "my-server" | ||||||
|     assert app.url_for("handler", _external=True) == "http://my-server/foo" |     assert app.url_for("handler", _external=True) == "http://my-server/foo" | ||||||
|     request, response = app.test_client.get("/foo") |     request, response = app.test_client.get("/foo") | ||||||
|     assert ( |     assert ( | ||||||
|         request.url_for("handler") |         request.url_for("handler") | ||||||
|         == f"http://my-server/foo" |         == f"http://my-server:{app.test_client.port}/foo" | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     app.config.SERVER_NAME = "https://my-server/path" |     app.config.SERVER_NAME = "https://my-server/path" | ||||||
| @@ -2018,7 +2005,7 @@ async def test_request_form_invalid_content_type_asgi(app): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_endpoint_basic(): | def test_endpoint_basic(): | ||||||
|     app = Sanic(name=__name__) |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     def my_unique_handler(request): |     def my_unique_handler(request): | ||||||
| @@ -2031,7 +2018,7 @@ def test_endpoint_basic(): | |||||||
|  |  | ||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| async def test_endpoint_basic_asgi(): | async def test_endpoint_basic_asgi(): | ||||||
|     app = Sanic(name=__name__) |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     def my_unique_handler(request): |     def my_unique_handler(request): | ||||||
| @@ -2110,5 +2097,5 @@ def test_url_for_without_server_name(app): | |||||||
|     request, response = app.test_client.get("/sample") |     request, response = app.test_client.get("/sample") | ||||||
|     assert ( |     assert ( | ||||||
|         response.json["url"] |         response.json["url"] | ||||||
|         == f"http://127.0.0.1:{request.server_port}/url-for" |         == f"http://127.0.0.1:{app.test_client.port}/url-for" | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import inspect | import inspect | ||||||
| import os | import os | ||||||
| import warnings |  | ||||||
|  |  | ||||||
| from collections import namedtuple | from collections import namedtuple | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| @@ -22,7 +21,6 @@ from sanic.response import ( | |||||||
|     json, |     json, | ||||||
|     raw, |     raw, | ||||||
|     stream, |     stream, | ||||||
|     text, |  | ||||||
| ) | ) | ||||||
| from sanic.server import HttpProtocol | from sanic.server import HttpProtocol | ||||||
| from sanic.testing import HOST, PORT | from sanic.testing import HOST, PORT | ||||||
| @@ -31,14 +29,13 @@ from sanic.testing import HOST, PORT | |||||||
| JSON_DATA = {"ok": True} | JSON_DATA = {"ok": True} | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.filterwarnings("ignore:Types other than str will be") |  | ||||||
| def test_response_body_not_a_string(app): | def test_response_body_not_a_string(app): | ||||||
|     """Test when a response body sent from the application is not a string""" |     """Test when a response body sent from the application is not a string""" | ||||||
|     random_num = choice(range(1000)) |     random_num = choice(range(1000)) | ||||||
|  |  | ||||||
|     @app.route("/hello") |     @app.route("/hello") | ||||||
|     async def hello_route(request): |     async def hello_route(request): | ||||||
|         return text(random_num) |         return HTTPResponse(body=random_num) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/hello") |     request, response = app.test_client.get("/hello") | ||||||
|     assert response.text == str(random_num) |     assert response.text == str(random_num) | ||||||
| @@ -235,6 +232,12 @@ def test_chunked_streaming_returns_correct_content(streaming_app): | |||||||
|     assert response.text == "foo,bar" |     assert response.text == "foo,bar" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): | ||||||
|  |     request, response = await streaming_app.asgi_client.get("/") | ||||||
|  |     assert response.text == "foo,bar" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | ||||||
|     request, response = non_chunked_streaming_app.test_client.get("/") |     request, response = non_chunked_streaming_app.test_client.get("/") | ||||||
|     assert "Transfer-Encoding" not in response.headers |     assert "Transfer-Encoding" not in response.headers | ||||||
| @@ -242,6 +245,16 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | |||||||
|     assert response.headers["Content-Length"] == "7" |     assert response.headers["Content-Length"] == "7" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_non_chunked_streaming_adds_correct_headers_asgi( | ||||||
|  |     non_chunked_streaming_app, | ||||||
|  | ): | ||||||
|  |     request, response = await non_chunked_streaming_app.asgi_client.get("/") | ||||||
|  |     assert "Transfer-Encoding" not in response.headers | ||||||
|  |     assert response.headers["Content-Type"] == "text/csv" | ||||||
|  |     assert response.headers["Content-Length"] == "7" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_non_chunked_streaming_returns_correct_content( | def test_non_chunked_streaming_returns_correct_content( | ||||||
|     non_chunked_streaming_app, |     non_chunked_streaming_app, | ||||||
| ): | ): | ||||||
| @@ -409,7 +422,7 @@ def test_file_response(app, file_name, static_file_directory, status): | |||||||
|             mime_type=guess_type(file_path)[0] or "text/plain", |             mime_type=guess_type(file_path)[0] or "text/plain", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(f"/files/{file_name}") |     request, response = app.test_client.get("/files/{}".format(file_name)) | ||||||
|     assert response.status == status |     assert response.status == status | ||||||
|     assert response.body == get_file_content(static_file_directory, file_name) |     assert response.body == get_file_content(static_file_directory, file_name) | ||||||
|     assert "Content-Disposition" not in response.headers |     assert "Content-Disposition" not in response.headers | ||||||
| @@ -432,13 +445,12 @@ def test_file_response_custom_filename( | |||||||
|         file_path = os.path.abspath(unquote(file_path)) |         file_path = os.path.abspath(unquote(file_path)) | ||||||
|         return file(file_path, filename=dest) |         return file(file_path, filename=dest) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(f"/files/{source}") |     request, response = app.test_client.get("/files/{}".format(source)) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == get_file_content(static_file_directory, source) |     assert response.body == get_file_content(static_file_directory, source) | ||||||
|     assert ( |     assert response.headers[ | ||||||
|         response.headers["Content-Disposition"] |         "Content-Disposition" | ||||||
|         == f'attachment; filename="{dest}"' |     ] == 'attachment; filename="{}"'.format(dest) | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||||
| @@ -463,7 +475,7 @@ def test_file_head_response(app, file_name, static_file_directory): | |||||||
|                 mime_type=guess_type(file_path)[0] or "text/plain", |                 mime_type=guess_type(file_path)[0] or "text/plain", | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     request, response = app.test_client.head(f"/files/{file_name}") |     request, response = app.test_client.head("/files/{}".format(file_name)) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert "Accept-Ranges" in response.headers |     assert "Accept-Ranges" in response.headers | ||||||
|     assert "Content-Length" in response.headers |     assert "Content-Length" in response.headers | ||||||
| @@ -486,7 +498,7 @@ def test_file_stream_response(app, file_name, static_file_directory): | |||||||
|             mime_type=guess_type(file_path)[0] or "text/plain", |             mime_type=guess_type(file_path)[0] or "text/plain", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(f"/files/{file_name}") |     request, response = app.test_client.get("/files/{}".format(file_name)) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == get_file_content(static_file_directory, file_name) |     assert response.body == get_file_content(static_file_directory, file_name) | ||||||
|     assert "Content-Disposition" not in response.headers |     assert "Content-Disposition" not in response.headers | ||||||
| @@ -509,13 +521,12 @@ def test_file_stream_response_custom_filename( | |||||||
|         file_path = os.path.abspath(unquote(file_path)) |         file_path = os.path.abspath(unquote(file_path)) | ||||||
|         return file_stream(file_path, chunk_size=32, filename=dest) |         return file_stream(file_path, chunk_size=32, filename=dest) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(f"/files/{source}") |     request, response = app.test_client.get("/files/{}".format(source)) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == get_file_content(static_file_directory, source) |     assert response.body == get_file_content(static_file_directory, source) | ||||||
|     assert ( |     assert response.headers[ | ||||||
|         response.headers["Content-Disposition"] |         "Content-Disposition" | ||||||
|         == f'attachment; filename="{dest}"' |     ] == 'attachment; filename="{}"'.format(dest) | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||||
| @@ -543,7 +554,7 @@ def test_file_stream_head_response(app, file_name, static_file_directory): | |||||||
|                 mime_type=guess_type(file_path)[0] or "text/plain", |                 mime_type=guess_type(file_path)[0] or "text/plain", | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     request, response = app.test_client.head(f"/files/{file_name}") |     request, response = app.test_client.head("/files/{}".format(file_name)) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     # A HEAD request should never be streamed/chunked. |     # A HEAD request should never be streamed/chunked. | ||||||
|     if "Transfer-Encoding" in response.headers: |     if "Transfer-Encoding" in response.headers: | ||||||
| @@ -581,12 +592,11 @@ def test_file_stream_response_range( | |||||||
|             _range=range, |             _range=range, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(f"/files/{file_name}") |     request, response = app.test_client.get("/files/{}".format(file_name)) | ||||||
|     assert response.status == 206 |     assert response.status == 206 | ||||||
|     assert "Content-Range" in response.headers |     assert "Content-Range" in response.headers | ||||||
|     assert ( |     assert response.headers["Content-Range"] == "bytes {}-{}/{}".format( | ||||||
|         response.headers["Content-Range"] |         range.start, range.end, range.total | ||||||
|         == f"bytes {range.start}-{range.end}/{range.total}" |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -608,17 +618,3 @@ def test_empty_response(app): | |||||||
|     request, response = app.test_client.get("/test") |     request, response = app.test_client.get("/test") | ||||||
|     assert response.content_type is None |     assert response.content_type is None | ||||||
|     assert response.body == b"" |     assert response.body == b"" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_response_body_bytes_deprecated(app): |  | ||||||
|     with warnings.catch_warnings(record=True) as w: |  | ||||||
|         warnings.simplefilter("always") |  | ||||||
|  |  | ||||||
|         HTTPResponse(body_bytes=b"bytes") |  | ||||||
|  |  | ||||||
|         assert len(w) == 1 |  | ||||||
|         assert issubclass(w[0].category, DeprecationWarning) |  | ||||||
|         assert ( |  | ||||||
|             "Parameter `body_bytes` is deprecated, use `body` instead" |  | ||||||
|             in str(w[0].message) |  | ||||||
|         ) |  | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ async def handler_2(request): | |||||||
| def test_default_server_error_response_timeout(): | def test_default_server_error_response_timeout(): | ||||||
|     request, response = response_timeout_default_app.test_client.get("/1") |     request, response = response_timeout_default_app.test_client.get("/1") | ||||||
|     assert response.status == 503 |     assert response.status == 503 | ||||||
|     assert "Response Timeout" in response.text |     assert response.text == "Error: Response Timeout" | ||||||
|  |  | ||||||
|  |  | ||||||
| response_handler_cancelled_app.flag = False | response_handler_cancelled_app.flag = False | ||||||
| @@ -65,5 +65,5 @@ async def handler_3(request): | |||||||
| def test_response_handler_cancelled(): | def test_response_handler_cancelled(): | ||||||
|     request, response = response_handler_cancelled_app.test_client.get("/1") |     request, response = response_handler_cancelled_app.test_client.get("/1") | ||||||
|     assert response.status == 503 |     assert response.status == 503 | ||||||
|     assert "Response Timeout" in response.text |     assert response.text == "Error: Response Timeout" | ||||||
|     assert response_handler_cancelled_app.flag is False |     assert response_handler_cancelled_app.flag is False | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ from sanic import Sanic | |||||||
| from sanic.constants import HTTP_METHODS | from sanic.constants import HTTP_METHODS | ||||||
| from sanic.response import json, text | from sanic.response import json, text | ||||||
| from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists | from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists | ||||||
| from sanic.testing import SanicTestClient |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
| @@ -21,17 +20,17 @@ def test_versioned_routes_get(app, method): | |||||||
|     func = getattr(app, method) |     func = getattr(app, method) | ||||||
|     if callable(func): |     if callable(func): | ||||||
|  |  | ||||||
|         @func(f"/{method}", version=1) |         @func("/{}".format(method), version=1) | ||||||
|         def handler(request): |         def handler(request): | ||||||
|             return text("OK") |             return text("OK") | ||||||
|  |  | ||||||
|     else: |     else: | ||||||
|         print(func) |         print(func) | ||||||
|         raise Exception(f"Method: {method} is not callable") |         raise Exception("Method: {} is not callable".format(method)) | ||||||
|  |  | ||||||
|     client_method = getattr(app.test_client, method) |     client_method = getattr(app.test_client, method) | ||||||
|  |  | ||||||
|     request, response = client_method(f"/v1/{method}") |     request, response = client_method("/v1/{}".format(method)) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -168,36 +167,35 @@ def test_route_optional_slash(app): | |||||||
| def test_route_strict_slashes_set_to_false_and_host_is_a_list(app): | def test_route_strict_slashes_set_to_false_and_host_is_a_list(app): | ||||||
|     # Part of regression test for issue #1120 |     # Part of regression test for issue #1120 | ||||||
|  |  | ||||||
|     test_client = SanicTestClient(app, port=42101) |     site1 = "127.0.0.1:{}".format(app.test_client.port) | ||||||
|     site1 = f"127.0.0.1:{test_client.port}" |  | ||||||
|  |  | ||||||
|     # before fix, this raises a RouteExists error |     # before fix, this raises a RouteExists error | ||||||
|     @app.get("/get", host=[site1, "site2.com"], strict_slashes=False) |     @app.get("/get", host=[site1, "site2.com"], strict_slashes=False) | ||||||
|     def get_handler(request): |     def get_handler(request): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     request, response = test_client.get("http://" + site1 + "/get") |     request, response = app.test_client.get("http://" + site1 + "/get") | ||||||
|     assert response.text == "OK" |     assert response.text == "OK" | ||||||
|  |  | ||||||
|     @app.post("/post", host=[site1, "site2.com"], strict_slashes=False) |     @app.post("/post", host=[site1, "site2.com"], strict_slashes=False) | ||||||
|     def post_handler(request): |     def post_handler(request): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     request, response = test_client.post("http://" + site1 + "/post") |     request, response = app.test_client.post("http://" + site1 + "/post") | ||||||
|     assert response.text == "OK" |     assert response.text == "OK" | ||||||
|  |  | ||||||
|     @app.put("/put", host=[site1, "site2.com"], strict_slashes=False) |     @app.put("/put", host=[site1, "site2.com"], strict_slashes=False) | ||||||
|     def put_handler(request): |     def put_handler(request): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     request, response = test_client.put("http://" + site1 + "/put") |     request, response = app.test_client.put("http://" + site1 + "/put") | ||||||
|     assert response.text == "OK" |     assert response.text == "OK" | ||||||
|  |  | ||||||
|     @app.delete("/delete", host=[site1, "site2.com"], strict_slashes=False) |     @app.delete("/delete", host=[site1, "site2.com"], strict_slashes=False) | ||||||
|     def delete_handler(request): |     def delete_handler(request): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     request, response = test_client.delete("http://" + site1 + "/delete") |     request, response = app.test_client.delete("http://" + site1 + "/delete") | ||||||
|     assert response.text == "OK" |     assert response.text == "OK" | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -414,8 +412,7 @@ def test_dynamic_route_uuid(app): | |||||||
|     assert response.text == "OK" |     assert response.text == "OK" | ||||||
|     assert type(results[0]) is uuid.UUID |     assert type(results[0]) is uuid.UUID | ||||||
|  |  | ||||||
|     generated_uuid = uuid.uuid4() |     request, response = app.test_client.get("/quirky/{}".format(uuid.uuid4())) | ||||||
|     request, response = app.test_client.get(f"/quirky/{generated_uuid}") |  | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/quirky/non-existing") |     request, response = app.test_client.get("/quirky/non-existing") | ||||||
| @@ -531,19 +528,6 @@ def test_add_webscoket_route(app, strict_slashes): | |||||||
|     assert ev.is_set() |     assert ev.is_set() | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_add_webscoket_route_with_version(app): |  | ||||||
|     ev = asyncio.Event() |  | ||||||
|  |  | ||||||
|     async def handler(request, ws): |  | ||||||
|         assert ws.subprotocol is None |  | ||||||
|         ev.set() |  | ||||||
|  |  | ||||||
|     app.add_websocket_route(handler, "/ws", version=1) |  | ||||||
|     request, response = app.test_client.websocket("/v1/ws") |  | ||||||
|     assert response.opened is True |  | ||||||
|     assert ev.is_set() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_route_duplicate(app): | def test_route_duplicate(app): | ||||||
|  |  | ||||||
|     with pytest.raises(RouteExists): |     with pytest.raises(RouteExists): | ||||||
| @@ -783,6 +767,55 @@ def test_add_route_method_not_allowed(app): | |||||||
|     assert response.status == 405 |     assert response.status == 405 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_static_route(app): | ||||||
|  |     async def handler1(request): | ||||||
|  |         return text("OK1") | ||||||
|  |  | ||||||
|  |     async def handler2(request): | ||||||
|  |         return text("OK2") | ||||||
|  |  | ||||||
|  |     app.add_route(handler1, "/test") | ||||||
|  |     app.add_route(handler2, "/test2") | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/test") | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/test2") | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route("/test") | ||||||
|  |     app.remove_route("/test2") | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/test") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/test2") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_dynamic_route(app): | ||||||
|  |     async def handler(request, name): | ||||||
|  |         return text("OK") | ||||||
|  |  | ||||||
|  |     app.add_route(handler, "/folder/<name>") | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/folder/test123") | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route("/folder/<name>") | ||||||
|  |     request, response = app.test_client.get("/folder/test123") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_inexistent_route(app): | ||||||
|  |  | ||||||
|  |     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): | def test_removing_slash(app): | ||||||
|     @app.get("/rest/<resource>") |     @app.get("/rest/<resource>") | ||||||
|     def get(_): |     def get(_): | ||||||
| @@ -795,6 +828,59 @@ def test_removing_slash(app): | |||||||
|     assert len(app.router.routes_all.keys()) == 2 |     assert len(app.router.routes_all.keys()) == 2 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_unhashable_route(app): | ||||||
|  |     async def handler(request, unhashable): | ||||||
|  |         return text("OK") | ||||||
|  |  | ||||||
|  |     app.add_route(handler, "/folder/<unhashable:[A-Za-z0-9/]+>/end/") | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/folder/test/asdf/end/") | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/folder/test///////end/") | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/folder/test/end/") | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route("/folder/<unhashable:[A-Za-z0-9/]+>/end/") | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/folder/test/asdf/end/") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/folder/test///////end/") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/folder/test/end/") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_route_without_clean_cache(app): | ||||||
|  |     async def handler(request): | ||||||
|  |         return text("OK") | ||||||
|  |  | ||||||
|  |     app.add_route(handler, "/test") | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/test") | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route("/test", clean_cache=True) | ||||||
|  |     app.remove_route("/test/", clean_cache=True) | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/test") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     app.add_route(handler, "/test") | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/test") | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route("/test", clean_cache=False) | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/test") | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_overload_routes(app): | def test_overload_routes(app): | ||||||
|     @app.route("/overload", methods=["GET"]) |     @app.route("/overload", methods=["GET"]) | ||||||
|     async def handler1(request): |     async def handler1(request): | ||||||
|   | |||||||
| @@ -1,9 +1,6 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import signal | import signal | ||||||
|  |  | ||||||
| from contextlib import closing |  | ||||||
| from socket import socket |  | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic.testing import HOST, PORT | from sanic.testing import HOST, PORT | ||||||
| @@ -25,7 +22,7 @@ skipif_no_alarm = pytest.mark.skipif( | |||||||
|  |  | ||||||
| def create_listener(listener_name, in_list): | def create_listener(listener_name, in_list): | ||||||
|     async def _listener(app, loop): |     async def _listener(app, loop): | ||||||
|         print(f"DEBUG MESSAGE FOR PYTEST for {listener_name}") |         print("DEBUG MESSAGE FOR PYTEST for {}".format(listener_name)) | ||||||
|         in_list.insert(0, app.name + listener_name) |         in_list.insert(0, app.name + listener_name) | ||||||
|  |  | ||||||
|     return _listener |     return _listener | ||||||
| @@ -121,12 +118,7 @@ def test_create_server_trigger_events(app): | |||||||
|     app.listener("after_server_stop")(after_stop) |     app.listener("after_server_stop")(after_stop) | ||||||
|  |  | ||||||
|     loop = asyncio.get_event_loop() |     loop = asyncio.get_event_loop() | ||||||
|  |     serv_coro = app.create_server(return_asyncio_server=True) | ||||||
|     # Use random port for tests |  | ||||||
|     with closing(socket()) as sock: |  | ||||||
|         sock.bind(("127.0.0.1", 0)) |  | ||||||
|  |  | ||||||
|         serv_coro = app.create_server(return_asyncio_server=True, sock=sock) |  | ||||||
|     serv_task = asyncio.ensure_future(serv_coro, loop=loop) |     serv_task = asyncio.ensure_future(serv_coro, loop=loop) | ||||||
|     server = loop.run_until_complete(serv_task) |     server = loop.run_until_complete(serv_task) | ||||||
|     server.after_start() |     server.after_start() | ||||||
|   | |||||||
| @@ -1,13 +1,8 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import os |  | ||||||
| import signal |  | ||||||
|  |  | ||||||
| from queue import Queue | from queue import Queue | ||||||
| from unittest.mock import MagicMock | from unittest.mock import MagicMock | ||||||
|  |  | ||||||
| import pytest |  | ||||||
|  |  | ||||||
| from sanic.compat import ctrlc_workaround_for_windows |  | ||||||
| from sanic.response import HTTPResponse | from sanic.response import HTTPResponse | ||||||
| from sanic.testing import HOST, PORT | from sanic.testing import HOST, PORT | ||||||
|  |  | ||||||
| @@ -21,19 +16,13 @@ calledq = Queue() | |||||||
|  |  | ||||||
|  |  | ||||||
| def set_loop(app, loop): | def set_loop(app, loop): | ||||||
|     global mock |     loop.add_signal_handler = MagicMock() | ||||||
|     mock = MagicMock() |  | ||||||
|     if os.name == "nt": |  | ||||||
|         signal.signal = mock |  | ||||||
|     else: |  | ||||||
|         loop.add_signal_handler = mock |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def after(app, loop): | def after(app, loop): | ||||||
|     calledq.put(mock.called) |     calledq.put(loop.add_signal_handler.called) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") |  | ||||||
| def test_register_system_signals(app): | def test_register_system_signals(app): | ||||||
|     """Test if sanic register system signals""" |     """Test if sanic register system signals""" | ||||||
|  |  | ||||||
| @@ -49,7 +38,6 @@ def test_register_system_signals(app): | |||||||
|     assert calledq.get() is True |     assert calledq.get() is True | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") |  | ||||||
| def test_dont_register_system_signals(app): | def test_dont_register_system_signals(app): | ||||||
|     """Test if sanic don't register system signals""" |     """Test if sanic don't register system signals""" | ||||||
|  |  | ||||||
| @@ -63,47 +51,3 @@ def test_dont_register_system_signals(app): | |||||||
|  |  | ||||||
|     app.run(HOST, PORT, register_sys_signals=False) |     app.run(HOST, PORT, register_sys_signals=False) | ||||||
|     assert calledq.get() is False |     assert calledq.get() is False | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif(os.name == "nt", reason="windows cannot SIGINT processes") |  | ||||||
| def test_windows_workaround(): |  | ||||||
|     """Test Windows workaround (on any other OS)""" |  | ||||||
|     # At least some code coverage, even though this test doesn't work on |  | ||||||
|     # Windows... |  | ||||||
|     class MockApp: |  | ||||||
|         def __init__(self): |  | ||||||
|             self.is_stopping = False |  | ||||||
|  |  | ||||||
|         def stop(self): |  | ||||||
|             assert not self.is_stopping |  | ||||||
|             self.is_stopping = True |  | ||||||
|  |  | ||||||
|         def add_task(self, func): |  | ||||||
|             loop = asyncio.get_event_loop() |  | ||||||
|             self.stay_active_task = loop.create_task(func(self)) |  | ||||||
|  |  | ||||||
|     async def atest(stop_first): |  | ||||||
|         app = MockApp() |  | ||||||
|         ctrlc_workaround_for_windows(app) |  | ||||||
|         await asyncio.sleep(0.05) |  | ||||||
|         if stop_first: |  | ||||||
|             app.stop() |  | ||||||
|             await asyncio.sleep(0.2) |  | ||||||
|         assert app.is_stopping == stop_first |  | ||||||
|         # First Ctrl+C: should call app.stop() within 0.1 seconds |  | ||||||
|         os.kill(os.getpid(), signal.SIGINT) |  | ||||||
|         await asyncio.sleep(0.2) |  | ||||||
|         assert app.is_stopping |  | ||||||
|         assert app.stay_active_task.result() == None |  | ||||||
|         # Second Ctrl+C should raise |  | ||||||
|         with pytest.raises(KeyboardInterrupt): |  | ||||||
|             os.kill(os.getpid(), signal.SIGINT) |  | ||||||
|         return "OK" |  | ||||||
|  |  | ||||||
|     # Run in our private loop |  | ||||||
|     loop = asyncio.new_event_loop() |  | ||||||
|     asyncio.set_event_loop(loop) |  | ||||||
|     res = loop.run_until_complete(atest(False)) |  | ||||||
|     assert res == "OK" |  | ||||||
|     res = loop.run_until_complete(atest(True)) |  | ||||||
|     assert res == "OK" |  | ||||||
|   | |||||||
| @@ -97,7 +97,9 @@ def test_static_file_content_type(app, static_file_directory, file_name): | |||||||
| def test_static_directory(app, file_name, base_uri, static_file_directory): | def test_static_directory(app, file_name, base_uri, static_file_directory): | ||||||
|     app.static(base_uri, static_file_directory) |     app.static(base_uri, static_file_directory) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(uri=f"{base_uri}/{file_name}") |     request, response = app.test_client.get( | ||||||
|  |         uri="{}/{}".format(base_uri, file_name) | ||||||
|  |     ) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == get_file_content(static_file_directory, file_name) |     assert response.body == get_file_content(static_file_directory, file_name) | ||||||
|  |  | ||||||
| @@ -232,11 +234,11 @@ def test_static_content_range_invalid_unit( | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     unit = "bit" |     unit = "bit" | ||||||
|     headers = {"Range": f"{unit}=1-0"} |     headers = {"Range": "{}=1-0".format(unit)} | ||||||
|     request, response = app.test_client.get("/testing.file", headers=headers) |     request, response = app.test_client.get("/testing.file", headers=headers) | ||||||
|  |  | ||||||
|     assert response.status == 416 |     assert response.status == 416 | ||||||
|     assert f"{unit} is not a valid Range Type" in response.text |     assert response.text == "Error: {} is not a valid Range Type".format(unit) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||||
| @@ -250,11 +252,13 @@ def test_static_content_range_invalid_start( | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     start = "start" |     start = "start" | ||||||
|     headers = {"Range": f"bytes={start}-0"} |     headers = {"Range": "bytes={}-0".format(start)} | ||||||
|     request, response = app.test_client.get("/testing.file", headers=headers) |     request, response = app.test_client.get("/testing.file", headers=headers) | ||||||
|  |  | ||||||
|     assert response.status == 416 |     assert response.status == 416 | ||||||
|     assert f"'{start}' is invalid for Content Range" in response.text |     assert response.text == "Error: '{}' is invalid for Content Range".format( | ||||||
|  |         start | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||||
| @@ -268,11 +272,13 @@ def test_static_content_range_invalid_end( | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     end = "end" |     end = "end" | ||||||
|     headers = {"Range": f"bytes=1-{end}"} |     headers = {"Range": "bytes=1-{}".format(end)} | ||||||
|     request, response = app.test_client.get("/testing.file", headers=headers) |     request, response = app.test_client.get("/testing.file", headers=headers) | ||||||
|  |  | ||||||
|     assert response.status == 416 |     assert response.status == 416 | ||||||
|     assert f"'{end}' is invalid for Content Range" in response.text |     assert response.text == "Error: '{}' is invalid for Content Range".format( | ||||||
|  |         end | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) | ||||||
| @@ -289,7 +295,7 @@ def test_static_content_range_invalid_parameters( | |||||||
|     request, response = app.test_client.get("/testing.file", headers=headers) |     request, response = app.test_client.get("/testing.file", headers=headers) | ||||||
|  |  | ||||||
|     assert response.status == 416 |     assert response.status == 416 | ||||||
|     assert "Invalid for Content Range parameters" in response.text |     assert response.text == "Error: Invalid for Content Range parameters" | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
| @@ -363,7 +369,7 @@ def test_file_not_found(app, static_file_directory): | |||||||
|     request, response = app.test_client.get("/static/not_found") |     request, response = app.test_client.get("/static/not_found") | ||||||
|  |  | ||||||
|     assert response.status == 404 |     assert response.status == 404 | ||||||
|     assert "File not found" in response.text |     assert response.text == "Error: File not found" | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("static_name", ["_static_name", "static"]) | @pytest.mark.parametrize("static_name", ["_static_name", "static"]) | ||||||
| @@ -371,6 +377,20 @@ def test_file_not_found(app, static_file_directory): | |||||||
| def test_static_name(app, static_file_directory, static_name, file_name): | def test_static_name(app, static_file_directory, static_name, file_name): | ||||||
|     app.static("/static", static_file_directory, name=static_name) |     app.static("/static", static_file_directory, name=static_name) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(f"/static/{file_name}") |     request, response = app.test_client.get("/static/{}".format(file_name)) | ||||||
|  |  | ||||||
|     assert response.status == 200 |     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,3 +1,5 @@ | |||||||
|  | import socket | ||||||
|  |  | ||||||
| from sanic.response import json, text | from sanic.response import json, text | ||||||
| from sanic.testing import PORT, SanicTestClient | from sanic.testing import PORT, SanicTestClient | ||||||
|  |  | ||||||
| @@ -27,8 +29,7 @@ def test_test_client_port_default(app): | |||||||
|         return json(request.transport.get_extra_info("sockname")[1]) |         return json(request.transport.get_extra_info("sockname")[1]) | ||||||
|  |  | ||||||
|     test_client = SanicTestClient(app) |     test_client = SanicTestClient(app) | ||||||
|     assert test_client.port == PORT  # Can be None before request |     assert test_client.port == PORT | ||||||
|  |  | ||||||
|     request, response = test_client.get("/get") |     request, response = test_client.get("/get") | ||||||
|     assert test_client.port > 0 |     assert response.json == PORT | ||||||
|     assert response.json == test_client.port |  | ||||||
|   | |||||||
| @@ -1,235 +0,0 @@ | |||||||
| import asyncio |  | ||||||
| import logging |  | ||||||
| import os |  | ||||||
| import subprocess |  | ||||||
| import sys |  | ||||||
|  |  | ||||||
| import httpx |  | ||||||
| import pytest |  | ||||||
|  |  | ||||||
| from sanic import Sanic |  | ||||||
| from sanic.response import text |  | ||||||
|  |  | ||||||
|  |  | ||||||
| pytestmark = pytest.mark.skipif(os.name != "posix", reason="UNIX only") |  | ||||||
| SOCKPATH = "/tmp/sanictest.sock" |  | ||||||
| SOCKPATH2 = "/tmp/sanictest2.sock" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(autouse=True) |  | ||||||
| def socket_cleanup(): |  | ||||||
|     try: |  | ||||||
|         os.unlink(SOCKPATH) |  | ||||||
|     except FileNotFoundError: |  | ||||||
|         pass |  | ||||||
|     try: |  | ||||||
|         os.unlink(SOCKPATH2) |  | ||||||
|     except FileNotFoundError: |  | ||||||
|         pass |  | ||||||
|     # Run test function |  | ||||||
|     yield |  | ||||||
|     try: |  | ||||||
|         os.unlink(SOCKPATH2) |  | ||||||
|     except FileNotFoundError: |  | ||||||
|         pass |  | ||||||
|     try: |  | ||||||
|         os.unlink(SOCKPATH) |  | ||||||
|     except FileNotFoundError: |  | ||||||
|         pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unix_socket_creation(caplog): |  | ||||||
|     from socket import AF_UNIX, socket |  | ||||||
|  |  | ||||||
|     with socket(AF_UNIX) as sock: |  | ||||||
|         sock.bind(SOCKPATH) |  | ||||||
|     assert os.path.exists(SOCKPATH) |  | ||||||
|     ino = os.stat(SOCKPATH).st_ino |  | ||||||
|  |  | ||||||
|     app = Sanic(name=__name__) |  | ||||||
|  |  | ||||||
|     @app.listener("after_server_start") |  | ||||||
|     def running(app, loop): |  | ||||||
|         assert os.path.exists(SOCKPATH) |  | ||||||
|         assert ino != os.stat(SOCKPATH).st_ino |  | ||||||
|         app.stop() |  | ||||||
|  |  | ||||||
|     with caplog.at_level(logging.INFO): |  | ||||||
|         app.run(unix=SOCKPATH) |  | ||||||
|  |  | ||||||
|     assert ( |  | ||||||
|         "sanic.root", |  | ||||||
|         logging.INFO, |  | ||||||
|         f"Goin' Fast @ {SOCKPATH} http://...", |  | ||||||
|     ) in caplog.record_tuples |  | ||||||
|     assert not os.path.exists(SOCKPATH) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_invalid_paths(): |  | ||||||
|     app = Sanic(name=__name__) |  | ||||||
|  |  | ||||||
|     with pytest.raises(FileExistsError): |  | ||||||
|         app.run(unix=".") |  | ||||||
|  |  | ||||||
|     with pytest.raises(FileNotFoundError): |  | ||||||
|         app.run(unix="no-such-directory/sanictest.sock") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_dont_replace_file(): |  | ||||||
|     with open(SOCKPATH, "w") as f: |  | ||||||
|         f.write("File, not socket") |  | ||||||
|  |  | ||||||
|     app = Sanic(name=__name__) |  | ||||||
|  |  | ||||||
|     @app.listener("after_server_start") |  | ||||||
|     def stop(app, loop): |  | ||||||
|         app.stop() |  | ||||||
|  |  | ||||||
|     with pytest.raises(FileExistsError): |  | ||||||
|         app.run(unix=SOCKPATH) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_dont_follow_symlink(): |  | ||||||
|     from socket import AF_UNIX, socket |  | ||||||
|  |  | ||||||
|     with socket(AF_UNIX) as sock: |  | ||||||
|         sock.bind(SOCKPATH2) |  | ||||||
|     os.symlink(SOCKPATH2, SOCKPATH) |  | ||||||
|  |  | ||||||
|     app = Sanic(name=__name__) |  | ||||||
|  |  | ||||||
|     @app.listener("after_server_start") |  | ||||||
|     def stop(app, loop): |  | ||||||
|         app.stop() |  | ||||||
|  |  | ||||||
|     with pytest.raises(FileExistsError): |  | ||||||
|         app.run(unix=SOCKPATH) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_socket_deleted_while_running(): |  | ||||||
|     app = Sanic(name=__name__) |  | ||||||
|  |  | ||||||
|     @app.listener("after_server_start") |  | ||||||
|     async def hack(app, loop): |  | ||||||
|         os.unlink(SOCKPATH) |  | ||||||
|         app.stop() |  | ||||||
|  |  | ||||||
|     app.run(host="myhost.invalid", unix=SOCKPATH) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_socket_replaced_with_file(): |  | ||||||
|     app = Sanic(name=__name__) |  | ||||||
|  |  | ||||||
|     @app.listener("after_server_start") |  | ||||||
|     async def hack(app, loop): |  | ||||||
|         os.unlink(SOCKPATH) |  | ||||||
|         with open(SOCKPATH, "w") as f: |  | ||||||
|             f.write("Not a socket") |  | ||||||
|         app.stop() |  | ||||||
|  |  | ||||||
|     app.run(host="myhost.invalid", unix=SOCKPATH) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unix_connection(): |  | ||||||
|     app = Sanic(name=__name__) |  | ||||||
|  |  | ||||||
|     @app.get("/") |  | ||||||
|     def handler(request): |  | ||||||
|         return text(f"{request.conn_info.server}") |  | ||||||
|  |  | ||||||
|     @app.listener("after_server_start") |  | ||||||
|     async def client(app, loop): |  | ||||||
|         try: |  | ||||||
|             async with httpx.AsyncClient(uds=SOCKPATH) as client: |  | ||||||
|                 r = await client.get("http://myhost.invalid/") |  | ||||||
|                 assert r.status_code == 200 |  | ||||||
|                 assert r.text == os.path.abspath(SOCKPATH) |  | ||||||
|         finally: |  | ||||||
|             app.stop() |  | ||||||
|  |  | ||||||
|     app.run(host="myhost.invalid", unix=SOCKPATH) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app_multi = Sanic(name=__name__) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def handler(request): |  | ||||||
|     return text(f"{request.conn_info.server}") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def client(app, loop): |  | ||||||
|     try: |  | ||||||
|         async with httpx.AsyncClient(uds=SOCKPATH) as client: |  | ||||||
|             r = await client.get("http://myhost.invalid/") |  | ||||||
|             assert r.status_code == 200 |  | ||||||
|             assert r.text == os.path.abspath(SOCKPATH) |  | ||||||
|     finally: |  | ||||||
|         app.stop() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unix_connection_multiple_workers(): |  | ||||||
|     app_multi.get("/")(handler) |  | ||||||
|     app_multi.listener("after_server_start")(client) |  | ||||||
|     app_multi.run(host="myhost.invalid", unix=SOCKPATH, workers=2) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def test_zero_downtime(): |  | ||||||
|     """Graceful server termination and socket replacement on restarts""" |  | ||||||
|     from signal import SIGINT |  | ||||||
|     from time import monotonic as current_time |  | ||||||
|  |  | ||||||
|     async def client(): |  | ||||||
|         for _ in range(40): |  | ||||||
|             async with httpx.AsyncClient(uds=SOCKPATH) as client: |  | ||||||
|                 r = await client.get("http://localhost/sleep/0.1") |  | ||||||
|                 assert r.status_code == 200 |  | ||||||
|                 assert r.text == f"Slept 0.1 seconds.\n" |  | ||||||
|  |  | ||||||
|     def spawn(): |  | ||||||
|         command = [ |  | ||||||
|             sys.executable, |  | ||||||
|             "-m", |  | ||||||
|             "sanic", |  | ||||||
|             "--unix", |  | ||||||
|             SOCKPATH, |  | ||||||
|             "examples.delayed_response.app", |  | ||||||
|         ] |  | ||||||
|         DN = subprocess.DEVNULL |  | ||||||
|         return subprocess.Popen( |  | ||||||
|             command, stdin=DN, stdout=DN, stderr=subprocess.PIPE |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     try: |  | ||||||
|         processes = [spawn()] |  | ||||||
|         while not os.path.exists(SOCKPATH): |  | ||||||
|             if processes[0].poll() is not None: |  | ||||||
|                 raise Exception("Worker did not start properly") |  | ||||||
|             await asyncio.sleep(0.0001) |  | ||||||
|         ino = os.stat(SOCKPATH).st_ino |  | ||||||
|         task = asyncio.get_event_loop().create_task(client()) |  | ||||||
|         start_time = current_time() |  | ||||||
|         while current_time() < start_time + 4: |  | ||||||
|             # Start a new one and wait until the socket is replaced |  | ||||||
|             processes.append(spawn()) |  | ||||||
|             while ino == os.stat(SOCKPATH).st_ino: |  | ||||||
|                 await asyncio.sleep(0.001) |  | ||||||
|             ino = os.stat(SOCKPATH).st_ino |  | ||||||
|             # Graceful termination of the previous one |  | ||||||
|             processes[-2].send_signal(SIGINT) |  | ||||||
|         # Wait until client has completed all requests |  | ||||||
|         await task |  | ||||||
|         processes[-1].send_signal(SIGINT) |  | ||||||
|         for worker in processes: |  | ||||||
|             try: |  | ||||||
|                 worker.wait(1.0) |  | ||||||
|             except subprocess.TimeoutExpired: |  | ||||||
|                 raise Exception( |  | ||||||
|                     f"Worker would not terminate:\n{worker.stderr}" |  | ||||||
|                 ) |  | ||||||
|     finally: |  | ||||||
|         for worker in processes: |  | ||||||
|             worker.kill() |  | ||||||
|     # Test for clean run and termination |  | ||||||
|     assert len(processes) > 5 |  | ||||||
|     assert [worker.poll() for worker in processes] == len(processes) * [0] |  | ||||||
|     assert not os.path.exists(SOCKPATH) |  | ||||||
| @@ -20,24 +20,30 @@ URL_FOR_ARGS3 = dict( | |||||||
|     arg1="v1", |     arg1="v1", | ||||||
|     _anchor="anchor", |     _anchor="anchor", | ||||||
|     _scheme="http", |     _scheme="http", | ||||||
|     _server=f"{test_host}:{test_port}", |     _server="{}:{}".format(test_host, test_port), | ||||||
|     _external=True, |     _external=True, | ||||||
| ) | ) | ||||||
| URL_FOR_VALUE3 = f"http://{test_host}:{test_port}/myurl?arg1=v1#anchor" | URL_FOR_VALUE3 = "http://{}:{}/myurl?arg1=v1#anchor".format( | ||||||
|  |     test_host, test_port | ||||||
|  | ) | ||||||
| URL_FOR_ARGS4 = dict( | URL_FOR_ARGS4 = dict( | ||||||
|     arg1="v1", |     arg1="v1", | ||||||
|     _anchor="anchor", |     _anchor="anchor", | ||||||
|     _external=True, |     _external=True, | ||||||
|     _server=f"http://{test_host}:{test_port}", |     _server="http://{}:{}".format(test_host, test_port), | ||||||
|  | ) | ||||||
|  | URL_FOR_VALUE4 = "http://{}:{}/myurl?arg1=v1#anchor".format( | ||||||
|  |     test_host, test_port | ||||||
| ) | ) | ||||||
| URL_FOR_VALUE4 = f"http://{test_host}:{test_port}/myurl?arg1=v1#anchor" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _generate_handlers_from_names(app, l): | def _generate_handlers_from_names(app, l): | ||||||
|     for name in l: |     for name in l: | ||||||
|         # this is the easiest way to generate functions with dynamic names |         # this is the easiest way to generate functions with dynamic names | ||||||
|         exec( |         exec( | ||||||
|             f'@app.route(name)\ndef {name}(request):\n\treturn text("{name}")' |             '@app.route(name)\ndef {}(request):\n\treturn text("{}")'.format( | ||||||
|  |                 name, name | ||||||
|  |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -54,7 +60,7 @@ def test_simple_url_for_getting(simple_app): | |||||||
|     for letter in string.ascii_letters: |     for letter in string.ascii_letters: | ||||||
|         url = simple_app.url_for(letter) |         url = simple_app.url_for(letter) | ||||||
|  |  | ||||||
|         assert url == f"/{letter}" |         assert url == "/{}".format(letter) | ||||||
|         request, response = simple_app.test_client.get(url) |         request, response = simple_app.test_client.get(url) | ||||||
|         assert response.status == 200 |         assert response.status == 200 | ||||||
|         assert response.text == letter |         assert response.text == letter | ||||||
| @@ -82,7 +88,7 @@ def test_simple_url_for_getting_with_more_params(app, args, url): | |||||||
|  |  | ||||||
| def test_url_for_with_server_name(app): | def test_url_for_with_server_name(app): | ||||||
|  |  | ||||||
|     server_name = f"{test_host}:{test_port}" |     server_name = "{}:{}".format(test_host, test_port) | ||||||
|     app.config.update({"SERVER_NAME": server_name}) |     app.config.update({"SERVER_NAME": server_name}) | ||||||
|     path = "/myurl" |     path = "/myurl" | ||||||
|  |  | ||||||
| @@ -90,7 +96,7 @@ def test_url_for_with_server_name(app): | |||||||
|     def passes(request): |     def passes(request): | ||||||
|         return text("this should pass") |         return text("this should pass") | ||||||
|  |  | ||||||
|     url = f"http://{server_name}{path}" |     url = "http://{}{}".format(server_name, path) | ||||||
|     assert url == app.url_for("passes", _server=None, _external=True) |     assert url == app.url_for("passes", _server=None, _external=True) | ||||||
|     request, response = app.test_client.get(url) |     request, response = app.test_client.get(url) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
| @@ -112,7 +118,7 @@ def test_fails_url_build_if_param_not_passed(app): | |||||||
|     url = "/" |     url = "/" | ||||||
|  |  | ||||||
|     for letter in string.ascii_letters: |     for letter in string.ascii_letters: | ||||||
|         url += f"<{letter}>/" |         url += "<{}>/".format(letter) | ||||||
|  |  | ||||||
|     @app.route(url) |     @app.route(url) | ||||||
|     def fail(request): |     def fail(request): | ||||||
| @@ -176,7 +182,7 @@ def test_passes_with_negative_int_message(app): | |||||||
|     @app.route("path/<possibly_neg:int>/another-word") |     @app.route("path/<possibly_neg:int>/another-word") | ||||||
|     def good(request, possibly_neg): |     def good(request, possibly_neg): | ||||||
|         assert isinstance(possibly_neg, int) |         assert isinstance(possibly_neg, int) | ||||||
|         return text(f"this should pass with `{possibly_neg}`") |         return text("this should pass with `{}`".format(possibly_neg)) | ||||||
|  |  | ||||||
|     u_plus_3 = app.url_for("good", possibly_neg=3) |     u_plus_3 = app.url_for("good", possibly_neg=3) | ||||||
|     assert u_plus_3 == "/path/3/another-word", u_plus_3 |     assert u_plus_3 == "/path/3/another-word", u_plus_3 | ||||||
| @@ -231,13 +237,13 @@ def test_passes_with_negative_number_message(app, number): | |||||||
|     @app.route("path/<possibly_neg:number>/another-word") |     @app.route("path/<possibly_neg:number>/another-word") | ||||||
|     def good(request, possibly_neg): |     def good(request, possibly_neg): | ||||||
|         assert isinstance(possibly_neg, (int, float)) |         assert isinstance(possibly_neg, (int, float)) | ||||||
|         return text(f"this should pass with `{possibly_neg}`") |         return text("this should pass with `{}`".format(possibly_neg)) | ||||||
|  |  | ||||||
|     u = app.url_for("good", possibly_neg=number) |     u = app.url_for("good", possibly_neg=number) | ||||||
|     assert u == f"/path/{number}/another-word", u |     assert u == "/path/{}/another-word".format(number), u | ||||||
|     request, response = app.test_client.get(u) |     request, response = app.test_client.get(u) | ||||||
|     # For ``number``, it has been cast to a float - so a ``3`` becomes a ``3.0`` |     # For ``number``, it has been cast to a float - so a ``3`` becomes a ``3.0`` | ||||||
|     assert response.text == f"this should pass with `{float(number)}`" |     assert response.text == "this should pass with `{}`".format(float(number)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_adds_other_supplied_values_as_query_string(app): | def test_adds_other_supplied_values_as_query_string(app): | ||||||
| @@ -269,7 +275,7 @@ def blueprint_app(app): | |||||||
|  |  | ||||||
|     @first_print.route("/foo/<param>") |     @first_print.route("/foo/<param>") | ||||||
|     def foo_with_param(request, param): |     def foo_with_param(request, param): | ||||||
|         return text(f"foo from first : {param}") |         return text("foo from first : {}".format(param)) | ||||||
|  |  | ||||||
|     @second_print.route("/foo")  # noqa |     @second_print.route("/foo")  # noqa | ||||||
|     def foo(request): |     def foo(request): | ||||||
| @@ -277,7 +283,7 @@ def blueprint_app(app): | |||||||
|  |  | ||||||
|     @second_print.route("/foo/<param>")  # noqa |     @second_print.route("/foo/<param>")  # noqa | ||||||
|     def foo_with_param(request, param): |     def foo_with_param(request, param): | ||||||
|         return text(f"foo from second : {param}") |         return text("foo from second : {}".format(param)) | ||||||
|  |  | ||||||
|     app.blueprint(first_print) |     app.blueprint(first_print) | ||||||
|     app.blueprint(second_print) |     app.blueprint(second_print) | ||||||
|   | |||||||
| @@ -1,63 +0,0 @@ | |||||||
| import asyncio |  | ||||||
|  |  | ||||||
| from sanic.blueprints import Blueprint |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_routes_with_host(app): |  | ||||||
|     @app.route("/") |  | ||||||
|     @app.route("/", name="hostindex", host="example.com") |  | ||||||
|     @app.route("/path", name="hostpath", host="path.example.com") |  | ||||||
|     def index(request): |  | ||||||
|         pass |  | ||||||
|  |  | ||||||
|     assert app.url_for("index") == "/" |  | ||||||
|     assert app.url_for("hostindex") == "/" |  | ||||||
|     assert app.url_for("hostpath") == "/path" |  | ||||||
|     assert app.url_for("hostindex", _external=True) == "http://example.com/" |  | ||||||
|     assert ( |  | ||||||
|         app.url_for("hostpath", _external=True) |  | ||||||
|         == "http://path.example.com/path" |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_websocket_bp_route_name(app): |  | ||||||
|     """Tests that blueprint websocket route is named.""" |  | ||||||
|     event = asyncio.Event() |  | ||||||
|     bp = Blueprint("test_bp", url_prefix="/bp") |  | ||||||
|  |  | ||||||
|     @bp.get("/main") |  | ||||||
|     async def main(request): |  | ||||||
|         ... |  | ||||||
|  |  | ||||||
|     @bp.websocket("/route") |  | ||||||
|     async def test_route(request, ws): |  | ||||||
|         event.set() |  | ||||||
|  |  | ||||||
|     @bp.websocket("/route2") |  | ||||||
|     async def test_route2(request, ws): |  | ||||||
|         event.set() |  | ||||||
|  |  | ||||||
|     @bp.websocket("/route3", name="foobar_3") |  | ||||||
|     async def test_route3(request, ws): |  | ||||||
|         event.set() |  | ||||||
|  |  | ||||||
|     app.blueprint(bp) |  | ||||||
|  |  | ||||||
|     uri = app.url_for("test_bp.main") |  | ||||||
|     assert uri == "/bp/main" |  | ||||||
|  |  | ||||||
|     uri = app.url_for("test_bp.test_route") |  | ||||||
|     assert uri == "/bp/route" |  | ||||||
|     request, response = app.test_client.websocket(uri) |  | ||||||
|     assert response.opened is True |  | ||||||
|     assert event.is_set() |  | ||||||
|  |  | ||||||
|     event.clear() |  | ||||||
|     uri = app.url_for("test_bp.test_route2") |  | ||||||
|     assert uri == "/bp/route2" |  | ||||||
|     request, response = app.test_client.websocket(uri) |  | ||||||
|     assert response.opened is True |  | ||||||
|     assert event.is_set() |  | ||||||
|  |  | ||||||
|     uri = app.url_for("test_bp.foobar_3") |  | ||||||
|     assert uri == "/bp/route3" |  | ||||||
| @@ -118,7 +118,7 @@ def test_static_directory(app, file_name, base_uri, static_file_directory): | |||||||
|     app.static(base_uri2, static_file_directory, name="uploads") |     app.static(base_uri2, static_file_directory, name="uploads") | ||||||
|  |  | ||||||
|     uri = app.url_for("static", name="static", filename=file_name) |     uri = app.url_for("static", name="static", filename=file_name) | ||||||
|     assert uri == f"{base_uri}/{file_name}" |     assert uri == "{}/{}".format(base_uri, file_name) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(uri) |     request, response = app.test_client.get(uri) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
| @@ -134,7 +134,7 @@ def test_static_directory(app, file_name, base_uri, static_file_directory): | |||||||
|     assert uri2 == uri3 |     assert uri2 == uri3 | ||||||
|     assert uri3 == uri4 |     assert uri3 == uri4 | ||||||
|  |  | ||||||
|     assert uri5 == f"{base_uri2}/{file_name}" |     assert uri5 == "{}/{}".format(base_uri2, file_name) | ||||||
|     assert uri5 == uri6 |     assert uri5 == uri6 | ||||||
|  |  | ||||||
|     bp = Blueprint("test_bp_static", url_prefix="/bp") |     bp = Blueprint("test_bp_static", url_prefix="/bp") | ||||||
| @@ -157,10 +157,10 @@ def test_static_directory(app, file_name, base_uri, static_file_directory): | |||||||
|         "static", name="test_bp_static.uploads", filename="/" + file_name |         "static", name="test_bp_static.uploads", filename="/" + file_name | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     assert uri == f"/bp{base_uri}/{file_name}" |     assert uri == "/bp{}/{}".format(base_uri, file_name) | ||||||
|     assert uri == uri2 |     assert uri == uri2 | ||||||
|  |  | ||||||
|     assert uri4 == f"/bp{base_uri2}/{file_name}" |     assert uri4 == "/bp{}/{}".format(base_uri2, file_name) | ||||||
|     assert uri4 == uri5 |     assert uri4 == uri5 | ||||||
|  |  | ||||||
|     request, response = app.test_client.get(uri) |     request, response = app.test_client.get(uri) | ||||||
|   | |||||||
| @@ -48,3 +48,17 @@ def test_vhosts_with_defaults(app): | |||||||
|  |  | ||||||
|     request, response = app.test_client.get("/") |     request, response = app.test_client.get("/") | ||||||
|     assert response.text == "default" |     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 | ||||||
|   | |||||||
| @@ -49,7 +49,7 @@ def test_unexisting_methods(app): | |||||||
|     request, response = app.test_client.get("/") |     request, response = app.test_client.get("/") | ||||||
|     assert response.text == "I am get method" |     assert response.text == "I am get method" | ||||||
|     request, response = app.test_client.post("/") |     request, response = app.test_client.post("/") | ||||||
|     assert "Method POST not allowed for URL /" in response.text |     assert response.text == "Error: Method POST not allowed for URL /" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_argument_methods(app): | def test_argument_methods(app): | ||||||
| @@ -151,7 +151,8 @@ def test_with_custom_class_methods(app): | |||||||
|         def get(self, request): |         def get(self, request): | ||||||
|             self._iternal_method() |             self._iternal_method() | ||||||
|             return text( |             return text( | ||||||
|                 f"I am get method and global var " f"is {self.global_var}" |                 "I am get method and global var " | ||||||
|  |                 "is {}".format(self.global_var) | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     app.add_route(DummyView.as_view(), "/") |     app.add_route(DummyView.as_view(), "/") | ||||||
|   | |||||||
| @@ -129,10 +129,6 @@ def test_handle_quit(worker): | |||||||
|     assert worker.exit_code == 0 |     assert worker.exit_code == 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| async def _a_noop(*a, **kw): |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_run_max_requests_exceeded(worker): | def test_run_max_requests_exceeded(worker): | ||||||
|     loop = asyncio.new_event_loop() |     loop = asyncio.new_event_loop() | ||||||
|     worker.ppid = 1 |     worker.ppid = 1 | ||||||
| @@ -149,7 +145,7 @@ def test_run_max_requests_exceeded(worker): | |||||||
|         "server2": {"requests_count": 15}, |         "server2": {"requests_count": 15}, | ||||||
|     } |     } | ||||||
|     worker.max_requests = 10 |     worker.max_requests = 10 | ||||||
|     worker._run = mock.Mock(wraps=_a_noop) |     worker._run = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None)) | ||||||
|  |  | ||||||
|     # exceeding request count |     # exceeding request count | ||||||
|     _runner = asyncio.ensure_future(worker._check_alive(), loop=loop) |     _runner = asyncio.ensure_future(worker._check_alive(), loop=loop) | ||||||
| @@ -164,7 +160,7 @@ def test_run_max_requests_exceeded(worker): | |||||||
|  |  | ||||||
| def test_worker_close(worker): | def test_worker_close(worker): | ||||||
|     loop = asyncio.new_event_loop() |     loop = asyncio.new_event_loop() | ||||||
|     asyncio.sleep = mock.Mock(wraps=_a_noop) |     asyncio.sleep = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None)) | ||||||
|     worker.ppid = 1 |     worker.ppid = 1 | ||||||
|     worker.pid = 2 |     worker.pid = 2 | ||||||
|     worker.cfg.graceful_timeout = 1.0 |     worker.cfg.graceful_timeout = 1.0 | ||||||
| @@ -173,13 +169,17 @@ def test_worker_close(worker): | |||||||
|     worker.wsgi = mock.Mock() |     worker.wsgi = mock.Mock() | ||||||
|     conn = mock.Mock() |     conn = mock.Mock() | ||||||
|     conn.websocket = mock.Mock() |     conn.websocket = mock.Mock() | ||||||
|     conn.websocket.close_connection = mock.Mock(wraps=_a_noop) |     conn.websocket.close_connection = mock.Mock( | ||||||
|  |         wraps=asyncio.coroutine(lambda *a, **kw: None) | ||||||
|  |     ) | ||||||
|     worker.connections = set([conn]) |     worker.connections = set([conn]) | ||||||
|     worker.log = mock.Mock() |     worker.log = mock.Mock() | ||||||
|     worker.loop = loop |     worker.loop = loop | ||||||
|     server = mock.Mock() |     server = mock.Mock() | ||||||
|     server.close = mock.Mock(wraps=lambda *a, **kw: None) |     server.close = mock.Mock(wraps=lambda *a, **kw: None) | ||||||
|     server.wait_closed = mock.Mock(wraps=_a_noop) |     server.wait_closed = mock.Mock( | ||||||
|  |         wraps=asyncio.coroutine(lambda *a, **kw: None) | ||||||
|  |     ) | ||||||
|     worker.servers = {server: {"requests_count": 14}} |     worker.servers = {server: {"requests_count": 14}} | ||||||
|     worker.max_requests = 10 |     worker.max_requests = 10 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,11 +1,11 @@ | |||||||
| [tox] | [tox] | ||||||
| envlist = py36, py37, py38, pyNightly, {py36,py37,py38,pyNightly}-no-ext, lint, check, security, docs | envlist = py36, py37, pyNightly, {py36,py37,pyNightly}-no-ext, lint, check, security, docs | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
| usedevelop = True | usedevelop = True | ||||||
| setenv = | setenv = | ||||||
|     {py36,py37,py38,pyNightly}-no-ext: SANIC_NO_UJSON=1 |     {py36,py37,pyNightly}-no-ext: SANIC_NO_UJSON=1 | ||||||
|     {py36,py37,py38,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 |     {py36,py37,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 | ||||||
| deps = | deps = | ||||||
|     coverage |     coverage | ||||||
|     pytest==5.2.1 |     pytest==5.2.1 | ||||||
| @@ -13,13 +13,13 @@ deps = | |||||||
|     pytest-sanic |     pytest-sanic | ||||||
|     pytest-sugar |     pytest-sugar | ||||||
|     httpcore==0.3.0 |     httpcore==0.3.0 | ||||||
|     httpx==0.11.1 |     httpx==0.9.3 | ||||||
|     chardet<=2.3.0 |     chardet<=2.3.0 | ||||||
|     beautifulsoup4 |     beautifulsoup4 | ||||||
|     gunicorn |     gunicorn | ||||||
|     pytest-benchmark |     pytest-benchmark | ||||||
|     uvicorn |     uvicorn | ||||||
|     websockets>=8.1,<9.0 |     websockets>=7.0,<8.0 | ||||||
| commands = | commands = | ||||||
|     pytest {posargs:tests --cov sanic} |     pytest {posargs:tests --cov sanic} | ||||||
|     - coverage combine --append |     - coverage combine --append | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user