Compare commits
	
		
			4 Commits
		
	
	
		
			21.12LTS
			...
			pre-regist
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | c8fa52e2d2 | ||
|   | 266af1e279 | ||
|   | 6d3f1e9982 | ||
|   | a0a3840094 | 
							
								
								
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -2,13 +2,9 @@ name: "CodeQL" | |||||||
|  |  | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: |     branches: [ main ] | ||||||
|       - main |  | ||||||
|       - "*LTS" |  | ||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: [ main ] | ||||||
|       - 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,7 +3,6 @@ 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,7 +3,6 @@ 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,7 +3,6 @@ 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,7 +3,6 @@ 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,7 +3,6 @@ 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,7 +3,6 @@ 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,7 +3,6 @@ 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,7 +3,6 @@ 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,7 +3,6 @@ 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: | ||||||
| @@ -16,7 +15,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,7 +3,6 @@ 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:: | ||||||
|  |  | ||||||
|   CHANGELOG files are maintained in ``./docs/sanic/releases``. To view the full CHANGELOG, please visit https://sanic.readthedocs.io/en/stable/sanic/changelog.html. |   From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases`` | ||||||
|  |  | ||||||
|  |  | ||||||
| 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,7 +20,8 @@ Version 21.6.1 | |||||||
| 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 | ||||||
| @@ -67,7 +68,8 @@ Version 21.6.0 | |||||||
|   * `#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__`` | ||||||
| @@ -83,7 +85,8 @@ Version 21.6.0 | |||||||
|     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`` | ||||||
| @@ -92,12 +95,14 @@ Version 21.6.0 | |||||||
|   * `#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 | ||||||
| @@ -107,7 +112,8 @@ Version 21.6.0 | |||||||
| 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 | ||||||
| @@ -118,7 +124,8 @@ Version 21.3.2 | |||||||
| 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) | ||||||
| @@ -128,7 +135,8 @@ 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>`_ | ||||||
| @@ -181,7 +189,8 @@ Version 21.3.0 | |||||||
|     `#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** | Bugfixes and issues resolved | ||||||
|  | **************************** | ||||||
|  |  | ||||||
|   * 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 ``/`` | ||||||
| @@ -211,7 +220,8 @@ Version 21.3.0 | |||||||
|     `#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>`_ | ||||||
| @@ -230,7 +240,8 @@ Version 21.3.0 | |||||||
|   * ``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>`_ | ||||||
| @@ -248,7 +259,8 @@ Version 21.3.0 | |||||||
|     `#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>`_ | ||||||
| @@ -270,7 +282,8 @@ Version 21.3.0 | |||||||
|     `#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 | ||||||
| @@ -316,7 +329,8 @@ Version 21.3.0 | |||||||
| 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>`_ | ||||||
| @@ -325,7 +339,8 @@ Version 20.12.3 | |||||||
| 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>`_ | ||||||
| @@ -338,7 +353,8 @@ Version 20.12.2 | |||||||
| 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>`_ | ||||||
| @@ -351,7 +367,8 @@ Version 19.12.5 | |||||||
| 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>`_ | ||||||
| @@ -360,7 +377,8 @@ Version 20.12.0 | |||||||
| 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>`_ | ||||||
| @@ -398,19 +416,22 @@ Version 20.12.0 | |||||||
|     `#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>`_ | ||||||
| @@ -424,7 +445,8 @@ Version 20.12.0 | |||||||
|     `#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>`_ | ||||||
| @@ -442,7 +464,8 @@ Version 20.12.0 | |||||||
| 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>`_ | ||||||
| @@ -455,7 +478,8 @@ Version 20.9.1 | |||||||
| 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>`_ | ||||||
| @@ -466,7 +490,8 @@ Version 20.9.0 | |||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
|  |  | ||||||
| **Features** | Features | ||||||
|  | ******** | ||||||
|  |  | ||||||
|   * |   * | ||||||
|     `#1887 <https://github.com/sanic-org/sanic/pull/1887>`_ |     `#1887 <https://github.com/sanic-org/sanic/pull/1887>`_ | ||||||
| @@ -493,19 +518,22 @@ Version 20.9.0 | |||||||
|     `#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>`_, | ||||||
| @@ -520,7 +548,8 @@ Version 20.9.0 | |||||||
|     `#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>`_ | ||||||
| @@ -530,7 +559,8 @@ Version 20.9.0 | |||||||
| 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>`_ | ||||||
| @@ -540,7 +570,8 @@ Version 20.6.3 | |||||||
| 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>`_ | ||||||
| @@ -550,7 +581,8 @@ Version 20.6.2 | |||||||
| 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>`_ | ||||||
| @@ -564,7 +596,8 @@ Version 20.6.1 | |||||||
|     `#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>`_ | ||||||
| @@ -586,13 +619,15 @@ Version 20.6.1 | |||||||
|     `#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>`_ | ||||||
| @@ -607,7 +642,8 @@ Version 20.6.1 | |||||||
|     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>`_ | ||||||
| @@ -627,7 +663,8 @@ 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>`_ | ||||||
| @@ -658,7 +695,8 @@ Version 20.3.0 | |||||||
|     `#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>`_ | ||||||
| @@ -676,7 +714,8 @@ Version 20.3.0 | |||||||
|     `#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>`_ | ||||||
| @@ -694,7 +733,8 @@ Version 20.3.0 | |||||||
|     `#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>`_ | ||||||
| @@ -704,13 +744,15 @@ Version 20.3.0 | |||||||
|     `#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>`_ | ||||||
| @@ -752,7 +794,8 @@ Version 20.3.0 | |||||||
| Version 19.12.0 | Version 19.12.0 | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| **Bugfixes** | Bugfixes | ||||||
|  | ******** | ||||||
|  |  | ||||||
| - Fix blueprint middleware application | - Fix blueprint middleware application | ||||||
|  |  | ||||||
| @@ -771,7 +814,8 @@ Version 19.12.0 | |||||||
|   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 | ||||||
|  |  | ||||||
| @@ -785,7 +829,8 @@ Version 19.12.0 | |||||||
| Version 19.6.3 | Version 19.6.3 | ||||||
| -------------- | -------------- | ||||||
|  |  | ||||||
| **Features** | Features | ||||||
|  | ******** | ||||||
|  |  | ||||||
| - Enable Towncrier Support | - Enable Towncrier Support | ||||||
|  |  | ||||||
| @@ -793,7 +838,8 @@ Version 19.6.3 | |||||||
|   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 | ||||||
|  |  | ||||||
| @@ -806,7 +852,8 @@ Version 19.6.3 | |||||||
| 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>`_ | ||||||
| @@ -822,7 +869,8 @@ Version 19.6.2 | |||||||
|     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>`_ | ||||||
| @@ -840,7 +888,8 @@ Version 19.6.2 | |||||||
|     `#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>`_ | ||||||
| @@ -864,7 +913,8 @@ Version 19.6.2 | |||||||
| 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>`_ | ||||||
| @@ -932,7 +982,8 @@ Version 19.3 | |||||||
|  |  | ||||||
|     This is a breaking change. |     This is a breaking change. | ||||||
|  |  | ||||||
| **Bugfixes** | Bugfixes | ||||||
|  | ******** | ||||||
|  |  | ||||||
|  |  | ||||||
|   * |   * | ||||||
| @@ -968,7 +1019,8 @@ Version 19.3 | |||||||
|     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) | ||||||
| @@ -976,7 +1028,8 @@ Version 19.3 | |||||||
|   * `#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 | ||||||
| @@ -1043,13 +1096,15 @@ 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: | ||||||
|  |  | ||||||
| @@ -1129,16 +1184,19 @@ 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 | ||||||
| @@ -1146,19 +1204,23 @@ 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 | ||||||
|       - | |Py310Test| |Py39Test| |Py38Test| |Py37Test| |       - | |Py39Test| |Py38Test| |Py37Test| | ||||||
|     * - Docs |     * - Docs | ||||||
|       - | |UserGuide| |Documentation| |       - | |UserGuide| |Documentation| | ||||||
|     * - Package |     * - Package | ||||||
| @@ -27,8 +27,6 @@ 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 | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								codecov.yml
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								codecov.yml
									
									
									
									
									
								
							| @@ -1,28 +0,0 @@ | |||||||
