Compare commits
	
		
			14 Commits
		
	
	
		
			pre-regist
			...
			v21.12.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | c4da66bf1f | ||
|   | d50d3b8448 | ||
|   | 313f97ac77 | ||
|   | a23547d73b | ||
|   | 34d1dee407 | ||
|   | dc3ccba527 | ||
|   | b91ffed010 | ||
|   | 8c07e388cd | ||
|   | 98ce4bdeb2 | ||
|   | 4659069350 | ||
|   | 080d41627a | ||
|   | d799c5f03c | ||
|   | abe062b371 | ||
|   | b5a00ac1ca | 
							
								
								
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -2,9 +2,13 @@ name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ main ] | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|   pull_request: | ||||
|     branches: [ main ] | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|   schedule: | ||||
|     - cron: '25 16 * * 0' | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     tags: | ||||
|       - "!*" # Do not execute on tags | ||||
|   pull_request: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
| @@ -15,7 +16,7 @@ jobs: | ||||
|       matrix: | ||||
|         os: [ubuntu-latest] | ||||
|         config: | ||||
|           - { python-version: 3.7, tox-env: type-checking} | ||||
|           # - { python-version: 3.7, tox-env: type-checking} | ||||
|           - { python-version: 3.8, tox-env: type-checking} | ||||
|           - { python-version: 3.9, tox-env: type-checking} | ||||
|           - { python-version: "3.10", tox-env: type-checking} | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										192
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							| @@ -1,12 +1,12 @@ | ||||
| .. note:: | ||||
|  | ||||
|   From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases`` | ||||
|   CHANGELOG files are maintained in ``./docs/sanic/releases``. To view the full CHANGELOG, please visit https://sanic.readthedocs.io/en/stable/sanic/changelog.html. | ||||
|  | ||||
|  | ||||
| Version 21.6.1 | ||||
| -------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_ | ||||
|     Update sanic-routing to allow for better splitting of complex URI templates | ||||
| @@ -20,8 +20,7 @@ Bugfixes | ||||
| Version 21.6.0 | ||||
| -------------- | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_ | ||||
|     Add ``response.eof()`` method for closing a stream in a handler | ||||
| @@ -68,8 +67,7 @@ Features | ||||
|   * `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_ | ||||
|     Additional methods for attaching ``HTTPMethodView`` | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_ | ||||
|     Fix ``UserWarning`` in ASGI mode for missing ``__slots__`` | ||||
| @@ -85,8 +83,7 @@ Bugfixes | ||||
|     Fix issue where Blueprint exception handlers do not consistently route to proper handler | ||||
|  | ||||
|  | ||||
| Deprecations and Removals | ||||
| ************************* | ||||
| **Deprecations and Removals** | ||||
|  | ||||
|   * `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_ | ||||
|     Remove config value ``REQUEST_BUFFER_QUEUE_SIZE`` | ||||
| @@ -95,14 +92,12 @@ Deprecations and Removals | ||||
|   * `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_ | ||||
|     Deprecate StreamingHTTPResponse | ||||
|  | ||||
| Developer infrastructure | ||||
| ************************ | ||||
| **Developer infrastructure** | ||||
|  | ||||
|   * `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_ | ||||
|     Remove Travis CI in favor of GitHub Actions | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
| **Improved Documentation** | ||||
|  | ||||
|   * `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_ | ||||
|     Fix typo in documentation | ||||
| @@ -112,8 +107,7 @@ Improved Documentation | ||||
| Version 21.3.2 | ||||
| -------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_ | ||||
|     Disable response timeout on websocket connections | ||||
| @@ -124,8 +118,7 @@ Bugfixes | ||||
| Version 21.3.1 | ||||
| -------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_ | ||||
|     Static files inside subfolders are not accessible (404) | ||||
| @@ -135,8 +128,7 @@ Version 21.3.0 | ||||
|  | ||||
| `Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_ | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * | ||||
|     `#1876 <https://github.com/sanic-org/sanic/pull/1876>`_ | ||||
| @@ -189,8 +181,7 @@ Features | ||||
|     `#2063 <https://github.com/sanic-org/sanic/pull/2063>`_ | ||||
|     App and connection level context objects | ||||
|  | ||||
| Bugfixes and issues resolved | ||||
| **************************** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_ | ||||
|     ``url_for`` where ``strict_slashes`` are on for a path ending in ``/`` | ||||
| @@ -220,8 +211,7 @@ Bugfixes and issues resolved | ||||
|     `#2001 <https://github.com/sanic-org/sanic/pull/2001>`_ | ||||
|     Raise ValueError when cookie max-age is not an integer | ||||
|  | ||||
| Deprecations and Removals | ||||
| ************************* | ||||
| **Deprecations and Removals** | ||||
|  | ||||
|   * | ||||
|     `#2007 <https://github.com/sanic-org/sanic/pull/2007>`_ | ||||
| @@ -240,8 +230,7 @@ Deprecations and Removals | ||||
|   * ``Request.endpoint`` deprecated in favor of ``Request.name`` | ||||
|   * handler type name prefixes removed (static, websocket, etc) | ||||
|  | ||||
| Developer infrastructure | ||||
| ************************ | ||||
| **Developer infrastructure** | ||||
|  | ||||
|   * | ||||
|     `#1995 <https://github.com/sanic-org/sanic/pull/1995>`_ | ||||
| @@ -259,8 +248,7 @@ Developer infrastructure | ||||
|     `#2049 <https://github.com/sanic-org/sanic/pull/2049>`_ | ||||
|     Updated setup.py to use ``find_packages`` | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
| **Improved Documentation** | ||||
|  | ||||
|   * | ||||
|     `#1218 <https://github.com/sanic-org/sanic/pull/1218>`_ | ||||
| @@ -282,8 +270,7 @@ Improved Documentation | ||||
|     `#2052 <https://github.com/sanic-org/sanic/pull/2052>`_ | ||||
|     Fix some examples and docs | ||||
|  | ||||
| Miscellaneous | ||||
| ************* | ||||
| **Miscellaneous** | ||||
|  | ||||
|   * ``Request.route`` property | ||||
|   * Better websocket subprotocols support | ||||
| @@ -329,8 +316,7 @@ Miscellaneous | ||||
| Version 20.12.3 | ||||
| --------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * | ||||
|     `#2021 <https://github.com/sanic-org/sanic/pull/2021>`_ | ||||
| @@ -339,8 +325,7 @@ Bugfixes | ||||
| Version 20.12.2 | ||||
| --------------- | ||||
|  | ||||
| Dependencies | ||||
| ************ | ||||
| **Dependencies** | ||||
|  | ||||
|   * | ||||
|     `#2026 <https://github.com/sanic-org/sanic/pull/2026>`_ | ||||
| @@ -353,8 +338,7 @@ Dependencies | ||||
| Version 19.12.5 | ||||
| --------------- | ||||
|  | ||||
| Dependencies | ||||
| ************ | ||||
| **Dependencies** | ||||
|  | ||||
|   * | ||||
|     `#2025 <https://github.com/sanic-org/sanic/pull/2025>`_ | ||||
| @@ -367,8 +351,7 @@ Dependencies | ||||
| Version 20.12.0 | ||||
| --------------- | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * | ||||
|     `#1993 <https://github.com/sanic-org/sanic/pull/1993>`_ | ||||
| @@ -377,8 +360,7 @@ Features | ||||
| Version 20.12.0 | ||||
| --------------- | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * | ||||
|     `#1945 <https://github.com/sanic-org/sanic/pull/1945>`_ | ||||
| @@ -416,22 +398,19 @@ Features | ||||
|     `#1979 <https://github.com/sanic-org/sanic/pull/1979>`_ | ||||
|     Add app registry and Sanic class level app retrieval | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * | ||||
|     `#1965 <https://github.com/sanic-org/sanic/pull/1965>`_ | ||||
|     Fix Chunked Transport-Encoding in ASGI streaming response | ||||
|  | ||||
| Deprecations and Removals | ||||
| ************************* | ||||
| **Deprecations and Removals** | ||||
|  | ||||
|   * | ||||
|     `#1981 <https://github.com/sanic-org/sanic/pull/1981>`_ | ||||
|     Cleanup and remove deprecated code | ||||
|  | ||||
| Developer infrastructure | ||||
| ************************ | ||||
| **Developer infrastructure** | ||||
|  | ||||
|   * | ||||
|     `#1956 <https://github.com/sanic-org/sanic/pull/1956>`_ | ||||
| @@ -445,8 +424,7 @@ Developer infrastructure | ||||
|     `#1986 <https://github.com/sanic-org/sanic/pull/1986>`_ | ||||
|     Update tox requirements | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
| **Improved Documentation** | ||||
|  | ||||
|   * | ||||
|     `#1951 <https://github.com/sanic-org/sanic/pull/1951>`_ | ||||
| @@ -464,8 +442,7 @@ Improved Documentation | ||||
| Version 20.9.1 | ||||
| --------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * | ||||
|     `#1954 <https://github.com/sanic-org/sanic/pull/1954>`_ | ||||
| @@ -478,8 +455,7 @@ Bugfixes | ||||
| Version 19.12.3 | ||||
| --------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * | ||||
|     `#1959 <https://github.com/sanic-org/sanic/pull/1959>`_ | ||||
| @@ -490,8 +466,7 @@ Version 20.9.0 | ||||
| --------------- | ||||
|  | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * | ||||
|     `#1887 <https://github.com/sanic-org/sanic/pull/1887>`_ | ||||
| @@ -518,22 +493,19 @@ Features | ||||
|     `#1937 <https://github.com/sanic-org/sanic/pull/1937>`_ | ||||
|     Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto) | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * | ||||
|     `#1897 <https://github.com/sanic-org/sanic/pull/1897>`_ | ||||
|     Resolves exception from unread bytes in stream | ||||
|  | ||||
| Deprecations and Removals | ||||
| ************************* | ||||
| **Deprecations and Removals** | ||||
|  | ||||
|   * | ||||
|     `#1903 <https://github.com/sanic-org/sanic/pull/1903>`_ | ||||
|     config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3 | ||||
|  | ||||
| Developer infrastructure | ||||
| ************************ | ||||
| **Developer infrastructure** | ||||
|  | ||||
|   * | ||||
|     `#1890 <https://github.com/sanic-org/sanic/pull/1890>`_, | ||||
| @@ -548,8 +520,7 @@ Developer infrastructure | ||||
|     `#1924 <https://github.com/sanic-org/sanic/pull/1924>`_ | ||||
|     Adding --strict-markers for pytest | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
| **Improved Documentation** | ||||
|  | ||||
|   * | ||||
|     `#1922 <https://github.com/sanic-org/sanic/pull/1922>`_ | ||||
| @@ -559,8 +530,7 @@ Improved Documentation | ||||
| Version 20.6.3 | ||||
| --------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * | ||||
|     `#1884 <https://github.com/sanic-org/sanic/pull/1884>`_ | ||||
| @@ -570,8 +540,7 @@ Bugfixes | ||||
| Version 20.6.2 | ||||
| --------------- | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * | ||||
|     `#1641 <https://github.com/sanic-org/sanic/pull/1641>`_ | ||||
| @@ -581,8 +550,7 @@ Features | ||||
| Version 20.6.1 | ||||
| --------------- | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * | ||||
|     `#1760 <https://github.com/sanic-org/sanic/pull/1760>`_ | ||||
| @@ -596,8 +564,7 @@ Features | ||||
|     `#1880 <https://github.com/sanic-org/sanic/pull/1880>`_ | ||||
|     Add handler names for websockets for url_for usage | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * | ||||
|     `#1776 <https://github.com/sanic-org/sanic/pull/1776>`_ | ||||
| @@ -619,15 +586,13 @@ Bugfixes | ||||
|     `#1853 <https://github.com/sanic-org/sanic/pull/1853>`_ | ||||
|     Fix pickle error when attempting to pickle an application which contains websocket routes | ||||
|  | ||||
| Deprecations and Removals | ||||
| ************************* | ||||
| **Deprecations and Removals** | ||||
|  | ||||
|   * | ||||
|     `#1739 <https://github.com/sanic-org/sanic/pull/1739>`_ | ||||
|     Deprecate body_bytes to merge into body | ||||
|  | ||||
| Developer infrastructure | ||||
| ************************ | ||||
| **Developer infrastructure** | ||||
|  | ||||
|   * | ||||
|     `#1852 <https://github.com/sanic-org/sanic/pull/1852>`_ | ||||
| @@ -642,8 +607,7 @@ Developer infrastructure | ||||
|     Wrap run()'s "protocol" type annotation in Optional[] | ||||
|  | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
| **Improved Documentation** | ||||
|  | ||||
|   * | ||||
|     `#1846 <https://github.com/sanic-org/sanic/pull/1846>`_ | ||||
| @@ -663,8 +627,7 @@ Version 20.6.0 | ||||
| Version 20.3.0 | ||||
| --------------- | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * | ||||
|     `#1762 <https://github.com/sanic-org/sanic/pull/1762>`_ | ||||
| @@ -695,8 +658,7 @@ Features | ||||
|     `#1820 <https://github.com/sanic-org/sanic/pull/1820>`_ | ||||
|     Do not set content-type and content-length headers in exceptions | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * | ||||
|     `#1748 <https://github.com/sanic-org/sanic/pull/1748>`_ | ||||
| @@ -714,8 +676,7 @@ Bugfixes | ||||
|     `#1808 <https://github.com/sanic-org/sanic/pull/1808>`_ | ||||
|      Fix Ctrl+C and tests on Windows | ||||
|  | ||||
| Deprecations and Removals | ||||
| ************************* | ||||
| **Deprecations and Removals** | ||||
|  | ||||
|   * | ||||
|     `#1800 <https://github.com/sanic-org/sanic/pull/1800>`_ | ||||
| @@ -733,8 +694,7 @@ Deprecations and Removals | ||||
|     `#1818 <https://github.com/sanic-org/sanic/pull/1818>`_ | ||||
|     Complete deprecation of ``app.remove_route`` and ``request.raw_args`` | ||||
|  | ||||
| Dependencies | ||||
| ************ | ||||
| **Dependencies** | ||||
|  | ||||
|   * | ||||
|     `#1794 <https://github.com/sanic-org/sanic/pull/1794>`_ | ||||
| @@ -744,15 +704,13 @@ Dependencies | ||||
|     `#1806 <https://github.com/sanic-org/sanic/pull/1806>`_ | ||||
|     Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation) | ||||
|  | ||||
| Developer infrastructure | ||||
| ************************ | ||||
| **Developer infrastructure** | ||||
|  | ||||
|   * | ||||
|     `#1833 <https://github.com/sanic-org/sanic/pull/1833>`_ | ||||
|     Resolve broken documentation builds | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
| **Improved Documentation** | ||||
|  | ||||
|   * | ||||
|     `#1755 <https://github.com/sanic-org/sanic/pull/1755>`_ | ||||
| @@ -794,8 +752,7 @@ Improved Documentation | ||||
| Version 19.12.0 | ||||
| --------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
| - Fix blueprint middleware application | ||||
|  | ||||
| @@ -814,8 +771,7 @@ Bugfixes | ||||
|   due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__) | ||||
|  | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
| **Improved Documentation** | ||||
|  | ||||
| - Move docs from MD to RST | ||||
|  | ||||
| @@ -829,8 +785,7 @@ Improved Documentation | ||||
| Version 19.6.3 | ||||
| -------------- | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
| - Enable Towncrier Support | ||||
|  | ||||
| @@ -838,8 +793,7 @@ Features | ||||
|   of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__) | ||||
|  | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
| **Improved Documentation** | ||||
|  | ||||
| - Documentation infrastructure changes | ||||
|  | ||||
| @@ -852,8 +806,7 @@ Improved Documentation | ||||
| Version 19.6.2 | ||||
| -------------- | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * | ||||
|     `#1562 <https://github.com/sanic-org/sanic/pull/1562>`_ | ||||
| @@ -869,8 +822,7 @@ Features | ||||
|     Add Configure support from object string | ||||
|  | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|   * | ||||
|     `#1587 <https://github.com/sanic-org/sanic/pull/1587>`_ | ||||
| @@ -888,8 +840,7 @@ Bugfixes | ||||
|     `#1594 <https://github.com/sanic-org/sanic/pull/1594>`_ | ||||
|     Strict Slashes behavior fix | ||||
|  | ||||
| Deprecations and Removals | ||||
| ************************* | ||||
| **Deprecations and Removals** | ||||
|  | ||||
|   * | ||||
|     `#1544 <https://github.com/sanic-org/sanic/pull/1544>`_ | ||||
| @@ -913,8 +864,7 @@ Deprecations and Removals | ||||
| Version 19.3 | ||||
| ------------ | ||||
|  | ||||
| Features | ||||
| ******** | ||||
| **Features** | ||||
|  | ||||
|   * | ||||
|     `#1497 <https://github.com/sanic-org/sanic/pull/1497>`_ | ||||
| @@ -982,8 +932,7 @@ Features | ||||
|  | ||||
|     This is a breaking change. | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
| **Bugfixes** | ||||
|  | ||||
|  | ||||
|   * | ||||
| @@ -1019,8 +968,7 @@ Bugfixes | ||||
|     This allows the access log to be disabled for example when running via | ||||
|     gunicorn. | ||||
|  | ||||
| Developer infrastructure | ||||
| ************************ | ||||
| **Developer infrastructure** | ||||
|  | ||||
|   * `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials | ||||
|   * `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514) | ||||
| @@ -1028,8 +976,7 @@ Developer infrastructure | ||||
|   * `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build | ||||
|   * `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
| **Improved Documentation** | ||||
|  | ||||
|   * `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation | ||||
|   * `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example | ||||
| @@ -1096,15 +1043,13 @@ Version 18.12 | ||||
| Version 0.8 | ||||
| ----------- | ||||
|  | ||||
| 0.8.3 | ||||
| ***** | ||||
| **0.8.3** | ||||
|  | ||||
| * Changes: | ||||
|  | ||||
|   * Ownership changed to org 'sanic-org' | ||||
|  | ||||
| 0.8.0 | ||||
| ***** | ||||
| **0.8.0** | ||||
|  | ||||
| * Changes: | ||||
|  | ||||
| @@ -1184,19 +1129,16 @@ Version 0.1 | ||||
| ----------- | ||||
|  | ||||
|  | ||||
| 0.1.7 | ||||
| ***** | ||||
| **0.1.7** | ||||
|  | ||||
|   * Reversed static url and directory arguments to meet spec | ||||
|  | ||||
| 0.1.6 | ||||
| ***** | ||||
| **0.1.6** | ||||
|  | ||||
|   * Static files | ||||
|   * Lazy Cookie Loading | ||||
|  | ||||
| 0.1.5 | ||||
| ***** | ||||
| **0.1.5** | ||||
|  | ||||
|   * Cookies | ||||
|   * Blueprint listeners and ordering | ||||
| @@ -1204,23 +1146,19 @@ Version 0.1 | ||||
|   * Fix: Incomplete file reads on medium+ sized post requests | ||||
|   * Breaking: after_start and before_stop now pass sanic as their first argument | ||||
|  | ||||
| 0.1.4 | ||||
| ***** | ||||
| **0.1.4** | ||||
|  | ||||
|   * Multiprocessing | ||||
|  | ||||
| 0.1.3 | ||||
| ***** | ||||
| **0.1.3** | ||||
|  | ||||
|   * Blueprint support | ||||
|   * Faster Response processing | ||||
|  | ||||
| 0.1.1 - 0.1.2 | ||||
| ************* | ||||
| **0.1.1 - 0.1.2** | ||||
|  | ||||
|   * Struggling to update pypi via CI | ||||
|  | ||||
| 0.1.0 | ||||
| ***** | ||||
| **0.1.0** | ||||
|  | ||||
|   * Released to public | ||||
|   | ||||
| @@ -11,7 +11,7 @@ Sanic | Build fast. Run fast. | ||||
|     :stub-columns: 1 | ||||
|  | ||||
|     * - Build | ||||
|       - | |Py39Test| |Py38Test| |Py37Test| | ||||
|       - | |Py310Test| |Py39Test| |Py38Test| |Py37Test| | ||||
|     * - Docs | ||||
|       - | |UserGuide| |Documentation| | ||||
|     * - Package | ||||
| @@ -27,6 +27,8 @@ Sanic | Build fast. Run fast. | ||||
|    :target: https://community.sanicframework.org/ | ||||
| .. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord | ||||
|    :target: https://discord.gg/FARQzAEMAA | ||||
| .. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main | ||||
|    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml | ||||
| .. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main | ||||
|    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml | ||||
| .. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main | ||||
|   | ||||
| @@ -38,10 +38,3 @@ sanic.views | ||||
| .. automodule:: sanic.views | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.websocket | ||||
| --------------- | ||||
|  | ||||
| .. automodule:: sanic.websocket | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| 📜 Changelog | ||||
| ============ | ||||
|  | ||||
| .. mdinclude:: ./releases/21.9.md | ||||
|  | ||||
| .. mdinclude:: ./releases/21/21.12.md | ||||
| .. mdinclude:: ./releases/21/21.9.md | ||||
| .. include:: ../../CHANGELOG.rst | ||||
|   | ||||
							
								
								
									
										64
									
								
								docs/sanic/releases/21/21.12.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								docs/sanic/releases/21/21.12.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| ## Version 21.12.1 | ||||
