Compare commits
	
		
			18 Commits
		
	
	
		
			start-rest
			...
			motd_addre
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 0489dd9aaf | ||
|   | 47215d4635 | ||
|   | 38ff9069f3 | ||
|   | 4dde4572ec | ||
|   | 31d14704cb | ||
|   | 6a89f4b2fe | ||
|   | 16256522f6 | ||
|   | 205795d1e8 | ||
|   | 9cbe1fb8ad | ||
|   | 31d7ba8f8c | ||
|   | dc3c4d1393 | ||
|   | 929d270569 | ||
|   | 93714df051 | ||
|   | 6e61eab872 | ||
|   | 6848ff24d8 | ||
|   | 666371bb92 | ||
|   | 4a2b82e42e | ||
|   | 5dd1623192 | 
							
								
								
									
										16
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,14 @@ body: | ||||
|     id: code | ||||
|     attributes: | ||||
|       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: | ||||
|       required: false | ||||
|   - type: textarea | ||||
| @@ -42,11 +49,16 @@ body: | ||||
|         - ASGI | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: input | ||||
|   - type: dropdown | ||||
|     id: os | ||||
|     attributes: | ||||
|       label: Operating System | ||||
|       description: What OS? | ||||
|       options: | ||||
|         - Linux | ||||
|         - MacOS | ||||
|         - Windows | ||||
|         - Other (tell us in the description) | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: input | ||||
|   | ||||
							
								
								
									
										28
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,29 +12,23 @@ on: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|  | ||||
| jobs: | ||||
|   test: | ||||
|     runs-on: ${{ matrix.os }} | ||||
|   coverage: | ||||
|     name: Check coverage | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         python-version: [3.9] | ||||
|         os: [ubuntu-latest] | ||||
|       fail-fast: false | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/setup-python@v1 | ||||
|         with: | ||||
|           python-version: ${{ matrix.python-version }} | ||||
|  | ||||
|       - name: Install dependencies 🔨 | ||||
|         run: | | ||||
|           python -m pip install --upgrade pip | ||||
|           pip install tox | ||||
|       - name: Run coverage | ||||
|         run: tox -e coverage | ||||
|         continue-on-error: true | ||||
|       - uses: codecov/codecov-action@v2 | ||||
|         uses: sanic-org/simple-tox-action@v1 | ||||
|         with: | ||||
|           python-version: "3.11" | ||||
|           tox-env: coverage | ||||
|           ignore-errors: true | ||||
|       - name: Run Codecov | ||||
|         uses: codecov/codecov-action@v3 | ||||
|         with: | ||||
|           files: ./coverage.xml | ||||
|           fail_ci_if_error: false | ||||
|   | ||||
							
								
								
									
										39
									
								
								.github/workflows/on-demand.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										39
									
								
								.github/workflows/on-demand.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,39 +0,0 @@ | ||||
| name: On Demand Task | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|     inputs: | ||||
|       python-version: | ||||
|         description: 'Version of Python to use for running Test' | ||||
|         required: false | ||||
|         default: "3.8" | ||||
|       tox-env: | ||||
|         description: 'Test Environment to Run' | ||||
|         required: true | ||||
|         default: '' | ||||
|       os: | ||||
|         description: 'Operating System to Run Test on' | ||||
|         required: false | ||||
|         default: ubuntu-latest | ||||
| jobs: | ||||
|   onDemand: | ||||
|     name: tox-${{ matrix.config.tox-env }}-on-${{ matrix.os }} | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         os: ["${{ github.event.inputs.os}}"] | ||||
|         config: | ||||
|           - { tox-env: "${{ github.event.inputs.tox-env }}", py-version: "${{ github.event.inputs.python-version }}"} | ||||
|     steps: | ||||
|       - name: Checkout Repository | ||||
|         uses: actions/checkout@v2 | ||||
|  | ||||
|       - name: Run tests | ||||
|         uses: harshanarayana/custom-actions@main | ||||
|         with: | ||||
|           python-version: ${{ matrix.config.py-version }} | ||||
|           test-infra-tool: tox | ||||
|           test-infra-version: latest | ||||
|           action: tests | ||||
|           test-additional-args: "-e=${{ matrix.config.tox-env }}" | ||||
|           experimental-ignore-error: "yes" | ||||
							
								
								
									
										38
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,38 +0,0 @@ | ||||
| name: Security Analysis | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   bandit: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     name: type-check-${{ matrix.config.python-version }} | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         os: [ubuntu-latest] | ||||
|         config: | ||||
|           - { python-version: 3.7, tox-env: security} | ||||
|           - { python-version: 3.8, tox-env: security} | ||||
|           - { python-version: 3.9, tox-env: security} | ||||
|           - { python-version: "3.10", tox-env: security} | ||||
|           - { python-version: "3.11", tox-env: security} | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v2 | ||||
|         id: checkout-branch | ||||
|  | ||||
|       - name: Run Linter Checks | ||||
|         id: linter-check | ||||
|         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 }}" | ||||
							
								
								
									
										33
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,33 +0,0 @@ | ||||
| name: Document Linter | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   docsLinter: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     name: Lint Documentation | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         config: | ||||
|           - {python-version: "3.10", tox-env: "docs"} | ||||
|       fail-fast: false | ||||
|  | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v2 | ||||
|  | ||||
|       - name: Run Document Linter | ||||
|         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 }}" | ||||
							
								
								
									
										34
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,34 +0,0 @@ | ||||
| name: Linter Checks | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   linter: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     name: lint | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         os: [ubuntu-latest] | ||||
|         config: | ||||
|           - { python-version: "3.10", tox-env: lint} | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v2 | ||||
|         id: checkout-branch | ||||
|  | ||||
|       - name: Run Linter Checks | ||||
|         id: linter-check | ||||
|         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 }}" | ||||
							
								
								
									
										41
									
								
								.github/workflows/pr-python-pypy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										41
									
								
								.github/workflows/pr-python-pypy.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,41 +0,0 @@ | ||||
| name: Python PyPy Tests | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|     inputs: | ||||
|       tox-env: | ||||
|         description: "Tox Env to run on the PyPy Infra" | ||||
|         required: false | ||||
|         default: "pypy37" | ||||
|       pypy-version: | ||||
|         description: "Version of PyPy to use" | ||||
|         required: false | ||||
|         default: "pypy-3.7" | ||||
| jobs: | ||||
|   testPyPy: | ||||
|     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         # os: [ubuntu-latest, macos-latest] | ||||
|         os: [ubuntu-latest] | ||||
|         config: | ||||
|           - { | ||||
|               python-version: "${{ github.event.inputs.pypy-version }}", | ||||
|               tox-env: "${{ github.event.inputs.tox-env }}", | ||||
|             } | ||||
|     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 }}" | ||||
|           experimental-ignore-error: "true" | ||||
|           command-timeout: "600000" | ||||
							
								
								
									
										48
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,48 +0,0 @@ | ||||
| name: Python 3.10 Tests | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   testPy310: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         # os: [ubuntu-latest, macos-latest] | ||||
|         os: [ubuntu-latest] | ||||
|         config: | ||||
|           - { | ||||
|               python-version: "3.10", | ||||
|               tox-env: py310, | ||||
|               ignore-error-flake: "false", | ||||
|               command-timeout: "0", | ||||
|             } | ||||
|           - { | ||||
|               python-version: "3.10", | ||||
|               tox-env: py310-no-ext, | ||||
|               ignore-error-flake: "true", | ||||
|               command-timeout: "600000", | ||||
|             } | ||||
|     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 }},-vv=''" | ||||
|           experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}" | ||||
|           command-timeout: "${{ matrix.config.command-timeout }}" | ||||
|           test-failure-retry: "3" | ||||
							
								
								
									
										48
									
								
								.github/workflows/pr-python311.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								.github/workflows/pr-python311.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,48 +0,0 @@ | ||||
| name: Python 3.11 Tests | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   testPy311: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         # os: [ubuntu-latest, macos-latest] | ||||
|         os: [ubuntu-latest] | ||||
|         config: | ||||
|           - { | ||||
|               python-version: "3.11", | ||||
|               tox-env: py311, | ||||
|               ignore-error-flake: "false", | ||||
|               command-timeout: "0", | ||||
|             } | ||||
|           - { | ||||
|               python-version: "3.11", | ||||
|               tox-env: py311-no-ext, | ||||
|               ignore-error-flake: "true", | ||||
|               command-timeout: "600000", | ||||
|             } | ||||
|     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 }},-vv=''" | ||||
|           experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}" | ||||
|           command-timeout: "${{ matrix.config.command-timeout }}" | ||||
|           test-failure-retry: "3" | ||||
							
								
								
									
										36
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,36 +0,0 @@ | ||||
| name: Python 3.7 Tests | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*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" | ||||
							
								
								
									
										36
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,36 +0,0 @@ | ||||
| name: Python 3.8 Tests | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   testPy38: | ||||
|     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.8, tox-env: py38 } | ||||
|           - { python-version: 3.8, tox-env: py38-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" | ||||
							
								
								
									
										48
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,48 +0,0 @@ | ||||
| name: Python 3.9 Tests | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   testPy39: | ||||
|     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.9, | ||||
|               tox-env: py39, | ||||
|               ignore-error-flake: "false", | ||||
|               command-timeout: "0", | ||||
|             } | ||||
|           - { | ||||
|               python-version: 3.9, | ||||
|               tox-env: py39-no-ext, | ||||
|               ignore-error-flake: "true", | ||||
|               command-timeout: "600000", | ||||
|             } | ||||
|     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 }},-vv=''" | ||||
|           experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}" | ||||
|           command-timeout: "${{ matrix.config.command-timeout }}" | ||||
|           test-failure-retry: "3" | ||||
							
								
								
									
										38
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,38 +0,0 @@ | ||||
| name: Typing Checks | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   typeChecking: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     name: type-check-${{ matrix.config.python-version }} | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         os: [ubuntu-latest] | ||||
|         config: | ||||
|           # - { python-version: 3.7, tox-env: type-checking} | ||||
|           - { python-version: 3.8, tox-env: type-checking} | ||||
|           - { python-version: 3.9, tox-env: type-checking} | ||||
|           - { python-version: "3.10", tox-env: type-checking} | ||||
|           - { python-version: "3.11", tox-env: type-checking} | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v2 | ||||
|         id: checkout-branch | ||||
|  | ||||
|       - name: Run Linter Checks | ||||
|         id: linter-check | ||||
|         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 }}" | ||||
							
								
								
									
										40
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										40
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,40 +0,0 @@ | ||||
| name: Run Unit Tests on Windows | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   testsOnWindows: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     name: ut-${{ matrix.config.tox-env }} | ||||
|     runs-on: windows-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         config: | ||||
|           - { python-version: 3.7, tox-env: py37-no-ext } | ||||
|           - { python-version: 3.8, tox-env: py38-no-ext } | ||||
|           - { python-version: 3.9, tox-env: py39-no-ext } | ||||
|           - { python-version: "3.10", 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: | ||||
|       - name: Checkout Repository | ||||
|         uses: actions/checkout@v2 | ||||
|  | ||||
|       - name: Run Unit Tests | ||||
|         uses: ahopkins/custom-actions@pip-extra-args | ||||
|         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 }}" | ||||
|           experimental-ignore-error: "true" | ||||
|           command-timeout: "600000" | ||||
|           pip-extra-args: "--user" | ||||
							
								
								
									
										48
									
								
								.github/workflows/publish-images.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								.github/workflows/publish-images.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,48 +0,0 @@ | ||||
