Compare commits
	
		
			16 Commits
		
	
	
		
			pre-regist
			...
			py37-catch
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 162cb43c4b | ||
|   | 698a359808 | ||
|   | 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: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [ main ] |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|   pull_request: |   pull_request: | ||||||
|     branches: [ main ] |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|   schedule: |   schedule: | ||||||
|     - cron: '25 16 * * 0' |     - cron: '25 16 * * 0' | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     tags: |     tags: | ||||||
|       - "!*" # Do not execute on tags |       - "!*" # Do not execute on tags | ||||||
|   pull_request: |   pull_request: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
| @@ -15,7 +16,7 @@ jobs: | |||||||
|       matrix: |       matrix: | ||||||
|         os: [ubuntu-latest] |         os: [ubuntu-latest] | ||||||
|         config: |         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.8, tox-env: type-checking} | ||||||
|           - { python-version: 3.9, tox-env: type-checking} |           - { python-version: 3.9, tox-env: type-checking} | ||||||
|           - { python-version: "3.10", 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: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										192
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							| @@ -1,12 +1,12 @@ | |||||||
| .. note:: | .. 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 | Version 21.6.1 | ||||||
| -------------- | -------------- | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_ |   * `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_ | ||||||
|     Update sanic-routing to allow for better splitting of complex URI templates |     Update sanic-routing to allow for better splitting of complex URI templates | ||||||
| @@ -20,8 +20,7 @@ Bugfixes | |||||||
| Version 21.6.0 | Version 21.6.0 | ||||||
| -------------- | -------------- | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_ |   * `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_ | ||||||
|     Add ``response.eof()`` method for closing a stream in a handler |     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>`_ |   * `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_ | ||||||
|     Additional methods for attaching ``HTTPMethodView`` |     Additional methods for attaching ``HTTPMethodView`` | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_ |   * `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_ | ||||||
|     Fix ``UserWarning`` in ASGI mode for missing ``__slots__`` |     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 |     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>`_ |   * `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_ | ||||||
|     Remove config value ``REQUEST_BUFFER_QUEUE_SIZE`` |     Remove config value ``REQUEST_BUFFER_QUEUE_SIZE`` | ||||||
| @@ -95,14 +92,12 @@ Deprecations and Removals | |||||||
|   * `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_ |   * `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_ | ||||||
|     Deprecate StreamingHTTPResponse |     Deprecate StreamingHTTPResponse | ||||||
|  |  | ||||||
| Developer infrastructure | **Developer infrastructure** | ||||||
| ************************ |  | ||||||
|  |  | ||||||
|   * `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_ |   * `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_ | ||||||
|     Remove Travis CI in favor of GitHub Actions |     Remove Travis CI in favor of GitHub Actions | ||||||
|  |  | ||||||
| Improved Documentation | **Improved Documentation** | ||||||
| ********************** |  | ||||||
|  |  | ||||||
|   * `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_ |   * `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_ | ||||||
|     Fix typo in documentation |     Fix typo in documentation | ||||||
| @@ -112,8 +107,7 @@ Improved Documentation | |||||||
| Version 21.3.2 | Version 21.3.2 | ||||||
| -------------- | -------------- | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_ |   * `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_ | ||||||
|     Disable response timeout on websocket connections |     Disable response timeout on websocket connections | ||||||
| @@ -124,8 +118,7 @@ Bugfixes | |||||||
| Version 21.3.1 | Version 21.3.1 | ||||||
| -------------- | -------------- | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_ |   * `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_ | ||||||
|     Static files inside subfolders are not accessible (404) |     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>`_ | `Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_ | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1876 <https://github.com/sanic-org/sanic/pull/1876>`_ |     `#1876 <https://github.com/sanic-org/sanic/pull/1876>`_ | ||||||
| @@ -189,8 +181,7 @@ Features | |||||||
|     `#2063 <https://github.com/sanic-org/sanic/pull/2063>`_ |     `#2063 <https://github.com/sanic-org/sanic/pull/2063>`_ | ||||||
|     App and connection level context objects |     App and connection level context objects | ||||||
|  |  | ||||||
| Bugfixes and issues resolved | **Bugfixes** | ||||||
| **************************** |  | ||||||
|  |  | ||||||
|   * Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_ |   * Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_ | ||||||
|     ``url_for`` where ``strict_slashes`` are on for a path ending in ``/`` |     ``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>`_ |     `#2001 <https://github.com/sanic-org/sanic/pull/2001>`_ | ||||||
|     Raise ValueError when cookie max-age is not an integer |     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>`_ |     `#2007 <https://github.com/sanic-org/sanic/pull/2007>`_ | ||||||
| @@ -240,8 +230,7 @@ Deprecations and Removals | |||||||
|   * ``Request.endpoint`` deprecated in favor of ``Request.name`` |   * ``Request.endpoint`` deprecated in favor of ``Request.name`` | ||||||
|   * handler type name prefixes removed (static, websocket, etc) |   * handler type name prefixes removed (static, websocket, etc) | ||||||
|  |  | ||||||
| Developer infrastructure | **Developer infrastructure** | ||||||
| ************************ |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1995 <https://github.com/sanic-org/sanic/pull/1995>`_ |     `#1995 <https://github.com/sanic-org/sanic/pull/1995>`_ | ||||||
| @@ -259,8 +248,7 @@ Developer infrastructure | |||||||
|     `#2049 <https://github.com/sanic-org/sanic/pull/2049>`_ |     `#2049 <https://github.com/sanic-org/sanic/pull/2049>`_ | ||||||
|     Updated setup.py to use ``find_packages`` |     Updated setup.py to use ``find_packages`` | ||||||
|  |  | ||||||
| Improved Documentation | **Improved Documentation** | ||||||
| ********************** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1218 <https://github.com/sanic-org/sanic/pull/1218>`_ |     `#1218 <https://github.com/sanic-org/sanic/pull/1218>`_ | ||||||
| @@ -282,8 +270,7 @@ Improved Documentation | |||||||
|     `#2052 <https://github.com/sanic-org/sanic/pull/2052>`_ |     `#2052 <https://github.com/sanic-org/sanic/pull/2052>`_ | ||||||
|     Fix some examples and docs |     Fix some examples and docs | ||||||
|  |  | ||||||
| Miscellaneous | **Miscellaneous** | ||||||
| ************* |  | ||||||
|  |  | ||||||
|   * ``Request.route`` property |   * ``Request.route`` property | ||||||
|   * Better websocket subprotocols support |   * Better websocket subprotocols support | ||||||
| @@ -329,8 +316,7 @@ Miscellaneous | |||||||
| Version 20.12.3 | Version 20.12.3 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#2021 <https://github.com/sanic-org/sanic/pull/2021>`_ |     `#2021 <https://github.com/sanic-org/sanic/pull/2021>`_ | ||||||
| @@ -339,8 +325,7 @@ Bugfixes | |||||||
| Version 20.12.2 | Version 20.12.2 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Dependencies | **Dependencies** | ||||||
| ************ |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#2026 <https://github.com/sanic-org/sanic/pull/2026>`_ |     `#2026 <https://github.com/sanic-org/sanic/pull/2026>`_ | ||||||
| @@ -353,8 +338,7 @@ Dependencies | |||||||
| Version 19.12.5 | Version 19.12.5 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Dependencies | **Dependencies** | ||||||
| ************ |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#2025 <https://github.com/sanic-org/sanic/pull/2025>`_ |     `#2025 <https://github.com/sanic-org/sanic/pull/2025>`_ | ||||||
| @@ -367,8 +351,7 @@ Dependencies | |||||||
| Version 20.12.0 | Version 20.12.0 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1993 <https://github.com/sanic-org/sanic/pull/1993>`_ |     `#1993 <https://github.com/sanic-org/sanic/pull/1993>`_ | ||||||
| @@ -377,8 +360,7 @@ Features | |||||||
| Version 20.12.0 | Version 20.12.0 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1945 <https://github.com/sanic-org/sanic/pull/1945>`_ |     `#1945 <https://github.com/sanic-org/sanic/pull/1945>`_ | ||||||
| @@ -416,22 +398,19 @@ Features | |||||||
|     `#1979 <https://github.com/sanic-org/sanic/pull/1979>`_ |     `#1979 <https://github.com/sanic-org/sanic/pull/1979>`_ | ||||||
|     Add app registry and Sanic class level app retrieval |     Add app registry and Sanic class level app retrieval | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1965 <https://github.com/sanic-org/sanic/pull/1965>`_ |     `#1965 <https://github.com/sanic-org/sanic/pull/1965>`_ | ||||||
|     Fix Chunked Transport-Encoding in ASGI streaming response |     Fix Chunked Transport-Encoding in ASGI streaming response | ||||||
|  |  | ||||||
| Deprecations and Removals | **Deprecations and Removals** | ||||||
| ************************* |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1981 <https://github.com/sanic-org/sanic/pull/1981>`_ |     `#1981 <https://github.com/sanic-org/sanic/pull/1981>`_ | ||||||
|     Cleanup and remove deprecated code |     Cleanup and remove deprecated code | ||||||
|  |  | ||||||
| Developer infrastructure | **Developer infrastructure** | ||||||
| ************************ |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1956 <https://github.com/sanic-org/sanic/pull/1956>`_ |     `#1956 <https://github.com/sanic-org/sanic/pull/1956>`_ | ||||||
| @@ -445,8 +424,7 @@ Developer infrastructure | |||||||
|     `#1986 <https://github.com/sanic-org/sanic/pull/1986>`_ |     `#1986 <https://github.com/sanic-org/sanic/pull/1986>`_ | ||||||
|     Update tox requirements |     Update tox requirements | ||||||
|  |  | ||||||
| Improved Documentation | **Improved Documentation** | ||||||
| ********************** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1951 <https://github.com/sanic-org/sanic/pull/1951>`_ |     `#1951 <https://github.com/sanic-org/sanic/pull/1951>`_ | ||||||
| @@ -464,8 +442,7 @@ Improved Documentation | |||||||
| Version 20.9.1 | Version 20.9.1 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1954 <https://github.com/sanic-org/sanic/pull/1954>`_ |     `#1954 <https://github.com/sanic-org/sanic/pull/1954>`_ | ||||||
| @@ -478,8 +455,7 @@ Bugfixes | |||||||
| Version 19.12.3 | Version 19.12.3 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1959 <https://github.com/sanic-org/sanic/pull/1959>`_ |     `#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>`_ |     `#1887 <https://github.com/sanic-org/sanic/pull/1887>`_ | ||||||
| @@ -518,22 +493,19 @@ Features | |||||||
|     `#1937 <https://github.com/sanic-org/sanic/pull/1937>`_ |     `#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) |     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>`_ |     `#1897 <https://github.com/sanic-org/sanic/pull/1897>`_ | ||||||
|     Resolves exception from unread bytes in stream |     Resolves exception from unread bytes in stream | ||||||
|  |  | ||||||
| Deprecations and Removals | **Deprecations and Removals** | ||||||
| ************************* |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1903 <https://github.com/sanic-org/sanic/pull/1903>`_ |     `#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 |     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>`_, |     `#1890 <https://github.com/sanic-org/sanic/pull/1890>`_, | ||||||
| @@ -548,8 +520,7 @@ Developer infrastructure | |||||||
|     `#1924 <https://github.com/sanic-org/sanic/pull/1924>`_ |     `#1924 <https://github.com/sanic-org/sanic/pull/1924>`_ | ||||||
|     Adding --strict-markers for pytest |     Adding --strict-markers for pytest | ||||||
|  |  | ||||||
| Improved Documentation | **Improved Documentation** | ||||||
| ********************** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1922 <https://github.com/sanic-org/sanic/pull/1922>`_ |     `#1922 <https://github.com/sanic-org/sanic/pull/1922>`_ | ||||||
| @@ -559,8 +530,7 @@ Improved Documentation | |||||||
| Version 20.6.3 | Version 20.6.3 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1884 <https://github.com/sanic-org/sanic/pull/1884>`_ |     `#1884 <https://github.com/sanic-org/sanic/pull/1884>`_ | ||||||
| @@ -570,8 +540,7 @@ Bugfixes | |||||||
| Version 20.6.2 | Version 20.6.2 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1641 <https://github.com/sanic-org/sanic/pull/1641>`_ |     `#1641 <https://github.com/sanic-org/sanic/pull/1641>`_ | ||||||
| @@ -581,8 +550,7 @@ Features | |||||||
| Version 20.6.1 | Version 20.6.1 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1760 <https://github.com/sanic-org/sanic/pull/1760>`_ |     `#1760 <https://github.com/sanic-org/sanic/pull/1760>`_ | ||||||
| @@ -596,8 +564,7 @@ Features | |||||||
|     `#1880 <https://github.com/sanic-org/sanic/pull/1880>`_ |     `#1880 <https://github.com/sanic-org/sanic/pull/1880>`_ | ||||||
|     Add handler names for websockets for url_for usage |     Add handler names for websockets for url_for usage | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1776 <https://github.com/sanic-org/sanic/pull/1776>`_ |     `#1776 <https://github.com/sanic-org/sanic/pull/1776>`_ | ||||||
| @@ -619,15 +586,13 @@ Bugfixes | |||||||
|     `#1853 <https://github.com/sanic-org/sanic/pull/1853>`_ |     `#1853 <https://github.com/sanic-org/sanic/pull/1853>`_ | ||||||
|     Fix pickle error when attempting to pickle an application which contains websocket routes |     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>`_ |     `#1739 <https://github.com/sanic-org/sanic/pull/1739>`_ | ||||||
|     Deprecate body_bytes to merge into body |     Deprecate body_bytes to merge into body | ||||||
|  |  | ||||||
| Developer infrastructure | **Developer infrastructure** | ||||||
| ************************ |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1852 <https://github.com/sanic-org/sanic/pull/1852>`_ |     `#1852 <https://github.com/sanic-org/sanic/pull/1852>`_ | ||||||
| @@ -642,8 +607,7 @@ Developer infrastructure | |||||||
|     Wrap run()'s "protocol" type annotation in Optional[] |     Wrap run()'s "protocol" type annotation in Optional[] | ||||||
|  |  | ||||||
|  |  | ||||||
| Improved Documentation | **Improved Documentation** | ||||||
| ********************** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1846 <https://github.com/sanic-org/sanic/pull/1846>`_ |     `#1846 <https://github.com/sanic-org/sanic/pull/1846>`_ | ||||||
| @@ -663,8 +627,7 @@ Version 20.6.0 | |||||||
| Version 20.3.0 | Version 20.3.0 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1762 <https://github.com/sanic-org/sanic/pull/1762>`_ |     `#1762 <https://github.com/sanic-org/sanic/pull/1762>`_ | ||||||
| @@ -695,8 +658,7 @@ Features | |||||||
|     `#1820 <https://github.com/sanic-org/sanic/pull/1820>`_ |     `#1820 <https://github.com/sanic-org/sanic/pull/1820>`_ | ||||||
|     Do not set content-type and content-length headers in exceptions |     Do not set content-type and content-length headers in exceptions | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1748 <https://github.com/sanic-org/sanic/pull/1748>`_ |     `#1748 <https://github.com/sanic-org/sanic/pull/1748>`_ | ||||||
| @@ -714,8 +676,7 @@ Bugfixes | |||||||
|     `#1808 <https://github.com/sanic-org/sanic/pull/1808>`_ |     `#1808 <https://github.com/sanic-org/sanic/pull/1808>`_ | ||||||
|      Fix Ctrl+C and tests on Windows |      Fix Ctrl+C and tests on Windows | ||||||
|  |  | ||||||
| Deprecations and Removals | **Deprecations and Removals** | ||||||
| ************************* |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1800 <https://github.com/sanic-org/sanic/pull/1800>`_ |     `#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>`_ |     `#1818 <https://github.com/sanic-org/sanic/pull/1818>`_ | ||||||
|     Complete deprecation of ``app.remove_route`` and ``request.raw_args`` |     Complete deprecation of ``app.remove_route`` and ``request.raw_args`` | ||||||
|  |  | ||||||
| Dependencies | **Dependencies** | ||||||
| ************ |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1794 <https://github.com/sanic-org/sanic/pull/1794>`_ |     `#1794 <https://github.com/sanic-org/sanic/pull/1794>`_ | ||||||
| @@ -744,15 +704,13 @@ Dependencies | |||||||
|     `#1806 <https://github.com/sanic-org/sanic/pull/1806>`_ |     `#1806 <https://github.com/sanic-org/sanic/pull/1806>`_ | ||||||
|     Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation) |     Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation) | ||||||
|  |  | ||||||
| Developer infrastructure | **Developer infrastructure** | ||||||
| ************************ |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1833 <https://github.com/sanic-org/sanic/pull/1833>`_ |     `#1833 <https://github.com/sanic-org/sanic/pull/1833>`_ | ||||||
|     Resolve broken documentation builds |     Resolve broken documentation builds | ||||||
|  |  | ||||||
| Improved Documentation | **Improved Documentation** | ||||||
| ********************** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1755 <https://github.com/sanic-org/sanic/pull/1755>`_ |     `#1755 <https://github.com/sanic-org/sanic/pull/1755>`_ | ||||||
| @@ -794,8 +752,7 @@ Improved Documentation | |||||||
| Version 19.12.0 | Version 19.12.0 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
| - Fix blueprint middleware application | - 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>`__) |   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 | - Move docs from MD to RST | ||||||
|  |  | ||||||
| @@ -829,8 +785,7 @@ Improved Documentation | |||||||
| Version 19.6.3 | Version 19.6.3 | ||||||
| -------------- | -------------- | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
| - Enable Towncrier Support | - 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>`__) |   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 | - Documentation infrastructure changes | ||||||
|  |  | ||||||
| @@ -852,8 +806,7 @@ Improved Documentation | |||||||
| Version 19.6.2 | Version 19.6.2 | ||||||
| -------------- | -------------- | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1562 <https://github.com/sanic-org/sanic/pull/1562>`_ |     `#1562 <https://github.com/sanic-org/sanic/pull/1562>`_ | ||||||
| @@ -869,8 +822,7 @@ Features | |||||||
|     Add Configure support from object string |     Add Configure support from object string | ||||||
|  |  | ||||||
|  |  | ||||||
| Bugfixes | **Bugfixes** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1587 <https://github.com/sanic-org/sanic/pull/1587>`_ |     `#1587 <https://github.com/sanic-org/sanic/pull/1587>`_ | ||||||
| @@ -888,8 +840,7 @@ Bugfixes | |||||||
|     `#1594 <https://github.com/sanic-org/sanic/pull/1594>`_ |     `#1594 <https://github.com/sanic-org/sanic/pull/1594>`_ | ||||||
|     Strict Slashes behavior fix |     Strict Slashes behavior fix | ||||||
|  |  | ||||||
| Deprecations and Removals | **Deprecations and Removals** | ||||||
| ************************* |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1544 <https://github.com/sanic-org/sanic/pull/1544>`_ |     `#1544 <https://github.com/sanic-org/sanic/pull/1544>`_ | ||||||
| @@ -913,8 +864,7 @@ Deprecations and Removals | |||||||
| Version 19.3 | Version 19.3 | ||||||
| ------------ | ------------ | ||||||
|  |  | ||||||
| Features | **Features** | ||||||
| ******** |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1497 <https://github.com/sanic-org/sanic/pull/1497>`_ |     `#1497 <https://github.com/sanic-org/sanic/pull/1497>`_ | ||||||
| @@ -982,8 +932,7 @@ Features | |||||||
|  |  | ||||||
|     This is a breaking change. |     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 |     This allows the access log to be disabled for example when running via | ||||||
|     gunicorn. |     gunicorn. | ||||||
|  |  | ||||||
| Developer infrastructure | **Developer infrastructure** | ||||||
| ************************ |  | ||||||
|  |  | ||||||
|   * `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials |   * `#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) |   * `#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 |   * `#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 |   * `#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 |   * `#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 |   * `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example | ||||||
| @@ -1096,15 +1043,13 @@ Version 18.12 | |||||||
| Version 0.8 | Version 0.8 | ||||||
| ----------- | ----------- | ||||||
|  |  | ||||||
| 0.8.3 | **0.8.3** | ||||||
| ***** |  | ||||||
|  |  | ||||||
| * Changes: | * Changes: | ||||||
|  |  | ||||||
|   * Ownership changed to org 'sanic-org' |   * Ownership changed to org 'sanic-org' | ||||||
|  |  | ||||||
| 0.8.0 | **0.8.0** | ||||||
| ***** |  | ||||||
|  |  | ||||||
| * Changes: | * Changes: | ||||||
|  |  | ||||||
| @@ -1184,19 +1129,16 @@ Version 0.1 | |||||||
| ----------- | ----------- | ||||||
|  |  | ||||||
|  |  | ||||||
| 0.1.7 | **0.1.7** | ||||||
| ***** |  | ||||||
|  |  | ||||||
|   * Reversed static url and directory arguments to meet spec |   * Reversed static url and directory arguments to meet spec | ||||||
|  |  | ||||||
| 0.1.6 | **0.1.6** | ||||||
| ***** |  | ||||||
|  |  | ||||||
|   * Static files |   * Static files | ||||||
|   * Lazy Cookie Loading |   * Lazy Cookie Loading | ||||||
|  |  | ||||||
| 0.1.5 | **0.1.5** | ||||||
| ***** |  | ||||||
|  |  | ||||||
|   * Cookies |   * Cookies | ||||||
|   * Blueprint listeners and ordering |   * Blueprint listeners and ordering | ||||||
| @@ -1204,23 +1146,19 @@ Version 0.1 | |||||||
|   * Fix: Incomplete file reads on medium+ sized post requests |   * Fix: Incomplete file reads on medium+ sized post requests | ||||||
|   * Breaking: after_start and before_stop now pass sanic as their first argument |   * Breaking: after_start and before_stop now pass sanic as their first argument | ||||||
|  |  | ||||||
| 0.1.4 | **0.1.4** | ||||||
| ***** |  | ||||||
|  |  | ||||||
|   * Multiprocessing |   * Multiprocessing | ||||||
|  |  | ||||||
| 0.1.3 | **0.1.3** | ||||||
| ***** |  | ||||||
|  |  | ||||||
|   * Blueprint support |   * Blueprint support | ||||||
|   * Faster Response processing |   * Faster Response processing | ||||||
|  |  | ||||||
| 0.1.1 - 0.1.2 | **0.1.1 - 0.1.2** | ||||||
| ************* |  | ||||||
|  |  | ||||||
|   * Struggling to update pypi via CI |   * Struggling to update pypi via CI | ||||||
|  |  | ||||||
| 0.1.0 | **0.1.0** | ||||||
| ***** |  | ||||||
|  |  | ||||||
|   * Released to public |   * Released to public | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ Sanic | Build fast. Run fast. | |||||||
|     :stub-columns: 1 |     :stub-columns: 1 | ||||||
|  |  | ||||||
|     * - Build |     * - Build | ||||||
|       - | |Py39Test| |Py38Test| |Py37Test| |       - | |Py310Test| |Py39Test| |Py38Test| |Py37Test| | ||||||
|     * - Docs |     * - Docs | ||||||
|       - | |UserGuide| |Documentation| |       - | |UserGuide| |Documentation| | ||||||
|     * - Package |     * - Package | ||||||
| @@ -27,6 +27,8 @@ Sanic | Build fast. Run fast. | |||||||
|    :target: https://community.sanicframework.org/ |    :target: https://community.sanicframework.org/ | ||||||
| .. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord | .. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord | ||||||
|    :target: https://discord.gg/FARQzAEMAA |    :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 | .. |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 |    :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 | .. |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 | .. automodule:: sanic.views | ||||||
|     :members: |     :members: | ||||||
|     :show-inheritance: |     :show-inheritance: | ||||||
|  |  | ||||||
| sanic.websocket |  | ||||||
| --------------- |  | ||||||
|  |  | ||||||
| .. automodule:: sanic.websocket |  | ||||||
|     :members: |  | ||||||
|     :show-inheritance: |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| 📜 Changelog | 📜 Changelog | ||||||
| ============ | ============ | ||||||
|  |  | ||||||
| .. mdinclude:: ./releases/21.9.md | .. mdinclude:: ./releases/21/21.12.md | ||||||
|  | .. mdinclude:: ./releases/21/21.9.md | ||||||
| .. include:: ../../CHANGELOG.rst | .. 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 | ### 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 | - [#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 | from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| async def notify_server_started_after_five_seconds(): | async def notify_server_started_after_five_seconds(): | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from sanic import Sanic | |||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.middleware("request") | @app.middleware("request") | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ from sanic import Sanic | |||||||
| from sanic.response import json | from sanic.response import json | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| def check_request_for_authorization_status(request): | 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 | 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 | @bp.on_request | ||||||
|   | |||||||
| @@ -2,10 +2,10 @@ from sanic import Blueprint, Sanic | |||||||
| from sanic.response import file, json | from sanic.response import file, json | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
| blueprint = Blueprint("name", url_prefix="/my_blueprint") | blueprint = Blueprint("bp_example", url_prefix="/my_blueprint") | ||||||
| blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2") | blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2") | ||||||
| blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3") | blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3") | ||||||
|  |  | ||||||
|  |  | ||||||
| @blueprint.route("/foo") | @blueprint.route("/foo") | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ from asyncio import sleep | |||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__, strict_slashes=True) | app = Sanic("DelayedResponseApp", strict_slashes=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/") | @app.get("/") | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ from sanic import Sanic | |||||||
|  |  | ||||||
|  |  | ||||||
| handler = CustomHandler() | handler = CustomHandler() | ||||||
| app = Sanic(__name__, error_handler=handler) | app = Sanic("Example", error_handler=handler) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic, response | ||||||
| from sanic import response |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) |  | ||||||
|  | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
| @@ -9,5 +9,5 @@ async def test(request): | |||||||
|     return response.json({"test": True}) |     return response.json({"test": True}) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == "__main__": | ||||||
|     app.run(host="0.0.0.0", port=8000) |     app.run(host="0.0.0.0", port=8000) | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ from sanic import Sanic | |||||||
| from sanic.response import json | from sanic.response import json | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
| sem = None | 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 | @app.on_request | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ logdna = logging.getLogger(__name__) | |||||||
| logdna.setLevel(logging.INFO) | logdna.setLevel(logging.INFO) | ||||||
| logdna.addHandler(logdna_handler) | logdna.addHandler(logdna_handler) | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.middleware | @app.middleware | ||||||
|   | |||||||
| @@ -2,27 +2,29 @@ | |||||||
| Modify header or status in response | Modify header or status in response | ||||||
| """ | """ | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic, response | ||||||
| from sanic import response |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route('/') | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route("/") | ||||||
| def handle_request(request): | def handle_request(request): | ||||||
|     return response.json( |     return response.json( | ||||||
|         {'message': 'Hello world!'}, |         {"message": "Hello world!"}, | ||||||
|         headers={'X-Served-By': 'sanic'}, |         headers={"X-Served-By": "sanic"}, | ||||||
|         status=200 |         status=200, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route('/unauthorized') | @app.route("/unauthorized") | ||||||
| def handle_request(request): | def handle_request(request): | ||||||
|     return response.json( |     return response.json( | ||||||
|         {'message': 'You are not authorized'}, |         {"message": "You are not authorized"}, | ||||||
|         headers={'X-Served-By': 'sanic'}, |         headers={"X-Served-By": "sanic"}, | ||||||
|         status=404 |         status=404, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|     app.run(host="0.0.0.0", port=8000, debug=True) |     app.run(host="0.0.0.0", port=8000, debug=True) | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ def test_port(worker_id): | |||||||
|  |  | ||||||
| @pytest.fixture(scope="session") | @pytest.fixture(scope="session") | ||||||
| def app(): | def app(): | ||||||
|     app = Sanic() |     app = Sanic("Example") | ||||||
|  |  | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     async def index(request): |     async def index(request): | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ from sanic.handlers import ErrorHandler | |||||||
|  |  | ||||||
|  |  | ||||||
| class RaygunExceptionReporter(ErrorHandler): | class RaygunExceptionReporter(ErrorHandler): | ||||||
|  |  | ||||||
|     def __init__(self, raygun_api_key=None): |     def __init__(self, raygun_api_key=None): | ||||||
|         super().__init__() |         super().__init__() | ||||||
|         if raygun_api_key is None: |         if raygun_api_key is None: | ||||||
| @@ -22,16 +21,13 @@ class RaygunExceptionReporter(ErrorHandler): | |||||||
|  |  | ||||||
|  |  | ||||||
| raygun_error_reporter = RaygunExceptionReporter() | raygun_error_reporter = RaygunExceptionReporter() | ||||||
| app = Sanic(__name__, error_handler=raygun_error_reporter) | app = Sanic("Example", error_handler=raygun_error_reporter) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/raise") | @app.route("/raise") | ||||||
| async def test(request): | async def test(request): | ||||||
|     raise SanicException('You Broke It!') |     raise SanicException("You Broke It!") | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == "__main__": | ||||||
|     app.run( |     app.run(host="0.0.0.0", port=getenv("PORT", 8080)) | ||||||
|         host="0.0.0.0", |  | ||||||
|         port=getenv("PORT", 8080) |  | ||||||
|     ) |  | ||||||
|   | |||||||
| @@ -1,18 +1,18 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic, response | ||||||
| from sanic import response |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route('/') | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route("/") | ||||||
| def handle_request(request): | def handle_request(request): | ||||||
|     return response.redirect('/redirect') |     return response.redirect("/redirect") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route('/redirect') | @app.route("/redirect") | ||||||
| async def test(request): | async def test(request): | ||||||
|     return response.json({"Redirected": True}) |     return response.json({"Redirected": True}) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == "__main__": | ||||||
|     app.run(host="0.0.0.0", port=8000) |     app.run(host="0.0.0.0", port=8000) | ||||||
| @@ -1,65 +1,63 @@ | |||||||
| from sanic import Sanic | 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.blueprints import Blueprint | ||||||
| from sanic.response import stream, text | 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): | class SimpleView(HTTPMethodView): | ||||||
|  |  | ||||||
|     @stream_decorator |     @stream_decorator | ||||||
|     async def post(self, request): |     async def post(self, request): | ||||||
|         result = '' |         result = "" | ||||||
|         while True: |         while True: | ||||||
|             body = await request.stream.get() |             body = await request.stream.get() | ||||||
|             if body is None: |             if body is None: | ||||||
|                 break |                 break | ||||||
|             result += body.decode('utf-8') |             result += body.decode("utf-8") | ||||||
|         return text(result) |         return text(result) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.post('/stream', stream=True) | @app.post("/stream", stream=True) | ||||||
| async def handler(request): | async def handler(request): | ||||||
|     async def streaming(response): |     async def streaming(response): | ||||||
|         while True: |         while True: | ||||||
|             body = await request.stream.get() |             body = await request.stream.get() | ||||||
|             if body is None: |             if body is None: | ||||||
|                 break |                 break | ||||||
|             body = body.decode('utf-8').replace('1', 'A') |             body = body.decode("utf-8").replace("1", "A") | ||||||
|             await response.write(body) |             await response.write(body) | ||||||
|  |  | ||||||
|     return stream(streaming) |     return stream(streaming) | ||||||
|  |  | ||||||
|  |  | ||||||
| @bp.put('/bp_stream', stream=True) | @bp.put("/bp_stream", stream=True) | ||||||
| async def bp_handler(request): | async def bp_handler(request): | ||||||
|     result = '' |     result = "" | ||||||
|     while True: |     while True: | ||||||
|         body = await request.stream.get() |         body = await request.stream.get() | ||||||
|         if body is None: |         if body is None: | ||||||
|             break |             break | ||||||
|         result += body.decode('utf-8').replace('1', 'A') |         result += body.decode("utf-8").replace("1", "A") | ||||||
|     return text(result) |     return text(result) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def post_handler(request): | async def post_handler(request): | ||||||
|     result = '' |     result = "" | ||||||
|     while True: |     while True: | ||||||
|         body = await request.stream.get() |         body = await request.stream.get() | ||||||
|         if body is None: |         if body is None: | ||||||
|             break |             break | ||||||
|         result += body.decode('utf-8') |         result += body.decode("utf-8") | ||||||
|     return text(result) |     return text(result) | ||||||
|  |  | ||||||
|  |  | ||||||
| app.blueprint(bp) | app.blueprint(bp) | ||||||
| app.add_route(SimpleView.as_view(), '/method_view') | app.add_route(SimpleView.as_view(), "/method_view") | ||||||
| view = CompositionView() |  | ||||||
| view.add(['POST'], post_handler, stream=True) |  | ||||||
| app.add_route(view, '/composition_view') |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == "__main__": | ||||||
|     app.run(host='0.0.0.0', port=8000) |     app.run(host="0.0.0.0", port=8000) | ||||||
|   | |||||||
| @@ -1,21 +1,23 @@ | |||||||
| import asyncio | import asyncio | ||||||
| from sanic import Sanic |  | ||||||
| from sanic import response | from sanic import Sanic, response | ||||||
| from sanic.config import Config | from sanic.config import Config | ||||||
| from sanic.exceptions import RequestTimeout | from sanic.exceptions import RequestTimeout | ||||||
|  |  | ||||||
|  |  | ||||||
| Config.REQUEST_TIMEOUT = 1 | Config.REQUEST_TIMEOUT = 1 | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route('/') | @app.route("/") | ||||||
| async def test(request): | async def test(request): | ||||||
|     await asyncio.sleep(3) |     await asyncio.sleep(3) | ||||||
|     return response.text('Hello, world!') |     return response.text("Hello, world!") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.exception(RequestTimeout) | @app.exception(RequestTimeout) | ||||||
| def timeout(request, exception): | 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 | import rollbar | ||||||
|  |  | ||||||
| from sanic.handlers import ErrorHandler |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.exceptions import SanicException | from sanic.exceptions import SanicException | ||||||
| from os import getenv | from sanic.handlers import ErrorHandler | ||||||
|  |  | ||||||
|  |  | ||||||
| rollbar.init(getenv("ROLLBAR_API_KEY")) | rollbar.init(getenv("ROLLBAR_API_KEY")) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RollbarExceptionHandler(ErrorHandler): | class RollbarExceptionHandler(ErrorHandler): | ||||||
|  |  | ||||||
|     def default(self, request, exception): |     def default(self, request, exception): | ||||||
|         rollbar.report_message(str(exception)) |         rollbar.report_message(str(exception)) | ||||||
|         return super().default(request, exception) |         return super().default(request, exception) | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__, error_handler=RollbarExceptionHandler()) | app = Sanic("Example", error_handler=RollbarExceptionHandler()) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/raise") | @app.route("/raise") | ||||||
| @@ -24,7 +25,4 @@ def create_error(request): | |||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     app.run( |     app.run(host="0.0.0.0", port=getenv("PORT", 8080)) | ||||||
|         host="0.0.0.0", |  | ||||||
|         port=getenv("PORT", 8080) |  | ||||||
|     ) |  | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ from pathlib import Path | |||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/text") | @app.route("/text") | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import uvloop | |||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ from sanic import Sanic, response | |||||||
| from sanic.server import AsyncioServer | from sanic.server import AsyncioServer | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.before_server_start | @app.before_server_start | ||||||
|   | |||||||
| @@ -6,20 +6,19 @@ from sentry_sdk.integrations.sanic import SanicIntegration | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic.response import json | ||||||
|  |  | ||||||
|  |  | ||||||
| sentry_init( | sentry_init( | ||||||
|     dsn=getenv("SENTRY_DSN"), |     dsn=getenv("SENTRY_DSN"), | ||||||
|     integrations=[SanicIntegration()], |     integrations=[SanicIntegration()], | ||||||
| ) | ) | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| # noinspection PyUnusedLocal | # noinspection PyUnusedLocal | ||||||
| @app.route("/working") | @app.route("/working") | ||||||
| async def working_path(request): | async def working_path(request): | ||||||
|     return json({ |     return json({"response": "Working API Response"}) | ||||||
|         "response": "Working API Response" |  | ||||||
|     }) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # noinspection PyUnusedLocal | # noinspection PyUnusedLocal | ||||||
| @@ -28,8 +27,5 @@ async def raise_error(request): | |||||||
|     raise Exception("Testing Sentry Integration") |     raise Exception("Testing Sentry Integration") | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == "__main__": | ||||||
|     app.run( |     app.run(host="0.0.0.0", port=getenv("PORT", 8080)) | ||||||
|         host="0.0.0.0", |  | ||||||
|         port=getenv("PORT", 8080) |  | ||||||
|     ) |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
| app.static("/", "./static") | app.static("/", "./static") | ||||||
|   | |||||||
| @@ -1,13 +1,14 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic import response as res | from sanic import response as res | ||||||
|  |  | ||||||
| app = Sanic(__name__) |  | ||||||
|  | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
| async def test(req): | 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) |     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 | from sanic.log import logger as log | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import socket | |||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/test") | @app.route("/test") | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @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/question | ||||||
| # curl -H "Host: bp.example.com" localhost:8000/answer | # curl -H "Host: bp.example.com" localhost:8000/answer | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
| bp = Blueprint("bp", host="bp.example.com") | bp = Blueprint("bp", host="bp.example.com") | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ from sanic import Sanic | |||||||
| from sanic.response import redirect | from sanic.response import redirect | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic("Example") | ||||||
|  |  | ||||||
|  |  | ||||||
| app.static("index.html", "websocket.html") | app.static("index.html", "websocket.html") | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "21.12.0dev" | __version__ = "21.12.1" | ||||||
|   | |||||||
							
								
								
									
										523
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										523
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -1,5 +1,6 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
| import logging | import logging | ||||||
| import logging.config | import logging.config | ||||||
| import os | import os | ||||||
| @@ -11,6 +12,7 @@ from asyncio import ( | |||||||
|     AbstractEventLoop, |     AbstractEventLoop, | ||||||
|     CancelledError, |     CancelledError, | ||||||
|     Protocol, |     Protocol, | ||||||
|  |     Task, | ||||||
|     ensure_future, |     ensure_future, | ||||||
|     get_event_loop, |     get_event_loop, | ||||||
|     wait_for, |     wait_for, | ||||||
| @@ -26,6 +28,7 @@ from ssl import SSLContext | |||||||
| from traceback import format_exc | from traceback import format_exc | ||||||
| from types import SimpleNamespace | from types import SimpleNamespace | ||||||
| from typing import ( | from typing import ( | ||||||
|  |     TYPE_CHECKING, | ||||||
|     Any, |     Any, | ||||||
|     AnyStr, |     AnyStr, | ||||||
|     Awaitable, |     Awaitable, | ||||||
| @@ -39,10 +42,11 @@ from typing import ( | |||||||
|     Set, |     Set, | ||||||
|     Tuple, |     Tuple, | ||||||
|     Type, |     Type, | ||||||
|  |     TypeVar, | ||||||
|     Union, |     Union, | ||||||
| ) | ) | ||||||
| from urllib.parse import urlencode, urlunparse | from urllib.parse import urlencode, urlunparse | ||||||
| from warnings import filterwarnings, warn | from warnings import filterwarnings | ||||||
|  |  | ||||||
| from sanic_routing.exceptions import (  # type: ignore | from sanic_routing.exceptions import (  # type: ignore | ||||||
|     FinalizationError, |     FinalizationError, | ||||||
| @@ -51,11 +55,12 @@ from sanic_routing.exceptions import (  # type: ignore | |||||||
| from sanic_routing.route import Route  # type: ignore | from sanic_routing.route import Route  # type: ignore | ||||||
|  |  | ||||||
| from sanic import reloader_helpers | from sanic import reloader_helpers | ||||||
|  | from sanic.application.ext import setup_ext | ||||||
| from sanic.application.logo import get_logo | from sanic.application.logo import get_logo | ||||||
| from sanic.application.motd import MOTD | from sanic.application.motd import MOTD | ||||||
| from sanic.application.state import ApplicationState, Mode | from sanic.application.state import ApplicationState, Mode | ||||||
| from sanic.asgi import ASGIApp | from sanic.asgi import ASGIApp | ||||||
| from sanic.base import BaseSanic | from sanic.base.root import BaseSanic | ||||||
| from sanic.blueprint_group import BlueprintGroup | from sanic.blueprint_group import BlueprintGroup | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
| from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support | from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support | ||||||
| @@ -67,9 +72,15 @@ from sanic.exceptions import ( | |||||||
|     URLBuildError, |     URLBuildError, | ||||||
| ) | ) | ||||||
| from sanic.handlers import ErrorHandler | from sanic.handlers import ErrorHandler | ||||||
| from sanic.helpers import Default, _default | from sanic.helpers import _default | ||||||
| from sanic.http import Stage | 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.mixins.listeners import ListenerEvent | ||||||
| from sanic.models.futures import ( | from sanic.models.futures import ( | ||||||
|     FutureException, |     FutureException, | ||||||
| @@ -83,11 +94,11 @@ from sanic.models.futures import ( | |||||||
| from sanic.models.handler_types import ListenerType, MiddlewareType | from sanic.models.handler_types import ListenerType, MiddlewareType | ||||||
| from sanic.models.handler_types import Sanic as SanicVar | from sanic.models.handler_types import Sanic as SanicVar | ||||||
| from sanic.request import Request | 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.router import Router | ||||||
| from sanic.server import AsyncioServer, HttpProtocol | from sanic.server import AsyncioServer, HttpProtocol | ||||||
| from sanic.server import Signal as ServerSignal | 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.protocols.websocket_protocol import WebSocketProtocol | ||||||
| from sanic.server.websockets.impl import ConnectionClosed | from sanic.server.websockets.impl import ConnectionClosed | ||||||
| from sanic.signals import Signal, SignalRouter | from sanic.signals import Signal, SignalRouter | ||||||
| @@ -95,11 +106,21 @@ from sanic.tls import process_to_context | |||||||
| from sanic.touchup import TouchUp, TouchUpMeta | 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: | if OS_IS_WINDOWS: | ||||||
|     enable_windows_color_support() |     enable_windows_color_support() | ||||||
|  |  | ||||||
| filterwarnings("once", category=DeprecationWarning) | filterwarnings("once", category=DeprecationWarning) | ||||||
|  |  | ||||||
|  | SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext") | ||||||
|  |  | ||||||
|  |  | ||||||
| class Sanic(BaseSanic, metaclass=TouchUpMeta): | class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||||
|     """ |     """ | ||||||
| @@ -112,12 +133,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         "_run_response_middleware", |         "_run_response_middleware", | ||||||
|         "_run_request_middleware", |         "_run_request_middleware", | ||||||
|     ) |     ) | ||||||
|     __fake_slots__ = ( |     __slots__ = ( | ||||||
|         "_app_registry", |  | ||||||
|         "_asgi_app", |         "_asgi_app", | ||||||
|         "_asgi_client", |         "_asgi_client", | ||||||
|         "_blueprint_order", |         "_blueprint_order", | ||||||
|         "_delayed_tasks", |         "_delayed_tasks", | ||||||
|  |         "_ext", | ||||||
|         "_future_exceptions", |         "_future_exceptions", | ||||||
|         "_future_listeners", |         "_future_listeners", | ||||||
|         "_future_middleware", |         "_future_middleware", | ||||||
| @@ -126,20 +147,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         "_future_signals", |         "_future_signals", | ||||||
|         "_future_statics", |         "_future_statics", | ||||||
|         "_state", |         "_state", | ||||||
|  |         "_task_registry", | ||||||
|         "_test_client", |         "_test_client", | ||||||
|         "_test_manager", |         "_test_manager", | ||||||
|         "asgi", |  | ||||||
|         "auto_reload", |  | ||||||
|         "auto_reload", |  | ||||||
|         "blueprints", |         "blueprints", | ||||||
|         "config", |         "config", | ||||||
|         "configure_logging", |         "configure_logging", | ||||||
|         "ctx", |         "ctx", | ||||||
|         "debug", |  | ||||||
|         "error_handler", |         "error_handler", | ||||||
|         "go_fast", |         "go_fast", | ||||||
|         "is_running", |  | ||||||
|         "is_stopping", |  | ||||||
|         "listeners", |         "listeners", | ||||||
|         "name", |         "name", | ||||||
|         "named_request_middleware", |         "named_request_middleware", | ||||||
| @@ -151,12 +167,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         "signal_router", |         "signal_router", | ||||||
|         "sock", |         "sock", | ||||||
|         "strict_slashes", |         "strict_slashes", | ||||||
|         "test_mode", |  | ||||||
|         "websocket_enabled", |         "websocket_enabled", | ||||||
|         "websocket_tasks", |         "websocket_tasks", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     _app_registry: Dict[str, "Sanic"] = {} |     _app_registry: Dict[str, "Sanic"] = {} | ||||||
|  |     _uvloop_setting = None  # TODO: Remove in v22.6 | ||||||
|     test_mode = False |     test_mode = False | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
| @@ -167,7 +183,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         router: Optional[Router] = None, |         router: Optional[Router] = None, | ||||||
|         signal_router: Optional[SignalRouter] = None, |         signal_router: Optional[SignalRouter] = None, | ||||||
|         error_handler: Optional[ErrorHandler] = None, |         error_handler: Optional[ErrorHandler] = None, | ||||||
|         load_env: Union[bool, str] = True, |  | ||||||
|         env_prefix: Optional[str] = SANIC_PREFIX, |         env_prefix: Optional[str] = SANIC_PREFIX, | ||||||
|         request_class: Optional[Type[Request]] = None, |         request_class: Optional[Type[Request]] = None, | ||||||
|         strict_slashes: bool = False, |         strict_slashes: bool = False, | ||||||
| @@ -183,25 +198,27 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|             dict_config = log_config or LOGGING_CONFIG_DEFAULTS |             dict_config = log_config or LOGGING_CONFIG_DEFAULTS | ||||||
|             logging.config.dictConfig(dict_config)  # type: ignore |             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( |             raise SanicException( | ||||||
|                 "When instantiating Sanic with config, you cannot also pass " |                 "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._asgi_client: Any = None | ||||||
|         self._test_client: Any = None |  | ||||||
|         self._test_manager: Any = None |  | ||||||
|         self._blueprint_order: List[Blueprint] = [] |         self._blueprint_order: List[Blueprint] = [] | ||||||
|         self._delayed_tasks: List[str] = [] |         self._delayed_tasks: List[str] = [] | ||||||
|         self._future_registry: FutureRegistry = FutureRegistry() |         self._future_registry: FutureRegistry = FutureRegistry() | ||||||
|         self._state: ApplicationState = ApplicationState(app=self) |         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.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.configure_logging: bool = configure_logging | ||||||
|         self.ctx: Any = ctx or SimpleNamespace() |         self.ctx: Any = ctx or SimpleNamespace() | ||||||
|         self.debug = False |         self.debug = False | ||||||
| @@ -223,6 +240,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         self.go_fast = self.run |         self.go_fast = self.run | ||||||
|  |  | ||||||
|         if register is not None: |         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 |             self.config.REGISTER = register | ||||||
|         if self.config.REGISTER: |         if self.config.REGISTER: | ||||||
|             self.__class__.register_app(self) |             self.__class__.register_app(self) | ||||||
| @@ -253,32 +276,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|     # Registration |     # 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( |     def register_listener( | ||||||
|         self, listener: ListenerType[SanicVar], event: str |         self, listener: ListenerType[SanicVar], event: str | ||||||
|     ) -> ListenerType[SanicVar]: |     ) -> ListenerType[SanicVar]: | ||||||
| @@ -403,12 +400,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|             websocket_handler.is_websocket = True  # type: ignore |             websocket_handler.is_websocket = True  # type: ignore | ||||||
|             params["handler"] = websocket_handler |             params["handler"] = websocket_handler | ||||||
|  |  | ||||||
|  |         ctx = params.pop("route_context") | ||||||
|  |  | ||||||
|         routes = self.router.add(**params) |         routes = self.router.add(**params) | ||||||
|         if isinstance(routes, Route): |         if isinstance(routes, Route): | ||||||
|             routes = [routes] |             routes = [routes] | ||||||
|  |  | ||||||
|         for r in routes: |         for r in routes: | ||||||
|             r.ctx.websocket = websocket |             r.ctx.websocket = websocket | ||||||
|             r.ctx.static = params.get("static", False) |             r.ctx.static = params.get("static", False) | ||||||
|  |             r.ctx.__dict__.update(ctx) | ||||||
|  |  | ||||||
|         return routes |         return routes | ||||||
|  |  | ||||||
| @@ -537,30 +538,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|             blueprint.strict_slashes = self.strict_slashes |             blueprint.strict_slashes = self.strict_slashes | ||||||
|         blueprint.register(self, options) |         blueprint.register(self, options) | ||||||
|  |  | ||||||
|     def _register_lazy_blueprints(self): |  | ||||||
|         registry = {**Blueprint.__pre_registry__} |  | ||||||
|         if _default in Blueprint.__pre_registry__: |  | ||||||
|             if len(Sanic._app_registry) > 1: |  | ||||||
|                 raise SanicException( |  | ||||||
|                     "Ambiguous Blueprint pre-registration detected. When " |  | ||||||
|                     "there are multiple Sanic application instances, all " |  | ||||||
|                     "pre-registrations must use an application name." |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|             if self.name in registry and _default in registry: |  | ||||||
|                 registry[_default].extend(registry.pop(self.name)) |  | ||||||
|  |  | ||||||
|             registry = { |  | ||||||
|                 self.name if k is _default else k: v |  | ||||||
|                 for k, v in registry.items() |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|         for name, registrants in registry.items(): |  | ||||||
|             for reg_info in registrants: |  | ||||||
|                 blueprint = reg_info.pop("bp") |  | ||||||
|                 if name == self.name and blueprint.name not in self.blueprints: |  | ||||||
|                     self.blueprint(blueprint, **reg_info) |  | ||||||
|  |  | ||||||
|     def url_for(self, view_name: str, **kwargs): |     def url_for(self, view_name: str, **kwargs): | ||||||
|         """Build a URL based on a view name and the values provided. |         """Build a URL based on a view name and the values provided. | ||||||
|  |  | ||||||
| @@ -778,7 +755,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                 exception, request.name if request else None |                 exception, request.name if request else None | ||||||
|             ) |             ) | ||||||
|             if handler: |             if handler: | ||||||
|                 warn( |                 deprecation( | ||||||
|                     "An error occurred while handling the request after at " |                     "An error occurred while handling the request after at " | ||||||
|                     "least some part of the response was sent to the client. " |                     "least some part of the response was sent to the client. " | ||||||
|                     "Therefore, the response from your custom exception " |                     "Therefore, the response from your custom exception " | ||||||
| @@ -793,7 +770,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                     "For further information, please see the docs: " |                     "For further information, please see the docs: " | ||||||
|                     "https://sanicframework.org/en/guide/advanced/" |                     "https://sanicframework.org/en/guide/advanced/" | ||||||
|                     "signals.html", |                     "signals.html", | ||||||
|                     DeprecationWarning, |                     22.6, | ||||||
|                 ) |                 ) | ||||||
|                 try: |                 try: | ||||||
|                     response = self.error_handler.response(request, exception) |                     response = self.error_handler.response(request, exception) | ||||||
| @@ -846,6 +823,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         else: |         else: | ||||||
|             if request.stream: |             if request.stream: | ||||||
|                 response = request.stream.response |                 response = request.stream.response | ||||||
|  |  | ||||||
|  |         # Marked for cleanup and DRY with handle_request/handle_exception | ||||||
|  |         # when ResponseStream is no longer supporder | ||||||
|         if isinstance(response, BaseHTTPResponse): |         if isinstance(response, BaseHTTPResponse): | ||||||
|             await self.dispatch( |             await self.dispatch( | ||||||
|                 "http.lifecycle.response", |                 "http.lifecycle.response", | ||||||
| @@ -856,6 +836,17 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|             await response.send(end_stream=True) |             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: |         else: | ||||||
|             raise ServerError( |             raise ServerError( | ||||||
|                 f"Invalid response type {response!r} (need HTTPResponse)" |                 f"Invalid response type {response!r} (need HTTPResponse)" | ||||||
| @@ -959,7 +950,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|             elif not hasattr(handler, "is_websocket"): |             elif not hasattr(handler, "is_websocket"): | ||||||
|                 response = request.stream.response  # type: ignore |                 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): |             if isinstance(response, BaseHTTPResponse): | ||||||
|                 await self.dispatch( |                 await self.dispatch( | ||||||
|                     "http.lifecycle.response", |                     "http.lifecycle.response", | ||||||
| @@ -970,6 +962,17 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                     }, |                     }, | ||||||
|                 ) |                 ) | ||||||
|                 await response.send(end_stream=True) |                 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: |             else: | ||||||
|                 if not hasattr(handler, "is_websocket"): |                 if not hasattr(handler, "is_websocket"): | ||||||
|                     raise ServerError( |                     raise ServerError( | ||||||
| @@ -1001,10 +1004,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         cancelled = False |         cancelled = False | ||||||
|         try: |         try: | ||||||
|             await fut |             await fut | ||||||
|         except Exception as e: |  | ||||||
|             self.error_handler.log(request, e) |  | ||||||
|         except (CancelledError, ConnectionClosed): |         except (CancelledError, ConnectionClosed): | ||||||
|             cancelled = True |             cancelled = True | ||||||
|  |         except Exception as e: | ||||||
|  |             self.error_handler.log(request, e) | ||||||
|         finally: |         finally: | ||||||
|             self.websocket_tasks.remove(fut) |             self.websocket_tasks.remove(fut) | ||||||
|             if cancelled: |             if cancelled: | ||||||
| @@ -1183,6 +1186,11 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|             register_sys_signals=register_sys_signals, |             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: |         try: | ||||||
|             self.is_running = True |             self.is_running = True | ||||||
|             self.is_stopping = False |             self.is_stopping = False | ||||||
| @@ -1210,6 +1218,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         This kills the Sanic |         This kills the Sanic | ||||||
|         """ |         """ | ||||||
|         if not self.is_stopping: |         if not self.is_stopping: | ||||||
|  |             self.shutdown_tasks(timeout=0) | ||||||
|             self.is_stopping = True |             self.is_stopping = True | ||||||
|             get_event_loop().stop() |             get_event_loop().stop() | ||||||
|  |  | ||||||
| @@ -1279,12 +1288,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                 WebSocketProtocol if self.websocket_enabled else HttpProtocol |                 WebSocketProtocol if self.websocket_enabled else HttpProtocol | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # if access_log is passed explicitly change config.ACCESS_LOG |         # Set explicitly passed configuration values | ||||||
|         if access_log is not None: |         for attribute, value in { | ||||||
|             self.config.ACCESS_LOG = access_log |             "ACCESS_LOG": access_log, | ||||||
|  |             "NOISY_EXCEPTIONS": noisy_exceptions, | ||||||
|         if noisy_exceptions is not None: |         }.items(): | ||||||
|             self.config.NOISY_EXCEPTIONS = noisy_exceptions |             if value is not None: | ||||||
|  |                 setattr(self.config, attribute, value) | ||||||
|  |  | ||||||
|         server_settings = self._helper( |         server_settings = self._helper( | ||||||
|             host=host, |             host=host, | ||||||
| @@ -1299,6 +1309,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|             run_async=return_asyncio_server, |             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_start = server_settings.pop("main_start", None) | ||||||
|         main_stop = server_settings.pop("main_stop", None) |         main_stop = server_settings.pop("main_stop", None) | ||||||
|         if main_start or main_stop: |         if main_start or main_stop: | ||||||
| @@ -1417,26 +1435,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                 "#proxy-configuration" |                 "#proxy-configuration" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |         ssl = process_to_context(ssl) | ||||||
|  |  | ||||||
|         self.debug = debug |         self.debug = debug | ||||||
|         self.state.host = host |         self.state.host = host | ||||||
|         self.state.port = port |         self.state.port = port | ||||||
|         self.state.workers = workers |         self.state.workers = workers | ||||||
|  |         self.state.ssl = ssl | ||||||
|         # Serve |         self.state.unix = unix | ||||||
|         serve_location = "" |         self.state.sock = sock | ||||||
|         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) |  | ||||||
|  |  | ||||||
|         server_settings = { |         server_settings = { | ||||||
|             "protocol": protocol, |             "protocol": protocol, | ||||||
| @@ -1452,7 +1459,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|             "backlog": backlog, |             "backlog": backlog, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         self.motd(serve_location) |         self.motd(self.serve_location) | ||||||
|  |  | ||||||
|         if sys.stdout.isatty() and not self.state.is_debug: |         if sys.stdout.isatty() and not self.state.is_debug: | ||||||
|             error_logger.warning( |             error_logger.warning( | ||||||
| @@ -1478,12 +1485,55 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|  |  | ||||||
|         return server_settings |         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): |     def _build_endpoint_name(self, *parts): | ||||||
|         parts = [self.name, *parts] |         parts = [self.name, *parts] | ||||||
|         return ".".join(parts) |         return ".".join(parts) | ||||||
|  |  | ||||||
|     @classmethod |     @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): |         if callable(task): | ||||||
|             try: |             try: | ||||||
|                 task = task(app) |                 task = task(app) | ||||||
| @@ -1493,14 +1543,31 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         return task |         return task | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _loop_add_task(cls, task, app, loop): |     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) |             prepped = cls._prep_task(task, app, loop) | ||||||
|         loop.create_task(prepped) |             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 |         if name and register and sys.version_info > (3, 7): | ||||||
|     def _cancel_websocket_tasks(cls, app, loop): |             app._task_registry[name] = task | ||||||
|         for task in app.websocket_tasks: |  | ||||||
|             task.cancel() |         return task | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     async def dispatch_delayed_tasks(app, loop): |     async def dispatch_delayed_tasks(app, loop): | ||||||
| @@ -1513,13 +1580,142 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         prepped = app._prep_task(task, app, loop) |         prepped = app._prep_task(task, app, loop) | ||||||
|         await prepped |         await prepped | ||||||
|  |  | ||||||
|     @staticmethod |     def add_task( | ||||||
|     async def _listener( |         self, | ||||||
|         app: Sanic, loop: AbstractEventLoop, listener: ListenerType |         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 sys.version_info < (3, 8): | ||||||
|         if maybe_coro and isawaitable(maybe_coro): |             error_logger.warning( | ||||||
|             await maybe_coro |                 "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 |     # ASGI | ||||||
| @@ -1532,6 +1728,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         details: https://asgi.readthedocs.io/en/latest |         details: https://asgi.readthedocs.io/en/latest | ||||||
|         """ |         """ | ||||||
|         self.asgi = True |         self.asgi = True | ||||||
|  |         if scope["type"] == "lifespan": | ||||||
|             self.motd("") |             self.motd("") | ||||||
|         self._asgi_app = await ASGIApp.create(self, scope, receive, send) |         self._asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||||
|         asgi_app = self._asgi_app |         asgi_app = self._asgi_app | ||||||
| @@ -1637,11 +1834,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                 display["auto-reload"] = reload_display |                 display["auto-reload"] = reload_display | ||||||
|  |  | ||||||
|             packages = [] |             packages = [] | ||||||
|             for package_name, module_name in { |             for package_name in SANIC_PACKAGES: | ||||||
|                 "sanic-routing": "sanic_routing", |                 module_name = package_name.replace("-", "_") | ||||||
|                 "sanic-testing": "sanic_testing", |  | ||||||
|                 "sanic-ext": "sanic_ext", |  | ||||||
|             }.items(): |  | ||||||
|                 try: |                 try: | ||||||
|                     module = import_module(module_name) |                     module = import_module(module_name) | ||||||
|                     packages.append(f"{package_name}=={module.__version__}") |                     packages.append(f"{package_name}=={module.__version__}") | ||||||
| @@ -1661,6 +1855,41 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|             ) |             ) | ||||||
|             MOTD.output(logo, serve_location, display, extra) |             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 |     # Class methods | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
| @@ -1702,32 +1931,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                 return cls(name) |                 return cls(name) | ||||||
|             raise SanicException(f'Sanic app name "{name}" not found.') |             raise SanicException(f'Sanic app name "{name}" not found.') | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def lazy( |  | ||||||
|         cls, |  | ||||||
|         app_name: Union[str, Default] = _default, |  | ||||||
|         *, |  | ||||||
|         name: str = None, |  | ||||||
|         url_prefix: Optional[str] = None, |  | ||||||
|         host: Optional[Union[List[str], str]] = None, |  | ||||||
|         version: Optional[Union[int, str, float]] = None, |  | ||||||
|         strict_slashes: Optional[bool] = None, |  | ||||||
|         version_prefix: str = "/v", |  | ||||||
|     ) -> Blueprint: |  | ||||||
|         if not name: |  | ||||||
|             flat = [1 for x in Blueprint.__pre_registry__.values() for _ in x] |  | ||||||
|             name = f"bp{len(flat)}" |  | ||||||
|         bp = Blueprint( |  | ||||||
|             name=name, |  | ||||||
|             url_prefix=url_prefix, |  | ||||||
|             host=host, |  | ||||||
|             version=version, |  | ||||||
|             strict_slashes=strict_slashes, |  | ||||||
|             version_prefix=version_prefix, |  | ||||||
|         ) |  | ||||||
|         bp.pre_register(app_name) |  | ||||||
|         return bp |  | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|     # Lifecycle |     # Lifecycle | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
| @@ -1748,13 +1951,33 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|  |  | ||||||
|     async def _startup(self): |     async def _startup(self): | ||||||
|         self._future_registry.clear() |         self._future_registry.clear() | ||||||
|         self._register_lazy_blueprints() |  | ||||||
|  |         # Startup Sanic Extensions | ||||||
|  |         if not hasattr(self, "_ext"): | ||||||
|  |             setup_ext(self) | ||||||
|  |         if hasattr(self, "_ext"): | ||||||
|  |             self.ext._display() | ||||||
|  |  | ||||||
|  |         # Setup routers | ||||||
|         self.signalize() |         self.signalize() | ||||||
|         self.finalize() |         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) |         TouchUp.run(self) | ||||||
|  |  | ||||||
|         self.state.is_started = True |         self.state.is_started = True | ||||||
|  |  | ||||||
|     async def _server_event( |     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): | class MOTDBasic(MOTD): | ||||||
|     def __init__(self, *args, **kwargs) -> None: |  | ||||||
|         super().__init__(*args, **kwargs) |  | ||||||
|  |  | ||||||
|     def display(self): |     def display(self): | ||||||
|         if self.logo: |         if self.logo: | ||||||
|             logger.debug(self.logo) |             logger.debug(self.logo) | ||||||
|   | |||||||
| @@ -5,7 +5,9 @@ import logging | |||||||
| from dataclasses import dataclass, field | from dataclasses import dataclass, field | ||||||
| from enum import Enum, auto | from enum import Enum, auto | ||||||
| from pathlib import Path | 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 | from sanic.log import logger | ||||||
|  |  | ||||||
| @@ -37,8 +39,11 @@ class ApplicationState: | |||||||
|     coffee: bool = field(default=False) |     coffee: bool = field(default=False) | ||||||
|     fast: bool = field(default=False) |     fast: bool = field(default=False) | ||||||
|     host: str = field(default="") |     host: str = field(default="") | ||||||
|     mode: Mode = field(default=Mode.PRODUCTION) |  | ||||||
|     port: int = field(default=0) |     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) |     reload_dirs: Set[Path] = field(default_factory=set) | ||||||
|     server: Server = field(default=Server.SANIC) |     server: Server = field(default=Server.SANIC) | ||||||
|     is_running: bool = field(default=False) |     is_running: bool = field(default=False) | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import sanic.app  # noqa | |||||||
|  |  | ||||||
| from sanic.compat import Header | from sanic.compat import Header | ||||||
| from sanic.exceptions import ServerError | from sanic.exceptions import ServerError | ||||||
|  | from sanic.helpers import _default | ||||||
| from sanic.http import Stage | from sanic.http import Stage | ||||||
| from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport | from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport | ||||||
| from sanic.request import Request | 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", "before") | ||||||
|         await self.asgi_app.sanic_app._server_event("init", "after") |         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: |     async def shutdown(self) -> None: | ||||||
|         """ |         """ | ||||||
|         Gather the listeners to fire on server stop. |         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 | import re | ||||||
| 
 | 
 | ||||||
| from typing import Any, Tuple | from typing import Any | ||||||
| from warnings import warn |  | ||||||
| 
 | 
 | ||||||
|  | from sanic.base.meta import SanicMeta | ||||||
| from sanic.exceptions import SanicException | from sanic.exceptions import SanicException | ||||||
| from sanic.mixins.exceptions import ExceptionMixin | from sanic.mixins.exceptions import ExceptionMixin | ||||||
| from sanic.mixins.listeners import ListenerMixin | from sanic.mixins.listeners import ListenerMixin | ||||||
| @@ -20,8 +20,9 @@ class BaseSanic( | |||||||
|     ListenerMixin, |     ListenerMixin, | ||||||
|     ExceptionMixin, |     ExceptionMixin, | ||||||
|     SignalMixin, |     SignalMixin, | ||||||
|  |     metaclass=SanicMeta, | ||||||
| ): | ): | ||||||
|     __fake_slots__: Tuple[str, ...] |     __slots__ = ("name",) | ||||||
| 
 | 
 | ||||||
|     def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None: |     def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None: | ||||||
|         class_name = self.__class__.__name__ |         class_name = self.__class__.__name__ | ||||||
| @@ -33,11 +34,10 @@ class BaseSanic( | |||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         if not VALID_NAME.match(name): |         if not VALID_NAME.match(name): | ||||||
|             warn( |             raise SanicException( | ||||||
|                 f"{class_name} instance named '{name}' uses a format that is" |                 f"{class_name} instance named '{name}' uses an invalid " | ||||||
|                 f"deprecated. Starting in version 21.12, {class_name} objects " |                 "format. Names must begin with a character and may only " | ||||||
|                 "must be named only using alphanumeric characters, _, or -.", |                 "contain alphanumeric characters, _, or -." | ||||||
|                 DeprecationWarning, |  | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         self.name = name |         self.name = name | ||||||
| @@ -52,15 +52,12 @@ class BaseSanic( | |||||||
|         return f'{self.__class__.__name__}(name="{self.name}")' |         return f'{self.__class__.__name__}(name="{self.name}")' | ||||||
| 
 | 
 | ||||||
|     def __setattr__(self, name: str, value: Any) -> None: |     def __setattr__(self, name: str, value: Any) -> None: | ||||||
|         # This is a temporary compat layer so we can raise a warning until |         try: | ||||||
|         # 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( |  | ||||||
|                 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 " |  | ||||||
|                 f"instance.ctx.{name} instead.", |  | ||||||
|                 DeprecationWarning, |  | ||||||
|             ) |  | ||||||
|             super().__setattr__(name, value) |             super().__setattr__(name, value) | ||||||
|  |         except AttributeError as e: | ||||||
|  |             raise AttributeError( | ||||||
|  |                 f"Setting variables on {self.__class__.__name__} instances is " | ||||||
|  |                 "not allowed. You should change your " | ||||||
|  |                 f"{self.__class__.__name__} instance to use " | ||||||
|  |                 f"instance.ctx.{name} instead.", | ||||||
|  |             ) from e | ||||||
| @@ -5,7 +5,7 @@ from functools import partial | |||||||
| from typing import TYPE_CHECKING, List, Optional, Union | from typing import TYPE_CHECKING, List, Optional, Union | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING:  # no cov | ||||||
|     from sanic.blueprints import Blueprint |     from sanic.blueprints import Blueprint | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ from typing import ( | |||||||
| from sanic_routing.exceptions import NotFound  # type: ignore | from sanic_routing.exceptions import NotFound  # type: ignore | ||||||
| from sanic_routing.route import Route  # 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.blueprint_group import BlueprintGroup | ||||||
| from sanic.exceptions import SanicException | from sanic.exceptions import SanicException | ||||||
| from sanic.helpers import Default, _default | from sanic.helpers import Default, _default | ||||||
| @@ -36,8 +36,8 @@ from sanic.models.handler_types import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING:  # no cov | ||||||
|     from sanic import Sanic  # noqa |     from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
| def lazy(func, as_decorator=True): | def lazy(func, as_decorator=True): | ||||||
| @@ -85,7 +85,7 @@ class Blueprint(BaseSanic): | |||||||
|         trailing */* |         trailing */* | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     __fake_slots__ = ( |     __slots__ = ( | ||||||
|         "_apps", |         "_apps", | ||||||
|         "_future_routes", |         "_future_routes", | ||||||
|         "_future_statics", |         "_future_statics", | ||||||
| @@ -98,7 +98,6 @@ class Blueprint(BaseSanic): | |||||||
|         "host", |         "host", | ||||||
|         "listeners", |         "listeners", | ||||||
|         "middlewares", |         "middlewares", | ||||||
|         "name", |  | ||||||
|         "routes", |         "routes", | ||||||
|         "statics", |         "statics", | ||||||
|         "strict_slashes", |         "strict_slashes", | ||||||
| @@ -107,7 +106,6 @@ class Blueprint(BaseSanic): | |||||||
|         "version_prefix", |         "version_prefix", | ||||||
|         "websocket_routes", |         "websocket_routes", | ||||||
|     ) |     ) | ||||||
|     __pre_registry__: Dict[Union[str, Default], Any] = defaultdict(list) |  | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
| @@ -349,6 +347,7 @@ class Blueprint(BaseSanic): | |||||||
|                 future.static, |                 future.static, | ||||||
|                 version_prefix, |                 version_prefix, | ||||||
|                 error_format, |                 error_format, | ||||||
|  |                 future.route_context, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             if (self, apply_route) in app._future_registry: |             if (self, apply_route) in app._future_registry: | ||||||
| @@ -401,8 +400,9 @@ class Blueprint(BaseSanic): | |||||||
|         for future in self._future_signals: |         for future in self._future_signals: | ||||||
|             if (self, future) in app._future_registry: |             if (self, future) in app._future_registry: | ||||||
|                 continue |                 continue | ||||||
|             future.condition.update({"blueprint": self.name}) |             future.condition.update({"__blueprint__": self.name}) | ||||||
|             app._apply_signal(future) |             # Force exclusive to be False | ||||||
|  |             app._apply_signal(tuple((*future[:-1], False))) | ||||||
|  |  | ||||||
|         self.routes += [route for route in routes if isinstance(route, Route)] |         self.routes += [route for route in routes if isinstance(route, Route)] | ||||||
|         self.websocket_routes += [ |         self.websocket_routes += [ | ||||||
| @@ -427,7 +427,7 @@ class Blueprint(BaseSanic): | |||||||
|  |  | ||||||
|     async def dispatch(self, *args, **kwargs): |     async def dispatch(self, *args, **kwargs): | ||||||
|         condition = kwargs.pop("condition", {}) |         condition = kwargs.pop("condition", {}) | ||||||
|         condition.update({"blueprint": self.name}) |         condition.update({"__blueprint__": self.name}) | ||||||
|         kwargs["condition"] = condition |         kwargs["condition"] = condition | ||||||
|         await asyncio.gather( |         await asyncio.gather( | ||||||
|             *[app.dispatch(*args, **kwargs) for app in self.apps] |             *[app.dispatch(*args, **kwargs) for app in self.apps] | ||||||
| @@ -462,30 +462,3 @@ class Blueprint(BaseSanic): | |||||||
|     ): |     ): | ||||||
|         for app in apps: |         for app in apps: | ||||||
|             app._future_registry.update(set((bp, item) for item in futures)) |             app._future_registry.update(set((bp, item) for item in futures)) | ||||||
|  |  | ||||||
|     def pre_register( |  | ||||||
|         self, |  | ||||||
|         name: Union[str, Default], |  | ||||||
|         url_prefix: Optional[str] = None, |  | ||||||
|         host: Optional[Union[List[str], str]] = None, |  | ||||||
|         version: Optional[Union[int, str, float]] = None, |  | ||||||
|         strict_slashes: Optional[bool] = None, |  | ||||||
|         version_prefix: Union[str, Default] = _default, |  | ||||||
|     ) -> None: |  | ||||||
|         if not hasattr(self.ctx, "_prereg"): |  | ||||||
|             self.ctx._prereg = [] |  | ||||||
|         self.ctx._prereg.append(name) |  | ||||||
|         self.__class__.__pre_registry__[name].append( |  | ||||||
|             { |  | ||||||
|                 k: v |  | ||||||
|                 for k, v in { |  | ||||||
|                     "bp": self, |  | ||||||
|                     "url_prefix": url_prefix, |  | ||||||
|                     "host": host, |  | ||||||
|                     "version": version, |  | ||||||
|                     "strict_slashes": strict_slashes, |  | ||||||
|                     "version_prefix": version_prefix, |  | ||||||
|                 }.items() |  | ||||||
|                 if v is not None and v is not _default |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|   | |||||||
| @@ -10,9 +10,7 @@ from typing import Any, List, Union | |||||||
|  |  | ||||||
| from sanic.app import Sanic | from sanic.app import Sanic | ||||||
| from sanic.application.logo import get_logo | from sanic.application.logo import get_logo | ||||||
| from sanic.blueprints import Blueprint |  | ||||||
| from sanic.cli.arguments import Group | from sanic.cli.arguments import Group | ||||||
| from sanic.helpers import _default |  | ||||||
| from sanic.log import error_logger | from sanic.log import error_logger | ||||||
| from sanic.simple import create_simple_server | from sanic.simple import create_simple_server | ||||||
|  |  | ||||||
| @@ -22,7 +20,6 @@ class SanicArgumentParser(ArgumentParser): | |||||||
|  |  | ||||||
|  |  | ||||||
| class SanicCLI: | class SanicCLI: | ||||||
|     DEFAULT_APP_NAME = "SANIC" |  | ||||||
|     DESCRIPTION = indent( |     DESCRIPTION = indent( | ||||||
|         f""" |         f""" | ||||||
| {get_logo(True)} | {get_logo(True)} | ||||||
| @@ -134,17 +131,7 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|  |  | ||||||
|                 app_type_name = type(app).__name__ |                 app_type_name = type(app).__name__ | ||||||
|  |  | ||||||
|                 if isinstance(app, Blueprint): |                 if not isinstance(app, Sanic): | ||||||
|                     bp = app |  | ||||||
|                     name = ( |  | ||||||
|                         bp.ctx._prereg[0] |  | ||||||
|                         if hasattr(bp.ctx, "_prereg") |  | ||||||
|                         else _default |  | ||||||
|                     ) |  | ||||||
|                     if name is _default: |  | ||||||
|                         name = self.DEFAULT_APP_NAME |  | ||||||
|                     app = Sanic(name) |  | ||||||
|                 elif not isinstance(app, Sanic): |  | ||||||
|                     raise ValueError( |                     raise ValueError( | ||||||
|                         f"Module is not a Sanic app, it is a {app_type_name}\n" |                         f"Module is not a Sanic app, it is a {app_type_name}\n" | ||||||
|                         f"  Perhaps you meant {self.args.module}.app?" |                         f"  Perhaps you meant {self.args.module}.app?" | ||||||
|   | |||||||
| @@ -8,6 +8,14 @@ from multidict import CIMultiDict  # type: ignore | |||||||
|  |  | ||||||
|  |  | ||||||
| OS_IS_WINDOWS = os.name == "nt" | 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(): | def enable_windows_color_support(): | ||||||
|   | |||||||
							
								
								
									
										146
									
								
								sanic/config.py
									
									
									
									
									
								
							
							
						
						
									
										146
									
								
								sanic/config.py
									
									
									
									
									
								
							| @@ -1,28 +1,26 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| from inspect import isclass | from inspect import getmembers, isclass, isdatadescriptor | ||||||
| from os import environ | from os import environ | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import TYPE_CHECKING, Any, Dict, Optional, Union | from typing import Any, Callable, Dict, Optional, Sequence, Union | ||||||
| from warnings import warn |  | ||||||
|  |  | ||||||
| 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.http import Http | ||||||
|  | from sanic.log import deprecation, error_logger | ||||||
| from sanic.utils import load_module_from_file_location, str_to_bool | from sanic.utils import load_module_from_file_location, str_to_bool | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING:  # no cov |  | ||||||
|     from sanic import Sanic |  | ||||||
|  |  | ||||||
|  |  | ||||||
| SANIC_PREFIX = "SANIC_" | SANIC_PREFIX = "SANIC_" | ||||||
|  |  | ||||||
|  |  | ||||||
| DEFAULT_CONFIG = { | DEFAULT_CONFIG = { | ||||||
|  |     "_FALLBACK_ERROR_FORMAT": _default, | ||||||
|     "ACCESS_LOG": True, |     "ACCESS_LOG": True, | ||||||
|  |     "AUTO_EXTEND": True, | ||||||
|     "AUTO_RELOAD": False, |     "AUTO_RELOAD": False, | ||||||
|     "EVENT_AUTOREGISTER": False, |     "EVENT_AUTOREGISTER": False, | ||||||
|     "FALLBACK_ERROR_FORMAT": "auto", |  | ||||||
|     "FORWARDED_FOR_HEADER": "X-Forwarded-For", |     "FORWARDED_FOR_HEADER": "X-Forwarded-For", | ||||||
|     "FORWARDED_SECRET": None, |     "FORWARDED_SECRET": None, | ||||||
|     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,  # 15 sec |     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,  # 15 sec | ||||||
| @@ -40,17 +38,31 @@ DEFAULT_CONFIG = { | |||||||
|     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes |     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes | ||||||
|     "REQUEST_TIMEOUT": 60,  # 60 seconds |     "REQUEST_TIMEOUT": 60,  # 60 seconds | ||||||
|     "RESPONSE_TIMEOUT": 60,  # 60 seconds |     "RESPONSE_TIMEOUT": 60,  # 60 seconds | ||||||
|  |     "USE_UVLOOP": _default, | ||||||
|     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 megabyte |     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 megabyte | ||||||
|     "WEBSOCKET_PING_INTERVAL": 20, |     "WEBSOCKET_PING_INTERVAL": 20, | ||||||
|     "WEBSOCKET_PING_TIMEOUT": 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 |     ACCESS_LOG: bool | ||||||
|  |     AUTO_EXTEND: bool | ||||||
|     AUTO_RELOAD: bool |     AUTO_RELOAD: bool | ||||||
|     EVENT_AUTOREGISTER: bool |     EVENT_AUTOREGISTER: bool | ||||||
|     FALLBACK_ERROR_FORMAT: str |  | ||||||
|     FORWARDED_FOR_HEADER: str |     FORWARDED_FOR_HEADER: str | ||||||
|     FORWARDED_SECRET: Optional[str] |     FORWARDED_SECRET: Optional[str] | ||||||
|     GRACEFUL_SHUTDOWN_TIMEOUT: float |     GRACEFUL_SHUTDOWN_TIMEOUT: float | ||||||
| @@ -69,6 +81,7 @@ class Config(dict): | |||||||
|     REQUEST_TIMEOUT: int |     REQUEST_TIMEOUT: int | ||||||
|     RESPONSE_TIMEOUT: int |     RESPONSE_TIMEOUT: int | ||||||
|     SERVER_NAME: str |     SERVER_NAME: str | ||||||
|  |     USE_UVLOOP: Union[Default, bool] | ||||||
|     WEBSOCKET_MAX_SIZE: int |     WEBSOCKET_MAX_SIZE: int | ||||||
|     WEBSOCKET_PING_INTERVAL: int |     WEBSOCKET_PING_INTERVAL: int | ||||||
|     WEBSOCKET_PING_TIMEOUT: int |     WEBSOCKET_PING_TIMEOUT: int | ||||||
| @@ -76,33 +89,27 @@ class Config(dict): | |||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         defaults: Dict[str, Union[str, bool, int, float, None]] = None, |         defaults: Dict[str, Union[str, bool, int, float, None]] = None, | ||||||
|         load_env: Optional[Union[bool, str]] = True, |  | ||||||
|         env_prefix: Optional[str] = SANIC_PREFIX, |         env_prefix: Optional[str] = SANIC_PREFIX, | ||||||
|         keep_alive: Optional[bool] = None, |         keep_alive: Optional[bool] = None, | ||||||
|         *, |         *, | ||||||
|         app: Optional[Sanic] = None, |         converters: Optional[Sequence[Callable[[str], Any]]] = None, | ||||||
|     ): |     ): | ||||||
|         defaults = defaults or {} |         defaults = defaults or {} | ||||||
|         super().__init__({**DEFAULT_CONFIG, **defaults}) |         super().__init__({**DEFAULT_CONFIG, **defaults}) | ||||||
|  |  | ||||||
|         self._app = app |         self._converters = [str, str_to_bool, float, int] | ||||||
|         self._LOGO = "" |         self._LOGO = "" | ||||||
|  |  | ||||||
|  |         if converters: | ||||||
|  |             for converter in converters: | ||||||
|  |                 self.register_type(converter) | ||||||
|  |  | ||||||
|         if keep_alive is not None: |         if keep_alive is not None: | ||||||
|             self.KEEP_ALIVE = keep_alive |             self.KEEP_ALIVE = keep_alive | ||||||
|  |  | ||||||
|         if env_prefix != SANIC_PREFIX: |         if env_prefix != SANIC_PREFIX: | ||||||
|             if env_prefix: |             if env_prefix: | ||||||
|                 self.load_environment_vars(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: |         else: | ||||||
|             self.load_environment_vars(SANIC_PREFIX) |             self.load_environment_vars(SANIC_PREFIX) | ||||||
|  |  | ||||||
| @@ -123,9 +130,21 @@ class Config(dict): | |||||||
|         self.update({attr: value}) |         self.update({attr: value}) | ||||||
|  |  | ||||||
|     def update(self, *other, **kwargs) -> None: |     def update(self, *other, **kwargs) -> None: | ||||||
|         other_mapping = {k: v for item in other for k, v in dict(item).items()} |         kwargs.update({k: v for item in other for k, v in dict(item).items()}) | ||||||
|         super().update(*other, **kwargs) |         setters: Dict[str, Any] = { | ||||||
|         for attr, value in {**other_mapping, **kwargs}.items(): |             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) |             self._post_set(attr, value) | ||||||
|  |  | ||||||
|     def _post_set(self, attr, value) -> None: |     def _post_set(self, attr, value) -> None: | ||||||
| @@ -136,32 +155,37 @@ class Config(dict): | |||||||
|                 "REQUEST_MAX_SIZE", |                 "REQUEST_MAX_SIZE", | ||||||
|             ): |             ): | ||||||
|                 self._configure_header_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": |             elif attr == "LOGO": | ||||||
|                 self._LOGO = value |                 self._LOGO = value | ||||||
|                 warn( |                 deprecation( | ||||||
|                     "Setting the config.LOGO is deprecated and will no longer " |                     "Setting the config.LOGO is deprecated and will no longer " | ||||||
|                     "be supported starting in v22.6.", |                     "be supported starting in v22.6.", | ||||||
|                     DeprecationWarning, |                     22.6, | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def app(self): |  | ||||||
|         return self._app |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def LOGO(self): |     def LOGO(self): | ||||||
|         return self._LOGO |         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): |     def _configure_header_size(self): | ||||||
|         Http.set_header_max_size( |         Http.set_header_max_size( | ||||||
|             self.REQUEST_MAX_HEADER_SIZE, |             self.REQUEST_MAX_HEADER_SIZE, | ||||||
| @@ -169,8 +193,8 @@ class Config(dict): | |||||||
|             self.REQUEST_MAX_SIZE, |             self.REQUEST_MAX_SIZE, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def _check_error_format(self): |     def _check_error_format(self, format: Optional[str] = None): | ||||||
|         check_error_format(self.FALLBACK_ERROR_FORMAT) |         check_error_format(format or self.FALLBACK_ERROR_FORMAT) | ||||||
|  |  | ||||||
|     def load_environment_vars(self, prefix=SANIC_PREFIX): |     def load_environment_vars(self, prefix=SANIC_PREFIX): | ||||||
|         """ |         """ | ||||||
| @@ -184,7 +208,23 @@ class Config(dict): | |||||||
|         - ``float`` |         - ``float`` | ||||||
|         - ``bool`` |         - ``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(): |         for key, value in environ.items(): | ||||||
|             if not key.startswith(prefix): |             if not key.startswith(prefix): | ||||||
| @@ -192,7 +232,7 @@ class Config(dict): | |||||||
|  |  | ||||||
|             _, config_key = key.split(prefix, 1) |             _, config_key = key.split(prefix, 1) | ||||||
|  |  | ||||||
|             for converter in (int, float, str_to_bool, str): |             for converter in reversed(self._converters): | ||||||
|                 try: |                 try: | ||||||
|                     self[config_key] = converter(value) |                     self[config_key] = converter(value) | ||||||
|                     break |                     break | ||||||
| @@ -267,3 +307,17 @@ class Config(dict): | |||||||
|         self.update(config) |         self.update(config) | ||||||
|  |  | ||||||
|     load = 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 |     from json import dumps | ||||||
|  |  | ||||||
|  |  | ||||||
|  | DEFAULT_FORMAT = "auto" | ||||||
| FALLBACK_TEXT = ( | FALLBACK_TEXT = ( | ||||||
|     "The server encountered an internal error and " |     "The server encountered an internal error and " | ||||||
|     "cannot complete your request." |     "cannot complete your request." | ||||||
|   | |||||||
| @@ -244,25 +244,3 @@ class InvalidSignal(SanicException): | |||||||
| class WebsocketClosed(SanicException): | class WebsocketClosed(SanicException): | ||||||
|     quiet = True |     quiet = True | ||||||
|     message = "Client has closed the websocket connection" |     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 __future__ import annotations | ||||||
| from typing import Dict, List, Optional, Tuple, Type |  | ||||||
| from warnings import warn |  | ||||||
|  |  | ||||||
| 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 ( | from sanic.exceptions import ( | ||||||
|     ContentRangeError, |     ContentRangeError, | ||||||
|     HeaderNotFound, |     HeaderNotFound, | ||||||
|     InvalidRangeType, |     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.models.handler_types import RouteHandler | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  |  | ||||||
| @@ -28,24 +37,91 @@ class ErrorHandler: | |||||||
|  |  | ||||||
|     # Beginning in v22.3, the base renderer will be TextRenderer |     # Beginning in v22.3, the base renderer will be TextRenderer | ||||||
|     def __init__( |     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.handlers: List[Tuple[Type[BaseException], RouteHandler]] = [] | ||||||
|         self.cached_handlers: Dict[ |         self.cached_handlers: Dict[ | ||||||
|             Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler] |             Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler] | ||||||
|         ] = {} |         ] = {} | ||||||
|         self.debug = False |         self.debug = False | ||||||
|         self.fallback = fallback |         self._fallback = fallback | ||||||
|         self.base = base |         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 |     @classmethod | ||||||
|     def finalize(cls, error_handler, fallback: Optional[str] = None): |     def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config): | ||||||
|         if ( |         if error_handler._fallback is not _default: | ||||||
|             fallback |             if config._FALLBACK_ERROR_FORMAT is _default: | ||||||
|             and fallback != "auto" |                 return error_handler.fallback | ||||||
|             and error_handler.fallback == "auto" |  | ||||||
|  |             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, | ||||||
|     ): |     ): | ||||||
|             error_handler.fallback = fallback |         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): |         if not isinstance(error_handler, cls): | ||||||
|             error_logger.warning( |             error_logger.warning( | ||||||
| @@ -54,7 +130,7 @@ class ErrorHandler: | |||||||
|  |  | ||||||
|         sig = signature(error_handler.lookup) |         sig = signature(error_handler.lookup) | ||||||
|         if len(sig.parameters) == 1: |         if len(sig.parameters) == 1: | ||||||
|             warn( |             deprecation( | ||||||
|                 "You are using a deprecated error handler. The lookup " |                 "You are using a deprecated error handler. The lookup " | ||||||
|                 "method should accept two positional parameters: " |                 "method should accept two positional parameters: " | ||||||
|                 "(exception, route_name: Optional[str]). " |                 "(exception, route_name: Optional[str]). " | ||||||
| @@ -62,9 +138,10 @@ class ErrorHandler: | |||||||
|                 "specific exceptions will not work properly. Beginning " |                 "specific exceptions will not work properly. Beginning " | ||||||
|                 "in v22.3, the legacy style lookup method will not " |                 "in v22.3, the legacy style lookup method will not " | ||||||
|                 "work at all.", |                 "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): |     def _full_lookup(self, exception, route_name: Optional[str] = None): | ||||||
|         return self.lookup(exception, route_name) |         return self.lookup(exception, route_name) | ||||||
| @@ -188,12 +265,13 @@ class ErrorHandler: | |||||||
|         :return: |         :return: | ||||||
|         """ |         """ | ||||||
|         self.log(request, exception) |         self.log(request, exception) | ||||||
|  |         fallback = ErrorHandler._get_fallback_value(self, request.app.config) | ||||||
|         return exception_response( |         return exception_response( | ||||||
|             request, |             request, | ||||||
|             exception, |             exception, | ||||||
|             debug=self.debug, |             debug=self.debug, | ||||||
|             base=self.base, |             base=self.base, | ||||||
|             fallback=self.fallback, |             fallback=fallback, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ Options = Dict[str, Union[int, str]]  # key=value fields in various headers | |||||||
| OptionsIterable = Iterable[Tuple[str, str]]  # May contain duplicate keys | OptionsIterable = Iterable[Tuple[str, str]]  # May contain duplicate keys | ||||||
|  |  | ||||||
| _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"' | _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"' | ||||||
| _param = re.compile(fr";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII) | _param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII) | ||||||
| _firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)') | _firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)') | ||||||
| _ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}" | _ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}" | ||||||
| _ipv6_re = re.compile(_ipv6) | _ipv6_re = re.compile(_ipv6) | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ from __future__ import annotations | |||||||
| from typing import TYPE_CHECKING, Optional | from typing import TYPE_CHECKING, Optional | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING:  # no cov | ||||||
|     from sanic.request import Request |     from sanic.request import Request | ||||||
|     from sanic.response import BaseHTTPResponse |     from sanic.response import BaseHTTPResponse | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import sys | |||||||
|  |  | ||||||
| from enum import Enum | from enum import Enum | ||||||
| from typing import Any, Dict | from typing import Any, Dict | ||||||
|  | from warnings import warn | ||||||
|  |  | ||||||
|  |  | ||||||
| LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( | LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( | ||||||
| @@ -78,3 +79,11 @@ access_logger = logging.getLogger("sanic.access") | |||||||
| """ | """ | ||||||
| Logger used by Sanic for access logging | 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 typing import Set | ||||||
|  |  | ||||||
|  | from sanic.base.meta import SanicMeta | ||||||
| from sanic.models.futures import FutureException | from sanic.models.futures import FutureException | ||||||
|  |  | ||||||
|  |  | ||||||
| class ExceptionMixin: | class ExceptionMixin(metaclass=SanicMeta): | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         self._future_exceptions: Set[FutureException] = set() |         self._future_exceptions: Set[FutureException] = set() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ from enum import Enum, auto | |||||||
| from functools import partial | from functools import partial | ||||||
| from typing import List, Optional, Union | from typing import List, Optional, Union | ||||||
|  |  | ||||||
|  | from sanic.base.meta import SanicMeta | ||||||
| from sanic.models.futures import FutureListener | from sanic.models.futures import FutureListener | ||||||
| from sanic.models.handler_types import ListenerType, Sanic | from sanic.models.handler_types import ListenerType, Sanic | ||||||
|  |  | ||||||
| @@ -18,7 +19,7 @@ class ListenerEvent(str, Enum): | |||||||
|     MAIN_PROCESS_STOP = auto() |     MAIN_PROCESS_STOP = auto() | ||||||
|  |  | ||||||
|  |  | ||||||
| class ListenerMixin: | class ListenerMixin(metaclass=SanicMeta): | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         self._future_listeners: List[FutureListener] = [] |         self._future_listeners: List[FutureListener] = [] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| from functools import partial | from functools import partial | ||||||
| from typing import List | from typing import List | ||||||
|  |  | ||||||
|  | from sanic.base.meta import SanicMeta | ||||||
| from sanic.models.futures import FutureMiddleware | from sanic.models.futures import FutureMiddleware | ||||||
|  |  | ||||||
|  |  | ||||||
| class MiddlewareMixin: | class MiddlewareMixin(metaclass=SanicMeta): | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         self._future_middleware: List[FutureMiddleware] = [] |         self._future_middleware: List[FutureMiddleware] = [] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| from ast import NodeVisitor, Return, parse | from ast import NodeVisitor, Return, parse | ||||||
|  | from contextlib import suppress | ||||||
| from functools import partial, wraps | from functools import partial, wraps | ||||||
| from inspect import getsource, signature | from inspect import getsource, signature | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| @@ -12,6 +13,7 @@ from urllib.parse import unquote | |||||||
|  |  | ||||||
| from sanic_routing.route import Route  # type: ignore | from sanic_routing.route import Route  # type: ignore | ||||||
|  |  | ||||||
|  | from sanic.base.meta import SanicMeta | ||||||
| from sanic.compat import stat_async | from sanic.compat import stat_async | ||||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS | from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS | ||||||
| from sanic.errorpages import RESPONSE_MAPPING | from sanic.errorpages import RESPONSE_MAPPING | ||||||
| @@ -22,19 +24,27 @@ from sanic.exceptions import ( | |||||||
|     InvalidUsage, |     InvalidUsage, | ||||||
| ) | ) | ||||||
| from sanic.handlers import ContentRangeHandler | 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.futures import FutureRoute, FutureStatic | ||||||
| from sanic.models.handler_types import RouteHandler | from sanic.models.handler_types import RouteHandler | ||||||
| from sanic.response import HTTPResponse, file, file_stream | from sanic.response import HTTPResponse, file, file_stream | ||||||
| from sanic.views import CompositionView | from sanic.types import HashableDict | ||||||
|  |  | ||||||
|  |  | ||||||
| RouteWrapper = Callable[ | RouteWrapper = Callable[ | ||||||
|     [RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]] |     [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 |     name: str | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
| @@ -65,10 +75,20 @@ class RouteMixin: | |||||||
|         static: bool = False, |         static: bool = False, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs: Any, | ||||||
|     ) -> RouteWrapper: |     ) -> RouteWrapper: | ||||||
|         """ |         """ | ||||||
|         Decorate a function to be registered as a route |         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 uri: path of the URL | ||||||
|         :param methods: list or tuple of methods allowed |         :param methods: list or tuple of methods allowed | ||||||
|         :param host: the host, if required |         :param host: the host, if required | ||||||
| @@ -80,6 +100,8 @@ class RouteMixin: | |||||||
|             body (eg. GET requests) |             body (eg. GET requests) | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: tuple of routes, decorated function | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
| @@ -94,6 +116,8 @@ class RouteMixin: | |||||||
|         if not methods and not websocket: |         if not methods and not websocket: | ||||||
|             methods = frozenset({"GET"}) |             methods = frozenset({"GET"}) | ||||||
|  |  | ||||||
|  |         route_context = self._build_route_context(ctx_kwargs) | ||||||
|  |  | ||||||
|         def decorator(handler): |         def decorator(handler): | ||||||
|             nonlocal uri |             nonlocal uri | ||||||
|             nonlocal methods |             nonlocal methods | ||||||
| @@ -152,6 +176,7 @@ class RouteMixin: | |||||||
|                 static, |                 static, | ||||||
|                 version_prefix, |                 version_prefix, | ||||||
|                 error_format, |                 error_format, | ||||||
|  |                 route_context, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             self._future_routes.add(route) |             self._future_routes.add(route) | ||||||
| @@ -196,6 +221,7 @@ class RouteMixin: | |||||||
|         stream: bool = False, |         stream: bool = False, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ) -> RouteHandler: |     ) -> RouteHandler: | ||||||
|         """A helper method to register class instance or |         """A helper method to register class instance or | ||||||
|         functions as a handler to the application url |         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 stream: boolean specifying if the handler is a stream handler | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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 |         :return: function or class instance | ||||||
|         """ |         """ | ||||||
|         # Handle HTTPMethodView differently |         # Handle HTTPMethodView differently | ||||||
| @@ -226,14 +254,6 @@ class RouteMixin: | |||||||
|                     if hasattr(_handler, "is_stream"): |                     if hasattr(_handler, "is_stream"): | ||||||
|                         stream = True |                         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: |         if strict_slashes is None: | ||||||
|             strict_slashes = self.strict_slashes |             strict_slashes = self.strict_slashes | ||||||
|  |  | ||||||
| @@ -247,6 +267,7 @@ class RouteMixin: | |||||||
|             name=name, |             name=name, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         )(handler) |         )(handler) | ||||||
|         return handler |         return handler | ||||||
|  |  | ||||||
| @@ -261,6 +282,7 @@ class RouteMixin: | |||||||
|         ignore_body: bool = True, |         ignore_body: bool = True, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ) -> RouteWrapper: |     ) -> RouteWrapper: | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **GET** *HTTP* method |         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 name: Unique name that can be used to identify the Route | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -285,6 +309,7 @@ class RouteMixin: | |||||||
|             ignore_body=ignore_body, |             ignore_body=ignore_body, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def post( |     def post( | ||||||
| @@ -297,6 +322,7 @@ class RouteMixin: | |||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ) -> RouteWrapper: |     ) -> RouteWrapper: | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **POST** *HTTP* method |         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 name: Unique name that can be used to identify the Route | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -321,6 +349,7 @@ class RouteMixin: | |||||||
|             name=name, |             name=name, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def put( |     def put( | ||||||
| @@ -333,6 +362,7 @@ class RouteMixin: | |||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ) -> RouteWrapper: |     ) -> RouteWrapper: | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **PUT** *HTTP* method |         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 name: Unique name that can be used to identify the Route | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -357,6 +389,7 @@ class RouteMixin: | |||||||
|             name=name, |             name=name, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def head( |     def head( | ||||||
| @@ -369,6 +402,7 @@ class RouteMixin: | |||||||
|         ignore_body: bool = True, |         ignore_body: bool = True, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ) -> RouteWrapper: |     ) -> RouteWrapper: | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **HEAD** *HTTP* method |         Add an API URL under the **HEAD** *HTTP* method | ||||||
| @@ -389,6 +423,8 @@ class RouteMixin: | |||||||
|         :type ignore_body: bool, optional |         :type ignore_body: bool, optional | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -401,6 +437,7 @@ class RouteMixin: | |||||||
|             ignore_body=ignore_body, |             ignore_body=ignore_body, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def options( |     def options( | ||||||
| @@ -413,6 +450,7 @@ class RouteMixin: | |||||||
|         ignore_body: bool = True, |         ignore_body: bool = True, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ) -> RouteWrapper: |     ) -> RouteWrapper: | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **OPTIONS** *HTTP* method |         Add an API URL under the **OPTIONS** *HTTP* method | ||||||
| @@ -433,6 +471,8 @@ class RouteMixin: | |||||||
|         :type ignore_body: bool, optional |         :type ignore_body: bool, optional | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -445,6 +485,7 @@ class RouteMixin: | |||||||
|             ignore_body=ignore_body, |             ignore_body=ignore_body, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def patch( |     def patch( | ||||||
| @@ -457,6 +498,7 @@ class RouteMixin: | |||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ) -> RouteWrapper: |     ) -> RouteWrapper: | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **PATCH** *HTTP* method |         Add an API URL under the **PATCH** *HTTP* method | ||||||
| @@ -479,6 +521,8 @@ class RouteMixin: | |||||||
|         :type ignore_body: bool, optional |         :type ignore_body: bool, optional | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -491,6 +535,7 @@ class RouteMixin: | |||||||
|             name=name, |             name=name, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def delete( |     def delete( | ||||||
| @@ -503,6 +548,7 @@ class RouteMixin: | |||||||
|         ignore_body: bool = True, |         ignore_body: bool = True, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ) -> RouteWrapper: |     ) -> RouteWrapper: | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **DELETE** *HTTP* method |         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 name: Unique name that can be used to identify the Route | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -527,6 +575,7 @@ class RouteMixin: | |||||||
|             ignore_body=ignore_body, |             ignore_body=ignore_body, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def websocket( |     def websocket( | ||||||
| @@ -540,6 +589,7 @@ class RouteMixin: | |||||||
|         apply: bool = True, |         apply: bool = True, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Decorate a function to be registered as a websocket route |         Decorate a function to be registered as a websocket route | ||||||
| @@ -553,6 +603,8 @@ class RouteMixin: | |||||||
|                      be used with :func:`url_for` |                      be used with :func:`url_for` | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: tuple of routes, decorated function | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -567,6 +619,7 @@ class RouteMixin: | |||||||
|             websocket=True, |             websocket=True, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def add_websocket_route( |     def add_websocket_route( | ||||||
| @@ -580,6 +633,7 @@ class RouteMixin: | |||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|  |         **ctx_kwargs, | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         A helper method to register a function as a websocket route. |         A helper method to register a function as a websocket route. | ||||||
| @@ -598,6 +652,8 @@ class RouteMixin: | |||||||
|                 be used with :func:`url_for` |                 be used with :func:`url_for` | ||||||
|         :param version_prefix: URL path that should be before the version |         :param version_prefix: URL path that should be before the version | ||||||
|             value; default: ``/v`` |             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: Objected decorated by :func:`websocket` | ||||||
|         """ |         """ | ||||||
|         return self.websocket( |         return self.websocket( | ||||||
| @@ -609,6 +665,7 @@ class RouteMixin: | |||||||
|             name=name, |             name=name, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|  |             **ctx_kwargs, | ||||||
|         )(handler) |         )(handler) | ||||||
|  |  | ||||||
|     def static( |     def static( | ||||||
| @@ -918,19 +975,16 @@ class RouteMixin: | |||||||
|  |  | ||||||
|         return route |         return route | ||||||
|  |  | ||||||
|     def _determine_error_format(self, handler) -> Optional[str]: |     def _determine_error_format(self, handler) -> str: | ||||||
|         if not isinstance(handler, CompositionView): |         with suppress(OSError, TypeError): | ||||||
|             try: |  | ||||||
|             src = dedent(getsource(handler)) |             src = dedent(getsource(handler)) | ||||||
|             tree = parse(src) |             tree = parse(src) | ||||||
|             http_response_types = self._get_response_types(tree) |             http_response_types = self._get_response_types(tree) | ||||||
|  |  | ||||||
|             if len(http_response_types) == 1: |             if len(http_response_types) == 1: | ||||||
|                 return next(iter(http_response_types)) |                 return next(iter(http_response_types)) | ||||||
|             except (OSError, TypeError): |  | ||||||
|                 ... |  | ||||||
|  |  | ||||||
|         return None |         return "" | ||||||
|  |  | ||||||
|     def _get_response_types(self, node): |     def _get_response_types(self, node): | ||||||
|         types = set() |         types = set() | ||||||
| @@ -939,7 +993,18 @@ class RouteMixin: | |||||||
|             def visit_Return(self, node: Return) -> Any: |             def visit_Return(self, node: Return) -> Any: | ||||||
|                 nonlocal types |                 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 |                     checks = [node.value.func.id]  # type: ignore | ||||||
|                     if node.value.keywords:  # type: ignore |                     if node.value.keywords:  # type: ignore | ||||||
|                         checks += [ |                         checks += [ | ||||||
| @@ -951,9 +1016,32 @@ class RouteMixin: | |||||||
|                     for check in checks: |                     for check in checks: | ||||||
|                         if check in RESPONSE_MAPPING: |                         if check in RESPONSE_MAPPING: | ||||||
|                             types.add(RESPONSE_MAPPING[check]) |                             types.add(RESPONSE_MAPPING[check]) | ||||||
|                 except AttributeError: |  | ||||||
|                     ... |  | ||||||
|  |  | ||||||
|         HttpResponseVisitor().visit(node) |         HttpResponseVisitor().visit(node) | ||||||
|  |  | ||||||
|         return types |         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 enum import Enum | ||||||
| from typing import Any, Callable, Dict, Optional, Set, Union | from typing import Any, Callable, Dict, Optional, Set, Union | ||||||
|  |  | ||||||
|  | from sanic.base.meta import SanicMeta | ||||||
| from sanic.models.futures import FutureSignal | from sanic.models.futures import FutureSignal | ||||||
| from sanic.models.handler_types import SignalHandler | from sanic.models.handler_types import SignalHandler | ||||||
| from sanic.signals import Signal | from sanic.signals import Signal | ||||||
|  | from sanic.types import HashableDict | ||||||
|  |  | ||||||
|  |  | ||||||
| class HashableDict(dict): | class SignalMixin(metaclass=SanicMeta): | ||||||
|     def __hash__(self): |  | ||||||
|         return hash(tuple(sorted(self.items()))) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SignalMixin: |  | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         self._future_signals: Set[FutureSignal] = set() |         self._future_signals: Set[FutureSignal] = set() | ||||||
|  |  | ||||||
| @@ -24,6 +21,7 @@ class SignalMixin: | |||||||
|         *, |         *, | ||||||
|         apply: bool = True, |         apply: bool = True, | ||||||
|         condition: Dict[str, Any] = None, |         condition: Dict[str, Any] = None, | ||||||
|  |         exclusive: bool = True, | ||||||
|     ) -> Callable[[SignalHandler], SignalHandler]: |     ) -> Callable[[SignalHandler], SignalHandler]: | ||||||
|         """ |         """ | ||||||
|         For creating a signal handler, used similar to a route handler: |         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 |         :param event: Representation of the event in ``one.two.three`` form | ||||||
|         :type event: str |         :type event: str | ||||||
|         :param apply: For lazy evaluation, defaults to True |         :param apply: For lazy evaluation, defaults to ``True`` | ||||||
|         :type apply: bool, optional |         :type apply: bool, optional | ||||||
|         :param condition: For use with the ``condition`` argument in dispatch |         :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 |         :type condition: Dict[str, Any], optional | ||||||
|         """ |         """ | ||||||
|         event_value = str(event.value) if isinstance(event, Enum) else event |         event_value = str(event.value) if isinstance(event, Enum) else event | ||||||
|  |  | ||||||
|         def decorator(handler: SignalHandler): |         def decorator(handler: SignalHandler): | ||||||
|             future_signal = FutureSignal( |             future_signal = FutureSignal( | ||||||
|                 handler, event_value, HashableDict(condition or {}) |                 handler, event_value, HashableDict(condition or {}), exclusive | ||||||
|             ) |             ) | ||||||
|             self._future_signals.add(future_signal) |             self._future_signals.add(future_signal) | ||||||
|  |  | ||||||
| @@ -62,6 +65,7 @@ class SignalMixin: | |||||||
|         handler: Optional[Callable[..., Any]], |         handler: Optional[Callable[..., Any]], | ||||||
|         event: str, |         event: str, | ||||||
|         condition: Dict[str, Any] = None, |         condition: Dict[str, Any] = None, | ||||||
|  |         exclusive: bool = True, | ||||||
|     ): |     ): | ||||||
|         if not handler: |         if not handler: | ||||||
|  |  | ||||||
| @@ -69,7 +73,9 @@ class SignalMixin: | |||||||
|                 ... |                 ... | ||||||
|  |  | ||||||
|             handler = noop |             handler = noop | ||||||
|         self.signal(event=event, condition=condition)(handler) |         self.signal(event=event, condition=condition, exclusive=exclusive)( | ||||||
|  |             handler | ||||||
|  |         ) | ||||||
|         return handler |         return handler | ||||||
|  |  | ||||||
|     def event(self, event: str): |     def event(self, event: str): | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ from sanic.models.handler_types import ( | |||||||
|     MiddlewareType, |     MiddlewareType, | ||||||
|     SignalHandler, |     SignalHandler, | ||||||
| ) | ) | ||||||
|  | from sanic.types import HashableDict | ||||||
|  |  | ||||||
|  |  | ||||||
| class FutureRoute(NamedTuple): | class FutureRoute(NamedTuple): | ||||||
| @@ -25,6 +26,7 @@ class FutureRoute(NamedTuple): | |||||||
|     static: bool |     static: bool | ||||||
|     version_prefix: str |     version_prefix: str | ||||||
|     error_format: Optional[str] |     error_format: Optional[str] | ||||||
|  |     route_context: HashableDict | ||||||
|  |  | ||||||
|  |  | ||||||
| class FutureListener(NamedTuple): | class FutureListener(NamedTuple): | ||||||
| @@ -60,6 +62,7 @@ class FutureSignal(NamedTuple): | |||||||
|     handler: SignalHandler |     handler: SignalHandler | ||||||
|     event: str |     event: str | ||||||
|     condition: Optional[Dict[str, str]] |     condition: Optional[Dict[str, str]] | ||||||
|  |     exclusive: bool | ||||||
|  |  | ||||||
|  |  | ||||||
| class FutureRegistry(set): | class FutureRegistry(set): | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ from typing import ( | |||||||
| from sanic_routing.route import Route  # type: ignore | from sanic_routing.route import Route  # type: ignore | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING:  # no cov | ||||||
|     from sanic.server import ConnInfo |     from sanic.server import ConnInfo | ||||||
|     from sanic.app import Sanic |     from sanic.app import Sanic | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
| from functools import partial | from functools import partial | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path | ||||||
| @@ -12,10 +14,10 @@ from typing import ( | |||||||
|     Iterator, |     Iterator, | ||||||
|     Optional, |     Optional, | ||||||
|     Tuple, |     Tuple, | ||||||
|  |     TypeVar, | ||||||
|     Union, |     Union, | ||||||
| ) | ) | ||||||
| from urllib.parse import quote_plus | from urllib.parse import quote_plus | ||||||
| from warnings import warn |  | ||||||
|  |  | ||||||
| from sanic.compat import Header, open_async | from sanic.compat import Header, open_async | ||||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE | from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE | ||||||
| @@ -28,6 +30,10 @@ from sanic.models.protocol_types import HTMLProtocol, Range | |||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from sanic.asgi import ASGIApp |     from sanic.asgi import ASGIApp | ||||||
|  |     from sanic.request import Request | ||||||
|  | else: | ||||||
|  |     Request = TypeVar("Request") | ||||||
|  |  | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     from ujson import dumps as json_dumps |     from ujson import dumps as json_dumps | ||||||
| @@ -136,95 +142,6 @@ class BaseHTTPResponse: | |||||||
|         await self.stream.send(data, end_stream=end_stream) |         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): | class HTTPResponse(BaseHTTPResponse): | ||||||
|     """ |     """ | ||||||
|     HTTP response to be sent back to the client. |     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( | async def file_stream( | ||||||
|     location: Union[str, PurePath], |     location: Union[str, PurePath], | ||||||
|     status: int = 200, |     status: int = 200, | ||||||
| @@ -427,7 +447,7 @@ async def file_stream( | |||||||
|     headers: Optional[Dict[str, str]] = None, |     headers: Optional[Dict[str, str]] = None, | ||||||
|     filename: Optional[str] = None, |     filename: Optional[str] = None, | ||||||
|     _range: Optional[Range] = None, |     _range: Optional[Range] = None, | ||||||
| ) -> StreamingHTTPResponse: | ) -> ResponseStream: | ||||||
|     """Return a streaming response object with file data. |     """Return a streaming response object with file data. | ||||||
|  |  | ||||||
|     :param location: Location of file on system. |     :param location: Location of file on system. | ||||||
| @@ -435,7 +455,6 @@ async def file_stream( | |||||||
|     :param mime_type: Specific mime_type. |     :param mime_type: Specific mime_type. | ||||||
|     :param headers: Custom Headers. |     :param headers: Custom Headers. | ||||||
|     :param filename: Override filename. |     :param filename: Override filename. | ||||||
|     :param chunked: Deprecated |  | ||||||
|     :param _range: |     :param _range: | ||||||
|     """ |     """ | ||||||
|     headers = headers or {} |     headers = headers or {} | ||||||
| @@ -471,23 +490,24 @@ async def file_stream( | |||||||
|                         break |                         break | ||||||
|                     await response.write(content) |                     await response.write(content) | ||||||
|  |  | ||||||
|     return StreamingHTTPResponse( |     return ResponseStream( | ||||||
|         streaming_fn=_streaming_fn, |         streaming_fn=_streaming_fn, | ||||||
|         status=status, |         status=status, | ||||||
|         headers=headers, |         headers=headers, | ||||||
|         content_type=mime_type, |         content_type=mime_type, | ||||||
|         ignore_deprecation_notice=True, |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def stream( | def stream( | ||||||
|     streaming_fn: StreamingFunction, |     streaming_fn: Callable[ | ||||||
|  |         [Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None] | ||||||
|  |     ], | ||||||
|     status: int = 200, |     status: int = 200, | ||||||
|     headers: Optional[Dict[str, str]] = None, |     headers: Optional[Dict[str, str]] = None, | ||||||
|     content_type: str = "text/plain; charset=utf-8", |     content_type: str = "text/plain; charset=utf-8", | ||||||
| ): | ) -> ResponseStream: | ||||||
|     """Accepts an coroutine `streaming_fn` which can be used to |     """Accepts a coroutine `streaming_fn` which can be used to | ||||||
|     write chunks to a streaming response. Returns a `StreamingHTTPResponse`. |     write chunks to a streaming response. Returns a `ResponseStream`. | ||||||
|  |  | ||||||
|     Example usage:: |     Example usage:: | ||||||
|  |  | ||||||
| @@ -501,42 +521,13 @@ def stream( | |||||||
|  |  | ||||||
|     :param streaming_fn: A coroutine accepts a response and |     :param streaming_fn: A coroutine accepts a response and | ||||||
|         writes content to that response. |         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 headers: Custom Headers. | ||||||
|     :param chunked: Deprecated |  | ||||||
|     """ |     """ | ||||||
|     return StreamingHTTPResponse( |     return ResponseStream( | ||||||
|         streaming_fn, |         streaming_fn, | ||||||
|         headers=headers, |         headers=headers, | ||||||
|         content_type=content_type, |         content_type=content_type, | ||||||
|         status=status, |         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.models.server_types import ConnInfo, Signal | ||||||
| from sanic.server.async_server import AsyncioServer | 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.protocols.http_protocol import HttpProtocol | ||||||
| from sanic.server.runners import serve, serve_multiple, serve_single | 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__ = ( | __all__ = ( | ||||||
|     "AsyncioServer", |     "AsyncioServer", | ||||||
|     "ConnInfo", |     "ConnInfo", | ||||||
| @@ -23,4 +13,5 @@ __all__ = ( | |||||||
|     "serve", |     "serve", | ||||||
|     "serve_multiple", |     "serve_multiple", | ||||||
|     "serve_single", |     "serve_single", | ||||||
|  |     "try_use_uvloop", | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -3,9 +3,9 @@ from __future__ import annotations | |||||||
| import asyncio | import asyncio | ||||||
|  |  | ||||||
| from typing import TYPE_CHECKING | from typing import TYPE_CHECKING | ||||||
| from warnings import warn |  | ||||||
|  |  | ||||||
| from sanic.exceptions import SanicException | from sanic.exceptions import SanicException | ||||||
|  | from sanic.log import deprecation | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
| @@ -37,10 +37,10 @@ class AsyncioServer: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def init(self): |     def init(self): | ||||||
|         warn( |         deprecation( | ||||||
|             "AsyncioServer.init has been deprecated and will be removed " |             "AsyncioServer.init has been deprecated and will be removed " | ||||||
|             "in v22.6. Use Sanic.state.is_started instead.", |             "in v22.6. Use Sanic.state.is_started instead.", | ||||||
|             DeprecationWarning, |             22.6, | ||||||
|         ) |         ) | ||||||
|         return self.app.state.is_started |         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 typing import TYPE_CHECKING, Optional, Sequence, cast | ||||||
| from warnings import warn |  | ||||||
|  |  | ||||||
| from websockets.connection import CLOSED, CLOSING, OPEN | from websockets.connection import CLOSED, CLOSING, OPEN | ||||||
| from websockets.server import ServerConnection | from websockets.server import ServerConnection | ||||||
| from websockets.typing import Subprotocol | from websockets.typing import Subprotocol | ||||||
|  |  | ||||||
| from sanic.exceptions import ServerError | from sanic.exceptions import ServerError | ||||||
| from sanic.log import error_logger | from sanic.log import deprecation, error_logger | ||||||
| from sanic.server import HttpProtocol | from sanic.server import HttpProtocol | ||||||
|  |  | ||||||
| from ..websockets.impl import WebsocketImplProtocol | from ..websockets.impl import WebsocketImplProtocol | ||||||
| @@ -17,6 +16,14 @@ if TYPE_CHECKING: | |||||||
|  |  | ||||||
|  |  | ||||||
| class WebSocketProtocol(HttpProtocol): | class WebSocketProtocol(HttpProtocol): | ||||||
|  |     __slots__ = ( | ||||||
|  |         "websocket", | ||||||
|  |         "websocket_timeout", | ||||||
|  |         "websocket_max_size", | ||||||
|  |         "websocket_ping_interval", | ||||||
|  |         "websocket_ping_timeout", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         *args, |         *args, | ||||||
| @@ -35,24 +42,24 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|         self.websocket_max_size = websocket_max_size |         self.websocket_max_size = websocket_max_size | ||||||
|         if websocket_max_queue is not None and websocket_max_queue > 0: |         if websocket_max_queue is not None and websocket_max_queue > 0: | ||||||
|             # TODO: Reminder remove this warning in v22.3 |             # TODO: Reminder remove this warning in v22.3 | ||||||
|             warn( |             deprecation( | ||||||
|                 "Websocket no longer uses queueing, so websocket_max_queue" |                 "Websocket no longer uses queueing, so websocket_max_queue" | ||||||
|                 " is no longer required.", |                 " is no longer required.", | ||||||
|                 DeprecationWarning, |                 22.3, | ||||||
|             ) |             ) | ||||||
|         if websocket_read_limit is not None and websocket_read_limit > 0: |         if websocket_read_limit is not None and websocket_read_limit > 0: | ||||||
|             # TODO: Reminder remove this warning in v22.3 |             # TODO: Reminder remove this warning in v22.3 | ||||||
|             warn( |             deprecation( | ||||||
|                 "Websocket no longer uses read buffers, so " |                 "Websocket no longer uses read buffers, so " | ||||||
|                 "websocket_read_limit is not required.", |                 "websocket_read_limit is not required.", | ||||||
|                 DeprecationWarning, |                 22.3, | ||||||
|             ) |             ) | ||||||
|         if websocket_write_limit is not None and websocket_write_limit > 0: |         if websocket_write_limit is not None and websocket_write_limit > 0: | ||||||
|             # TODO: Reminder remove this warning in v22.3 |             # TODO: Reminder remove this warning in v22.3 | ||||||
|             warn( |             deprecation( | ||||||
|                 "Websocket no longer uses write buffers, so " |                 "Websocket no longer uses write buffers, so " | ||||||
|                 "websocket_write_limit is not required.", |                 "websocket_write_limit is not required.", | ||||||
|                 DeprecationWarning, |                 22.3, | ||||||
|             ) |             ) | ||||||
|         self.websocket_ping_interval = websocket_ping_interval |         self.websocket_ping_interval = websocket_ping_interval | ||||||
|         self.websocket_ping_timeout = websocket_ping_timeout |         self.websocket_ping_timeout = websocket_ping_timeout | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import sys | ||||||
|  |  | ||||||
| from ssl import SSLContext | from ssl import SSLContext | ||||||
| from typing import TYPE_CHECKING, Dict, Optional, Type, Union | 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 SIG_IGN, SIGINT, SIGTERM, Signals | ||||||
| from signal import signal as signal_func | 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.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows | ||||||
| from sanic.log import error_logger, logger | from sanic.log import error_logger, logger | ||||||
| from sanic.models.server_types import Signal | from sanic.models.server_types import Signal | ||||||
| @@ -114,6 +117,7 @@ def serve( | |||||||
|         **asyncio_server_kwargs, |         **asyncio_server_kwargs, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     setup_ext(app) | ||||||
|     if run_async: |     if run_async: | ||||||
|         return AsyncioServer( |         return AsyncioServer( | ||||||
|             app=app, |             app=app, | ||||||
| @@ -174,6 +178,9 @@ def serve( | |||||||
|             loop.run_until_complete(asyncio.sleep(0.1)) |             loop.run_until_complete(asyncio.sleep(0.1)) | ||||||
|             start_shutdown = start_shutdown + 0.1 |             start_shutdown = start_shutdown + 0.1 | ||||||
|  |  | ||||||
|  |         if sys.version_info > (3, 7): | ||||||
|  |             app.shutdown_tasks(graceful - start_shutdown) | ||||||
|  |  | ||||||
|         # Force close non-idle connection after waiting for |         # Force close non-idle connection after waiting for | ||||||
|         # graceful_shutdown_timeout |         # graceful_shutdown_timeout | ||||||
|         for conn in connections: |         for conn in connections: | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import asyncio | |||||||
|  |  | ||||||
| from enum import Enum | from enum import Enum | ||||||
| from inspect import isawaitable | 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 import BaseRouter, Route, RouteGroup  # type: ignore | ||||||
| from sanic_routing.exceptions import NotFound  # type: ignore | from sanic_routing.exceptions import NotFound  # type: ignore | ||||||
| @@ -142,12 +142,21 @@ class SignalRouter(BaseRouter): | |||||||
|         if context: |         if context: | ||||||
|             params.update(context) |             params.update(context) | ||||||
|  |  | ||||||
|  |         signals = group.routes | ||||||
|         if not reverse: |         if not reverse: | ||||||
|             handlers = handlers[::-1] |             signals = signals[::-1] | ||||||
|         try: |         try: | ||||||
|             for handler in handlers: |             for signal in signals: | ||||||
|                 if condition is None or condition == handler.__requirements__: |                 params.pop("__trigger__", None) | ||||||
|                     maybe_coroutine = handler(**params) |                 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): |                     if isawaitable(maybe_coroutine): | ||||||
|                         retval = await maybe_coroutine |                         retval = await maybe_coroutine | ||||||
|                         if retval: |                         if retval: | ||||||
| @@ -190,23 +199,36 @@ class SignalRouter(BaseRouter): | |||||||
|         handler: SignalHandler, |         handler: SignalHandler, | ||||||
|         event: str, |         event: str, | ||||||
|         condition: Optional[Dict[str, Any]] = None, |         condition: Optional[Dict[str, Any]] = None, | ||||||
|  |         exclusive: bool = True, | ||||||
|     ) -> Signal: |     ) -> Signal: | ||||||
|  |         event_definition = event | ||||||
|         parts = self._build_event_parts(event) |         parts = self._build_event_parts(event) | ||||||
|         if parts[2].startswith("<"): |         if parts[2].startswith("<"): | ||||||
|             name = ".".join([*parts[:-1], "*"]) |             name = ".".join([*parts[:-1], "*"]) | ||||||
|  |             trigger = self._clean_trigger(parts[2]) | ||||||
|         else: |         else: | ||||||
|             name = event |             name = event | ||||||
|  |             trigger = "" | ||||||
|  |  | ||||||
|  |         if not trigger: | ||||||
|  |             event = ".".join([*parts[:2], "<__trigger__>"]) | ||||||
|  |  | ||||||
|         handler.__requirements__ = condition  # type: ignore |         handler.__requirements__ = condition  # type: ignore | ||||||
|  |         handler.__trigger__ = trigger  # type: ignore | ||||||
|  |  | ||||||
|         return super().add( |         signal = super().add( | ||||||
|             event, |             event, | ||||||
|             handler, |             handler, | ||||||
|             requirements=condition, |  | ||||||
|             name=name, |             name=name, | ||||||
|             append=True, |             append=True, | ||||||
|         )  # type: ignore |         )  # 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): |     def finalize(self, do_compile: bool = True, do_optimize: bool = False): | ||||||
|         self.add(_blank, "sanic.__signal__.__init__") |         self.add(_blank, "sanic.__signal__.__init__") | ||||||
|  |  | ||||||
| @@ -238,3 +260,9 @@ class SignalRouter(BaseRouter): | |||||||
|                 "Cannot declare reserved signal event: %s" % event |                 "Cannot declare reserved signal event: %s" % event | ||||||
|             ) |             ) | ||||||
|         return parts |         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 sanic.exceptions import SanicException | ||||||
|  |  | ||||||
| from .service import TouchUp | from .service import TouchUp | ||||||
|  |  | ||||||
|  |  | ||||||
| class TouchUpMeta(type): | class TouchUpMeta(SanicMeta): | ||||||
|     def __new__(cls, name, bases, attrs, **kwargs): |     def __new__(cls, name, bases, attrs, **kwargs): | ||||||
|         gen_class = super().__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, |     Optional, | ||||||
|     Union, |     Union, | ||||||
| ) | ) | ||||||
| from warnings import warn |  | ||||||
|  |  | ||||||
| from sanic.constants import HTTP_METHODS |  | ||||||
| from sanic.exceptions import InvalidUsage |  | ||||||
| from sanic.models.handler_types import RouteHandler | from sanic.models.handler_types import RouteHandler | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING:  # no cov | ||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|     from sanic.blueprints import Blueprint |     from sanic.blueprints import Blueprint | ||||||
|  |  | ||||||
| @@ -84,6 +81,8 @@ class HTTPMethodView: | |||||||
|  |  | ||||||
|     def dispatch_request(self, request, *args, **kwargs): |     def dispatch_request(self, request, *args, **kwargs): | ||||||
|         handler = getattr(self, request.method.lower(), None) |         handler = getattr(self, request.method.lower(), None) | ||||||
|  |         if not handler and request.method == "HEAD": | ||||||
|  |             handler = self.get | ||||||
|         return handler(request, *args, **kwargs) |         return handler(request, *args, **kwargs) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
| @@ -136,48 +135,3 @@ class HTTPMethodView: | |||||||
| def stream(func): | def stream(func): | ||||||
|     func.is_stream = True |     func.is_stream = True | ||||||
|     return func |     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 gunicorn.workers import base  # type: ignore | ||||||
|  |  | ||||||
|  | from sanic.compat import UVLOOP_INSTALLED | ||||||
| from sanic.log import logger | 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 | from sanic.server.protocols.websocket_protocol import WebSocketProtocol | ||||||
|  |  | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     import ssl  # type: ignore |     import ssl  # type: ignore | ||||||
| except ImportError: | except ImportError:  # no cov | ||||||
|     ssl = None  # type: ignore |     ssl = None  # type: ignore | ||||||
|  |  | ||||||
| try: | if UVLOOP_INSTALLED:  # no cov | ||||||
|     import uvloop  # type: ignore |     try_use_uvloop() | ||||||
|  |  | ||||||
|     asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) |  | ||||||
| except ImportError: |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class GunicornWorker(base.Worker): | class GunicornWorker(base.Worker): | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								setup.py
									
									
									
									
									
								
							| @@ -147,6 +147,7 @@ extras_require = { | |||||||
|     "dev": dev_require, |     "dev": dev_require, | ||||||
|     "docs": docs_require, |     "docs": docs_require, | ||||||
|     "all": all_require, |     "all": all_require, | ||||||
|  |     "ext": ["sanic-ext"], | ||||||
| } | } | ||||||
|  |  | ||||||
| setup_kwargs["install_requires"] = requirements | setup_kwargs["install_requires"] = requirements | ||||||
|   | |||||||
| @@ -6,8 +6,10 @@ import string | |||||||
| import sys | import sys | ||||||
| import uuid | import uuid | ||||||
|  |  | ||||||
|  | from contextlib import suppress | ||||||
| from logging import LogRecord | from logging import LogRecord | ||||||
| from typing import Callable, List, Tuple | from typing import List, Tuple | ||||||
|  | from unittest.mock import MagicMock | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| @@ -184,3 +186,21 @@ def message_in_records(): | |||||||
|         return error_captured |         return error_captured | ||||||
|  |  | ||||||
|     return msg_in_log |     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 json | ||||||
| import logging |  | ||||||
|  |  | ||||||
| from sanic import Sanic, text | from sanic import Sanic, text | ||||||
| from sanic.log import LOGGING_CONFIG_DEFAULTS, logger | 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["formatters"]["generic"]["format"] = "%(message)s" | ||||||
| LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG" | LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG" | ||||||
|  |  | ||||||
| app = Sanic(__name__, log_config=LOGGING_CONFIG) | app = Sanic("FakeServer", log_config=LOGGING_CONFIG) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/") | @app.get("/") | ||||||
|   | |||||||
| @@ -2,15 +2,20 @@ import asyncio | |||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
|  |  | ||||||
|  | from collections import Counter | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from os import environ | from os import environ | ||||||
| from unittest.mock import Mock, patch | from unittest.mock import Mock, patch | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
|  | import sanic | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
|  | from sanic.compat import OS_IS_WINDOWS | ||||||
| from sanic.config import Config | from sanic.config import Config | ||||||
| from sanic.exceptions import SanicException | from sanic.exceptions import SanicException | ||||||
|  | from sanic.helpers import _default | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -19,15 +24,6 @@ def clear_app_registry(): | |||||||
|     Sanic._app_registry = {} |     Sanic._app_registry = {} | ||||||
|  |  | ||||||
|  |  | ||||||
| def uvloop_installed(): |  | ||||||
|     try: |  | ||||||
|         import uvloop  # noqa |  | ||||||
|  |  | ||||||
|         return True |  | ||||||
|     except ImportError: |  | ||||||
|         return False |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_loop_running(app): | def test_app_loop_running(app): | ||||||
|     @app.get("/test") |     @app.get("/test") | ||||||
|     async def handler(request): |     async def handler(request): | ||||||
| @@ -397,6 +393,22 @@ def test_app_no_registry(): | |||||||
|         Sanic.get_app("no-register") |         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(): | def test_app_no_registry_env(): | ||||||
|     environ["SANIC_REGISTER"] = "False" |     environ["SANIC_REGISTER"] = "False" | ||||||
|     Sanic("no-register") |     Sanic("no-register") | ||||||
| @@ -408,15 +420,12 @@ def test_app_no_registry_env(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_set_attribute_warning(app): | def test_app_set_attribute_warning(app): | ||||||
|     with pytest.warns(DeprecationWarning) as record: |     message = ( | ||||||
|         app.foo = 1 |         "Setting variables on Sanic instances is not allowed. You should " | ||||||
|  |         "change your Sanic instance to use instance.ctx.foo instead." | ||||||
|     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." |  | ||||||
|     ) |     ) | ||||||
|  |     with pytest.raises(AttributeError, match=message): | ||||||
|  |         app.foo = 1 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_set_context(app): | def test_app_set_context(app): | ||||||
| @@ -438,15 +447,7 @@ def test_bad_custom_config(): | |||||||
|         SanicException, |         SanicException, | ||||||
|         match=( |         match=( | ||||||
|             "When instantiating Sanic with config, you cannot also pass " |             "When instantiating Sanic with config, you cannot also pass " | ||||||
|             "load_env or env_prefix" |             "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" |  | ||||||
|         ), |         ), | ||||||
|     ): |     ): | ||||||
|         Sanic("test", config=1, env_prefix=1) |         Sanic("test", config=1, env_prefix=1) | ||||||
| @@ -472,6 +473,98 @@ def test_custom_context(): | |||||||
|     assert app.ctx == ctx |     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): | def test_cannot_run_fast_and_workers(app): | ||||||
|     message = "You cannot use both fast=True and workers=X" |     message = "You cannot use both fast=True and workers=X" | ||||||
|     with pytest.raises(RuntimeError, match=message): |     with pytest.raises(RuntimeError, match=message): | ||||||
|   | |||||||
| @@ -145,6 +145,37 @@ def test_listeners_triggered_async(app): | |||||||
|     assert after_server_stop |     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 | @pytest.mark.asyncio | ||||||
| async def test_mockprotocol_events(protocol): | async def test_mockprotocol_events(protocol): | ||||||
|     assert protocol._not_paused.is_set() |     assert protocol._not_paused.is_set() | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic import Blueprint, Sanic | from sanic import Blueprint, Sanic | ||||||
|  | from sanic.exceptions import SanicException | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture | @pytest.fixture | ||||||
| @@ -79,24 +80,18 @@ def test_names_okay(name): | |||||||
| ) | ) | ||||||
| def test_names_not_okay(name): | def test_names_not_okay(name): | ||||||
|     app_message = ( |     app_message = ( | ||||||
|         f"Sanic instance named '{name}' uses a format that isdeprecated. " |         f"Sanic instance named '{name}' uses an invalid format. Names must " | ||||||
|         "Starting in version 21.12, Sanic objects must be named only using " |         "begin with a character and may only contain alphanumeric " | ||||||
|         "alphanumeric characters, _, or -." |         "characters, _, or -." | ||||||
|     ) |     ) | ||||||
|     bp_message = ( |     bp_message = ( | ||||||
|         f"Blueprint instance named '{name}' uses a format that isdeprecated. " |         f"Blueprint instance named '{name}' uses an invalid format. Names " | ||||||
|         "Starting in version 21.12, Blueprint objects must be named only using " |         "must begin with a character and may only contain alphanumeric " | ||||||
|         "alphanumeric characters, _, or -." |         "characters, _, or -." | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     with pytest.warns(DeprecationWarning) as app_e: |     with pytest.raises(SanicException, match=app_message): | ||||||
|         app = Sanic(name) |         Sanic(name) | ||||||
|  |  | ||||||
|     with pytest.warns(DeprecationWarning) as bp_e: |     with pytest.raises(SanicException, match=bp_message): | ||||||
|         bp = Blueprint(name) |         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 |  | ||||||
|   | |||||||
| @@ -15,7 +15,6 @@ from sanic.exceptions import ( | |||||||
| ) | ) | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import json, text | 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"]) | @pytest.mark.parametrize("file_name", ["test.file"]) | ||||||
| def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | 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: |     with open(current_file, "rb") as file: | ||||||
|         file.read() |         file.read() | ||||||
|  |  | ||||||
| @@ -862,31 +861,6 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | |||||||
|     assert triggered is True |     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): | def test_websocket_route(app: Sanic): | ||||||
|     event = asyncio.Event() |     event = asyncio.Event() | ||||||
|  |  | ||||||
| @@ -1079,15 +1053,12 @@ def test_blueprint_registered_multiple_apps(): | |||||||
|  |  | ||||||
| def test_bp_set_attribute_warning(): | def test_bp_set_attribute_warning(): | ||||||
|     bp = Blueprint("bp") |     bp = Blueprint("bp") | ||||||
|     with pytest.warns(DeprecationWarning) as record: |     message = ( | ||||||
|         bp.foo = 1 |         "Setting variables on Blueprint instances is not allowed. You should " | ||||||
|  |         "change your Blueprint instance to use instance.ctx.foo instead." | ||||||
|     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." |  | ||||||
|     ) |     ) | ||||||
|  |     with pytest.raises(AttributeError, match=message): | ||||||
|  |         bp.foo = 1 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_early_registration(app): | def test_early_registration(app): | ||||||
|   | |||||||
| @@ -32,6 +32,12 @@ def starting_line(lines): | |||||||
|     return 0 |     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( | @pytest.mark.parametrize( | ||||||
|     "appname", |     "appname", | ||||||
|     ( |     ( | ||||||
| @@ -199,9 +205,7 @@ def test_debug(cmd): | |||||||
|     command = ["sanic", "fake.server.app", cmd] |     command = ["sanic", "fake.server.app", cmd] | ||||||
|     out, err, exitcode = capture(command) |     out, err, exitcode = capture(command) | ||||||
|     lines = out.split(b"\n") |     lines = out.split(b"\n") | ||||||
|  |     info = read_app_info(lines) | ||||||
|     app_info = lines[starting_line(lines) + 9] |  | ||||||
|     info = json.loads(app_info) |  | ||||||
|  |  | ||||||
|     assert info["debug"] is True |     assert info["debug"] is True | ||||||
|     assert info["auto_reload"] is True |     assert info["auto_reload"] is True | ||||||
| @@ -212,9 +216,7 @@ def test_auto_reload(cmd): | |||||||
|     command = ["sanic", "fake.server.app", cmd] |     command = ["sanic", "fake.server.app", cmd] | ||||||
|     out, err, exitcode = capture(command) |     out, err, exitcode = capture(command) | ||||||
|     lines = out.split(b"\n") |     lines = out.split(b"\n") | ||||||
|  |     info = read_app_info(lines) | ||||||
|     app_info = lines[starting_line(lines) + 9] |  | ||||||
|     info = json.loads(app_info) |  | ||||||
|  |  | ||||||
|     assert info["debug"] is False |     assert info["debug"] is False | ||||||
|     assert info["auto_reload"] is True |     assert info["auto_reload"] is True | ||||||
| @@ -227,9 +229,7 @@ def test_access_logs(cmd, expected): | |||||||
|     command = ["sanic", "fake.server.app", cmd] |     command = ["sanic", "fake.server.app", cmd] | ||||||
|     out, err, exitcode = capture(command) |     out, err, exitcode = capture(command) | ||||||
|     lines = out.split(b"\n") |     lines = out.split(b"\n") | ||||||
|  |     info = read_app_info(lines) | ||||||
|     app_info = lines[starting_line(lines) + 8] |  | ||||||
|     info = json.loads(app_info) |  | ||||||
|  |  | ||||||
|     assert info["access_log"] is expected |     assert info["access_log"] is expected | ||||||
|  |  | ||||||
| @@ -254,8 +254,6 @@ def test_noisy_exceptions(cmd, expected): | |||||||
|     command = ["sanic", "fake.server.app", cmd] |     command = ["sanic", "fake.server.app", cmd] | ||||||
|     out, err, exitcode = capture(command) |     out, err, exitcode = capture(command) | ||||||
|     lines = out.split(b"\n") |     lines = out.split(b"\n") | ||||||
|  |     info = read_app_info(lines) | ||||||
|     app_info = lines[starting_line(lines) + 8] |  | ||||||
|     info = json.loads(app_info) |  | ||||||
|  |  | ||||||
|     assert info["noisy_exceptions"] is expected |     assert info["noisy_exceptions"] is expected | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
|  | import logging | ||||||
|  |  | ||||||
| from contextlib import contextmanager | from contextlib import contextmanager | ||||||
| from os import environ | from os import environ | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from tempfile import TemporaryDirectory | from tempfile import TemporaryDirectory | ||||||
| from textwrap import dedent | from textwrap import dedent | ||||||
| from unittest.mock import Mock | from unittest.mock import Mock, call | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| @@ -32,6 +34,11 @@ class ConfigTest: | |||||||
|         return self.not_for_config |         return self.not_for_config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UltimateAnswer: | ||||||
|  |     def __init__(self, answer): | ||||||
|  |         self.answer = int(answer) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_load_from_object(app): | def test_load_from_object(app): | ||||||
|     app.config.load(ConfigTest) |     app.config.load(ConfigTest) | ||||||
|     assert "CONFIG_VALUE" in app.config |     assert "CONFIG_VALUE" in app.config | ||||||
| @@ -74,26 +81,6 @@ def test_auto_bool_env_prefix(): | |||||||
|     del environ["SANIC_TEST_ANSWER"] |     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, ""]) | @pytest.mark.parametrize("env_prefix", [None, ""]) | ||||||
| def test_empty_load_env_prefix(env_prefix): | def test_empty_load_env_prefix(env_prefix): | ||||||
|     environ["SANIC_TEST_ANSWER"] = "42" |     environ["SANIC_TEST_ANSWER"] = "42" | ||||||
| @@ -102,20 +89,6 @@ def test_empty_load_env_prefix(env_prefix): | |||||||
|     del environ["SANIC_TEST_ANSWER"] |     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(): | def test_env_prefix(): | ||||||
|     environ["MYAPP_TEST_ANSWER"] = "42" |     environ["MYAPP_TEST_ANSWER"] = "42" | ||||||
|     app = Sanic(name=__name__, env_prefix="MYAPP_") |     app = Sanic(name=__name__, env_prefix="MYAPP_") | ||||||
| @@ -137,6 +110,32 @@ def test_env_prefix_string_value(): | |||||||
|     del environ["MYAPP_TEST_TOKEN"] |     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): | def test_load_from_file(app): | ||||||
|     config = dedent( |     config = dedent( | ||||||
|         """ |         """ | ||||||
| @@ -386,5 +385,24 @@ def test_config_set_methods(app, monkeypatch): | |||||||
|     post_set.assert_called_once_with("FOO", 5) |     post_set.assert_called_once_with("FOO", 5) | ||||||
|     post_set.reset_mock() |     post_set.reset_mock() | ||||||
|  |  | ||||||
|     app.config.update_config({"FOO": 6}) |     app.config.update({"FOO": 6}, {"BAR": 7}) | ||||||
|     post_set.assert_called_once_with("FOO", 6) |     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 asyncio | ||||||
|  | import sys | ||||||
|  |  | ||||||
| from threading import Event | from threading import Event | ||||||
|  | from unittest.mock import Mock | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from sanic.exceptions import SanicException | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -48,3 +53,60 @@ def test_create_task_with_app_arg(app): | |||||||
|  |  | ||||||
|     _, response = app.test_client.get("/") |     _, response = app.test_client.get("/") | ||||||
|     assert response.text == "test_create_task_with_app_arg" |     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, _): |     async def start(app, _): | ||||||
|         app.config.FALLBACK_ERROR_FORMAT = "text" |         app.config.FALLBACK_ERROR_FORMAT = "text" | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/error") |     _, response = app.test_client.get("/error") | ||||||
|     assert request.app.error_handler.fallback == "text" |  | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|     assert response.content_type == "text/plain; charset=utf-8" |     assert response.content_type == "text/plain; charset=utf-8" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_setting_fallback_to_non_default_raise_warning(app): | def test_setting_fallback_on_config_changes_as_expected(app): | ||||||
|     app.error_handler = ErrorHandler(fallback="text") |     app.error_handler = ErrorHandler() | ||||||
|  |  | ||||||
|     assert app.error_handler.fallback == "text" |     _, response = app.test_client.get("/error") | ||||||
|  |     assert response.content_type == "text/html; charset=utf-8" | ||||||
|     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" |  | ||||||
|  |  | ||||||
|     app.config.FALLBACK_ERROR_FORMAT = "text" |     app.config.FALLBACK_ERROR_FORMAT = "text" | ||||||
|  |     _, response = app.test_client.get("/error") | ||||||
|     with pytest.warns( |     assert response.content_type == "text/plain; charset=utf-8" | ||||||
|         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" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_allow_fallback_error_format_in_config_injection(): | 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") |         raise Exception("something went wrong") | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/error") |     request, response = app.test_client.get("/error") | ||||||
|     assert request.app.error_handler.fallback == "text" |  | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|     assert response.content_type == "text/plain; charset=utf-8" |     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() |     app.config = MyConfig() | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/error") |     request, response = app.test_client.get("/error") | ||||||
|     assert request.app.error_handler.fallback == "text" |  | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|     assert response.content_type == "text/plain; charset=utf-8" |     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, |     SanicException, | ||||||
|     ServerError, |     ServerError, | ||||||
|     Unauthorized, |     Unauthorized, | ||||||
|     abort, |  | ||||||
| ) | ) | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  |  | ||||||
| @@ -88,10 +87,6 @@ def exception_app(): | |||||||
|     def handler_500_error(request): |     def handler_500_error(request): | ||||||
|         raise SanicException(status_code=500) |         raise SanicException(status_code=500) | ||||||
|  |  | ||||||
|     @app.route("/old_abort") |  | ||||||
|     def handler_old_abort_error(request): |  | ||||||
|         abort(500) |  | ||||||
|  |  | ||||||
|     @app.route("/abort/message") |     @app.route("/abort/message") | ||||||
|     def handler_abort_message(request): |     def handler_abort_message(request): | ||||||
|         raise SanicException(message="Custom Message", status_code=500) |         raise SanicException(message="Custom Message", status_code=500) | ||||||
| @@ -239,11 +234,6 @@ def test_sanic_exception(exception_app): | |||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|     assert "Custom Message" in response.text |     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): | def test_custom_exception_default_message(exception_app): | ||||||
|     class TeaError(SanicException): |     class TeaError(SanicException): | ||||||
| @@ -262,7 +252,7 @@ def test_custom_exception_default_message(exception_app): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_exception_in_ws_logged(caplog): | def test_exception_in_ws_logged(caplog): | ||||||
|     app = Sanic(__file__) |     app = Sanic(__name__) | ||||||
|  |  | ||||||
|     @app.websocket("/feed") |     @app.websocket("/feed") | ||||||
|     async def feed(request, ws): |     async def feed(request, ws): | ||||||
| @@ -271,9 +261,13 @@ def test_exception_in_ws_logged(caplog): | |||||||
|     with caplog.at_level(logging.INFO): |     with caplog.at_level(logging.INFO): | ||||||
|         app.test_client.websocket("/feed") |         app.test_client.websocket("/feed") | ||||||
|  |  | ||||||
|     error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"] |     for record in caplog.record_tuples: | ||||||
|     assert error_logs[1][1] == logging.ERROR |         if record[2].startswith("Exception occurred"): | ||||||
|     assert "Exception occurred while handling uri:" in error_logs[1][2] |             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)) | @pytest.mark.parametrize("debug", (True, False)) | ||||||
|   | |||||||
| @@ -226,11 +226,12 @@ def test_single_arg_exception_handler_notice( | |||||||
|     exception_handler_app.error_handler = CustomErrorHandler() |     exception_handler_app.error_handler = CustomErrorHandler() | ||||||
|  |  | ||||||
|     message = ( |     message = ( | ||||||
|         "You are using a deprecated error handler. The lookup method should " |         "[DEPRECATION v22.3] You are using a deprecated error handler. The " | ||||||
|         "accept two positional parameters: (exception, route_name: " |         "lookup method should accept two positional parameters: (exception, " | ||||||
|         "Optional[str]). Until you upgrade your ErrorHandler.lookup, " |         "route_name: Optional[str]). Until you upgrade your " | ||||||
|         "Blueprint specific exceptions will not work properly. Beginning in " |         "ErrorHandler.lookup, Blueprint specific exceptions will not work " | ||||||
|         "v22.3, the legacy style lookup method will not work at all." |         "properly. Beginning in v22.3, the legacy style lookup method will " | ||||||
|  |         "not work at all." | ||||||
|     ) |     ) | ||||||
|     with pytest.warns(DeprecationWarning) as record: |     with pytest.warns(DeprecationWarning) as record: | ||||||
|         _, response = exception_handler_app.test_client.get("/1") |         _, 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) | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user