Compare commits
	
		
			31 Commits
		
	
	
		
			v23.3.0
			...
			motd-fixes
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 44bf7ba79a | ||
|   | 9e7ca10c52 | ||
|   | fe32f4eb74 | ||
|   | ebe29d3d26 | ||
|   | f651f7436f | ||
|   | 16256522f6 | ||
|   | 205795d1e8 | ||
|   | 9cbe1fb8ad | ||
|   | 31d7ba8f8c | ||
|   | dc3c4d1393 | ||
|   | 929d270569 | ||
|   | 93714df051 | ||
|   | 6e61eab872 | ||
|   | 6848ff24d8 | ||
|   | 666371bb92 | ||
|   | 4a2b82e42e | ||
|   | 5dd1623192 | ||
|   | 976da69e79 | ||
|   | 11a0b15194 | ||
|   | c21999a248 | ||
|   | c17230ef94 | ||
|   | 049983cb70 | ||
|   | e374409567 | ||
|   | 4068a0d83d | ||
|   | 70da5e9879 | ||
|   | f48506d620 | ||
|   | f2cc83c1ba | ||
|   | 273825dab6 | ||
|   | 9a7dafd531 | ||
|   | 50117d174c | ||
|   | af67801062 | 
							
								
								
									
										16
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,14 @@ body: | |||||||
|     id: code |     id: code | ||||||
|     attributes: |     attributes: | ||||||
|       label: Code snippet |       label: Code snippet | ||||||
|       description: Relevant source code, make sure to remove what is not necessary. |       description: | | ||||||
|  |           Relevant source code, make sure to remove what is not necessary. Please try and format your code so that it is easier to read. For example: | ||||||
|  |  | ||||||
|  |               ```python | ||||||
|  |               from sanic import Sanic | ||||||
|  |  | ||||||
|  |               app = Sanic("Example") | ||||||
|  |               ``` | ||||||
|     validations: |     validations: | ||||||
|       required: false |       required: false | ||||||
|   - type: textarea |   - type: textarea | ||||||
| @@ -42,11 +49,16 @@ body: | |||||||
|         - ASGI |         - ASGI | ||||||
|     validations: |     validations: | ||||||
|       required: true |       required: true | ||||||
|   - type: input |   - type: dropdown | ||||||
|     id: os |     id: os | ||||||
|     attributes: |     attributes: | ||||||
|       label: Operating System |       label: Operating System | ||||||
|       description: What OS? |       description: What OS? | ||||||
|  |       options: | ||||||
|  |         - Linux | ||||||
|  |         - MacOS | ||||||
|  |         - Windows | ||||||
|  |         - Other (tell us in the description) | ||||||
|     validations: |     validations: | ||||||
|       required: true |       required: true | ||||||
|   - type: input |   - type: input | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,10 +4,12 @@ on: | |||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|   schedule: |   schedule: | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,12 +3,14 @@ on: | |||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     tags: |     tags: | ||||||
|       - "!*" # Do not execute on tags |       - "!*" # Do not execute on tags | ||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
| jobs: | jobs: | ||||||
|   test: |   test: | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| @@ -16,7 +17,6 @@ jobs: | |||||||
|       matrix: |       matrix: | ||||||
|         os: [ubuntu-latest] |         os: [ubuntu-latest] | ||||||
|         config: |         config: | ||||||
|           - { python-version: 3.7, tox-env: security} |  | ||||||
|           - { python-version: 3.8, tox-env: security} |           - { python-version: 3.8, tox-env: security} | ||||||
|           - { python-version: 3.9, tox-env: security} |           - { python-version: 3.9, tox-env: security} | ||||||
|           - { python-version: "3.10", tox-env: security} |           - { python-version: "3.10", tox-env: security} | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/pr-python-pypy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/pr-python-pypy.yml
									
									
									
									
										vendored
									
									
								
							| @@ -5,11 +5,11 @@ on: | |||||||
|       tox-env: |       tox-env: | ||||||
|         description: "Tox Env to run on the PyPy Infra" |         description: "Tox Env to run on the PyPy Infra" | ||||||
|         required: false |         required: false | ||||||
|         default: "pypy37" |         default: "pypy310" | ||||||
|       pypy-version: |       pypy-version: | ||||||
|         description: "Version of PyPy to use" |         description: "Version of PyPy to use" | ||||||
|         required: false |         required: false | ||||||
|         default: "pypy-3.7" |         default: "pypy-3.10" | ||||||
| jobs: | jobs: | ||||||
|   testPyPy: |   testPyPy: | ||||||
|     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} |     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python311.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python311.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										35
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,35 +0,0 @@ | |||||||
| name: Python 3.7 Tests |  | ||||||
| on: |  | ||||||
|   pull_request: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|       - "*LTS" |  | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   testPy37: |  | ||||||
|     if: github.event.pull_request.draft == false |  | ||||||
|     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} |  | ||||||
|     runs-on: ${{ matrix.os }} |  | ||||||
|     strategy: |  | ||||||
|       fail-fast: true |  | ||||||
|       matrix: |  | ||||||
|         #         os: [ubuntu-latest, macos-latest] |  | ||||||
|         os: [ubuntu-latest] |  | ||||||
|         config: |  | ||||||
|           - { python-version: 3.7, tox-env: py37 } |  | ||||||
|           - { python-version: 3.7, tox-env: py37-no-ext } |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout the Repository |  | ||||||
|         uses: actions/checkout@v2 |  | ||||||
|         id: checkout-branch |  | ||||||
|  |  | ||||||
|       - name: Run Unit Tests |  | ||||||
|         uses: harshanarayana/custom-actions@main |  | ||||||
|         with: |  | ||||||
|           python-version: ${{ matrix.config.python-version }} |  | ||||||
|           test-infra-tool: tox |  | ||||||
|           test-infra-version: latest |  | ||||||
|           action: tests |  | ||||||
|           test-additional-args: "-e=${{ matrix.config.tox-env }}" |  | ||||||
|           test-failure-retry: "3" |  | ||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| @@ -16,7 +17,6 @@ jobs: | |||||||
|       matrix: |       matrix: | ||||||
|         os: [ubuntu-latest] |         os: [ubuntu-latest] | ||||||
|         config: |         config: | ||||||
|           # - { 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} | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - current-release | ||||||
|       - "*LTS" |       - "*LTS" | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
| @@ -15,12 +16,10 @@ jobs: | |||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         config: |         config: | ||||||
|           - { python-version: 3.7, tox-env: py37-no-ext } |  | ||||||
|           - { python-version: 3.8, tox-env: py38-no-ext } |           - { python-version: 3.8, tox-env: py38-no-ext } | ||||||
|           - { python-version: 3.9, tox-env: py39-no-ext } |           - { python-version: 3.9, tox-env: py39-no-ext } | ||||||
|           - { python-version: "3.10", tox-env: py310-no-ext } |           - { python-version: "3.10", tox-env: py310-no-ext } | ||||||
|           - { python-version: "3.11", tox-env: py310-no-ext } |           - { python-version: "3.11", tox-env: py310-no-ext } | ||||||
|           - { python-version: pypy-3.7, tox-env: pypy37-no-ext } |  | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout Repository |       - name: Checkout Repository | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/publish-images.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/publish-images.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,7 @@ jobs: | |||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: true |       fail-fast: true | ||||||
|       matrix: |       matrix: | ||||||
|         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] |         python-version: ["3.8", "3.9", "3.10", "3.11"] | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								.github/workflows/publish-package.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										55
									
								
								.github/workflows/publish-package.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,28 +1,39 @@ | |||||||