| name: Publish Docker Images | ||||
| on: | ||||
|   workflow_run: | ||||
|     workflows: | ||||
|       - 'Publish Artifacts' | ||||
|     types: | ||||
|       - completed | ||||
|  | ||||
| jobs: | ||||
|   publishDockerImages: | ||||
|     name: Docker Image Build [${{ matrix.python-version }}] | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     strategy: | ||||
|       fail-fast: true | ||||
|       matrix: | ||||
|         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v2 | ||||
|  | ||||
|       - name: Build Latest Base images for ${{ matrix.python-version }} | ||||
|         uses: harshanarayana/custom-actions@main | ||||
|         with: | ||||
|           docker-image-base-name: sanicframework/sanic-build | ||||
|           ignore-python-setup: 'true' | ||||
|           dockerfile-base-dir: './docker' | ||||
|           action: 'image-publish' | ||||
|           docker-image-tag: "${{ matrix.python-version }}" | ||||
|           docker-file-suffix: "base" | ||||
|           docker-build-args: "PYTHON_VERSION=${{ matrix.python-version }}" | ||||
|           registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }} | ||||
|           registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }} | ||||
|           push-images: 'true' | ||||
|  | ||||
|       - name: Publish Sanic Docker Image for ${{ matrix.python-version }} | ||||
|         uses: harshanarayana/custom-actions@main | ||||
|         with: | ||||
|           docker-image-base-name: sanicframework/sanic | ||||
|           ignore-python-setup: 'true' | ||||
|           dockerfile-base-dir: './docker' | ||||
|           action: 'image-publish' | ||||
|           docker-build-args: "BASE_IMAGE_TAG=${{ matrix.python-version }}" | ||||
|           docker-image-prefix: "${{ matrix.python-version }}" | ||||
|           registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }} | ||||
|           registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }} | ||||
|           push-images: 'true' | ||||
							
								
								
									
										28
									
								
								.github/workflows/publish-package.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/publish-package.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,28 +0,0 @@ | ||||
| name: Publish Artifacts | ||||
| on: | ||||
|   release: | ||||
|     types: [created] | ||||
|  | ||||
| jobs: | ||||
|   publishPythonPackage: | ||||
|     name: Publishing Sanic Release Artifacts | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     strategy: | ||||
|       fail-fast: true | ||||
|       matrix: | ||||
|         python-version: ["3.10"] | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout Repository | ||||
|         uses: actions/checkout@v2 | ||||
|  | ||||
|       - name: Publish Python Package | ||||
|         uses: harshanarayana/custom-actions@main | ||||
|         with: | ||||
|           python-version: ${{ matrix.python-version }} | ||||
|           package-infra-name: "twine" | ||||
|           pypi-user: __token__ | ||||
|           pypi-access-token: ${{ secrets.PYPI_ACCESS_TOKEN }} | ||||
|           action: "package-publish" | ||||
|           pypi-verify-metadata: "true" | ||||
							
								
								
									
										174
									
								
								.github/workflows/publish-release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								.github/workflows/publish-release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| name: Publish release | ||||