| coverage: |  | ||||||
|   status: |  | ||||||
|     patch: |  | ||||||
|       default: |  | ||||||
|         target: auto |  | ||||||
|         threshold: 0.75 |  | ||||||
|         informational: true |  | ||||||
|     project: |  | ||||||
|       default: |  | ||||||
|         target: auto |  | ||||||
|         threshold: 0.5 |  | ||||||
|   precision: 3 |  | ||||||
| codecov: |  | ||||||
|   require_ci_to_pass: false |  | ||||||
| ignore: |  | ||||||
|   - "sanic/__main__.py" |  | ||||||
|   - "sanic/compat.py" |  | ||||||
|   - "sanic/reloader_helpers.py" |  | ||||||
|   - "sanic/simple.py" |  | ||||||
|   - "sanic/utils.py" |  | ||||||
|   - "sanic/cli" |  | ||||||
|   - ".github/" |  | ||||||
|   - "changelogs/" |  | ||||||
|   - "docker/" |  | ||||||
|   - "docs/" |  | ||||||
|   - "examples/" |  | ||||||
|   - "scripts/" |  | ||||||
|   - "tests/" |  | ||||||
| @@ -38,3 +38,10 @@ 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/21.12.md | .. mdinclude:: ./releases/21.9.md | ||||||
| .. mdinclude:: ./releases/21/21.9.md |  | ||||||
| .. include:: ../../CHANGELOG.rst | .. include:: ../../CHANGELOG.rst | ||||||
|   | |||||||
| @@ -1,14 +1,4 @@ | |||||||
| ## Version 21.9.3 | ## Version 21.9 | ||||||
| *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 | ||||||
| @@ -1,64 +0,0 @@ | |||||||
| ## 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 |  | ||||||
| @@ -5,7 +5,7 @@ import asyncio | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| 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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| 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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
| bp = Blueprint("bp_example") | bp = Blueprint("bp_" + __name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @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("Example") | app = Sanic(__name__) | ||||||
| blueprint = Blueprint("bp_example", url_prefix="/my_blueprint") | blueprint = Blueprint("name", url_prefix="/my_blueprint") | ||||||
| blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2") | blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2") | ||||||
| blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3") | blueprint3 = Blueprint("name3", 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("DelayedResponseApp", strict_slashes=True) | app = Sanic(__name__, strict_slashes=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/") | @app.get("/") | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ from sanic import Sanic | |||||||
|  |  | ||||||
|  |  | ||||||
| handler = CustomHandler() | handler = CustomHandler() | ||||||
| app = Sanic("Example", error_handler=handler) | app = Sanic(__name__, error_handler=handler) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| from sanic import Sanic, response | from sanic import Sanic | ||||||
|  | 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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
| sem = None | sem = None | ||||||
|  |  | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ LOG_SETTINGS = { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("Example", log_config=LOG_SETTINGS) | app = Sanic(__name__, 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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.middleware | @app.middleware | ||||||
|   | |||||||
| @@ -2,29 +2,27 @@ | |||||||
| Modify header or status in response | Modify header or status in response | ||||||
| """ | """ | ||||||
|  |  | ||||||
| from sanic import Sanic, response | from sanic import Sanic | ||||||
|  | from sanic import response | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("Example") | @app.route('/') | ||||||
|  |  | ||||||
|  |  | ||||||
| @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("Example") |     app = Sanic() | ||||||
|  |  | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
|     async def index(request): |     async def index(request): | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ 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: | ||||||
| @@ -21,13 +22,16 @@ class RaygunExceptionReporter(ErrorHandler): | |||||||
|  |  | ||||||
|  |  | ||||||
| raygun_error_reporter = RaygunExceptionReporter() | raygun_error_reporter = RaygunExceptionReporter() | ||||||
| app = Sanic("Example", error_handler=raygun_error_reporter) | app = Sanic(__name__, 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(host="0.0.0.0", port=getenv("PORT", 8080)) |     app.run( | ||||||
|  |         host="0.0.0.0", | ||||||
|  |         port=getenv("PORT", 8080) | ||||||
|  |     ) | ||||||
|   | |||||||
| @@ -1,18 +1,18 @@ | |||||||
| from sanic import Sanic, response | from sanic import Sanic | ||||||
|  | from sanic import response | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|      |      | ||||||
| app = Sanic("Example") | @app.route('/') | ||||||
|  |  | ||||||
|  |  | ||||||
| @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,63 +1,65 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.blueprints import Blueprint | from sanic.views import CompositionView | ||||||
| from sanic.response import stream, text |  | ||||||
| from sanic.views import HTTPMethodView | from sanic.views import HTTPMethodView | ||||||
| from sanic.views import stream as stream_decorator | from sanic.views import stream as stream_decorator | ||||||
|  | from sanic.blueprints import Blueprint | ||||||
|  | from sanic.response import stream, text | ||||||
|  |  | ||||||
|  | bp = Blueprint('blueprint_request_stream') | ||||||
| bp = Blueprint("bp_example") | app = Sanic('request_stream') | ||||||
| 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,23 +1,21 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  | from sanic import Sanic | ||||||
| from sanic import Sanic, response | from sanic import 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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @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,22 +1,21 @@ | |||||||
| 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 sanic.handlers import ErrorHandler | from os import getenv | ||||||
|  |  | ||||||
|  |  | ||||||
| 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("Example", error_handler=RollbarExceptionHandler()) | app = Sanic(__name__, error_handler=RollbarExceptionHandler()) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/raise") | @app.route("/raise") | ||||||
| @@ -25,4 +24,7 @@ def create_error(request): | |||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     app.run(host="0.0.0.0", port=getenv("PORT", 8080)) |     app.run( | ||||||
|  |         host="0.0.0.0", | ||||||
|  |         port=getenv("PORT", 8080) | ||||||
|  |     ) | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ from pathlib import Path | |||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/text") | @app.route("/text") | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import uvloop | |||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.before_server_start | @app.before_server_start | ||||||
|   | |||||||
| @@ -6,19 +6,20 @@ 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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| # noinspection PyUnusedLocal | # noinspection PyUnusedLocal | ||||||
| @app.route("/working") | @app.route("/working") | ||||||
| async def working_path(request): | async def working_path(request): | ||||||
|     return json({"response": "Working API Response"}) |     return json({ | ||||||
|  |         "response": "Working API Response" | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |  | ||||||
| # noinspection PyUnusedLocal | # noinspection PyUnusedLocal | ||||||
| @@ -27,5 +28,8 @@ async def raise_error(request): | |||||||
|     raise Exception("Testing Sentry Integration") |     raise Exception("Testing Sentry Integration") | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == '__main__': | ||||||
|     app.run(host="0.0.0.0", port=getenv("PORT", 8080)) |     app.run( | ||||||
|  |         host="0.0.0.0", | ||||||
|  |         port=getenv("PORT", 8080) | ||||||
|  |     ) | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
| app.static("/", "./static") | app.static("/", "./static") | ||||||
|   | |||||||
| @@ -1,14 +1,13 @@ | |||||||
| 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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import socket | |||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/test") | @app.route("/test") | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @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("Example") | app = Sanic(__name__) | ||||||
| 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("Example") | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| app.static("index.html", "websocket.html") | app.static("index.html", "websocket.html") | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "21.12.2" | __version__ = "21.12.0dev" | ||||||
|   | |||||||
							
								
								
									
										519
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										519
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -1,6 +1,5 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| import asyncio |  | ||||||
| import logging | import logging | ||||||
| import logging.config | import logging.config | ||||||
| import os | import os | ||||||
| @@ -12,7 +11,6 @@ from asyncio import ( | |||||||
|     AbstractEventLoop, |     AbstractEventLoop, | ||||||
|     CancelledError, |     CancelledError, | ||||||
|     Protocol, |     Protocol, | ||||||
|     Task, |  | ||||||
|     ensure_future, |     ensure_future, | ||||||
|     get_event_loop, |     get_event_loop, | ||||||
|     wait_for, |     wait_for, | ||||||
| @@ -28,7 +26,6 @@ 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, | ||||||
| @@ -42,11 +39,10 @@ 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 | from warnings import filterwarnings, warn | ||||||
|  |  | ||||||
| from sanic_routing.exceptions import (  # type: ignore | from sanic_routing.exceptions import (  # type: ignore | ||||||
|     FinalizationError, |     FinalizationError, | ||||||
| @@ -55,12 +51,11 @@ 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.root import BaseSanic | from sanic.base 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 | ||||||
| @@ -72,15 +67,9 @@ from sanic.exceptions import ( | |||||||
|     URLBuildError, |     URLBuildError, | ||||||
| ) | ) | ||||||
| from sanic.handlers import ErrorHandler | from sanic.handlers import ErrorHandler | ||||||
| from sanic.helpers import _default | from sanic.helpers import Default, _default | ||||||
| from sanic.http import Stage | from sanic.http import Stage | ||||||
| from sanic.log import ( | from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger | ||||||
|     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, | ||||||
| @@ -94,11 +83,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, ResponseStream | from sanic.response import BaseHTTPResponse, HTTPResponse | ||||||
| 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, try_use_uvloop | from sanic.server import serve, serve_multiple, serve_single | ||||||
| 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 | ||||||
| @@ -106,21 +95,11 @@ 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): | ||||||
|     """ |     """ | ||||||
| @@ -133,12 +112,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         "_run_response_middleware", |         "_run_response_middleware", | ||||||
|         "_run_request_middleware", |         "_run_request_middleware", | ||||||
|     ) |     ) | ||||||
|     __slots__ = ( |     __fake_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", | ||||||
| @@ -147,15 +126,20 @@ 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", | ||||||
| @@ -167,12 +151,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__( | ||||||
| @@ -183,6 +167,7 @@ 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, | ||||||
| @@ -198,27 +183,25 @@ 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 env_prefix != SANIC_PREFIX: |         if config and (load_env is not True or 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 " | ||||||
|                 "env_prefix" |                 "load_env or 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 | ||||||
| @@ -240,12 +223,6 @@ 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) | ||||||
| @@ -276,6 +253,32 @@ 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]: | ||||||
| @@ -400,16 +403,12 @@ 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 | ||||||
|  |  | ||||||
| @@ -538,6 +537,30 @@ 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. | ||||||
|  |  | ||||||
| @@ -755,7 +778,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                 exception, request.name if request else None |                 exception, request.name if request else None | ||||||
|             ) |             ) | ||||||
|             if handler: |             if handler: | ||||||
|                 deprecation( |                 warn( | ||||||
|                     "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 " | ||||||
| @@ -770,7 +793,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", | ||||||
|                     22.6, |                     DeprecationWarning, | ||||||
|                 ) |                 ) | ||||||
|                 try: |                 try: | ||||||
|                     response = self.error_handler.response(request, exception) |                     response = self.error_handler.response(request, exception) | ||||||
| @@ -823,9 +846,6 @@ 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", | ||||||
| @@ -836,17 +856,6 @@ 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)" | ||||||
| @@ -950,8 +959,7 @@ 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 | ||||||
|  |  | ||||||
|             # Marked for cleanup and DRY with handle_request/handle_exception |             # Make sure that response is finished / run StreamingHTTP callback | ||||||
|             # 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", | ||||||
| @@ -962,17 +970,6 @@ 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( | ||||||
| @@ -1186,11 +1183,6 @@ 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 | ||||||
| @@ -1218,7 +1210,6 @@ 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() | ||||||
|  |  | ||||||
| @@ -1288,13 +1279,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                 WebSocketProtocol if self.websocket_enabled else HttpProtocol |                 WebSocketProtocol if self.websocket_enabled else HttpProtocol | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # Set explicitly passed configuration values |         # if access_log is passed explicitly change config.ACCESS_LOG | ||||||
|         for attribute, value in { |         if access_log is not None: | ||||||
|             "ACCESS_LOG": access_log, |             self.config.ACCESS_LOG = access_log | ||||||
|             "NOISY_EXCEPTIONS": noisy_exceptions, |  | ||||||
|         }.items(): |         if noisy_exceptions is not None: | ||||||
|             if value is not None: |             self.config.NOISY_EXCEPTIONS = noisy_exceptions | ||||||
|                 setattr(self.config, attribute, value) |  | ||||||
|  |  | ||||||
|         server_settings = self._helper( |         server_settings = self._helper( | ||||||
|             host=host, |             host=host, | ||||||
| @@ -1309,14 +1299,6 @@ 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: | ||||||
| @@ -1435,15 +1417,26 @@ 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 |  | ||||||
|         self.state.unix = unix |         # Serve | ||||||
|         self.state.sock = sock |         serve_location = "" | ||||||
|  |         proto = "http" | ||||||
|  |         if ssl is not None: | ||||||
|  |             proto = "https" | ||||||
|  |         if unix: | ||||||
|  |             serve_location = f"{unix} {proto}://..." | ||||||
|  |         elif sock: | ||||||
|  |             serve_location = f"{sock.getsockname()} {proto}://..." | ||||||
|  |         elif host and port: | ||||||
|  |             # colon(:) is legal for a host only in an ipv6 address | ||||||
|  |             display_host = f"[{host}]" if ":" in host else host | ||||||
|  |             serve_location = f"{proto}://{display_host}:{port}" | ||||||
|  |  | ||||||
|  |         ssl = process_to_context(ssl) | ||||||
|  |  | ||||||
|         server_settings = { |         server_settings = { | ||||||
|             "protocol": protocol, |             "protocol": protocol, | ||||||
| @@ -1459,7 +1452,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|             "backlog": backlog, |             "backlog": backlog, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         self.motd(self.serve_location) |         self.motd(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( | ||||||
| @@ -1485,55 +1478,12 @@ 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 _cancel_websocket_tasks(cls, app, loop): |     def _prep_task(cls, task, 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) | ||||||
| @@ -1543,31 +1493,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         return task |         return task | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _loop_add_task( |     def _loop_add_task(cls, task, app, loop): | ||||||
|         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) | ||||||
|             if sys.version_info < (3, 8): |         loop.create_task(prepped) | ||||||
|                 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) |  | ||||||
|  |  | ||||||
|         if name and register and sys.version_info > (3, 7): |     @classmethod | ||||||
|             app._task_registry[name] = task |     def _cancel_websocket_tasks(cls, app, loop): | ||||||
|  |         for task in app.websocket_tasks: | ||||||
|         return task |             task.cancel() | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     async def dispatch_delayed_tasks(app, loop): |     async def dispatch_delayed_tasks(app, loop): | ||||||
| @@ -1580,142 +1513,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         prepped = app._prep_task(task, app, loop) |         prepped = app._prep_task(task, app, loop) | ||||||
|         await prepped |         await prepped | ||||||
|  |  | ||||||
|     def add_task( |     @staticmethod | ||||||
|         self, |     async def _listener( | ||||||
|         task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]], |         app: Sanic, loop: AbstractEventLoop, listener: ListenerType | ||||||
|         *, |  | ||||||
|         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 |  | ||||||
|     ): |     ): | ||||||
|         if sys.version_info < (3, 8): |         maybe_coro = listener(app, loop) | ||||||
|             error_logger.warning( |         if maybe_coro and isawaitable(maybe_coro): | ||||||
|                 "This feature (shutdown_tasks) is only supported on using " |             await maybe_coro | ||||||
|                 "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 | ||||||
| @@ -1728,7 +1532,6 @@ 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 | ||||||
| @@ -1834,8 +1637,11 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|                 display["auto-reload"] = reload_display |                 display["auto-reload"] = reload_display | ||||||
|  |  | ||||||
|             packages = [] |             packages = [] | ||||||
|             for package_name in SANIC_PACKAGES: |             for package_name, module_name in { | ||||||
|                 module_name = package_name.replace("-", "_") |                 "sanic-routing": "sanic_routing", | ||||||
|  |                 "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__}") | ||||||
| @@ -1855,41 +1661,6 @@ 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 | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
| @@ -1931,6 +1702,32 @@ 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 | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
| @@ -1951,33 +1748,13 @@ 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( | ||||||
|         # TODO: Replace in v22.6 to check against apps in app registry |             self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT | ||||||
|         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( | ||||||
|   | |||||||
| @@ -1,39 +0,0 @@ | |||||||
| 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,6 +41,9 @@ 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,9 +5,7 @@ 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 socket import socket | from typing import TYPE_CHECKING, Any, Set, Union | ||||||
| from ssl import SSLContext |  | ||||||
| from typing import TYPE_CHECKING, Any, Optional, Set, Union |  | ||||||
|  |  | ||||||
| from sanic.log import logger | from sanic.log import logger | ||||||
|  |  | ||||||
| @@ -39,11 +37,8 @@ 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="") | ||||||
|     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) |     mode: Mode = field(default=Mode.PRODUCTION) | ||||||
|  |     port: int = field(default=0) | ||||||
|     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,7 +7,6 @@ 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 | ||||||
| @@ -54,13 +53,6 @@ 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. | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import re | import re | ||||||
| 
 | 
 | ||||||
| from typing import Any | from typing import Any, Tuple | ||||||
|  | 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,9 +20,8 @@ class BaseSanic( | |||||||
|     ListenerMixin, |     ListenerMixin, | ||||||
|     ExceptionMixin, |     ExceptionMixin, | ||||||
|     SignalMixin, |     SignalMixin, | ||||||
|     metaclass=SanicMeta, |  | ||||||
| ): | ): | ||||||
|     __slots__ = ("name",) |     __fake_slots__: Tuple[str, ...] | ||||||
| 
 | 
 | ||||||
|     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__ | ||||||
| @@ -34,10 +33,11 @@ class BaseSanic( | |||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         if not VALID_NAME.match(name): |         if not VALID_NAME.match(name): | ||||||
|             raise SanicException( |             warn( | ||||||
|                 f"{class_name} instance named '{name}' uses an invalid " |                 f"{class_name} instance named '{name}' uses a format that is" | ||||||
|                 "format. Names must begin with a character and may only " |                 f"deprecated. Starting in version 21.12, {class_name} objects " | ||||||
|                 "contain alphanumeric characters, _, or -." |                 "must be named only using alphanumeric characters, _, or -.", | ||||||
|  |                 DeprecationWarning, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         self.name = name |         self.name = name | ||||||
| @@ -52,12 +52,15 @@ 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: | ||||||
|         try: |         # This is a temporary compat layer so we can raise a warning until | ||||||
|             super().__setattr__(name, value) |         # setting attributes on the app instance can be removed and deprecated | ||||||
|         except AttributeError as e: |         # with a proper implementation of __slots__ | ||||||
|             raise AttributeError( |         if name not in self.__fake_slots__: | ||||||
|  |             warn( | ||||||
|                 f"Setting variables on {self.__class__.__name__} instances is " |                 f"Setting variables on {self.__class__.__name__} instances is " | ||||||
|                 "not allowed. You should change your " |                 "deprecated and will be removed in version 21.12. You should " | ||||||
|                 f"{self.__class__.__name__} instance to use " |                 f"change your {self.__class__.__name__} instance to use " | ||||||
|                 f"instance.ctx.{name} instead.", |                 f"instance.ctx.{name} instead.", | ||||||
|             ) from e |                 DeprecationWarning, | ||||||
|  |             ) | ||||||
|  |         super().__setattr__(name, value) | ||||||
| @@ -1,6 +0,0 @@ | |||||||
| class SanicMeta(type): |  | ||||||
|     @classmethod |  | ||||||
|     def __prepare__(metaclass, name, bases, **kwds): |  | ||||||
|         cls = super().__prepare__(metaclass, name, bases, **kwds) |  | ||||||
|         cls["__slots__"] = () |  | ||||||
|         return cls |  | ||||||
| @@ -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:  # no cov | if TYPE_CHECKING: | ||||||
|     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.root import BaseSanic | from sanic.base 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:  # no cov | if TYPE_CHECKING: | ||||||
|     from sanic import Sanic |     from sanic import Sanic  # noqa | ||||||
|  |  | ||||||
|  |  | ||||||
| def lazy(func, as_decorator=True): | def lazy(func, as_decorator=True): | ||||||
| @@ -85,7 +85,7 @@ class Blueprint(BaseSanic): | |||||||
|         trailing */* |         trailing */* | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     __slots__ = ( |     __fake_slots__ = ( | ||||||
|         "_apps", |         "_apps", | ||||||
|         "_future_routes", |         "_future_routes", | ||||||
|         "_future_statics", |         "_future_statics", | ||||||
| @@ -98,6 +98,7 @@ class Blueprint(BaseSanic): | |||||||
|         "host", |         "host", | ||||||
|         "listeners", |         "listeners", | ||||||
|         "middlewares", |         "middlewares", | ||||||
|  |         "name", | ||||||
|         "routes", |         "routes", | ||||||
|         "statics", |         "statics", | ||||||
|         "strict_slashes", |         "strict_slashes", | ||||||
| @@ -106,6 +107,7 @@ 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, | ||||||
| @@ -347,7 +349,6 @@ 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: | ||||||
| @@ -400,9 +401,8 @@ 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}) | ||||||
|             # Force exclusive to be False |             app._apply_signal(future) | ||||||
|             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,3 +462,30 @@ 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,7 +10,9 @@ 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 | ||||||
|  |  | ||||||
| @@ -20,6 +22,7 @@ class SanicArgumentParser(ArgumentParser): | |||||||
|  |  | ||||||
|  |  | ||||||
| class SanicCLI: | class SanicCLI: | ||||||
|  |     DEFAULT_APP_NAME = "SANIC" | ||||||
|     DESCRIPTION = indent( |     DESCRIPTION = indent( | ||||||
|         f""" |         f""" | ||||||
| {get_logo(True)} | {get_logo(True)} | ||||||
| @@ -131,7 +134,17 @@ 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 not isinstance(app, Sanic): |                 if isinstance(app, Blueprint): | ||||||
|  |                     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,14 +8,6 @@ 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,26 +1,28 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| from inspect import getmembers, isclass, isdatadescriptor | from inspect import isclass | ||||||
| from os import environ | from os import environ | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Any, Callable, Dict, Optional, Sequence, Union | from typing import TYPE_CHECKING, Any, Dict, Optional, Union | ||||||
|  | from warnings import warn | ||||||
|  |  | ||||||
| from sanic.errorpages import DEFAULT_FORMAT, check_error_format | from sanic.errorpages import 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 | ||||||
| @@ -38,31 +40,17 @@ 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 | ||||||
| @@ -81,7 +69,6 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|     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 | ||||||
| @@ -89,27 +76,33 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|     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, | ||||||
|         *, |         *, | ||||||
|         converters: Optional[Sequence[Callable[[str], Any]]] = None, |         app: Optional[Sanic] = None, | ||||||
|     ): |     ): | ||||||
|         defaults = defaults or {} |         defaults = defaults or {} | ||||||
|         super().__init__({**DEFAULT_CONFIG, **defaults}) |         super().__init__({**DEFAULT_CONFIG, **defaults}) | ||||||
|  |  | ||||||
|         self._converters = [str, str_to_bool, float, int] |         self._app = app | ||||||
|         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) | ||||||
|  |  | ||||||
| @@ -130,21 +123,9 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|         self.update({attr: value}) |         self.update({attr: value}) | ||||||
|  |  | ||||||
|     def update(self, *other, **kwargs) -> None: |     def update(self, *other, **kwargs) -> None: | ||||||
|         kwargs.update({k: v for item in other for k, v in dict(item).items()}) |         other_mapping = {k: v for item in other for k, v in dict(item).items()} | ||||||
|         setters: Dict[str, Any] = { |         super().update(*other, **kwargs) | ||||||
|             k: kwargs.pop(k) |         for attr, value in {**other_mapping, **kwargs}.items(): | ||||||
|             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: | ||||||
| @@ -155,37 +136,32 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|                 "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 | ||||||
|                 deprecation( |                 warn( | ||||||
|                     "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.", | ||||||
|                     22.6, |                     DeprecationWarning, | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|  |     @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, | ||||||
| @@ -193,8 +169,8 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|             self.REQUEST_MAX_SIZE, |             self.REQUEST_MAX_SIZE, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def _check_error_format(self, format: Optional[str] = None): |     def _check_error_format(self): | ||||||
|         check_error_format(format or self.FALLBACK_ERROR_FORMAT) |         check_error_format(self.FALLBACK_ERROR_FORMAT) | ||||||
|  |  | ||||||
|     def load_environment_vars(self, prefix=SANIC_PREFIX): |     def load_environment_vars(self, prefix=SANIC_PREFIX): | ||||||
|         """ |         """ | ||||||
| @@ -208,23 +184,7 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|         - ``float`` |         - ``float`` | ||||||
|         - ``bool`` |         - ``bool`` | ||||||
|  |  | ||||||
|         Anything else will be imported as a ``str``. If you would like to add |         Anything else will be imported as a ``str``. | ||||||
|         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): | ||||||
| @@ -232,7 +192,7 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|  |  | ||||||
|             _, config_key = key.split(prefix, 1) |             _, config_key = key.split(prefix, 1) | ||||||
|  |  | ||||||
|             for converter in reversed(self._converters): |             for converter in (int, float, str_to_bool, str): | ||||||
|                 try: |                 try: | ||||||
|                     self[config_key] = converter(value) |                     self[config_key] = converter(value) | ||||||
|                     break |                     break | ||||||
| @@ -307,17 +267,3 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|         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,7 +34,6 @@ 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,3 +244,25 @@ 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,23 +1,14 @@ | |||||||
| from __future__ import annotations |  | ||||||
|  |  | ||||||
| from inspect import signature | from inspect import signature | ||||||
| from typing import Dict, List, Optional, Tuple, Type, Union | from typing import Dict, List, Optional, Tuple, Type | ||||||
|  | from warnings import warn | ||||||
|  |  | ||||||
| from sanic.config import Config | from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response | ||||||
| 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.helpers import Default, _default | from sanic.log import error_logger | ||||||
| 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 | ||||||
|  |  | ||||||
| @@ -37,91 +28,24 @@ 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, |         self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer | ||||||
|         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 _get_fallback_value(cls, error_handler: ErrorHandler, config: Config): |     def finalize(cls, error_handler, fallback: Optional[str] = None): | ||||||
|         if error_handler._fallback is not _default: |         if ( | ||||||
|             if config._FALLBACK_ERROR_FORMAT is _default: |             fallback | ||||||
|                 return error_handler.fallback |             and fallback != "auto" | ||||||
|  |             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, |  | ||||||
|         ): |         ): | ||||||
|         if fallback: |             error_handler.fallback = 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( | ||||||
| @@ -130,7 +54,7 @@ class ErrorHandler: | |||||||
|  |  | ||||||
|         sig = signature(error_handler.lookup) |         sig = signature(error_handler.lookup) | ||||||
|         if len(sig.parameters) == 1: |         if len(sig.parameters) == 1: | ||||||
|             deprecation( |             warn( | ||||||
|                 "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]). " | ||||||
| @@ -138,10 +62,9 @@ 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.", | ||||||
|                 22.3, |                 DeprecationWarning, | ||||||
|             ) |             ) | ||||||
|             legacy_lookup = error_handler._legacy_lookup |             error_handler._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) | ||||||
| @@ -265,13 +188,12 @@ 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=fallback, |             fallback=self.fallback, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ from __future__ import annotations | |||||||
| from typing import TYPE_CHECKING, Optional | from typing import TYPE_CHECKING, Optional | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING:  # no cov | if TYPE_CHECKING: | ||||||
|     from sanic.request import Request |     from sanic.request import Request | ||||||
|     from sanic.response import BaseHTTPResponse |     from sanic.response import BaseHTTPResponse | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ 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( | ||||||
| @@ -79,11 +78,3 @@ 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,10 +1,9 @@ | |||||||
| 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(metaclass=SanicMeta): | class ExceptionMixin: | ||||||
|     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,7 +2,6 @@ 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 | ||||||
|  |  | ||||||
| @@ -19,7 +18,7 @@ class ListenerEvent(str, Enum): | |||||||
|     MAIN_PROCESS_STOP = auto() |     MAIN_PROCESS_STOP = auto() | ||||||
|  |  | ||||||
|  |  | ||||||
| class ListenerMixin(metaclass=SanicMeta): | class ListenerMixin: | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         self._future_listeners: List[FutureListener] = [] |         self._future_listeners: List[FutureListener] = [] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,11 +1,10 @@ | |||||||
| 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(metaclass=SanicMeta): | class MiddlewareMixin: | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         self._future_middleware: List[FutureMiddleware] = [] |         self._future_middleware: List[FutureMiddleware] = [] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| 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 | ||||||
| from os import path | from os import path | ||||||
| from pathlib import Path, PurePath | from pathlib import PurePath | ||||||
|  | from re import sub | ||||||
| from textwrap import dedent | from textwrap import dedent | ||||||
| from time import gmtime, strftime | from time import gmtime, strftime | ||||||
| from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union | from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union | ||||||
| @@ -12,33 +12,29 @@ 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 | ||||||
| from sanic.exceptions import ContentRangeError, FileNotFound, HeaderNotFound | from sanic.exceptions import ( | ||||||
|  |     ContentRangeError, | ||||||
|  |     FileNotFound, | ||||||
|  |     HeaderNotFound, | ||||||
|  |     InvalidUsage, | ||||||
|  | ) | ||||||
| from sanic.handlers import ContentRangeHandler | from sanic.handlers import ContentRangeHandler | ||||||
| from sanic.log import deprecation, error_logger | from sanic.log import 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.types import HashableDict | from sanic.views import CompositionView | ||||||
|  |  | ||||||
|  |  | ||||||
| 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(metaclass=SanicMeta): | class RouteMixin: | ||||||
|     name: str |     name: str | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
| @@ -69,20 +65,10 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -94,8 +80,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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 | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
| @@ -110,8 +94,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -170,7 +152,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|                 static, |                 static, | ||||||
|                 version_prefix, |                 version_prefix, | ||||||
|                 error_format, |                 error_format, | ||||||
|                 route_context, |  | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             self._future_routes.add(route) |             self._future_routes.add(route) | ||||||
| @@ -215,7 +196,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -232,8 +212,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         :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 | ||||||
| @@ -248,6 +226,14 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|                     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 | ||||||
|  |  | ||||||
| @@ -261,7 +247,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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 | ||||||
|  |  | ||||||
| @@ -276,7 +261,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -289,8 +273,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         :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( | ||||||
| @@ -303,7 +285,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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( | ||||||
| @@ -316,7 +297,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -329,8 +309,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         :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( | ||||||
| @@ -343,7 +321,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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( | ||||||
| @@ -356,7 +333,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -369,8 +345,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         :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( | ||||||
| @@ -383,7 +357,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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( | ||||||
| @@ -396,7 +369,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -417,8 +389,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         :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( | ||||||
| @@ -431,7 +401,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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( | ||||||
| @@ -444,7 +413,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -465,8 +433,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         :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( | ||||||
| @@ -479,7 +445,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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( | ||||||
| @@ -492,7 +457,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -515,8 +479,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         :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( | ||||||
| @@ -529,7 +491,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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( | ||||||
| @@ -542,7 +503,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -555,8 +515,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         :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( | ||||||
| @@ -569,7 +527,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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( | ||||||
| @@ -583,7 +540,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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 | ||||||
| @@ -597,8 +553,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|                      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( | ||||||
| @@ -613,7 +567,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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( | ||||||
| @@ -627,7 +580,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         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. | ||||||
| @@ -646,8 +598,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|                 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( | ||||||
| @@ -659,7 +609,6 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             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( | ||||||
| @@ -769,40 +718,32 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         content_type=None, |         content_type=None, | ||||||
|         __file_uri__=None, |         __file_uri__=None, | ||||||
|     ): |     ): | ||||||
|  |         # Using this to determine if the URL is trying to break out of the path | ||||||
|  |         # served.  os.path.realpath seems to be very slow | ||||||
|  |         if __file_uri__ and "../" in __file_uri__: | ||||||
|  |             raise InvalidUsage("Invalid URL") | ||||||
|         # Merge served directory and requested file if provided |         # Merge served directory and requested file if provided | ||||||
|         file_path_raw = Path(unquote(file_or_directory)) |         # Strip all / that in the beginning of the URL to help prevent python | ||||||
|         root_path = file_path = file_path_raw.resolve() |         # from herping a derp and treating the uri as an absolute path | ||||||
|         not_found = FileNotFound( |         root_path = file_path = file_or_directory | ||||||
|  |         if __file_uri__: | ||||||
|  |             file_path = path.join( | ||||||
|  |                 file_or_directory, sub("^[/]*", "", __file_uri__) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # URL decode the path sent by the browser otherwise we won't be able to | ||||||
|  |         # match filenames which got encoded (filenames with spaces etc) | ||||||
|  |         file_path = path.abspath(unquote(file_path)) | ||||||
|  |         if not file_path.startswith(path.abspath(unquote(root_path))): | ||||||
|  |             error_logger.exception( | ||||||
|  |                 f"File not found: path={file_or_directory}, " | ||||||
|  |                 f"relative_url={__file_uri__}" | ||||||
|  |             ) | ||||||
|  |             raise FileNotFound( | ||||||
|                 "File not found", |                 "File not found", | ||||||
|                 path=file_or_directory, |                 path=file_or_directory, | ||||||
|                 relative_url=__file_uri__, |                 relative_url=__file_uri__, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         if __file_uri__: |  | ||||||
|             # Strip all / that in the beginning of the URL to help prevent |  | ||||||
|             # python from herping a derp and treating the uri as an |  | ||||||
|             # absolute path |  | ||||||
|             unquoted_file_uri = unquote(__file_uri__).lstrip("/") |  | ||||||
|             file_path_raw = Path(file_or_directory, unquoted_file_uri) |  | ||||||
|             file_path = file_path_raw.resolve() |  | ||||||
|             if ( |  | ||||||
|                 file_path < root_path and not file_path_raw.is_symlink() |  | ||||||
|             ) or ".." in file_path_raw.parts: |  | ||||||
|                 error_logger.exception( |  | ||||||
|                     f"File not found: path={file_or_directory}, " |  | ||||||
|                     f"relative_url={__file_uri__}" |  | ||||||
|                 ) |  | ||||||
|                 raise not_found |  | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             file_path.relative_to(root_path) |  | ||||||
|         except ValueError: |  | ||||||
|             if not file_path_raw.is_symlink(): |  | ||||||
|                 error_logger.exception( |  | ||||||
|                     f"File not found: path={file_or_directory}, " |  | ||||||
|                     f"relative_url={__file_uri__}" |  | ||||||
|                 ) |  | ||||||
|                 raise not_found |  | ||||||
|         try: |         try: | ||||||
|             headers = {} |             headers = {} | ||||||
|             # Check if the client has been sent this file before |             # Check if the client has been sent this file before | ||||||
| @@ -870,7 +811,11 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|         except ContentRangeError: |         except ContentRangeError: | ||||||
|             raise |             raise | ||||||
|         except FileNotFoundError: |         except FileNotFoundError: | ||||||
|             raise not_found |             raise FileNotFound( | ||||||
|  |                 "File not found", | ||||||
|  |                 path=file_or_directory, | ||||||
|  |                 relative_url=__file_uri__, | ||||||
|  |             ) | ||||||
|         except Exception: |         except Exception: | ||||||
|             error_logger.exception( |             error_logger.exception( | ||||||
|                 f"Exception in static request handler: " |                 f"Exception in static request handler: " | ||||||
| @@ -973,16 +918,19 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|  |  | ||||||
|         return route |         return route | ||||||
|  |  | ||||||
|     def _determine_error_format(self, handler) -> str: |     def _determine_error_format(self, handler) -> Optional[str]: | ||||||
|         with suppress(OSError, TypeError): |         if not isinstance(handler, CompositionView): | ||||||
|  |             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 "" |         return None | ||||||
|  |  | ||||||
|     def _get_response_types(self, node): |     def _get_response_types(self, node): | ||||||
|         types = set() |         types = set() | ||||||
| @@ -991,18 +939,7 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|             def visit_Return(self, node: Return) -> Any: |             def visit_Return(self, node: Return) -> Any: | ||||||
|                 nonlocal types |                 nonlocal types | ||||||
|  |  | ||||||
|                 with suppress(AttributeError): |                 try: | ||||||
|                     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 += [ | ||||||
| @@ -1014,32 +951,9 @@ class RouteMixin(metaclass=SanicMeta): | |||||||
|                     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,14 +1,17 @@ | |||||||
| 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 SignalMixin(metaclass=SanicMeta): | class HashableDict(dict): | ||||||
|  |     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() | ||||||
|  |  | ||||||
| @@ -21,7 +24,6 @@ class SignalMixin(metaclass=SanicMeta): | |||||||
|         *, |         *, | ||||||
|         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: | ||||||
| @@ -34,22 +36,17 @@ class SignalMixin(metaclass=SanicMeta): | |||||||
|  |  | ||||||
|         :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 {}), exclusive |                 handler, event_value, HashableDict(condition or {}) | ||||||
|             ) |             ) | ||||||
|             self._future_signals.add(future_signal) |             self._future_signals.add(future_signal) | ||||||
|  |  | ||||||
| @@ -65,7 +62,6 @@ class SignalMixin(metaclass=SanicMeta): | |||||||
|         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: | ||||||
|  |  | ||||||
| @@ -73,9 +69,7 @@ class SignalMixin(metaclass=SanicMeta): | |||||||
|                 ... |                 ... | ||||||
|  |  | ||||||
|             handler = noop |             handler = noop | ||||||
|         self.signal(event=event, condition=condition, exclusive=exclusive)( |         self.signal(event=event, condition=condition)(handler) | ||||||
|             handler |  | ||||||
|         ) |  | ||||||
|         return handler |         return handler | ||||||
|  |  | ||||||
|     def event(self, event: str): |     def event(self, event: str): | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ from sanic.models.handler_types import ( | |||||||
|     MiddlewareType, |     MiddlewareType, | ||||||
|     SignalHandler, |     SignalHandler, | ||||||
| ) | ) | ||||||
| from sanic.types import HashableDict |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class FutureRoute(NamedTuple): | class FutureRoute(NamedTuple): | ||||||
| @@ -26,7 +25,6 @@ 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): | ||||||
| @@ -62,7 +60,6 @@ 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:  # no cov | if TYPE_CHECKING: | ||||||
|     from sanic.server import ConnInfo |     from sanic.server import ConnInfo | ||||||
|     from sanic.app import Sanic |     from sanic.app import Sanic | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| 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 | ||||||
| @@ -14,10 +12,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 | ||||||
| @@ -30,10 +28,6 @@ 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 | ||||||
| @@ -142,6 +136,95 @@ 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. | ||||||
| @@ -336,109 +419,6 @@ 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, | ||||||
| @@ -447,7 +427,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, | ||||||
| ) -> ResponseStream: | ) -> StreamingHTTPResponse: | ||||||
|     """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. | ||||||
| @@ -455,6 +435,7 @@ 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 {} | ||||||
| @@ -490,24 +471,23 @@ async def file_stream( | |||||||
|                         break |                         break | ||||||
|                     await response.write(content) |                     await response.write(content) | ||||||
|  |  | ||||||
|     return ResponseStream( |     return StreamingHTTPResponse( | ||||||
|         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: Callable[ |     streaming_fn: StreamingFunction, | ||||||
|         [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 a coroutine `streaming_fn` which can be used to |     """Accepts an coroutine `streaming_fn` which can be used to | ||||||
|     write chunks to a streaming response. Returns a `ResponseStream`. |     write chunks to a streaming response. Returns a `StreamingHTTPResponse`. | ||||||
|  |  | ||||||
|     Example usage:: |     Example usage:: | ||||||
|  |  | ||||||
| @@ -521,13 +501,42 @@ 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 status: HTTP status. |     :param mime_type: Specific mime_type. | ||||||
|     :param content_type: Specific content_type. |  | ||||||
|     :param headers: Custom Headers. |     :param headers: Custom Headers. | ||||||
|  |     :param chunked: Deprecated | ||||||
|     """ |     """ | ||||||
|     return ResponseStream( |     return StreamingHTTPResponse( | ||||||
|         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,10 +1,20 @@ | |||||||
|  | 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", | ||||||
| @@ -13,5 +23,4 @@ __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): | ||||||
|         deprecation( |         warn( | ||||||
|             "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.", | ||||||
|             22.6, |             DeprecationWarning, | ||||||
|         ) |         ) | ||||||
|         return self.app.state.is_started |         return self.app.state.is_started | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,49 +0,0 @@ | |||||||
| 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,11 +1,12 @@ | |||||||
| 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 deprecation, error_logger | from sanic.log import error_logger | ||||||
| from sanic.server import HttpProtocol | from sanic.server import HttpProtocol | ||||||
|  |  | ||||||
| from ..websockets.impl import WebsocketImplProtocol | from ..websockets.impl import WebsocketImplProtocol | ||||||
| @@ -16,14 +17,6 @@ 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, | ||||||
| @@ -42,24 +35,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 | ||||||
|             deprecation( |             warn( | ||||||
|                 "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.", | ||||||
|                 22.3, |                 DeprecationWarning, | ||||||
|             ) |             ) | ||||||
|         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 | ||||||
|             deprecation( |             warn( | ||||||
|                 "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.", | ||||||
|                 22.3, |                 DeprecationWarning, | ||||||
|             ) |             ) | ||||||
|         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 | ||||||
|             deprecation( |             warn( | ||||||
|                 "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.", | ||||||
|                 22.3, |                 DeprecationWarning, | ||||||
|             ) |             ) | ||||||
|         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,7 +1,5 @@ | |||||||
| 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 | ||||||
|  |  | ||||||
| @@ -21,7 +19,6 @@ 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 | ||||||
| @@ -117,7 +114,6 @@ 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, | ||||||
| @@ -178,9 +174,6 @@ 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, cast | from typing import Any, Dict, List, Optional, Tuple, Union | ||||||
|  |  | ||||||
| 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,21 +142,12 @@ class SignalRouter(BaseRouter): | |||||||
|         if context: |         if context: | ||||||
|             params.update(context) |             params.update(context) | ||||||
|  |  | ||||||
|         signals = group.routes |  | ||||||
|         if not reverse: |         if not reverse: | ||||||
|             signals = signals[::-1] |             handlers = handlers[::-1] | ||||||
|         try: |         try: | ||||||
|             for signal in signals: |             for handler in handlers: | ||||||
|                 params.pop("__trigger__", None) |                 if condition is None or condition == handler.__requirements__: | ||||||
|                 if ( |                     maybe_coroutine = handler(**params) | ||||||
|                     (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: | ||||||
| @@ -199,36 +190,23 @@ 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 |  | ||||||
|  |  | ||||||
|         signal = super().add( |         return 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__") | ||||||
|  |  | ||||||
| @@ -260,9 +238,3 @@ 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,10 +1,9 @@ | |||||||
| 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(SanicMeta): | class TouchUpMeta(type): | ||||||
|     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) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +0,0 @@ | |||||||
| from .hashable_dict import HashableDict |  | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = ("HashableDict",) |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| class HashableDict(dict): |  | ||||||
|     def __hash__(self): |  | ||||||
|         return hash(tuple(sorted(self.items()))) |  | ||||||
| @@ -9,11 +9,14 @@ 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:  # no cov | if TYPE_CHECKING: | ||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|     from sanic.blueprints import Blueprint |     from sanic.blueprints import Blueprint | ||||||
|  |  | ||||||
| @@ -81,8 +84,6 @@ 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 | ||||||
| @@ -135,3 +136,48 @@ 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,19 +7,22 @@ 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, try_use_uvloop | from sanic.server import HttpProtocol, Signal, serve | ||||||
| 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:  # no cov | except ImportError: | ||||||
|     ssl = None  # type: ignore |     ssl = None  # type: ignore | ||||||
|  |  | ||||||
| if UVLOOP_INSTALLED:  # no cov | try: | ||||||
|     try_use_uvloop() |     import uvloop  # type: ignore | ||||||
|  |  | ||||||
|  |     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,7 +147,6 @@ 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,10 +6,8 @@ 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 List, Tuple | from typing import Callable, List, Tuple | ||||||
| from unittest.mock import MagicMock |  | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| @@ -186,21 +184,3 @@ 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,4 +1,5 @@ | |||||||
| 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 | ||||||
| @@ -8,7 +9,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("FakeServer", log_config=LOGGING_CONFIG) | app = Sanic(__name__, log_config=LOGGING_CONFIG) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/") | @app.get("/") | ||||||
|   | |||||||
| @@ -2,20 +2,15 @@ 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 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -24,6 +19,15 @@ 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): | ||||||
| @@ -393,22 +397,6 @@ 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") | ||||||
| @@ -420,13 +408,16 @@ def test_app_no_registry_env(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_set_attribute_warning(app): | def test_app_set_attribute_warning(app): | ||||||
|     message = ( |     with pytest.warns(DeprecationWarning) as record: | ||||||
|         "Setting variables on Sanic instances is not allowed. You should " |  | ||||||
|         "change your Sanic instance to use instance.ctx.foo instead." |  | ||||||
|     ) |  | ||||||
|     with pytest.raises(AttributeError, match=message): |  | ||||||
|         app.foo = 1 |         app.foo = 1 | ||||||
|  |  | ||||||
|  |     assert len(record) == 1 | ||||||
|  |     assert record[0].message.args[0] == ( | ||||||
|  |         "Setting variables on Sanic instances is deprecated " | ||||||
|  |         "and will be removed in version 21.12. You should change your " | ||||||
|  |         "Sanic instance to use instance.ctx.foo instead." | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_set_context(app): | def test_app_set_context(app): | ||||||
|     app.ctx.foo = 1 |     app.ctx.foo = 1 | ||||||
| @@ -447,7 +438,15 @@ 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 " | ||||||
|             "env_prefix" |             "load_env or env_prefix" | ||||||
|  |         ), | ||||||
|  |     ): | ||||||
|  |         Sanic("test", config=1, load_env=1) | ||||||
|  |     with pytest.raises( | ||||||
|  |         SanicException, | ||||||
|  |         match=( | ||||||
|  |             "When instantiating Sanic with config, you cannot also pass " | ||||||
|  |             "load_env or env_prefix" | ||||||
|         ), |         ), | ||||||
|     ): |     ): | ||||||
|         Sanic("test", config=1, env_prefix=1) |         Sanic("test", config=1, env_prefix=1) | ||||||
| @@ -473,98 +472,6 @@ 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,37 +145,6 @@ 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,7 +1,6 @@ | |||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic import Blueprint, Sanic | from sanic import Blueprint, Sanic | ||||||
| from sanic.exceptions import SanicException |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture | @pytest.fixture | ||||||
| @@ -80,18 +79,24 @@ 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 an invalid format. Names must " |         f"Sanic instance named '{name}' uses a format that isdeprecated. " | ||||||
|         "begin with a character and may only contain alphanumeric " |         "Starting in version 21.12, Sanic objects must be named only using " | ||||||
|         "characters, _, or -." |         "alphanumeric characters, _, or -." | ||||||
|     ) |     ) | ||||||
|     bp_message = ( |     bp_message = ( | ||||||
|         f"Blueprint instance named '{name}' uses an invalid format. Names " |         f"Blueprint instance named '{name}' uses a format that isdeprecated. " | ||||||
|         "must begin with a character and may only contain alphanumeric " |         "Starting in version 21.12, Blueprint objects must be named only using " | ||||||
|         "characters, _, or -." |         "alphanumeric characters, _, or -." | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     with pytest.raises(SanicException, match=app_message): |     with pytest.warns(DeprecationWarning) as app_e: | ||||||
|         Sanic(name) |         app = Sanic(name) | ||||||
|  |  | ||||||
|     with pytest.raises(SanicException, match=bp_message): |     with pytest.warns(DeprecationWarning) as bp_e: | ||||||
|         Blueprint(name) |         bp = Blueprint(name) | ||||||
|  |  | ||||||
|  |     assert app.name == name | ||||||
|  |     assert bp.name == name | ||||||
|  |  | ||||||
|  |     assert app_e[0].message.args[0] == app_message | ||||||
|  |     assert bp_e[0].message.args[0] == bp_message | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ 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 | ||||||
|  |  | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
| @@ -832,7 +833,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())  # type: ignore |     current_file = inspect.getfile(inspect.currentframe()) | ||||||
|     with open(current_file, "rb") as file: |     with open(current_file, "rb") as file: | ||||||
|         file.read() |         file.read() | ||||||
|  |  | ||||||
| @@ -861,6 +862,31 @@ 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() | ||||||
|  |  | ||||||
| @@ -1053,13 +1079,16 @@ def test_blueprint_registered_multiple_apps(): | |||||||
|  |  | ||||||
| def test_bp_set_attribute_warning(): | def test_bp_set_attribute_warning(): | ||||||
|     bp = Blueprint("bp") |     bp = Blueprint("bp") | ||||||
|     message = ( |     with pytest.warns(DeprecationWarning) as record: | ||||||
|         "Setting variables on Blueprint instances is not allowed. You should " |  | ||||||
|         "change your Blueprint instance to use instance.ctx.foo instead." |  | ||||||
|     ) |  | ||||||
|     with pytest.raises(AttributeError, match=message): |  | ||||||
|         bp.foo = 1 |         bp.foo = 1 | ||||||
|  |  | ||||||
|  |     assert len(record) == 1 | ||||||
|  |     assert record[0].message.args[0] == ( | ||||||
|  |         "Setting variables on Blueprint instances is deprecated " | ||||||
|  |         "and will be removed in version 21.12. You should change your " | ||||||
|  |         "Blueprint instance to use instance.ctx.foo instead." | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_early_registration(app): | def test_early_registration(app): | ||||||
|     assert len(app.router.routes) == 0 |     assert len(app.router.routes) == 0 | ||||||
|   | |||||||
| @@ -32,12 +32,6 @@ 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", | ||||||
|     ( |     ( | ||||||
| @@ -205,7 +199,9 @@ 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 | ||||||
| @@ -216,7 +212,9 @@ 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 | ||||||
| @@ -229,7 +227,9 @@ 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,6 +254,8 @@ 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,11 +1,9 @@ | |||||||
| 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, call | from unittest.mock import Mock | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| @@ -34,11 +32,6 @@ 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 | ||||||
| @@ -81,6 +74,26 @@ 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" | ||||||
| @@ -89,6 +102,20 @@ 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_") | ||||||
| @@ -110,32 +137,6 @@ 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( | ||||||
|         """ |         """ | ||||||
| @@ -385,24 +386,5 @@ 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({"FOO": 6}, {"BAR": 7}) |     app.config.update_config({"FOO": 6}) | ||||||
|     post_set.assert_has_calls( |     post_set.assert_called_once_with("FOO", 6) | ||||||
|         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,12 +1,7 @@ | |||||||
| 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 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -53,60 +48,3 @@ 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") |  | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| 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,20 +280,40 @@ 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" | ||||||
|  |  | ||||||
|     _, 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_setting_fallback_on_config_changes_as_expected(app): | def test_setting_fallback_to_non_default_raise_warning(app): | ||||||
|     app.error_handler = ErrorHandler() |     app.error_handler = ErrorHandler(fallback="text") | ||||||
|  |  | ||||||
|     _, response = app.test_client.get("/error") |     assert app.error_handler.fallback == "text" | ||||||
|     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") |  | ||||||
|     assert response.content_type == "text/plain; charset=utf-8" |     with pytest.warns( | ||||||
|  |         UserWarning, | ||||||
|  |         match=( | ||||||
|  |             "Overriding non-default ErrorHandler fallback value. " | ||||||
|  |             "Changing from text to json." | ||||||
|  |         ), | ||||||
|  |     ): | ||||||
|  |         app.config.FALLBACK_ERROR_FORMAT = "json" | ||||||
|  |  | ||||||
|  |     assert app.error_handler.fallback == "json" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_allow_fallback_error_format_in_config_injection(): | def test_allow_fallback_error_format_in_config_injection(): | ||||||
| @@ -307,6 +327,7 @@ 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" | ||||||
|  |  | ||||||
| @@ -318,39 +339,6 @@ 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,6 +13,7 @@ from sanic.exceptions import ( | |||||||
|     SanicException, |     SanicException, | ||||||
|     ServerError, |     ServerError, | ||||||
|     Unauthorized, |     Unauthorized, | ||||||
|  |     abort, | ||||||
| ) | ) | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  |  | ||||||
| @@ -87,6 +88,10 @@ 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) | ||||||
| @@ -234,6 +239,11 @@ 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): | ||||||
| @@ -252,7 +262,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(__name__) |     app = Sanic(__file__) | ||||||
|  |  | ||||||
|     @app.websocket("/feed") |     @app.websocket("/feed") | ||||||
|     async def feed(request, ws): |     async def feed(request, ws): | ||||||
| @@ -261,13 +271,9 @@ 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") | ||||||
|  |  | ||||||
|     for record in caplog.record_tuples: |     error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"] | ||||||
|         if record[2].startswith("Exception occurred"): |     assert error_logs[1][1] == logging.ERROR | ||||||
|             break |     assert "Exception occurred while handling uri:" in error_logs[1][2] | ||||||
|  |  | ||||||
|     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,12 +226,11 @@ def test_single_arg_exception_handler_notice( | |||||||
|     exception_handler_app.error_handler = CustomErrorHandler() |     exception_handler_app.error_handler = CustomErrorHandler() | ||||||
|  |  | ||||||
|     message = ( |     message = ( | ||||||
|         "[DEPRECATION v22.3] You are using a deprecated error handler. The " |         "You are using a deprecated error handler. The lookup method should " | ||||||
|         "lookup method should accept two positional parameters: (exception, " |         "accept two positional parameters: (exception, route_name: " | ||||||
|         "route_name: Optional[str]). Until you upgrade your " |         "Optional[str]). Until you upgrade your ErrorHandler.lookup, " | ||||||
|         "ErrorHandler.lookup, Blueprint specific exceptions will not work " |         "Blueprint specific exceptions will not work properly. Beginning in " | ||||||
|         "properly. Beginning in v22.3, the legacy style lookup method will " |         "v22.3, the legacy style lookup method will not work at all." | ||||||
|         "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") | ||||||
|   | |||||||
| @@ -1,84 +0,0 @@ | |||||||
| 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