| name: Publish Artifacts | name: Upload Python Package | ||||||
|  |  | ||||||
| on: | on: | ||||||
|   release: |   release: | ||||||
|     types: [created] |     types: [created] | ||||||
|  |   workflow_dispatch: | ||||||
| jobs: | jobs: | ||||||
|   publishPythonPackage: |   build-n-publish: | ||||||
|     name: Publishing Sanic Release Artifacts |     name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|  |  | ||||||
|     strategy: |  | ||||||
|       fail-fast: true |  | ||||||
|       matrix: |  | ||||||
|         python-version: ["3.10"] |  | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout Repository |     - uses: actions/checkout@v3 | ||||||
|         uses: actions/checkout@v2 |     - name: Set up Python | ||||||
|  |       uses: actions/setup-python@v4 | ||||||
|       - name: Publish Python Package |       with: | ||||||
|         uses: harshanarayana/custom-actions@main |         python-version: "3.x" | ||||||
|         with: |     - name: Install pypa/build | ||||||
|           python-version: ${{ matrix.python-version }} |       run: >- | ||||||
|           package-infra-name: "twine" |         python3 -m | ||||||
|           pypi-user: __token__ |         pip install | ||||||
|           pypi-access-token: ${{ secrets.PYPI_ACCESS_TOKEN }} |         build | ||||||
|           action: "package-publish" |         --user | ||||||
|           pypi-verify-metadata: "true" |     - name: Build a binary wheel and a source tarball | ||||||
|  |       run: >- | ||||||
|  |         python3 -m | ||||||
|  |         build | ||||||
|  |         --sdist | ||||||
|  |         --wheel | ||||||
|  |         --outdir dist/ | ||||||
|  |         . | ||||||
|  |     # - name: Publish distribution 📦 to Test PyPI | ||||||
|  |     #   uses: pypa/gh-action-pypi-publish@release/v1 | ||||||
|  |     #   with: | ||||||
|  |     #     password: ${{ secrets.SANIC_TEST_PYPI_API_TOKEN }} | ||||||
|  |     #     repository-url: https://test.pypi.org/legacy/ | ||||||
|  |     - name: Publish distribution 📦 to PyPI | ||||||
|  |       uses: pypa/gh-action-pypi-publish@release/v1 | ||||||
|  |       with: | ||||||
|  |         password: ${{ secrets.SANIC_PYPI_API_TOKEN }} | ||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -21,4 +21,5 @@ dist/* | |||||||
| pip-wheel-metadata/ | pip-wheel-metadata/ | ||||||
| .pytest_cache/* | .pytest_cache/* | ||||||
| .venv/* | .venv/* | ||||||
|  | venv/* | ||||||
| .vscode/* | .vscode/* | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
| | 🔷 In support release | | 🔷 In support release | ||||||
| | | | | ||||||
|  |  | ||||||
|  | .. mdinclude:: ./releases/23/23.6.md | ||||||
| .. mdinclude:: ./releases/23/23.3.md | .. mdinclude:: ./releases/23/23.3.md | ||||||
| .. mdinclude:: ./releases/22/22.12.md | .. mdinclude:: ./releases/22/22.12.md | ||||||
| .. mdinclude:: ./releases/22/22.9.md | .. mdinclude:: ./releases/22/22.9.md | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| ## Version 23.3.0 🔶 | ## Version 23.3.0 | ||||||
|  |  | ||||||
| ### Features | ### Features | ||||||
| - [#2545](https://github.com/sanic-org/sanic/pull/2545) Standardize init of exceptions for more consistent control of HTTP responses using exceptions | - [#2545](https://github.com/sanic-org/sanic/pull/2545) Standardize init of exceptions for more consistent control of HTTP responses using exceptions | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								docs/sanic/releases/23/23.6.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								docs/sanic/releases/23/23.6.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | ## Version 23.6.0  🔶 | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  | - [#2670](https://github.com/sanic-org/sanic/pull/2670) Increase `KEEP_ALIVE_TIMEOUT` default to 120 seconds | ||||||
|  | - [#2716](https://github.com/sanic-org/sanic/pull/2716) Adding allow route overwrite option in blueprint | ||||||
|  | - [#2724](https://github.com/sanic-org/sanic/pull/2724) and [#2792](https://github.com/sanic-org/sanic/pull/2792) Add a new exception signal for ALL exceptions raised anywhere in application | ||||||
|  | - [#2727](https://github.com/sanic-org/sanic/pull/2727) Add name prefixing to BP groups | ||||||
|  | - [#2754](https://github.com/sanic-org/sanic/pull/2754) Update request type on middleware types | ||||||
|  | - [#2770](https://github.com/sanic-org/sanic/pull/2770) Better exception message on startup time application induced import error | ||||||
|  | - [#2776](https://github.com/sanic-org/sanic/pull/2776) Set multiprocessing start method early | ||||||
|  | - [#2785](https://github.com/sanic-org/sanic/pull/2785) Add custom typing to config and ctx objects | ||||||
|  | - [#2790](https://github.com/sanic-org/sanic/pull/2790) Add `request.client_ip` | ||||||
|  |  | ||||||
|  | ### Bugfixes | ||||||
|  | - [#2728](https://github.com/sanic-org/sanic/pull/2728) Fix traversals for intended results | ||||||
|  | - [#2729](https://github.com/sanic-org/sanic/pull/2729) Handle case when headers argument of ResponseStream constructor is None | ||||||
|  | - [#2737](https://github.com/sanic-org/sanic/pull/2737) Fix type annotation for `JSONREsponse` default content type | ||||||
|  | - [#2740](https://github.com/sanic-org/sanic/pull/2740) Use Sanic's serializer for JSON responses in the Inspector | ||||||
|  | - [#2760](https://github.com/sanic-org/sanic/pull/2760) Support for `Request.get_current` in ASGI mode | ||||||
|  | - [#2773](https://github.com/sanic-org/sanic/pull/2773) Alow Blueprint routes to explicitly define error_format | ||||||
|  | - [#2774](https://github.com/sanic-org/sanic/pull/2774) Resolve headers on different renderers | ||||||
|  | - [#2782](https://github.com/sanic-org/sanic/pull/2782) Resolve pypy compatibility issues | ||||||
|  |  | ||||||
|  | ### Deprecations and Removals | ||||||
|  | - [#2777](https://github.com/sanic-org/sanic/pull/2777) Remove Python 3.7 support | ||||||
|  |  | ||||||
|  | ### Developer infrastructure | ||||||
|  | - [#2766](https://github.com/sanic-org/sanic/pull/2766) Unpin setuptools version | ||||||
|  | - [#2779](https://github.com/sanic-org/sanic/pull/2779) Run keep alive tests in loop to get available port | ||||||
|  |  | ||||||
|  | ### Improved Documentation | ||||||
|  | - [#2741](https://github.com/sanic-org/sanic/pull/2741) Better documentation examples about running Sanic | ||||||
|  | From that list, the items to highlight in the release notes: | ||||||
| @@ -25,5 +25,5 @@ def key_exist_handler(request): | |||||||
|  |  | ||||||
|     return text("num does not exist in request") |     return text("num does not exist in request") | ||||||
|  |  | ||||||
|  | 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) | ||||||
|   | |||||||
| @@ -50,4 +50,5 @@ def pop_handler(request): | |||||||
|  |  | ||||||
| app.blueprint(bp, url_prefix="/bp") | app.blueprint(bp, url_prefix="/bp") | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False) | if __name__ == "__main__": | ||||||
|  |     app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False) | ||||||
|   | |||||||
| @@ -37,4 +37,5 @@ app.blueprint(blueprint) | |||||||
| app.blueprint(blueprint2) | app.blueprint(blueprint2) | ||||||
| app.blueprint(blueprint3) | app.blueprint(blueprint3) | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=9999, debug=True) | if __name__ == "__main__": | ||||||
|  |     app.run(host="0.0.0.0", port=9999, debug=True) | ||||||
|   | |||||||
| @@ -69,5 +69,5 @@ async def runner(app: Sanic, app_server: AsyncioServer): | |||||||
|         app.is_running = False |         app.is_running = False | ||||||
|         app.is_stopping = True |         app.is_stopping = True | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
| https.run(port=HTTPS_PORT, debug=True) |     https.run(port=HTTPS_PORT, debug=True) | ||||||
|   | |||||||
| @@ -39,4 +39,5 @@ async def test(request): | |||||||
|         return json(response) |         return json(response) | ||||||
|  |  | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000, workers=2) | if __name__ == "__main__": | ||||||
|  |     app.run(host="0.0.0.0", port=8000, workers=2) | ||||||
|   | |||||||
| @@ -20,4 +20,5 @@ def test(request): | |||||||
|     return text("hey") |     return text("hey") | ||||||
|  |  | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000) | if __name__ == "__main__": | ||||||
|  |     app.run(host="0.0.0.0", port=8000) | ||||||
|   | |||||||
| @@ -6,5 +6,5 @@ data = "" | |||||||
| for i in range(1, 250000): | for i in range(1, 250000): | ||||||
|     data += str(i) |     data += str(i) | ||||||
|  |  | ||||||
| r = requests.post('http://0.0.0.0:8000/stream', data=data) | r = requests.post("http://0.0.0.0:8000/stream", data=data) | ||||||
| print(r.text) | print(r.text) | ||||||
|   | |||||||
| @@ -20,4 +20,5 @@ 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) | if __name__ == "__main__": | ||||||
|  |     app.run(host="0.0.0.0", port=8000) | ||||||
|   | |||||||
| @@ -35,34 +35,34 @@ async def after_server_stop(app, loop): | |||||||
| async def test(request): | async def test(request): | ||||||
|     return response.json({"answer": "42"}) |     return response.json({"answer": "42"}) | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     asyncio.set_event_loop(uvloop.new_event_loop()) | ||||||
|  |     serv_coro = app.create_server( | ||||||
|  |         host="0.0.0.0", port=8000, return_asyncio_server=True | ||||||
|  |     ) | ||||||
|  |     loop = asyncio.get_event_loop() | ||||||
|  |     serv_task = asyncio.ensure_future(serv_coro, loop=loop) | ||||||
|  |     signal(SIGINT, lambda s, f: loop.stop()) | ||||||
|  |     server: AsyncioServer = loop.run_until_complete(serv_task) | ||||||
|  |     loop.run_until_complete(server.startup()) | ||||||
|  |  | ||||||
| asyncio.set_event_loop(uvloop.new_event_loop()) |     # When using app.run(), this actually triggers before the serv_coro. | ||||||
| serv_coro = app.create_server( |     # But, in this example, we are using the convenience method, even if it is | ||||||
|     host="0.0.0.0", port=8000, return_asyncio_server=True |     # out of order. | ||||||
| ) |     loop.run_until_complete(server.before_start()) | ||||||
| loop = asyncio.get_event_loop() |     loop.run_until_complete(server.after_start()) | ||||||
| serv_task = asyncio.ensure_future(serv_coro, loop=loop) |     try: | ||||||
| signal(SIGINT, lambda s, f: loop.stop()) |         loop.run_forever() | ||||||
| server: AsyncioServer = loop.run_until_complete(serv_task) |     except KeyboardInterrupt: | ||||||
| loop.run_until_complete(server.startup()) |         loop.stop() | ||||||
|  |     finally: | ||||||
|  |         loop.run_until_complete(server.before_stop()) | ||||||
|  |  | ||||||
| # When using app.run(), this actually triggers before the serv_coro. |         # Wait for server to close | ||||||
| # But, in this example, we are using the convenience method, even if it is |         close_task = server.close() | ||||||
| # out of order. |         loop.run_until_complete(close_task) | ||||||
| loop.run_until_complete(server.before_start()) |  | ||||||
| loop.run_until_complete(server.after_start()) |  | ||||||
| try: |  | ||||||
|     loop.run_forever() |  | ||||||
| except KeyboardInterrupt: |  | ||||||
|     loop.stop() |  | ||||||
| finally: |  | ||||||
|     loop.run_until_complete(server.before_stop()) |  | ||||||
|  |  | ||||||
|     # Wait for server to close |         # Complete all tasks on the loop | ||||||
|     close_task = server.close() |         for connection in server.connections: | ||||||
|     loop.run_until_complete(close_task) |             connection.close_if_idle() | ||||||
|  |         loop.run_until_complete(server.after_stop()) | ||||||
|     # Complete all tasks on the loop |  | ||||||
|     for connection in server.connections: |  | ||||||
|         connection.close_if_idle() |  | ||||||
|     loop.run_until_complete(server.after_stop()) |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| [build-system] | [build-system] | ||||||
| requires = ["setuptools<60.0", "wheel"] | requires = ["setuptools", "wheel"] | ||||||
| build-backend = "setuptools.build_meta" | build-backend = "setuptools.build_meta" | ||||||
|  |  | ||||||
| [tool.black] | [tool.black] | ||||||
|   | |||||||
| @@ -1,6 +1,11 @@ | |||||||
|  | from types import SimpleNamespace | ||||||
|  |  | ||||||
|  | from typing_extensions import TypeAlias | ||||||
|  |  | ||||||
| from sanic.__version__ import __version__ | from sanic.__version__ import __version__ | ||||||
| from sanic.app import Sanic | from sanic.app import Sanic | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
|  | from sanic.config import Config | ||||||
| from sanic.constants import HTTPMethod | from sanic.constants import HTTPMethod | ||||||
| from sanic.exceptions import ( | from sanic.exceptions import ( | ||||||
|     BadRequest, |     BadRequest, | ||||||
| @@ -32,15 +37,29 @@ from sanic.response import ( | |||||||
| from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket | from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket | ||||||
|  |  | ||||||
|  |  | ||||||
|  | DefaultSanic: TypeAlias = "Sanic[Config, SimpleNamespace]" | ||||||
|  | """ | ||||||
|  | A type alias for a Sanic app with a default config and namespace. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | DefaultRequest: TypeAlias = Request[DefaultSanic, SimpleNamespace] | ||||||
|  | """ | ||||||
|  | A type alias for a request with a default Sanic app and namespace. | ||||||
|  | """ | ||||||
|  |  | ||||||
| __all__ = ( | __all__ = ( | ||||||
|     "__version__", |     "__version__", | ||||||
|     # Common objects |     # Common objects | ||||||
|     "Sanic", |     "Sanic", | ||||||
|  |     "Config", | ||||||
|     "Blueprint", |     "Blueprint", | ||||||
|     "HTTPMethod", |     "HTTPMethod", | ||||||
|     "HTTPResponse", |     "HTTPResponse", | ||||||
|     "Request", |     "Request", | ||||||
|     "Websocket", |     "Websocket", | ||||||
|  |     # Common types | ||||||
|  |     "DefaultSanic", | ||||||
|  |     "DefaultRequest", | ||||||
|     # Common exceptions |     # Common exceptions | ||||||
|     "BadRequest", |     "BadRequest", | ||||||
|     "ExpectationFailed", |     "ExpectationFailed", | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "23.3.0" | __version__ = "23.6.0" | ||||||
|   | |||||||
							
								
								
									
										188
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										188
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -17,7 +17,7 @@ from asyncio import ( | |||||||
| from asyncio.futures import Future | from asyncio.futures import Future | ||||||
| from collections import defaultdict, deque | from collections import defaultdict, deque | ||||||
| from contextlib import contextmanager, suppress | from contextlib import contextmanager, suppress | ||||||
| from functools import partial | from functools import partial, wraps | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from os import environ | from os import environ | ||||||
| from socket import socket | from socket import socket | ||||||
| @@ -29,9 +29,11 @@ from typing import ( | |||||||
|     AnyStr, |     AnyStr, | ||||||
|     Awaitable, |     Awaitable, | ||||||
|     Callable, |     Callable, | ||||||
|  |     ClassVar, | ||||||
|     Coroutine, |     Coroutine, | ||||||
|     Deque, |     Deque, | ||||||
|     Dict, |     Dict, | ||||||
|  |     Generic, | ||||||
|     Iterable, |     Iterable, | ||||||
|     Iterator, |     Iterator, | ||||||
|     List, |     List, | ||||||
| @@ -41,6 +43,8 @@ from typing import ( | |||||||
|     Type, |     Type, | ||||||
|     TypeVar, |     TypeVar, | ||||||
|     Union, |     Union, | ||||||
|  |     cast, | ||||||
|  |     overload, | ||||||
| ) | ) | ||||||
| from urllib.parse import urlencode, urlunparse | from urllib.parse import urlencode, urlunparse | ||||||
|  |  | ||||||
| @@ -83,7 +87,7 @@ from sanic.request import Request | |||||||
| from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream | from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream | ||||||
| from sanic.router import Router | from sanic.router import Router | ||||||
| from sanic.server.websockets.impl import ConnectionClosed | from sanic.server.websockets.impl import ConnectionClosed | ||||||
| from sanic.signals import Signal, SignalRouter | from sanic.signals import Event, Signal, SignalRouter | ||||||
| from sanic.touchup import TouchUp, TouchUpMeta | from sanic.touchup import TouchUp, TouchUpMeta | ||||||
| from sanic.types.shared_ctx import SharedContext | from sanic.types.shared_ctx import SharedContext | ||||||
| from sanic.worker.inspector import Inspector | from sanic.worker.inspector import Inspector | ||||||
| @@ -102,8 +106,17 @@ if TYPE_CHECKING: | |||||||
| if OS_IS_WINDOWS:  # no cov | if OS_IS_WINDOWS:  # no cov | ||||||
|     enable_windows_color_support() |     enable_windows_color_support() | ||||||
|  |  | ||||||
|  | ctx_type = TypeVar("ctx_type") | ||||||
|  | config_type = TypeVar("config_type", bound=Config) | ||||||
|  |  | ||||||
| class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): |  | ||||||
|  | class Sanic( | ||||||
|  |     Generic[config_type, ctx_type], | ||||||
|  |     StaticHandleMixin, | ||||||
|  |     BaseSanic, | ||||||
|  |     StartupMixin, | ||||||
|  |     metaclass=TouchUpMeta, | ||||||
|  | ): | ||||||
|     """ |     """ | ||||||
|     The main application instance |     The main application instance | ||||||
|     """ |     """ | ||||||
| @@ -158,14 +171,102 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         "websocket_tasks", |         "websocket_tasks", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     _app_registry: Dict[str, "Sanic"] = {} |     _app_registry: ClassVar[Dict[str, "Sanic"]] = {} | ||||||
|     test_mode = False |     test_mode: ClassVar[bool] = False | ||||||
|  |  | ||||||
|  |     @overload | ||||||
|  |     def __init__( | ||||||
|  |         self: Sanic[Config, SimpleNamespace], | ||||||
|  |         name: str, | ||||||
|  |         config: None = None, | ||||||
|  |         ctx: None = None, | ||||||
|  |         router: Optional[Router] = None, | ||||||
|  |         signal_router: Optional[SignalRouter] = None, | ||||||
|  |         error_handler: Optional[ErrorHandler] = None, | ||||||
|  |         env_prefix: Optional[str] = SANIC_PREFIX, | ||||||
|  |         request_class: Optional[Type[Request]] = None, | ||||||
|  |         strict_slashes: bool = False, | ||||||
|  |         log_config: Optional[Dict[str, Any]] = None, | ||||||
|  |         configure_logging: bool = True, | ||||||
|  |         dumps: Optional[Callable[..., AnyStr]] = None, | ||||||
|  |         loads: Optional[Callable[..., Any]] = None, | ||||||
|  |         inspector: bool = False, | ||||||
|  |         inspector_class: Optional[Type[Inspector]] = None, | ||||||
|  |         certloader_class: Optional[Type[CertLoader]] = None, | ||||||
|  |     ) -> None: | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |     @overload | ||||||
|  |     def __init__( | ||||||
|  |         self: Sanic[config_type, SimpleNamespace], | ||||||
|  |         name: str, | ||||||
|  |         config: Optional[config_type] = None, | ||||||
|  |         ctx: None = None, | ||||||
|  |         router: Optional[Router] = None, | ||||||
|  |         signal_router: Optional[SignalRouter] = None, | ||||||
|  |         error_handler: Optional[ErrorHandler] = None, | ||||||
|  |         env_prefix: Optional[str] = SANIC_PREFIX, | ||||||
|  |         request_class: Optional[Type[Request]] = None, | ||||||
|  |         strict_slashes: bool = False, | ||||||
|  |         log_config: Optional[Dict[str, Any]] = None, | ||||||
|  |         configure_logging: bool = True, | ||||||
|  |         dumps: Optional[Callable[..., AnyStr]] = None, | ||||||
|  |         loads: Optional[Callable[..., Any]] = None, | ||||||
|  |         inspector: bool = False, | ||||||
|  |         inspector_class: Optional[Type[Inspector]] = None, | ||||||
|  |         certloader_class: Optional[Type[CertLoader]] = None, | ||||||
|  |     ) -> None: | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |     @overload | ||||||
|  |     def __init__( | ||||||
|  |         self: Sanic[Config, ctx_type], | ||||||
|  |         name: str, | ||||||
|  |         config: None = None, | ||||||
|  |         ctx: Optional[ctx_type] = None, | ||||||
|  |         router: Optional[Router] = None, | ||||||
|  |         signal_router: Optional[SignalRouter] = None, | ||||||
|  |         error_handler: Optional[ErrorHandler] = None, | ||||||
|  |         env_prefix: Optional[str] = SANIC_PREFIX, | ||||||
|  |         request_class: Optional[Type[Request]] = None, | ||||||
|  |         strict_slashes: bool = False, | ||||||
|  |         log_config: Optional[Dict[str, Any]] = None, | ||||||
|  |         configure_logging: bool = True, | ||||||
|  |         dumps: Optional[Callable[..., AnyStr]] = None, | ||||||
|  |         loads: Optional[Callable[..., Any]] = None, | ||||||
|  |         inspector: bool = False, | ||||||
|  |         inspector_class: Optional[Type[Inspector]] = None, | ||||||
|  |         certloader_class: Optional[Type[CertLoader]] = None, | ||||||
|  |     ) -> None: | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |     @overload | ||||||
|  |     def __init__( | ||||||
|  |         self: Sanic[config_type, ctx_type], | ||||||
|  |         name: str, | ||||||
|  |         config: Optional[config_type] = None, | ||||||
|  |         ctx: Optional[ctx_type] = None, | ||||||
|  |         router: Optional[Router] = None, | ||||||
|  |         signal_router: Optional[SignalRouter] = None, | ||||||
|  |         error_handler: Optional[ErrorHandler] = None, | ||||||
|  |         env_prefix: Optional[str] = SANIC_PREFIX, | ||||||
|  |         request_class: Optional[Type[Request]] = None, | ||||||
|  |         strict_slashes: bool = False, | ||||||
|  |         log_config: Optional[Dict[str, Any]] = None, | ||||||
|  |         configure_logging: bool = True, | ||||||
|  |         dumps: Optional[Callable[..., AnyStr]] = None, | ||||||
|  |         loads: Optional[Callable[..., Any]] = None, | ||||||
|  |         inspector: bool = False, | ||||||
|  |         inspector_class: Optional[Type[Inspector]] = None, | ||||||
|  |         certloader_class: Optional[Type[CertLoader]] = None, | ||||||
|  |     ) -> None: | ||||||
|  |         ... | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         name: Optional[str] = None, |         name: str, | ||||||
|         config: Optional[Config] = None, |         config: Optional[config_type] = None, | ||||||
|         ctx: Optional[Any] = None, |         ctx: Optional[ctx_type] = None, | ||||||
|         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, | ||||||
| @@ -193,7 +294,9 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # First setup config |         # First setup config | ||||||
|         self.config: Config = config or Config(env_prefix=env_prefix) |         self.config: config_type = cast( | ||||||
|  |             config_type, config or Config(env_prefix=env_prefix) | ||||||
|  |         ) | ||||||
|         if inspector: |         if inspector: | ||||||
|             self.config.INSPECTOR = inspector |             self.config.INSPECTOR = inspector | ||||||
|  |  | ||||||
| @@ -217,7 +320,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|             certloader_class or CertLoader |             certloader_class or CertLoader | ||||||
|         ) |         ) | ||||||
|         self.configure_logging: bool = configure_logging |         self.configure_logging: bool = configure_logging | ||||||
|         self.ctx: Any = ctx or SimpleNamespace() |         self.ctx: ctx_type = cast(ctx_type, ctx or SimpleNamespace()) | ||||||
|         self.error_handler: ErrorHandler = error_handler or ErrorHandler() |         self.error_handler: ErrorHandler = error_handler or ErrorHandler() | ||||||
|         self.inspector_class: Type[Inspector] = inspector_class or Inspector |         self.inspector_class: Type[Inspector] = inspector_class or Inspector | ||||||
|         self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list) |         self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list) | ||||||
| @@ -417,8 +520,11 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|     def _apply_listener(self, listener: FutureListener): |     def _apply_listener(self, listener: FutureListener): | ||||||
|         return self.register_listener(listener.listener, listener.event) |         return self.register_listener(listener.listener, listener.event) | ||||||
|  |  | ||||||
|     def _apply_route(self, route: FutureRoute) -> List[Route]: |     def _apply_route( | ||||||
|  |         self, route: FutureRoute, overwrite: bool = False | ||||||
|  |     ) -> List[Route]: | ||||||
|         params = route._asdict() |         params = route._asdict() | ||||||
|  |         params["overwrite"] = overwrite | ||||||
|         websocket = params.pop("websocket", False) |         websocket = params.pop("websocket", False) | ||||||
|         subprotocols = params.pop("subprotocols", None) |         subprotocols = params.pop("subprotocols", None) | ||||||
|  |  | ||||||
| @@ -499,6 +605,19 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|                 raise NotFound("Could not find signal %s" % event) |                 raise NotFound("Could not find signal %s" % event) | ||||||
|         return await wait_for(signal.ctx.event.wait(), timeout=timeout) |         return await wait_for(signal.ctx.event.wait(), timeout=timeout) | ||||||
|  |  | ||||||
|  |     def report_exception( | ||||||
|  |         self, handler: Callable[[Sanic, Exception], Coroutine[Any, Any, None]] | ||||||
|  |     ): | ||||||
|  |         @wraps(handler) | ||||||
|  |         async def report(exception: Exception) -> None: | ||||||
|  |             await handler(self, exception) | ||||||
|  |  | ||||||
|  |         self.add_signal( | ||||||
|  |             handler=report, event=Event.SERVER_EXCEPTION_REPORT.value | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return report | ||||||
|  |  | ||||||
|     def enable_websocket(self, enable=True): |     def enable_websocket(self, enable=True): | ||||||
|         """Enable or disable the support for websocket. |         """Enable or disable the support for websocket. | ||||||
|  |  | ||||||
| @@ -550,6 +669,9 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|                             ) |                             ) | ||||||
|                         else: |                         else: | ||||||
|                             params["version_prefix"] = blueprint.version_prefix |                             params["version_prefix"] = blueprint.version_prefix | ||||||
|  |                     name_prefix = getattr(blueprint, "name_prefix", None) | ||||||
|  |                     if name_prefix and "name_prefix" not in params: | ||||||
|  |                         params["name_prefix"] = name_prefix | ||||||
|                 self.blueprint(item, **params) |                 self.blueprint(item, **params) | ||||||
|             return |             return | ||||||
|         if blueprint.name in self.blueprints: |         if blueprint.name in self.blueprints: | ||||||
| @@ -767,6 +889,12 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         :raises ServerError: response 500 |         :raises ServerError: response 500 | ||||||
|         """ |         """ | ||||||
|         response = None |         response = None | ||||||
|  |         if not getattr(exception, "__dispatched__", False): | ||||||
|  |             ...  # DO NOT REMOVE THIS LINE. IT IS NEEDED FOR TOUCHUP. | ||||||
|  |             await self.dispatch( | ||||||
|  |                 "server.exception.report", | ||||||
|  |                 context={"exception": exception}, | ||||||
|  |             ) | ||||||
|         await self.dispatch( |         await self.dispatch( | ||||||
|             "http.lifecycle.exception", |             "http.lifecycle.exception", | ||||||
|             inline=True, |             inline=True, | ||||||
| @@ -1197,13 +1325,28 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         app, |         app, | ||||||
|         loop, |         loop, | ||||||
|     ): |     ): | ||||||
|         if callable(task): |         async def do(task): | ||||||
|             try: |             try: | ||||||
|                 task = task(app) |                 if callable(task): | ||||||
|             except TypeError: |                     try: | ||||||
|                 task = task() |                         task = task(app) | ||||||
|  |                     except TypeError: | ||||||
|  |                         task = task() | ||||||
|  |                 if isawaitable(task): | ||||||
|  |                     await task | ||||||
|  |             except CancelledError: | ||||||
|  |                 error_logger.warning( | ||||||
|  |                     f"Task {task} was cancelled before it completed." | ||||||
|  |                 ) | ||||||
|  |                 raise | ||||||
|  |             except Exception as e: | ||||||
|  |                 await app.dispatch( | ||||||
|  |                     "server.exception.report", | ||||||
|  |                     context={"exception": e}, | ||||||
|  |                 ) | ||||||
|  |                 raise | ||||||
|  |  | ||||||
|         return task |         return do(task) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _loop_add_task( |     def _loop_add_task( | ||||||
| @@ -1217,18 +1360,9 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|     ) -> Task: |     ) -> Task: | ||||||
|         if not isinstance(task, Future): |         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):  # no cov |             task = loop.create_task(prepped, name=name) | ||||||
|                 task = 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.get_name = lambda: name |  | ||||||
|             else: |  | ||||||
|                 task = loop.create_task(prepped, name=name) |  | ||||||
|  |  | ||||||
|         if name and register and sys.version_info > (3, 7): |         if name and register: | ||||||
|             app._task_registry[name] = task |             app._task_registry[name] = task | ||||||
|  |  | ||||||
|         return task |         return task | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import sys | |||||||
|  |  | ||||||
| from os import environ | from os import environ | ||||||
|  |  | ||||||
| from sanic.compat import is_atty | from sanic.helpers import is_atty | ||||||
|  |  | ||||||
|  |  | ||||||
| BASE_LOGO = """ | BASE_LOGO = """ | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from textwrap import indent, wrap | |||||||
| from typing import Dict, Optional | from typing import Dict, Optional | ||||||
|  |  | ||||||
| from sanic import __version__ | from sanic import __version__ | ||||||
| from sanic.compat import is_atty | from sanic.helpers import is_atty | ||||||
| from sanic.log import logger | from sanic.log import logger | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -73,6 +73,14 @@ class MOTDTTY(MOTD): | |||||||
|             self.value_width = min( |             self.value_width = min( | ||||||
|                 max(map(len, self.data.values())), self.max_value_width |                 max(map(len, self.data.values())), self.max_value_width | ||||||
|             ) |             ) | ||||||
|  |         if self.extra: | ||||||
|  |             self.key_width = max( | ||||||
|  |                 self.key_width, max(map(len, self.extra.keys())) | ||||||
|  |             ) | ||||||
|  |             self.value_width = min( | ||||||
|  |                 max((*map(len, self.extra.values()), self.value_width)), | ||||||
|  |                 self.max_value_width, | ||||||
|  |             ) | ||||||
|         self.logo_lines = self.logo.split("\n") if self.logo else [] |         self.logo_lines = self.logo.split("\n") if self.logo else [] | ||||||
|         self.logo_line_length = 24 |         self.logo_line_length = 24 | ||||||
|         self.centering_length = ( |         self.centering_length = ( | ||||||
| @@ -104,7 +112,7 @@ class MOTDTTY(MOTD): | |||||||
|         self._render_data(lines, self.data, 0) |         self._render_data(lines, self.data, 0) | ||||||
|         if self.extra: |         if self.extra: | ||||||
|             logo_part = self._get_logo_part(len(lines) - 4) |             logo_part = self._get_logo_part(len(lines) - 4) | ||||||
|             lines.append(f"| {logo_part} ├{display_filler}┤") |             lines.append(f"│ {logo_part} ├{display_filler}┤") | ||||||
|             self._render_data(lines, self.extra, len(lines) - 4) |             self._render_data(lines, self.extra, len(lines) - 4) | ||||||
|  |  | ||||||
|         self._render_fill(lines) |         self._render_fill(lines) | ||||||
|   | |||||||
| @@ -175,6 +175,7 @@ class ASGIApp: | |||||||
|             instance.transport, |             instance.transport, | ||||||
|             sanic_app, |             sanic_app, | ||||||
|         ) |         ) | ||||||
|  |         request_class._current.set(instance.request) | ||||||
|         instance.request.stream = instance  # type: ignore |         instance.request.stream = instance  # type: ignore | ||||||
|         instance.request_body = True |         instance.request_body = True | ||||||
|         instance.request.conn_info = ConnInfo(instance.transport) |         instance.request.conn_info = ConnInfo(instance.transport) | ||||||
|   | |||||||
| @@ -65,6 +65,7 @@ class BlueprintGroup(MutableSequence): | |||||||
|         "_version", |         "_version", | ||||||
|         "_strict_slashes", |         "_strict_slashes", | ||||||
|         "_version_prefix", |         "_version_prefix", | ||||||
|  |         "_name_prefix", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
| @@ -73,6 +74,7 @@ class BlueprintGroup(MutableSequence): | |||||||
|         version: Optional[Union[int, str, float]] = None, |         version: Optional[Union[int, str, float]] = None, | ||||||
|         strict_slashes: Optional[bool] = None, |         strict_slashes: Optional[bool] = None, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|  |         name_prefix: Optional[str] = "", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Create a new Blueprint Group |         Create a new Blueprint Group | ||||||
| @@ -87,6 +89,7 @@ class BlueprintGroup(MutableSequence): | |||||||
|         self._version = version |         self._version = version | ||||||
|         self._version_prefix = version_prefix |         self._version_prefix = version_prefix | ||||||
|         self._strict_slashes = strict_slashes |         self._strict_slashes = strict_slashes | ||||||
|  |         self._name_prefix = name_prefix | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def url_prefix(self) -> Optional[Union[int, str, float]]: |     def url_prefix(self) -> Optional[Union[int, str, float]]: | ||||||
| @@ -134,6 +137,15 @@ class BlueprintGroup(MutableSequence): | |||||||
|         """ |         """ | ||||||
|         return self._version_prefix |         return self._version_prefix | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def name_prefix(self) -> Optional[str]: | ||||||
|  |         """ | ||||||
|  |         Name prefix for the blueprint group | ||||||
|  |  | ||||||
|  |         :return: str | ||||||
|  |         """ | ||||||
|  |         return self._name_prefix | ||||||
|  |  | ||||||
|     def __iter__(self): |     def __iter__(self): | ||||||
|         """ |         """ | ||||||
|         Tun the class Blueprint Group into an Iterable item |         Tun the class Blueprint Group into an Iterable item | ||||||
|   | |||||||
| @@ -93,6 +93,7 @@ class Blueprint(BaseSanic): | |||||||
|         "_future_listeners", |         "_future_listeners", | ||||||
|         "_future_exceptions", |         "_future_exceptions", | ||||||
|         "_future_signals", |         "_future_signals", | ||||||
|  |         "_allow_route_overwrite", | ||||||
|         "copied_from", |         "copied_from", | ||||||
|         "ctx", |         "ctx", | ||||||
|         "exceptions", |         "exceptions", | ||||||
| @@ -110,7 +111,7 @@ class Blueprint(BaseSanic): | |||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         name: str = None, |         name: str, | ||||||
|         url_prefix: Optional[str] = None, |         url_prefix: Optional[str] = None, | ||||||
|         host: Optional[Union[List[str], str]] = None, |         host: Optional[Union[List[str], str]] = None, | ||||||
|         version: Optional[Union[int, str, float]] = None, |         version: Optional[Union[int, str, float]] = None, | ||||||
| @@ -119,6 +120,7 @@ class Blueprint(BaseSanic): | |||||||
|     ): |     ): | ||||||
|         super().__init__(name=name) |         super().__init__(name=name) | ||||||
|         self.reset() |         self.reset() | ||||||
|  |         self._allow_route_overwrite = False | ||||||
|         self.copied_from = "" |         self.copied_from = "" | ||||||
|         self.ctx = SimpleNamespace() |         self.ctx = SimpleNamespace() | ||||||
|         self.host = host |         self.host = host | ||||||
| @@ -169,6 +171,7 @@ class Blueprint(BaseSanic): | |||||||
|  |  | ||||||
|     def reset(self): |     def reset(self): | ||||||
|         self._apps: Set[Sanic] = set() |         self._apps: Set[Sanic] = set() | ||||||
|  |         self._allow_route_overwrite = False | ||||||
|         self.exceptions: List[RouteHandler] = [] |         self.exceptions: List[RouteHandler] = [] | ||||||
|         self.listeners: Dict[str, List[ListenerType[Any]]] = {} |         self.listeners: Dict[str, List[ListenerType[Any]]] = {} | ||||||
|         self.middlewares: List[MiddlewareType] = [] |         self.middlewares: List[MiddlewareType] = [] | ||||||
| @@ -182,6 +185,7 @@ class Blueprint(BaseSanic): | |||||||
|         url_prefix: Optional[Union[str, Default]] = _default, |         url_prefix: Optional[Union[str, Default]] = _default, | ||||||
|         version: Optional[Union[int, str, float, Default]] = _default, |         version: Optional[Union[int, str, float, Default]] = _default, | ||||||
|         version_prefix: Union[str, Default] = _default, |         version_prefix: Union[str, Default] = _default, | ||||||
|  |         allow_route_overwrite: Union[bool, Default] = _default, | ||||||
|         strict_slashes: Optional[Union[bool, Default]] = _default, |         strict_slashes: Optional[Union[bool, Default]] = _default, | ||||||
|         with_registration: bool = True, |         with_registration: bool = True, | ||||||
|         with_ctx: bool = False, |         with_ctx: bool = False, | ||||||
| @@ -225,6 +229,8 @@ class Blueprint(BaseSanic): | |||||||
|             new_bp.strict_slashes = strict_slashes |             new_bp.strict_slashes = strict_slashes | ||||||
|         if not isinstance(version_prefix, Default): |         if not isinstance(version_prefix, Default): | ||||||
|             new_bp.version_prefix = version_prefix |             new_bp.version_prefix = version_prefix | ||||||
|  |         if not isinstance(allow_route_overwrite, Default): | ||||||
|  |             new_bp._allow_route_overwrite = allow_route_overwrite | ||||||
|  |  | ||||||
|         for key, value in attrs_backup.items(): |         for key, value in attrs_backup.items(): | ||||||
|             setattr(self, key, value) |             setattr(self, key, value) | ||||||
| @@ -250,6 +256,7 @@ class Blueprint(BaseSanic): | |||||||
|         version: Optional[Union[int, str, float]] = None, |         version: Optional[Union[int, str, float]] = None, | ||||||
|         strict_slashes: Optional[bool] = None, |         strict_slashes: Optional[bool] = None, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|  |         name_prefix: Optional[str] = "", | ||||||
|     ) -> BlueprintGroup: |     ) -> BlueprintGroup: | ||||||
|         """ |         """ | ||||||
|         Create a list of blueprints, optionally grouping them under a |         Create a list of blueprints, optionally grouping them under a | ||||||
| @@ -275,6 +282,7 @@ class Blueprint(BaseSanic): | |||||||
|             version=version, |             version=version, | ||||||
|             strict_slashes=strict_slashes, |             strict_slashes=strict_slashes, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|  |             name_prefix=name_prefix, | ||||||
|         ) |         ) | ||||||
|         for bp in chain(blueprints): |         for bp in chain(blueprints): | ||||||
|             bps.append(bp) |             bps.append(bp) | ||||||
| @@ -295,6 +303,7 @@ class Blueprint(BaseSanic): | |||||||
|         opt_version = options.get("version", None) |         opt_version = options.get("version", None) | ||||||
|         opt_strict_slashes = options.get("strict_slashes", None) |         opt_strict_slashes = options.get("strict_slashes", None) | ||||||
|         opt_version_prefix = options.get("version_prefix", self.version_prefix) |         opt_version_prefix = options.get("version_prefix", self.version_prefix) | ||||||
|  |         opt_name_prefix = options.get("name_prefix", None) | ||||||
|         error_format = options.get( |         error_format = options.get( | ||||||
|             "error_format", app.config.FALLBACK_ERROR_FORMAT |             "error_format", app.config.FALLBACK_ERROR_FORMAT | ||||||
|         ) |         ) | ||||||
| @@ -310,6 +319,10 @@ class Blueprint(BaseSanic): | |||||||
|             # Prepend the blueprint URI prefix if available |             # Prepend the blueprint URI prefix if available | ||||||
|             uri = self._setup_uri(future.uri, url_prefix) |             uri = self._setup_uri(future.uri, url_prefix) | ||||||
|  |  | ||||||
|  |             route_error_format = ( | ||||||
|  |                 future.error_format if future.error_format else error_format | ||||||
|  |             ) | ||||||
|  |  | ||||||
|             version_prefix = self.version_prefix |             version_prefix = self.version_prefix | ||||||
|             for prefix in ( |             for prefix in ( | ||||||
|                 future.version_prefix, |                 future.version_prefix, | ||||||
| @@ -326,7 +339,10 @@ class Blueprint(BaseSanic): | |||||||
|                 future.strict_slashes, opt_strict_slashes, self.strict_slashes |                 future.strict_slashes, opt_strict_slashes, self.strict_slashes | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             name = app._generate_name(future.name) |             name = future.name | ||||||
|  |             if opt_name_prefix: | ||||||
|  |                 name = f"{opt_name_prefix}_{future.name}" | ||||||
|  |             name = app._generate_name(name) | ||||||
|             host = future.host or self.host |             host = future.host or self.host | ||||||
|             if isinstance(host, list): |             if isinstance(host, list): | ||||||
|                 host = tuple(host) |                 host = tuple(host) | ||||||
| @@ -346,7 +362,7 @@ class Blueprint(BaseSanic): | |||||||
|                 future.unquote, |                 future.unquote, | ||||||
|                 future.static, |                 future.static, | ||||||
|                 version_prefix, |                 version_prefix, | ||||||
|                 error_format, |                 route_error_format, | ||||||
|                 future.route_context, |                 future.route_context, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
| @@ -354,7 +370,9 @@ class Blueprint(BaseSanic): | |||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             registered.add(apply_route) |             registered.add(apply_route) | ||||||
|             route = app._apply_route(apply_route) |             route = app._apply_route( | ||||||
|  |                 apply_route, overwrite=self._allow_route_overwrite | ||||||
|  |             ) | ||||||
|  |  | ||||||
|             # If it is a copied BP, then make sure all of the names of routes |             # If it is a copied BP, then make sure all of the names of routes | ||||||
|             # matchup with the new BP name |             # matchup with the new BP name | ||||||
|   | |||||||
| @@ -180,6 +180,10 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|                     "  Example File: project/sanic_server.py -> app\n" |                     "  Example File: project/sanic_server.py -> app\n" | ||||||
|                     "  Example Module: project.sanic_server.app" |                     "  Example Module: project.sanic_server.app" | ||||||
|                 ) |                 ) | ||||||
|  |                 error_logger.error( | ||||||
|  |                     "\nThe error below might have caused the above one:\n" | ||||||
|  |                     f"{e.msg}" | ||||||
|  |                 ) | ||||||
|                 sys.exit(1) |                 sys.exit(1) | ||||||
|             else: |             else: | ||||||
|                 raise e |                 raise e | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import os | import os | ||||||
|  | import platform | ||||||
| import signal | import signal | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| @@ -10,6 +11,7 @@ from typing import Awaitable, Union | |||||||
| from multidict import CIMultiDict  # type: ignore | from multidict import CIMultiDict  # type: ignore | ||||||
|  |  | ||||||
| from sanic.helpers import Default | from sanic.helpers import Default | ||||||
|  | from sanic.log import error_logger | ||||||
|  |  | ||||||
|  |  | ||||||
| if sys.version_info < (3, 8):  # no cov | if sys.version_info < (3, 8):  # no cov | ||||||
| @@ -22,6 +24,7 @@ else:  # no cov | |||||||
|     ] |     ] | ||||||
|  |  | ||||||
| OS_IS_WINDOWS = os.name == "nt" | OS_IS_WINDOWS = os.name == "nt" | ||||||
|  | PYPY_IMPLEMENTATION = platform.python_implementation() == "PyPy" | ||||||
| UVLOOP_INSTALLED = False | UVLOOP_INSTALLED = False | ||||||
|  |  | ||||||
| try: | try: | ||||||
| @@ -73,6 +76,38 @@ def enable_windows_color_support(): | |||||||
|     kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7) |     kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def pypy_os_module_patch() -> None: | ||||||
|  |     """ | ||||||
|  |     The PyPy os module is missing the 'readlink' function, which causes issues | ||||||
|  |     withaiofiles. This workaround replaces the missing 'readlink' function | ||||||
|  |     with 'os.path.realpath', which serves the same purpose. | ||||||
|  |     """ | ||||||
|  |     if hasattr(os, "readlink"): | ||||||
|  |         error_logger.warning( | ||||||
|  |             "PyPy: Skipping patching of the os module as it appears the " | ||||||
|  |             "'readlink' function has been added." | ||||||
|  |         ) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     module = sys.modules["os"] | ||||||
|  |     module.readlink = os.path.realpath  # type: ignore | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def pypy_windows_set_console_cp_patch() -> None: | ||||||
|  |     """ | ||||||
|  |     A patch function for PyPy on Windows that sets the console code page to | ||||||
|  |     UTF-8 encodingto allow for proper handling of non-ASCII characters. This | ||||||
|  |     function uses ctypes to call the Windows API functions SetConsoleCP and | ||||||
|  |     SetConsoleOutputCP to set the code page. | ||||||
|  |     """ | ||||||
|  |     from ctypes import windll  # type: ignore | ||||||
|  |  | ||||||
|  |     code: int = windll.kernel32.GetConsoleOutputCP() | ||||||
|  |     if code != 65001: | ||||||
|  |         windll.kernel32.SetConsoleCP(65001) | ||||||
|  |         windll.kernel32.SetConsoleOutputCP(65001) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Header(CIMultiDict): | class Header(CIMultiDict): | ||||||
|     """ |     """ | ||||||
|     Container used for both request and response headers. It is a subclass of |     Container used for both request and response headers. It is a subclass of | ||||||
| @@ -86,7 +121,7 @@ class Header(CIMultiDict): | |||||||
|     <https://multidict.readthedocs.io/en/stable/multidict.html#multidict>`_ |     <https://multidict.readthedocs.io/en/stable/multidict.html#multidict>`_ | ||||||
|     for more details about how to use the object. In general, it should work |     for more details about how to use the object. In general, it should work | ||||||
|     very similar to a regular dictionary. |     very similar to a regular dictionary. | ||||||
|     """ |     """  # noqa: E501 | ||||||
|  |  | ||||||
|     def __getattr__(self, key: str) -> str: |     def __getattr__(self, key: str) -> str: | ||||||
|         if key.startswith("_"): |         if key.startswith("_"): | ||||||
| @@ -112,6 +147,12 @@ if use_trio:  # pragma: no cover | |||||||
|     open_async = trio.open_file |     open_async = trio.open_file | ||||||
|     CancelledErrors = tuple([asyncio.CancelledError, trio.Cancelled]) |     CancelledErrors = tuple([asyncio.CancelledError, trio.Cancelled]) | ||||||
| else: | else: | ||||||
|  |     if PYPY_IMPLEMENTATION: | ||||||
|  |         pypy_os_module_patch() | ||||||
|  |  | ||||||
|  |         if OS_IS_WINDOWS: | ||||||
|  |             pypy_windows_set_console_cp_patch() | ||||||
|  |  | ||||||
|     from aiofiles import open as aio_open  # type: ignore |     from aiofiles import open as aio_open  # type: ignore | ||||||
|     from aiofiles.os import stat as stat_async  # type: ignore  # noqa: F401 |     from aiofiles.os import stat as stat_async  # type: ignore  # noqa: F401 | ||||||
|  |  | ||||||
| @@ -143,7 +184,3 @@ def ctrlc_workaround_for_windows(app): | |||||||
|     die = False |     die = False | ||||||
|     signal.signal(signal.SIGINT, ctrlc_handler) |     signal.signal(signal.SIGINT, ctrlc_handler) | ||||||
|     app.add_task(stay_active) |     app.add_task(stay_active) | ||||||
|  |  | ||||||
|  |  | ||||||
| def is_atty() -> bool: |  | ||||||
|     return bool(sys.stdout and sys.stdout.isatty()) |  | ||||||
|   | |||||||
| @@ -43,14 +43,14 @@ DEFAULT_CONFIG = { | |||||||
|     "DEPRECATION_FILTER": "once", |     "DEPRECATION_FILTER": "once", | ||||||
|     "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, | ||||||
|     "INSPECTOR": False, |     "INSPECTOR": False, | ||||||
|     "INSPECTOR_HOST": "localhost", |     "INSPECTOR_HOST": "localhost", | ||||||
|     "INSPECTOR_PORT": 6457, |     "INSPECTOR_PORT": 6457, | ||||||
|     "INSPECTOR_TLS_KEY": _default, |     "INSPECTOR_TLS_KEY": _default, | ||||||
|     "INSPECTOR_TLS_CERT": _default, |     "INSPECTOR_TLS_CERT": _default, | ||||||
|     "INSPECTOR_API_KEY": "", |     "INSPECTOR_API_KEY": "", | ||||||
|     "KEEP_ALIVE_TIMEOUT": 5,  # 5 seconds |     "KEEP_ALIVE_TIMEOUT": 120, | ||||||
|     "KEEP_ALIVE": True, |     "KEEP_ALIVE": True, | ||||||
|     "LOCAL_CERT_CREATOR": LocalCertCreator.AUTO, |     "LOCAL_CERT_CREATOR": LocalCertCreator.AUTO, | ||||||
|     "LOCAL_TLS_KEY": _default, |     "LOCAL_TLS_KEY": _default, | ||||||
| @@ -61,16 +61,16 @@ DEFAULT_CONFIG = { | |||||||
|     "NOISY_EXCEPTIONS": False, |     "NOISY_EXCEPTIONS": False, | ||||||
|     "PROXIES_COUNT": None, |     "PROXIES_COUNT": None, | ||||||
|     "REAL_IP_HEADER": None, |     "REAL_IP_HEADER": None, | ||||||
|     "REQUEST_BUFFER_SIZE": 65536,  # 64 KiB |     "REQUEST_BUFFER_SIZE": 65536, | ||||||
|     "REQUEST_MAX_HEADER_SIZE": 8192,  # 8 KiB, but cannot exceed 16384 |     "REQUEST_MAX_HEADER_SIZE": 8192,  # Cannot exceed 16384 | ||||||
|     "REQUEST_ID_HEADER": "X-Request-ID", |     "REQUEST_ID_HEADER": "X-Request-ID", | ||||||
|     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes |     "REQUEST_MAX_SIZE": 100_000_000, | ||||||
|     "REQUEST_TIMEOUT": 60,  # 60 seconds |     "REQUEST_TIMEOUT": 60, | ||||||
|     "RESPONSE_TIMEOUT": 60,  # 60 seconds |     "RESPONSE_TIMEOUT": 60, | ||||||
|     "TLS_CERT_PASSWORD": "", |     "TLS_CERT_PASSWORD": "", | ||||||
|     "TOUCHUP": _default, |     "TOUCHUP": _default, | ||||||
|     "USE_UVLOOP": _default, |     "USE_UVLOOP": _default, | ||||||
|     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 megabyte |     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 MiB | ||||||
|     "WEBSOCKET_PING_INTERVAL": 20, |     "WEBSOCKET_PING_INTERVAL": 20, | ||||||
|     "WEBSOCKET_PING_TIMEOUT": 20, |     "WEBSOCKET_PING_TIMEOUT": 20, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -92,8 +92,10 @@ class BaseRenderer: | |||||||
|             self.full |             self.full | ||||||
|             if self.debug and not getattr(self.exception, "quiet", False) |             if self.debug and not getattr(self.exception, "quiet", False) | ||||||
|             else self.minimal |             else self.minimal | ||||||
|         ) |         )() | ||||||
|         return output() |         output.status = self.status | ||||||
|  |         output.headers.update(self.headers) | ||||||
|  |         return output | ||||||
|  |  | ||||||
|     def minimal(self) -> HTTPResponse:  # noqa |     def minimal(self) -> HTTPResponse:  # noqa | ||||||
|         """ |         """ | ||||||
| @@ -125,7 +127,7 @@ class HTMLRenderer(BaseRenderer): | |||||||
|             request=self.request, |             request=self.request, | ||||||
|             exc=self.exception, |             exc=self.exception, | ||||||
|         ) |         ) | ||||||
|         return html(page.render(), status=self.status, headers=self.headers) |         return html(page.render()) | ||||||
|  |  | ||||||
|     def minimal(self) -> HTTPResponse: |     def minimal(self) -> HTTPResponse: | ||||||
|         return self.full() |         return self.full() | ||||||
| @@ -146,8 +148,7 @@ class TextRenderer(BaseRenderer): | |||||||
|                 text=self.text, |                 text=self.text, | ||||||
|                 bar=("=" * len(self.title)), |                 bar=("=" * len(self.title)), | ||||||
|                 body=self._generate_body(full=True), |                 body=self._generate_body(full=True), | ||||||
|             ), |             ) | ||||||
|             status=self.status, |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def minimal(self) -> HTTPResponse: |     def minimal(self) -> HTTPResponse: | ||||||
| @@ -157,9 +158,7 @@ class TextRenderer(BaseRenderer): | |||||||
|                 text=self.text, |                 text=self.text, | ||||||
|                 bar=("=" * len(self.title)), |                 bar=("=" * len(self.title)), | ||||||
|                 body=self._generate_body(full=False), |                 body=self._generate_body(full=False), | ||||||
|             ), |             ) | ||||||
|             status=self.status, |  | ||||||
|             headers=self.headers, |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @@ -218,11 +217,11 @@ class JSONRenderer(BaseRenderer): | |||||||
|  |  | ||||||
|     def full(self) -> HTTPResponse: |     def full(self) -> HTTPResponse: | ||||||
|         output = self._generate_output(full=True) |         output = self._generate_output(full=True) | ||||||
|         return json(output, status=self.status, dumps=self.dumps) |         return json(output, dumps=self.dumps) | ||||||
|  |  | ||||||
|     def minimal(self) -> HTTPResponse: |     def minimal(self) -> HTTPResponse: | ||||||
|         output = self._generate_output(full=False) |         output = self._generate_output(full=False) | ||||||
|         return json(output, status=self.status, dumps=self.dumps) |         return json(output, dumps=self.dumps) | ||||||
|  |  | ||||||
|     def _generate_output(self, *, full): |     def _generate_output(self, *, full): | ||||||
|         output = { |         output = { | ||||||
| @@ -313,7 +312,7 @@ def exception_response( | |||||||
|     debug: bool, |     debug: bool, | ||||||
|     fallback: str, |     fallback: str, | ||||||
|     base: t.Type[BaseRenderer], |     base: t.Type[BaseRenderer], | ||||||
|     renderer: t.Type[t.Optional[BaseRenderer]] = None, |     renderer: t.Optional[t.Type[BaseRenderer]] = None, | ||||||
| ) -> HTTPResponse: | ) -> HTTPResponse: | ||||||
|     """ |     """ | ||||||
|     Render a response for the default FALLBACK exception handler. |     Render a response for the default FALLBACK exception handler. | ||||||
|   | |||||||
| @@ -90,7 +90,7 @@ class SanicException(Exception): | |||||||
|  |  | ||||||
|         super().__init__(message) |         super().__init__(message) | ||||||
|  |  | ||||||
|         self.status_code = status_code |         self.status_code = status_code or self.status_code | ||||||
|         self.quiet = quiet |         self.quiet = quiet | ||||||
|         self.headers = headers |         self.headers = headers | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,7 +6,9 @@ from sanic.errorpages import BaseRenderer, TextRenderer, exception_response | |||||||
| from sanic.exceptions import ServerError | from sanic.exceptions import ServerError | ||||||
| from sanic.log import error_logger | from sanic.log import error_logger | ||||||
| from sanic.models.handler_types import RouteHandler | from sanic.models.handler_types import RouteHandler | ||||||
|  | from sanic.request.types import Request | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  | from sanic.response.types import HTTPResponse | ||||||
|  |  | ||||||
|  |  | ||||||
| class ErrorHandler: | class ErrorHandler: | ||||||
| @@ -148,7 +150,7 @@ class ErrorHandler: | |||||||
|                 return text("An error occurred while handling an error", 500) |                 return text("An error occurred while handling an error", 500) | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|     def default(self, request, exception): |     def default(self, request: Request, exception: Exception) -> HTTPResponse: | ||||||
|         """ |         """ | ||||||
|         Provide a default behavior for the objects of :class:`ErrorHandler`. |         Provide a default behavior for the objects of :class:`ErrorHandler`. | ||||||
|         If a developer chooses to extent the :class:`ErrorHandler` they can |         If a developer chooses to extent the :class:`ErrorHandler` they can | ||||||
|   | |||||||
| @@ -436,7 +436,7 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes: | |||||||
|  |  | ||||||
| def parse_credentials( | def parse_credentials( | ||||||
|     header: Optional[str], |     header: Optional[str], | ||||||
|     prefixes: Union[List, Tuple, Set] = None, |     prefixes: Optional[Union[List, Tuple, Set]] = None, | ||||||
| ) -> Tuple[Optional[str], Optional[str]]: | ) -> Tuple[Optional[str], Optional[str]]: | ||||||
|     """Parses any header with the aim to retrieve any credentials from it.""" |     """Parses any header with the aim to retrieve any credentials from it.""" | ||||||
|     if not prefixes or not isinstance(prefixes, (list, tuple, set)): |     if not prefixes or not isinstance(prefixes, (list, tuple, set)): | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| """Defines basics of HTTP standard.""" | """Defines basics of HTTP standard.""" | ||||||
|  |  | ||||||
|  | import sys | ||||||
|  |  | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
| from inspect import ismodule | from inspect import ismodule | ||||||
| from typing import Dict | from typing import Dict | ||||||
| @@ -157,6 +159,10 @@ def import_string(module_name, package=None): | |||||||
|     return obj() |     return obj() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def is_atty() -> bool: | ||||||
|  |     return bool(sys.stdout and sys.stdout.isatty()) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Default: | class Default: | ||||||
|     """ |     """ | ||||||
|     It is used to replace `None` or `object()` as a sentinel |     It is used to replace `None` or `object()` as a sentinel | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ from enum import Enum | |||||||
| from typing import TYPE_CHECKING, Any, Dict | from typing import TYPE_CHECKING, Any, Dict | ||||||
| from warnings import warn | from warnings import warn | ||||||
|  |  | ||||||
| from sanic.compat import is_atty | from sanic.helpers import is_atty | ||||||
|  |  | ||||||
|  |  | ||||||
| # Python 3.11 changed the way Enum formatting works for mixed-in types. | # Python 3.11 changed the way Enum formatting works for mixed-in types. | ||||||
|   | |||||||
| @@ -38,3 +38,15 @@ class ExceptionMixin(metaclass=SanicMeta): | |||||||
|             return handler |             return handler | ||||||
|  |  | ||||||
|         return decorator |         return decorator | ||||||
|  |  | ||||||
|  |     def all_exceptions(self, handler): | ||||||
|  |         """ | ||||||
|  |         This method enables the process of creating a global exception | ||||||
|  |         handler for the current blueprint under question. | ||||||
|  |  | ||||||
|  |         :param handler: A coroutine function to handle exceptions | ||||||
|  |  | ||||||
|  |         :return a decorated method to handle global exceptions for any | ||||||
|  |             route registered under this blueprint. | ||||||
|  |         """ | ||||||
|  |         return self.exception(Exception)(handler) | ||||||
|   | |||||||
| @@ -159,7 +159,11 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta): | |||||||
|                 error_format, |                 error_format, | ||||||
|                 route_context, |                 route_context, | ||||||
|             ) |             ) | ||||||
|  |             overwrite = getattr(self, "_allow_route_overwrite", False) | ||||||
|  |             if overwrite: | ||||||
|  |                 self._future_routes = set( | ||||||
|  |                     filter(lambda x: x.uri != uri, self._future_routes) | ||||||
|  |                 ) | ||||||
|             self._future_routes.add(route) |             self._future_routes.add(route) | ||||||
|  |  | ||||||
|             args = list(signature(handler).parameters.keys()) |             args = list(signature(handler).parameters.keys()) | ||||||
| @@ -182,7 +186,7 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta): | |||||||
|                 handler.is_stream = stream |                 handler.is_stream = stream | ||||||
|  |  | ||||||
|             if apply: |             if apply: | ||||||
|                 self._apply_route(route) |                 self._apply_route(route, overwrite=overwrite) | ||||||
|  |  | ||||||
|             if static: |             if static: | ||||||
|                 return route, handler |                 return route, handler | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, Optional, Set, Union | |||||||
| from sanic.base.meta import SanicMeta | 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 Event, Signal | ||||||
| from sanic.types import HashableDict | from sanic.types import HashableDict | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -80,3 +80,9 @@ class SignalMixin(metaclass=SanicMeta): | |||||||
|  |  | ||||||
|     def event(self, event: str): |     def event(self, event: str): | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |     def catch_exception(self, handler): | ||||||
|  |         async def signal_handler(exception: Exception): | ||||||
|  |             await handler(self, exception) | ||||||
|  |  | ||||||
|  |         self.signal(Event.SERVER_LIFECYCLE_EXCEPTION)(signal_handler) | ||||||
|   | |||||||
| @@ -16,7 +16,13 @@ from asyncio import ( | |||||||
| from contextlib import suppress | from contextlib import suppress | ||||||
| from functools import partial | from functools import partial | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
| from multiprocessing import Manager, Pipe, get_context | from multiprocessing import ( | ||||||
|  |     Manager, | ||||||
|  |     Pipe, | ||||||
|  |     get_context, | ||||||
|  |     get_start_method, | ||||||
|  |     set_start_method, | ||||||
|  | ) | ||||||
| from multiprocessing.context import BaseContext | from multiprocessing.context import BaseContext | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from socket import SHUT_RDWR, socket | from socket import SHUT_RDWR, socket | ||||||
| @@ -25,6 +31,7 @@ from typing import ( | |||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
|     Any, |     Any, | ||||||
|     Callable, |     Callable, | ||||||
|  |     ClassVar, | ||||||
|     Dict, |     Dict, | ||||||
|     List, |     List, | ||||||
|     Mapping, |     Mapping, | ||||||
| @@ -41,9 +48,9 @@ from sanic.application.logo import get_logo | |||||||
| from sanic.application.motd import MOTD | from sanic.application.motd import MOTD | ||||||
| from sanic.application.state import ApplicationServerInfo, Mode, ServerStage | from sanic.application.state import ApplicationServerInfo, Mode, ServerStage | ||||||
| from sanic.base.meta import SanicMeta | from sanic.base.meta import SanicMeta | ||||||
| from sanic.compat import OS_IS_WINDOWS, StartMethod, is_atty | from sanic.compat import OS_IS_WINDOWS, StartMethod | ||||||
| from sanic.exceptions import ServerKilled | from sanic.exceptions import ServerKilled | ||||||
| from sanic.helpers import Default, _default | from sanic.helpers import Default, _default, is_atty | ||||||
| from sanic.http.constants import HTTP | from sanic.http.constants import HTTP | ||||||
| from sanic.http.tls import get_ssl_context, process_to_context | from sanic.http.tls import get_ssl_context, process_to_context | ||||||
| from sanic.http.tls.context import SanicSSLContext | from sanic.http.tls.context import SanicSSLContext | ||||||
| @@ -81,13 +88,18 @@ else:  # no cov | |||||||
|  |  | ||||||
|  |  | ||||||
| class StartupMixin(metaclass=SanicMeta): | class StartupMixin(metaclass=SanicMeta): | ||||||
|     _app_registry: Dict[str, Sanic] |     _app_registry: ClassVar[Dict[str, Sanic]] | ||||||
|  |  | ||||||
|  |     name: str | ||||||
|     config: Config |     config: Config | ||||||
|     listeners: Dict[str, List[ListenerType[Any]]] |     listeners: Dict[str, List[ListenerType[Any]]] | ||||||
|     state: ApplicationState |     state: ApplicationState | ||||||
|     websocket_enabled: bool |     websocket_enabled: bool | ||||||
|     multiplexer: WorkerMultiplexer |     multiplexer: WorkerMultiplexer | ||||||
|     start_method: StartMethod = _default |  | ||||||
|  |     test_mode: ClassVar[bool] | ||||||
|  |     start_method: ClassVar[StartMethod] = _default | ||||||
|  |     START_METHOD_SET: ClassVar[bool] = False | ||||||
|  |  | ||||||
|     def setup_loop(self): |     def setup_loop(self): | ||||||
|         if not self.asgi: |         if not self.asgi: | ||||||
| @@ -594,6 +606,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|             server = "ASGI" if self.asgi else "unknown"  # type: ignore |             server = "ASGI" if self.asgi else "unknown"  # type: ignore | ||||||
|  |  | ||||||
|         display = { |         display = { | ||||||
|  |             "app": self.name, | ||||||
|             "mode": " ".join(mode), |             "mode": " ".join(mode), | ||||||
|             "server": server, |             "server": server, | ||||||
|             "python": platform.python_version(), |             "python": platform.python_version(), | ||||||
| @@ -691,11 +704,26 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|             else "spawn" |             else "spawn" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _set_startup_method(cls) -> None: | ||||||
|  |         if cls.START_METHOD_SET and not cls.test_mode: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         method = cls._get_startup_method() | ||||||
|  |         set_start_method(method, force=cls.test_mode) | ||||||
|  |         cls.START_METHOD_SET = True | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _get_context(cls) -> BaseContext: |     def _get_context(cls) -> BaseContext: | ||||||
|         method = cls._get_startup_method() |         method = cls._get_startup_method() | ||||||
|         logger.debug("Creating multiprocessing context using '%s'", method) |         logger.debug("Creating multiprocessing context using '%s'", method) | ||||||
|         return get_context(method) |         actual = get_start_method() | ||||||
|  |         if method != actual: | ||||||
|  |             raise RuntimeError( | ||||||
|  |                 f"Start method '{method}' was requested, but '{actual}' " | ||||||
|  |                 "was actually set." | ||||||
|  |             ) | ||||||
|  |         return get_context() | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def serve( |     def serve( | ||||||
| @@ -705,6 +733,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|         app_loader: Optional[AppLoader] = None, |         app_loader: Optional[AppLoader] = None, | ||||||
|         factory: Optional[Callable[[], Sanic]] = None, |         factory: Optional[Callable[[], Sanic]] = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|  |         cls._set_startup_method() | ||||||
|         os.environ["SANIC_MOTD_OUTPUT"] = "true" |         os.environ["SANIC_MOTD_OUTPUT"] = "true" | ||||||
|         apps = list(cls._app_registry.values()) |         apps = list(cls._app_registry.values()) | ||||||
|         if factory: |         if factory: | ||||||
|   | |||||||
| @@ -95,7 +95,7 @@ class StaticMixin(BaseMixin, metaclass=SanicMeta): | |||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             file_or_directory = Path(file_or_directory) |             file_or_directory = Path(file_or_directory).resolve() | ||||||
|         except TypeError: |         except TypeError: | ||||||
|             raise TypeError( |             raise TypeError( | ||||||
|                 "Static file or directory must be a path-like object or string" |                 "Static file or directory must be a path-like object or string" | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import sys |  | ||||||
|  |  | ||||||
| from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union | from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union | ||||||
|  |  | ||||||
| @@ -16,20 +15,10 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]] | |||||||
|  |  | ||||||
| class MockProtocol:  # no cov | class MockProtocol:  # no cov | ||||||
|     def __init__(self, transport: "MockTransport", loop): |     def __init__(self, transport: "MockTransport", loop): | ||||||
|         # This should be refactored when < 3.8 support is dropped |  | ||||||
|         self.transport = transport |         self.transport = transport | ||||||
|         # Fixup for 3.8+; Sanic still supports 3.7 where loop is required |         self._not_paused = asyncio.Event() | ||||||
|         loop = loop if sys.version_info[:2] < (3, 8) else None |         self._not_paused.set() | ||||||
|         # Optional in 3.9, necessary in 3.10 because the parameter "loop" |         self._complete = asyncio.Event() | ||||||
|         # was completely removed |  | ||||||
|         if not loop: |  | ||||||
|             self._not_paused = asyncio.Event() |  | ||||||
|             self._not_paused.set() |  | ||||||
|             self._complete = asyncio.Event() |  | ||||||
|         else: |  | ||||||
|             self._not_paused = asyncio.Event(loop=loop) |  | ||||||
|             self._not_paused.set() |  | ||||||
|             self._complete = asyncio.Event(loop=loop) |  | ||||||
|  |  | ||||||
|     def pause_writing(self) -> None: |     def pause_writing(self) -> None: | ||||||
|         self._not_paused.clear() |         self._not_paused.clear() | ||||||
|   | |||||||
| @@ -3,11 +3,12 @@ from typing import Any, Callable, Coroutine, Optional, TypeVar, Union | |||||||
|  |  | ||||||
| import sanic | import sanic | ||||||
|  |  | ||||||
| from sanic.request import Request | from sanic import request | ||||||
| from sanic.response import BaseHTTPResponse, HTTPResponse | from sanic.response import BaseHTTPResponse, HTTPResponse | ||||||
|  |  | ||||||
|  |  | ||||||
| Sanic = TypeVar("Sanic", bound="sanic.Sanic") | Sanic = TypeVar("Sanic", bound="sanic.Sanic") | ||||||
|  | Request = TypeVar("Request", bound="request.Request") | ||||||
|  |  | ||||||
| MiddlewareResponse = Union[ | MiddlewareResponse = Union[ | ||||||
|     Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]] |     Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]] | ||||||
|   | |||||||
| @@ -2,11 +2,13 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| from contextvars import ContextVar | from contextvars import ContextVar | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
|  | from types import SimpleNamespace | ||||||
| from typing import ( | from typing import ( | ||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
|     Any, |     Any, | ||||||
|     DefaultDict, |     DefaultDict, | ||||||
|     Dict, |     Dict, | ||||||
|  |     Generic, | ||||||
|     List, |     List, | ||||||
|     Optional, |     Optional, | ||||||
|     Tuple, |     Tuple, | ||||||
| @@ -15,6 +17,7 @@ from typing import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| from sanic_routing.route import Route | from sanic_routing.route import Route | ||||||
|  | from typing_extensions import TypeVar | ||||||
|  |  | ||||||
| from sanic.http.constants import HTTP  # type: ignore | from sanic.http.constants import HTTP  # type: ignore | ||||||
| from sanic.http.stream import Stream | from sanic.http.stream import Stream | ||||||
| @@ -23,13 +26,13 @@ from sanic.models.http_types import Credentials | |||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from sanic.server import ConnInfo |  | ||||||
|     from sanic.app import Sanic |     from sanic.app import Sanic | ||||||
|  |     from sanic.config import Config | ||||||
|  |     from sanic.server import ConnInfo | ||||||
|  |  | ||||||
| import uuid | import uuid | ||||||
|  |  | ||||||
| from collections import defaultdict | from collections import defaultdict | ||||||
| from types import SimpleNamespace |  | ||||||
| from urllib.parse import parse_qs, parse_qsl, urlunparse | from urllib.parse import parse_qs, parse_qsl, urlunparse | ||||||
|  |  | ||||||
| from httptools import parse_url | from httptools import parse_url | ||||||
| @@ -68,8 +71,21 @@ try: | |||||||
| except ImportError: | except ImportError: | ||||||
|     from json import loads as json_loads  # type: ignore |     from json import loads as json_loads  # type: ignore | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     # The default argument of TypeVar is proposed to be added in Python 3.13 | ||||||
|  |     # by PEP 696 (https://www.python.org/dev/peps/pep-0696/). | ||||||
|  |     # Therefore, we use typing_extensions.TypeVar for compatibility. | ||||||
|  |     # For more information, see: | ||||||
|  |     # https://discuss.python.org/t/pep-696-type-defaults-for-typevarlikes | ||||||
|  |     sanic_type = TypeVar( | ||||||
|  |         "sanic_type", bound=Sanic, default=Sanic[Config, SimpleNamespace] | ||||||
|  |     ) | ||||||
|  | else: | ||||||
|  |     sanic_type = TypeVar("sanic_type") | ||||||
|  | ctx_type = TypeVar("ctx_type") | ||||||
|  |  | ||||||
| class Request: |  | ||||||
|  | class Request(Generic[sanic_type, ctx_type]): | ||||||
|     """ |     """ | ||||||
|     Properties of an HTTP request such as URL, headers, etc. |     Properties of an HTTP request such as URL, headers, etc. | ||||||
|     """ |     """ | ||||||
| @@ -80,6 +96,7 @@ class Request: | |||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
|         "__weakref__", |         "__weakref__", | ||||||
|         "_cookies", |         "_cookies", | ||||||
|  |         "_ctx", | ||||||
|         "_id", |         "_id", | ||||||
|         "_ip", |         "_ip", | ||||||
|         "_parsed_url", |         "_parsed_url", | ||||||
| @@ -96,7 +113,6 @@ class Request: | |||||||
|         "app", |         "app", | ||||||
|         "body", |         "body", | ||||||
|         "conn_info", |         "conn_info", | ||||||
|         "ctx", |  | ||||||
|         "head", |         "head", | ||||||
|         "headers", |         "headers", | ||||||
|         "method", |         "method", | ||||||
| @@ -125,7 +141,7 @@ class Request: | |||||||
|         version: str, |         version: str, | ||||||
|         method: str, |         method: str, | ||||||
|         transport: TransportProtocol, |         transport: TransportProtocol, | ||||||
|         app: Sanic, |         app: sanic_type, | ||||||
|         head: bytes = b"", |         head: bytes = b"", | ||||||
|         stream_id: int = 0, |         stream_id: int = 0, | ||||||
|     ): |     ): | ||||||
| @@ -149,7 +165,7 @@ class Request: | |||||||
|         # Init but do not inhale |         # Init but do not inhale | ||||||
|         self.body = b"" |         self.body = b"" | ||||||
|         self.conn_info: Optional[ConnInfo] = None |         self.conn_info: Optional[ConnInfo] = None | ||||||
|         self.ctx = SimpleNamespace() |         self._ctx: Optional[ctx_type] = None | ||||||
|         self.parsed_accept: Optional[AcceptList] = None |         self.parsed_accept: Optional[AcceptList] = None | ||||||
|         self.parsed_args: DefaultDict[ |         self.parsed_args: DefaultDict[ | ||||||
|             Tuple[bool, bool, str, str], RequestParameters |             Tuple[bool, bool, str, str], RequestParameters | ||||||
| @@ -176,6 +192,10 @@ class Request: | |||||||
|         class_name = self.__class__.__name__ |         class_name = self.__class__.__name__ | ||||||
|         return f"<{class_name}: {self.method} {self.path}>" |         return f"<{class_name}: {self.method} {self.path}>" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def make_context() -> ctx_type: | ||||||
|  |         return cast(ctx_type, SimpleNamespace()) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get_current(cls) -> Request: |     def get_current(cls) -> Request: | ||||||
|         """ |         """ | ||||||
| @@ -205,6 +225,15 @@ class Request: | |||||||
|     def generate_id(*_): |     def generate_id(*_): | ||||||
|         return uuid.uuid4() |         return uuid.uuid4() | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def ctx(self) -> ctx_type: | ||||||
|  |         """ | ||||||
|  |         :return: The current request context | ||||||
|  |         """ | ||||||
|  |         if not self._ctx: | ||||||
|  |             self._ctx = self.make_context() | ||||||
|  |         return self._ctx | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def stream_id(self): |     def stream_id(self): | ||||||
|         """ |         """ | ||||||
| @@ -809,19 +838,31 @@ class Request: | |||||||
|     @property |     @property | ||||||
|     def remote_addr(self) -> str: |     def remote_addr(self) -> str: | ||||||
|         """ |         """ | ||||||
|         Client IP address, if available. |         Client IP address, if available from proxy. | ||||||
|         1. proxied remote address `self.forwarded['for']` |  | ||||||
|         2. local remote address `self.ip` |  | ||||||
|  |  | ||||||
|         :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string |         :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string | ||||||
|         :rtype: str |         :rtype: str | ||||||
|         """ |         """ | ||||||
|         if not hasattr(self, "_remote_addr"): |         if not hasattr(self, "_remote_addr"): | ||||||
|             self._remote_addr = str( |             self._remote_addr = str(self.forwarded.get("for", "")) | ||||||
|                 self.forwarded.get("for", "") |  | ||||||
|             )  # or self.ip |  | ||||||
|         return self._remote_addr |         return self._remote_addr | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def client_ip(self) -> str: | ||||||
|  |         """ | ||||||
|  |         Client IP address. | ||||||
|  |         1. proxied remote address `self.forwarded['for']` | ||||||
|  |         2. local peer address `self.ip` | ||||||
|  |  | ||||||
|  |         New in Sanic 23.6. Prefer this over `remote_addr` for determining the | ||||||
|  |         client address regardless of whether the service runs behind a proxy | ||||||
|  |         or not (proxy deployment needs separate configuration). | ||||||
|  |  | ||||||
|  |         :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string | ||||||
|  |         :rtype: str | ||||||
|  |         """ | ||||||
|  |         return self.remote_addr or self.ip | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def scheme(self) -> str: |     def scheme(self) -> str: | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -38,7 +38,9 @@ else: | |||||||
|  |  | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     from ujson import dumps as json_dumps |     from ujson import dumps as ujson_dumps | ||||||
|  |  | ||||||
|  |     json_dumps = partial(ujson_dumps, escape_forward_slashes=False) | ||||||
| except ImportError: | except ImportError: | ||||||
|     # This is done in order to ensure that the JSON response is |     # This is done in order to ensure that the JSON response is | ||||||
|     # kept consistent across both ujson and inbuilt json usage. |     # kept consistent across both ujson and inbuilt json usage. | ||||||
| @@ -345,7 +347,7 @@ class JSONResponse(HTTPResponse): | |||||||
|         body: Optional[Any] = None, |         body: Optional[Any] = None, | ||||||
|         status: int = 200, |         status: int = 200, | ||||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, |         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||||
|         content_type: Optional[str] = None, |         content_type: str = "application/json", | ||||||
|         dumps: Optional[Callable[..., str]] = None, |         dumps: Optional[Callable[..., str]] = None, | ||||||
|         **kwargs: Any, |         **kwargs: Any, | ||||||
|     ): |     ): | ||||||
| @@ -520,7 +522,9 @@ class ResponseStream: | |||||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, |         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||||
|         content_type: Optional[str] = None, |         content_type: Optional[str] = None, | ||||||
|     ): |     ): | ||||||
|         if not isinstance(headers, Header): |         if headers is None: | ||||||
|  |             headers = Header() | ||||||
|  |         elif not isinstance(headers, Header): | ||||||
|             headers = Header(headers) |             headers = Header(headers) | ||||||
|         self.streaming_fn = streaming_fn |         self.streaming_fn = streaming_fn | ||||||
|         self.status = status |         self.status = status | ||||||
|   | |||||||
| @@ -75,11 +75,12 @@ class Router(BaseRouter): | |||||||
|         strict_slashes: bool = False, |         strict_slashes: bool = False, | ||||||
|         stream: bool = False, |         stream: bool = False, | ||||||
|         ignore_body: bool = False, |         ignore_body: bool = False, | ||||||
|         version: Union[str, float, int] = None, |         version: Optional[Union[str, float, int]] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         unquote: bool = False, |         unquote: bool = False, | ||||||
|         static: bool = False, |         static: bool = False, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|  |         overwrite: bool = False, | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|     ) -> Union[Route, List[Route]]: |     ) -> Union[Route, List[Route]]: | ||||||
|         """ |         """ | ||||||
| @@ -122,6 +123,7 @@ class Router(BaseRouter): | |||||||
|             name=name, |             name=name, | ||||||
|             strict=strict_slashes, |             strict=strict_slashes, | ||||||
|             unquote=unquote, |             unquote=unquote, | ||||||
|  |             overwrite=overwrite, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         if isinstance(host, str): |         if isinstance(host, str): | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
| @@ -251,8 +249,7 @@ def _serve_http_1( | |||||||
|             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) | ||||||
|             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 | ||||||
|   | |||||||
| @@ -96,6 +96,7 @@ class WebsocketFrameAssembler: | |||||||
|         If ``timeout`` is set and elapses before a complete message is |         If ``timeout`` is set and elapses before a complete message is | ||||||
|         received, :meth:`get` returns ``None``. |         received, :meth:`get` returns ``None``. | ||||||
|         """ |         """ | ||||||
|  |         completed: bool | ||||||
|         async with self.read_mutex: |         async with self.read_mutex: | ||||||
|             if timeout is not None and timeout <= 0: |             if timeout is not None and timeout <= 0: | ||||||
|                 if not self.message_complete.is_set(): |                 if not self.message_complete.is_set(): | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ from websockets.frames import Frame, Opcode | |||||||
|  |  | ||||||
|  |  | ||||||
| try:  # websockets < 11.0 | try:  # websockets < 11.0 | ||||||
|     from websockets.connection import Event, State |     from websockets.connection import Event, State  # type: ignore | ||||||
|     from websockets.server import ServerConnection as ServerProtocol |     from websockets.server import ServerConnection as ServerProtocol | ||||||
| except ImportError:  # websockets >= 11.0 | except ImportError:  # websockets >= 11.0 | ||||||
|     from websockets.protocol import Event, State  # type: ignore |     from websockets.protocol import Event, State  # type: ignore | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ from sanic.models.handler_types import SignalHandler | |||||||
|  |  | ||||||
|  |  | ||||||
| class Event(Enum): | class Event(Enum): | ||||||
|  |     SERVER_EXCEPTION_REPORT = "server.exception.report" | ||||||
|     SERVER_INIT_AFTER = "server.init.after" |     SERVER_INIT_AFTER = "server.init.after" | ||||||
|     SERVER_INIT_BEFORE = "server.init.before" |     SERVER_INIT_BEFORE = "server.init.before" | ||||||
|     SERVER_SHUTDOWN_AFTER = "server.shutdown.after" |     SERVER_SHUTDOWN_AFTER = "server.shutdown.after" | ||||||
| @@ -39,6 +40,7 @@ class Event(Enum): | |||||||
|  |  | ||||||
| RESERVED_NAMESPACES = { | RESERVED_NAMESPACES = { | ||||||
|     "server": ( |     "server": ( | ||||||
|  |         Event.SERVER_EXCEPTION_REPORT.value, | ||||||
|         Event.SERVER_INIT_AFTER.value, |         Event.SERVER_INIT_AFTER.value, | ||||||
|         Event.SERVER_INIT_BEFORE.value, |         Event.SERVER_INIT_BEFORE.value, | ||||||
|         Event.SERVER_SHUTDOWN_AFTER.value, |         Event.SERVER_SHUTDOWN_AFTER.value, | ||||||
| @@ -168,6 +170,17 @@ class SignalRouter(BaseRouter): | |||||||
|                     elif maybe_coroutine: |                     elif maybe_coroutine: | ||||||
|                         return maybe_coroutine |                         return maybe_coroutine | ||||||
|             return None |             return None | ||||||
|  |         except Exception as e: | ||||||
|  |             if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: | ||||||
|  |                 error_logger.exception(e) | ||||||
|  |  | ||||||
|  |             if event != Event.SERVER_EXCEPTION_REPORT.value: | ||||||
|  |                 await self.dispatch( | ||||||
|  |                     Event.SERVER_EXCEPTION_REPORT.value, | ||||||
|  |                     context={"exception": e}, | ||||||
|  |                 ) | ||||||
|  |                 setattr(e, "__dispatched__", True) | ||||||
|  |             raise e | ||||||
|         finally: |         finally: | ||||||
|             for signal_event in events: |             for signal_event in events: | ||||||
|                 signal_event.clear() |                 signal_event.clear() | ||||||
| @@ -217,14 +230,6 @@ class SignalRouter(BaseRouter): | |||||||
|         if not trigger: |         if not trigger: | ||||||
|             event = ".".join([*parts[:2], "<__trigger__>"]) |             event = ".".join([*parts[:2], "<__trigger__>"]) | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             # Attaching __requirements__ and __trigger__ to the handler |  | ||||||
|             # is deprecated and will be removed in v23.6. |  | ||||||
|             handler.__requirements__ = condition  # type: ignore |  | ||||||
|             handler.__trigger__ = trigger  # type: ignore |  | ||||||
|         except AttributeError: |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|         signal = super().add( |         signal = super().add( | ||||||
|             event, |             event, | ||||||
|             handler, |             handler, | ||||||
|   | |||||||
| @@ -83,10 +83,7 @@ class Inspector: | |||||||
|  |  | ||||||
|     async def _respond(self, request: Request, output: Any): |     async def _respond(self, request: Request, output: Any): | ||||||
|         name = request.match_info.get("action", "info") |         name = request.match_info.get("action", "info") | ||||||
|         return json( |         return json({"meta": {"action": name}, "result": output}) | ||||||
|             {"meta": {"action": name}, "result": output}, |  | ||||||
|             escape_forward_slashes=False, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def _state_to_json(self) -> Dict[str, Any]: |     def _state_to_json(self) -> Dict[str, Any]: | ||||||
|         output = {"info": self.app_info} |         output = {"info": self.app_info} | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								setup.py
									
									
									
									
									
								
							| @@ -83,12 +83,11 @@ setup_kwargs = { | |||||||
|     "packages": find_packages(exclude=("tests", "tests.*")), |     "packages": find_packages(exclude=("tests", "tests.*")), | ||||||
|     "package_data": {"sanic": ["py.typed", "pages/styles/*"]}, |     "package_data": {"sanic": ["py.typed", "pages/styles/*"]}, | ||||||
|     "platforms": "any", |     "platforms": "any", | ||||||
|     "python_requires": ">=3.7", |     "python_requires": ">=3.8", | ||||||
|     "classifiers": [ |     "classifiers": [ | ||||||
|         "Development Status :: 4 - Beta", |         "Development Status :: 4 - Beta", | ||||||
|         "Environment :: Web Environment", |         "Environment :: Web Environment", | ||||||
|         "License :: OSI Approved :: MIT License", |         "License :: OSI Approved :: MIT License", | ||||||
|         "Programming Language :: Python :: 3.7", |  | ||||||
|         "Programming Language :: Python :: 3.8", |         "Programming Language :: Python :: 3.8", | ||||||
|         "Programming Language :: Python :: 3.9", |         "Programming Language :: Python :: 3.9", | ||||||
|         "Programming Language :: Python :: 3.10", |         "Programming Language :: Python :: 3.10", | ||||||
| @@ -104,7 +103,7 @@ ujson = "ujson>=1.35" + env_dependency | |||||||
| uvloop = "uvloop>=0.15.0" + env_dependency | uvloop = "uvloop>=0.15.0" + env_dependency | ||||||
| types_ujson = "types-ujson" + env_dependency | types_ujson = "types-ujson" + env_dependency | ||||||
| requirements = [ | requirements = [ | ||||||
|     "sanic-routing>=22.8.0", |     "sanic-routing>=23.6.0", | ||||||
|     "httptools>=0.0.10", |     "httptools>=0.0.10", | ||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
| @@ -113,10 +112,11 @@ requirements = [ | |||||||
|     "multidict>=5.0,<7.0", |     "multidict>=5.0,<7.0", | ||||||
|     "html5tagger>=1.2.1", |     "html5tagger>=1.2.1", | ||||||
|     "tracerite>=1.0.0", |     "tracerite>=1.0.0", | ||||||
|  |     "typing-extensions>=4.4.0", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| tests_require = [ | tests_require = [ | ||||||
|     "sanic-testing>=23.3.0", |     "sanic-testing>=23.6.0", | ||||||
|     "pytest==7.1.*", |     "pytest==7.1.*", | ||||||
|     "coverage", |     "coverage", | ||||||
|     "beautifulsoup4", |     "beautifulsoup4", | ||||||
| @@ -127,7 +127,7 @@ tests_require = [ | |||||||
|     "black", |     "black", | ||||||
|     "isort>=5.0.0", |     "isort>=5.0.0", | ||||||
|     "bandit", |     "bandit", | ||||||
|     "mypy>=0.901,<0.910", |     "mypy", | ||||||
|     "docutils", |     "docutils", | ||||||
|     "pygments", |     "pygments", | ||||||
|     "uvicorn<0.15.0", |     "uvicorn<0.15.0", | ||||||
|   | |||||||
| @@ -293,7 +293,7 @@ def test_handle_request_with_nested_sanic_exception( | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_name_required(): | def test_app_name_required(): | ||||||
|     with pytest.raises(SanicException): |     with pytest.raises(TypeError): | ||||||
|         Sanic() |         Sanic() | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,8 @@ | |||||||
| from sanic import Blueprint, Sanic | import pytest | ||||||
|  |  | ||||||
|  | from sanic_routing.exceptions import RouteExists | ||||||
|  |  | ||||||
|  | from sanic import Blueprint, Request, Sanic | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -74,3 +78,76 @@ def test_bp_copy(app: Sanic): | |||||||
|     assert "test_bp_copy.test_bp4.handle_request" in route_names |     assert "test_bp_copy.test_bp4.handle_request" in route_names | ||||||
|     assert "test_bp_copy.test_bp5.handle_request" in route_names |     assert "test_bp_copy.test_bp5.handle_request" in route_names | ||||||
|     assert "test_bp_copy.test_bp6.handle_request" in route_names |     assert "test_bp_copy.test_bp6.handle_request" in route_names | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_bp_copy_without_route_overwriting(app: Sanic): | ||||||
|  |     bpv1 = Blueprint("bp_v1", version=1, url_prefix="my_api") | ||||||
|  |  | ||||||
|  |     @bpv1.route("/") | ||||||
|  |     async def handler(request: Request): | ||||||
|  |         return text("v1") | ||||||
|  |  | ||||||
|  |     app.blueprint(bpv1) | ||||||
|  |  | ||||||
|  |     bpv2 = bpv1.copy("bp_v2", version=2, allow_route_overwrite=False) | ||||||
|  |     bpv3 = bpv1.copy( | ||||||
|  |         "bp_v3", | ||||||
|  |         version=3, | ||||||
|  |         allow_route_overwrite=False, | ||||||
|  |         with_registration=False, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     with pytest.raises(RouteExists, match="Route already registered*"): | ||||||
|  |  | ||||||
|  |         @bpv2.route("/") | ||||||
|  |         async def handler(request: Request): | ||||||
|  |             return text("v2") | ||||||
|  |  | ||||||
|  |         app.blueprint(bpv2) | ||||||
|  |  | ||||||
|  |     with pytest.raises(RouteExists, match="Route already registered*"): | ||||||
|  |  | ||||||
|  |         @bpv3.route("/") | ||||||
|  |         async def handler(request: Request): | ||||||
|  |             return text("v3") | ||||||
|  |  | ||||||
|  |         app.blueprint(bpv3) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_bp_copy_with_route_overwriting(app: Sanic): | ||||||
|  |     bpv1 = Blueprint("bp_v1", version=1, url_prefix="my_api") | ||||||
|  |  | ||||||
|  |     @bpv1.route("/") | ||||||
|  |     async def handler(request: Request): | ||||||
|  |         return text("v1") | ||||||
|  |  | ||||||
|  |     app.blueprint(bpv1) | ||||||
|  |  | ||||||
|  |     bpv2 = bpv1.copy("bp_v2", version=2, allow_route_overwrite=True) | ||||||
|  |     bpv3 = bpv1.copy( | ||||||
|  |         "bp_v3", version=3, allow_route_overwrite=True, with_registration=False | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @bpv2.route("/") | ||||||
|  |     async def handler(request: Request): | ||||||
|  |         return text("v2") | ||||||
|  |  | ||||||
|  |     app.blueprint(bpv2) | ||||||
|  |  | ||||||
|  |     @bpv3.route("/") | ||||||
|  |     async def handler(request: Request): | ||||||
|  |         return text("v3") | ||||||
|  |  | ||||||
|  |     app.blueprint(bpv3) | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/v1/my_api") | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.text == "v1" | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/v2/my_api") | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.text == "v2" | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/v3/my_api") | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.text == "v3" | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | import pytest | ||||||
|  |  | ||||||
| from pytest import raises | from pytest import raises | ||||||
|  |  | ||||||
| from sanic.app import Sanic | from sanic.app import Sanic | ||||||
| @@ -340,3 +342,40 @@ def test_nested_bp_group_properties(): | |||||||
|  |  | ||||||
|     routes = [route.path for route in app.router.routes] |     routes = [route.path for route in app.router.routes] | ||||||
|     assert routes == ["three/one/four"] |     assert routes == ["three/one/four"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_multiple_nested_bp_group(): | ||||||
|  |     bp1 = Blueprint("bp1", url_prefix="/bp1") | ||||||
|  |     bp2 = Blueprint("bp2", url_prefix="/bp2") | ||||||
|  |  | ||||||
|  |     bp1.add_route(lambda _: ..., "/", name="route1") | ||||||
|  |     bp2.add_route(lambda _: ..., "/", name="route2") | ||||||
|  |  | ||||||
|  |     group_a = Blueprint.group( | ||||||
|  |         bp1, bp2, url_prefix="/group-a", name_prefix="group-a" | ||||||
|  |     ) | ||||||
|  |     group_b = Blueprint.group( | ||||||
|  |         bp1, bp2, url_prefix="/group-b", name_prefix="group-b" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     app = Sanic("PropTest") | ||||||
|  |     app.blueprint(group_a) | ||||||
|  |     app.blueprint(group_b) | ||||||
|  |  | ||||||
|  |     await app._startup() | ||||||
|  |  | ||||||
|  |     routes = [route.path for route in app.router.routes] | ||||||
|  |     assert routes == [ | ||||||
|  |         "group-a/bp1", | ||||||
|  |         "group-a/bp2", | ||||||
|  |         "group-b/bp1", | ||||||
|  |         "group-b/bp2", | ||||||
|  |     ] | ||||||
|  |     names = [route.name for route in app.router.routes] | ||||||
|  |     assert names == [ | ||||||
|  |         "PropTest.group-a_bp1.route1", | ||||||
|  |         "PropTest.group-a_bp2.route2", | ||||||
|  |         "PropTest.group-b_bp1.route1", | ||||||
|  |         "PropTest.group-b_bp2.route2", | ||||||
|  |     ] | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ import logging | |||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
|  | import sanic | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.config import Config | from sanic.config import Config | ||||||
| from sanic.errorpages import TextRenderer, exception_response, guess_mime | from sanic.errorpages import TextRenderer, exception_response, guess_mime | ||||||
| @@ -205,6 +207,27 @@ def test_route_error_response_from_explicit_format(app): | |||||||
|     assert response.content_type == "text/plain; charset=utf-8" |     assert response.content_type == "text/plain; charset=utf-8" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_blueprint_error_response_from_explicit_format(app): | ||||||
|  |     bp = sanic.Blueprint("MyBlueprint") | ||||||
|  |  | ||||||
|  |     @bp.get("/text", error_format="json") | ||||||
|  |     def text_response(request): | ||||||
|  |         raise Exception("oops") | ||||||
|  |         return text("Never gonna see this") | ||||||
|  |  | ||||||
|  |     @bp.get("/json", error_format="text") | ||||||
|  |     def json_response(request): | ||||||
|  |         raise Exception("oops") | ||||||
|  |         return json({"message": "Never gonna see this"}) | ||||||
|  |  | ||||||
|  |     app.blueprint(bp) | ||||||
|  |     _, response = app.test_client.get("/text") | ||||||
|  |     assert response.content_type == "application/json" | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/json") | ||||||
|  |     assert response.content_type == "text/plain; charset=utf-8" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unknown_fallback_format(app): | def test_unknown_fallback_format(app): | ||||||
|     with pytest.raises(SanicException, match="Unknown format: bad"): |     with pytest.raises(SanicException, match="Unknown format: bad"): | ||||||
|         app.config.FALLBACK_ERROR_FORMAT = "bad" |         app.config.FALLBACK_ERROR_FORMAT = "bad" | ||||||
| @@ -527,3 +550,26 @@ def test_guess_mime_logging( | |||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     assert logmsg == expected |     assert logmsg == expected | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     "format,expected", | ||||||
|  |     ( | ||||||
|  |         ("html", "text/html; charset=utf-8"), | ||||||
|  |         ("text", "text/plain; charset=utf-8"), | ||||||
|  |         ("json", "application/json"), | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | def test_exception_header_on_renderers(app: Sanic, format, expected): | ||||||
|  |     app.config.FALLBACK_ERROR_FORMAT = format | ||||||
|  |  | ||||||
|  |     @app.get("/test") | ||||||
|  |     def test(request): | ||||||
|  |         raise SanicException( | ||||||
|  |             "test", status_code=400, headers={"exception": "test"} | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/test") | ||||||
|  |     assert response.status == 400 | ||||||
|  |     assert response.headers.get("exception") == "test" | ||||||
|  |     assert response.content_type == expected | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ from sanic.response import text | |||||||
| CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | ||||||
|  |  | ||||||
| PORT = 42001  # test_keep_alive_timeout_reuse doesn't work with random port | PORT = 42001  # test_keep_alive_timeout_reuse doesn't work with random port | ||||||
|  | MAX_LOOPS = 15 | ||||||
| port_counter = count() | port_counter = count() | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -69,23 +70,35 @@ def test_keep_alive_timeout_reuse(): | |||||||
|     """If the server keep-alive timeout and client keep-alive timeout are |     """If the server keep-alive timeout and client keep-alive timeout are | ||||||
|     both longer than the delay, the client _and_ server will successfully |     both longer than the delay, the client _and_ server will successfully | ||||||
|     reuse the existing connection.""" |     reuse the existing connection.""" | ||||||
|     port = get_port() |     loops = 0 | ||||||
|     loop = asyncio.new_event_loop() |     while True: | ||||||
|     asyncio.set_event_loop(loop) |         port = get_port() | ||||||
|     client = ReusableClient(keep_alive_timeout_app_reuse, loop=loop, port=port) |         loop = asyncio.new_event_loop() | ||||||
|     with client: |         asyncio.set_event_loop(loop) | ||||||
|         headers = {"Connection": "keep-alive"} |         client = ReusableClient( | ||||||
|         request, response = client.get("/1", headers=headers) |             keep_alive_timeout_app_reuse, loop=loop, port=port | ||||||
|         assert response.status == 200 |         ) | ||||||
|         assert response.text == "OK" |         try: | ||||||
|         assert request.protocol.state["requests_count"] == 1 |             with client: | ||||||
|  |                 headers = {"Connection": "keep-alive"} | ||||||
|  |                 request, response = client.get("/1", headers=headers) | ||||||
|  |                 assert response.status == 200 | ||||||
|  |                 assert response.text == "OK" | ||||||
|  |                 assert request.protocol.state["requests_count"] == 1 | ||||||
|  |  | ||||||
|         loop.run_until_complete(aio_sleep(1)) |                 loop.run_until_complete(aio_sleep(1)) | ||||||
|  |  | ||||||
|         request, response = client.get("/1") |                 request, response = client.get("/1") | ||||||
|         assert response.status == 200 |                 assert response.status == 200 | ||||||
|         assert response.text == "OK" |                 assert response.text == "OK" | ||||||
|         assert request.protocol.state["requests_count"] == 2 |                 assert request.protocol.state["requests_count"] == 2 | ||||||
|  |         except OSError: | ||||||
|  |             loops += 1 | ||||||
|  |             if loops > MAX_LOOPS: | ||||||
|  |                 raise | ||||||
|  |             continue | ||||||
|  |         else: | ||||||
|  |             break | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
| @@ -97,23 +110,35 @@ def test_keep_alive_timeout_reuse(): | |||||||
| def test_keep_alive_client_timeout(): | def test_keep_alive_client_timeout(): | ||||||
|     """If the server keep-alive timeout is longer than the client |     """If the server keep-alive timeout is longer than the client | ||||||
|     keep-alive timeout, client will try to create a new connection here.""" |     keep-alive timeout, client will try to create a new connection here.""" | ||||||
|     port = get_port() |     loops = 0 | ||||||
|     loop = asyncio.new_event_loop() |     while True: | ||||||
|     asyncio.set_event_loop(loop) |         try: | ||||||
|     client = ReusableClient( |             port = get_port() | ||||||
|         keep_alive_app_client_timeout, loop=loop, port=port |             loop = asyncio.new_event_loop() | ||||||
|     ) |             asyncio.set_event_loop(loop) | ||||||
|     with client: |             client = ReusableClient( | ||||||
|         headers = {"Connection": "keep-alive"} |                 keep_alive_app_client_timeout, loop=loop, port=port | ||||||
|         request, response = client.get("/1", headers=headers, timeout=1) |             ) | ||||||
|  |             with client: | ||||||
|  |                 headers = {"Connection": "keep-alive"} | ||||||
|  |                 request, response = client.get( | ||||||
|  |                     "/1", headers=headers, timeout=1 | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|         assert response.status == 200 |                 assert response.status == 200 | ||||||
|         assert response.text == "OK" |                 assert response.text == "OK" | ||||||
|         assert request.protocol.state["requests_count"] == 1 |                 assert request.protocol.state["requests_count"] == 1 | ||||||
|  |  | ||||||
|         loop.run_until_complete(aio_sleep(2)) |                 loop.run_until_complete(aio_sleep(2)) | ||||||
|         request, response = client.get("/1", timeout=1) |                 request, response = client.get("/1", timeout=1) | ||||||
|         assert request.protocol.state["requests_count"] == 1 |                 assert request.protocol.state["requests_count"] == 1 | ||||||
|  |         except OSError: | ||||||
|  |             loops += 1 | ||||||
|  |             if loops > MAX_LOOPS: | ||||||
|  |                 raise | ||||||
|  |             continue | ||||||
|  |         else: | ||||||
|  |             break | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
| @@ -125,24 +150,36 @@ def test_keep_alive_server_timeout(): | |||||||
|     keep-alive timeout, the client will either a 'Connection reset' error |     keep-alive timeout, the client will either a 'Connection reset' error | ||||||
|     _or_ a new connection. Depending on how the event-loop handles the |     _or_ a new connection. Depending on how the event-loop handles the | ||||||
|     broken server connection.""" |     broken server connection.""" | ||||||
|     port = get_port() |     loops = 0 | ||||||
|     loop = asyncio.new_event_loop() |     while True: | ||||||
|     asyncio.set_event_loop(loop) |         try: | ||||||
|     client = ReusableClient( |             port = get_port() | ||||||
|         keep_alive_app_server_timeout, loop=loop, port=port |             loop = asyncio.new_event_loop() | ||||||
|     ) |             asyncio.set_event_loop(loop) | ||||||
|     with client: |             client = ReusableClient( | ||||||
|         headers = {"Connection": "keep-alive"} |                 keep_alive_app_server_timeout, loop=loop, port=port | ||||||
|         request, response = client.get("/1", headers=headers, timeout=60) |             ) | ||||||
|  |             with client: | ||||||
|  |                 headers = {"Connection": "keep-alive"} | ||||||
|  |                 request, response = client.get( | ||||||
|  |                     "/1", headers=headers, timeout=60 | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|         assert response.status == 200 |                 assert response.status == 200 | ||||||
|         assert response.text == "OK" |                 assert response.text == "OK" | ||||||
|         assert request.protocol.state["requests_count"] == 1 |                 assert request.protocol.state["requests_count"] == 1 | ||||||
|  |  | ||||||
|         loop.run_until_complete(aio_sleep(3)) |                 loop.run_until_complete(aio_sleep(3)) | ||||||
|         request, response = client.get("/1", timeout=60) |                 request, response = client.get("/1", timeout=60) | ||||||
|  |  | ||||||
|         assert request.protocol.state["requests_count"] == 1 |                 assert request.protocol.state["requests_count"] == 1 | ||||||
|  |         except OSError: | ||||||
|  |             loops += 1 | ||||||
|  |             if loops > MAX_LOOPS: | ||||||
|  |                 raise | ||||||
|  |             continue | ||||||
|  |         else: | ||||||
|  |             break | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
| @@ -150,20 +187,34 @@ def test_keep_alive_server_timeout(): | |||||||
|     reason="Not testable with current client", |     reason="Not testable with current client", | ||||||
| ) | ) | ||||||
| def test_keep_alive_connection_context(): | def test_keep_alive_connection_context(): | ||||||
|     port = get_port() |     loops = 0 | ||||||
|     loop = asyncio.new_event_loop() |     while True: | ||||||
|     asyncio.set_event_loop(loop) |         try: | ||||||
|     client = ReusableClient(keep_alive_app_context, loop=loop, port=port) |             port = get_port() | ||||||
|     with client: |             loop = asyncio.new_event_loop() | ||||||
|         headers = {"Connection": "keep-alive"} |             asyncio.set_event_loop(loop) | ||||||
|         request1, _ = client.post("/ctx", headers=headers) |             client = ReusableClient( | ||||||
|  |                 keep_alive_app_context, loop=loop, port=port | ||||||
|  |             ) | ||||||
|  |             with client: | ||||||
|  |                 headers = {"Connection": "keep-alive"} | ||||||
|  |                 request1, _ = client.post("/ctx", headers=headers) | ||||||
|  |  | ||||||
|         loop.run_until_complete(aio_sleep(1)) |                 loop.run_until_complete(aio_sleep(1)) | ||||||
|         request2, response = client.get("/ctx") |                 request2, response = client.get("/ctx") | ||||||
|  |  | ||||||
|         assert response.text == "hello" |                 assert response.text == "hello" | ||||||
|         assert id(request1.conn_info.ctx) == id(request2.conn_info.ctx) |                 assert id(request1.conn_info.ctx) == id(request2.conn_info.ctx) | ||||||
|         assert ( |                 assert ( | ||||||
|             request1.conn_info.ctx.foo == request2.conn_info.ctx.foo == "hello" |                     request1.conn_info.ctx.foo | ||||||
|         ) |                     == request2.conn_info.ctx.foo | ||||||
|         assert request2.protocol.state["requests_count"] == 2 |                     == "hello" | ||||||
|  |                 ) | ||||||
|  |                 assert request2.protocol.state["requests_count"] == 2 | ||||||
|  |         except OSError: | ||||||
|  |             loops += 1 | ||||||
|  |             if loops > MAX_LOOPS: | ||||||
|  |                 raise | ||||||
|  |             continue | ||||||
|  |         else: | ||||||
|  |             break | ||||||
|   | |||||||
| @@ -31,10 +31,11 @@ def test_motd_with_expected_info(app, run_startup): | |||||||
|     logs = run_startup(app) |     logs = run_startup(app) | ||||||
|  |  | ||||||
|     assert logs[1][2] == f"Sanic v{__version__}" |     assert logs[1][2] == f"Sanic v{__version__}" | ||||||
|     assert logs[3][2] == "mode: debug, single worker" |     assert logs[3][2] == "app: test_motd_with_expected_info" | ||||||
|     assert logs[4][2] == "server: sanic, HTTP/1.1" |     assert logs[4][2] == "mode: debug, single worker" | ||||||
|     assert logs[5][2] == f"python: {platform.python_version()}" |     assert logs[5][2] == "server: sanic, HTTP/1.1" | ||||||
|     assert logs[6][2] == f"platform: {platform.platform()}" |     assert logs[6][2] == f"python: {platform.python_version()}" | ||||||
|  |     assert logs[7][2] == f"platform: {platform.platform()}" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_motd_init(): | def test_motd_init(): | ||||||
| @@ -61,7 +62,7 @@ def test_motd_display(caplog): | |||||||
|   │                                │ |   │                                │ | ||||||
|   ├───────────────────────┬────────┤ |   ├───────────────────────┬────────┤ | ||||||
|   │        foobar         │ one: 1 │ |   │        foobar         │ one: 1 │ | ||||||
|   |                       ├────────┤ |   │                       ├────────┤ | ||||||
|   │                       │ two: 2 │ |   │                       │ two: 2 │ | ||||||
|   └───────────────────────┴────────┘ |   └───────────────────────┴────────┘ | ||||||
| """ | """ | ||||||
|   | |||||||
| @@ -126,7 +126,7 @@ def test_redirect_with_params(app, test_str): | |||||||
|  |  | ||||||
|     @app.route("/api/v2/test/<test>/", unquote=True) |     @app.route("/api/v2/test/<test>/", unquote=True) | ||||||
|     async def target_handler(request, test): |     async def target_handler(request, test): | ||||||
|         assert test == test_str |         assert test == quote(test_str) | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     _, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/") |     _, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/") | ||||||
|   | |||||||
| @@ -310,3 +310,29 @@ def test_request_idempotent(method, idempotent): | |||||||
| def test_request_cacheable(method, cacheable): | def test_request_cacheable(method, cacheable): | ||||||
|     request = Request(b"/", {}, None, method, None, None) |     request = Request(b"/", {}, None, method, None, None) | ||||||
|     assert request.is_cacheable is cacheable |     assert request.is_cacheable is cacheable | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_custom_ctx(): | ||||||
|  |     class CustomContext: | ||||||
|  |         FOO = "foo" | ||||||
|  |  | ||||||
|  |     class CustomRequest(Request[Sanic, CustomContext]): | ||||||
|  |         @staticmethod | ||||||
|  |         def make_context() -> CustomContext: | ||||||
|  |             return CustomContext() | ||||||
|  |  | ||||||
|  |     app = Sanic("Test", request_class=CustomRequest) | ||||||
|  |  | ||||||
|  |     @app.get("/") | ||||||
|  |     async def handler(request: CustomRequest): | ||||||
|  |         return response.json( | ||||||
|  |             [ | ||||||
|  |                 isinstance(request, CustomRequest), | ||||||
|  |                 isinstance(request.ctx, CustomContext), | ||||||
|  |                 request.ctx.FOO, | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     _, resp = app.test_client.get("/") | ||||||
|  |  | ||||||
|  |     assert resp.json == [True, True, "foo"] | ||||||
|   | |||||||
| @@ -513,6 +513,7 @@ def test_standard_forwarded(app): | |||||||
|     request, response = app.test_client.get("/", headers=headers) |     request, response = app.test_client.get("/", headers=headers) | ||||||
|     assert response.json == {"for": "127.0.0.2", "proto": "ws"} |     assert response.json == {"for": "127.0.0.2", "proto": "ws"} | ||||||
|     assert request.remote_addr == "127.0.0.2" |     assert request.remote_addr == "127.0.0.2" | ||||||
|  |     assert request.client_ip == "127.0.0.2" | ||||||
|     assert request.scheme == "ws" |     assert request.scheme == "ws" | ||||||
|     assert request.server_name == "local.site" |     assert request.server_name == "local.site" | ||||||
|     assert request.server_port == 80 |     assert request.server_port == 80 | ||||||
| @@ -737,6 +738,7 @@ def test_remote_addr_with_two_proxies(app): | |||||||
|     headers = {"X-Forwarded-For": "127.0.1.1"} |     headers = {"X-Forwarded-For": "127.0.1.1"} | ||||||
|     request, response = app.test_client.get("/", headers=headers) |     request, response = app.test_client.get("/", headers=headers) | ||||||
|     assert request.remote_addr == "" |     assert request.remote_addr == "" | ||||||
|  |     assert request.client_ip == "127.0.0.1" | ||||||
|     assert response.body == b"" |     assert response.body == b"" | ||||||
|  |  | ||||||
|     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} |     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ from sanic.compat import Header | |||||||
| from sanic.cookies import CookieJar | from sanic.cookies import CookieJar | ||||||
| from sanic.response import ( | from sanic.response import ( | ||||||
|     HTTPResponse, |     HTTPResponse, | ||||||
|  |     ResponseStream, | ||||||
|     empty, |     empty, | ||||||
|     file, |     file, | ||||||
|     file_stream, |     file_stream, | ||||||
| @@ -943,3 +944,17 @@ def test_file_validating_304_response( | |||||||
|     ) |     ) | ||||||
|     assert response.status == 304 |     assert response.status == 304 | ||||||
|     assert response.body == b"" |     assert response.body == b"" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_stream_response_with_default_headers(app: Sanic): | ||||||
|  |     async def sample_streaming_fn(response_): | ||||||
|  |         await response_.write("foo") | ||||||
|  |  | ||||||
|  |     @app.route("/") | ||||||
|  |     async def test(request: Request): | ||||||
|  |         return ResponseStream(sample_streaming_fn, content_type="text/csv") | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/") | ||||||
|  |     assert response.text == "foo" | ||||||
|  |     assert response.headers["Transfer-Encoding"] == "chunked" | ||||||
|  |     assert response.headers["Content-Type"] == "text/csv" | ||||||
|   | |||||||
| @@ -213,3 +213,12 @@ def test_pop_list(json_app: Sanic): | |||||||
|  |  | ||||||
|     _, resp = json_app.test_client.get("/json-pop") |     _, resp = json_app.test_client.get("/json-pop") | ||||||
|     assert resp.body == json_dumps(["b"]).encode() |     assert resp.body == json_dumps(["b"]).encode() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_json_response_class_sets_proper_content_type(json_app: Sanic): | ||||||
|  |     @json_app.get("/json-class") | ||||||
|  |     async def handler(request: Request): | ||||||
|  |         return JSONResponse(JSON_BODY) | ||||||
|  |  | ||||||
|  |     _, resp = json_app.test_client.get("/json-class") | ||||||
|  |     assert resp.headers["content-type"] == "application/json" | ||||||
|   | |||||||
| @@ -4,15 +4,18 @@ import signal | |||||||
|  |  | ||||||
| from queue import Queue | from queue import Queue | ||||||
| from types import SimpleNamespace | from types import SimpleNamespace | ||||||
|  | from typing import Optional | ||||||
| from unittest.mock import MagicMock | from unittest.mock import MagicMock | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic_testing.testing import HOST, PORT | from sanic_testing.testing import HOST, PORT | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
| from sanic.compat import ctrlc_workaround_for_windows | from sanic.compat import ctrlc_workaround_for_windows | ||||||
| from sanic.exceptions import BadRequest | from sanic.exceptions import BadRequest, ServerError | ||||||
| from sanic.response import HTTPResponse | from sanic.response import HTTPResponse | ||||||
|  | from sanic.signals import Event | ||||||
|  |  | ||||||
|  |  | ||||||
| async def stop(app, loop): | async def stop(app, loop): | ||||||
| @@ -148,3 +151,26 @@ def test_signals_with_invalid_invocation(app): | |||||||
|         BadRequest, match="Invalid event registration: Missing event name" |         BadRequest, match="Invalid event registration: Missing event name" | ||||||
|     ): |     ): | ||||||
|         app.listener(stop) |         app.listener(stop) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_signal_server_lifecycle_exception(app: Sanic): | ||||||
|  |     trigger: Optional[Exception] = None | ||||||
|  |  | ||||||
|  |     @app.route("/hello") | ||||||
|  |     async def hello_route(request): | ||||||
|  |         return HTTPResponse() | ||||||
|  |  | ||||||
|  |     @app.signal(Event.SERVER_EXCEPTION_REPORT) | ||||||
|  |     async def test_signal(exception: Exception): | ||||||
|  |         nonlocal trigger | ||||||
|  |         trigger = exception | ||||||
|  |  | ||||||
|  |     @app.before_server_start | ||||||
|  |     async def test_before_server_start(app): | ||||||
|  |         raise ServerError("test_before_server_start") | ||||||
|  |  | ||||||
|  |     with pytest.raises(ServerError, match="test_before_server_start"): | ||||||
|  |         app.run(single_process=True) | ||||||
|  |  | ||||||
|  |     assert isinstance(trigger, ServerError) | ||||||
|  |     assert str(trigger) == "test_before_server_start" | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import asyncio | |||||||
|  |  | ||||||
| from enum import Enum | from enum import Enum | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
|  | from itertools import count | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| @@ -9,6 +10,7 @@ from sanic_routing.exceptions import NotFound | |||||||
|  |  | ||||||
| from sanic import Blueprint, Sanic, empty | from sanic import Blueprint, Sanic, empty | ||||||
| from sanic.exceptions import InvalidSignal, SanicException | from sanic.exceptions import InvalidSignal, SanicException | ||||||
|  | from sanic.signals import Event | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_add_signal(app): | def test_add_signal(app): | ||||||
| @@ -427,3 +429,114 @@ def test_signal_reservation(app, event, expected): | |||||||
|             app.signal(event)(lambda: ...) |             app.signal(event)(lambda: ...) | ||||||
|     else: |     else: | ||||||
|         app.signal(event)(lambda: ...) |         app.signal(event)(lambda: ...) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_report_exception(app: Sanic): | ||||||
|  |     @app.report_exception | ||||||
|  |     async def catch_any_exception(app: Sanic, exception: Exception): | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |     @app.route("/") | ||||||
|  |     async def handler(request): | ||||||
|  |         1 / 0 | ||||||
|  |  | ||||||
|  |     app.signal_router.finalize() | ||||||
|  |  | ||||||
|  |     registered_signal_handlers = [ | ||||||
|  |         handler | ||||||
|  |         for handler, *_ in app.signal_router.get( | ||||||
|  |             Event.SERVER_EXCEPTION_REPORT.value | ||||||
|  |         ) | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     assert catch_any_exception in registered_signal_handlers | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_report_exception_runs(app: Sanic): | ||||||
|  |     event = asyncio.Event() | ||||||
|  |  | ||||||
|  |     @app.report_exception | ||||||
|  |     async def catch_any_exception(app: Sanic, exception: Exception): | ||||||
|  |         event.set() | ||||||
|  |  | ||||||
|  |     @app.route("/") | ||||||
|  |     async def handler(request): | ||||||
|  |         1 / 0 | ||||||
|  |  | ||||||
|  |     app.test_client.get("/") | ||||||
|  |  | ||||||
|  |     assert event.is_set() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_report_exception_runs_once_inline(app: Sanic): | ||||||
|  |     event = asyncio.Event() | ||||||
|  |     c = count() | ||||||
|  |  | ||||||
|  |     @app.report_exception | ||||||
|  |     async def catch_any_exception(app: Sanic, exception: Exception): | ||||||
|  |         event.set() | ||||||
|  |         next(c) | ||||||
|  |  | ||||||
|  |     @app.route("/") | ||||||
|  |     async def handler(request): | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |     @app.signal(Event.HTTP_ROUTING_AFTER.value) | ||||||
|  |     async def after_routing(**_): | ||||||
|  |         1 / 0 | ||||||
|  |  | ||||||
|  |     app.test_client.get("/") | ||||||
|  |  | ||||||
|  |     assert event.is_set() | ||||||
|  |     assert next(c) == 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_report_exception_runs_once_custom(app: Sanic): | ||||||
|  |     event = asyncio.Event() | ||||||
|  |     c = count() | ||||||
|  |  | ||||||
|  |     @app.report_exception | ||||||
|  |     async def catch_any_exception(app: Sanic, exception: Exception): | ||||||
|  |         event.set() | ||||||
|  |         next(c) | ||||||
|  |  | ||||||
|  |     @app.route("/") | ||||||
|  |     async def handler(request): | ||||||
|  |         await app.dispatch("one.two.three") | ||||||
|  |         return empty() | ||||||
|  |  | ||||||
|  |     @app.signal("one.two.three") | ||||||
|  |     async def one_two_three(**_): | ||||||
|  |         1 / 0 | ||||||
|  |  | ||||||
|  |     app.test_client.get("/") | ||||||
|  |  | ||||||
|  |     assert event.is_set() | ||||||
|  |     assert next(c) == 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_report_exception_runs_task(app: Sanic): | ||||||
|  |     c = count() | ||||||
|  |  | ||||||
|  |     async def task_1(): | ||||||
|  |         next(c) | ||||||
|  |  | ||||||
|  |     async def task_2(app): | ||||||
|  |         next(c) | ||||||
|  |  | ||||||
|  |     @app.report_exception | ||||||
|  |     async def catch_any_exception(app: Sanic, exception: Exception): | ||||||
|  |         next(c) | ||||||
|  |  | ||||||
|  |     @app.route("/") | ||||||
|  |     async def handler(request): | ||||||
|  |         app.add_task(task_1) | ||||||
|  |         app.add_task(task_1()) | ||||||
|  |         app.add_task(task_2) | ||||||
|  |         app.add_task(task_2(app)) | ||||||
|  |         return empty() | ||||||
|  |  | ||||||
|  |     app.test_client.get("/") | ||||||
|  |  | ||||||
|  |     assert next(c) == 4 | ||||||
|   | |||||||
| @@ -101,6 +101,31 @@ def test_static_file_pathlib(app, static_file_directory, file_name): | |||||||
|     assert response.body == get_file_content(static_file_directory, file_name) |     assert response.body == get_file_content(static_file_directory, file_name) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     "file_name", | ||||||
|  |     [ | ||||||
|  |         "test.file", | ||||||
|  |         "decode me.txt", | ||||||
|  |         "python.png", | ||||||
|  |         "symlink", | ||||||
|  |         "hard_link", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
|  | def test_static_file_pathlib_relative_path_traversal( | ||||||
|  |     app, static_file_directory, file_name | ||||||
|  | ): | ||||||
|  |     """Get the current working directory and check if it ends with "sanic" """ | ||||||
|  |     cwd = Path.cwd() | ||||||
|  |     if not str(cwd).endswith("sanic"): | ||||||
|  |         pytest.skip("Current working directory does not end with 'sanic'") | ||||||
|  |  | ||||||
|  |     file_path = "./tests/static/../static/" | ||||||
|  |     app.static("/", file_path) | ||||||
|  |     _, response = app.test_client.get(f"/{file_name}") | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.body == get_file_content(static_file_directory, file_name) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "file_name", |     "file_name", | ||||||
|     [b"test.file", b"decode me.txt", b"python.png"], |     [b"test.file", b"decode me.txt", b"python.png"], | ||||||
| @@ -492,7 +517,7 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog): | |||||||
|     counter = Counter([(r[0], r[1]) for r in caplog.record_tuples]) |     counter = Counter([(r[0], r[1]) for r in caplog.record_tuples]) | ||||||
|  |  | ||||||
|     assert response.status == 404 |     assert response.status == 404 | ||||||
|     assert counter[("sanic.root", logging.INFO)] == 9 |     assert counter[("sanic.root", logging.INFO)] == 10 | ||||||
|     assert counter[("sanic.root", logging.ERROR)] == 0 |     assert counter[("sanic.root", logging.ERROR)] == 0 | ||||||
|     assert counter[("sanic.error", logging.ERROR)] == 0 |     assert counter[("sanic.error", logging.ERROR)] == 0 | ||||||
|     assert counter[("sanic.server", logging.INFO)] == 2 |     assert counter[("sanic.server", logging.INFO)] == 2 | ||||||
| @@ -511,7 +536,7 @@ def test_no_stack_trace_on_not_found(app, static_file_directory, caplog): | |||||||
|     counter = Counter([(r[0], r[1]) for r in caplog.record_tuples]) |     counter = Counter([(r[0], r[1]) for r in caplog.record_tuples]) | ||||||
|  |  | ||||||
|     assert response.status == 404 |     assert response.status == 404 | ||||||
|     assert counter[("sanic.root", logging.INFO)] == 9 |     assert counter[("sanic.root", logging.INFO)] == 10 | ||||||
|     assert counter[("sanic.root", logging.ERROR)] == 0 |     assert counter[("sanic.root", logging.ERROR)] == 0 | ||||||
|     assert counter[("sanic.error", logging.ERROR)] == 0 |     assert counter[("sanic.error", logging.ERROR)] == 0 | ||||||
|     assert counter[("sanic.server", logging.INFO)] == 2 |     assert counter[("sanic.server", logging.INFO)] == 2 | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								tests/typing/samples/app_custom_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/typing/samples/app_custom_config.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.config import Config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomConfig(Config): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app = Sanic("test", config=CustomConfig()) | ||||||
|  | reveal_type(app) | ||||||
							
								
								
									
										9
									
								
								tests/typing/samples/app_custom_ctx.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tests/typing/samples/app_custom_ctx.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Foo: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app = Sanic("test", ctx=Foo()) | ||||||
|  | reveal_type(app) | ||||||
							
								
								
									
										5
									
								
								tests/typing/samples/app_default.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/typing/samples/app_default.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app = Sanic("test") | ||||||
|  | reveal_type(app) | ||||||
							
								
								
									
										14
									
								
								tests/typing/samples/app_fully_custom.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								tests/typing/samples/app_fully_custom.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.config import Config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomConfig(Config): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Foo: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app = Sanic("test", config=CustomConfig(), ctx=Foo()) | ||||||
|  | reveal_type(app) | ||||||
							
								
								
									
										17
									
								
								tests/typing/samples/request_custom_ctx.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tests/typing/samples/request_custom_ctx.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | from types import SimpleNamespace | ||||||
|  |  | ||||||
|  | from sanic import Request, Sanic | ||||||
|  | from sanic.config import Config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Foo: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app = Sanic("test") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("/") | ||||||
|  | async def handler(request: Request[Sanic[Config, SimpleNamespace], Foo]): | ||||||
|  |     reveal_type(request.ctx) | ||||||
|  |     reveal_type(request.app) | ||||||
							
								
								
									
										19
									
								
								tests/typing/samples/request_custom_sanic.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								tests/typing/samples/request_custom_sanic.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | from types import SimpleNamespace | ||||||
|  |  | ||||||
|  | from sanic import Request, Sanic | ||||||
|  | from sanic.config import Config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomConfig(Config): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app = Sanic("test", config=CustomConfig()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("/") | ||||||
|  | async def handler( | ||||||
|  |     request: Request[Sanic[CustomConfig, SimpleNamespace], SimpleNamespace] | ||||||
|  | ): | ||||||
|  |     reveal_type(request.ctx) | ||||||
|  |     reveal_type(request.app) | ||||||
							
								
								
									
										34
									
								
								tests/typing/samples/request_fully_custom.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								tests/typing/samples/request_fully_custom.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | from sanic import Request, Sanic | ||||||
|  | from sanic.config import Config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomConfig(Config): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Foo: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestContext: | ||||||
|  |     foo: Foo | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomRequest(Request[Sanic[CustomConfig, Foo], RequestContext]): | ||||||
|  |     @staticmethod | ||||||
|  |     def make_context() -> RequestContext: | ||||||
|  |         ctx = RequestContext() | ||||||
|  |         ctx.foo = Foo() | ||||||
|  |         return ctx | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app = Sanic( | ||||||
|  |     "test", config=CustomConfig(), ctx=Foo(), request_class=CustomRequest | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("/") | ||||||
|  | async def handler(request: CustomRequest): | ||||||
|  |     reveal_type(request) | ||||||
|  |     reveal_type(request.ctx) | ||||||
|  |     reveal_type(request.app) | ||||||
							
								
								
									
										127
									
								
								tests/typing/test_typing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								tests/typing/test_typing.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | |||||||
|  | # flake8: noqa: E501 | ||||||
|  |  | ||||||
|  | import subprocess | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  | from pathlib import Path | ||||||
|  | from typing import List, Tuple | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CURRENT_DIR = Path(__file__).parent | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def run_check(path_location: str) -> str: | ||||||
|  |     """Use mypy to check the given path location and return the output.""" | ||||||
|  |  | ||||||
|  |     mypy_path = "mypy" | ||||||
|  |     path = CURRENT_DIR / path_location | ||||||
|  |     command = [mypy_path, path.resolve().as_posix()] | ||||||
|  |  | ||||||
|  |     process = subprocess.run( | ||||||
|  |         command, | ||||||
|  |         stdout=subprocess.PIPE, | ||||||
|  |         stderr=subprocess.PIPE, | ||||||
|  |         universal_newlines=True, | ||||||
|  |     ) | ||||||
|  |     output = process.stdout + process.stderr | ||||||
|  |     return output | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     "path_location,expected", | ||||||
|  |     ( | ||||||
|  |         ( | ||||||
|  |             "app_default.py", | ||||||
|  |             [ | ||||||
|  |                 ( | ||||||
|  |                     "sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]", | ||||||
|  |                     5, | ||||||
|  |                 ) | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "app_custom_config.py", | ||||||
|  |             [ | ||||||
|  |                 ( | ||||||
|  |                     "sanic.app.Sanic[app_custom_config.CustomConfig, types.SimpleNamespace]", | ||||||
|  |                     10, | ||||||
|  |                 ) | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "app_custom_ctx.py", | ||||||
|  |             [("sanic.app.Sanic[sanic.config.Config, app_custom_ctx.Foo]", 9)], | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "app_fully_custom.py", | ||||||
|  |             [ | ||||||
|  |                 ( | ||||||
|  |                     "sanic.app.Sanic[app_fully_custom.CustomConfig, app_fully_custom.Foo]", | ||||||
|  |                     14, | ||||||
|  |                 ) | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "request_custom_sanic.py", | ||||||
|  |             [ | ||||||
|  |                 ("types.SimpleNamespace", 18), | ||||||
|  |                 ( | ||||||
|  |                     "sanic.app.Sanic[request_custom_sanic.CustomConfig, types.SimpleNamespace]", | ||||||
|  |                     19, | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "request_custom_ctx.py", | ||||||
|  |             [ | ||||||
|  |                 ("request_custom_ctx.Foo", 16), | ||||||
|  |                 ( | ||||||
|  |                     "sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]", | ||||||
|  |                     17, | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "request_fully_custom.py", | ||||||
|  |             [ | ||||||
|  |                 ("request_fully_custom.CustomRequest", 32), | ||||||
|  |                 ("request_fully_custom.RequestContext", 33), | ||||||
|  |                 ( | ||||||
|  |                     "sanic.app.Sanic[request_fully_custom.CustomConfig, request_fully_custom.Foo]", | ||||||
|  |                     34, | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | def test_check_app_default( | ||||||
|  |     path_location: str, expected: List[Tuple[str, int]] | ||||||
|  | ) -> None: | ||||||
|  |     output = run_check(f"samples/{path_location}") | ||||||
|  |  | ||||||
|  |     for text, number in expected: | ||||||
|  |         current = CURRENT_DIR / f"samples/{path_location}" | ||||||
|  |         path = current.relative_to(CURRENT_DIR.parent) | ||||||
|  |  | ||||||
|  |         target = Path.cwd() | ||||||
|  |         while True: | ||||||
|  |             note = _text_from_path(current, path, target, number, text) | ||||||
|  |             try: | ||||||
|  |                 assert note in output, output | ||||||
|  |             except AssertionError: | ||||||
|  |                 target = target.parent | ||||||
|  |                 if not target.exists(): | ||||||
|  |                     raise | ||||||
|  |             else: | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _text_from_path( | ||||||
|  |     base: Path, path: Path, target: Path, number: int, text: str | ||||||
|  | ) -> str: | ||||||
|  |     relative_to_cwd = base.relative_to(target) | ||||||
|  |     prefix = ".".join(relative_to_cwd.parts[:-1]) | ||||||
|  |     text = text.replace(path.stem, f"{prefix}.{path.stem}") | ||||||
|  |     return f'{path}:{number}: note: Revealed type is "{text}"' | ||||||
							
								
								
									
										8
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,14 +1,14 @@ | |||||||
| [tox] | [tox] | ||||||
| envlist = py37, py38, py39, py310, py311, pyNightly, pypy37, {py37,py38,py39,py310,py311,pyNightly,pypy37}-no-ext, lint, check, security, docs, type-checking | envlist = py38, py39, py310, py311, pyNightly, pypy310, {py38,py39,py310,py311,pyNightly,pypy310}-no-ext, lint, check, security, docs, type-checking | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
| usedevelop = true | usedevelop = true | ||||||
| setenv = | setenv = | ||||||
|     {py37,py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UJSON=1 |     {py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UJSON=1 | ||||||
|     {py37,py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 |     {py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 | ||||||
| extras = test, http3 | extras = test, http3 | ||||||
| deps = | deps = | ||||||
|     httpx==0.23 |     httpx>=0.23 | ||||||
| allowlist_externals = | allowlist_externals = | ||||||
|     pytest |     pytest | ||||||
|     coverage |     coverage | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user