|  | ||||
| on: | ||||
|   release: | ||||
|     types: [created] | ||||
|  | ||||
| env: | ||||
|   IS_TEST: false | ||||
|   DOCKER_ORG_NAME: sanicframework | ||||
|   DOCKER_IMAGE_NAME: sanic | ||||
|   DOCKER_BASE_IMAGE_NAME: sanic-build | ||||
|   DOCKER_IMAGE_DOCKERFILE: ./docker/Dockerfile | ||||
|   DOCKER_BASE_IMAGE_DOCKERFILE: ./docker/Dockerfile-base | ||||
|  | ||||
| jobs: | ||||
|   generate_info: | ||||
|     name: Generate info | ||||
|     runs-on: ubuntu-latest | ||||
|     outputs: | ||||
|       docker-tags: ${{ steps.generate_docker_info.outputs.tags }} | ||||
|       pypi-version: ${{ steps.parse_version_tag.outputs.pypi-version }} | ||||
|     steps: | ||||
|       - name: Parse version tag | ||||
|         id: parse_version_tag | ||||
|         env: | ||||
|           TAG_NAME: ${{ github.event.release.tag_name }} | ||||
|         run: | | ||||
|           tag_name="${{ env.TAG_NAME }}" | ||||
|  | ||||
|           if [[ ! "${tag_name}" =~ ^v([0-9]{2})\.([0-9]{1,2})\.([0-9]+)$ ]]; then | ||||
|             echo "::error::Tag name must be in the format vYY.MM.MICRO" | ||||
|             exit 1 | ||||
|           fi | ||||
|  | ||||
|           year_output="year=${BASH_REMATCH[1]}" | ||||
|           month_output="month=${BASH_REMATCH[2]}" | ||||
|           pypi_output="pypi-version=${tag_name#v}" | ||||
|  | ||||
|           echo "${year_output}" | ||||
|           echo "${month_output}" | ||||
|           echo "${pypi_output}" | ||||
|  | ||||
|           echo "${year_output}" >> $GITHUB_OUTPUT | ||||
|           echo "${month_output}" >> $GITHUB_OUTPUT | ||||
|           echo "${pypi_output}" >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Get latest release | ||||
|         id: get_latest_release | ||||
|         run: | | ||||
|           latest_tag=$( | ||||
|             curl -L \ | ||||
|               -H "Accept: application/vnd.github+json" \ | ||||
|               -H "Authorization: Bearer ${{ github.token }}" \ | ||||
|               -H "X-GitHub-Api-Version: 2022-11-28" \ | ||||
|               https://api.github.com/repos/${{ github.repository }}/releases/latest \ | ||||
|               | jq -r '.tag_name' | ||||
|           ) | ||||
|           echo "latest_tag=$latest_tag" >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Generate Docker info | ||||
|         id: generate_docker_info | ||||
|         run: | | ||||
|           tag_year="${{ steps.parse_version_tag.outputs.year }}" | ||||
|           tag_month="${{ steps.parse_version_tag.outputs.month }}" | ||||
|           latest_tag="${{ steps.get_latest_release.outputs.latest_tag }}" | ||||
|           tag="${{ github.event.release.tag_name }}" | ||||
|  | ||||
|           tags="${tag_year}.${tag_month}" | ||||
|  | ||||
|           if [[ "${tag_month}" == "12" ]]; then | ||||
|             tags+=",LTS" | ||||
|             echo "::notice::Tag ${tag} is LTS version" | ||||
|           else | ||||
|             echo "::notice::Tag ${tag} is not LTS version" | ||||
|           fi | ||||
|  | ||||
|           if [[ "${latest_tag}" == "${{ github.event.release.tag_name }}" ]]; then | ||||
|             tags+=",latest" | ||||
|             echo "::notice::Tag ${tag} is marked as latest" | ||||
|           else | ||||
|             echo "::notice::Tag ${tag} is not marked as latest" | ||||
|           fi | ||||
|  | ||||
|           tags_output="tags=${tags}" | ||||
|  | ||||
|           echo "${tags_output}" | ||||
|           echo "${tags_output}" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   publish_package: | ||||
|     name: Build and publish package | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: generate_info | ||||
|     steps: | ||||
|     - name: Checkout repo | ||||
|       uses: actions/checkout@v3 | ||||
|  | ||||
|     - name: Setup Python | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: "3.11" | ||||
|  | ||||
|     - name: Install dependencies | ||||
|       run: pip install build twine | ||||
|  | ||||
|     - name: Update package version | ||||
|       run: | | ||||
|         echo "__version__ = \"${{ needs.generate_info.outputs.pypi-version }}\"" > sanic/__version__.py | ||||
|  | ||||
|     - name: Build a binary wheel and a source tarball | ||||
|       run: python -m build --sdist --wheel --outdir dist/ . | ||||
|  | ||||
|     - name: Publish to PyPi 🚀 | ||||
|       run: twine upload --non-interactive --disable-progress-bar dist/* | ||||
|       env: | ||||
|         TWINE_USERNAME: __token__ | ||||
|         TWINE_PASSWORD: ${{ env.IS_TEST == 'true' && secrets.SANIC_TEST_PYPI_API_TOKEN || secrets.SANIC_PYPI_API_TOKEN }} | ||||
|         TWINE_REPOSITORY: ${{ env.IS_TEST == 'true' && 'testpypi' || 'pypi' }} | ||||
|  | ||||
|   publish_docker: | ||||
|     name: Publish Docker / Python ${{ matrix.python-version }} | ||||
|     needs: [generate_info, publish_package] | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: true | ||||
|       matrix: | ||||
|         python-version: ["3.10", "3.11"] | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|  | ||||
|       - name: Login to Docker Hub | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKER_ACCESS_USER }} | ||||
|           password: ${{ secrets.DOCKER_ACCESS_TOKEN }} | ||||
|  | ||||
|       - name: Build and push base image | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           push: ${{ env.IS_TEST == 'false' }} | ||||
|           file: ${{ env.DOCKER_BASE_IMAGE_DOCKERFILE }} | ||||
|           tags: ${{ env.DOCKER_ORG_NAME }}/${{ env.DOCKER_BASE_IMAGE_NAME }}:${{ matrix.python-version }} | ||||
|           build-args: | | ||||
|             PYTHON_VERSION=${{ matrix.python-version }} | ||||
|  | ||||
|       - name: Parse tags for this Python version | ||||
|         id: parse_tags | ||||
|         run: | | ||||
|           IFS=',' read -ra tags <<< "${{ needs.generate_info.outputs.docker-tags }}" | ||||
|           tag_args="" | ||||
|  | ||||
|           for tag in "${tags[@]}"; do | ||||
|               tag_args+=",${{ env.DOCKER_ORG_NAME }}/${{ env.DOCKER_IMAGE_NAME }}:${tag}-py${{ matrix.python-version }}" | ||||
|           done | ||||
|  | ||||
|           tag_args_output="tag_args=${tag_args:1}" | ||||
|  | ||||
|           echo "${tag_args_output}" | ||||
|           echo "${tag_args_output}" >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Build and push Sanic image | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           push: ${{ env.IS_TEST == 'false' }} | ||||
|           file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} | ||||
|           tags: ${{ steps.parse_tags.outputs.tag_args }} | ||||
|           build-args: | | ||||
|             BASE_IMAGE_ORG=${{ env.DOCKER_ORG_NAME }} | ||||
|             BASE_IMAGE_NAME=${{ env.DOCKER_BASE_IMAGE_NAME }} | ||||
|             BASE_IMAGE_TAG=${{ matrix.python-version }} | ||||
|             SANIC_PYPI_VERSION=${{ needs.generate_info.outputs.pypi-version }} | ||||
							
								
								
									
										56
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| name: Tests | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     tags: | ||||
|       - "!*" | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - current-release | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   run_tests: | ||||
|     name: "${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }}" | ||||
|     if: github.event.pull_request.draft == false | ||||
|     runs-on: ${{ matrix.config.platform || 'ubuntu-latest' }} | ||||
|     strategy: | ||||
|       fail-fast: true | ||||
|       matrix: | ||||
|         config: | ||||
|           - { python-version: "3.8",  tox-env: security } | ||||
|           - { python-version: "3.9",  tox-env: security } | ||||
|           - { python-version: "3.10", tox-env: security } | ||||
|           - { python-version: "3.11", tox-env: security } | ||||
|           - { python-version: "3.10", tox-env: lint } | ||||
|           - { python-version: "3.10", tox-env: docs } | ||||
|           - { python-version: "3.8",  tox-env: type-checking } | ||||
|           - { python-version: "3.9",  tox-env: type-checking } | ||||
|           - { python-version: "3.10", tox-env: type-checking } | ||||
|           - { python-version: "3.11", tox-env: type-checking } | ||||
|           - { python-version: "3.8",  tox-env: py38,          max-attempts: 3 } | ||||
|           - { python-version: "3.8",  tox-env: py38-no-ext,   max-attempts: 3 } | ||||
|           - { python-version: "3.9",  tox-env: py39,          max-attempts: 3 } | ||||
|           - { python-version: "3.9",  tox-env: py39-no-ext,   max-attempts: 3 } | ||||
|           - { python-version: "3.10", tox-env: py310,         max-attempts: 3 } | ||||
|           - { python-version: "3.10", tox-env: py310-no-ext,  max-attempts: 3 } | ||||
|           - { python-version: "3.11", tox-env: py311,         max-attempts: 3 } | ||||
|           - { python-version: "3.11", tox-env: py311-no-ext,  max-attempts: 3 } | ||||
|           - { python-version: "3.8",  tox-env: py38-no-ext,   platform: windows-latest, ignore-errors: true } | ||||
|           - { python-version: "3.9",  tox-env: py39-no-ext,   platform: windows-latest, ignore-errors: true } | ||||
|           - { python-version: "3.10", tox-env: py310-no-ext,  platform: windows-latest, ignore-errors: true } | ||||
|           - { python-version: "3.11", tox-env: py310-no-ext,  platform: windows-latest, ignore-errors: true } | ||||
|     steps: | ||||
|       - name: Run tests | ||||
|         uses: sanic-org/simple-tox-action@v1 | ||||
|         with: | ||||
|           python-version: ${{ matrix.config.python-version }} | ||||
|           tox-env: ${{ matrix.config.tox-env }} | ||||
|           max-attempts: ${{ matrix.config.max-attempts || 1 }} | ||||
|           ignore-errors: ${{ matrix.config.ignore-errors || false }} | ||||
							
								
								
									
										35
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								README.rst
									
									
									
									
									
								
							| @@ -11,7 +11,7 @@ Sanic | Build fast. Run fast. | ||||
|     :stub-columns: 1 | ||||
|  | ||||
|     * - Build | ||||
|       - | |Py310Test| |Py39Test| |Py38Test| |Py37Test| | ||||
|       - | |Tests| | ||||
|     * - Docs | ||||
|       - | |UserGuide| |Documentation| | ||||
|     * - Package | ||||
| @@ -19,7 +19,7 @@ Sanic | Build fast. Run fast. | ||||
|     * - Support | ||||
|       - | |Forums| |Discord| |Awesome| | ||||
|     * - Stats | ||||
|       - | |Downloads| |WkDownloads| |Conda downloads| | ||||
|       - | |Monthly Downloads| |Weekly Downloads| |Conda downloads| | ||||
|  | ||||
| .. |UserGuide| image:: https://img.shields.io/badge/user%20guide-sanic-ff0068 | ||||
|    :target: https://sanicframework.org/ | ||||
| @@ -27,14 +27,8 @@ Sanic | Build fast. Run fast. | ||||
|    :target: https://community.sanicframework.org/ | ||||
| .. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord | ||||
|    :target: https://discord.gg/FARQzAEMAA | ||||
| .. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main | ||||
|    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml | ||||
| .. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main | ||||
|    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml | ||||
| .. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main | ||||
|    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml | ||||
| .. |Py37Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml/badge.svg?branch=main | ||||
|    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml | ||||
| .. |Tests| image:: https://github.com/sanic-org/sanic/actions/workflows/tests.yml/badge.svg?branch=main | ||||
|    :target: https://github.com/sanic-org/sanic/actions/workflows/tests.yml | ||||
| .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest | ||||
|    :target: http://sanic.readthedocs.io/en/latest/?badge=latest | ||||
| .. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg | ||||
| @@ -52,19 +46,23 @@ Sanic | Build fast. Run fast. | ||||
| .. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg | ||||
|     :alt: Awesome Sanic List | ||||
|     :target: https://github.com/mekicha/awesome-sanic | ||||
| .. |Downloads| image:: https://pepy.tech/badge/sanic/month | ||||
| .. |Monthly Downloads| image:: https://img.shields.io/pypi/dm/sanic.svg | ||||
|     :alt: Downloads | ||||
|     :target: https://pepy.tech/project/sanic | ||||
| .. |WkDownloads| image:: https://pepy.tech/badge/sanic/week | ||||
| .. |Weekly Downloads| image:: https://img.shields.io/pypi/dw/sanic.svg | ||||
|     :alt: Downloads | ||||
|     :target: https://pepy.tech/project/sanic | ||||
| .. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg | ||||
|     :alt: Downloads | ||||
|     :target: https://anaconda.org/conda-forge/sanic | ||||
| .. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg | ||||
|     :alt: Linode | ||||
|     :target: https://www.linode.com | ||||
|     :width: 200px | ||||
|  | ||||
| .. end-badges | ||||
|  | ||||
| Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. | ||||
| Sanic is a **Python 3.8+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. | ||||
|  | ||||
| Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanicframework.org/en/guide/deployment/running.html#asgi>`_. | ||||
|  | ||||
| @@ -141,17 +139,17 @@ And, we can verify it is working: ``curl localhost:8000 -i`` | ||||
|  | ||||
| **Now, let's go build something fast!** | ||||
|  | ||||
| Minimum Python version is 3.7. If you need Python 3.6 support, please use v20.12LTS. | ||||
| Minimum Python version is 3.8. If you need Python 3.7 support, please use v22.12LTS. | ||||
|  | ||||
| Documentation | ||||
| ------------- | ||||
|  | ||||
| `User Guide <https://sanicframework.org>`__ and `API Documentation <http://sanic.readthedocs.io/>`__. | ||||
| `User Guide <https://sanic.dev>`__ and `API Documentation <http://sanic.readthedocs.io/>`__. | ||||
|  | ||||
| Changelog | ||||
| --------- | ||||
|  | ||||
| `Release Changelogs <https://github.com/sanic-org/sanic/blob/master/CHANGELOG.rst>`__. | ||||
| `Release Changelogs <https://sanic.readthedocs.io/en/stable/sanic/changelog.html>`__. | ||||
|  | ||||
|  | ||||
| Questions and Discussion | ||||
| @@ -163,8 +161,3 @@ Contribution | ||||
| ------------ | ||||
|  | ||||
| We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_. | ||||
|  | ||||
| .. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg | ||||
|     :alt: Linode | ||||
|     :target: https://www.linode.com | ||||
|     :width: 200px | ||||
|   | ||||
| @@ -1,9 +1,13 @@ | ||||
| ARG BASE_IMAGE_ORG | ||||
| ARG BASE_IMAGE_NAME | ||||
| ARG BASE_IMAGE_TAG | ||||
|  | ||||
| FROM sanicframework/sanic-build:${BASE_IMAGE_TAG} | ||||
| FROM ${BASE_IMAGE_ORG}/${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG} | ||||
|  | ||||
| RUN apk update | ||||
| RUN update-ca-certificates | ||||
|  | ||||
| RUN pip install sanic | ||||
| ARG SANIC_PYPI_VERSION | ||||
|  | ||||
| RUN pip install -U pip && pip install sanic==${SANIC_PYPI_VERSION} | ||||
| RUN apk del build-base | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| | 🔷 In support release | ||||
| | | ||||
|  | ||||
| .. mdinclude:: ./releases/23/23.6.md | ||||
| .. mdinclude:: ./releases/23/23.3.md | ||||
| .. mdinclude:: ./releases/22/22.12.md | ||||
| .. mdinclude:: ./releases/22/22.9.md | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| ## Version 23.3.0 🔶 | ||||
| ## Version 23.3.0 | ||||
|  | ||||
| ### Features | ||||
| - [#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: | ||||
| @@ -1,6 +1,11 @@ | ||||
| from types import SimpleNamespace | ||||
|  | ||||
| from typing_extensions import TypeAlias | ||||
|  | ||||
| from sanic.__version__ import __version__ | ||||
| from sanic.app import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.config import Config | ||||
| from sanic.constants import HTTPMethod | ||||
| from sanic.exceptions import ( | ||||
|     BadRequest, | ||||
| @@ -32,15 +37,29 @@ from sanic.response import ( | ||||
| 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__ = ( | ||||
|     "__version__", | ||||
|     # Common objects | ||||
|     "Sanic", | ||||
|     "Config", | ||||
|     "Blueprint", | ||||
|     "HTTPMethod", | ||||
|     "HTTPResponse", | ||||
|     "Request", | ||||
|     "Websocket", | ||||
|     # Common types | ||||
|     "DefaultSanic", | ||||
|     "DefaultRequest", | ||||
|     # Common exceptions | ||||
|     "BadRequest", | ||||
|     "ExpectationFailed", | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "23.3.1" | ||||
| __version__ = "23.6.0" | ||||
|   | ||||
							
								
								
									
										183
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										183
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -5,6 +5,7 @@ import logging | ||||
| import logging.config | ||||
| import re | ||||
| import sys | ||||
|  | ||||
| from asyncio import ( | ||||
|     AbstractEventLoop, | ||||
|     CancelledError, | ||||
| @@ -16,7 +17,7 @@ from asyncio import ( | ||||
| from asyncio.futures import Future | ||||
| from collections import defaultdict, deque | ||||
| from contextlib import contextmanager, suppress | ||||
| from functools import partial | ||||
| from functools import partial, wraps | ||||
| from inspect import isawaitable | ||||
| from os import environ | ||||
| from socket import socket | ||||
| @@ -28,9 +29,11 @@ from typing import ( | ||||
|     AnyStr, | ||||
|     Awaitable, | ||||
|     Callable, | ||||
|     ClassVar, | ||||
|     Coroutine, | ||||
|     Deque, | ||||
|     Dict, | ||||
|     Generic, | ||||
|     Iterable, | ||||
|     Iterator, | ||||
|     List, | ||||
| @@ -40,6 +43,8 @@ from typing import ( | ||||
|     Type, | ||||
|     TypeVar, | ||||
|     Union, | ||||
|     cast, | ||||
|     overload, | ||||
| ) | ||||
| from urllib.parse import urlencode, urlunparse | ||||
|  | ||||
| @@ -54,7 +59,12 @@ from sanic.blueprint_group import BlueprintGroup | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support | ||||
| from sanic.config import SANIC_PREFIX, Config | ||||
| from sanic.exceptions import BadRequest, SanicException, ServerError, URLBuildError | ||||
| from sanic.exceptions import ( | ||||
|     BadRequest, | ||||
|     SanicException, | ||||
|     ServerError, | ||||
|     URLBuildError, | ||||
| ) | ||||
| from sanic.handlers import ErrorHandler | ||||
| from sanic.helpers import Default, _default | ||||
| from sanic.http import Stage | ||||
| @@ -77,13 +87,14 @@ from sanic.request import Request | ||||
| from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream | ||||
| from sanic.router import Router | ||||
| 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.types.shared_ctx import SharedContext | ||||
| from sanic.worker.inspector import Inspector | ||||
| from sanic.worker.loader import CertLoader | ||||
| from sanic.worker.manager import WorkerManager | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     try: | ||||
|         from sanic_ext import Extend  # type: ignore | ||||
| @@ -95,8 +106,17 @@ if TYPE_CHECKING: | ||||
| if OS_IS_WINDOWS:  # no cov | ||||
|     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 | ||||
|     """ | ||||
| @@ -151,14 +171,102 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||
|         "websocket_tasks", | ||||
|     ) | ||||
|  | ||||
|     _app_registry: Dict[str, "Sanic"] = {} | ||||
|     test_mode = False | ||||
|     _app_registry: ClassVar[Dict[str, "Sanic"]] = {} | ||||
|     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__( | ||||
|         self, | ||||
|         name: Optional[str] = None, | ||||
|         config: Optional[Config] = None, | ||||
|         ctx: Optional[Any] = None, | ||||
|         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, | ||||
| @@ -186,7 +294,9 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||
|             ) | ||||
|  | ||||
|         # 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: | ||||
|             self.config.INSPECTOR = inspector | ||||
|  | ||||
| @@ -210,7 +320,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||
|             certloader_class or CertLoader | ||||
|         ) | ||||
|         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.inspector_class: Type[Inspector] = inspector_class or Inspector | ||||
|         self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list) | ||||
| @@ -495,6 +605,19 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||
|                 raise NotFound("Could not find signal %s" % event) | ||||
|         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): | ||||
|         """Enable or disable the support for websocket. | ||||
|  | ||||
| @@ -766,8 +889,10 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||
|         :raises ServerError: response 500 | ||||
|         """ | ||||
|         response = None | ||||
|         if not getattr(exception, "__dispatched__", False): | ||||
|             ...  # DO NOT REMOVE THIS LINE. IT IS NEEDED FOR TOUCHUP. | ||||
|             await self.dispatch( | ||||
|             "server.lifecycle.exception", | ||||
|                 "server.exception.report", | ||||
|                 context={"exception": exception}, | ||||
|             ) | ||||
|         await self.dispatch( | ||||
| @@ -1200,13 +1325,28 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||
|         app, | ||||
|         loop, | ||||
|     ): | ||||
|         async def do(task): | ||||
|             try: | ||||
|                 if callable(task): | ||||
|                     try: | ||||
|                         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 | ||||
|     def _loop_add_task( | ||||
| @@ -1220,18 +1360,9 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||
|     ) -> Task: | ||||
|         if not isinstance(task, Future): | ||||
|             prepped = cls._prep_task(task, app, loop) | ||||
|             if sys.version_info < (3, 8):  # no cov | ||||
|                 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 | ||||
|  | ||||
|         return task | ||||
| @@ -1670,9 +1801,6 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||
|         if environ.get("SANIC_WORKER_PROCESS") or not self._inspector: | ||||
|             raise SanicException( | ||||
|                 "Can only access the inspector from the main process" | ||||
|                 "after main_process_start has run. For example, you most " | ||||
|                 "likely want to use it inside the @app.main_process_ready " | ||||
|                 "event listener." | ||||
|             ) | ||||
|         return self._inspector | ||||
|  | ||||
| @@ -1681,8 +1809,5 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||
|         if environ.get("SANIC_WORKER_PROCESS") or not self._manager: | ||||
|             raise SanicException( | ||||
|                 "Can only access the manager from the main process" | ||||
|                 "after main_process_start has run. For example, you most " | ||||
|                 "likely want to use it inside the @app.main_process_ready " | ||||
|                 "event listener." | ||||
|             ) | ||||
|         return self._manager | ||||
|   | ||||
| @@ -111,7 +111,7 @@ class Blueprint(BaseSanic): | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         name: str = None, | ||||
|         name: str, | ||||
|         url_prefix: Optional[str] = None, | ||||
|         host: Optional[Union[List[str], str]] = None, | ||||
|         version: Optional[Union[int, str, float]] = None, | ||||
| @@ -319,6 +319,10 @@ class Blueprint(BaseSanic): | ||||
|             # Prepend the blueprint URI prefix if available | ||||
|             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 | ||||
|             for prefix in ( | ||||
|                 future.version_prefix, | ||||
| @@ -358,7 +362,7 @@ class Blueprint(BaseSanic): | ||||
|                 future.unquote, | ||||
|                 future.static, | ||||
|                 version_prefix, | ||||
|                 error_format, | ||||
|                 route_error_format, | ||||
|                 future.route_context, | ||||
|             ) | ||||
|  | ||||
|   | ||||
| @@ -43,14 +43,14 @@ DEFAULT_CONFIG = { | ||||
|     "DEPRECATION_FILTER": "once", | ||||
|     "FORWARDED_FOR_HEADER": "X-Forwarded-For", | ||||
|     "FORWARDED_SECRET": None, | ||||
|     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,  # 15 sec | ||||
|     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, | ||||
|     "INSPECTOR": False, | ||||
|     "INSPECTOR_HOST": "localhost", | ||||
|     "INSPECTOR_PORT": 6457, | ||||
|     "INSPECTOR_TLS_KEY": _default, | ||||
|     "INSPECTOR_TLS_CERT": _default, | ||||
|     "INSPECTOR_API_KEY": "", | ||||
|     "KEEP_ALIVE_TIMEOUT": 5,  # 5 seconds | ||||
|     "KEEP_ALIVE_TIMEOUT": 120, | ||||
|     "KEEP_ALIVE": True, | ||||
|     "LOCAL_CERT_CREATOR": LocalCertCreator.AUTO, | ||||
|     "LOCAL_TLS_KEY": _default, | ||||
| @@ -61,16 +61,16 @@ DEFAULT_CONFIG = { | ||||
|     "NOISY_EXCEPTIONS": False, | ||||
|     "PROXIES_COUNT": None, | ||||
|     "REAL_IP_HEADER": None, | ||||
|     "REQUEST_BUFFER_SIZE": 65536,  # 64 KiB | ||||
|     "REQUEST_MAX_HEADER_SIZE": 8192,  # 8 KiB, but cannot exceed 16384 | ||||
|     "REQUEST_BUFFER_SIZE": 65536, | ||||
|     "REQUEST_MAX_HEADER_SIZE": 8192,  # Cannot exceed 16384 | ||||
|     "REQUEST_ID_HEADER": "X-Request-ID", | ||||
|     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes | ||||
|     "REQUEST_TIMEOUT": 60,  # 60 seconds | ||||
|     "RESPONSE_TIMEOUT": 60,  # 60 seconds | ||||
|     "REQUEST_MAX_SIZE": 100_000_000, | ||||
|     "REQUEST_TIMEOUT": 60, | ||||
|     "RESPONSE_TIMEOUT": 60, | ||||
|     "TLS_CERT_PASSWORD": "", | ||||
|     "TOUCHUP": _default, | ||||
|     "USE_UVLOOP": _default, | ||||
|     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 megabyte | ||||
|     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 MiB | ||||
|     "WEBSOCKET_PING_INTERVAL": 20, | ||||
|     "WEBSOCKET_PING_TIMEOUT": 20, | ||||
| } | ||||
|   | ||||
| @@ -312,7 +312,7 @@ def exception_response( | ||||
|     debug: bool, | ||||
|     fallback: str, | ||||
|     base: t.Type[BaseRenderer], | ||||
|     renderer: t.Type[t.Optional[BaseRenderer]] = None, | ||||
|     renderer: t.Optional[t.Type[BaseRenderer]] = None, | ||||
| ) -> HTTPResponse: | ||||
|     """ | ||||
|     Render a response for the default FALLBACK exception handler. | ||||
|   | ||||
| @@ -90,7 +90,7 @@ class SanicException(Exception): | ||||
|  | ||||
|         super().__init__(message) | ||||
|  | ||||
|         self.status_code = status_code | ||||
|         self.status_code = status_code or self.status_code | ||||
|         self.quiet = quiet | ||||
|         self.headers = headers | ||||
|  | ||||
|   | ||||
| @@ -436,7 +436,7 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes: | ||||
|  | ||||
| def parse_credentials( | ||||
|     header: Optional[str], | ||||
|     prefixes: Union[List, Tuple, Set] = None, | ||||
|     prefixes: Optional[Union[List, Tuple, Set]] = None, | ||||
| ) -> Tuple[Optional[str], Optional[str]]: | ||||
|     """Parses any header with the aim to retrieve any credentials from it.""" | ||||
|     if not prefixes or not isinstance(prefixes, (list, tuple, set)): | ||||
|   | ||||
| @@ -38,3 +38,15 @@ class ExceptionMixin(metaclass=SanicMeta): | ||||
|             return handler | ||||
|  | ||||
|         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) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, Optional, Set, Union | ||||
| from sanic.base.meta import SanicMeta | ||||
| from sanic.models.futures import FutureSignal | ||||
| from sanic.models.handler_types import SignalHandler | ||||
| from sanic.signals import Signal | ||||
| from sanic.signals import Event, Signal | ||||
| from sanic.types import HashableDict | ||||
|  | ||||
|  | ||||
| @@ -80,3 +80,9 @@ class SignalMixin(metaclass=SanicMeta): | ||||
|  | ||||
|     def event(self, event: str): | ||||
|         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 functools import partial | ||||
| 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 pathlib import Path | ||||
| from socket import SHUT_RDWR, socket | ||||
| @@ -25,6 +31,7 @@ from typing import ( | ||||
|     TYPE_CHECKING, | ||||
|     Any, | ||||
|     Callable, | ||||
|     ClassVar, | ||||
|     Dict, | ||||
|     List, | ||||
|     Mapping, | ||||
| @@ -81,13 +88,17 @@ else:  # no cov | ||||
|  | ||||
|  | ||||
| class StartupMixin(metaclass=SanicMeta): | ||||
|     _app_registry: Dict[str, Sanic] | ||||
|     _app_registry: ClassVar[Dict[str, Sanic]] | ||||
|  | ||||
|     config: Config | ||||
|     listeners: Dict[str, List[ListenerType[Any]]] | ||||
|     state: ApplicationState | ||||
|     websocket_enabled: bool | ||||
|     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): | ||||
|         if not self.asgi: | ||||
| @@ -646,10 +657,9 @@ class StartupMixin(metaclass=SanicMeta): | ||||
|     def get_server_location( | ||||
|         server_settings: Optional[Dict[str, Any]] = None | ||||
|     ) -> str: | ||||
|         serve_location = "" | ||||
|         proto = "http" | ||||
|         if not server_settings: | ||||
|             return serve_location | ||||
|             return "" | ||||
|  | ||||
|         host = server_settings["host"] | ||||
|         port = server_settings["port"] | ||||
| @@ -657,16 +667,33 @@ class StartupMixin(metaclass=SanicMeta): | ||||
|         if server_settings.get("ssl") is not None: | ||||
|             proto = "https" | ||||
|         if server_settings.get("unix"): | ||||
|             serve_location = f'{server_settings["unix"]} {proto}://...' | ||||
|         elif server_settings.get("sock"): | ||||
|             return f'{server_settings["unix"]} {proto}://localhost' | ||||
|         if server_settings.get("sock"): | ||||
|             host, port, *_ = server_settings["sock"].getsockname() | ||||
|         if not host or not port: | ||||
|             return "" | ||||
|  | ||||
|         if not serve_location and host and port: | ||||
|         # colon(:) is legal for a host only in an ipv6 address | ||||
|             display_host = f"[{host}]" if ":" in host else host | ||||
|             serve_location = f"{proto}://{display_host}:{port}" | ||||
|         url_host = f"[{host}]" if ":" in host else host | ||||
|         url_port = ( | ||||
|             "" | ||||
|             if ( | ||||
|                 (proto == "https" and port == 443) | ||||
|                 or (proto == "http" and port == 80) | ||||
|             ) | ||||
|             else f":{port}" | ||||
|         ) | ||||
|  | ||||
|         return serve_location | ||||
|         special = { | ||||
|             "127.0.0.1": "IPv4", | ||||
|             "0.0.0.0": "IPv4 wildcard", | ||||
|             "::1": "IPv6", | ||||
|             "::": "IPv6 wildcard", | ||||
|         }.get(host, "") | ||||
|         if special: | ||||
|             return f"({special}) {proto}://localhost{url_port}" | ||||
|  | ||||
|         return f"{proto}://{url_host}{url_port}" | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_address( | ||||
| @@ -691,11 +718,26 @@ class StartupMixin(metaclass=SanicMeta): | ||||
|             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 | ||||
|     def _get_context(cls) -> BaseContext: | ||||
|         method = cls._get_startup_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 | ||||
|     def serve( | ||||
| @@ -705,6 +747,7 @@ class StartupMixin(metaclass=SanicMeta): | ||||
|         app_loader: Optional[AppLoader] = None, | ||||
|         factory: Optional[Callable[[], Sanic]] = None, | ||||
|     ) -> None: | ||||
|         cls._set_startup_method() | ||||
|         os.environ["SANIC_MOTD_OUTPUT"] = "true" | ||||
|         apps = list(cls._app_registry.values()) | ||||
|         if factory: | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import asyncio | ||||
| import sys | ||||
|  | ||||
| from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union | ||||
|  | ||||
| @@ -16,20 +15,10 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]] | ||||
|  | ||||
| class MockProtocol:  # no cov | ||||
|     def __init__(self, transport: "MockTransport", loop): | ||||
|         # This should be refactored when < 3.8 support is dropped | ||||
|         self.transport = transport | ||||
|         # Fixup for 3.8+; Sanic still supports 3.7 where loop is required | ||||
|         loop = loop if sys.version_info[:2] < (3, 8) else None | ||||
|         # Optional in 3.9, necessary in 3.10 because the parameter "loop" | ||||
|         # 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: | ||||
|         self._not_paused.clear() | ||||
|   | ||||
| @@ -2,11 +2,13 @@ from __future__ import annotations | ||||
|  | ||||
| from contextvars import ContextVar | ||||
| from inspect import isawaitable | ||||
| from types import SimpleNamespace | ||||
| from typing import ( | ||||
|     TYPE_CHECKING, | ||||
|     Any, | ||||
|     DefaultDict, | ||||
|     Dict, | ||||
|     Generic, | ||||
|     List, | ||||
|     Optional, | ||||
|     Tuple, | ||||
| @@ -15,6 +17,7 @@ from typing import ( | ||||
| ) | ||||
|  | ||||
| from sanic_routing.route import Route | ||||
| from typing_extensions import TypeVar | ||||
|  | ||||
| from sanic.http.constants import HTTP  # type: ignore | ||||
| from sanic.http.stream import Stream | ||||
| @@ -23,13 +26,13 @@ from sanic.models.http_types import Credentials | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from sanic.server import ConnInfo | ||||
|     from sanic.app import Sanic | ||||
|     from sanic.config import Config | ||||
|     from sanic.server import ConnInfo | ||||
|  | ||||
| import uuid | ||||
|  | ||||
| from collections import defaultdict | ||||
| from types import SimpleNamespace | ||||
| from urllib.parse import parse_qs, parse_qsl, urlunparse | ||||
|  | ||||
| from httptools import parse_url | ||||
| @@ -68,8 +71,21 @@ try: | ||||
| except ImportError: | ||||
|     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. | ||||
|     """ | ||||
| @@ -80,6 +96,7 @@ class Request: | ||||
|     __slots__ = ( | ||||
|         "__weakref__", | ||||
|         "_cookies", | ||||
|         "_ctx", | ||||
|         "_id", | ||||
|         "_ip", | ||||
|         "_parsed_url", | ||||
| @@ -96,7 +113,6 @@ class Request: | ||||
|         "app", | ||||
|         "body", | ||||
|         "conn_info", | ||||
|         "ctx", | ||||
|         "head", | ||||
|         "headers", | ||||
|         "method", | ||||
| @@ -125,7 +141,7 @@ class Request: | ||||
|         version: str, | ||||
|         method: str, | ||||
|         transport: TransportProtocol, | ||||
|         app: Sanic, | ||||
|         app: sanic_type, | ||||
|         head: bytes = b"", | ||||
|         stream_id: int = 0, | ||||
|     ): | ||||
| @@ -149,7 +165,7 @@ class Request: | ||||
|         # Init but do not inhale | ||||
|         self.body = b"" | ||||
|         self.conn_info: Optional[ConnInfo] = None | ||||
|         self.ctx = SimpleNamespace() | ||||
|         self._ctx: Optional[ctx_type] = None | ||||
|         self.parsed_accept: Optional[AcceptList] = None | ||||
|         self.parsed_args: DefaultDict[ | ||||
|             Tuple[bool, bool, str, str], RequestParameters | ||||
| @@ -176,6 +192,10 @@ class Request: | ||||
|         class_name = self.__class__.__name__ | ||||
|         return f"<{class_name}: {self.method} {self.path}>" | ||||
|  | ||||
|     @staticmethod | ||||
|     def make_context() -> ctx_type: | ||||
|         return cast(ctx_type, SimpleNamespace()) | ||||
|  | ||||
|     @classmethod | ||||
|     def get_current(cls) -> Request: | ||||
|         """ | ||||
| @@ -205,6 +225,15 @@ class Request: | ||||
|     def generate_id(*_): | ||||
|         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 | ||||
|     def stream_id(self): | ||||
|         """ | ||||
| @@ -809,19 +838,31 @@ class Request: | ||||
|     @property | ||||
|     def remote_addr(self) -> str: | ||||
|         """ | ||||
|         Client IP address, if available. | ||||
|         1. proxied remote address `self.forwarded['for']` | ||||
|         2. local remote address `self.ip` | ||||
|         Client IP address, if available from proxy. | ||||
|  | ||||
|         :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string | ||||
|         :rtype: str | ||||
|         """ | ||||
|         if not hasattr(self, "_remote_addr"): | ||||
|             self._remote_addr = str( | ||||
|                 self.forwarded.get("for", "") | ||||
|             )  # or self.ip | ||||
|             self._remote_addr = str(self.forwarded.get("for", "")) | ||||
|         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 | ||||
|     def scheme(self) -> str: | ||||
|         """ | ||||
|   | ||||
| @@ -75,7 +75,7 @@ class Router(BaseRouter): | ||||
|         strict_slashes: bool = False, | ||||
|         stream: bool = False, | ||||
|         ignore_body: bool = False, | ||||
|         version: Union[str, float, int] = None, | ||||
|         version: Optional[Union[str, float, int]] = None, | ||||
|         name: Optional[str] = None, | ||||
|         unquote: bool = False, | ||||
|         static: bool = False, | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import sys | ||||
|  | ||||
| from ssl import SSLContext | ||||
| from typing import TYPE_CHECKING, Dict, Optional, Type, Union | ||||
|  | ||||
| @@ -251,7 +249,6 @@ def _serve_http_1( | ||||
|             loop.run_until_complete(asyncio.sleep(0.1)) | ||||
|             start_shutdown = start_shutdown + 0.1 | ||||
|  | ||||
|         if sys.version_info > (3, 7): | ||||
|         app.shutdown_tasks(graceful - start_shutdown) | ||||
|  | ||||
|         # Force close non-idle connection after waiting for | ||||
|   | ||||
| @@ -96,6 +96,7 @@ class WebsocketFrameAssembler: | ||||
|         If ``timeout`` is set and elapses before a complete message is | ||||
|         received, :meth:`get` returns ``None``. | ||||
|         """ | ||||
|         completed: bool | ||||
|         async with self.read_mutex: | ||||
|             if timeout is not None and timeout <= 0: | ||||
|                 if not self.message_complete.is_set(): | ||||
|   | ||||
| @@ -21,7 +21,7 @@ from websockets.frames import Frame, Opcode | ||||
|  | ||||
|  | ||||
| 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 | ||||
| except ImportError:  # websockets >= 11.0 | ||||
|     from websockets.protocol import Event, State  # type: ignore | ||||
| @@ -532,12 +532,11 @@ class WebsocketImplProtocol: | ||||
|             raise WebsocketClosed( | ||||
|                 "Cannot receive from websocket interface after it is closed." | ||||
|             ) | ||||
|         assembler_get: Optional[asyncio.Task] = None | ||||
|         try: | ||||
|             self.recv_cancel = asyncio.Future() | ||||
|             tasks = ( | ||||
|                 self.recv_cancel, | ||||
|                 asyncio.ensure_future(self.assembler.get(timeout)), | ||||
|             ) | ||||
|             assembler_get = asyncio.create_task(self.assembler.get(timeout)) | ||||
|             tasks = (self.recv_cancel, assembler_get) | ||||
|             done, pending = await asyncio.wait( | ||||
|                 tasks, | ||||
|                 return_when=asyncio.FIRST_COMPLETED, | ||||
| @@ -551,6 +550,11 @@ class WebsocketImplProtocol: | ||||
|             else: | ||||
|                 self.recv_cancel.cancel() | ||||
|                 return done_task.result() | ||||
|         except asyncio.CancelledError: | ||||
|             # recv was cancelled | ||||
|             if assembler_get: | ||||
|                 assembler_get.cancel() | ||||
|             raise | ||||
|         finally: | ||||
|             self.recv_cancel = None | ||||
|             self.recv_lock.release() | ||||
| @@ -584,16 +588,15 @@ class WebsocketImplProtocol: | ||||
|                 "Cannot receive from websocket interface after it is closed." | ||||
|             ) | ||||
|         messages = [] | ||||
|         assembler_get: Optional[asyncio.Task] = None | ||||
|         try: | ||||
|             # Prevent pausing the transport when we're | ||||
|             # receiving a burst of messages | ||||
|             self.can_pause = False | ||||
|             self.recv_cancel = asyncio.Future() | ||||
|             while True: | ||||
|                 tasks = ( | ||||
|                     self.recv_cancel, | ||||
|                     asyncio.ensure_future(self.assembler.get(timeout=0)), | ||||
|                 ) | ||||
|                 assembler_get = asyncio.create_task(self.assembler.get(0)) | ||||
|                 tasks = (self.recv_cancel, assembler_get) | ||||
|                 done, pending = await asyncio.wait( | ||||
|                     tasks, | ||||
|                     return_when=asyncio.FIRST_COMPLETED, | ||||
| @@ -616,6 +619,11 @@ class WebsocketImplProtocol: | ||||
|                 # next message to pass into the Assembler | ||||
|                 await asyncio.sleep(0) | ||||
|             self.recv_cancel.cancel() | ||||
|         except asyncio.CancelledError: | ||||
|             # recv_burst was cancelled | ||||
|             if assembler_get: | ||||
|                 assembler_get.cancel() | ||||
|             raise | ||||
|         finally: | ||||
|             self.recv_cancel = None | ||||
|             self.can_pause = True | ||||
|   | ||||
| @@ -16,11 +16,11 @@ from sanic.models.handler_types import SignalHandler | ||||
|  | ||||
|  | ||||
| class Event(Enum): | ||||
|     SERVER_EXCEPTION_REPORT = "server.exception.report" | ||||
|     SERVER_INIT_AFTER = "server.init.after" | ||||
|     SERVER_INIT_BEFORE = "server.init.before" | ||||
|     SERVER_SHUTDOWN_AFTER = "server.shutdown.after" | ||||
|     SERVER_SHUTDOWN_BEFORE = "server.shutdown.before" | ||||
|     SERVER_LIFECYCLE_EXCEPTION = "server.lifecycle.exception" | ||||
|     HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin" | ||||
|     HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete" | ||||
|     HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception" | ||||
| @@ -40,11 +40,11 @@ class Event(Enum): | ||||
|  | ||||
| RESERVED_NAMESPACES = { | ||||
|     "server": ( | ||||
|         Event.SERVER_EXCEPTION_REPORT.value, | ||||
|         Event.SERVER_INIT_AFTER.value, | ||||
|         Event.SERVER_INIT_BEFORE.value, | ||||
|         Event.SERVER_SHUTDOWN_AFTER.value, | ||||
|         Event.SERVER_SHUTDOWN_BEFORE.value, | ||||
|         Event.SERVER_LIFECYCLE_EXCEPTION.value, | ||||
|     ), | ||||
|     "http": ( | ||||
|         Event.HTTP_LIFECYCLE_BEGIN.value, | ||||
| @@ -174,11 +174,12 @@ class SignalRouter(BaseRouter): | ||||
|             if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: | ||||
|                 error_logger.exception(e) | ||||
|  | ||||
|             if event != Event.SERVER_LIFECYCLE_EXCEPTION.value: | ||||
|             if event != Event.SERVER_EXCEPTION_REPORT.value: | ||||
|                 await self.dispatch( | ||||
|                     Event.SERVER_LIFECYCLE_EXCEPTION.value, | ||||
|                     Event.SERVER_EXCEPTION_REPORT.value, | ||||
|                     context={"exception": e}, | ||||
|                 ) | ||||
|                 setattr(e, "__dispatched__", True) | ||||
|             raise e | ||||
|         finally: | ||||
|             for signal_event in events: | ||||
| @@ -229,14 +230,6 @@ class SignalRouter(BaseRouter): | ||||
|         if not 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( | ||||
|             event, | ||||
|             handler, | ||||
|   | ||||
| @@ -16,5 +16,3 @@ class ProcessState(IntEnum): | ||||
|     ACKED = auto() | ||||
|     JOINED = auto() | ||||
|     TERMINATED = auto() | ||||
|     FAILED = auto() | ||||
|     COMPLETED = auto() | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import os | ||||
|  | ||||
| from contextlib import suppress | ||||
| from enum import IntEnum, auto | ||||
| from itertools import chain, count | ||||
| from itertools import count | ||||
| from random import choice | ||||
| from signal import SIGINT, SIGTERM, Signals | ||||
| from signal import signal as signal_func | ||||
| from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple | ||||
| from typing import Any, Callable, Dict, List, Optional | ||||
|  | ||||
| from sanic.compat import OS_IS_WINDOWS | ||||
| from sanic.exceptions import ServerKilled | ||||
| @@ -13,17 +13,13 @@ from sanic.log import error_logger, logger | ||||
| from sanic.worker.constants import RestartOrder | ||||
| from sanic.worker.process import ProcessState, Worker, WorkerProcess | ||||
|  | ||||
|  | ||||
| if not OS_IS_WINDOWS: | ||||
|     from signal import SIGKILL | ||||
| else: | ||||
|     SIGKILL = SIGINT | ||||
|  | ||||
|  | ||||
| class MonitorCycle(IntEnum): | ||||
|     BREAK = auto() | ||||
|     CONTINUE = auto() | ||||
|  | ||||
|  | ||||
| class WorkerManager: | ||||
|     THRESHOLD = WorkerProcess.THRESHOLD | ||||
|     MAIN_IDENT = "Sanic-Main" | ||||
| @@ -64,8 +60,6 @@ class WorkerManager: | ||||
|         func: Callable[..., Any], | ||||
|         kwargs: Dict[str, Any], | ||||
|         transient: bool = False, | ||||
|         restartable: Optional[bool] = None, | ||||
|         tracked: bool = True, | ||||
|         workers: int = 1, | ||||
|     ) -> Worker: | ||||
|         """ | ||||
| @@ -81,35 +75,14 @@ class WorkerManager: | ||||
|             then the Worker Manager will restart the process along | ||||
|             with any global restart (ex: auto-reload), defaults to False | ||||
|         :type transient: bool, optional | ||||
|         :param restartable: Whether to mark the process as restartable. If | ||||
|             True then the Worker Manager will be able to restart the process | ||||
|             if prompted. If transient=True, this property will be implied | ||||
|             to be True, defaults to None | ||||
|         :type restartable: Optional[bool], optional | ||||
|         :param tracked: Whether to track the process after completion, | ||||
|             defaults to True | ||||
|         :param workers: The number of worker processes to run, defaults to 1 | ||||
|         :type workers: int, optional | ||||
|         :return: The Worker instance | ||||
|         :rtype: Worker | ||||
|         """ | ||||
|         if ident in self.transient or ident in self.durable: | ||||
|             raise ValueError(f"Worker {ident} already exists") | ||||
|         restartable = restartable if restartable is not None else transient | ||||
|         if transient and not restartable: | ||||
|             raise ValueError( | ||||
|                 "Cannot create a transient worker that is not restartable" | ||||
|             ) | ||||
|         container = self.transient if transient else self.durable | ||||
|         worker = Worker( | ||||
|             ident, | ||||
|             func, | ||||
|             kwargs, | ||||
|             self.context, | ||||
|             self.worker_state, | ||||
|             workers, | ||||
|             restartable, | ||||
|             tracked, | ||||
|             ident, func, kwargs, self.context, self.worker_state, workers | ||||
|         ) | ||||
|         container[worker.ident] = worker | ||||
|         return worker | ||||
| @@ -121,7 +94,6 @@ class WorkerManager: | ||||
|             self._serve, | ||||
|             self._server_settings, | ||||
|             transient=True, | ||||
|             restartable=True, | ||||
|         ) | ||||
|  | ||||
|     def shutdown_server(self, ident: Optional[str] = None) -> None: | ||||
| @@ -181,32 +153,9 @@ class WorkerManager: | ||||
|         restart_order=RestartOrder.SHUTDOWN_FIRST, | ||||
|         **kwargs, | ||||
|     ): | ||||
|         restarted = set() | ||||
|         for process in self.transient_processes: | ||||
|             if process.restartable and ( | ||||
|                 not process_names or process.name in process_names | ||||
|             ): | ||||
|             if not process_names or process.name in process_names: | ||||
|                 process.restart(restart_order=restart_order, **kwargs) | ||||
|                 restarted.add(process.name) | ||||
|         if process_names: | ||||
|             for process in self.durable_processes: | ||||
|                 if process.restartable and process.name in process_names: | ||||
|                     if process.state not in ( | ||||
|                         ProcessState.COMPLETED, | ||||
|                         ProcessState.FAILED, | ||||
|                     ): | ||||
|                         error_logger.error( | ||||
|                             f"Cannot restart process {process.name} because " | ||||
|                             "it is not in a final state. Current state is: " | ||||
|                             f"{process.state.name}." | ||||
|                         ) | ||||
|                         continue | ||||
|                     process.restart(restart_order=restart_order, **kwargs) | ||||
|                     restarted.add(process.name) | ||||
|         if process_names and not restarted: | ||||
|             error_logger.error( | ||||
|                 f"Failed to restart processes: {', '.join(process_names)}" | ||||
|             ) | ||||
|  | ||||
|     def scale(self, num_worker: int): | ||||
|         if num_worker <= 0: | ||||
| @@ -234,13 +183,45 @@ class WorkerManager: | ||||
|         self.wait_for_ack() | ||||
|         while True: | ||||
|             try: | ||||
|                 cycle = self._poll_monitor() | ||||
|                 if cycle is MonitorCycle.BREAK: | ||||
|                 if self.monitor_subscriber.poll(0.1): | ||||
|                     message = self.monitor_subscriber.recv() | ||||
|                     logger.debug( | ||||
|                         f"Monitor message: {message}", extra={"verbosity": 2} | ||||
|                     ) | ||||
|                     if not message: | ||||
|                         break | ||||
|                 elif cycle is MonitorCycle.CONTINUE: | ||||
|                     elif message == "__TERMINATE__": | ||||
|                         self.shutdown() | ||||
|                         break | ||||
|                     logger.debug( | ||||
|                         "Incoming monitor message: %s", | ||||
|                         message, | ||||
|                         extra={"verbosity": 1}, | ||||
|                     ) | ||||
|                     split_message = message.split(":", 2) | ||||
|                     if message.startswith("__SCALE__"): | ||||
|                         self.scale(int(split_message[-1])) | ||||
|                         continue | ||||
|                     processes = split_message[0] | ||||
|                     reloaded_files = ( | ||||
|                         split_message[1] if len(split_message) > 1 else None | ||||
|                     ) | ||||
|                     process_names = [ | ||||
|                         name.strip() for name in processes.split(",") | ||||
|                     ] | ||||
|                     if "__ALL_PROCESSES__" in process_names: | ||||
|                         process_names = None | ||||
|                     order = ( | ||||
|                         RestartOrder.STARTUP_FIRST | ||||
|                         if "STARTUP_FIRST" in split_message | ||||
|                         else RestartOrder.SHUTDOWN_FIRST | ||||
|                     ) | ||||
|                     self.restart( | ||||
|                         process_names=process_names, | ||||
|                         reloaded_files=reloaded_files, | ||||
|                         restart_order=order, | ||||
|                     ) | ||||
|                 self._sync_states() | ||||
|                 self._cleanup_non_tracked_workers() | ||||
|             except InterruptedError: | ||||
|                 if not OS_IS_WINDOWS: | ||||
|                     raise | ||||
| @@ -283,10 +264,6 @@ class WorkerManager: | ||||
|     def workers(self) -> List[Worker]: | ||||
|         return list(self.transient.values()) + list(self.durable.values()) | ||||
|  | ||||
|     @property | ||||
|     def all_workers(self) -> Iterable[Tuple[str, Worker]]: | ||||
|         return chain(self.transient.items(), self.durable.items()) | ||||
|  | ||||
|     @property | ||||
|     def processes(self): | ||||
|         for worker in self.workers: | ||||
| @@ -299,12 +276,6 @@ class WorkerManager: | ||||
|             for process in worker.processes: | ||||
|                 yield process | ||||
|  | ||||
|     @property | ||||
|     def durable_processes(self): | ||||
|         for worker in self.durable.values(): | ||||
|             for process in worker.processes: | ||||
|                 yield process | ||||
|  | ||||
|     def kill(self): | ||||
|         for process in self.processes: | ||||
|             logger.info("Killing %s [%s]", process.name, process.pid) | ||||
| @@ -327,25 +298,6 @@ class WorkerManager: | ||||
|                 process.terminate() | ||||
|         self._shutting_down = True | ||||
|  | ||||
|     def remove_worker(self, worker: Worker) -> None: | ||||
|         if worker.tracked: | ||||
|             error_logger.error( | ||||
|                 f"Worker {worker.ident} is tracked and cannot be removed." | ||||
|             ) | ||||
|             return | ||||
|         if worker.has_alive_processes(): | ||||
|             error_logger.error( | ||||
|                 f"Worker {worker.ident} has alive processes and cannot be " | ||||
|                 "removed." | ||||
|             ) | ||||
|             return | ||||
|         self.transient.pop(worker.ident, None) | ||||
|         self.durable.pop(worker.ident, None) | ||||
|         for process in worker.processes: | ||||
|             self.worker_state.pop(process.name, None) | ||||
|         logger.info("Removed worker %s", worker.ident) | ||||
|         del worker | ||||
|  | ||||
|     @property | ||||
|     def pid(self): | ||||
|         return os.getpid() | ||||
| @@ -365,97 +317,5 @@ class WorkerManager: | ||||
|             except KeyError: | ||||
|                 process.set_state(ProcessState.TERMINATED, True) | ||||
|                 continue | ||||
|             if not process.is_alive(): | ||||
|                 state = "FAILED" if process.exitcode else "COMPLETED" | ||||
|             if state and process.state.name != state: | ||||
|                 process.set_state(ProcessState[state], True) | ||||
|  | ||||
|     def _cleanup_non_tracked_workers(self) -> None: | ||||
|         to_remove = [ | ||||
|             worker | ||||
|             for worker in self.workers | ||||
|             if not worker.tracked and not worker.has_alive_processes() | ||||
|         ] | ||||
|  | ||||
|         for worker in to_remove: | ||||
|             self.remove_worker(worker) | ||||
|  | ||||
|     def _poll_monitor(self) -> Optional[MonitorCycle]: | ||||
|         if self.monitor_subscriber.poll(0.1): | ||||
|             message = self.monitor_subscriber.recv() | ||||
|             logger.debug(f"Monitor message: {message}", extra={"verbosity": 2}) | ||||
|             if not message: | ||||
|                 return MonitorCycle.BREAK | ||||
|             elif message == "__TERMINATE__": | ||||
|                 self._handle_terminate() | ||||
|                 return MonitorCycle.BREAK | ||||
|             elif isinstance(message, tuple) and len(message) == 7: | ||||
|                 self._handle_manage(*message) | ||||
|                 return MonitorCycle.CONTINUE | ||||
|             elif not isinstance(message, str): | ||||
|                 error_logger.error( | ||||
|                     "Monitor received an invalid message: %s", message | ||||
|                 ) | ||||
|                 return MonitorCycle.CONTINUE | ||||
|             return self._handle_message(message) | ||||
|         return None | ||||
|  | ||||
|     def _handle_terminate(self) -> None: | ||||
|         self.shutdown() | ||||
|  | ||||
|     def _handle_message(self, message: str) -> Optional[MonitorCycle]: | ||||
|         logger.debug( | ||||
|             "Incoming monitor message: %s", | ||||
|             message, | ||||
|             extra={"verbosity": 1}, | ||||
|         ) | ||||
|         split_message = message.split(":", 2) | ||||
|         if message.startswith("__SCALE__"): | ||||
|             self.scale(int(split_message[-1])) | ||||
|             return MonitorCycle.CONTINUE | ||||
|  | ||||
|         processes = split_message[0] | ||||
|         reloaded_files = split_message[1] if len(split_message) > 1 else None | ||||
|         process_names: Optional[List[str]] = [ | ||||
|             name.strip() for name in processes.split(",") | ||||
|         ] | ||||
|         if process_names and "__ALL_PROCESSES__" in process_names: | ||||
|             process_names = None | ||||
|         order = ( | ||||
|             RestartOrder.STARTUP_FIRST | ||||
|             if "STARTUP_FIRST" in split_message | ||||
|             else RestartOrder.SHUTDOWN_FIRST | ||||
|         ) | ||||
|         self.restart( | ||||
|             process_names=process_names, | ||||
|             reloaded_files=reloaded_files, | ||||
|             restart_order=order, | ||||
|         ) | ||||
|  | ||||
|         return None | ||||
|  | ||||
|     def _handle_manage( | ||||
|         self, | ||||
|         ident: str, | ||||
|         func: Callable[..., Any], | ||||
|         kwargs: Dict[str, Any], | ||||
|         transient: bool, | ||||
|         restartable: Optional[bool], | ||||
|         tracked: bool, | ||||
|         workers: int, | ||||
|     ) -> None: | ||||
|         try: | ||||
|             worker = self.manage( | ||||
|                 ident, | ||||
|                 func, | ||||
|                 kwargs, | ||||
|                 transient=transient, | ||||
|                 restartable=restartable, | ||||
|                 tracked=tracked, | ||||
|                 workers=workers, | ||||
|             ) | ||||
|         except Exception: | ||||
|             error_logger.exception("Failed to manage worker %s", ident) | ||||
|         else: | ||||
|             for process in worker.processes: | ||||
|                 process.start() | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| from multiprocessing.connection import Connection | ||||
| from os import environ, getpid | ||||
| from typing import Any, Callable, Dict, Optional | ||||
| from typing import Any, Dict | ||||
|  | ||||
| from sanic.log import Colors, logger | ||||
| from sanic.worker.process import ProcessState | ||||
| @@ -28,27 +28,6 @@ class WorkerMultiplexer: | ||||
|             "state": ProcessState.ACKED.name, | ||||
|         } | ||||
|  | ||||
|     def manage( | ||||
|         self, | ||||
|         ident: str, | ||||
|         func: Callable[..., Any], | ||||
|         kwargs: Dict[str, Any], | ||||
|         transient: bool = False, | ||||
|         restartable: Optional[bool] = None, | ||||
|         tracked: bool = False, | ||||
|         workers: int = 1, | ||||
|     ) -> None: | ||||
|         bundle = ( | ||||
|             ident, | ||||
|             func, | ||||
|             kwargs, | ||||
|             transient, | ||||
|             restartable, | ||||
|             tracked, | ||||
|             workers, | ||||
|         ) | ||||
|         self._monitor_publisher.send(bundle) | ||||
|  | ||||
|     def restart( | ||||
|         self, | ||||
|         name: str = "", | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import os | ||||
|  | ||||
| from datetime import datetime, timezone | ||||
| from multiprocessing.context import BaseContext | ||||
| from signal import SIGINT | ||||
| @@ -19,22 +20,13 @@ class WorkerProcess: | ||||
|     THRESHOLD = 300  # == 30 seconds | ||||
|     SERVER_LABEL = "Server" | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         factory, | ||||
|         name, | ||||
|         target, | ||||
|         kwargs, | ||||
|         worker_state, | ||||
|         restartable: bool = False, | ||||
|     ): | ||||
|     def __init__(self, factory, name, target, kwargs, worker_state): | ||||
|         self.state = ProcessState.IDLE | ||||
|         self.factory = factory | ||||
|         self.name = name | ||||
|         self.target = target | ||||
|         self.kwargs = kwargs | ||||
|         self.worker_state = worker_state | ||||
|         self.restartable = restartable | ||||
|         if self.name not in self.worker_state: | ||||
|             self.worker_state[self.name] = { | ||||
|                 "server": self.SERVER_LABEL in self.name | ||||
| @@ -140,10 +132,6 @@ class WorkerProcess: | ||||
|     def pid(self): | ||||
|         return self._current_process.pid | ||||
|  | ||||
|     @property | ||||
|     def exitcode(self): | ||||
|         return self._current_process.exitcode | ||||
|  | ||||
|     def _terminate_now(self): | ||||
|         logger.debug( | ||||
|             f"{Colors.BLUE}Begin restart termination: " | ||||
| @@ -205,8 +193,6 @@ class Worker: | ||||
|         context: BaseContext, | ||||
|         worker_state: Dict[str, Any], | ||||
|         num: int = 1, | ||||
|         restartable: bool = False, | ||||
|         tracked: bool = True, | ||||
|     ): | ||||
|         self.ident = ident | ||||
|         self.num = num | ||||
| @@ -215,8 +201,6 @@ class Worker: | ||||
|         self.server_settings = server_settings | ||||
|         self.worker_state = worker_state | ||||
|         self.processes: Set[WorkerProcess] = set() | ||||
|         self.restartable = restartable | ||||
|         self.tracked = tracked | ||||
|         for _ in range(num): | ||||
|             self.create_process() | ||||
|  | ||||
| @@ -231,10 +215,6 @@ class Worker: | ||||
|             target=self.serve, | ||||
|             kwargs={**self.server_settings}, | ||||
|             worker_state=self.worker_state, | ||||
|             restartable=self.restartable, | ||||
|         ) | ||||
|         self.processes.add(process) | ||||
|         return process | ||||
|  | ||||
|     def has_alive_processes(self) -> bool: | ||||
|         return any(process.is_alive() for process in self.processes) | ||||
|   | ||||
							
								
								
									
										11
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								setup.py
									
									
									
									
									
								
							| @@ -83,12 +83,11 @@ setup_kwargs = { | ||||
|     "packages": find_packages(exclude=("tests", "tests.*")), | ||||
|     "package_data": {"sanic": ["py.typed", "pages/styles/*"]}, | ||||
|     "platforms": "any", | ||||
|     "python_requires": ">=3.7", | ||||
|     "python_requires": ">=3.8", | ||||
|     "classifiers": [ | ||||
|         "Development Status :: 4 - Beta", | ||||
|         "Environment :: Web Environment", | ||||
|         "License :: OSI Approved :: MIT License", | ||||
|         "Programming Language :: Python :: 3.7", | ||||
|         "Programming Language :: Python :: 3.8", | ||||
|         "Programming Language :: Python :: 3.9", | ||||
|         "Programming Language :: Python :: 3.10", | ||||
| @@ -104,7 +103,7 @@ ujson = "ujson>=1.35" + env_dependency | ||||
| uvloop = "uvloop>=0.15.0" + env_dependency | ||||
| types_ujson = "types-ujson" + env_dependency | ||||
| requirements = [ | ||||
|     "sanic-routing>=22.8.0", | ||||
|     "sanic-routing>=23.6.0", | ||||
|     "httptools>=0.0.10", | ||||
|     uvloop, | ||||
|     ujson, | ||||
| @@ -113,10 +112,11 @@ requirements = [ | ||||
|     "multidict>=5.0,<7.0", | ||||
|     "html5tagger>=1.2.1", | ||||
|     "tracerite>=1.0.0", | ||||
|     "typing-extensions>=4.4.0", | ||||
| ] | ||||
|  | ||||
| tests_require = [ | ||||
|     "sanic-testing>=23.3.0", | ||||
|     "sanic-testing>=23.6.0", | ||||
|     "pytest==7.1.*", | ||||
|     "coverage", | ||||
|     "beautifulsoup4", | ||||
| @@ -127,7 +127,7 @@ tests_require = [ | ||||
|     "black", | ||||
|     "isort>=5.0.0", | ||||
|     "bandit", | ||||
|     "mypy>=0.901,<0.910", | ||||
|     "mypy", | ||||
|     "docutils", | ||||
|     "pygments", | ||||
|     "uvicorn<0.15.0", | ||||
| @@ -143,6 +143,7 @@ docs_require = [ | ||||
|     "m2r2", | ||||
|     "enum-tools[sphinx]", | ||||
|     "mistune<2.0.0", | ||||
|     "autodocsumm>=0.2.11", | ||||
| ] | ||||
|  | ||||
| dev_require = tests_require + [ | ||||
|   | ||||
| @@ -293,7 +293,7 @@ def test_handle_request_with_nested_sanic_exception( | ||||
|  | ||||
|  | ||||
| def test_app_name_required(): | ||||
|     with pytest.raises(SanicException): | ||||
|     with pytest.raises(TypeError): | ||||
|         Sanic() | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,8 @@ import logging | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| import sanic | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.config import Config | ||||
| 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" | ||||
|  | ||||
|  | ||||
| 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): | ||||
|     with pytest.raises(SanicException, match="Unknown format: bad"): | ||||
|         app.config.FALLBACK_ERROR_FORMAT = "bad" | ||||
|   | ||||
| @@ -17,6 +17,7 @@ from sanic.response import text | ||||
| CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | ||||
|  | ||||
| PORT = 42001  # test_keep_alive_timeout_reuse doesn't work with random port | ||||
| MAX_LOOPS = 15 | ||||
| port_counter = count() | ||||
|  | ||||
|  | ||||
| @@ -69,10 +70,15 @@ def test_keep_alive_timeout_reuse(): | ||||
|     """If the server keep-alive timeout and client keep-alive timeout are | ||||
|     both longer than the delay, the client _and_ server will successfully | ||||
|     reuse the existing connection.""" | ||||
|     loops = 0 | ||||
|     while True: | ||||
|         port = get_port() | ||||
|         loop = asyncio.new_event_loop() | ||||
|         asyncio.set_event_loop(loop) | ||||
|     client = ReusableClient(keep_alive_timeout_app_reuse, loop=loop, port=port) | ||||
|         client = ReusableClient( | ||||
|             keep_alive_timeout_app_reuse, loop=loop, port=port | ||||
|         ) | ||||
|         try: | ||||
|             with client: | ||||
|                 headers = {"Connection": "keep-alive"} | ||||
|                 request, response = client.get("/1", headers=headers) | ||||
| @@ -86,6 +92,13 @@ def test_keep_alive_timeout_reuse(): | ||||
|                 assert response.status == 200 | ||||
|                 assert response.text == "OK" | ||||
|                 assert request.protocol.state["requests_count"] == 2 | ||||
|         except OSError: | ||||
|             loops += 1 | ||||
|             if loops > MAX_LOOPS: | ||||
|                 raise | ||||
|             continue | ||||
|         else: | ||||
|             break | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
| @@ -97,6 +110,9 @@ def test_keep_alive_timeout_reuse(): | ||||
| def test_keep_alive_client_timeout(): | ||||
|     """If the server keep-alive timeout is longer than the client | ||||
|     keep-alive timeout, client will try to create a new connection here.""" | ||||
|     loops = 0 | ||||
|     while True: | ||||
|         try: | ||||
|             port = get_port() | ||||
|             loop = asyncio.new_event_loop() | ||||
|             asyncio.set_event_loop(loop) | ||||
| @@ -105,7 +121,9 @@ def test_keep_alive_client_timeout(): | ||||
|             ) | ||||
|             with client: | ||||
|                 headers = {"Connection": "keep-alive"} | ||||
|         request, response = client.get("/1", headers=headers, timeout=1) | ||||
|                 request, response = client.get( | ||||
|                     "/1", headers=headers, timeout=1 | ||||
|                 ) | ||||
|  | ||||
|                 assert response.status == 200 | ||||
|                 assert response.text == "OK" | ||||
| @@ -114,6 +132,13 @@ def test_keep_alive_client_timeout(): | ||||
|                 loop.run_until_complete(aio_sleep(2)) | ||||
|                 request, response = client.get("/1", timeout=1) | ||||
|                 assert request.protocol.state["requests_count"] == 1 | ||||
|         except OSError: | ||||
|             loops += 1 | ||||
|             if loops > MAX_LOOPS: | ||||
|                 raise | ||||
|             continue | ||||
|         else: | ||||
|             break | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
| @@ -125,6 +150,9 @@ def test_keep_alive_server_timeout(): | ||||
|     keep-alive timeout, the client will either a 'Connection reset' error | ||||
|     _or_ a new connection. Depending on how the event-loop handles the | ||||
|     broken server connection.""" | ||||
|     loops = 0 | ||||
|     while True: | ||||
|         try: | ||||
|             port = get_port() | ||||
|             loop = asyncio.new_event_loop() | ||||
|             asyncio.set_event_loop(loop) | ||||
| @@ -133,7 +161,9 @@ def test_keep_alive_server_timeout(): | ||||
|             ) | ||||
|             with client: | ||||
|                 headers = {"Connection": "keep-alive"} | ||||
|         request, response = client.get("/1", headers=headers, timeout=60) | ||||
|                 request, response = client.get( | ||||
|                     "/1", headers=headers, timeout=60 | ||||
|                 ) | ||||
|  | ||||
|                 assert response.status == 200 | ||||
|                 assert response.text == "OK" | ||||
| @@ -143,6 +173,13 @@ def test_keep_alive_server_timeout(): | ||||
|                 request, response = client.get("/1", timeout=60) | ||||
|  | ||||
|                 assert request.protocol.state["requests_count"] == 1 | ||||
|         except OSError: | ||||
|             loops += 1 | ||||
|             if loops > MAX_LOOPS: | ||||
|                 raise | ||||
|             continue | ||||
|         else: | ||||
|             break | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
| @@ -150,10 +187,15 @@ def test_keep_alive_server_timeout(): | ||||
|     reason="Not testable with current client", | ||||
| ) | ||||
| def test_keep_alive_connection_context(): | ||||
|     loops = 0 | ||||
|     while True: | ||||
|         try: | ||||
|             port = get_port() | ||||
|             loop = asyncio.new_event_loop() | ||||
|             asyncio.set_event_loop(loop) | ||||
|     client = ReusableClient(keep_alive_app_context, loop=loop, port=port) | ||||
|             client = ReusableClient( | ||||
|                 keep_alive_app_context, loop=loop, port=port | ||||
|             ) | ||||
|             with client: | ||||
|                 headers = {"Connection": "keep-alive"} | ||||
|                 request1, _ = client.post("/ctx", headers=headers) | ||||
| @@ -164,6 +206,15 @@ def test_keep_alive_connection_context(): | ||||
|                 assert response.text == "hello" | ||||
|                 assert id(request1.conn_info.ctx) == id(request2.conn_info.ctx) | ||||
|                 assert ( | ||||
|             request1.conn_info.ctx.foo == request2.conn_info.ctx.foo == "hello" | ||||
|                     request1.conn_info.ctx.foo | ||||
|                     == request2.conn_info.ctx.foo | ||||
|                     == "hello" | ||||
|                 ) | ||||
|                 assert request2.protocol.state["requests_count"] == 2 | ||||
|         except OSError: | ||||
|             loops += 1 | ||||
|             if loops > MAX_LOOPS: | ||||
|                 raise | ||||
|             continue | ||||
|         else: | ||||
|             break | ||||
|   | ||||
| @@ -126,7 +126,7 @@ def test_redirect_with_params(app, test_str): | ||||
|  | ||||
|     @app.route("/api/v2/test/<test>/", unquote=True) | ||||
|     async def target_handler(request, test): | ||||
|         assert test == test_str | ||||
|         assert test == quote(test_str) | ||||
|         return text("OK") | ||||
|  | ||||
|     _, 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): | ||||
|     request = Request(b"/", {}, None, method, None, None) | ||||
|     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) | ||||
|     assert response.json == {"for": "127.0.0.2", "proto": "ws"} | ||||
|     assert request.remote_addr == "127.0.0.2" | ||||
|     assert request.client_ip == "127.0.0.2" | ||||
|     assert request.scheme == "ws" | ||||
|     assert request.server_name == "local.site" | ||||
|     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"} | ||||
|     request, response = app.test_client.get("/", headers=headers) | ||||
|     assert request.remote_addr == "" | ||||
|     assert request.client_ip == "127.0.0.1" | ||||
|     assert response.body == b"" | ||||
|  | ||||
|     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| import asyncio | ||||
| import os | ||||
| import signal | ||||
|  | ||||
| from queue import Queue | ||||
| from types import SimpleNamespace | ||||
| from typing import Optional | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_testing.testing import HOST, PORT | ||||
|  | ||||
| from sanic import Sanic | ||||
| @@ -158,7 +160,7 @@ def test_signal_server_lifecycle_exception(app: Sanic): | ||||
|     async def hello_route(request): | ||||
|         return HTTPResponse() | ||||
|  | ||||
|     @app.signal(Event.SERVER_LIFECYCLE_EXCEPTION) | ||||
|     @app.signal(Event.SERVER_EXCEPTION_REPORT) | ||||
|     async def test_signal(exception: Exception): | ||||
|         nonlocal trigger | ||||
|         trigger = exception | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import asyncio | ||||
|  | ||||
| from enum import Enum | ||||
| from inspect import isawaitable | ||||
| from itertools import count | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| @@ -9,6 +10,7 @@ from sanic_routing.exceptions import NotFound | ||||
|  | ||||
| from sanic import Blueprint, Sanic, empty | ||||
| from sanic.exceptions import InvalidSignal, SanicException | ||||
| from sanic.signals import Event | ||||
|  | ||||
|  | ||||
| def test_add_signal(app): | ||||
| @@ -427,3 +429,114 @@ def test_signal_reservation(app, event, expected): | ||||
|             app.signal(event)(lambda: ...) | ||||
|     else: | ||||
|         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 | ||||
|   | ||||
							
								
								
									
										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] | ||||
| 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] | ||||
| usedevelop = true | ||||
| setenv = | ||||
|     {py37,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_UJSON=1 | ||||
|     {py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 | ||||
| extras = test, http3 | ||||
| deps = | ||||
|     httpx==0.23 | ||||
|     httpx>=0.23 | ||||
| allowlist_externals = | ||||
|     pytest | ||||
|     coverage | ||||
|   | ||||
		Reference in New Issue
	
	Block a user