|  | ||||
| - [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup | ||||
| - [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7 | ||||
| - [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values | ||||
|  | ||||
| ## Version 21.12.0 | ||||
|  | ||||
| ### Features | ||||
| - [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects | ||||
| - [#2262](https://github.com/sanic-org/sanic/pull/2262) Noisy exceptions - force logging of all exceptions | ||||
| - [#2264](https://github.com/sanic-org/sanic/pull/2264) Optional `uvloop` by configuration | ||||
| - [#2270](https://github.com/sanic-org/sanic/pull/2270) Vhost support using multiple TLS certificates | ||||
| - [#2277](https://github.com/sanic-org/sanic/pull/2277) Change signal routing for increased consistency | ||||
|     - *BREAKING CHANGE*: If you were manually routing signals there is a breaking change. The signal router's `get` is no longer 100% determinative. There is now an additional step to loop thru the returned signals for proper matching on the requirements. If signals are being dispatched using `app.dispatch` or `bp.dispatch`, there is no change. | ||||
| - [#2290](https://github.com/sanic-org/sanic/pull/2290) Add contextual exceptions | ||||
| - [#2291](https://github.com/sanic-org/sanic/pull/2291) Increase join concat performance  | ||||
| - [#2295](https://github.com/sanic-org/sanic/pull/2295), [#2316](https://github.com/sanic-org/sanic/pull/2316), [#2331](https://github.com/sanic-org/sanic/pull/2331) Restructure of CLI and application state with new displays and more command parity with `app.run` | ||||
| - [#2302](https://github.com/sanic-org/sanic/pull/2302) Add route context at definition time | ||||
| - [#2304](https://github.com/sanic-org/sanic/pull/2304) Named tasks and new API for managing background tasks | ||||
| - [#2307](https://github.com/sanic-org/sanic/pull/2307) On app auto-reload, provide insight of changed files | ||||
| - [#2308](https://github.com/sanic-org/sanic/pull/2308) Auto extend application with [Sanic Extensions](https://sanicframework.org/en/plugins/sanic-ext/getting-started.html) if it is installed, and provide first class support for accessing the extensions | ||||
| - [#2309](https://github.com/sanic-org/sanic/pull/2309) Builtin signals changed to `Enum` | ||||
| - [#2313](https://github.com/sanic-org/sanic/pull/2313) Support additional config implementation use case | ||||
| - [#2321](https://github.com/sanic-org/sanic/pull/2321) Refactor environment variable hydration logic | ||||
| - [#2327](https://github.com/sanic-org/sanic/pull/2327) Prevent sending multiple or mixed responses on a single request | ||||
| - [#2330](https://github.com/sanic-org/sanic/pull/2330) Custom type casting on environment variables | ||||
| - [#2332](https://github.com/sanic-org/sanic/pull/2332) Make all deprecation notices consistent | ||||
| - [#2335](https://github.com/sanic-org/sanic/pull/2335) Allow underscore to start instance names | ||||
|  | ||||
| ### Bugfixes | ||||
| - [#2273](https://github.com/sanic-org/sanic/pull/2273) Replace assignation by typing for `websocket_handshake` | ||||
| - [#2285](https://github.com/sanic-org/sanic/pull/2285) Fix IPv6 display in startup logs | ||||
| - [#2299](https://github.com/sanic-org/sanic/pull/2299) Dispatch `http.lifecyle.response` from exception handler | ||||
|  | ||||
| ### Deprecations and Removals | ||||
| - [#2306](https://github.com/sanic-org/sanic/pull/2306) Removal of deprecated items | ||||
|     - `Sanic` and `Blueprint` may no longer have arbitrary properties attached to them | ||||
|     - `Sanic` and `Blueprint` forced to have compliant names | ||||
|         - alphanumeric + `_` + `-` | ||||
|         - must start with letter or `_` | ||||
|     - `load_env` keyword argument of `Sanic` | ||||
|     - `sanic.exceptions.abort` | ||||
|     - `sanic.views.CompositionView` | ||||
|     - `sanic.response.StreamingHTTPResponse` | ||||
|         - *NOTE:* the `stream()` response method (where you pass a callable streaming function) has been deprecated and will be removed in v22.6. You should upgrade all streaming responses to the new style: https://sanicframework.org/en/guide/advanced/streaming.html#response-streaming | ||||
| - [#2320](https://github.com/sanic-org/sanic/pull/2320) Remove app instance from Config for error handler setting | ||||
|  | ||||
| ### Developer infrastructure | ||||
| - [#2251](https://github.com/sanic-org/sanic/pull/2251) Change dev install command | ||||
| - [#2286](https://github.com/sanic-org/sanic/pull/2286) Change codeclimate complexity threshold from 5 to 10 | ||||
| - [#2287](https://github.com/sanic-org/sanic/pull/2287) Update host test function names so they are not overwritten | ||||
| - [#2292](https://github.com/sanic-org/sanic/pull/2292) Fail CI on error | ||||
| - [#2311](https://github.com/sanic-org/sanic/pull/2311), [#2324](https://github.com/sanic-org/sanic/pull/2324) Do not run tests for draft PRs | ||||
| - [#2336](https://github.com/sanic-org/sanic/pull/2336) Remove paths from coverage checks | ||||
| - [#2338](https://github.com/sanic-org/sanic/pull/2338) Cleanup ports on tests | ||||
|  | ||||
| ### Improved Documentation | ||||
| - [#2269](https://github.com/sanic-org/sanic/pull/2269), [#2329](https://github.com/sanic-org/sanic/pull/2329), [#2333](https://github.com/sanic-org/sanic/pull/2333) Cleanup typos and fix language | ||||
|  | ||||
| ### Miscellaneous | ||||
| - [#2257](https://github.com/sanic-org/sanic/pull/2257), [#2294](https://github.com/sanic-org/sanic/pull/2294), [#2341](https://github.com/sanic-org/sanic/pull/2341) Add Python 3.10 support | ||||
| - [#2279](https://github.com/sanic-org/sanic/pull/2279), [#2317](https://github.com/sanic-org/sanic/pull/2317), [#2322](https://github.com/sanic-org/sanic/pull/2322) Add/correct missing type annotations | ||||
| - [#2305](https://github.com/sanic-org/sanic/pull/2305) Fix examples to use modern implementations | ||||
| @@ -1,4 +1,14 @@ | ||||
| ## Version 21.9 | ||||
| ## Version 21.9.3 | ||||
| *Rerelease of v21.9.2 with some cleanup* | ||||
| 
 | ||||
| ## Version 21.9.2 | ||||
| - [#2268](https://github.com/sanic-org/sanic/pull/2268) Make HTTP connections start in IDLE stage, avoiding delays and error messages | ||||
| - [#2310](https://github.com/sanic-org/sanic/pull/2310) More consistent config setting with post-FALLBACK_ERROR_FORMAT apply | ||||
| 
 | ||||
| ## Version 21.9.1 | ||||
| - [#2259](https://github.com/sanic-org/sanic/pull/2259) Allow non-conforming ErrorHandlers | ||||
| 
 | ||||
| ## Version 21.9.0 | ||||
| 
 | ||||
| ### Features | ||||
| - [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets | ||||
| @@ -5,7 +5,7 @@ import asyncio | ||||
| from sanic import Sanic | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| async def notify_server_started_after_five_seconds(): | ||||
|   | ||||
| @@ -4,7 +4,7 @@ from sanic import Sanic | ||||
| from sanic.response import text | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.middleware("request") | ||||
|   | ||||
| @@ -6,7 +6,7 @@ from sanic import Sanic | ||||
| from sanic.response import json | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| def check_request_for_authorization_status(request): | ||||
|   | ||||
| @@ -8,9 +8,9 @@ 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__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
| bp = Blueprint("bp_" + __name__) | ||||
| bp = Blueprint("bp_example") | ||||
|  | ||||
|  | ||||
| @bp.on_request | ||||
|   | ||||
| @@ -2,10 +2,10 @@ from sanic import Blueprint, Sanic | ||||
| from sanic.response import file, json | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| blueprint = Blueprint("name", url_prefix="/my_blueprint") | ||||
| blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2") | ||||
| blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3") | ||||
| app = Sanic("Example") | ||||
| blueprint = Blueprint("bp_example", url_prefix="/my_blueprint") | ||||
| blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2") | ||||
| blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3") | ||||
|  | ||||
|  | ||||
| @blueprint.route("/foo") | ||||
|   | ||||
| @@ -3,7 +3,7 @@ from asyncio import sleep | ||||
| from sanic import Sanic, response | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__, strict_slashes=True) | ||||
| app = Sanic("DelayedResponseApp", strict_slashes=True) | ||||
|  | ||||
|  | ||||
| @app.get("/") | ||||
|   | ||||
| @@ -41,7 +41,7 @@ from sanic import Sanic | ||||
|  | ||||
|  | ||||
| handler = CustomHandler() | ||||
| app = Sanic(__name__, error_handler=handler) | ||||
| app = Sanic("Example", error_handler=handler) | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
| from sanic import Sanic, response | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| @@ -9,5 +9,5 @@ async def test(request): | ||||
|     return response.json({"test": True}) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
| if __name__ == "__main__": | ||||
|     app.run(host="0.0.0.0", port=8000) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ from sanic import Sanic | ||||
| from sanic.response import json | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
| sem = None | ||||
|  | ||||
|   | ||||
| @@ -44,7 +44,7 @@ LOG_SETTINGS = { | ||||
| } | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__, log_config=LOG_SETTINGS) | ||||
| app = Sanic("Example", log_config=LOG_SETTINGS) | ||||
|  | ||||
|  | ||||
| @app.on_request | ||||
|   | ||||
| @@ -43,7 +43,7 @@ logdna = logging.getLogger(__name__) | ||||
| logdna.setLevel(logging.INFO) | ||||
| logdna.addHandler(logdna_handler) | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.middleware | ||||
|   | ||||
| @@ -2,27 +2,29 @@ | ||||
| Modify header or status in response | ||||
| """ | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| from sanic import Sanic, response | ||||
|  | ||||
|  | ||||
| @app.route('/') | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| def handle_request(request): | ||||
|     return response.json( | ||||
|         {'message': 'Hello world!'}, | ||||
|         headers={'X-Served-By': 'sanic'}, | ||||
|         status=200 | ||||
|         {"message": "Hello world!"}, | ||||
|         headers={"X-Served-By": "sanic"}, | ||||
|         status=200, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @app.route('/unauthorized') | ||||
| @app.route("/unauthorized") | ||||
| def handle_request(request): | ||||
|     return response.json( | ||||
|         {'message': 'You are not authorized'}, | ||||
|         headers={'X-Served-By': 'sanic'}, | ||||
|         status=404 | ||||
|         {"message": "You are not authorized"}, | ||||
|         headers={"X-Served-By": "sanic"}, | ||||
|         status=404, | ||||
|     ) | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000, debug=True) | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     app.run(host="0.0.0.0", port=8000, debug=True) | ||||
|   | ||||
| @@ -32,7 +32,7 @@ def test_port(worker_id): | ||||
|  | ||||
| @pytest.fixture(scope="session") | ||||
| def app(): | ||||
|     app = Sanic() | ||||
|     app = Sanic("Example") | ||||
|  | ||||
|     @app.route("/") | ||||
|     async def index(request): | ||||
|   | ||||
| @@ -8,7 +8,6 @@ from sanic.handlers import ErrorHandler | ||||
|  | ||||
|  | ||||
| class RaygunExceptionReporter(ErrorHandler): | ||||
|  | ||||
|     def __init__(self, raygun_api_key=None): | ||||
|         super().__init__() | ||||
|         if raygun_api_key is None: | ||||
| @@ -22,16 +21,13 @@ class RaygunExceptionReporter(ErrorHandler): | ||||
|  | ||||
|  | ||||
| raygun_error_reporter = RaygunExceptionReporter() | ||||
| app = Sanic(__name__, error_handler=raygun_error_reporter) | ||||
| app = Sanic("Example", error_handler=raygun_error_reporter) | ||||
|  | ||||
|  | ||||
| @app.route("/raise") | ||||
| async def test(request): | ||||
|     raise SanicException('You Broke It!') | ||||
|     raise SanicException("You Broke It!") | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run( | ||||
|         host="0.0.0.0", | ||||
|         port=getenv("PORT", 8080) | ||||
|     ) | ||||
| if __name__ == "__main__": | ||||
|     app.run(host="0.0.0.0", port=getenv("PORT", 8080)) | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
| from sanic import Sanic, response | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
|      | ||||
| @app.route('/') | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| def handle_request(request): | ||||
|     return response.redirect('/redirect') | ||||
|     return response.redirect("/redirect") | ||||
|  | ||||
|  | ||||
| @app.route('/redirect') | ||||
| @app.route("/redirect") | ||||
| async def test(request): | ||||
|     return response.json({"Redirected": True}) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host="0.0.0.0", port=8000) | ||||
| if __name__ == "__main__": | ||||
|     app.run(host="0.0.0.0", port=8000) | ||||
|   | ||||
| @@ -1,65 +1,63 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.views import CompositionView | ||||
| from sanic.views import HTTPMethodView | ||||
| from sanic.views import stream as stream_decorator | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.response import stream, text | ||||
| from sanic.views import HTTPMethodView | ||||
| from sanic.views import stream as stream_decorator | ||||
|  | ||||
| bp = Blueprint('blueprint_request_stream') | ||||
| app = Sanic('request_stream') | ||||
|  | ||||
| bp = Blueprint("bp_example") | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| class SimpleView(HTTPMethodView): | ||||
|  | ||||
|     @stream_decorator | ||||
|     async def post(self, request): | ||||
|         result = '' | ||||
|         result = "" | ||||
|         while True: | ||||
|             body = await request.stream.get() | ||||
|             if body is None: | ||||
|                 break | ||||
|             result += body.decode('utf-8') | ||||
|             result += body.decode("utf-8") | ||||
|         return text(result) | ||||
|  | ||||
|  | ||||
| @app.post('/stream', stream=True) | ||||
| @app.post("/stream", stream=True) | ||||
| async def handler(request): | ||||
|     async def streaming(response): | ||||
|         while True: | ||||
|             body = await request.stream.get() | ||||
|             if body is None: | ||||
|                 break | ||||
|             body = body.decode('utf-8').replace('1', 'A') | ||||
|             body = body.decode("utf-8").replace("1", "A") | ||||
|             await response.write(body) | ||||
|  | ||||
|     return stream(streaming) | ||||
|  | ||||
|  | ||||
| @bp.put('/bp_stream', stream=True) | ||||
| @bp.put("/bp_stream", stream=True) | ||||
| async def bp_handler(request): | ||||
|     result = '' | ||||
|     result = "" | ||||
|     while True: | ||||
|         body = await request.stream.get() | ||||
|         if body is None: | ||||
|             break | ||||
|         result += body.decode('utf-8').replace('1', 'A') | ||||
|         result += body.decode("utf-8").replace("1", "A") | ||||
|     return text(result) | ||||
|  | ||||
|  | ||||
| async def post_handler(request): | ||||
|     result = '' | ||||
|     result = "" | ||||
|     while True: | ||||
|         body = await request.stream.get() | ||||
|         if body is None: | ||||
|             break | ||||
|         result += body.decode('utf-8') | ||||
|         result += body.decode("utf-8") | ||||
|     return text(result) | ||||
|  | ||||
|  | ||||
| app.blueprint(bp) | ||||
| app.add_route(SimpleView.as_view(), '/method_view') | ||||
| view = CompositionView() | ||||
| view.add(['POST'], post_handler, stream=True) | ||||
| app.add_route(view, '/composition_view') | ||||
| app.add_route(SimpleView.as_view(), "/method_view") | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host='0.0.0.0', port=8000) | ||||
| if __name__ == "__main__": | ||||
|     app.run(host="0.0.0.0", port=8000) | ||||
|   | ||||
| @@ -1,21 +1,23 @@ | ||||
| import asyncio | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
|  | ||||
| from sanic import Sanic, response | ||||
| from sanic.config import Config | ||||
| from sanic.exceptions import RequestTimeout | ||||
|  | ||||
|  | ||||
| Config.REQUEST_TIMEOUT = 1 | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route('/') | ||||
| @app.route("/") | ||||
| async def test(request): | ||||
|     await asyncio.sleep(3) | ||||
|     return response.text('Hello, world!') | ||||
|     return response.text("Hello, world!") | ||||
|  | ||||
|  | ||||
| @app.exception(RequestTimeout) | ||||
| def timeout(request, exception): | ||||
|     return response.text('RequestTimeout from error_handler.', 408) | ||||
|     return response.text("RequestTimeout from error_handler.", 408) | ||||
|  | ||||
| app.run(host='0.0.0.0', port=8000) | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000) | ||||
|   | ||||
| @@ -1,21 +1,22 @@ | ||||
| from os import getenv | ||||
|  | ||||
| import rollbar | ||||
|  | ||||
| from sanic.handlers import ErrorHandler | ||||
| from sanic import Sanic | ||||
| from sanic.exceptions import SanicException | ||||
| from os import getenv | ||||
| from sanic.handlers import ErrorHandler | ||||
|  | ||||
|  | ||||
| rollbar.init(getenv("ROLLBAR_API_KEY")) | ||||
|  | ||||
|  | ||||
| class RollbarExceptionHandler(ErrorHandler): | ||||
|  | ||||
|     def default(self, request, exception): | ||||
|         rollbar.report_message(str(exception)) | ||||
|         return super().default(request, exception) | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__, error_handler=RollbarExceptionHandler()) | ||||
| app = Sanic("Example", error_handler=RollbarExceptionHandler()) | ||||
|  | ||||
|  | ||||
| @app.route("/raise") | ||||
| @@ -24,7 +25,4 @@ def create_error(request): | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     app.run( | ||||
|         host="0.0.0.0", | ||||
|         port=getenv("PORT", 8080) | ||||
|     ) | ||||
|     app.run(host="0.0.0.0", port=getenv("PORT", 8080)) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ from pathlib import Path | ||||
| from sanic import Sanic, response | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route("/text") | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import uvloop | ||||
| from sanic import Sanic, response | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from sanic import Sanic, response | ||||
| from sanic.server import AsyncioServer | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.before_server_start | ||||
|   | ||||
| @@ -6,20 +6,19 @@ from sentry_sdk.integrations.sanic import SanicIntegration | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
|  | ||||
|  | ||||
| sentry_init( | ||||
|     dsn=getenv("SENTRY_DSN"), | ||||
|     integrations=[SanicIntegration()], | ||||
| ) | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| # noinspection PyUnusedLocal | ||||
| @app.route("/working") | ||||
| async def working_path(request): | ||||
|     return json({ | ||||
|         "response": "Working API Response" | ||||
|     }) | ||||
|     return json({"response": "Working API Response"}) | ||||
|  | ||||
|  | ||||
| # noinspection PyUnusedLocal | ||||
| @@ -28,8 +27,5 @@ async def raise_error(request): | ||||
|     raise Exception("Testing Sentry Integration") | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run( | ||||
|         host="0.0.0.0", | ||||
|         port=getenv("PORT", 8080) | ||||
|     ) | ||||
| if __name__ == "__main__": | ||||
|     app.run(host="0.0.0.0", port=getenv("PORT", 8080)) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| from sanic import Sanic | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
| app.static("/", "./static") | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| from sanic import Sanic | ||||
| from sanic import response as res | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| async def test(req): | ||||
|     return res.text("I\'m a teapot", status=418) | ||||
|     return res.text("I'm a teapot", status=418) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
| if __name__ == "__main__": | ||||
|     app.run(host="0.0.0.0", port=8000) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from sanic.exceptions import ServerError | ||||
| from sanic.log import logger as log | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import socket | ||||
| from sanic import Sanic, response | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route("/test") | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| from sanic import Sanic, response | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from sanic.blueprints import Blueprint | ||||
| # curl -H "Host: bp.example.com" localhost:8000/question | ||||
| # curl -H "Host: bp.example.com" localhost:8000/answer | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
| bp = Blueprint("bp", host="bp.example.com") | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ from sanic import Sanic | ||||
| from sanic.response import redirect | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app = Sanic("Example") | ||||
|  | ||||
|  | ||||
| app.static("index.html", "websocket.html") | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "21.12.0dev" | ||||
| __version__ = "21.12.1" | ||||
|   | ||||
							
								
								
									
										473
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										473
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -1,5 +1,6 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| import logging | ||||
| import logging.config | ||||
| import os | ||||
| @@ -11,6 +12,7 @@ from asyncio import ( | ||||
|     AbstractEventLoop, | ||||
|     CancelledError, | ||||
|     Protocol, | ||||
|     Task, | ||||
|     ensure_future, | ||||
|     get_event_loop, | ||||
|     wait_for, | ||||
| @@ -26,6 +28,7 @@ from ssl import SSLContext | ||||
| from traceback import format_exc | ||||
| from types import SimpleNamespace | ||||
| from typing import ( | ||||
|     TYPE_CHECKING, | ||||
|     Any, | ||||
|     AnyStr, | ||||
|     Awaitable, | ||||
| @@ -39,10 +42,11 @@ from typing import ( | ||||
|     Set, | ||||
|     Tuple, | ||||
|     Type, | ||||
|     TypeVar, | ||||
|     Union, | ||||
| ) | ||||
| from urllib.parse import urlencode, urlunparse | ||||
| from warnings import filterwarnings, warn | ||||
| from warnings import filterwarnings | ||||
|  | ||||
| from sanic_routing.exceptions import (  # type: ignore | ||||
|     FinalizationError, | ||||
| @@ -51,11 +55,12 @@ from sanic_routing.exceptions import (  # type: ignore | ||||
| from sanic_routing.route import Route  # type: ignore | ||||
|  | ||||
| from sanic import reloader_helpers | ||||
| from sanic.application.ext import setup_ext | ||||
| from sanic.application.logo import get_logo | ||||
| from sanic.application.motd import MOTD | ||||
| from sanic.application.state import ApplicationState, Mode | ||||
| from sanic.asgi import ASGIApp | ||||
| from sanic.base import BaseSanic | ||||
| from sanic.base.root import BaseSanic | ||||
| from sanic.blueprint_group import BlueprintGroup | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support | ||||
| @@ -67,8 +72,15 @@ from sanic.exceptions import ( | ||||
|     URLBuildError, | ||||
| ) | ||||
| from sanic.handlers import ErrorHandler | ||||
| from sanic.helpers import _default | ||||
| from sanic.http import Stage | ||||
| from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger | ||||
| from sanic.log import ( | ||||
|     LOGGING_CONFIG_DEFAULTS, | ||||
|     Colors, | ||||
|     deprecation, | ||||
|     error_logger, | ||||
|     logger, | ||||
| ) | ||||
| from sanic.mixins.listeners import ListenerEvent | ||||
| from sanic.models.futures import ( | ||||
|     FutureException, | ||||
| @@ -82,11 +94,11 @@ from sanic.models.futures import ( | ||||
| from sanic.models.handler_types import ListenerType, MiddlewareType | ||||
| from sanic.models.handler_types import Sanic as SanicVar | ||||
| from sanic.request import Request | ||||
| from sanic.response import BaseHTTPResponse, HTTPResponse | ||||
| from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream | ||||
| from sanic.router import Router | ||||
| from sanic.server import AsyncioServer, HttpProtocol | ||||
| from sanic.server import Signal as ServerSignal | ||||
| from sanic.server import serve, serve_multiple, serve_single | ||||
| from sanic.server import serve, serve_multiple, serve_single, try_use_uvloop | ||||
| from sanic.server.protocols.websocket_protocol import WebSocketProtocol | ||||
| from sanic.server.websockets.impl import ConnectionClosed | ||||
| from sanic.signals import Signal, SignalRouter | ||||
| @@ -94,11 +106,21 @@ from sanic.tls import process_to_context | ||||
| from sanic.touchup import TouchUp, TouchUpMeta | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     try: | ||||
|         from sanic_ext import Extend  # type: ignore | ||||
|         from sanic_ext.extensions.base import Extension  # type: ignore | ||||
|     except ImportError: | ||||
|         Extend = TypeVar("Extend")  # type: ignore | ||||
|  | ||||
|  | ||||
| if OS_IS_WINDOWS: | ||||
|     enable_windows_color_support() | ||||
|  | ||||
| filterwarnings("once", category=DeprecationWarning) | ||||
|  | ||||
| SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext") | ||||
|  | ||||
|  | ||||
| class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|     """ | ||||
| @@ -111,12 +133,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         "_run_response_middleware", | ||||
|         "_run_request_middleware", | ||||
|     ) | ||||
|     __fake_slots__ = ( | ||||
|         "_app_registry", | ||||
|     __slots__ = ( | ||||
|         "_asgi_app", | ||||
|         "_asgi_client", | ||||
|         "_blueprint_order", | ||||
|         "_delayed_tasks", | ||||
|         "_ext", | ||||
|         "_future_exceptions", | ||||
|         "_future_listeners", | ||||
|         "_future_middleware", | ||||
| @@ -125,20 +147,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         "_future_signals", | ||||
|         "_future_statics", | ||||
|         "_state", | ||||
|         "_task_registry", | ||||
|         "_test_client", | ||||
|         "_test_manager", | ||||
|         "asgi", | ||||
|         "auto_reload", | ||||
|         "auto_reload", | ||||
|         "blueprints", | ||||
|         "config", | ||||
|         "configure_logging", | ||||
|         "ctx", | ||||
|         "debug", | ||||
|         "error_handler", | ||||
|         "go_fast", | ||||
|         "is_running", | ||||
|         "is_stopping", | ||||
|         "listeners", | ||||
|         "name", | ||||
|         "named_request_middleware", | ||||
| @@ -150,12 +167,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         "signal_router", | ||||
|         "sock", | ||||
|         "strict_slashes", | ||||
|         "test_mode", | ||||
|         "websocket_enabled", | ||||
|         "websocket_tasks", | ||||
|     ) | ||||
|  | ||||
|     _app_registry: Dict[str, "Sanic"] = {} | ||||
|     _uvloop_setting = None  # TODO: Remove in v22.6 | ||||
|     test_mode = False | ||||
|  | ||||
|     def __init__( | ||||
| @@ -166,7 +183,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         router: Optional[Router] = None, | ||||
|         signal_router: Optional[SignalRouter] = None, | ||||
|         error_handler: Optional[ErrorHandler] = None, | ||||
|         load_env: Union[bool, str] = True, | ||||
|         env_prefix: Optional[str] = SANIC_PREFIX, | ||||
|         request_class: Optional[Type[Request]] = None, | ||||
|         strict_slashes: bool = False, | ||||
| @@ -182,25 +198,27 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|             dict_config = log_config or LOGGING_CONFIG_DEFAULTS | ||||
|             logging.config.dictConfig(dict_config)  # type: ignore | ||||
|  | ||||
|         if config and (load_env is not True or env_prefix != SANIC_PREFIX): | ||||
|         if config and env_prefix != SANIC_PREFIX: | ||||
|             raise SanicException( | ||||
|                 "When instantiating Sanic with config, you cannot also pass " | ||||
|                 "load_env or env_prefix" | ||||
|                 "env_prefix" | ||||
|             ) | ||||
|  | ||||
|         # First setup config | ||||
|         self.config: Config = config or Config(env_prefix=env_prefix) | ||||
|  | ||||
|         # Then we can do the rest | ||||
|         self._asgi_client: Any = None | ||||
|         self._test_client: Any = None | ||||
|         self._test_manager: Any = None | ||||
|         self._blueprint_order: List[Blueprint] = [] | ||||
|         self._delayed_tasks: List[str] = [] | ||||
|         self._future_registry: FutureRegistry = FutureRegistry() | ||||
|         self._state: ApplicationState = ApplicationState(app=self) | ||||
|         self._task_registry: Dict[str, Task] = {} | ||||
|         self._test_client: Any = None | ||||
|         self._test_manager: Any = None | ||||
|         self.asgi = False | ||||
|         self.auto_reload = False | ||||
|         self.blueprints: Dict[str, Blueprint] = {} | ||||
|         self.config: Config = config or Config( | ||||
|             load_env=load_env, | ||||
|             env_prefix=env_prefix, | ||||
|             app=self, | ||||
|         ) | ||||
|         self.configure_logging: bool = configure_logging | ||||
|         self.ctx: Any = ctx or SimpleNamespace() | ||||
|         self.debug = False | ||||
| @@ -222,6 +240,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         self.go_fast = self.run | ||||
|  | ||||
|         if register is not None: | ||||
|             deprecation( | ||||
|                 "The register argument is deprecated and will stop working " | ||||
|                 "in v22.6. After v22.6 all apps will be added to the Sanic " | ||||
|                 "app registry.", | ||||
|                 22.6, | ||||
|             ) | ||||
|             self.config.REGISTER = register | ||||
|         if self.config.REGISTER: | ||||
|             self.__class__.register_app(self) | ||||
| @@ -252,32 +276,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|     # Registration | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     def add_task( | ||||
|         self, | ||||
|         task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]], | ||||
|     ) -> None: | ||||
|         """ | ||||
|         Schedule a task to run later, after the loop has started. | ||||
|         Different from asyncio.ensure_future in that it does not | ||||
|         also return a future, and the actual ensure_future call | ||||
|         is delayed until before server start. | ||||
|  | ||||
|         `See user guide re: background tasks | ||||
|         <https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__ | ||||
|  | ||||
|         :param task: future, couroutine or awaitable | ||||
|         """ | ||||
|         try: | ||||
|             loop = self.loop  # Will raise SanicError if loop is not started | ||||
|             self._loop_add_task(task, self, loop) | ||||
|         except SanicException: | ||||
|             task_name = f"sanic.delayed_task.{hash(task)}" | ||||
|             if not self._delayed_tasks: | ||||
|                 self.after_server_start(partial(self.dispatch_delayed_tasks)) | ||||
|  | ||||
|             self.signal(task_name)(partial(self.run_delayed_task, task=task)) | ||||
|             self._delayed_tasks.append(task_name) | ||||
|  | ||||
|     def register_listener( | ||||
|         self, listener: ListenerType[SanicVar], event: str | ||||
|     ) -> ListenerType[SanicVar]: | ||||
| @@ -402,12 +400,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|             websocket_handler.is_websocket = True  # type: ignore | ||||
|             params["handler"] = websocket_handler | ||||
|  | ||||
|         ctx = params.pop("route_context") | ||||
|  | ||||
|         routes = self.router.add(**params) | ||||
|         if isinstance(routes, Route): | ||||
|             routes = [routes] | ||||
|  | ||||
|         for r in routes: | ||||
|             r.ctx.websocket = websocket | ||||
|             r.ctx.static = params.get("static", False) | ||||
|             r.ctx.__dict__.update(ctx) | ||||
|  | ||||
|         return routes | ||||
|  | ||||
| @@ -753,7 +755,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|                 exception, request.name if request else None | ||||
|             ) | ||||
|             if handler: | ||||
|                 warn( | ||||
|                 deprecation( | ||||
|                     "An error occurred while handling the request after at " | ||||
|                     "least some part of the response was sent to the client. " | ||||
|                     "Therefore, the response from your custom exception " | ||||
| @@ -768,7 +770,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|                     "For further information, please see the docs: " | ||||
|                     "https://sanicframework.org/en/guide/advanced/" | ||||
|                     "signals.html", | ||||
|                     DeprecationWarning, | ||||
|                     22.6, | ||||
|                 ) | ||||
|                 try: | ||||
|                     response = self.error_handler.response(request, exception) | ||||
| @@ -821,6 +823,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         else: | ||||
|             if request.stream: | ||||
|                 response = request.stream.response | ||||
|  | ||||
|         # Marked for cleanup and DRY with handle_request/handle_exception | ||||
|         # when ResponseStream is no longer supporder | ||||
|         if isinstance(response, BaseHTTPResponse): | ||||
|             await self.dispatch( | ||||
|                 "http.lifecycle.response", | ||||
| @@ -831,6 +836,17 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|                 }, | ||||
|             ) | ||||
|             await response.send(end_stream=True) | ||||
|         elif isinstance(response, ResponseStream): | ||||
|             resp = await response(request) | ||||
|             await self.dispatch( | ||||
|                 "http.lifecycle.response", | ||||
|                 inline=True, | ||||
|                 context={ | ||||
|                     "request": request, | ||||
|                     "response": resp, | ||||
|                 }, | ||||
|             ) | ||||
|             await response.eof() | ||||
|         else: | ||||
|             raise ServerError( | ||||
|                 f"Invalid response type {response!r} (need HTTPResponse)" | ||||
| @@ -934,7 +950,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|             elif not hasattr(handler, "is_websocket"): | ||||
|                 response = request.stream.response  # type: ignore | ||||
|  | ||||
|             # Make sure that response is finished / run StreamingHTTP callback | ||||
|             # Marked for cleanup and DRY with handle_request/handle_exception | ||||
|             # when ResponseStream is no longer supporder | ||||
|             if isinstance(response, BaseHTTPResponse): | ||||
|                 await self.dispatch( | ||||
|                     "http.lifecycle.response", | ||||
| @@ -945,6 +962,17 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|                     }, | ||||
|                 ) | ||||
|                 await response.send(end_stream=True) | ||||
|             elif isinstance(response, ResponseStream): | ||||
|                 resp = await response(request) | ||||
|                 await self.dispatch( | ||||
|                     "http.lifecycle.response", | ||||
|                     inline=True, | ||||
|                     context={ | ||||
|                         "request": request, | ||||
|                         "response": resp, | ||||
|                     }, | ||||
|                 ) | ||||
|                 await response.eof() | ||||
|             else: | ||||
|                 if not hasattr(handler, "is_websocket"): | ||||
|                     raise ServerError( | ||||
| @@ -1158,6 +1186,11 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|             register_sys_signals=register_sys_signals, | ||||
|         ) | ||||
|  | ||||
|         if self.config.USE_UVLOOP is True or ( | ||||
|             self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS | ||||
|         ): | ||||
|             try_use_uvloop() | ||||
|  | ||||
|         try: | ||||
|             self.is_running = True | ||||
|             self.is_stopping = False | ||||
| @@ -1185,6 +1218,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         This kills the Sanic | ||||
|         """ | ||||
|         if not self.is_stopping: | ||||
|             self.shutdown_tasks(timeout=0) | ||||
|             self.is_stopping = True | ||||
|             get_event_loop().stop() | ||||
|  | ||||
| @@ -1254,12 +1288,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|                 WebSocketProtocol if self.websocket_enabled else HttpProtocol | ||||
|             ) | ||||
|  | ||||
|         # if access_log is passed explicitly change config.ACCESS_LOG | ||||
|         if access_log is not None: | ||||
|             self.config.ACCESS_LOG = access_log | ||||
|  | ||||
|         if noisy_exceptions is not None: | ||||
|             self.config.NOISY_EXCEPTIONS = noisy_exceptions | ||||
|         # Set explicitly passed configuration values | ||||
|         for attribute, value in { | ||||
|             "ACCESS_LOG": access_log, | ||||
|             "NOISY_EXCEPTIONS": noisy_exceptions, | ||||
|         }.items(): | ||||
|             if value is not None: | ||||
|                 setattr(self.config, attribute, value) | ||||
|  | ||||
|         server_settings = self._helper( | ||||
|             host=host, | ||||
| @@ -1274,6 +1309,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|             run_async=return_asyncio_server, | ||||
|         ) | ||||
|  | ||||
|         if self.config.USE_UVLOOP is not _default: | ||||
|             error_logger.warning( | ||||
|                 "You are trying to change the uvloop configuration, but " | ||||
|                 "this is only effective when using the run(...) method. " | ||||
|                 "When using the create_server(...) method Sanic will use " | ||||
|                 "the already existing loop." | ||||
|             ) | ||||
|  | ||||
|         main_start = server_settings.pop("main_start", None) | ||||
|         main_stop = server_settings.pop("main_stop", None) | ||||
|         if main_start or main_stop: | ||||
| @@ -1392,26 +1435,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|                 "#proxy-configuration" | ||||
|             ) | ||||
|  | ||||
|         ssl = process_to_context(ssl) | ||||
|  | ||||
|         self.debug = debug | ||||
|         self.state.host = host | ||||
|         self.state.port = port | ||||
|         self.state.workers = workers | ||||
|  | ||||
|         # Serve | ||||
|         serve_location = "" | ||||
|         proto = "http" | ||||
|         if ssl is not None: | ||||
|             proto = "https" | ||||
|         if unix: | ||||
|             serve_location = f"{unix} {proto}://..." | ||||
|         elif sock: | ||||
|             serve_location = f"{sock.getsockname()} {proto}://..." | ||||
|         elif host and port: | ||||
|             # colon(:) is legal for a host only in an ipv6 address | ||||
|             display_host = f"[{host}]" if ":" in host else host | ||||
|             serve_location = f"{proto}://{display_host}:{port}" | ||||
|  | ||||
|         ssl = process_to_context(ssl) | ||||
|         self.state.ssl = ssl | ||||
|         self.state.unix = unix | ||||
|         self.state.sock = sock | ||||
|  | ||||
|         server_settings = { | ||||
|             "protocol": protocol, | ||||
| @@ -1427,7 +1459,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|             "backlog": backlog, | ||||
|         } | ||||
|  | ||||
|         self.motd(serve_location) | ||||
|         self.motd(self.serve_location) | ||||
|  | ||||
|         if sys.stdout.isatty() and not self.state.is_debug: | ||||
|             error_logger.warning( | ||||
| @@ -1453,12 +1485,55 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|  | ||||
|         return server_settings | ||||
|  | ||||
|     @property | ||||
|     def serve_location(self) -> str: | ||||
|         serve_location = "" | ||||
|         proto = "http" | ||||
|         if self.state.ssl is not None: | ||||
|             proto = "https" | ||||
|         if self.state.unix: | ||||
|             serve_location = f"{self.state.unix} {proto}://..." | ||||
|         elif self.state.sock: | ||||
|             serve_location = f"{self.state.sock.getsockname()} {proto}://..." | ||||
|         elif self.state.host and self.state.port: | ||||
|             # colon(:) is legal for a host only in an ipv6 address | ||||
|             display_host = ( | ||||
|                 f"[{self.state.host}]" | ||||
|                 if ":" in self.state.host | ||||
|                 else self.state.host | ||||
|             ) | ||||
|             serve_location = f"{proto}://{display_host}:{self.state.port}" | ||||
|  | ||||
|         return serve_location | ||||
|  | ||||
|     def _build_endpoint_name(self, *parts): | ||||
|         parts = [self.name, *parts] | ||||
|         return ".".join(parts) | ||||
|  | ||||
|     @classmethod | ||||
|     def _prep_task(cls, task, app, loop): | ||||
|     def _cancel_websocket_tasks(cls, app, loop): | ||||
|         for task in app.websocket_tasks: | ||||
|             task.cancel() | ||||
|  | ||||
|     @staticmethod | ||||
|     async def _listener( | ||||
|         app: Sanic, loop: AbstractEventLoop, listener: ListenerType | ||||
|     ): | ||||
|         maybe_coro = listener(app, loop) | ||||
|         if maybe_coro and isawaitable(maybe_coro): | ||||
|             await maybe_coro | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Task management | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     @classmethod | ||||
|     def _prep_task( | ||||
|         cls, | ||||
|         task, | ||||
|         app, | ||||
|         loop, | ||||
|     ): | ||||
|         if callable(task): | ||||
|             try: | ||||
|                 task = task(app) | ||||
| @@ -1468,14 +1543,31 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         return task | ||||
|  | ||||
|     @classmethod | ||||
|     def _loop_add_task(cls, task, app, loop): | ||||
|         prepped = cls._prep_task(task, app, loop) | ||||
|         loop.create_task(prepped) | ||||
|     def _loop_add_task( | ||||
|         cls, | ||||
|         task, | ||||
|         app, | ||||
|         loop, | ||||
|         *, | ||||
|         name: Optional[str] = None, | ||||
|         register: bool = True, | ||||
|     ) -> Task: | ||||
|         if not isinstance(task, Future): | ||||
|             prepped = cls._prep_task(task, app, loop) | ||||
|             if sys.version_info < (3, 8): | ||||
|                 if name: | ||||
|                     error_logger.warning( | ||||
|                         "Cannot set a name for a task when using Python 3.7. " | ||||
|                         "Your task will be created without a name." | ||||
|                     ) | ||||
|                 task = loop.create_task(prepped) | ||||
|             else: | ||||
|                 task = loop.create_task(prepped, name=name) | ||||
|  | ||||
|     @classmethod | ||||
|     def _cancel_websocket_tasks(cls, app, loop): | ||||
|         for task in app.websocket_tasks: | ||||
|             task.cancel() | ||||
|         if name and register and sys.version_info > (3, 7): | ||||
|             app._task_registry[name] = task | ||||
|  | ||||
|         return task | ||||
|  | ||||
|     @staticmethod | ||||
|     async def dispatch_delayed_tasks(app, loop): | ||||
| @@ -1488,13 +1580,142 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         prepped = app._prep_task(task, app, loop) | ||||
|         await prepped | ||||
|  | ||||
|     @staticmethod | ||||
|     async def _listener( | ||||
|         app: Sanic, loop: AbstractEventLoop, listener: ListenerType | ||||
|     def add_task( | ||||
|         self, | ||||
|         task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]], | ||||
|         *, | ||||
|         name: Optional[str] = None, | ||||
|         register: bool = True, | ||||
|     ) -> Optional[Task]: | ||||
|         """ | ||||
|         Schedule a task to run later, after the loop has started. | ||||
|         Different from asyncio.ensure_future in that it does not | ||||
|         also return a future, and the actual ensure_future call | ||||
|         is delayed until before server start. | ||||
|  | ||||
|         `See user guide re: background tasks | ||||
|         <https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__ | ||||
|  | ||||
|         :param task: future, couroutine or awaitable | ||||
|         """ | ||||
|         if name and sys.version_info == (3, 7): | ||||
|             name = None | ||||
|             error_logger.warning( | ||||
|                 "Cannot set a name for a task when using Python 3.7. Your " | ||||
|                 "task will be created without a name." | ||||
|             ) | ||||
|         try: | ||||
|             loop = self.loop  # Will raise SanicError if loop is not started | ||||
|             return self._loop_add_task( | ||||
|                 task, self, loop, name=name, register=register | ||||
|             ) | ||||
|         except SanicException: | ||||
|             task_name = f"sanic.delayed_task.{hash(task)}" | ||||
|             if not self._delayed_tasks: | ||||
|                 self.after_server_start(partial(self.dispatch_delayed_tasks)) | ||||
|  | ||||
|             if name: | ||||
|                 raise RuntimeError( | ||||
|                     "Cannot name task outside of a running application" | ||||
|                 ) | ||||
|  | ||||
|             self.signal(task_name)(partial(self.run_delayed_task, task=task)) | ||||
|             self._delayed_tasks.append(task_name) | ||||
|             return None | ||||
|  | ||||
|     def get_task( | ||||
|         self, name: str, *, raise_exception: bool = True | ||||
|     ) -> Optional[Task]: | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (get_task) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         try: | ||||
|             return self._task_registry[name] | ||||
|         except KeyError: | ||||
|             if raise_exception: | ||||
|                 raise SanicException( | ||||
|                     f'Registered task named "{name}" not found.' | ||||
|                 ) | ||||
|             return None | ||||
|  | ||||
|     async def cancel_task( | ||||
|         self, | ||||
|         name: str, | ||||
|         msg: Optional[str] = None, | ||||
|         *, | ||||
|         raise_exception: bool = True, | ||||
|     ) -> None: | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (cancel_task) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         task = self.get_task(name, raise_exception=raise_exception) | ||||
|         if task and not task.cancelled(): | ||||
|             args: Tuple[str, ...] = () | ||||
|             if msg: | ||||
|                 if sys.version_info >= (3, 9): | ||||
|                     args = (msg,) | ||||
|                 else: | ||||
|                     raise RuntimeError( | ||||
|                         "Cancelling a task with a message is only supported " | ||||
|                         "on Python 3.9+." | ||||
|                     ) | ||||
|             task.cancel(*args) | ||||
|             try: | ||||
|                 await task | ||||
|             except CancelledError: | ||||
|                 ... | ||||
|  | ||||
|     def purge_tasks(self): | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (purge_tasks) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         for task in self.tasks: | ||||
|             if task.done() or task.cancelled(): | ||||
|                 name = task.get_name() | ||||
|                 self._task_registry[name] = None | ||||
|  | ||||
|         self._task_registry = { | ||||
|             k: v for k, v in self._task_registry.items() if v is not None | ||||
|         } | ||||
|  | ||||
|     def shutdown_tasks( | ||||
|         self, timeout: Optional[float] = None, increment: float = 0.1 | ||||
|     ): | ||||
|         maybe_coro = listener(app, loop) | ||||
|         if maybe_coro and isawaitable(maybe_coro): | ||||
|             await maybe_coro | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (shutdown_tasks) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         for task in self.tasks: | ||||
|             task.cancel() | ||||
|  | ||||
|         if timeout is None: | ||||
|             timeout = self.config.GRACEFUL_SHUTDOWN_TIMEOUT | ||||
|  | ||||
|         while len(self._task_registry) and timeout: | ||||
|             self.loop.run_until_complete(asyncio.sleep(increment)) | ||||
|             self.purge_tasks() | ||||
|             timeout -= increment | ||||
|  | ||||
|     @property | ||||
|     def tasks(self): | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (tasks) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         return iter(self._task_registry.values()) | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # ASGI | ||||
| @@ -1507,7 +1728,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         details: https://asgi.readthedocs.io/en/latest | ||||
|         """ | ||||
|         self.asgi = True | ||||
|         self.motd("") | ||||
|         if scope["type"] == "lifespan": | ||||
|             self.motd("") | ||||
|         self._asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||
|         asgi_app = self._asgi_app | ||||
|         await asgi_app() | ||||
| @@ -1612,11 +1834,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|                 display["auto-reload"] = reload_display | ||||
|  | ||||
|             packages = [] | ||||
|             for package_name, module_name in { | ||||
|                 "sanic-routing": "sanic_routing", | ||||
|                 "sanic-testing": "sanic_testing", | ||||
|                 "sanic-ext": "sanic_ext", | ||||
|             }.items(): | ||||
|             for package_name in SANIC_PACKAGES: | ||||
|                 module_name = package_name.replace("-", "_") | ||||
|                 try: | ||||
|                     module = import_module(module_name) | ||||
|                     packages.append(f"{package_name}=={module.__version__}") | ||||
| @@ -1636,6 +1855,41 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|             ) | ||||
|             MOTD.output(logo, serve_location, display, extra) | ||||
|  | ||||
|     @property | ||||
|     def ext(self) -> Extend: | ||||
|         if not hasattr(self, "_ext"): | ||||
|             setup_ext(self, fail=True) | ||||
|  | ||||
|         if not hasattr(self, "_ext"): | ||||
|             raise RuntimeError( | ||||
|                 "Sanic Extensions is not installed. You can add it to your " | ||||
|                 "environment using:\n$ pip install sanic[ext]\nor\n$ pip " | ||||
|                 "install sanic-ext" | ||||
|             ) | ||||
|         return self._ext  # type: ignore | ||||
|  | ||||
|     def extend( | ||||
|         self, | ||||
|         *, | ||||
|         extensions: Optional[List[Type[Extension]]] = None, | ||||
|         built_in_extensions: bool = True, | ||||
|         config: Optional[Union[Config, Dict[str, Any]]] = None, | ||||
|         **kwargs, | ||||
|     ) -> Extend: | ||||
|         if hasattr(self, "_ext"): | ||||
|             raise RuntimeError( | ||||
|                 "Cannot extend Sanic after Sanic Extensions has been setup." | ||||
|             ) | ||||
|         setup_ext( | ||||
|             self, | ||||
|             extensions=extensions, | ||||
|             built_in_extensions=built_in_extensions, | ||||
|             config=config, | ||||
|             fail=True, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         return self.ext | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Class methods | ||||
|     # -------------------------------------------------------------------- # | ||||
| @@ -1697,12 +1951,33 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|  | ||||
|     async def _startup(self): | ||||
|         self._future_registry.clear() | ||||
|  | ||||
|         # Startup Sanic Extensions | ||||
|         if not hasattr(self, "_ext"): | ||||
|             setup_ext(self) | ||||
|         if hasattr(self, "_ext"): | ||||
|             self.ext._display() | ||||
|  | ||||
|         # Setup routers | ||||
|         self.signalize() | ||||
|         self.finalize() | ||||
|         ErrorHandler.finalize( | ||||
|             self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT | ||||
|         ) | ||||
|  | ||||
|         # TODO: Replace in v22.6 to check against apps in app registry | ||||
|         if ( | ||||
|             self.__class__._uvloop_setting is not None | ||||
|             and self.__class__._uvloop_setting != self.config.USE_UVLOOP | ||||
|         ): | ||||
|             error_logger.warning( | ||||
|                 "It looks like you're running several apps with different " | ||||
|                 "uvloop settings. This is not supported and may lead to " | ||||
|                 "unintended behaviour." | ||||
|             ) | ||||
|         self.__class__._uvloop_setting = self.config.USE_UVLOOP | ||||
|  | ||||
|         # Startup time optimizations | ||||
|         ErrorHandler.finalize(self.error_handler, config=self.config) | ||||
|         TouchUp.run(self) | ||||
|  | ||||
|         self.state.is_started = True | ||||
|  | ||||
|     async def _server_event( | ||||
|   | ||||
							
								
								
									
										39
									
								
								sanic/application/ext.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								sanic/application/ext.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from contextlib import suppress | ||||
| from importlib import import_module | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     from sanic import Sanic | ||||
|  | ||||
|     try: | ||||
|         from sanic_ext import Extend  # type: ignore | ||||
|     except ImportError: | ||||
|         ... | ||||
|  | ||||
|  | ||||
| def setup_ext(app: Sanic, *, fail: bool = False, **kwargs): | ||||
|     if not app.config.AUTO_EXTEND: | ||||
|         return | ||||
|  | ||||
|     sanic_ext = None | ||||
|     with suppress(ModuleNotFoundError): | ||||
|         sanic_ext = import_module("sanic_ext") | ||||
|  | ||||
|     if not sanic_ext: | ||||
|         if fail: | ||||
|             raise RuntimeError( | ||||
|                 "Sanic Extensions is not installed. You can add it to your " | ||||
|                 "environment using:\n$ pip install sanic[ext]\nor\n$ pip " | ||||
|                 "install sanic-ext" | ||||
|             ) | ||||
|  | ||||
|         return | ||||
|  | ||||
|     if not getattr(app, "_ext", None): | ||||
|         Ext: Extend = getattr(sanic_ext, "Extend") | ||||
|         app._ext = Ext(app, **kwargs) | ||||
|  | ||||
|         return app.ext | ||||
| @@ -41,9 +41,6 @@ class MOTD(ABC): | ||||
|  | ||||
|  | ||||
| class MOTDBasic(MOTD): | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         super().__init__(*args, **kwargs) | ||||
|  | ||||
|     def display(self): | ||||
|         if self.logo: | ||||
|             logger.debug(self.logo) | ||||
|   | ||||
| @@ -5,7 +5,9 @@ import logging | ||||
| from dataclasses import dataclass, field | ||||
| from enum import Enum, auto | ||||
| from pathlib import Path | ||||
| from typing import TYPE_CHECKING, Any, Set, Union | ||||
| from socket import socket | ||||
| from ssl import SSLContext | ||||
| from typing import TYPE_CHECKING, Any, Optional, Set, Union | ||||
|  | ||||
| from sanic.log import logger | ||||
|  | ||||
| @@ -37,8 +39,11 @@ class ApplicationState: | ||||
|     coffee: bool = field(default=False) | ||||
|     fast: bool = field(default=False) | ||||
|     host: str = field(default="") | ||||
|     mode: Mode = field(default=Mode.PRODUCTION) | ||||
|     port: int = field(default=0) | ||||
|     ssl: Optional[SSLContext] = field(default=None) | ||||
|     sock: Optional[socket] = field(default=None) | ||||
|     unix: Optional[str] = field(default=None) | ||||
|     mode: Mode = field(default=Mode.PRODUCTION) | ||||
|     reload_dirs: Set[Path] = field(default_factory=set) | ||||
|     server: Server = field(default=Server.SANIC) | ||||
|     is_running: bool = field(default=False) | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import sanic.app  # noqa | ||||
|  | ||||
| from sanic.compat import Header | ||||
| from sanic.exceptions import ServerError | ||||
| from sanic.helpers import _default | ||||
| from sanic.http import Stage | ||||
| from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport | ||||
| from sanic.request import Request | ||||
| @@ -53,6 +54,13 @@ class Lifespan: | ||||
|         await self.asgi_app.sanic_app._server_event("init", "before") | ||||
|         await self.asgi_app.sanic_app._server_event("init", "after") | ||||
|  | ||||
|         if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default: | ||||
|             warnings.warn( | ||||
|                 "You have set the USE_UVLOOP configuration option, but Sanic " | ||||
|                 "cannot control the event loop when running in ASGI mode." | ||||
|                 "This option will be ignored." | ||||
|             ) | ||||
|  | ||||
|     async def shutdown(self) -> None: | ||||
|         """ | ||||
|         Gather the listeners to fire on server stop. | ||||
|   | ||||
							
								
								
									
										0
									
								
								sanic/base/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								sanic/base/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										6
									
								
								sanic/base/meta.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								sanic/base/meta.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| class SanicMeta(type): | ||||
|     @classmethod | ||||
|     def __prepare__(metaclass, name, bases, **kwds): | ||||
|         cls = super().__prepare__(metaclass, name, bases, **kwds) | ||||
|         cls["__slots__"] = () | ||||
|         return cls | ||||
| @@ -1,8 +1,8 @@ | ||||
| import re | ||||
| 
 | ||||
| from typing import Any, Tuple | ||||
| from warnings import warn | ||||
| from typing import Any | ||||
| 
 | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.exceptions import SanicException | ||||
| from sanic.mixins.exceptions import ExceptionMixin | ||||
| from sanic.mixins.listeners import ListenerMixin | ||||
| @@ -20,8 +20,9 @@ class BaseSanic( | ||||
|     ListenerMixin, | ||||
|     ExceptionMixin, | ||||
|     SignalMixin, | ||||
|     metaclass=SanicMeta, | ||||
| ): | ||||
|     __fake_slots__: Tuple[str, ...] | ||||
|     __slots__ = ("name",) | ||||
| 
 | ||||
|     def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None: | ||||
|         class_name = self.__class__.__name__ | ||||
| @@ -33,11 +34,10 @@ class BaseSanic( | ||||
|             ) | ||||
| 
 | ||||
|         if not VALID_NAME.match(name): | ||||
|             warn( | ||||
|                 f"{class_name} instance named '{name}' uses a format that is" | ||||
|                 f"deprecated. Starting in version 21.12, {class_name} objects " | ||||
|                 "must be named only using alphanumeric characters, _, or -.", | ||||
|                 DeprecationWarning, | ||||
|             raise SanicException( | ||||
|                 f"{class_name} instance named '{name}' uses an invalid " | ||||
|                 "format. Names must begin with a character and may only " | ||||
|                 "contain alphanumeric characters, _, or -." | ||||
|             ) | ||||
| 
 | ||||
|         self.name = name | ||||
| @@ -52,15 +52,12 @@ class BaseSanic( | ||||
|         return f'{self.__class__.__name__}(name="{self.name}")' | ||||
| 
 | ||||
|     def __setattr__(self, name: str, value: Any) -> None: | ||||
|         # This is a temporary compat layer so we can raise a warning until | ||||
|         # setting attributes on the app instance can be removed and deprecated | ||||
|         # with a proper implementation of __slots__ | ||||
|         if name not in self.__fake_slots__: | ||||
|             warn( | ||||
|         try: | ||||
|             super().__setattr__(name, value) | ||||
|         except AttributeError as e: | ||||
|             raise AttributeError( | ||||
|                 f"Setting variables on {self.__class__.__name__} instances is " | ||||
|                 "deprecated and will be removed in version 21.12. You should " | ||||
|                 f"change your {self.__class__.__name__} instance to use " | ||||
|                 "not allowed. You should change your " | ||||
|                 f"{self.__class__.__name__} instance to use " | ||||
|                 f"instance.ctx.{name} instead.", | ||||
|                 DeprecationWarning, | ||||
|             ) | ||||
|         super().__setattr__(name, value) | ||||
|             ) from e | ||||
| @@ -5,7 +5,7 @@ from functools import partial | ||||
| from typing import TYPE_CHECKING, List, Optional, Union | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     from sanic.blueprints import Blueprint | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -24,7 +24,7 @@ from typing import ( | ||||
| from sanic_routing.exceptions import NotFound  # type: ignore | ||||
| from sanic_routing.route import Route  # type: ignore | ||||
|  | ||||
| from sanic.base import BaseSanic | ||||
| from sanic.base.root import BaseSanic | ||||
| from sanic.blueprint_group import BlueprintGroup | ||||
| from sanic.exceptions import SanicException | ||||
| from sanic.helpers import Default, _default | ||||
| @@ -36,8 +36,8 @@ from sanic.models.handler_types import ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from sanic import Sanic  # noqa | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     from sanic import Sanic | ||||
|  | ||||
|  | ||||
| def lazy(func, as_decorator=True): | ||||
| @@ -85,7 +85,7 @@ class Blueprint(BaseSanic): | ||||
|         trailing */* | ||||
|     """ | ||||
|  | ||||
|     __fake_slots__ = ( | ||||
|     __slots__ = ( | ||||
|         "_apps", | ||||
|         "_future_routes", | ||||
|         "_future_statics", | ||||
| @@ -98,7 +98,6 @@ class Blueprint(BaseSanic): | ||||
|         "host", | ||||
|         "listeners", | ||||
|         "middlewares", | ||||
|         "name", | ||||
|         "routes", | ||||
|         "statics", | ||||
|         "strict_slashes", | ||||
| @@ -348,6 +347,7 @@ class Blueprint(BaseSanic): | ||||
|                 future.static, | ||||
|                 version_prefix, | ||||
|                 error_format, | ||||
|                 future.route_context, | ||||
|             ) | ||||
|  | ||||
|             if (self, apply_route) in app._future_registry: | ||||
| @@ -400,8 +400,9 @@ class Blueprint(BaseSanic): | ||||
|         for future in self._future_signals: | ||||
|             if (self, future) in app._future_registry: | ||||
|                 continue | ||||
|             future.condition.update({"blueprint": self.name}) | ||||
|             app._apply_signal(future) | ||||
|             future.condition.update({"__blueprint__": self.name}) | ||||
|             # Force exclusive to be False | ||||
|             app._apply_signal(tuple((*future[:-1], False))) | ||||
|  | ||||
|         self.routes += [route for route in routes if isinstance(route, Route)] | ||||
|         self.websocket_routes += [ | ||||
| @@ -426,7 +427,7 @@ class Blueprint(BaseSanic): | ||||
|  | ||||
|     async def dispatch(self, *args, **kwargs): | ||||
|         condition = kwargs.pop("condition", {}) | ||||
|         condition.update({"blueprint": self.name}) | ||||
|         condition.update({"__blueprint__": self.name}) | ||||
|         kwargs["condition"] = condition | ||||
|         await asyncio.gather( | ||||
|             *[app.dispatch(*args, **kwargs) for app in self.apps] | ||||
|   | ||||
| @@ -8,6 +8,14 @@ from multidict import CIMultiDict  # type: ignore | ||||
|  | ||||
|  | ||||
| OS_IS_WINDOWS = os.name == "nt" | ||||
| UVLOOP_INSTALLED = False | ||||
|  | ||||
| try: | ||||
|     import uvloop  # type: ignore # noqa | ||||
|  | ||||
|     UVLOOP_INSTALLED = True | ||||
| except ImportError: | ||||
|     pass | ||||
|  | ||||
|  | ||||
| def enable_windows_color_support(): | ||||
|   | ||||
							
								
								
									
										146
									
								
								sanic/config.py
									
									
									
									
									
								
							
							
						
						
									
										146
									
								
								sanic/config.py
									
									
									
									
									
								
							| @@ -1,28 +1,26 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from inspect import isclass | ||||
| from inspect import getmembers, isclass, isdatadescriptor | ||||
| from os import environ | ||||
| from pathlib import Path | ||||
| from typing import TYPE_CHECKING, Any, Dict, Optional, Union | ||||
| from warnings import warn | ||||
| from typing import Any, Callable, Dict, Optional, Sequence, Union | ||||
|  | ||||
| from sanic.errorpages import check_error_format | ||||
| from sanic.errorpages import DEFAULT_FORMAT, check_error_format | ||||
| from sanic.helpers import Default, _default | ||||
| from sanic.http import Http | ||||
| from sanic.log import deprecation, error_logger | ||||
| from sanic.utils import load_module_from_file_location, str_to_bool | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     from sanic import Sanic | ||||
|  | ||||
|  | ||||
| SANIC_PREFIX = "SANIC_" | ||||
|  | ||||
|  | ||||
| DEFAULT_CONFIG = { | ||||
|     "_FALLBACK_ERROR_FORMAT": _default, | ||||
|     "ACCESS_LOG": True, | ||||
|     "AUTO_EXTEND": True, | ||||
|     "AUTO_RELOAD": False, | ||||
|     "EVENT_AUTOREGISTER": False, | ||||
|     "FALLBACK_ERROR_FORMAT": "auto", | ||||
|     "FORWARDED_FOR_HEADER": "X-Forwarded-For", | ||||
|     "FORWARDED_SECRET": None, | ||||
|     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,  # 15 sec | ||||
| @@ -40,17 +38,31 @@ DEFAULT_CONFIG = { | ||||
|     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes | ||||
|     "REQUEST_TIMEOUT": 60,  # 60 seconds | ||||
|     "RESPONSE_TIMEOUT": 60,  # 60 seconds | ||||
|     "USE_UVLOOP": _default, | ||||
|     "WEBSOCKET_MAX_SIZE": 2 ** 20,  # 1 megabyte | ||||
|     "WEBSOCKET_PING_INTERVAL": 20, | ||||
|     "WEBSOCKET_PING_TIMEOUT": 20, | ||||
| } | ||||
|  | ||||
| # These values will be removed from the Config object in v22.6 and moved | ||||
| # to the application state | ||||
| DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES") | ||||
|  | ||||
| class Config(dict): | ||||
|  | ||||
| class DescriptorMeta(type): | ||||
|     def __init__(cls, *_): | ||||
|         cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)} | ||||
|  | ||||
|     @staticmethod | ||||
|     def _is_setter(member: object): | ||||
|         return isdatadescriptor(member) and hasattr(member, "setter") | ||||
|  | ||||
|  | ||||
| class Config(dict, metaclass=DescriptorMeta): | ||||
|     ACCESS_LOG: bool | ||||
|     AUTO_EXTEND: bool | ||||
|     AUTO_RELOAD: bool | ||||
|     EVENT_AUTOREGISTER: bool | ||||
|     FALLBACK_ERROR_FORMAT: str | ||||
|     FORWARDED_FOR_HEADER: str | ||||
|     FORWARDED_SECRET: Optional[str] | ||||
|     GRACEFUL_SHUTDOWN_TIMEOUT: float | ||||
| @@ -69,6 +81,7 @@ class Config(dict): | ||||
|     REQUEST_TIMEOUT: int | ||||
|     RESPONSE_TIMEOUT: int | ||||
|     SERVER_NAME: str | ||||
|     USE_UVLOOP: Union[Default, bool] | ||||
|     WEBSOCKET_MAX_SIZE: int | ||||
|     WEBSOCKET_PING_INTERVAL: int | ||||
|     WEBSOCKET_PING_TIMEOUT: int | ||||
| @@ -76,33 +89,27 @@ class Config(dict): | ||||
|     def __init__( | ||||
|         self, | ||||
|         defaults: Dict[str, Union[str, bool, int, float, None]] = None, | ||||
|         load_env: Optional[Union[bool, str]] = True, | ||||
|         env_prefix: Optional[str] = SANIC_PREFIX, | ||||
|         keep_alive: Optional[bool] = None, | ||||
|         *, | ||||
|         app: Optional[Sanic] = None, | ||||
|         converters: Optional[Sequence[Callable[[str], Any]]] = None, | ||||
|     ): | ||||
|         defaults = defaults or {} | ||||
|         super().__init__({**DEFAULT_CONFIG, **defaults}) | ||||
|  | ||||
|         self._app = app | ||||
|         self._converters = [str, str_to_bool, float, int] | ||||
|         self._LOGO = "" | ||||
|  | ||||
|         if converters: | ||||
|             for converter in converters: | ||||
|                 self.register_type(converter) | ||||
|  | ||||
|         if keep_alive is not None: | ||||
|             self.KEEP_ALIVE = keep_alive | ||||
|  | ||||
|         if env_prefix != SANIC_PREFIX: | ||||
|             if env_prefix: | ||||
|                 self.load_environment_vars(env_prefix) | ||||
|         elif load_env is not True: | ||||
|             if load_env: | ||||
|                 self.load_environment_vars(prefix=load_env) | ||||
|             warn( | ||||
|                 "Use of load_env is deprecated and will be removed in " | ||||
|                 "21.12. Modify the configuration prefix by passing " | ||||
|                 "env_prefix instead.", | ||||
|                 DeprecationWarning, | ||||
|             ) | ||||
|         else: | ||||
|             self.load_environment_vars(SANIC_PREFIX) | ||||
|  | ||||
| @@ -123,9 +130,21 @@ class Config(dict): | ||||
|         self.update({attr: value}) | ||||
|  | ||||
|     def update(self, *other, **kwargs) -> None: | ||||
|         other_mapping = {k: v for item in other for k, v in dict(item).items()} | ||||
|         super().update(*other, **kwargs) | ||||
|         for attr, value in {**other_mapping, **kwargs}.items(): | ||||
|         kwargs.update({k: v for item in other for k, v in dict(item).items()}) | ||||
|         setters: Dict[str, Any] = { | ||||
|             k: kwargs.pop(k) | ||||
|             for k in {**kwargs}.keys() | ||||
|             if k in self.__class__.__setters__ | ||||
|         } | ||||
|  | ||||
|         for key, value in setters.items(): | ||||
|             try: | ||||
|                 super().__setattr__(key, value) | ||||
|             except AttributeError: | ||||
|                 ... | ||||
|  | ||||
|         super().update(**kwargs) | ||||
|         for attr, value in {**setters, **kwargs}.items(): | ||||
|             self._post_set(attr, value) | ||||
|  | ||||
|     def _post_set(self, attr, value) -> None: | ||||
| @@ -136,32 +155,37 @@ class Config(dict): | ||||
|                 "REQUEST_MAX_SIZE", | ||||
|             ): | ||||
|                 self._configure_header_size() | ||||
|             elif attr == "FALLBACK_ERROR_FORMAT": | ||||
|                 self._check_error_format() | ||||
|                 if self.app and value != self.app.error_handler.fallback: | ||||
|                     if self.app.error_handler.fallback != "auto": | ||||
|                         warn( | ||||
|                             "Overriding non-default ErrorHandler fallback " | ||||
|                             "value. Changing from " | ||||
|                             f"{self.app.error_handler.fallback} to {value}." | ||||
|                         ) | ||||
|                     self.app.error_handler.fallback = value | ||||
|             elif attr == "LOGO": | ||||
|                 self._LOGO = value | ||||
|                 warn( | ||||
|                 deprecation( | ||||
|                     "Setting the config.LOGO is deprecated and will no longer " | ||||
|                     "be supported starting in v22.6.", | ||||
|                     DeprecationWarning, | ||||
|                     22.6, | ||||
|                 ) | ||||
|  | ||||
|     @property | ||||
|     def app(self): | ||||
|         return self._app | ||||
|  | ||||
|     @property | ||||
|     def LOGO(self): | ||||
|         return self._LOGO | ||||
|  | ||||
|     @property | ||||
|     def FALLBACK_ERROR_FORMAT(self) -> str: | ||||
|         if self._FALLBACK_ERROR_FORMAT is _default: | ||||
|             return DEFAULT_FORMAT | ||||
|         return self._FALLBACK_ERROR_FORMAT | ||||
|  | ||||
|     @FALLBACK_ERROR_FORMAT.setter | ||||
|     def FALLBACK_ERROR_FORMAT(self, value): | ||||
|         self._check_error_format(value) | ||||
|         if ( | ||||
|             self._FALLBACK_ERROR_FORMAT is not _default | ||||
|             and value != self._FALLBACK_ERROR_FORMAT | ||||
|         ): | ||||
|             error_logger.warning( | ||||
|                 "Setting config.FALLBACK_ERROR_FORMAT on an already " | ||||
|                 "configured value may have unintended consequences." | ||||
|             ) | ||||
|         self._FALLBACK_ERROR_FORMAT = value | ||||
|  | ||||
|     def _configure_header_size(self): | ||||
|         Http.set_header_max_size( | ||||
|             self.REQUEST_MAX_HEADER_SIZE, | ||||
| @@ -169,8 +193,8 @@ class Config(dict): | ||||
|             self.REQUEST_MAX_SIZE, | ||||
|         ) | ||||
|  | ||||
|     def _check_error_format(self): | ||||
|         check_error_format(self.FALLBACK_ERROR_FORMAT) | ||||
|     def _check_error_format(self, format: Optional[str] = None): | ||||
|         check_error_format(format or self.FALLBACK_ERROR_FORMAT) | ||||
|  | ||||
|     def load_environment_vars(self, prefix=SANIC_PREFIX): | ||||
|         """ | ||||
| @@ -184,7 +208,23 @@ class Config(dict): | ||||
|         - ``float`` | ||||
|         - ``bool`` | ||||
|  | ||||
|         Anything else will be imported as a ``str``. | ||||
|         Anything else will be imported as a ``str``. If you would like to add | ||||
|         additional types to this list, you can use | ||||
|         :meth:`sanic.config.Config.register_type`. Just make sure that they | ||||
|         are registered before you instantiate your application. | ||||
|  | ||||
|         .. code-block:: python | ||||
|  | ||||
|             class Foo: | ||||
|                 def __init__(self, name) -> None: | ||||
|                     self.name = name | ||||
|  | ||||
|  | ||||
|             config = Config(converters=[Foo]) | ||||
|             app = Sanic(__name__, config=config) | ||||
|  | ||||
|         `See user guide re: config | ||||
|         <https://sanicframework.org/guide/deployment/configuration.html>`__ | ||||
|         """ | ||||
|         for key, value in environ.items(): | ||||
|             if not key.startswith(prefix): | ||||
| @@ -192,7 +232,7 @@ class Config(dict): | ||||
|  | ||||
|             _, config_key = key.split(prefix, 1) | ||||
|  | ||||
|             for converter in (int, float, str_to_bool, str): | ||||
|             for converter in reversed(self._converters): | ||||
|                 try: | ||||
|                     self[config_key] = converter(value) | ||||
|                     break | ||||
| @@ -267,3 +307,17 @@ class Config(dict): | ||||
|         self.update(config) | ||||
|  | ||||
|     load = update_config | ||||
|  | ||||
|     def register_type(self, converter: Callable[[str], Any]) -> None: | ||||
|         """ | ||||
|         Allows for adding custom function to cast from a string value to any | ||||
|         other type. The function should raise ValueError if it is not the | ||||
|         correct type. | ||||
|         """ | ||||
|         if converter in self._converters: | ||||
|             error_logger.warning( | ||||
|                 f"Configuration value converter '{converter.__name__}' has " | ||||
|                 "already been registered" | ||||
|             ) | ||||
|             return | ||||
|         self._converters.append(converter) | ||||
|   | ||||
| @@ -34,6 +34,7 @@ except ImportError:  # noqa | ||||
|     from json import dumps | ||||
|  | ||||
|  | ||||
| DEFAULT_FORMAT = "auto" | ||||
| FALLBACK_TEXT = ( | ||||
|     "The server encountered an internal error and " | ||||
|     "cannot complete your request." | ||||
|   | ||||
| @@ -244,25 +244,3 @@ class InvalidSignal(SanicException): | ||||
| class WebsocketClosed(SanicException): | ||||
|     quiet = True | ||||
|     message = "Client has closed the websocket connection" | ||||
|  | ||||
|  | ||||
| def abort(status_code: int, message: Optional[Union[str, bytes]] = None): | ||||
|     """ | ||||
|     Raise an exception based on SanicException. Returns the HTTP response | ||||
|     message appropriate for the given status code, unless provided. | ||||
|  | ||||
|     STATUS_CODES from sanic.helpers for the given status code. | ||||
|  | ||||
|     :param status_code: The HTTP status code to return. | ||||
|     :param message: The HTTP response body. Defaults to the messages in | ||||
|     """ | ||||
|     import warnings | ||||
|  | ||||
|     warnings.warn( | ||||
|         "sanic.exceptions.abort has been marked as deprecated, and will be " | ||||
|         "removed in release 21.12.\n To migrate your code, simply replace " | ||||
|         "abort(status_code, msg) with raise SanicException(msg, status_code), " | ||||
|         "or even better, raise an appropriate SanicException subclass." | ||||
|     ) | ||||
|  | ||||
|     raise SanicException(message=message, status_code=status_code) | ||||
|   | ||||
| @@ -1,14 +1,23 @@ | ||||
| from inspect import signature | ||||
| from typing import Dict, List, Optional, Tuple, Type | ||||
| from warnings import warn | ||||
| from __future__ import annotations | ||||
|  | ||||
| from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response | ||||
| from inspect import signature | ||||
| from typing import Dict, List, Optional, Tuple, Type, Union | ||||
|  | ||||
| from sanic.config import Config | ||||
| from sanic.errorpages import ( | ||||
|     DEFAULT_FORMAT, | ||||
|     BaseRenderer, | ||||
|     HTMLRenderer, | ||||
|     exception_response, | ||||
| ) | ||||
| from sanic.exceptions import ( | ||||
|     ContentRangeError, | ||||
|     HeaderNotFound, | ||||
|     InvalidRangeType, | ||||
|     SanicException, | ||||
| ) | ||||
| from sanic.log import error_logger | ||||
| from sanic.helpers import Default, _default | ||||
| from sanic.log import deprecation, error_logger | ||||
| from sanic.models.handler_types import RouteHandler | ||||
| from sanic.response import text | ||||
|  | ||||
| @@ -28,24 +37,91 @@ class ErrorHandler: | ||||
|  | ||||
|     # Beginning in v22.3, the base renderer will be TextRenderer | ||||
|     def __init__( | ||||
|         self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer | ||||
|         self, | ||||
|         fallback: Union[str, Default] = _default, | ||||
|         base: Type[BaseRenderer] = HTMLRenderer, | ||||
|     ): | ||||
|         self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = [] | ||||
|         self.cached_handlers: Dict[ | ||||
|             Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler] | ||||
|         ] = {} | ||||
|         self.debug = False | ||||
|         self.fallback = fallback | ||||
|         self._fallback = fallback | ||||
|         self.base = base | ||||
|  | ||||
|         if fallback is not _default: | ||||
|             self._warn_fallback_deprecation() | ||||
|  | ||||
|     @property | ||||
|     def fallback(self): | ||||
|         # This is for backwards compat and can be removed in v22.6 | ||||
|         if self._fallback is _default: | ||||
|             return DEFAULT_FORMAT | ||||
|         return self._fallback | ||||
|  | ||||
|     @fallback.setter | ||||
|     def fallback(self, value: str): | ||||
|         self._warn_fallback_deprecation() | ||||
|         if not isinstance(value, str): | ||||
|             raise SanicException( | ||||
|                 f"Cannot set error handler fallback to: value={value}" | ||||
|             ) | ||||
|         self._fallback = value | ||||
|  | ||||
|     @staticmethod | ||||
|     def _warn_fallback_deprecation(): | ||||
|         deprecation( | ||||
|             "Setting the ErrorHandler fallback value directly is " | ||||
|             "deprecated and no longer supported. This feature will " | ||||
|             "be removed in v22.6. Instead, use " | ||||
|             "app.config.FALLBACK_ERROR_FORMAT.", | ||||
|             22.6, | ||||
|         ) | ||||
|  | ||||
|     @classmethod | ||||
|     def finalize(cls, error_handler, fallback: Optional[str] = None): | ||||
|         if ( | ||||
|             fallback | ||||
|             and fallback != "auto" | ||||
|             and error_handler.fallback == "auto" | ||||
|         ): | ||||
|             error_handler.fallback = fallback | ||||
|     def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config): | ||||
|         if error_handler._fallback is not _default: | ||||
|             if config._FALLBACK_ERROR_FORMAT is _default: | ||||
|                 return error_handler.fallback | ||||
|  | ||||
|             error_logger.warning( | ||||
|                 "Conflicting error fallback values were found in the " | ||||
|                 "error handler and in the app.config while handling an " | ||||
|                 "exception. Using the value from app.config." | ||||
|             ) | ||||
|         return config.FALLBACK_ERROR_FORMAT | ||||
|  | ||||
|     @classmethod | ||||
|     def finalize( | ||||
|         cls, | ||||
|         error_handler: ErrorHandler, | ||||
|         fallback: Optional[str] = None, | ||||
|         config: Optional[Config] = None, | ||||
|     ): | ||||
|         if fallback: | ||||
|             deprecation( | ||||
|                 "Setting the ErrorHandler fallback value via finalize() " | ||||
|                 "is deprecated and no longer supported. This feature will " | ||||
|                 "be removed in v22.6. Instead, use " | ||||
|                 "app.config.FALLBACK_ERROR_FORMAT.", | ||||
|                 22.6, | ||||
|             ) | ||||
|  | ||||
|         if config is None: | ||||
|             deprecation( | ||||
|                 "Starting in v22.3, config will be a required argument " | ||||
|                 "for ErrorHandler.finalize().", | ||||
|                 22.3, | ||||
|             ) | ||||
|  | ||||
|         if fallback and fallback != DEFAULT_FORMAT: | ||||
|             if error_handler._fallback is not _default: | ||||
|                 error_logger.warning( | ||||
|                     f"Setting the fallback value to {fallback}. This changes " | ||||
|                     "the current non-default value " | ||||
|                     f"'{error_handler._fallback}'." | ||||
|                 ) | ||||
|             error_handler._fallback = fallback | ||||
|  | ||||
|         if not isinstance(error_handler, cls): | ||||
|             error_logger.warning( | ||||
| @@ -54,7 +130,7 @@ class ErrorHandler: | ||||
|  | ||||
|         sig = signature(error_handler.lookup) | ||||
|         if len(sig.parameters) == 1: | ||||
|             warn( | ||||
|             deprecation( | ||||
|                 "You are using a deprecated error handler. The lookup " | ||||
|                 "method should accept two positional parameters: " | ||||
|                 "(exception, route_name: Optional[str]). " | ||||
| @@ -62,9 +138,10 @@ class ErrorHandler: | ||||
|                 "specific exceptions will not work properly. Beginning " | ||||
|                 "in v22.3, the legacy style lookup method will not " | ||||
|                 "work at all.", | ||||
|                 DeprecationWarning, | ||||
|                 22.3, | ||||
|             ) | ||||
|             error_handler._lookup = error_handler._legacy_lookup | ||||
|             legacy_lookup = error_handler._legacy_lookup | ||||
|             error_handler._lookup = legacy_lookup  # type: ignore | ||||
|  | ||||
|     def _full_lookup(self, exception, route_name: Optional[str] = None): | ||||
|         return self.lookup(exception, route_name) | ||||
| @@ -188,12 +265,13 @@ class ErrorHandler: | ||||
|         :return: | ||||
|         """ | ||||
|         self.log(request, exception) | ||||
|         fallback = ErrorHandler._get_fallback_value(self, request.app.config) | ||||
|         return exception_response( | ||||
|             request, | ||||
|             exception, | ||||
|             debug=self.debug, | ||||
|             base=self.base, | ||||
|             fallback=self.fallback, | ||||
|             fallback=fallback, | ||||
|         ) | ||||
|  | ||||
|     @staticmethod | ||||
|   | ||||
| @@ -3,7 +3,7 @@ from __future__ import annotations | ||||
| from typing import TYPE_CHECKING, Optional | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     from sanic.request import Request | ||||
|     from sanic.response import BaseHTTPResponse | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import sys | ||||
|  | ||||
| from enum import Enum | ||||
| from typing import Any, Dict | ||||
| from warnings import warn | ||||
|  | ||||
|  | ||||
| LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( | ||||
| @@ -78,3 +79,11 @@ access_logger = logging.getLogger("sanic.access") | ||||
| """ | ||||
| Logger used by Sanic for access logging | ||||
| """ | ||||
|  | ||||
|  | ||||
| def deprecation(message: str, version: float): | ||||
|     version_info = f"[DEPRECATION v{version}] " | ||||
|     if sys.stdout.isatty(): | ||||
|         version_info = f"{Colors.RED}{version_info}" | ||||
|         message = f"{Colors.YELLOW}{message}{Colors.END}" | ||||
|     warn(version_info + message, DeprecationWarning) | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| from typing import Set | ||||
|  | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.models.futures import FutureException | ||||
|  | ||||
|  | ||||
| class ExceptionMixin: | ||||
| class ExceptionMixin(metaclass=SanicMeta): | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         self._future_exceptions: Set[FutureException] = set() | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ from enum import Enum, auto | ||||
| from functools import partial | ||||
| from typing import List, Optional, Union | ||||
|  | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.models.futures import FutureListener | ||||
| from sanic.models.handler_types import ListenerType, Sanic | ||||
|  | ||||
| @@ -18,7 +19,7 @@ class ListenerEvent(str, Enum): | ||||
|     MAIN_PROCESS_STOP = auto() | ||||
|  | ||||
|  | ||||
| class ListenerMixin: | ||||
| class ListenerMixin(metaclass=SanicMeta): | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         self._future_listeners: List[FutureListener] = [] | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| from functools import partial | ||||
| from typing import List | ||||
|  | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.models.futures import FutureMiddleware | ||||
|  | ||||
|  | ||||
| class MiddlewareMixin: | ||||
| class MiddlewareMixin(metaclass=SanicMeta): | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         self._future_middleware: List[FutureMiddleware] = [] | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from ast import NodeVisitor, Return, parse | ||||
| from contextlib import suppress | ||||
| from functools import partial, wraps | ||||
| from inspect import getsource, signature | ||||
| from mimetypes import guess_type | ||||
| @@ -12,6 +13,7 @@ from urllib.parse import unquote | ||||
|  | ||||
| from sanic_routing.route import Route  # type: ignore | ||||
|  | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.compat import stat_async | ||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS | ||||
| from sanic.errorpages import RESPONSE_MAPPING | ||||
| @@ -22,19 +24,27 @@ from sanic.exceptions import ( | ||||
|     InvalidUsage, | ||||
| ) | ||||
| from sanic.handlers import ContentRangeHandler | ||||
| from sanic.log import error_logger | ||||
| from sanic.log import deprecation, error_logger | ||||
| from sanic.models.futures import FutureRoute, FutureStatic | ||||
| from sanic.models.handler_types import RouteHandler | ||||
| from sanic.response import HTTPResponse, file, file_stream | ||||
| from sanic.views import CompositionView | ||||
| from sanic.types import HashableDict | ||||
|  | ||||
|  | ||||
| RouteWrapper = Callable[ | ||||
|     [RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]] | ||||
| ] | ||||
| RESTRICTED_ROUTE_CONTEXT = ( | ||||
|     "ignore_body", | ||||
|     "stream", | ||||
|     "hosts", | ||||
|     "static", | ||||
|     "error_format", | ||||
|     "websocket", | ||||
| ) | ||||
|  | ||||
|  | ||||
| class RouteMixin: | ||||
| class RouteMixin(metaclass=SanicMeta): | ||||
|     name: str | ||||
|  | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
| @@ -65,10 +75,20 @@ class RouteMixin: | ||||
|         static: bool = False, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs: Any, | ||||
|     ) -> RouteWrapper: | ||||
|         """ | ||||
|         Decorate a function to be registered as a route | ||||
|  | ||||
|  | ||||
|         **Example using context kwargs** | ||||
|  | ||||
|         .. code-block:: python | ||||
|  | ||||
|             @app.route(..., ctx_foo="foobar") | ||||
|             async def route_handler(request: Request): | ||||
|                 assert request.route.ctx.foo == "foobar" | ||||
|  | ||||
|         :param uri: path of the URL | ||||
|         :param methods: list or tuple of methods allowed | ||||
|         :param host: the host, if required | ||||
| @@ -80,6 +100,8 @@ class RouteMixin: | ||||
|             body (eg. GET requests) | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: tuple of routes, decorated function | ||||
|         """ | ||||
|  | ||||
| @@ -94,6 +116,8 @@ class RouteMixin: | ||||
|         if not methods and not websocket: | ||||
|             methods = frozenset({"GET"}) | ||||
|  | ||||
|         route_context = self._build_route_context(ctx_kwargs) | ||||
|  | ||||
|         def decorator(handler): | ||||
|             nonlocal uri | ||||
|             nonlocal methods | ||||
| @@ -152,6 +176,7 @@ class RouteMixin: | ||||
|                 static, | ||||
|                 version_prefix, | ||||
|                 error_format, | ||||
|                 route_context, | ||||
|             ) | ||||
|  | ||||
|             self._future_routes.add(route) | ||||
| @@ -196,6 +221,7 @@ class RouteMixin: | ||||
|         stream: bool = False, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ) -> RouteHandler: | ||||
|         """A helper method to register class instance or | ||||
|         functions as a handler to the application url | ||||
| @@ -212,6 +238,8 @@ class RouteMixin: | ||||
|         :param stream: boolean specifying if the handler is a stream handler | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: function or class instance | ||||
|         """ | ||||
|         # Handle HTTPMethodView differently | ||||
| @@ -226,14 +254,6 @@ class RouteMixin: | ||||
|                     if hasattr(_handler, "is_stream"): | ||||
|                         stream = True | ||||
|  | ||||
|         # handle composition view differently | ||||
|         if isinstance(handler, CompositionView): | ||||
|             methods = handler.handlers.keys() | ||||
|             for _handler in handler.handlers.values(): | ||||
|                 if hasattr(_handler, "is_stream"): | ||||
|                     stream = True | ||||
|                     break | ||||
|  | ||||
|         if strict_slashes is None: | ||||
|             strict_slashes = self.strict_slashes | ||||
|  | ||||
| @@ -247,6 +267,7 @@ class RouteMixin: | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         )(handler) | ||||
|         return handler | ||||
|  | ||||
| @@ -261,6 +282,7 @@ class RouteMixin: | ||||
|         ignore_body: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ) -> RouteWrapper: | ||||
|         """ | ||||
|         Add an API URL under the **GET** *HTTP* method | ||||
| @@ -273,6 +295,8 @@ class RouteMixin: | ||||
|         :param name: Unique name that can be used to identify the Route | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -285,6 +309,7 @@ class RouteMixin: | ||||
|             ignore_body=ignore_body, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         ) | ||||
|  | ||||
|     def post( | ||||
| @@ -297,6 +322,7 @@ class RouteMixin: | ||||
|         name: Optional[str] = None, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ) -> RouteWrapper: | ||||
|         """ | ||||
|         Add an API URL under the **POST** *HTTP* method | ||||
| @@ -309,6 +335,8 @@ class RouteMixin: | ||||
|         :param name: Unique name that can be used to identify the Route | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -321,6 +349,7 @@ class RouteMixin: | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         ) | ||||
|  | ||||
|     def put( | ||||
| @@ -333,6 +362,7 @@ class RouteMixin: | ||||
|         name: Optional[str] = None, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ) -> RouteWrapper: | ||||
|         """ | ||||
|         Add an API URL under the **PUT** *HTTP* method | ||||
| @@ -345,6 +375,8 @@ class RouteMixin: | ||||
|         :param name: Unique name that can be used to identify the Route | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -357,6 +389,7 @@ class RouteMixin: | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         ) | ||||
|  | ||||
|     def head( | ||||
| @@ -369,6 +402,7 @@ class RouteMixin: | ||||
|         ignore_body: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ) -> RouteWrapper: | ||||
|         """ | ||||
|         Add an API URL under the **HEAD** *HTTP* method | ||||
| @@ -389,6 +423,8 @@ class RouteMixin: | ||||
|         :type ignore_body: bool, optional | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -401,6 +437,7 @@ class RouteMixin: | ||||
|             ignore_body=ignore_body, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         ) | ||||
|  | ||||
|     def options( | ||||
| @@ -413,6 +450,7 @@ class RouteMixin: | ||||
|         ignore_body: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ) -> RouteWrapper: | ||||
|         """ | ||||
|         Add an API URL under the **OPTIONS** *HTTP* method | ||||
| @@ -433,6 +471,8 @@ class RouteMixin: | ||||
|         :type ignore_body: bool, optional | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -445,6 +485,7 @@ class RouteMixin: | ||||
|             ignore_body=ignore_body, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         ) | ||||
|  | ||||
|     def patch( | ||||
| @@ -457,6 +498,7 @@ class RouteMixin: | ||||
|         name: Optional[str] = None, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ) -> RouteWrapper: | ||||
|         """ | ||||
|         Add an API URL under the **PATCH** *HTTP* method | ||||
| @@ -479,6 +521,8 @@ class RouteMixin: | ||||
|         :type ignore_body: bool, optional | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -491,6 +535,7 @@ class RouteMixin: | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         ) | ||||
|  | ||||
|     def delete( | ||||
| @@ -503,6 +548,7 @@ class RouteMixin: | ||||
|         ignore_body: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ) -> RouteWrapper: | ||||
|         """ | ||||
|         Add an API URL under the **DELETE** *HTTP* method | ||||
| @@ -515,6 +561,8 @@ class RouteMixin: | ||||
|         :param name: Unique name that can be used to identify the Route | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -527,6 +575,7 @@ class RouteMixin: | ||||
|             ignore_body=ignore_body, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         ) | ||||
|  | ||||
|     def websocket( | ||||
| @@ -540,6 +589,7 @@ class RouteMixin: | ||||
|         apply: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ): | ||||
|         """ | ||||
|         Decorate a function to be registered as a websocket route | ||||
| @@ -553,6 +603,8 @@ class RouteMixin: | ||||
|                      be used with :func:`url_for` | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: tuple of routes, decorated function | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -567,6 +619,7 @@ class RouteMixin: | ||||
|             websocket=True, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         ) | ||||
|  | ||||
|     def add_websocket_route( | ||||
| @@ -580,6 +633,7 @@ class RouteMixin: | ||||
|         name: Optional[str] = None, | ||||
|         version_prefix: str = "/v", | ||||
|         error_format: Optional[str] = None, | ||||
|         **ctx_kwargs, | ||||
|     ): | ||||
|         """ | ||||
|         A helper method to register a function as a websocket route. | ||||
| @@ -598,6 +652,8 @@ class RouteMixin: | ||||
|                 be used with :func:`url_for` | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :param  ctx_kwargs: Keyword arguments that begin with a ctx_* prefix | ||||
|             will be appended to the route context (``route.ctx``) | ||||
|         :return: Objected decorated by :func:`websocket` | ||||
|         """ | ||||
|         return self.websocket( | ||||
| @@ -609,6 +665,7 @@ class RouteMixin: | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|             error_format=error_format, | ||||
|             **ctx_kwargs, | ||||
|         )(handler) | ||||
|  | ||||
|     def static( | ||||
| @@ -918,19 +975,16 @@ class RouteMixin: | ||||
|  | ||||
|         return route | ||||
|  | ||||
|     def _determine_error_format(self, handler) -> Optional[str]: | ||||
|         if not isinstance(handler, CompositionView): | ||||
|             try: | ||||
|                 src = dedent(getsource(handler)) | ||||
|                 tree = parse(src) | ||||
|                 http_response_types = self._get_response_types(tree) | ||||
|     def _determine_error_format(self, handler) -> str: | ||||
|         with suppress(OSError, TypeError): | ||||
|             src = dedent(getsource(handler)) | ||||
|             tree = parse(src) | ||||
|             http_response_types = self._get_response_types(tree) | ||||
|  | ||||
|                 if len(http_response_types) == 1: | ||||
|                     return next(iter(http_response_types)) | ||||
|             except (OSError, TypeError): | ||||
|                 ... | ||||
|             if len(http_response_types) == 1: | ||||
|                 return next(iter(http_response_types)) | ||||
|  | ||||
|         return None | ||||
|         return "" | ||||
|  | ||||
|     def _get_response_types(self, node): | ||||
|         types = set() | ||||
| @@ -939,7 +993,18 @@ class RouteMixin: | ||||
|             def visit_Return(self, node: Return) -> Any: | ||||
|                 nonlocal types | ||||
|  | ||||
|                 try: | ||||
|                 with suppress(AttributeError): | ||||
|                     if node.value.func.id == "stream":  # type: ignore | ||||
|                         deprecation( | ||||
|                             "The sanic.response.stream method has been " | ||||
|                             "deprecated and will be removed in v22.6. Please " | ||||
|                             "upgrade your application to use the new style " | ||||
|                             "streaming pattern. See " | ||||
|                             "https://sanicframework.org/en/guide/advanced/" | ||||
|                             "streaming.html#response-streaming for more " | ||||
|                             "information.", | ||||
|                             22.6, | ||||
|                         ) | ||||
|                     checks = [node.value.func.id]  # type: ignore | ||||
|                     if node.value.keywords:  # type: ignore | ||||
|                         checks += [ | ||||
| @@ -951,9 +1016,32 @@ class RouteMixin: | ||||
|                     for check in checks: | ||||
|                         if check in RESPONSE_MAPPING: | ||||
|                             types.add(RESPONSE_MAPPING[check]) | ||||
|                 except AttributeError: | ||||
|                     ... | ||||
|  | ||||
|         HttpResponseVisitor().visit(node) | ||||
|  | ||||
|         return types | ||||
|  | ||||
|     def _build_route_context(self, raw): | ||||
|         ctx_kwargs = { | ||||
|             key.replace("ctx_", ""): raw.pop(key) | ||||
|             for key in {**raw}.keys() | ||||
|             if key.startswith("ctx_") | ||||
|         } | ||||
|         restricted = [ | ||||
|             key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT | ||||
|         ] | ||||
|         if restricted: | ||||
|             restricted_arguments = ", ".join(restricted) | ||||
|             raise AttributeError( | ||||
|                 "Cannot use restricted route context: " | ||||
|                 f"{restricted_arguments}. This limitation is only in place " | ||||
|                 "until v22.3 when the restricted names will no longer be in" | ||||
|                 "conflict. See https://github.com/sanic-org/sanic/issues/2303 " | ||||
|                 "for more information." | ||||
|             ) | ||||
|         if raw: | ||||
|             unexpected_arguments = ", ".join(raw.keys()) | ||||
|             raise TypeError( | ||||
|                 f"Unexpected keyword arguments: {unexpected_arguments}" | ||||
|             ) | ||||
|         return HashableDict(ctx_kwargs) | ||||
|   | ||||
| @@ -1,17 +1,14 @@ | ||||
| from enum import Enum | ||||
| from typing import Any, Callable, Dict, Optional, Set, Union | ||||
|  | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.models.futures import FutureSignal | ||||
| from sanic.models.handler_types import SignalHandler | ||||
| from sanic.signals import Signal | ||||
| from sanic.types import HashableDict | ||||
|  | ||||
|  | ||||
| class HashableDict(dict): | ||||
|     def __hash__(self): | ||||
|         return hash(tuple(sorted(self.items()))) | ||||
|  | ||||
|  | ||||
| class SignalMixin: | ||||
| class SignalMixin(metaclass=SanicMeta): | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         self._future_signals: Set[FutureSignal] = set() | ||||
|  | ||||
| @@ -24,6 +21,7 @@ class SignalMixin: | ||||
|         *, | ||||
|         apply: bool = True, | ||||
|         condition: Dict[str, Any] = None, | ||||
|         exclusive: bool = True, | ||||
|     ) -> Callable[[SignalHandler], SignalHandler]: | ||||
|         """ | ||||
|         For creating a signal handler, used similar to a route handler: | ||||
| @@ -36,17 +34,22 @@ class SignalMixin: | ||||
|  | ||||
|         :param event: Representation of the event in ``one.two.three`` form | ||||
|         :type event: str | ||||
|         :param apply: For lazy evaluation, defaults to True | ||||
|         :param apply: For lazy evaluation, defaults to ``True`` | ||||
|         :type apply: bool, optional | ||||
|         :param condition: For use with the ``condition`` argument in dispatch | ||||
|             filtering, defaults to None | ||||
|             filtering, defaults to ``None`` | ||||
|         :param exclusive: When ``True``, the signal can only be dispatched | ||||
|             when the condition has been met. When ``False``, the signal can | ||||
|             be dispatched either with or without it. *THIS IS INAPPLICABLE TO | ||||
|             BLUEPRINT SIGNALS. THEY ARE ALWAYS NON-EXCLUSIVE*, defaults | ||||
|             to ``True`` | ||||
|         :type condition: Dict[str, Any], optional | ||||
|         """ | ||||
|         event_value = str(event.value) if isinstance(event, Enum) else event | ||||
|  | ||||
|         def decorator(handler: SignalHandler): | ||||
|             future_signal = FutureSignal( | ||||
|                 handler, event_value, HashableDict(condition or {}) | ||||
|                 handler, event_value, HashableDict(condition or {}), exclusive | ||||
|             ) | ||||
|             self._future_signals.add(future_signal) | ||||
|  | ||||
| @@ -62,6 +65,7 @@ class SignalMixin: | ||||
|         handler: Optional[Callable[..., Any]], | ||||
|         event: str, | ||||
|         condition: Dict[str, Any] = None, | ||||
|         exclusive: bool = True, | ||||
|     ): | ||||
|         if not handler: | ||||
|  | ||||
| @@ -69,7 +73,9 @@ class SignalMixin: | ||||
|                 ... | ||||
|  | ||||
|             handler = noop | ||||
|         self.signal(event=event, condition=condition)(handler) | ||||
|         self.signal(event=event, condition=condition, exclusive=exclusive)( | ||||
|             handler | ||||
|         ) | ||||
|         return handler | ||||
|  | ||||
|     def event(self, event: str): | ||||
|   | ||||
| @@ -7,6 +7,7 @@ from sanic.models.handler_types import ( | ||||
|     MiddlewareType, | ||||
|     SignalHandler, | ||||
| ) | ||||
| from sanic.types import HashableDict | ||||
|  | ||||
|  | ||||
| class FutureRoute(NamedTuple): | ||||
| @@ -25,6 +26,7 @@ class FutureRoute(NamedTuple): | ||||
|     static: bool | ||||
|     version_prefix: str | ||||
|     error_format: Optional[str] | ||||
|     route_context: HashableDict | ||||
|  | ||||
|  | ||||
| class FutureListener(NamedTuple): | ||||
| @@ -60,6 +62,7 @@ class FutureSignal(NamedTuple): | ||||
|     handler: SignalHandler | ||||
|     event: str | ||||
|     condition: Optional[Dict[str, str]] | ||||
|     exclusive: bool | ||||
|  | ||||
|  | ||||
| class FutureRegistry(set): | ||||
|   | ||||
| @@ -15,7 +15,7 @@ from typing import ( | ||||
| from sanic_routing.route import Route  # type: ignore | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     from sanic.server import ConnInfo | ||||
|     from sanic.app import Sanic | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from functools import partial | ||||
| from mimetypes import guess_type | ||||
| from os import path | ||||
| @@ -12,10 +14,10 @@ from typing import ( | ||||
|     Iterator, | ||||
|     Optional, | ||||
|     Tuple, | ||||
|     TypeVar, | ||||
|     Union, | ||||
| ) | ||||
| from urllib.parse import quote_plus | ||||
| from warnings import warn | ||||
|  | ||||
| from sanic.compat import Header, open_async | ||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE | ||||
| @@ -28,6 +30,10 @@ from sanic.models.protocol_types import HTMLProtocol, Range | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from sanic.asgi import ASGIApp | ||||
|     from sanic.request import Request | ||||
| else: | ||||
|     Request = TypeVar("Request") | ||||
|  | ||||
|  | ||||
| try: | ||||
|     from ujson import dumps as json_dumps | ||||
| @@ -136,95 +142,6 @@ class BaseHTTPResponse: | ||||
|         await self.stream.send(data, end_stream=end_stream) | ||||
|  | ||||
|  | ||||
| StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]] | ||||
|  | ||||
|  | ||||
| class StreamingHTTPResponse(BaseHTTPResponse): | ||||
|     """ | ||||
|     Old style streaming response where you pass a streaming function: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         async def sample_streaming_fn(response): | ||||
|             await response.write("foo") | ||||
|             await asyncio.sleep(1) | ||||
|             await response.write("bar") | ||||
|             await asyncio.sleep(1) | ||||
|  | ||||
|             @app.post("/") | ||||
|             async def test(request): | ||||
|                 return stream(sample_streaming_fn) | ||||
|  | ||||
|     .. warning:: | ||||
|  | ||||
|         **Deprecated** and set for removal in v21.12. You can now achieve the | ||||
|         same functionality without a callback. | ||||
|  | ||||
|         .. code-block:: python | ||||
|  | ||||
|             @app.post("/") | ||||
|             async def test(request): | ||||
|                 response = await request.respond() | ||||
|                 await response.send("foo", False) | ||||
|                 await asyncio.sleep(1) | ||||
|                 await response.send("bar", False) | ||||
|                 await asyncio.sleep(1) | ||||
|                 await response.send("", True) | ||||
|                 return response | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     __slots__ = ( | ||||
|         "streaming_fn", | ||||
|         "status", | ||||
|         "content_type", | ||||
|         "headers", | ||||
|         "_cookies", | ||||
|     ) | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         streaming_fn: StreamingFunction, | ||||
|         status: int = 200, | ||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||
|         content_type: str = "text/plain; charset=utf-8", | ||||
|         ignore_deprecation_notice: bool = False, | ||||
|     ): | ||||
|         if not ignore_deprecation_notice: | ||||
|             warn( | ||||
|                 "Use of the StreamingHTTPResponse is deprecated in v21.6, and " | ||||
|                 "will be removed in v21.12. Please upgrade your streaming " | ||||
|                 "response implementation. You can learn more here: " | ||||
|                 "https://sanicframework.org/en/guide/advanced/streaming.html" | ||||
|                 "#response-streaming. If you use the builtin stream() or " | ||||
|                 "file_stream() methods, this upgrade will be be done for you." | ||||
|             ) | ||||
|  | ||||
|         super().__init__() | ||||
|  | ||||
|         self.content_type = content_type | ||||
|         self.streaming_fn = streaming_fn | ||||
|         self.status = status | ||||
|         self.headers = Header(headers or {}) | ||||
|         self._cookies = None | ||||
|  | ||||
|     async def write(self, data): | ||||
|         """Writes a chunk of data to the streaming response. | ||||
|  | ||||
|         :param data: str or bytes-ish data to be written. | ||||
|         """ | ||||
|         await super().send(self._encode_body(data)) | ||||
|  | ||||
|     async def send(self, *args, **kwargs): | ||||
|         if self.streaming_fn is not None: | ||||
|             await self.streaming_fn(self) | ||||
|             self.streaming_fn = None | ||||
|         await super().send(*args, **kwargs) | ||||
|  | ||||
|     async def eof(self): | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| class HTTPResponse(BaseHTTPResponse): | ||||
|     """ | ||||
|     HTTP response to be sent back to the client. | ||||
| @@ -419,6 +336,109 @@ async def file( | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def redirect( | ||||
|     to: str, | ||||
|     headers: Optional[Dict[str, str]] = None, | ||||
|     status: int = 302, | ||||
|     content_type: str = "text/html; charset=utf-8", | ||||
| ) -> HTTPResponse: | ||||
|     """ | ||||
|     Abort execution and cause a 302 redirect (by default) by setting a | ||||
|     Location header. | ||||
|  | ||||
|     :param to: path or fully qualified URL to redirect to | ||||
|     :param headers: optional dict of headers to include in the new request | ||||
|     :param status: status code (int) of the new request, defaults to 302 | ||||
|     :param content_type: the content type (string) of the response | ||||
|     """ | ||||
|     headers = headers or {} | ||||
|  | ||||
|     # URL Quote the URL before redirecting | ||||
|     safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;") | ||||
|  | ||||
|     # According to RFC 7231, a relative URI is now permitted. | ||||
|     headers["Location"] = safe_to | ||||
|  | ||||
|     return HTTPResponse( | ||||
|         status=status, headers=headers, content_type=content_type | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class ResponseStream: | ||||
|     """ | ||||
|     ResponseStream is a compat layer to bridge the gap after the deprecation | ||||
|     of StreamingHTTPResponse. In v22.6 it will be removed when: | ||||
|     - stream is removed | ||||
|     - file_stream is moved to new style streaming | ||||
|     - file and file_stream are combined into a single API | ||||
|     """ | ||||
|  | ||||
|     __slots__ = ( | ||||
|         "_cookies", | ||||
|         "content_type", | ||||
|         "headers", | ||||
|         "request", | ||||
|         "response", | ||||
|         "status", | ||||
|         "streaming_fn", | ||||
|     ) | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         streaming_fn: Callable[ | ||||
|             [Union[BaseHTTPResponse, ResponseStream]], | ||||
|             Coroutine[Any, Any, None], | ||||
|         ], | ||||
|         status: int = 200, | ||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||
|         content_type: Optional[str] = None, | ||||
|     ): | ||||
|         self.streaming_fn = streaming_fn | ||||
|         self.status = status | ||||
|         self.headers = headers or Header() | ||||
|         self.content_type = content_type | ||||
|         self.request: Optional[Request] = None | ||||
|         self._cookies: Optional[CookieJar] = None | ||||
|  | ||||
|     async def write(self, message: str): | ||||
|         await self.response.send(message) | ||||
|  | ||||
|     async def stream(self) -> HTTPResponse: | ||||
|         if not self.request: | ||||
|             raise ServerError("Attempted response to unknown request") | ||||
|         self.response = await self.request.respond( | ||||
|             headers=self.headers, | ||||
|             status=self.status, | ||||
|             content_type=self.content_type, | ||||
|         ) | ||||
|         await self.streaming_fn(self) | ||||
|         return self.response | ||||
|  | ||||
|     async def eof(self) -> None: | ||||
|         await self.response.eof() | ||||
|  | ||||
|     @property | ||||
|     def cookies(self) -> CookieJar: | ||||
|         if self._cookies is None: | ||||
|             self._cookies = CookieJar(self.headers) | ||||
|         return self._cookies | ||||
|  | ||||
|     @property | ||||
|     def processed_headers(self): | ||||
|         return self.response.processed_headers | ||||
|  | ||||
|     @property | ||||
|     def body(self): | ||||
|         return self.response.body | ||||
|  | ||||
|     def __call__(self, request: Request) -> ResponseStream: | ||||
|         self.request = request | ||||
|         return self | ||||
|  | ||||
|     def __await__(self): | ||||
|         return self.stream().__await__() | ||||
|  | ||||
|  | ||||
| async def file_stream( | ||||
|     location: Union[str, PurePath], | ||||
|     status: int = 200, | ||||
| @@ -427,7 +447,7 @@ async def file_stream( | ||||
|     headers: Optional[Dict[str, str]] = None, | ||||
|     filename: Optional[str] = None, | ||||
|     _range: Optional[Range] = None, | ||||
| ) -> StreamingHTTPResponse: | ||||
| ) -> ResponseStream: | ||||
|     """Return a streaming response object with file data. | ||||
|  | ||||
|     :param location: Location of file on system. | ||||
| @@ -435,7 +455,6 @@ async def file_stream( | ||||
|     :param mime_type: Specific mime_type. | ||||
|     :param headers: Custom Headers. | ||||
|     :param filename: Override filename. | ||||
|     :param chunked: Deprecated | ||||
|     :param _range: | ||||
|     """ | ||||
|     headers = headers or {} | ||||
| @@ -471,23 +490,24 @@ async def file_stream( | ||||
|                         break | ||||
|                     await response.write(content) | ||||
|  | ||||
|     return StreamingHTTPResponse( | ||||
|     return ResponseStream( | ||||
|         streaming_fn=_streaming_fn, | ||||
|         status=status, | ||||
|         headers=headers, | ||||
|         content_type=mime_type, | ||||
|         ignore_deprecation_notice=True, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def stream( | ||||
|     streaming_fn: StreamingFunction, | ||||
|     streaming_fn: Callable[ | ||||
|         [Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None] | ||||
|     ], | ||||
|     status: int = 200, | ||||
|     headers: Optional[Dict[str, str]] = None, | ||||
|     content_type: str = "text/plain; charset=utf-8", | ||||
| ): | ||||
|     """Accepts an coroutine `streaming_fn` which can be used to | ||||
|     write chunks to a streaming response. Returns a `StreamingHTTPResponse`. | ||||
| ) -> ResponseStream: | ||||
|     """Accepts a coroutine `streaming_fn` which can be used to | ||||
|     write chunks to a streaming response. Returns a `ResponseStream`. | ||||
|  | ||||
|     Example usage:: | ||||
|  | ||||
| @@ -501,42 +521,13 @@ def stream( | ||||
|  | ||||
|     :param streaming_fn: A coroutine accepts a response and | ||||
|         writes content to that response. | ||||
|     :param mime_type: Specific mime_type. | ||||
|     :param status: HTTP status. | ||||
|     :param content_type: Specific content_type. | ||||
|     :param headers: Custom Headers. | ||||
|     :param chunked: Deprecated | ||||
|     """ | ||||
|     return StreamingHTTPResponse( | ||||
|     return ResponseStream( | ||||
|         streaming_fn, | ||||
|         headers=headers, | ||||
|         content_type=content_type, | ||||
|         status=status, | ||||
|         ignore_deprecation_notice=True, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def redirect( | ||||
|     to: str, | ||||
|     headers: Optional[Dict[str, str]] = None, | ||||
|     status: int = 302, | ||||
|     content_type: str = "text/html; charset=utf-8", | ||||
| ) -> HTTPResponse: | ||||
|     """ | ||||
|     Abort execution and cause a 302 redirect (by default) by setting a | ||||
|     Location header. | ||||
|  | ||||
|     :param to: path or fully qualified URL to redirect to | ||||
|     :param headers: optional dict of headers to include in the new request | ||||
|     :param status: status code (int) of the new request, defaults to 302 | ||||
|     :param content_type: the content type (string) of the response | ||||
|     """ | ||||
|     headers = headers or {} | ||||
|  | ||||
|     # URL Quote the URL before redirecting | ||||
|     safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;") | ||||
|  | ||||
|     # According to RFC 7231, a relative URI is now permitted. | ||||
|     headers["Location"] = safe_to | ||||
|  | ||||
|     return HTTPResponse( | ||||
|         status=status, headers=headers, content_type=content_type | ||||
|     ) | ||||
|   | ||||
| @@ -1,20 +1,10 @@ | ||||
| import asyncio | ||||
|  | ||||
| from sanic.models.server_types import ConnInfo, Signal | ||||
| from sanic.server.async_server import AsyncioServer | ||||
| from sanic.server.loop import try_use_uvloop | ||||
| from sanic.server.protocols.http_protocol import HttpProtocol | ||||
| from sanic.server.runners import serve, serve_multiple, serve_single | ||||
|  | ||||
|  | ||||
| try: | ||||
|     import uvloop  # type: ignore | ||||
|  | ||||
|     if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy): | ||||
|         asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) | ||||
| except ImportError: | ||||
|     pass | ||||
|  | ||||
|  | ||||
| __all__ = ( | ||||
|     "AsyncioServer", | ||||
|     "ConnInfo", | ||||
| @@ -23,4 +13,5 @@ __all__ = ( | ||||
|     "serve", | ||||
|     "serve_multiple", | ||||
|     "serve_single", | ||||
|     "try_use_uvloop", | ||||
| ) | ||||
|   | ||||
| @@ -3,9 +3,9 @@ from __future__ import annotations | ||||
| import asyncio | ||||
|  | ||||
| from typing import TYPE_CHECKING | ||||
| from warnings import warn | ||||
|  | ||||
| from sanic.exceptions import SanicException | ||||
| from sanic.log import deprecation | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| @@ -37,10 +37,10 @@ class AsyncioServer: | ||||
|  | ||||
|     @property | ||||
|     def init(self): | ||||
|         warn( | ||||
|         deprecation( | ||||
|             "AsyncioServer.init has been deprecated and will be removed " | ||||
|             "in v22.6. Use Sanic.state.is_started instead.", | ||||
|             DeprecationWarning, | ||||
|             22.6, | ||||
|         ) | ||||
|         return self.app.state.is_started | ||||
|  | ||||
|   | ||||
							
								
								
									
										49
									
								
								sanic/server/loop.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								sanic/server/loop.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import asyncio | ||||
|  | ||||
| from distutils.util import strtobool | ||||
| from os import getenv | ||||
|  | ||||
| from sanic.compat import OS_IS_WINDOWS | ||||
| from sanic.log import error_logger | ||||
|  | ||||
|  | ||||
| def try_use_uvloop() -> None: | ||||
|     """ | ||||
|     Use uvloop instead of the default asyncio loop. | ||||
|     """ | ||||
|     if OS_IS_WINDOWS: | ||||
|         error_logger.warning( | ||||
|             "You are trying to use uvloop, but uvloop is not compatible " | ||||
|             "with your system. You can disable uvloop completely by setting " | ||||
|             "the 'USE_UVLOOP' configuration value to false, or simply not " | ||||
|             "defining it and letting Sanic handle it for you. Sanic will now " | ||||
|             "continue to run using the default event loop." | ||||
|         ) | ||||
|         return | ||||
|  | ||||
|     try: | ||||
|         import uvloop  # type: ignore | ||||
|     except ImportError: | ||||
|         error_logger.warning( | ||||
|             "You are trying to use uvloop, but uvloop is not " | ||||
|             "installed in your system. In order to use uvloop " | ||||
|             "you must first install it. Otherwise, you can disable " | ||||
|             "uvloop completely by setting the 'USE_UVLOOP' " | ||||
|             "configuration value to false. Sanic will now continue " | ||||
|             "to run with the default event loop." | ||||
|         ) | ||||
|         return | ||||
|  | ||||
|     uvloop_install_removed = strtobool(getenv("SANIC_NO_UVLOOP", "no")) | ||||
|     if uvloop_install_removed: | ||||
|         error_logger.info( | ||||
|             "You are requesting to run Sanic using uvloop, but the " | ||||
|             "install-time 'SANIC_NO_UVLOOP' environment variable (used to " | ||||
|             "opt-out of installing uvloop with Sanic) is set to true. If " | ||||
|             "you want to prevent Sanic from overriding the event loop policy " | ||||
|             "during runtime, set the 'USE_UVLOOP' configuration value to " | ||||
|             "false." | ||||
|         ) | ||||
|  | ||||
|     if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy): | ||||
|         asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) | ||||
| @@ -1,12 +1,11 @@ | ||||
| from typing import TYPE_CHECKING, Optional, Sequence, cast | ||||
| from warnings import warn | ||||
|  | ||||
| from websockets.connection import CLOSED, CLOSING, OPEN | ||||
| from websockets.server import ServerConnection | ||||
| from websockets.typing import Subprotocol | ||||
|  | ||||
| from sanic.exceptions import ServerError | ||||
| from sanic.log import error_logger | ||||
| from sanic.log import deprecation, error_logger | ||||
| from sanic.server import HttpProtocol | ||||
|  | ||||
| from ..websockets.impl import WebsocketImplProtocol | ||||
| @@ -17,6 +16,14 @@ if TYPE_CHECKING: | ||||
|  | ||||
|  | ||||
| class WebSocketProtocol(HttpProtocol): | ||||
|     __slots__ = ( | ||||
|         "websocket", | ||||
|         "websocket_timeout", | ||||
|         "websocket_max_size", | ||||
|         "websocket_ping_interval", | ||||
|         "websocket_ping_timeout", | ||||
|     ) | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         *args, | ||||
| @@ -35,24 +42,24 @@ class WebSocketProtocol(HttpProtocol): | ||||
|         self.websocket_max_size = websocket_max_size | ||||
|         if websocket_max_queue is not None and websocket_max_queue > 0: | ||||
|             # TODO: Reminder remove this warning in v22.3 | ||||
|             warn( | ||||
|             deprecation( | ||||
|                 "Websocket no longer uses queueing, so websocket_max_queue" | ||||
|                 " is no longer required.", | ||||
|                 DeprecationWarning, | ||||
|                 22.3, | ||||
|             ) | ||||
|         if websocket_read_limit is not None and websocket_read_limit > 0: | ||||
|             # TODO: Reminder remove this warning in v22.3 | ||||
|             warn( | ||||
|             deprecation( | ||||
|                 "Websocket no longer uses read buffers, so " | ||||
|                 "websocket_read_limit is not required.", | ||||
|                 DeprecationWarning, | ||||
|                 22.3, | ||||
|             ) | ||||
|         if websocket_write_limit is not None and websocket_write_limit > 0: | ||||
|             # TODO: Reminder remove this warning in v22.3 | ||||
|             warn( | ||||
|             deprecation( | ||||
|                 "Websocket no longer uses write buffers, so " | ||||
|                 "websocket_write_limit is not required.", | ||||
|                 DeprecationWarning, | ||||
|                 22.3, | ||||
|             ) | ||||
|         self.websocket_ping_interval = websocket_ping_interval | ||||
|         self.websocket_ping_timeout = websocket_ping_timeout | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import sys | ||||
|  | ||||
| from ssl import SSLContext | ||||
| from typing import TYPE_CHECKING, Dict, Optional, Type, Union | ||||
|  | ||||
| @@ -19,6 +21,7 @@ from functools import partial | ||||
| from signal import SIG_IGN, SIGINT, SIGTERM, Signals | ||||
| from signal import signal as signal_func | ||||
|  | ||||
| from sanic.application.ext import setup_ext | ||||
| from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows | ||||
| from sanic.log import error_logger, logger | ||||
| from sanic.models.server_types import Signal | ||||
| @@ -114,6 +117,7 @@ def serve( | ||||
|         **asyncio_server_kwargs, | ||||
|     ) | ||||
|  | ||||
|     setup_ext(app) | ||||
|     if run_async: | ||||
|         return AsyncioServer( | ||||
|             app=app, | ||||
| @@ -174,6 +178,9 @@ def serve( | ||||
|             loop.run_until_complete(asyncio.sleep(0.1)) | ||||
|             start_shutdown = start_shutdown + 0.1 | ||||
|  | ||||
|         if sys.version_info > (3, 7): | ||||
|             app.shutdown_tasks(graceful - start_shutdown) | ||||
|  | ||||
|         # Force close non-idle connection after waiting for | ||||
|         # graceful_shutdown_timeout | ||||
|         for conn in connections: | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import asyncio | ||||
|  | ||||
| from enum import Enum | ||||
| from inspect import isawaitable | ||||
| from typing import Any, Dict, List, Optional, Tuple, Union | ||||
| from typing import Any, Dict, List, Optional, Tuple, Union, cast | ||||
|  | ||||
| from sanic_routing import BaseRouter, Route, RouteGroup  # type: ignore | ||||
| from sanic_routing.exceptions import NotFound  # type: ignore | ||||
| @@ -142,12 +142,21 @@ class SignalRouter(BaseRouter): | ||||
|         if context: | ||||
|             params.update(context) | ||||
|  | ||||
|         signals = group.routes | ||||
|         if not reverse: | ||||
|             handlers = handlers[::-1] | ||||
|             signals = signals[::-1] | ||||
|         try: | ||||
|             for handler in handlers: | ||||
|                 if condition is None or condition == handler.__requirements__: | ||||
|                     maybe_coroutine = handler(**params) | ||||
|             for signal in signals: | ||||
|                 params.pop("__trigger__", None) | ||||
|                 if ( | ||||
|                     (condition is None and signal.ctx.exclusive is False) | ||||
|                     or ( | ||||
|                         condition is None | ||||
|                         and not signal.handler.__requirements__ | ||||
|                     ) | ||||
|                     or (condition == signal.handler.__requirements__) | ||||
|                 ) and (signal.ctx.trigger or event == signal.ctx.definition): | ||||
|                     maybe_coroutine = signal.handler(**params) | ||||
|                     if isawaitable(maybe_coroutine): | ||||
|                         retval = await maybe_coroutine | ||||
|                         if retval: | ||||
| @@ -190,23 +199,36 @@ class SignalRouter(BaseRouter): | ||||
|         handler: SignalHandler, | ||||
|         event: str, | ||||
|         condition: Optional[Dict[str, Any]] = None, | ||||
|         exclusive: bool = True, | ||||
|     ) -> Signal: | ||||
|         event_definition = event | ||||
|         parts = self._build_event_parts(event) | ||||
|         if parts[2].startswith("<"): | ||||
|             name = ".".join([*parts[:-1], "*"]) | ||||
|             trigger = self._clean_trigger(parts[2]) | ||||
|         else: | ||||
|             name = event | ||||
|             trigger = "" | ||||
|  | ||||
|         if not trigger: | ||||
|             event = ".".join([*parts[:2], "<__trigger__>"]) | ||||
|  | ||||
|         handler.__requirements__ = condition  # type: ignore | ||||
|         handler.__trigger__ = trigger  # type: ignore | ||||
|  | ||||
|         return super().add( | ||||
|         signal = super().add( | ||||
|             event, | ||||
|             handler, | ||||
|             requirements=condition, | ||||
|             name=name, | ||||
|             append=True, | ||||
|         )  # type: ignore | ||||
|  | ||||
|         signal.ctx.exclusive = exclusive | ||||
|         signal.ctx.trigger = trigger | ||||
|         signal.ctx.definition = event_definition | ||||
|  | ||||
|         return cast(Signal, signal) | ||||
|  | ||||
|     def finalize(self, do_compile: bool = True, do_optimize: bool = False): | ||||
|         self.add(_blank, "sanic.__signal__.__init__") | ||||
|  | ||||
| @@ -238,3 +260,9 @@ class SignalRouter(BaseRouter): | ||||
|                 "Cannot declare reserved signal event: %s" % event | ||||
|             ) | ||||
|         return parts | ||||
|  | ||||
|     def _clean_trigger(self, trigger: str) -> str: | ||||
|         trigger = trigger[1:-1] | ||||
|         if ":" in trigger: | ||||
|             trigger, _ = trigger.split(":") | ||||
|         return trigger | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.exceptions import SanicException | ||||
|  | ||||
| from .service import TouchUp | ||||
|  | ||||
|  | ||||
| class TouchUpMeta(type): | ||||
| class TouchUpMeta(SanicMeta): | ||||
|     def __new__(cls, name, bases, attrs, **kwargs): | ||||
|         gen_class = super().__new__(cls, name, bases, attrs, **kwargs) | ||||
|  | ||||
|   | ||||
							
								
								
									
										4
									
								
								sanic/types/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								sanic/types/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| from .hashable_dict import HashableDict | ||||
|  | ||||
|  | ||||
| __all__ = ("HashableDict",) | ||||
							
								
								
									
										3
									
								
								sanic/types/hashable_dict.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								sanic/types/hashable_dict.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| class HashableDict(dict): | ||||
|     def __hash__(self): | ||||
|         return hash(tuple(sorted(self.items()))) | ||||
| @@ -9,14 +9,11 @@ from typing import ( | ||||
|     Optional, | ||||
|     Union, | ||||
| ) | ||||
| from warnings import warn | ||||
|  | ||||
| from sanic.constants import HTTP_METHODS | ||||
| from sanic.exceptions import InvalidUsage | ||||
| from sanic.models.handler_types import RouteHandler | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| if TYPE_CHECKING:  # no cov | ||||
|     from sanic import Sanic | ||||
|     from sanic.blueprints import Blueprint | ||||
|  | ||||
| @@ -84,6 +81,8 @@ class HTTPMethodView: | ||||
|  | ||||
|     def dispatch_request(self, request, *args, **kwargs): | ||||
|         handler = getattr(self, request.method.lower(), None) | ||||
|         if not handler and request.method == "HEAD": | ||||
|             handler = self.get | ||||
|         return handler(request, *args, **kwargs) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -136,48 +135,3 @@ class HTTPMethodView: | ||||
| def stream(func): | ||||
|     func.is_stream = True | ||||
|     return func | ||||
|  | ||||
|  | ||||
| class CompositionView: | ||||
|     """Simple method-function mapped view for the sanic. | ||||
|     You can add handler functions to methods (get, post, put, patch, delete) | ||||
|     for every HTTP method you want to support. | ||||
|  | ||||
|     For example: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         view = CompositionView() | ||||
|         view.add(['GET'], lambda request: text('I am get method')) | ||||
|         view.add(['POST', 'PUT'], lambda request: text('I am post/put method')) | ||||
|  | ||||
|     If someone tries to use a non-implemented method, there will be a | ||||
|     405 response. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.handlers = {} | ||||
|         self.name = self.__class__.__name__ | ||||
|         warn( | ||||
|             "CompositionView has been deprecated and will be removed in " | ||||
|             "v21.12. Please update your view to HTTPMethodView.", | ||||
|             DeprecationWarning, | ||||
|         ) | ||||
|  | ||||
|     def __name__(self): | ||||
|         return self.name | ||||
|  | ||||
|     def add(self, methods, handler, stream=False): | ||||
|         if stream: | ||||
|             handler.is_stream = stream | ||||
|         for method in methods: | ||||
|             if method not in HTTP_METHODS: | ||||
|                 raise InvalidUsage(f"{method} is not a valid HTTP method.") | ||||
|  | ||||
|             if method in self.handlers: | ||||
|                 raise InvalidUsage(f"Method {method} is already registered.") | ||||
|             self.handlers[method] = handler | ||||
|  | ||||
|     def __call__(self, request, *args, **kwargs): | ||||
|         handler = self.handlers[request.method.upper()] | ||||
|         return handler(request, *args, **kwargs) | ||||
|   | ||||
| @@ -7,22 +7,19 @@ import traceback | ||||
|  | ||||
| from gunicorn.workers import base  # type: ignore | ||||
|  | ||||
| from sanic.compat import UVLOOP_INSTALLED | ||||
| from sanic.log import logger | ||||
| from sanic.server import HttpProtocol, Signal, serve | ||||
| from sanic.server import HttpProtocol, Signal, serve, try_use_uvloop | ||||
| from sanic.server.protocols.websocket_protocol import WebSocketProtocol | ||||
|  | ||||
|  | ||||
| try: | ||||
|     import ssl  # type: ignore | ||||
| except ImportError: | ||||
| except ImportError:  # no cov | ||||
|     ssl = None  # type: ignore | ||||
|  | ||||
| try: | ||||
|     import uvloop  # type: ignore | ||||
|  | ||||
|     asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) | ||||
| except ImportError: | ||||
|     pass | ||||
| if UVLOOP_INSTALLED:  # no cov | ||||
|     try_use_uvloop() | ||||
|  | ||||
|  | ||||
| class GunicornWorker(base.Worker): | ||||
|   | ||||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								setup.py
									
									
									
									
									
								
							| @@ -147,6 +147,7 @@ extras_require = { | ||||
|     "dev": dev_require, | ||||
|     "docs": docs_require, | ||||
|     "all": all_require, | ||||
|     "ext": ["sanic-ext"], | ||||
| } | ||||
|  | ||||
| setup_kwargs["install_requires"] = requirements | ||||
|   | ||||
| @@ -6,8 +6,10 @@ import string | ||||
| import sys | ||||
| import uuid | ||||
|  | ||||
| from contextlib import suppress | ||||
| from logging import LogRecord | ||||
| from typing import Callable, List, Tuple | ||||
| from typing import List, Tuple | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| @@ -184,3 +186,21 @@ def message_in_records(): | ||||
|         return error_captured | ||||
|  | ||||
|     return msg_in_log | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def ext_instance(): | ||||
|     ext_instance = MagicMock() | ||||
|     ext_instance.injection = MagicMock() | ||||
|     return ext_instance | ||||
|  | ||||
|  | ||||
| @pytest.fixture(autouse=True)  # type: ignore | ||||
| def sanic_ext(ext_instance):  # noqa | ||||
|     sanic_ext = MagicMock(__version__="1.2.3") | ||||
|     sanic_ext.Extend = MagicMock() | ||||
|     sanic_ext.Extend.return_value = ext_instance | ||||
|     sys.modules["sanic_ext"] = sanic_ext | ||||
|     yield sanic_ext | ||||
|     with suppress(KeyError): | ||||
|         del sys.modules["sanic_ext"] | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import json | ||||
| import logging | ||||
|  | ||||
| from sanic import Sanic, text | ||||
| from sanic.log import LOGGING_CONFIG_DEFAULTS, logger | ||||
| @@ -9,7 +8,7 @@ LOGGING_CONFIG = {**LOGGING_CONFIG_DEFAULTS} | ||||
| LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s" | ||||
| LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG" | ||||
|  | ||||
| app = Sanic(__name__, log_config=LOGGING_CONFIG) | ||||
| app = Sanic("FakeServer", log_config=LOGGING_CONFIG) | ||||
|  | ||||
|  | ||||
| @app.get("/") | ||||
|   | ||||
| @@ -2,15 +2,20 @@ import asyncio | ||||
| import logging | ||||
| import re | ||||
|  | ||||
| from collections import Counter | ||||
| from inspect import isawaitable | ||||
| from os import environ | ||||
| from unittest.mock import Mock, patch | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| import sanic | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.compat import OS_IS_WINDOWS | ||||
| from sanic.config import Config | ||||
| from sanic.exceptions import SanicException | ||||
| from sanic.helpers import _default | ||||
| from sanic.response import text | ||||
|  | ||||
|  | ||||
| @@ -19,15 +24,6 @@ def clear_app_registry(): | ||||
|     Sanic._app_registry = {} | ||||
|  | ||||
|  | ||||
| def uvloop_installed(): | ||||
|     try: | ||||
|         import uvloop  # noqa | ||||
|  | ||||
|         return True | ||||
|     except ImportError: | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def test_app_loop_running(app): | ||||
|     @app.get("/test") | ||||
|     async def handler(request): | ||||
| @@ -397,6 +393,22 @@ def test_app_no_registry(): | ||||
|         Sanic.get_app("no-register") | ||||
|  | ||||
|  | ||||
| def test_app_no_registry_deprecation_message(): | ||||
|     with pytest.warns(DeprecationWarning) as records: | ||||
|         Sanic("no-register", register=False) | ||||
|         Sanic("yes-register", register=True) | ||||
|  | ||||
|     message = ( | ||||
|         "[DEPRECATION v22.6] The register argument is deprecated and will " | ||||
|         "stop working in v22.6. After v22.6 all apps will be added to the " | ||||
|         "Sanic app registry." | ||||
|     ) | ||||
|  | ||||
|     assert len(records) == 2 | ||||
|     for record in records: | ||||
|         assert record.message.args[0] == message | ||||
|  | ||||
|  | ||||
| def test_app_no_registry_env(): | ||||
|     environ["SANIC_REGISTER"] = "False" | ||||
|     Sanic("no-register") | ||||
| @@ -408,15 +420,12 @@ def test_app_no_registry_env(): | ||||
|  | ||||
|  | ||||
| def test_app_set_attribute_warning(app): | ||||
|     with pytest.warns(DeprecationWarning) as record: | ||||
|         app.foo = 1 | ||||
|  | ||||
|     assert len(record) == 1 | ||||
|     assert record[0].message.args[0] == ( | ||||
|         "Setting variables on Sanic instances is deprecated " | ||||
|         "and will be removed in version 21.12. You should change your " | ||||
|         "Sanic instance to use instance.ctx.foo instead." | ||||
|     message = ( | ||||
|         "Setting variables on Sanic instances is not allowed. You should " | ||||
|         "change your Sanic instance to use instance.ctx.foo instead." | ||||
|     ) | ||||
|     with pytest.raises(AttributeError, match=message): | ||||
|         app.foo = 1 | ||||
|  | ||||
|  | ||||
| def test_app_set_context(app): | ||||
| @@ -438,15 +447,7 @@ def test_bad_custom_config(): | ||||
|         SanicException, | ||||
|         match=( | ||||
|             "When instantiating Sanic with config, you cannot also pass " | ||||
|             "load_env or env_prefix" | ||||
|         ), | ||||
|     ): | ||||
|         Sanic("test", config=1, load_env=1) | ||||
|     with pytest.raises( | ||||
|         SanicException, | ||||
|         match=( | ||||
|             "When instantiating Sanic with config, you cannot also pass " | ||||
|             "load_env or env_prefix" | ||||
|             "env_prefix" | ||||
|         ), | ||||
|     ): | ||||
|         Sanic("test", config=1, env_prefix=1) | ||||
| @@ -472,6 +473,98 @@ def test_custom_context(): | ||||
|     assert app.ctx == ctx | ||||
|  | ||||
|  | ||||
| def test_uvloop_config(app, monkeypatch): | ||||
|     @app.get("/test") | ||||
|     def handler(request): | ||||
|         return text("ok") | ||||
|  | ||||
|     try_use_uvloop = Mock() | ||||
|     monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop) | ||||
|  | ||||
|     # Default config | ||||
|     app.test_client.get("/test") | ||||
|     if OS_IS_WINDOWS: | ||||
|         try_use_uvloop.assert_not_called() | ||||
|     else: | ||||
|         try_use_uvloop.assert_called_once() | ||||
|  | ||||
|     try_use_uvloop.reset_mock() | ||||
|     app.config["USE_UVLOOP"] = False | ||||
|     app.test_client.get("/test") | ||||
|     try_use_uvloop.assert_not_called() | ||||
|  | ||||
|     try_use_uvloop.reset_mock() | ||||
|     app.config["USE_UVLOOP"] = True | ||||
|     app.test_client.get("/test") | ||||
|     try_use_uvloop.assert_called_once() | ||||
|  | ||||
|  | ||||
| def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch): | ||||
|     apps = (Sanic("default-uvloop"), Sanic("no-uvloop"), Sanic("yes-uvloop")) | ||||
|  | ||||
|     apps[1].config.USE_UVLOOP = False | ||||
|     apps[2].config.USE_UVLOOP = True | ||||
|  | ||||
|     try_use_uvloop = Mock() | ||||
|     monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop) | ||||
|  | ||||
|     loop = asyncio.get_event_loop() | ||||
|  | ||||
|     with caplog.at_level(logging.WARNING): | ||||
|         for app in apps: | ||||
|             srv_coro = app.create_server( | ||||
|                 return_asyncio_server=True, | ||||
|                 asyncio_server_kwargs=dict(start_serving=False), | ||||
|             ) | ||||
|             loop.run_until_complete(srv_coro) | ||||
|  | ||||
|     try_use_uvloop.assert_not_called()  # Check it didn't try to change policy | ||||
|  | ||||
|     message = ( | ||||
|         "You are trying to change the uvloop configuration, but " | ||||
|         "this is only effective when using the run(...) method. " | ||||
|         "When using the create_server(...) method Sanic will use " | ||||
|         "the already existing loop." | ||||
|     ) | ||||
|  | ||||
|     counter = Counter([(r[1], r[2]) for r in caplog.record_tuples]) | ||||
|     modified = sum(1 for app in apps if app.config.USE_UVLOOP is not _default) | ||||
|  | ||||
|     assert counter[(logging.WARNING, message)] == modified | ||||
|  | ||||
|  | ||||
| def test_multiple_uvloop_configs_display_warning(caplog): | ||||
|     Sanic._uvloop_setting = None  # Reset the setting (changed in prev tests) | ||||
|  | ||||
|     default_uvloop = Sanic("default-uvloop") | ||||
|     no_uvloop = Sanic("no-uvloop") | ||||
|     yes_uvloop = Sanic("yes-uvloop") | ||||
|  | ||||
|     no_uvloop.config.USE_UVLOOP = False | ||||
|     yes_uvloop.config.USE_UVLOOP = True | ||||
|  | ||||
|     loop = asyncio.get_event_loop() | ||||
|  | ||||
|     with caplog.at_level(logging.WARNING): | ||||
|         for app in (default_uvloop, no_uvloop, yes_uvloop): | ||||
|             srv_coro = app.create_server( | ||||
|                 return_asyncio_server=True, | ||||
|                 asyncio_server_kwargs=dict(start_serving=False), | ||||
|             ) | ||||
|             srv = loop.run_until_complete(srv_coro) | ||||
|             loop.run_until_complete(srv.startup()) | ||||
|  | ||||
|     message = ( | ||||
|         "It looks like you're running several apps with different " | ||||
|         "uvloop settings. This is not supported and may lead to " | ||||
|         "unintended behaviour." | ||||
|     ) | ||||
|  | ||||
|     counter = Counter([(r[1], r[2]) for r in caplog.record_tuples]) | ||||
|  | ||||
|     assert counter[(logging.WARNING, message)] == 2 | ||||
|  | ||||
|  | ||||
| def test_cannot_run_fast_and_workers(app): | ||||
|     message = "You cannot use both fast=True and workers=X" | ||||
|     with pytest.raises(RuntimeError, match=message): | ||||
|   | ||||
| @@ -145,6 +145,37 @@ def test_listeners_triggered_async(app): | ||||
|     assert after_server_stop | ||||
|  | ||||
|  | ||||
| def test_non_default_uvloop_config_raises_warning(app): | ||||
|     app.config.USE_UVLOOP = True | ||||
|  | ||||
|     class CustomServer(uvicorn.Server): | ||||
|         def install_signal_handlers(self): | ||||
|             pass | ||||
|  | ||||
|     config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0) | ||||
|     server = CustomServer(config=config) | ||||
|  | ||||
|     with pytest.warns(UserWarning) as records: | ||||
|         server.run() | ||||
|  | ||||
|     all_tasks = asyncio.all_tasks(asyncio.get_event_loop()) | ||||
|     for task in all_tasks: | ||||
|         task.cancel() | ||||
|  | ||||
|     msg = "" | ||||
|     for record in records: | ||||
|         _msg = str(record.message) | ||||
|         if _msg.startswith("You have set the USE_UVLOOP configuration"): | ||||
|             msg = _msg | ||||
|             break | ||||
|  | ||||
|     assert msg == ( | ||||
|         "You have set the USE_UVLOOP configuration option, but Sanic " | ||||
|         "cannot control the event loop when running in ASGI mode." | ||||
|         "This option will be ignored." | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_mockprotocol_events(protocol): | ||||
|     assert protocol._not_paused.is_set() | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import pytest | ||||
|  | ||||
| from sanic import Blueprint, Sanic | ||||
| from sanic.exceptions import SanicException | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| @@ -79,24 +80,18 @@ def test_names_okay(name): | ||||
| ) | ||||
| def test_names_not_okay(name): | ||||
|     app_message = ( | ||||
|         f"Sanic instance named '{name}' uses a format that isdeprecated. " | ||||
|         "Starting in version 21.12, Sanic objects must be named only using " | ||||
|         "alphanumeric characters, _, or -." | ||||
|         f"Sanic instance named '{name}' uses an invalid format. Names must " | ||||
|         "begin with a character and may only contain alphanumeric " | ||||
|         "characters, _, or -." | ||||
|     ) | ||||
|     bp_message = ( | ||||
|         f"Blueprint instance named '{name}' uses a format that isdeprecated. " | ||||
|         "Starting in version 21.12, Blueprint objects must be named only using " | ||||
|         "alphanumeric characters, _, or -." | ||||
|         f"Blueprint instance named '{name}' uses an invalid format. Names " | ||||
|         "must begin with a character and may only contain alphanumeric " | ||||
|         "characters, _, or -." | ||||
|     ) | ||||
|  | ||||
|     with pytest.warns(DeprecationWarning) as app_e: | ||||
|         app = Sanic(name) | ||||
|     with pytest.raises(SanicException, match=app_message): | ||||
|         Sanic(name) | ||||
|  | ||||
|     with pytest.warns(DeprecationWarning) as bp_e: | ||||
|         bp = Blueprint(name) | ||||
|  | ||||
|     assert app.name == name | ||||
|     assert bp.name == name | ||||
|  | ||||
|     assert app_e[0].message.args[0] == app_message | ||||
|     assert bp_e[0].message.args[0] == bp_message | ||||
|     with pytest.raises(SanicException, match=bp_message): | ||||
|         Blueprint(name) | ||||
|   | ||||
| @@ -15,7 +15,6 @@ from sanic.exceptions import ( | ||||
| ) | ||||
| from sanic.request import Request | ||||
| from sanic.response import json, text | ||||
| from sanic.views import CompositionView | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| @@ -833,7 +832,7 @@ def test_static_blueprint_name(static_file_directory, file_name): | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file"]) | ||||
| def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | ||||
|     current_file = inspect.getfile(inspect.currentframe()) | ||||
|     current_file = inspect.getfile(inspect.currentframe())  # type: ignore | ||||
|     with open(current_file, "rb") as file: | ||||
|         file.read() | ||||
|  | ||||
| @@ -862,31 +861,6 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | ||||
|     assert triggered is True | ||||
|  | ||||
|  | ||||
| def test_route_handler_add(app: Sanic): | ||||
|     view = CompositionView() | ||||
|  | ||||
|     async def get_handler(request): | ||||
|         return json({"response": "OK"}) | ||||
|  | ||||
|     view.add(["GET"], get_handler, stream=False) | ||||
|  | ||||
|     async def default_handler(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     bp = Blueprint(name="handler", url_prefix="/handler") | ||||
|     bp.add_route(default_handler, uri="/default/", strict_slashes=True) | ||||
|  | ||||
|     bp.add_route(view, uri="/view", name="test") | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     _, response = app.test_client.get("/handler/default/") | ||||
|     assert response.text == "OK" | ||||
|  | ||||
|     _, response = app.test_client.get("/handler/view") | ||||
|     assert response.json["response"] == "OK" | ||||
|  | ||||
|  | ||||
| def test_websocket_route(app: Sanic): | ||||
|     event = asyncio.Event() | ||||
|  | ||||
| @@ -1079,15 +1053,12 @@ def test_blueprint_registered_multiple_apps(): | ||||
|  | ||||
| def test_bp_set_attribute_warning(): | ||||
|     bp = Blueprint("bp") | ||||
|     with pytest.warns(DeprecationWarning) as record: | ||||
|         bp.foo = 1 | ||||
|  | ||||
|     assert len(record) == 1 | ||||
|     assert record[0].message.args[0] == ( | ||||
|         "Setting variables on Blueprint instances is deprecated " | ||||
|         "and will be removed in version 21.12. You should change your " | ||||
|         "Blueprint instance to use instance.ctx.foo instead." | ||||
|     message = ( | ||||
|         "Setting variables on Blueprint instances is not allowed. You should " | ||||
|         "change your Blueprint instance to use instance.ctx.foo instead." | ||||
|     ) | ||||
|     with pytest.raises(AttributeError, match=message): | ||||
|         bp.foo = 1 | ||||
|  | ||||
|  | ||||
| def test_early_registration(app): | ||||
|   | ||||
| @@ -32,6 +32,12 @@ def starting_line(lines): | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| def read_app_info(lines): | ||||
|     for line in lines: | ||||
|         if line.startswith(b"{") and line.endswith(b"}"): | ||||
|             return json.loads(line) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "appname", | ||||
|     ( | ||||
| @@ -199,9 +205,7 @@ def test_debug(cmd): | ||||
|     command = ["sanic", "fake.server.app", cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     app_info = lines[starting_line(lines) + 9] | ||||
|     info = json.loads(app_info) | ||||
|     info = read_app_info(lines) | ||||
|  | ||||
|     assert info["debug"] is True | ||||
|     assert info["auto_reload"] is True | ||||
| @@ -212,9 +216,7 @@ def test_auto_reload(cmd): | ||||
|     command = ["sanic", "fake.server.app", cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     app_info = lines[starting_line(lines) + 9] | ||||
|     info = json.loads(app_info) | ||||
|     info = read_app_info(lines) | ||||
|  | ||||
|     assert info["debug"] is False | ||||
|     assert info["auto_reload"] is True | ||||
| @@ -227,9 +229,7 @@ def test_access_logs(cmd, expected): | ||||
|     command = ["sanic", "fake.server.app", cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     app_info = lines[starting_line(lines) + 8] | ||||
|     info = json.loads(app_info) | ||||
|     info = read_app_info(lines) | ||||
|  | ||||
|     assert info["access_log"] is expected | ||||
|  | ||||
| @@ -254,8 +254,6 @@ def test_noisy_exceptions(cmd, expected): | ||||
|     command = ["sanic", "fake.server.app", cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     app_info = lines[starting_line(lines) + 8] | ||||
|     info = json.loads(app_info) | ||||
|     info = read_app_info(lines) | ||||
|  | ||||
|     assert info["noisy_exceptions"] is expected | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import logging | ||||
|  | ||||
| from contextlib import contextmanager | ||||
| from os import environ | ||||
| from pathlib import Path | ||||
| from tempfile import TemporaryDirectory | ||||
| from textwrap import dedent | ||||
| from unittest.mock import Mock | ||||
| from unittest.mock import Mock, call | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| @@ -32,6 +34,11 @@ class ConfigTest: | ||||
|         return self.not_for_config | ||||
|  | ||||
|  | ||||
| class UltimateAnswer: | ||||
|     def __init__(self, answer): | ||||
|         self.answer = int(answer) | ||||
|  | ||||
|  | ||||
| def test_load_from_object(app): | ||||
|     app.config.load(ConfigTest) | ||||
|     assert "CONFIG_VALUE" in app.config | ||||
| @@ -74,26 +81,6 @@ def test_auto_bool_env_prefix(): | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| def test_dont_load_env(): | ||||
|     environ["SANIC_TEST_ANSWER"] = "42" | ||||
|     app = Sanic(name=__name__, load_env=False) | ||||
|     assert getattr(app.config, "TEST_ANSWER", None) is None | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("load_env", [None, False, "", "MYAPP_"]) | ||||
| def test_load_env_deprecation(load_env): | ||||
|     with pytest.warns(DeprecationWarning, match=r"21\.12"): | ||||
|         _ = Sanic(name=__name__, load_env=load_env) | ||||
|  | ||||
|  | ||||
| def test_load_env_prefix(): | ||||
|     environ["MYAPP_TEST_ANSWER"] = "42" | ||||
|     app = Sanic(name=__name__, load_env="MYAPP_") | ||||
|     assert app.config.TEST_ANSWER == 42 | ||||
|     del environ["MYAPP_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("env_prefix", [None, ""]) | ||||
| def test_empty_load_env_prefix(env_prefix): | ||||
|     environ["SANIC_TEST_ANSWER"] = "42" | ||||
| @@ -102,20 +89,6 @@ def test_empty_load_env_prefix(env_prefix): | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| def test_load_env_prefix_float_values(): | ||||
|     environ["MYAPP_TEST_ROI"] = "2.3" | ||||
|     app = Sanic(name=__name__, load_env="MYAPP_") | ||||
|     assert app.config.TEST_ROI == 2.3 | ||||
|     del environ["MYAPP_TEST_ROI"] | ||||
|  | ||||
|  | ||||
| def test_load_env_prefix_string_value(): | ||||
|     environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken" | ||||
|     app = Sanic(name=__name__, load_env="MYAPP_") | ||||
|     assert app.config.TEST_TOKEN == "somerandomtesttoken" | ||||
|     del environ["MYAPP_TEST_TOKEN"] | ||||
|  | ||||
|  | ||||
| def test_env_prefix(): | ||||
|     environ["MYAPP_TEST_ANSWER"] = "42" | ||||
|     app = Sanic(name=__name__, env_prefix="MYAPP_") | ||||
| @@ -137,6 +110,32 @@ def test_env_prefix_string_value(): | ||||
|     del environ["MYAPP_TEST_TOKEN"] | ||||
|  | ||||
|  | ||||
| def test_env_w_custom_converter(): | ||||
|     environ["SANIC_TEST_ANSWER"] = "42" | ||||
|  | ||||
|     config = Config(converters=[UltimateAnswer]) | ||||
|     app = Sanic(name=__name__, config=config) | ||||
|     assert isinstance(app.config.TEST_ANSWER, UltimateAnswer) | ||||
|     assert app.config.TEST_ANSWER.answer == 42 | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| def test_add_converter_multiple_times(caplog): | ||||
|     def converter(): | ||||
|         ... | ||||
|  | ||||
|     message = ( | ||||
|         "Configuration value converter 'converter' has already been registered" | ||||
|     ) | ||||
|     config = Config() | ||||
|     config.register_type(converter) | ||||
|     with caplog.at_level(logging.WARNING): | ||||
|         config.register_type(converter) | ||||
|  | ||||
|     assert ("sanic.error", logging.WARNING, message) in caplog.record_tuples | ||||
|     assert len(config._converters) == 5 | ||||
|  | ||||
|  | ||||
| def test_load_from_file(app): | ||||
|     config = dedent( | ||||
|         """ | ||||
| @@ -386,5 +385,24 @@ def test_config_set_methods(app, monkeypatch): | ||||
|     post_set.assert_called_once_with("FOO", 5) | ||||
|     post_set.reset_mock() | ||||
|  | ||||
|     app.config.update_config({"FOO": 6}) | ||||
|     post_set.assert_called_once_with("FOO", 6) | ||||
|     app.config.update({"FOO": 6}, {"BAR": 7}) | ||||
|     post_set.assert_has_calls( | ||||
|         calls=[ | ||||
|             call("FOO", 6), | ||||
|             call("BAR", 7), | ||||
|         ] | ||||
|     ) | ||||
|     post_set.reset_mock() | ||||
|  | ||||
|     app.config.update({"FOO": 8}, BAR=9) | ||||
|     post_set.assert_has_calls( | ||||
|         calls=[ | ||||
|             call("FOO", 8), | ||||
|             call("BAR", 9), | ||||
|         ], | ||||
|         any_order=True, | ||||
|     ) | ||||
|     post_set.reset_mock() | ||||
|  | ||||
|     app.config.update_config({"FOO": 10}) | ||||
|     post_set.assert_called_once_with("FOO", 10) | ||||
|   | ||||
| @@ -1,7 +1,12 @@ | ||||
| import asyncio | ||||
| import sys | ||||
|  | ||||
| from threading import Event | ||||
| from unittest.mock import Mock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic.exceptions import SanicException | ||||
| from sanic.response import text | ||||
|  | ||||
|  | ||||
| @@ -48,3 +53,60 @@ def test_create_task_with_app_arg(app): | ||||
|  | ||||
|     _, response = app.test_client.get("/") | ||||
|     assert response.text == "test_create_task_with_app_arg" | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7") | ||||
| def test_create_named_task(app): | ||||
|     async def dummy(): | ||||
|         ... | ||||
|  | ||||
|     @app.before_server_start | ||||
|     async def setup(app, _): | ||||
|         app.add_task(dummy, name="dummy_task") | ||||
|  | ||||
|     @app.after_server_start | ||||
|     async def stop(app, _): | ||||
|         task = app.get_task("dummy_task") | ||||
|  | ||||
|         assert app._task_registry | ||||
|         assert isinstance(task, asyncio.Task) | ||||
|  | ||||
|         assert task.get_name() == "dummy_task" | ||||
|  | ||||
|         app.stop() | ||||
|  | ||||
|     app.run() | ||||
|  | ||||
|  | ||||
| def test_named_task_called(app): | ||||
|     e = Event() | ||||
|  | ||||
|     async def coro(): | ||||
|         e.set() | ||||
|  | ||||
|     @app.route("/") | ||||
|     async def isset(request): | ||||
|         await asyncio.sleep(0.05) | ||||
|         return text(str(e.is_set())) | ||||
|  | ||||
|     @app.before_server_start | ||||
|     async def setup(app, _): | ||||
|         app.add_task(coro, name="dummy_task") | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|     assert response.body == b"True" | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7") | ||||
| def test_create_named_task_fails_outside_app(app): | ||||
|     async def dummy(): | ||||
|         ... | ||||
|  | ||||
|     message = "Cannot name task outside of a running application" | ||||
|     with pytest.raises(RuntimeError, match=message): | ||||
|         app.add_task(dummy, name="dummy_task") | ||||
|     assert not app._task_registry | ||||
|  | ||||
|     message = 'Registered task named "dummy_task" not found.' | ||||
|     with pytest.raises(SanicException, match=message): | ||||
|         app.get_task("dummy_task") | ||||
|   | ||||
							
								
								
									
										9
									
								
								tests/test_deprecation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tests/test_deprecation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import pytest | ||||
|  | ||||
| from sanic.log import deprecation | ||||
|  | ||||
|  | ||||
| def test_deprecation(): | ||||
|     message = r"\[DEPRECATION v9\.9\] hello" | ||||
|     with pytest.warns(DeprecationWarning, match=message): | ||||
|         deprecation("hello", 9.9) | ||||
| @@ -280,40 +280,20 @@ def test_allow_fallback_error_format_set_main_process_start(app): | ||||
|     async def start(app, _): | ||||
|         app.config.FALLBACK_ERROR_FORMAT = "text" | ||||
|  | ||||
|     request, response = app.test_client.get("/error") | ||||
|     assert request.app.error_handler.fallback == "text" | ||||
|     _, response = app.test_client.get("/error") | ||||
|     assert response.status == 500 | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
|  | ||||
| def test_setting_fallback_to_non_default_raise_warning(app): | ||||
|     app.error_handler = ErrorHandler(fallback="text") | ||||
| def test_setting_fallback_on_config_changes_as_expected(app): | ||||
|     app.error_handler = ErrorHandler() | ||||
|  | ||||
|     assert app.error_handler.fallback == "text" | ||||
|  | ||||
|     with pytest.warns( | ||||
|         UserWarning, | ||||
|         match=( | ||||
|             "Overriding non-default ErrorHandler fallback value. " | ||||
|             "Changing from text to auto." | ||||
|         ), | ||||
|     ): | ||||
|         app.config.FALLBACK_ERROR_FORMAT = "auto" | ||||
|  | ||||
|     assert app.error_handler.fallback == "auto" | ||||
|     _, response = app.test_client.get("/error") | ||||
|     assert response.content_type == "text/html; charset=utf-8" | ||||
|  | ||||
|     app.config.FALLBACK_ERROR_FORMAT = "text" | ||||
|  | ||||
|     with pytest.warns( | ||||
|         UserWarning, | ||||
|         match=( | ||||
|             "Overriding non-default ErrorHandler fallback value. " | ||||
|             "Changing from text to json." | ||||
|         ), | ||||
|     ): | ||||
|         app.config.FALLBACK_ERROR_FORMAT = "json" | ||||
|  | ||||
|     assert app.error_handler.fallback == "json" | ||||
|     _, response = app.test_client.get("/error") | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
|  | ||||
| def test_allow_fallback_error_format_in_config_injection(): | ||||
| @@ -327,7 +307,6 @@ def test_allow_fallback_error_format_in_config_injection(): | ||||
|         raise Exception("something went wrong") | ||||
|  | ||||
|     request, response = app.test_client.get("/error") | ||||
|     assert request.app.error_handler.fallback == "text" | ||||
|     assert response.status == 500 | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
| @@ -339,6 +318,39 @@ def test_allow_fallback_error_format_in_config_replacement(app): | ||||
|     app.config = MyConfig() | ||||
|  | ||||
|     request, response = app.test_client.get("/error") | ||||
|     assert request.app.error_handler.fallback == "text" | ||||
|     assert response.status == 500 | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
|  | ||||
| def test_config_fallback_before_and_after_startup(app): | ||||
|     app.config.FALLBACK_ERROR_FORMAT = "json" | ||||
|  | ||||
|     @app.main_process_start | ||||
|     async def start(app, _): | ||||
|         app.config.FALLBACK_ERROR_FORMAT = "text" | ||||
|  | ||||
|     _, response = app.test_client.get("/error") | ||||
|     assert response.status == 500 | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
|  | ||||
| def test_config_fallback_using_update_dict(app): | ||||
|     app.config.update({"FALLBACK_ERROR_FORMAT": "text"}) | ||||
|  | ||||
|     _, response = app.test_client.get("/error") | ||||
|     assert response.status == 500 | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
|  | ||||
| def test_config_fallback_using_update_kwarg(app): | ||||
|     app.config.update(FALLBACK_ERROR_FORMAT="text") | ||||
|  | ||||
|     _, response = app.test_client.get("/error") | ||||
|     assert response.status == 500 | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
|  | ||||
| def test_config_fallback_bad_value(app): | ||||
|     message = "Unknown format: fake" | ||||
|     with pytest.raises(SanicException, match=message): | ||||
|         app.config.FALLBACK_ERROR_FORMAT = "fake" | ||||
|   | ||||
| @@ -13,7 +13,6 @@ from sanic.exceptions import ( | ||||
|     SanicException, | ||||
|     ServerError, | ||||
|     Unauthorized, | ||||
|     abort, | ||||
| ) | ||||
| from sanic.response import text | ||||
|  | ||||
| @@ -88,10 +87,6 @@ def exception_app(): | ||||
|     def handler_500_error(request): | ||||
|         raise SanicException(status_code=500) | ||||
|  | ||||
|     @app.route("/old_abort") | ||||
|     def handler_old_abort_error(request): | ||||
|         abort(500) | ||||
|  | ||||
|     @app.route("/abort/message") | ||||
|     def handler_abort_message(request): | ||||
|         raise SanicException(message="Custom Message", status_code=500) | ||||
| @@ -239,11 +234,6 @@ def test_sanic_exception(exception_app): | ||||
|     assert response.status == 500 | ||||
|     assert "Custom Message" in response.text | ||||
|  | ||||
|     with warnings.catch_warnings(record=True) as w: | ||||
|         request, response = exception_app.test_client.get("/old_abort") | ||||
|     assert response.status == 500 | ||||
|     assert len(w) == 1 and "deprecated" in w[0].message.args[0] | ||||
|  | ||||
|  | ||||
| def test_custom_exception_default_message(exception_app): | ||||
|     class TeaError(SanicException): | ||||
| @@ -262,7 +252,7 @@ def test_custom_exception_default_message(exception_app): | ||||
|  | ||||
|  | ||||
| def test_exception_in_ws_logged(caplog): | ||||
|     app = Sanic(__file__) | ||||
|     app = Sanic(__name__) | ||||
|  | ||||
|     @app.websocket("/feed") | ||||
|     async def feed(request, ws): | ||||
| @@ -271,9 +261,13 @@ def test_exception_in_ws_logged(caplog): | ||||
|     with caplog.at_level(logging.INFO): | ||||
|         app.test_client.websocket("/feed") | ||||
|  | ||||
|     error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"] | ||||
|     assert error_logs[1][1] == logging.ERROR | ||||
|     assert "Exception occurred while handling uri:" in error_logs[1][2] | ||||
|     for record in caplog.record_tuples: | ||||
|         if record[2].startswith("Exception occurred"): | ||||
|             break | ||||
|  | ||||
|     assert record[0] == "sanic.error" | ||||
|     assert record[1] == logging.ERROR | ||||
|     assert "Exception occurred while handling uri:" in record[2] | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("debug", (True, False)) | ||||
|   | ||||
| @@ -226,11 +226,12 @@ def test_single_arg_exception_handler_notice( | ||||
|     exception_handler_app.error_handler = CustomErrorHandler() | ||||
|  | ||||
|     message = ( | ||||
|         "You are using a deprecated error handler. The lookup method should " | ||||
|         "accept two positional parameters: (exception, route_name: " | ||||
|         "Optional[str]). Until you upgrade your ErrorHandler.lookup, " | ||||
|         "Blueprint specific exceptions will not work properly. Beginning in " | ||||
|         "v22.3, the legacy style lookup method will not work at all." | ||||
|         "[DEPRECATION v22.3] You are using a deprecated error handler. The " | ||||
|         "lookup method should accept two positional parameters: (exception, " | ||||
|         "route_name: Optional[str]). Until you upgrade your " | ||||
|         "ErrorHandler.lookup, Blueprint specific exceptions will not work " | ||||
|         "properly. Beginning in v22.3, the legacy style lookup method will " | ||||
|         "not work at all." | ||||
|     ) | ||||
|     with pytest.warns(DeprecationWarning) as record: | ||||
|         _, response = exception_handler_app.test_client.get("/1") | ||||
|   | ||||
							
								
								
									
										84
									
								
								tests/test_ext_integration.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								tests/test_ext_integration.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| import sys | ||||
|  | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
|  | ||||
|  | ||||
| try: | ||||
|     import sanic_ext | ||||
|  | ||||
|     SANIC_EXT_IN_ENV = True | ||||
| except ImportError: | ||||
|     SANIC_EXT_IN_ENV = False | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def stoppable_app(app): | ||||
|     @app.before_server_start | ||||
|     async def stop(*_): | ||||
|         app.stop() | ||||
|  | ||||
|     return app | ||||
|  | ||||
|  | ||||
| def test_ext_is_loaded(stoppable_app: Sanic, sanic_ext): | ||||
|     stoppable_app.run() | ||||
|     sanic_ext.Extend.assert_called_once_with(stoppable_app) | ||||
|  | ||||
|  | ||||
| def test_ext_is_not_loaded(stoppable_app: Sanic, sanic_ext): | ||||
|     stoppable_app.config.AUTO_EXTEND = False | ||||
|     stoppable_app.run() | ||||
|     sanic_ext.Extend.assert_not_called() | ||||
|  | ||||
|  | ||||
| def test_extend_with_args(stoppable_app: Sanic, sanic_ext): | ||||
|     stoppable_app.extend(built_in_extensions=False) | ||||
|     stoppable_app.run() | ||||
|     sanic_ext.Extend.assert_called_once_with( | ||||
|         stoppable_app, built_in_extensions=False, config=None, extensions=None | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_access_object_sets_up_extension(app: Sanic, sanic_ext): | ||||
|     app.ext | ||||
|     sanic_ext.Extend.assert_called_once_with(app) | ||||
|  | ||||
|  | ||||
| def test_extend_cannot_be_called_multiple_times(app: Sanic, sanic_ext): | ||||
|     app.extend() | ||||
|  | ||||
|     message = "Cannot extend Sanic after Sanic Extensions has been setup." | ||||
|     with pytest.raises(RuntimeError, match=message): | ||||
|         app.extend() | ||||
|     sanic_ext.Extend.assert_called_once_with( | ||||
|         app, extensions=None, built_in_extensions=True, config=None | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     SANIC_EXT_IN_ENV, | ||||
|     reason="Running tests with sanic_ext already in the environment", | ||||
| ) | ||||
| def test_fail_if_not_loaded(app: Sanic): | ||||
|     del sys.modules["sanic_ext"] | ||||
|     with pytest.raises( | ||||
|         RuntimeError, match="Sanic Extensions is not installed.*" | ||||
|     ): | ||||
|         app.extend(built_in_extensions=False) | ||||
|  | ||||
|  | ||||
| def test_can_access_app_ext_while_running(app: Sanic, sanic_ext, ext_instance): | ||||
|     class IceCream: | ||||
|         flavor: str | ||||
|  | ||||
|     @app.before_server_start | ||||
|     async def injections(*_): | ||||
|         app.ext.injection(IceCream) | ||||
|         app.stop() | ||||
|  | ||||
|     app.run() | ||||
|     ext_instance.injection.assert_called_with(IceCream) | ||||
| @@ -2,7 +2,6 @@ import asyncio | ||||
| import logging | ||||
| import time | ||||
|  | ||||
| from collections import Counter | ||||
| from multiprocessing import Process | ||||
|  | ||||
| import httpx | ||||
| @@ -36,11 +35,14 @@ def test_no_exceptions_when_cancel_pending_request(app, caplog): | ||||
|  | ||||
|     p.kill() | ||||
|  | ||||
|     counter = Counter([r[1] for r in caplog.record_tuples]) | ||||
|  | ||||
|     assert counter[logging.INFO] == 11 | ||||
|     assert logging.ERROR not in counter | ||||
|     assert ( | ||||
|         caplog.record_tuples[9][2] | ||||
|         == "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed." | ||||
|     ) | ||||
|     info = 0 | ||||
|     for record in caplog.record_tuples: | ||||
|         assert record[1] != logging.ERROR | ||||
|         if record[1] == logging.INFO: | ||||
|             info += 1 | ||||
|         if record[2].startswith("Request:"): | ||||
|             assert record[2] == ( | ||||
|                 "Request: GET http://127.0.0.1:8000/ stopped. " | ||||
|                 "Transport is closed." | ||||
|             ) | ||||
|     assert info == 11 | ||||
|   | ||||
| @@ -45,7 +45,7 @@ def default_back_to_ujson(): | ||||
|  | ||||
|  | ||||
| def test_change_encoder(): | ||||
|     Sanic("...", dumps=sdumps) | ||||
|     Sanic("Test", dumps=sdumps) | ||||
|     assert BaseHTTPResponse._dumps == sdumps | ||||
|  | ||||
|  | ||||
| @@ -53,7 +53,7 @@ def test_change_encoder_to_some_custom(): | ||||
|     def my_custom_encoder(): | ||||
|         return "foo" | ||||
|  | ||||
|     Sanic("...", dumps=my_custom_encoder) | ||||
|     Sanic("Test", dumps=my_custom_encoder) | ||||
|     assert BaseHTTPResponse._dumps == my_custom_encoder | ||||
|  | ||||
|  | ||||
| @@ -68,7 +68,7 @@ def test_json_response_ujson(payload): | ||||
|     ): | ||||
|         json(payload, dumps=sdumps) | ||||
|  | ||||
|     Sanic("...", dumps=sdumps) | ||||
|     Sanic("Test", dumps=sdumps) | ||||
|     with pytest.raises( | ||||
|         TypeError, match="Object of type Foo is not JSON serializable" | ||||
|     ): | ||||
| @@ -87,6 +87,6 @@ def test_json_response_json(): | ||||
|     response = json(too_big_for_ujson, dumps=sdumps) | ||||
|     assert sys.getsizeof(response.body) == 54 | ||||
|  | ||||
|     Sanic("...", dumps=sdumps) | ||||
|     Sanic("Test", dumps=sdumps) | ||||
|     response = json(too_big_for_ujson) | ||||
|     assert sys.getsizeof(response.body) == 54 | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user