Compare commits
	
		
			46 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 5a48b94089 | ||
|   | ba1c73d947 | ||
|   | a6e78b70ab | ||
|   | bb1174afc5 | ||
|   | df8abe9cfd | ||
|   | c3bca97ee1 | ||
|   | 8df80e276b | ||
|   | 30572c972d | ||
|   | 53da4dd091 | ||
|   | 108a4a99c7 | ||
|   | 7c180376d6 | ||
|   | f39b8b32f7 | ||
|   | c543d19f8a | ||
|   | 80fca9aef7 | ||
|   | 5bb9aa0c2c | ||
|   | 83c746ee57 | ||
|   | aff6604636 | ||
|   | 2c80571a8a | ||
|   | d964b552af | ||
|   | 48f8b37b74 | ||
|   | 141be0028d | ||
|   | a140c47195 | ||
|   | 0c3a8392f2 | ||
|   | 16875b1f41 | ||
|   | b1f31f2eeb | ||
|   | d16b9e5a02 | ||
|   | 680484bdc8 | ||
|   | 05cd44b5dd | ||
|   | ba374139f4 | ||
|   | 72a745bfd5 | ||
|   | 3a6fac7d59 | ||
|   | 28ba8e53df | ||
|   | 9b26358e63 | ||
|   | e21521f45c | ||
|   | 30479765cb | ||
|   | 53a571ec6c | ||
|   | ad97cac313 | ||
|   | 1a352ddf55 | ||
|   | 5ba43decf2 | ||
|   | 8f06d035cb | ||
|   | b716f48c84 | ||
|   | 42b1e7143e | ||
|   | eba7821a6d | ||
|   | 93a0246c03 | ||
|   | dfd1787a49 | ||
|   | 4998fd54c0 | 
							
								
								
									
										12
									
								
								.codeclimate.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.codeclimate.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| exclude_patterns: | ||||
|   - "sanic/__main__.py" | ||||
|   - "sanic/reloader_helpers.py" | ||||
|   - "sanic/simple.py" | ||||
|   - "sanic/utils.py" | ||||
|   - ".github/" | ||||
|   - "changelogs/" | ||||
|   - "docker/" | ||||
|   - "docs/" | ||||
|   - "examples/" | ||||
|   - "hack/" | ||||
|   - "scripts/" | ||||
| @@ -1,7 +1,12 @@ | ||||
| [run] | ||||
| branch = True | ||||
| source = sanic | ||||
| omit = site-packages, sanic/utils.py, sanic/__main__.py | ||||
| omit = | ||||
|     site-packages | ||||
|     sanic/__main__.py | ||||
|     sanic/reloader_helpers.py | ||||
|     sanic/simple.py | ||||
|     sanic/utils.py | ||||
|  | ||||
| [html] | ||||
| directory = coverage | ||||
|   | ||||
							
								
								
									
										37
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										37
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,22 +1,10 @@ | ||||
| # For most projects, this workflow file will not need changing; you simply need | ||||
| # to commit it to your repository. | ||||
| # | ||||
| # You may wish to alter this file to override the set of languages analyzed, | ||||
| # or to provide custom queries or build logic. | ||||
| # | ||||
| # ******** NOTE ******** | ||||
| # We have attempted to detect the languages in your repository. Please check | ||||
| # the `language` matrix defined below to confirm you have the correct set of | ||||
| # supported CodeQL languages. | ||||
| # | ||||
| name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master ] | ||||
|     branches: [ main ] | ||||
|   pull_request: | ||||
|     # The branches below must be a subset of the branches above | ||||
|     branches: [ master ] | ||||
|     branches: [ main ] | ||||
|   schedule: | ||||
|     - cron: '25 16 * * 0' | ||||
|  | ||||
| @@ -29,39 +17,18 @@ jobs: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         language: [ 'python' ] | ||||
|         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] | ||||
|         # Learn more: | ||||
|         # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed | ||||
|  | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v2 | ||||
|  | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - name: Initialize CodeQL | ||||
|       uses: github/codeql-action/init@v1 | ||||
|       with: | ||||
|         languages: ${{ matrix.language }} | ||||
|         # If you wish to specify custom queries, you can do so here or in a config file. | ||||
|         # By default, queries listed here will override any specified in a config file. | ||||
|         # Prefix the list here with "+" to use these queries and those in the config file. | ||||
|         # queries: ./path/to/local/query, your-org/your-repo/queries@main | ||||
|  | ||||
|     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||
|     # If this step fails, then you should remove it and run the build manually (see below) | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v1 | ||||
|  | ||||
|     # ℹ️ Command-line programs to run using the OS shell. | ||||
|     # 📚 https://git.io/JvXDl | ||||
|  | ||||
|     # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines | ||||
|     #    and modify them (or add more) to build your code if your project | ||||
|     #    uses a compiled language | ||||
|  | ||||
|     #- run: | | ||||
|     #   make bootstrap | ||||
|     #   make release | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v1 | ||||
|   | ||||
							
								
								
									
										40
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| name: Coverage check | ||||
| # on: | ||||
| #   push: | ||||
| #     branches: | ||||
| #       - main | ||||
| #     tags: | ||||
| #       - "!*" # Do not execute on tags | ||||
| #     paths: | ||||
| #       - sanic/* | ||||
| #       - tests/* | ||||
| #   pull_request: | ||||
| #     paths: | ||||
| #       - "!*.MD" | ||||
| on: [push, pull_request] | ||||
| jobs: | ||||
|   test: | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     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 | ||||
|       - uses: paambaati/codeclimate-action@v2.5.3 | ||||
|         if: always() | ||||
|         env: | ||||
|           CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }} | ||||
|         with: | ||||
|           coverageCommand: tox -e coverage | ||||
							
								
								
									
										39
									
								
								.github/workflows/on-demand.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								.github/workflows/on-demand.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| 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" | ||||
							
								
								
									
										32
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| name: Security Analysis | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|  | ||||
| jobs: | ||||
|   bandit: | ||||
|     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} | ||||
|     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 }}" | ||||
							
								
								
									
										29
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| name: Document Linter | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|  | ||||
| jobs: | ||||
|   docsLinter: | ||||
|     name: Lint Documentation | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         config: | ||||
|           - {python-version: "3.8", 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 }}" | ||||
							
								
								
									
										30
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| name: Linter Checks | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|  | ||||
| jobs: | ||||
|   linter: | ||||
|     name: lint | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         os: [ubuntu-latest] | ||||
|         config: | ||||
|           - { python-version: 3.8, 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
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								.github/workflows/pr-python-pypy.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| 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" | ||||
							
								
								
									
										38
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| name: Python 3.7 Tests | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - sanic/* | ||||
|       - tests/* | ||||
|  | ||||
| jobs: | ||||
|   testPy37: | ||||
|     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.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" | ||||
							
								
								
									
										38
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| name: Python 3.8 Tests | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - sanic/* | ||||
|       - tests/* | ||||
|  | ||||
| jobs: | ||||
|   testPy38: | ||||
|     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.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" | ||||
							
								
								
									
										50
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| name: Python 3.9 Tests | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - sanic/* | ||||
|       - tests/* | ||||
|  | ||||
| jobs: | ||||
|   testPy39: | ||||
|     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.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" | ||||
							
								
								
									
										32
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| name: Typing Checks | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|  | ||||
| jobs: | ||||
|   typeChecking: | ||||
|     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} | ||||
|     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 }}" | ||||
							
								
								
									
										34
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| # name: Run Unit Tests on Windows | ||||
| # on: | ||||
| #   pull_request: | ||||
| #     branches: | ||||
| #       - main | ||||
|  | ||||
| # jobs: | ||||
| #   testsOnWindows: | ||||
| #     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: 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
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								.github/workflows/publish-images.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| 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"] | ||||
|  | ||||
|     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
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/publish-package.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| 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.8"] | ||||
|  | ||||
|     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" | ||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -6,6 +6,7 @@ | ||||
| .coverage | ||||
| .coverage.* | ||||
| coverage | ||||
| coverage.xml | ||||
| .tox | ||||
| settings.py | ||||
| .idea/* | ||||
| @@ -18,3 +19,6 @@ build/* | ||||
| .DS_Store | ||||
| dist/* | ||||
| pip-wheel-metadata/ | ||||
| .pytest_cache/* | ||||
| .venv/* | ||||
| .vscode/* | ||||
|   | ||||
							
								
								
									
										94
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,94 +0,0 @@ | ||||
| sudo: false | ||||
| language: python | ||||
| cache: | ||||
|   directories: | ||||
|     - $HOME/.cache/pip | ||||
| matrix: | ||||
|   include: | ||||
|     - env: TOX_ENV=py37 | ||||
|       python: 3.7 | ||||
|       dist: xenial | ||||
|       sudo: true | ||||
|       name: "Python 3.7 with Extensions" | ||||
|     - env: TOX_ENV=py37-no-ext | ||||
|       python: 3.7 | ||||
|       dist: xenial | ||||
|       sudo: true | ||||
|       name: "Python 3.7 without Extensions" | ||||
|     - env: TOX_ENV=py38 | ||||
|       python: 3.8 | ||||
|       dist: xenial | ||||
|       sudo: true | ||||
|       name: "Python 3.8 with Extensions" | ||||
|     - env: TOX_ENV=py38-no-ext | ||||
|       python: 3.8 | ||||
|       dist: xenial | ||||
|       sudo: true | ||||
|       name: "Python 3.8 without Extensions" | ||||
|     - env: TOX_ENV=py39 | ||||
|       python: 3.9 | ||||
|       dist: bionic | ||||
|       sudo: true | ||||
|       name: "Python 3.9 with Extensions" | ||||
|     - env: TOX_ENV=py39-no-ext | ||||
|       python: 3.9 | ||||
|       dist: bionic | ||||
|       sudo: true | ||||
|       name: "Python 3.9 without Extensions" | ||||
|     - env: TOX_ENV=type-checking | ||||
|       python: 3.7 | ||||
|       name: "Python 3.7 Type checks" | ||||
|     - env: TOX_ENV=type-checking | ||||
|       python: 3.8 | ||||
|       name: "Python 3.8 Type checks" | ||||
|     - env: TOX_ENV=type-checking | ||||
|       python: 3.9 | ||||
|       dist: bionic | ||||
|       name: "Python 3.9 Type checks" | ||||
|     - env: TOX_ENV=security | ||||
|       python: 3.7 | ||||
|       dist: xenial | ||||
|       sudo: true | ||||
|       name: "Python 3.7 Bandit security scan" | ||||
|     - env: TOX_ENV=security | ||||
|       python: 3.8 | ||||
|       dist: xenial | ||||
|       sudo: true | ||||
|       name: "Python 3.8 Bandit security scan" | ||||
|     - env: TOX_ENV=security | ||||
|       python: 3.9 | ||||
|       dist: bionic | ||||
|       sudo: true | ||||
|       name: "Python 3.9 Bandit security scan" | ||||
|     - env: TOX_ENV=docs | ||||
|       python: 3.7 | ||||
|       dist: xenial | ||||
|       sudo: true | ||||
|       name: "Python 3.7 Documentation tests" | ||||
|     - env: TOX_ENV=pyNightly | ||||
|       python: "nightly" | ||||
|       name: "Python nightly with Extensions" | ||||
|     - env: TOX_ENV=pyNightly-no-ext | ||||
|       python: "nightly" | ||||
|       name: "Python nightly without Extensions" | ||||
|   allow_failures: | ||||
|     - env: TOX_ENV=pyNightly | ||||
|       python: "nightly" | ||||
|       name: "Python nightly with Extensions" | ||||
|     - env: TOX_ENV=pyNightly-no-ext | ||||
|       python: "nightly" | ||||
|       name: "Python nightly without Extensions" | ||||
| install: | ||||
|   - pip install -U tox | ||||
|   - pip install codecov | ||||
| script: travis_retry tox -e $TOX_ENV | ||||
| after_success: | ||||
|   - codecov | ||||
| deploy: | ||||
|   provider: pypi | ||||
|   user: brewmaster | ||||
|   password: | ||||
|     secure: "GoawLwmbtJOgKB6AJ0ZSYUUnNwIoonseHBxaAUH3zu79TS/Afrq+yB3lsVaMSG0CbyDgN4FrfD1phT1NzbvZ1VcLIOTDtCrmpQ1kLDw+zwgF40ab8sp8fPkKVHHHfCCs1mjltHIpxQa5lZTJcAs6Bpi/lbUWWwYxFzSV8pHw4W4hY09EHUd2o+evLTSVxaploetSt725DJUYKICUr2eAtCC11IDnIW4CzBJEx6krVV3uhzfTJW0Ls17x0c6sdZ9icMnV/G9xO/eQH6RIHe4xcrWJ6cmLDNKoGAkJp+BKr1CeVVg7Jw/MzPjvZKL2/ki6Beue1y6GUIy7lOS7jPVaOEhJ23b0zQwFcLMZw+Tt+E3v6QfHk+B/WBBBnM3zUZed9UI+QyW8+lqLLt39sQX0FO0P3eaDh8qTXtUuon2jTyFMMAMTFRTNpJmpAzuBH9yeMmDeALPTh0HphI+BkoUl5q1QbWFYjjnZMH2CatApxpLybt9A7rwm//PbOG0TSI93GEKNQ4w5DYryKTfwHzRBptNSephJSuxZYEfJsmUtas5es1D7Fe0PkyjxNNSU+eO+8wsTlitLUsJO4k0jAgy+cEKdU7YJ3J0GZVXocSkrNnUfd2hQPcJ3UtEJx3hLqqr8EM7EZBAasc1yGHh36NFetclzFY24YPih0G1+XurhTys=" | ||||
|   on: | ||||
|     tags: true | ||||
|   distributions: "sdist bdist_wheel" | ||||
							
								
								
									
										104
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							| @@ -1,3 +1,107 @@ | ||||
| Version 21.6.0 | ||||
| -------------- | ||||
|  | ||||
| Features | ||||
| ******** | ||||
|  | ||||
|   * `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_ | ||||
|     Add ``response.eof()`` method for closing a stream in a handler | ||||
|   * `#2097 <https://github.com/sanic-org/sanic/pull/2097>`_ | ||||
|     Allow case-insensitive HTTP Upgrade header | ||||
|   * `#2104 <https://github.com/sanic-org/sanic/pull/2104>`_ | ||||
|     Explicit usage of CIMultiDict getters | ||||
|   * `#2109 <https://github.com/sanic-org/sanic/pull/2109>`_ | ||||
|     Consistent use of error loggers | ||||
|   * `#2114 <https://github.com/sanic-org/sanic/pull/2114>`_ | ||||
|     New ``client_ip`` access of connection info instance | ||||
|   * `#2119 <https://github.com/sanic-org/sanic/pull/2119>`_ | ||||
|     Alternatate classes on instantiation for ``Config`` and ``Sanic.ctx`` | ||||
|   * `#2133 <https://github.com/sanic-org/sanic/pull/2133>`_ | ||||
|     Implement new version of AST router | ||||
|  | ||||
|       * Proper differentiation between ``alpha`` and ``string`` param types | ||||
|       * Adds a ``slug`` param type, example: ``<foo:slug>`` | ||||
|       * Deprecates ``<foo:string>`` in favor of ``<foo:str>`` | ||||
|       * Deprecates ``<foo:number>`` in favor of ``<foo:float>`` | ||||
|       * Adds a ``route.uri`` accessor | ||||
|   * `#2136 <https://github.com/sanic-org/sanic/pull/2136>`_ | ||||
|     CLI improvements with new optional params | ||||
|   * `#2137 <https://github.com/sanic-org/sanic/pull/2137>`_ | ||||
|     Add ``version_prefix`` to URL builders | ||||
|   * `#2140 <https://github.com/sanic-org/sanic/pull/2140>`_ | ||||
|     Event autoregistration with ``EVENT_AUTOREGISTER`` | ||||
|   * `#2146 <https://github.com/sanic-org/sanic/pull/2146>`_, `#2147 <https://github.com/sanic-org/sanic/pull/2147>`_ | ||||
|     Require stricter names on  ``Sanic()`` and ``Blueprint()`` | ||||
|   * `#2150 <https://github.com/sanic-org/sanic/pull/2150>`_ | ||||
|     Infinitely reusable and nestable ``Blueprint`` and ``BlueprintGroup`` | ||||
|   * `#2154 <https://github.com/sanic-org/sanic/pull/2154>`_ | ||||
|     Upgrade ``websockets`` dependency to min version | ||||
|   * `#2155 <https://github.com/sanic-org/sanic/pull/2155>`_ | ||||
|     Allow for maximum header sizes to be increased: ``REQUEST_MAX_HEADER_SIZE`` | ||||
|   * `#2157 <https://github.com/sanic-org/sanic/pull/2157>`_ | ||||
|     Allow app factory pattern in CLI | ||||
|   * `#2165 <https://github.com/sanic-org/sanic/pull/2165>`_ | ||||
|     Change HTTP methods to enums | ||||
|   * `#2167 <https://github.com/sanic-org/sanic/pull/2167>`_ | ||||
|     Allow auto-reloading on additional directories | ||||
|   * `#2168 <https://github.com/sanic-org/sanic/pull/2168>`_ | ||||
|     Add simple HTTP server to CLI | ||||
|   * `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_ | ||||
|     Additional methods for attaching ``HTTPMethodView`` | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
|  | ||||
|   * `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_ | ||||
|     Fix ``UserWarning`` in ASGI mode for missing ``__slots__`` | ||||
|   * `#2099 <https://github.com/sanic-org/sanic/pull/2099>`_ | ||||
|     Fix static request handler logging exception on 404 | ||||
|   * `#2110 <https://github.com/sanic-org/sanic/pull/2110>`_ | ||||
|     Fix request.args.pop removes parameters inconsistently | ||||
|   * `#2107 <https://github.com/sanic-org/sanic/pull/2107>`_ | ||||
|     Fix type hinting for load_env | ||||
|   * `#2127 <https://github.com/sanic-org/sanic/pull/2127>`_ | ||||
|     Make sure ASGI ws subprotocols is a list | ||||
|   * `#2128 <https://github.com/sanic-org/sanic/pull/2128>`_ | ||||
|     Fix issue where Blueprint exception handlers do not consistently route to proper handler | ||||
|  | ||||
|  | ||||
| Deprecations and Removals | ||||
| ************************* | ||||
|  | ||||
|   * `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_ | ||||
|     Remove config value ``REQUEST_BUFFER_QUEUE_SIZE`` | ||||
|   * `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_ | ||||
|     ``CompositionView`` deprecated and marked for removal in 21.12 | ||||
|   * `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_ | ||||
|     Deprecate StreamingHTTPResponse | ||||
|  | ||||
| Developer infrastructure | ||||
| ************************ | ||||
|  | ||||
|   * `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_ | ||||
|     Remove Travis CI in favor of GitHub Actions | ||||
|  | ||||
| Improved Documentation | ||||
| ********************** | ||||
|  | ||||
|   * `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_ | ||||
|     Fix typo in documentation | ||||
|   * `#2100 <https://github.com/sanic-org/sanic/pull/2100>`_ | ||||
|     Remove documentation for non-existent arguments | ||||
|  | ||||
| Version 21.3.2 | ||||
| -------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
|  | ||||
|   * `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_ | ||||
|     Disable response timeout on websocket connections | ||||
|  | ||||
|   * `#2085 <https://github.com/sanic-org/sanic/pull/2085>`_ | ||||
|     Make sure that blueprints with no slash is maintained when applied | ||||
|  | ||||
| Version 21.3.1 | ||||
| -------------- | ||||
|  | ||||
|   | ||||
| @@ -87,7 +87,7 @@ Permform ``flake8``\ , ``black`` and ``isort`` checks. | ||||
|    tox -e lint | ||||
|  | ||||
| Run type annotation checks | ||||
| --------------- | ||||
| -------------------------- | ||||
|  | ||||
| ``tox`` environment -> ``[testenv:type-checking]`` | ||||
|  | ||||
|   | ||||
							
								
								
									
										6
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								Makefile
									
									
									
									
									
								
							| @@ -49,6 +49,9 @@ test: clean | ||||
| test-coverage: clean | ||||
| 	python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append " | ||||
|  | ||||
| view-coverage: | ||||
| 	sanic ./coverage --simple | ||||
|  | ||||
| install: | ||||
| 	python setup.py install | ||||
|  | ||||
| @@ -85,8 +88,7 @@ docs-test: docs-clean | ||||
| 	cd docs && make dummy | ||||
|  | ||||
| docs-serve: | ||||
| 	# python -m http.server --directory=./docs/_build/html 9999 | ||||
| 	sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic | ||||
| 	sphinx-autobuild docs docs/_build/html --port 9999 --watch ./ | ||||
|  | ||||
| changelog: | ||||
| 	python scripts/changelog.py | ||||
|   | ||||
							
								
								
									
										12
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.rst
									
									
									
									
									
								
							| @@ -11,7 +11,7 @@ Sanic | Build fast. Run fast. | ||||
|     :stub-columns: 1 | ||||
|  | ||||
|     * - Build | ||||
|       - | |Build Status| |AppVeyor Build Status| |Codecov| | ||||
|       - | |Py39Test| |Py38Test| |Py37Test| |Codecov| | ||||
|     * - Docs | ||||
|       - | |UserGuide| |Documentation| | ||||
|     * - Package | ||||
| @@ -29,10 +29,12 @@ Sanic | Build fast. Run fast. | ||||
|    :target: https://discord.gg/FARQzAEMAA | ||||
| .. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg | ||||
|     :target: https://codecov.io/gh/sanic-org/sanic | ||||
| .. |Build Status| image:: https://travis-ci.com/sanic-org/sanic.svg?branch=master | ||||
|    :target: https://travis-ci.com/sanic-org/sanic | ||||
| .. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true | ||||
|    :target: https://ci.appveyor.com/project/sanic-org/sanic | ||||
| .. |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 | ||||
| .. |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 | ||||
|   | ||||
							
								
								
									
										14
									
								
								codecov.yml
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								codecov.yml
									
									
									
									
									
								
							| @@ -1,14 +0,0 @@ | ||||
| codecov: | ||||
|   require_ci_to_pass: no | ||||
| coverage: | ||||
|   precision: 3 | ||||
|   round: nearest | ||||
|   status: | ||||
|     project: | ||||
|       default: | ||||
|         target: auto | ||||
|         threshold: 0.5% | ||||
|     patch: | ||||
|       default: | ||||
|         target: auto | ||||
|         threshold: 0.75% | ||||
| @@ -1,28 +1,9 @@ | ||||
| FROM alpine:3.7 | ||||
| ARG BASE_IMAGE_TAG | ||||
|  | ||||
| RUN apk add --no-cache --update \ | ||||
|         curl \ | ||||
|         bash \ | ||||
|         build-base \ | ||||
|         ca-certificates \ | ||||
|         git \ | ||||
|         bzip2-dev \ | ||||
|         linux-headers \ | ||||
|         ncurses-dev \ | ||||
|         openssl \ | ||||
|         openssl-dev \ | ||||
|         readline-dev \ | ||||
|         sqlite-dev | ||||
| FROM sanicframework/sanic-build:${BASE_IMAGE_TAG} | ||||
|  | ||||
| RUN apk update | ||||
| RUN update-ca-certificates | ||||
| RUN rm -rf /var/cache/apk/* | ||||
|  | ||||
| ENV PYENV_ROOT="/root/.pyenv" | ||||
| ENV PATH="$PYENV_ROOT/bin:$PATH" | ||||
|  | ||||
| ADD . /app | ||||
| WORKDIR /app | ||||
|  | ||||
| RUN /app/docker/bin/install_python.sh 3.5.4 3.6.4 | ||||
|  | ||||
| ENTRYPOINT ["./docker/bin/entrypoint.sh"] | ||||
| RUN pip install sanic | ||||
| RUN apk del build-base | ||||
|   | ||||
							
								
								
									
										9
									
								
								docker/Dockerfile-base
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								docker/Dockerfile-base
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| ARG PYTHON_VERSION | ||||
|  | ||||
| FROM python:${PYTHON_VERSION}-alpine | ||||
| RUN apk update | ||||
| RUN apk add --no-cache --update build-base \ | ||||
|         ca-certificates \ | ||||
|         openssl | ||||
| RUN update-ca-certificates | ||||
| RUN rm -rf /var/cache/apk/* | ||||
| @@ -1,11 +0,0 @@ | ||||
| #!/bin/bash | ||||
| set -e | ||||
|  | ||||
| eval "$(pyenv init -)" | ||||
| eval "$(pyenv virtualenv-init -)" | ||||
| source /root/.pyenv/completions/pyenv.bash | ||||
|  | ||||
| pip install tox | ||||
|  | ||||
| exec $@ | ||||
|  | ||||
| @@ -1,17 +0,0 @@ | ||||
| #!/bin/bash | ||||
| set -e | ||||
|  | ||||
| export CFLAGS='-O2' | ||||
| export EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000" | ||||
|  | ||||
| curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash | ||||
| eval "$(pyenv init -)" | ||||
|  | ||||
| for ver in $@ | ||||
| do | ||||
|     pyenv install $ver | ||||
| done | ||||
|  | ||||
| pyenv global $@ | ||||
| pip install --upgrade pip | ||||
| pyenv rehash | ||||
							
								
								
									
										17
									
								
								docs/sanic/api/app.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docs/sanic/api/app.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| Application | ||||
| =========== | ||||
|  | ||||
| sanic.app | ||||
| --------- | ||||
|  | ||||
| .. automodule:: sanic.app | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|     :inherited-members: | ||||
|  | ||||
| sanic.config | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.config | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
							
								
								
									
										17
									
								
								docs/sanic/api/blueprints.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docs/sanic/api/blueprints.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| Blueprints | ||||
| ========== | ||||
|  | ||||
| sanic.blueprints | ||||
| ---------------- | ||||
|  | ||||
| .. automodule:: sanic.blueprints | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|     :inherited-members: | ||||
|  | ||||
| sanic.blueprint_group | ||||
| --------------------- | ||||
|  | ||||
| .. automodule:: sanic.blueprint_group | ||||
|     :members: | ||||
|     :special-members: | ||||
							
								
								
									
										47
									
								
								docs/sanic/api/core.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								docs/sanic/api/core.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| Core | ||||
| ==== | ||||
|  | ||||
| sanic.cookies | ||||
| ------------- | ||||
|  | ||||
| .. automodule:: sanic.cookies | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
|  | ||||
| sanic.handlers | ||||
| -------------- | ||||
|  | ||||
| .. automodule:: sanic.handlers | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
|  | ||||
| sanic.request | ||||
| ------------- | ||||
|  | ||||
| .. automodule:: sanic.request | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.response | ||||
| -------------- | ||||
|  | ||||
| .. automodule:: sanic.response | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
|  | ||||
| sanic.views | ||||
| ----------- | ||||
|  | ||||
| .. automodule:: sanic.views | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.websocket | ||||
| --------------- | ||||
|  | ||||
| .. automodule:: sanic.websocket | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
							
								
								
									
										16
									
								
								docs/sanic/api/exceptions.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								docs/sanic/api/exceptions.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| Exceptions | ||||
| ========== | ||||
|  | ||||
| sanic.errorpages | ||||
| ---------------- | ||||
|  | ||||
| .. automodule:: sanic.errorpages | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.exceptions | ||||
| ---------------- | ||||
|  | ||||
| .. automodule:: sanic.exceptions | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
							
								
								
									
										18
									
								
								docs/sanic/api/router.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								docs/sanic/api/router.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| Routing | ||||
| ======= | ||||
|  | ||||
| sanic_routing models | ||||
| -------------------- | ||||
|  | ||||
| .. autoclass:: sanic_routing.route::Route | ||||
|     :members: | ||||
|  | ||||
| .. autoclass:: sanic_routing.group::RouteGroup | ||||
|     :members: | ||||
|  | ||||
| sanic.router | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.router | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
							
								
								
									
										25
									
								
								docs/sanic/api/server.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								docs/sanic/api/server.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| Sanic Server | ||||
| ============ | ||||
|  | ||||
| sanic.http | ||||
| ---------- | ||||
|  | ||||
| .. automodule:: sanic.http | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
|  | ||||
| sanic.server | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.server | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
|  | ||||
| sanic.worker | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.worker | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
							
								
								
									
										16
									
								
								docs/sanic/api/utility.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								docs/sanic/api/utility.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| Utility | ||||
| ======= | ||||
|  | ||||
| sanic.compat | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.compat | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.log | ||||
| --------- | ||||
|  | ||||
| .. automodule:: sanic.log | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
| @@ -1,132 +1,13 @@ | ||||
| 📑 API Reference | ||||
| ================ | ||||
|  | ||||
| sanic.app | ||||
| --------- | ||||
| .. toctree:: | ||||
|    :maxdepth: 2 | ||||
|  | ||||
| .. automodule:: sanic.app | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|     :inherited-members: | ||||
|  | ||||
| sanic.blueprints | ||||
| ---------------- | ||||
|  | ||||
| .. automodule:: sanic.blueprints | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|     :inherited-members: | ||||
|  | ||||
| sanic.blueprint_group | ||||
| --------------------- | ||||
|  | ||||
| .. automodule:: sanic.blueprint_group | ||||
|     :members: | ||||
|     :special-members: | ||||
|  | ||||
|  | ||||
| sanic.compat | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.compat | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.config | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.config | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.cookies | ||||
| ------------- | ||||
|  | ||||
| .. automodule:: sanic.cookies | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.errorpages | ||||
| ---------------- | ||||
|  | ||||
| .. automodule:: sanic.errorpages | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.exceptions | ||||
| ---------------- | ||||
|  | ||||
| .. automodule:: sanic.exceptions | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.handlers | ||||
| -------------- | ||||
|  | ||||
| .. automodule:: sanic.handlers | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.http | ||||
| ---------- | ||||
|  | ||||
| .. automodule:: sanic.http | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.log | ||||
| --------- | ||||
|  | ||||
| .. automodule:: sanic.log | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.request | ||||
| ------------- | ||||
|  | ||||
| .. automodule:: sanic.request | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.response | ||||
| -------------- | ||||
|  | ||||
| .. automodule:: sanic.response | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.router | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.router | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.server | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.server | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
|  | ||||
| sanic.views | ||||
| ----------- | ||||
|  | ||||
| .. automodule:: sanic.views | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.websocket | ||||
| --------------- | ||||
|  | ||||
| .. automodule:: sanic.websocket | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| sanic.worker | ||||
| ------------ | ||||
|  | ||||
| .. automodule:: sanic.worker | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
|    api/app | ||||
|    api/blueprints | ||||
|    api/core | ||||
|    api/exceptions | ||||
|    api/router | ||||
|    api/server | ||||
|    api/utility | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| ♥️ Contributing | ||||
| =============== | ||||
| ============== | ||||
|  | ||||
| .. include:: ../../CONTRIBUTING.rst | ||||
|   | ||||
							
								
								
									
										6
									
								
								hack/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								hack/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| FROM catthehacker/ubuntu:act-latest | ||||
| SHELL [ "/bin/bash", "-c" ] | ||||
| ENTRYPOINT [] | ||||
| RUN apt-get update | ||||
| RUN apt-get install gcc -y | ||||
| RUN apt-get install -y --no-install-recommends g++ | ||||
| @@ -1,6 +1,7 @@ | ||||
| from sanic.__version__ import __version__ | ||||
| from sanic.app import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.constants import HTTPMethod | ||||
| from sanic.request import Request | ||||
| from sanic.response import HTTPResponse, html, json, text | ||||
|  | ||||
| @@ -9,6 +10,7 @@ __all__ = ( | ||||
|     "__version__", | ||||
|     "Sanic", | ||||
|     "Blueprint", | ||||
|     "HTTPMethod", | ||||
|     "HTTPResponse", | ||||
|     "Request", | ||||
|     "html", | ||||
|   | ||||
| @@ -1,21 +1,25 @@ | ||||
| import os | ||||
| import sys | ||||
|  | ||||
| from argparse import ArgumentParser, RawDescriptionHelpFormatter | ||||
| from argparse import ArgumentParser, RawTextHelpFormatter | ||||
| from importlib import import_module | ||||
| from pathlib import Path | ||||
| from typing import Any, Dict, Optional | ||||
|  | ||||
| from sanic_routing import __version__ as __routing_version__  # type: ignore | ||||
|  | ||||
| from sanic import __version__ | ||||
| from sanic.app import Sanic | ||||
| from sanic.config import BASE_LOGO | ||||
| from sanic.log import logger | ||||
| from sanic.log import error_logger | ||||
| from sanic.simple import create_simple_server | ||||
|  | ||||
|  | ||||
| class SanicArgumentParser(ArgumentParser): | ||||
|     def add_bool_arguments(self, *args, **kwargs): | ||||
|         group = self.add_mutually_exclusive_group() | ||||
|         group.add_argument(*args, action="store_true", **kwargs) | ||||
|         kwargs["help"] = "no " + kwargs["help"] | ||||
|         kwargs["help"] = f"no {kwargs['help']}\n " | ||||
|         group.add_argument( | ||||
|             "--no-" + args[0][2:], *args[1:], action="store_false", **kwargs | ||||
|         ) | ||||
| @@ -25,7 +29,30 @@ def main(): | ||||
|     parser = SanicArgumentParser( | ||||
|         prog="sanic", | ||||
|         description=BASE_LOGO, | ||||
|         formatter_class=RawDescriptionHelpFormatter, | ||||
|         formatter_class=lambda prog: RawTextHelpFormatter( | ||||
|             prog, max_help_position=33 | ||||
|         ), | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-v", | ||||
|         "--version", | ||||
|         action="version", | ||||
|         version=f"Sanic {__version__}; Routing {__routing_version__}", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--factory", | ||||
|         action="store_true", | ||||
|         help=( | ||||
|             "Treat app as an application factory, " | ||||
|             "i.e. a () -> <Sanic app> callable" | ||||
|         ), | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-s", | ||||
|         "--simple", | ||||
|         dest="simple", | ||||
|         action="store_true", | ||||
|         help="Run Sanic as a Simple Server (module arg should be a path)\n ", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-H", | ||||
| @@ -33,7 +60,7 @@ def main(): | ||||
|         dest="host", | ||||
|         type=str, | ||||
|         default="127.0.0.1", | ||||
|         help="host address [default 127.0.0.1]", | ||||
|         help="Host address [default 127.0.0.1]", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-p", | ||||
| @@ -41,7 +68,7 @@ def main(): | ||||
|         dest="port", | ||||
|         type=int, | ||||
|         default=8000, | ||||
|         help="port to serve on [default 8000]", | ||||
|         help="Port to serve on [default 8000]", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-u", | ||||
| @@ -49,13 +76,16 @@ def main(): | ||||
|         dest="unix", | ||||
|         type=str, | ||||
|         default="", | ||||
|         help="location of unix socket", | ||||
|         help="location of unix socket\n ", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--cert", dest="cert", type=str, help="location of certificate for SSL" | ||||
|         "--cert", dest="cert", type=str, help="Location of certificate for SSL" | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--key", dest="key", type=str, help="location of keyfile for SSL." | ||||
|         "--key", dest="key", type=str, help="location of keyfile for SSL\n " | ||||
|     ) | ||||
|     parser.add_bool_arguments( | ||||
|         "--access-logs", dest="access_log", help="display access logs" | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-w", | ||||
| @@ -63,20 +93,31 @@ def main(): | ||||
|         dest="workers", | ||||
|         type=int, | ||||
|         default=1, | ||||
|         help="number of worker processes [default 1]", | ||||
|         help="number of worker processes [default 1]\n ", | ||||
|     ) | ||||
|     parser.add_argument("--debug", dest="debug", action="store_true") | ||||
|     parser.add_bool_arguments( | ||||
|         "--access-logs", dest="access_log", help="display access logs" | ||||
|     parser.add_argument("-d", "--debug", dest="debug", action="store_true") | ||||
|     parser.add_argument( | ||||
|         "-r", | ||||
|         "--reload", | ||||
|         "--auto-reload", | ||||
|         dest="auto_reload", | ||||
|         action="store_true", | ||||
|         help="Watch source directory for file changes and reload on changes", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-v", | ||||
|         "--version", | ||||
|         action="version", | ||||
|         version=f"Sanic {__version__}", | ||||
|         "-R", | ||||
|         "--reload-dir", | ||||
|         dest="path", | ||||
|         action="append", | ||||
|         help="Extra directories to watch and reload on changes\n ", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "module", help="path to your Sanic app. Example: path.to.server:app" | ||||
|         "module", | ||||
|         help=( | ||||
|             "Path to your Sanic app. Example: path.to.server:app\n" | ||||
|             "If running a Simple Server, path to directory to serve. " | ||||
|             "Example: ./\n" | ||||
|         ), | ||||
|     ) | ||||
|     args = parser.parse_args() | ||||
|  | ||||
| @@ -85,47 +126,71 @@ def main(): | ||||
|         if module_path not in sys.path: | ||||
|             sys.path.append(module_path) | ||||
|  | ||||
|         if ":" in args.module: | ||||
|             module_name, app_name = args.module.rsplit(":", 1) | ||||
|         if args.simple: | ||||
|             path = Path(args.module) | ||||
|             app = create_simple_server(path) | ||||
|         else: | ||||
|             module_parts = args.module.split(".") | ||||
|             module_name = ".".join(module_parts[:-1]) | ||||
|             app_name = module_parts[-1] | ||||
|             delimiter = ":" if ":" in args.module else "." | ||||
|             module_name, app_name = args.module.rsplit(delimiter, 1) | ||||
|  | ||||
|         module = import_module(module_name) | ||||
|         app = getattr(module, app_name, None) | ||||
|         app_name = type(app).__name__ | ||||
|             if app_name.endswith("()"): | ||||
|                 args.factory = True | ||||
|                 app_name = app_name[:-2] | ||||
|  | ||||
|         if not isinstance(app, Sanic): | ||||
|             raise ValueError( | ||||
|                 f"Module is not a Sanic app, it is a {app_name}.  " | ||||
|                 f"Perhaps you meant {args.module}.app?" | ||||
|             ) | ||||
|             module = import_module(module_name) | ||||
|             app = getattr(module, app_name, None) | ||||
|             if args.factory: | ||||
|                 app = app() | ||||
|  | ||||
|             app_type_name = type(app).__name__ | ||||
|  | ||||
|             if not isinstance(app, Sanic): | ||||
|                 raise ValueError( | ||||
|                     f"Module is not a Sanic app, it is a {app_type_name}.  " | ||||
|                     f"Perhaps you meant {args.module}.app?" | ||||
|                 ) | ||||
|         if args.cert is not None or args.key is not None: | ||||
|             ssl = { | ||||
|             ssl: Optional[Dict[str, Any]] = { | ||||
|                 "cert": args.cert, | ||||
|                 "key": args.key, | ||||
|             }  # type: Optional[Dict[str, Any]] | ||||
|             } | ||||
|         else: | ||||
|             ssl = None | ||||
|  | ||||
|         app.run( | ||||
|             host=args.host, | ||||
|             port=args.port, | ||||
|             unix=args.unix, | ||||
|             workers=args.workers, | ||||
|             debug=args.debug, | ||||
|             access_log=args.access_log, | ||||
|             ssl=ssl, | ||||
|         ) | ||||
|         kwargs = { | ||||
|             "host": args.host, | ||||
|             "port": args.port, | ||||
|             "unix": args.unix, | ||||
|             "workers": args.workers, | ||||
|             "debug": args.debug, | ||||
|             "access_log": args.access_log, | ||||
|             "ssl": ssl, | ||||
|         } | ||||
|         if args.auto_reload: | ||||
|             kwargs["auto_reload"] = True | ||||
|  | ||||
|         if args.path: | ||||
|             if args.auto_reload or args.debug: | ||||
|                 kwargs["reload_dir"] = args.path | ||||
|             else: | ||||
|                 error_logger.warning( | ||||
|                     "Ignoring '--reload-dir' since auto reloading was not " | ||||
|                     "enabled. If you would like to watch directories for " | ||||
|                     "changes, consider using --debug or --auto-reload." | ||||
|                 ) | ||||
|  | ||||
|         app.run(**kwargs) | ||||
|     except ImportError as e: | ||||
|         logger.error( | ||||
|             f"No module named {e.name} found.\n" | ||||
|             f"  Example File: project/sanic_server.py -> app\n" | ||||
|             f"  Example Module: project.sanic_server.app" | ||||
|         ) | ||||
|         if module_name.startswith(e.name): | ||||
|             error_logger.error( | ||||
|                 f"No module named {e.name} found.\n" | ||||
|                 "  Example File: project/sanic_server.py -> app\n" | ||||
|                 "  Example Module: project.sanic_server.app" | ||||
|             ) | ||||
|         else: | ||||
|             raise e | ||||
|     except ValueError: | ||||
|         logger.exception("Failed to run app") | ||||
|         error_logger.exception("Failed to run app") | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "21.3.1" | ||||
| __version__ = "21.6.1" | ||||
|   | ||||
							
								
								
									
										126
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										126
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -14,6 +14,7 @@ from asyncio.futures import Future | ||||
| from collections import defaultdict, deque | ||||
| from functools import partial | ||||
| from inspect import isawaitable | ||||
| from pathlib import Path | ||||
| from socket import socket | ||||
| from ssl import Purpose, SSLContext, create_default_context | ||||
| from traceback import format_exc | ||||
| @@ -43,7 +44,7 @@ from sanic.asgi import ASGIApp | ||||
| from sanic.base import BaseSanic | ||||
| from sanic.blueprint_group import BlueprintGroup | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.config import BASE_LOGO, Config | ||||
| from sanic.config import BASE_LOGO, SANIC_PREFIX, Config | ||||
| from sanic.exceptions import ( | ||||
|     InvalidUsage, | ||||
|     SanicException, | ||||
| @@ -78,6 +79,7 @@ class Sanic(BaseSanic): | ||||
|     """ | ||||
|  | ||||
|     __fake_slots__ = ( | ||||
|         "_asgi_app", | ||||
|         "_app_registry", | ||||
|         "_asgi_client", | ||||
|         "_blueprint_order", | ||||
| @@ -89,6 +91,7 @@ class Sanic(BaseSanic): | ||||
|         "_future_signals", | ||||
|         "_test_client", | ||||
|         "_test_manager", | ||||
|         "auto_reload", | ||||
|         "asgi", | ||||
|         "blueprints", | ||||
|         "config", | ||||
| @@ -103,6 +106,7 @@ class Sanic(BaseSanic): | ||||
|         "name", | ||||
|         "named_request_middleware", | ||||
|         "named_response_middleware", | ||||
|         "reload_dirs", | ||||
|         "request_class", | ||||
|         "request_middleware", | ||||
|         "response_middleware", | ||||
| @@ -121,10 +125,13 @@ class Sanic(BaseSanic): | ||||
|     def __init__( | ||||
|         self, | ||||
|         name: str = None, | ||||
|         config: Optional[Config] = None, | ||||
|         ctx: Optional[Any] = None, | ||||
|         router: Optional[Router] = None, | ||||
|         signal_router: Optional[SignalRouter] = None, | ||||
|         error_handler: Optional[ErrorHandler] = None, | ||||
|         load_env: bool = True, | ||||
|         load_env: Union[bool, str] = True, | ||||
|         env_prefix: Optional[str] = SANIC_PREFIX, | ||||
|         request_class: Optional[Type[Request]] = None, | ||||
|         strict_slashes: bool = False, | ||||
|         log_config: Optional[Dict[str, Any]] = None, | ||||
| @@ -132,34 +139,38 @@ class Sanic(BaseSanic): | ||||
|         register: Optional[bool] = None, | ||||
|         dumps: Optional[Callable[..., str]] = None, | ||||
|     ) -> None: | ||||
|         super().__init__() | ||||
|         super().__init__(name=name) | ||||
|  | ||||
|         if name is None: | ||||
|             raise SanicException( | ||||
|                 "Sanic instance cannot be unnamed. " | ||||
|                 "Please use Sanic(name='your_application_name') instead.", | ||||
|             ) | ||||
|         # logging | ||||
|         if configure_logging: | ||||
|             logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) | ||||
|  | ||||
|         if config and (load_env is not True or env_prefix != SANIC_PREFIX): | ||||
|             raise SanicException( | ||||
|                 "When instantiating Sanic with config, you cannot also pass " | ||||
|                 "load_env or env_prefix" | ||||
|             ) | ||||
|  | ||||
|         self._asgi_client = None | ||||
|         self._blueprint_order: List[Blueprint] = [] | ||||
|         self._test_client = None | ||||
|         self._test_manager = None | ||||
|         self.asgi = False | ||||
|         self.auto_reload = False | ||||
|         self.blueprints: Dict[str, Blueprint] = {} | ||||
|         self.config = Config(load_env=load_env) | ||||
|         self.config = config or Config( | ||||
|             load_env=load_env, env_prefix=env_prefix | ||||
|         ) | ||||
|         self.configure_logging = configure_logging | ||||
|         self.ctx = SimpleNamespace() | ||||
|         self.ctx = ctx or SimpleNamespace() | ||||
|         self.debug = None | ||||
|         self.error_handler = error_handler or ErrorHandler() | ||||
|         self.is_running = False | ||||
|         self.is_stopping = False | ||||
|         self.listeners: Dict[str, List[ListenerType]] = defaultdict(list) | ||||
|         self.name = name | ||||
|         self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} | ||||
|         self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {} | ||||
|         self.reload_dirs: Set[Path] = set() | ||||
|         self.request_class = request_class | ||||
|         self.request_middleware: Deque[MiddlewareType] = deque() | ||||
|         self.response_middleware: Deque[MiddlewareType] = deque() | ||||
| @@ -175,7 +186,6 @@ class Sanic(BaseSanic): | ||||
|  | ||||
|         if register is not None: | ||||
|             self.config.REGISTER = register | ||||
|  | ||||
|         if self.config.REGISTER: | ||||
|             self.__class__.register_app(self) | ||||
|  | ||||
| @@ -374,11 +384,19 @@ class Sanic(BaseSanic): | ||||
|             condition=condition, | ||||
|         ) | ||||
|  | ||||
|     def event(self, event: str, timeout: Optional[Union[int, float]] = None): | ||||
|     async def event( | ||||
|         self, event: str, timeout: Optional[Union[int, float]] = None | ||||
|     ): | ||||
|         signal = self.signal_router.name_index.get(event) | ||||
|         if not signal: | ||||
|             raise NotFound("Could not find signal %s" % event) | ||||
|         return wait_for(signal.ctx.event.wait(), timeout=timeout) | ||||
|             if self.config.EVENT_AUTOREGISTER: | ||||
|                 self.signal_router.reset() | ||||
|                 self.add_signal(None, event) | ||||
|                 signal = self.signal_router.name_index[event] | ||||
|                 self.signal_router.finalize() | ||||
|             else: | ||||
|                 raise NotFound("Could not find signal %s" % event) | ||||
|         return await wait_for(signal.ctx.event.wait(), timeout=timeout) | ||||
|  | ||||
|     def enable_websocket(self, enable=True): | ||||
|         """Enable or disable the support for websocket. | ||||
| @@ -402,7 +420,33 @@ class Sanic(BaseSanic): | ||||
|         """ | ||||
|         if isinstance(blueprint, (list, tuple, BlueprintGroup)): | ||||
|             for item in blueprint: | ||||
|                 self.blueprint(item, **options) | ||||
|                 params = {**options} | ||||
|                 if isinstance(blueprint, BlueprintGroup): | ||||
|                     if blueprint.url_prefix: | ||||
|                         merge_from = [ | ||||
|                             options.get("url_prefix", ""), | ||||
|                             blueprint.url_prefix, | ||||
|                         ] | ||||
|                         if not isinstance(item, BlueprintGroup): | ||||
|                             merge_from.append(item.url_prefix or "") | ||||
|                         merged_prefix = "/".join( | ||||
|                             u.strip("/") for u in merge_from | ||||
|                         ).rstrip("/") | ||||
|                         params["url_prefix"] = f"/{merged_prefix}" | ||||
|  | ||||
|                     for _attr in ["version", "strict_slashes"]: | ||||
|                         if getattr(item, _attr) is None: | ||||
|                             params[_attr] = getattr( | ||||
|                                 blueprint, _attr | ||||
|                             ) or options.get(_attr) | ||||
|                     if item.version_prefix == "/v": | ||||
|                         if blueprint.version_prefix == "/v": | ||||
|                             params["version_prefix"] = options.get( | ||||
|                                 "version_prefix" | ||||
|                             ) | ||||
|                         else: | ||||
|                             params["version_prefix"] = blueprint.version_prefix | ||||
|                 self.blueprint(item, **params) | ||||
|             return | ||||
|         if blueprint.name in self.blueprints: | ||||
|             assert self.blueprints[blueprint.name] is blueprint, ( | ||||
| @@ -567,7 +611,12 @@ class Sanic(BaseSanic): | ||||
|             # determine if the parameter supplied by the caller | ||||
|             # passes the test in the URL | ||||
|             if param_info.pattern: | ||||
|                 passes_pattern = param_info.pattern.match(supplied_param) | ||||
|                 pattern = ( | ||||
|                     param_info.pattern[1] | ||||
|                     if isinstance(param_info.pattern, tuple) | ||||
|                     else param_info.pattern | ||||
|                 ) | ||||
|                 passes_pattern = pattern.match(supplied_param) | ||||
|                 if not passes_pattern: | ||||
|                     if param_info.cast != str: | ||||
|                         msg = ( | ||||
| @@ -575,13 +624,13 @@ class Sanic(BaseSanic): | ||||
|                             f"for parameter `{param_info.name}` does " | ||||
|                             "not match pattern for type " | ||||
|                             f"`{param_info.cast.__name__}`: " | ||||
|                             f"{param_info.pattern.pattern}" | ||||
|                             f"{pattern.pattern}" | ||||
|                         ) | ||||
|                     else: | ||||
|                         msg = ( | ||||
|                             f'Value "{supplied_param}" for parameter ' | ||||
|                             f"`{param_info.name}` does not satisfy " | ||||
|                             f"pattern {param_info.pattern.pattern}" | ||||
|                             f"pattern {pattern.pattern}" | ||||
|                         ) | ||||
|                     raise URLBuildError(msg) | ||||
|  | ||||
| @@ -664,11 +713,6 @@ class Sanic(BaseSanic): | ||||
|         exception handling must be done here | ||||
|  | ||||
|         :param request: HTTP Request object | ||||
|         :param write_callback: Synchronous response function to be | ||||
|             called with the response as the only argument | ||||
|         :param stream_callback: Coroutine that handles streaming a | ||||
|             StreamingHTTPResponse if produced by the handler. | ||||
|  | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         # Define `response` var here to remove warnings about | ||||
| @@ -677,7 +721,9 @@ class Sanic(BaseSanic): | ||||
|         try: | ||||
|             # Fetch handler from router | ||||
|             route, handler, kwargs = self.router.get( | ||||
|                 request.path, request.method, request.headers.get("host") | ||||
|                 request.path, | ||||
|                 request.method, | ||||
|                 request.headers.getone("host", None), | ||||
|             ) | ||||
|  | ||||
|             request._match_info = kwargs | ||||
| @@ -725,17 +771,14 @@ class Sanic(BaseSanic): | ||||
|  | ||||
|             if response: | ||||
|                 response = await request.respond(response) | ||||
|             else: | ||||
|             elif not hasattr(handler, "is_websocket"): | ||||
|                 response = request.stream.response  # type: ignore | ||||
|             # Make sure that response is finished / run StreamingHTTP callback | ||||
|  | ||||
|             # Make sure that response is finished / run StreamingHTTP callback | ||||
|             if isinstance(response, BaseHTTPResponse): | ||||
|                 await response.send(end_stream=True) | ||||
|             else: | ||||
|                 try: | ||||
|                     # Fastest method for checking if the property exists | ||||
|                     handler.is_websocket  # type: ignore | ||||
|                 except AttributeError: | ||||
|                 if not hasattr(handler, "is_websocket"): | ||||
|                     raise ServerError( | ||||
|                         f"Invalid response type {response!r} " | ||||
|                         "(need HTTPResponse)" | ||||
| @@ -762,6 +805,7 @@ class Sanic(BaseSanic): | ||||
|  | ||||
|         if self.asgi: | ||||
|             ws = request.transport.get_websocket_connection() | ||||
|             await ws.accept(subprotocols) | ||||
|         else: | ||||
|             protocol = request.transport.get_protocol() | ||||
|             protocol.app = self | ||||
| @@ -834,6 +878,7 @@ class Sanic(BaseSanic): | ||||
|         access_log: Optional[bool] = None, | ||||
|         unix: Optional[str] = None, | ||||
|         loop: None = None, | ||||
|         reload_dir: Optional[Union[List[str], str]] = None, | ||||
|     ) -> None: | ||||
|         """ | ||||
|         Run the HTTP Server and listen until keyboard interrupt or term | ||||
| @@ -868,6 +913,18 @@ class Sanic(BaseSanic): | ||||
|         :type unix: str | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         if reload_dir: | ||||
|             if isinstance(reload_dir, str): | ||||
|                 reload_dir = [reload_dir] | ||||
|  | ||||
|             for directory in reload_dir: | ||||
|                 direc = Path(directory) | ||||
|                 if not direc.is_dir(): | ||||
|                     logger.warning( | ||||
|                         f"Directory {directory} could not be located" | ||||
|                     ) | ||||
|                 self.reload_dirs.add(Path(directory)) | ||||
|  | ||||
|         if loop is not None: | ||||
|             raise TypeError( | ||||
|                 "loop is not a valid argument. To use an existing loop, " | ||||
| @@ -877,8 +934,9 @@ class Sanic(BaseSanic): | ||||
|             ) | ||||
|  | ||||
|         if auto_reload or auto_reload is None and debug: | ||||
|             self.auto_reload = True | ||||
|             if os.environ.get("SANIC_SERVER_RUNNING") != "true": | ||||
|                 return reloader_helpers.watchdog(1.0) | ||||
|                 return reloader_helpers.watchdog(1.0, self) | ||||
|  | ||||
|         if sock is None: | ||||
|             host, port = host or "127.0.0.1", port or 8000 | ||||
| @@ -1177,6 +1235,10 @@ class Sanic(BaseSanic): | ||||
|             else: | ||||
|                 logger.info(f"Goin' Fast @ {proto}://{host}:{port}") | ||||
|  | ||||
|         debug_mode = "enabled" if self.debug else "disabled" | ||||
|         logger.debug("Sanic auto-reload: enabled") | ||||
|         logger.debug(f"Sanic debug mode: {debug_mode}") | ||||
|  | ||||
|         return server_settings | ||||
|  | ||||
|     def _build_endpoint_name(self, *parts): | ||||
|   | ||||
| @@ -140,7 +140,6 @@ class ASGIApp: | ||||
|                 instance.ws = instance.transport.create_websocket_connection( | ||||
|                     send, receive | ||||
|                 ) | ||||
|                 await instance.ws.accept() | ||||
|             else: | ||||
|                 raise ServerError("Received unknown ASGI scope") | ||||
|  | ||||
| @@ -164,10 +163,12 @@ class ASGIApp: | ||||
|         Read and stream the body in chunks from an incoming ASGI message. | ||||
|         """ | ||||
|         message = await self.transport.receive() | ||||
|         body = message.get("body", b"") | ||||
|         if not message.get("more_body", False): | ||||
|             self.request_body = False | ||||
|             return None | ||||
|         return message.get("body", b"") | ||||
|             if not body: | ||||
|                 return None | ||||
|         return body | ||||
|  | ||||
|     async def __aiter__(self): | ||||
|         while self.request_body: | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| import re | ||||
|  | ||||
| from typing import Any, Tuple | ||||
| from warnings import warn | ||||
|  | ||||
| from sanic.exceptions import SanicException | ||||
| from sanic.mixins.exceptions import ExceptionMixin | ||||
| from sanic.mixins.listeners import ListenerMixin | ||||
| from sanic.mixins.middleware import MiddlewareMixin | ||||
| @@ -8,6 +11,9 @@ from sanic.mixins.routes import RouteMixin | ||||
| from sanic.mixins.signals import SignalMixin | ||||
|  | ||||
|  | ||||
| VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$") | ||||
|  | ||||
|  | ||||
| class BaseSanic( | ||||
|     RouteMixin, | ||||
|     MiddlewareMixin, | ||||
| @@ -17,7 +23,25 @@ class BaseSanic( | ||||
| ): | ||||
|     __fake_slots__: Tuple[str, ...] | ||||
|  | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|     def __init__(self, name: str = None, *args, **kwargs) -> None: | ||||
|         class_name = self.__class__.__name__ | ||||
|  | ||||
|         if name is None: | ||||
|             raise SanicException( | ||||
|                 f"{class_name} instance cannot be unnamed. " | ||||
|                 "Please use Sanic(name='your_application_name') instead.", | ||||
|             ) | ||||
|  | ||||
|         if not VALID_NAME.match(name): | ||||
|             warn( | ||||
|                 f"{class_name} instance named '{name}' uses a format that is" | ||||
|                 f"deprecated. Starting in version 21.12, {class_name} objects " | ||||
|                 "must be named only using alphanumeric characters, _, or -.", | ||||
|                 DeprecationWarning, | ||||
|             ) | ||||
|  | ||||
|         self.name = name | ||||
|  | ||||
|         for base in BaseSanic.__bases__: | ||||
|             base.__init__(self, *args, **kwargs)  # type: ignore | ||||
|  | ||||
| @@ -36,6 +60,7 @@ class BaseSanic( | ||||
|                 f"Setting variables on {self.__class__.__name__} instances is " | ||||
|                 "deprecated and will be removed in version 21.9. You should " | ||||
|                 f"change your {self.__class__.__name__} instance to use " | ||||
|                 f"instance.ctx.{name} instead." | ||||
|                 f"instance.ctx.{name} instead.", | ||||
|                 DeprecationWarning, | ||||
|             ) | ||||
|         super().__setattr__(name, value) | ||||
|   | ||||
| @@ -1,7 +1,11 @@ | ||||
| from collections.abc import MutableSequence | ||||
| from typing import List, Optional, Union | ||||
| from __future__ import annotations | ||||
|  | ||||
| import sanic | ||||
| from collections.abc import MutableSequence | ||||
| from typing import TYPE_CHECKING, List, Optional, Union | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from sanic.blueprints import Blueprint | ||||
|  | ||||
|  | ||||
| class BlueprintGroup(MutableSequence): | ||||
| @@ -54,9 +58,21 @@ class BlueprintGroup(MutableSequence): | ||||
|         app.blueprint(bpg) | ||||
|     """ | ||||
|  | ||||
|     __slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes") | ||||
|     __slots__ = ( | ||||
|         "_blueprints", | ||||
|         "_url_prefix", | ||||
|         "_version", | ||||
|         "_strict_slashes", | ||||
|         "_version_prefix", | ||||
|     ) | ||||
|  | ||||
|     def __init__(self, url_prefix=None, version=None, strict_slashes=None): | ||||
|     def __init__( | ||||
|         self, | ||||
|         url_prefix: Optional[str] = None, | ||||
|         version: Optional[Union[int, str, float]] = None, | ||||
|         strict_slashes: Optional[bool] = None, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Create a new Blueprint Group | ||||
|  | ||||
| @@ -65,13 +81,14 @@ class BlueprintGroup(MutableSequence): | ||||
|             inherited by each of the Blueprint | ||||
|         :param strict_slashes: URL Strict slash behavior indicator | ||||
|         """ | ||||
|         self._blueprints = [] | ||||
|         self._blueprints: List[Blueprint] = [] | ||||
|         self._url_prefix = url_prefix | ||||
|         self._version = version | ||||
|         self._version_prefix = version_prefix | ||||
|         self._strict_slashes = strict_slashes | ||||
|  | ||||
|     @property | ||||
|     def url_prefix(self) -> str: | ||||
|     def url_prefix(self) -> Optional[Union[int, str, float]]: | ||||
|         """ | ||||
|         Retrieve the URL prefix being used for the Current Blueprint Group | ||||
|  | ||||
| @@ -80,7 +97,7 @@ class BlueprintGroup(MutableSequence): | ||||
|         return self._url_prefix | ||||
|  | ||||
|     @property | ||||
|     def blueprints(self) -> List["sanic.Blueprint"]: | ||||
|     def blueprints(self) -> List[Blueprint]: | ||||
|         """ | ||||
|         Retrieve a list of all the available blueprints under this group. | ||||
|  | ||||
| @@ -107,6 +124,15 @@ class BlueprintGroup(MutableSequence): | ||||
|         """ | ||||
|         return self._strict_slashes | ||||
|  | ||||
|     @property | ||||
|     def version_prefix(self) -> str: | ||||
|         """ | ||||
|         Version prefix; defaults to ``/v`` | ||||
|  | ||||
|         :return: str | ||||
|         """ | ||||
|         return self._version_prefix | ||||
|  | ||||
|     def __iter__(self): | ||||
|         """ | ||||
|         Tun the class Blueprint Group into an Iterable item | ||||
| @@ -161,34 +187,16 @@ class BlueprintGroup(MutableSequence): | ||||
|         """ | ||||
|         return len(self._blueprints) | ||||
|  | ||||
|     def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint": | ||||
|         """ | ||||
|         Sanitize the Blueprint Entity to override the Version and strict slash | ||||
|         behaviors as required. | ||||
|  | ||||
|         :param bp: Sanic Blueprint entity Object | ||||
|         :return: Modified Blueprint | ||||
|         """ | ||||
|         if self._url_prefix: | ||||
|             merged_prefix = "/".join( | ||||
|                 u.strip("/") for u in [self._url_prefix, bp.url_prefix or ""] | ||||
|             ).rstrip("/") | ||||
|             bp.url_prefix = f"/{merged_prefix}" | ||||
|         for _attr in ["version", "strict_slashes"]: | ||||
|             if getattr(bp, _attr) is None: | ||||
|                 setattr(bp, _attr, getattr(self, _attr)) | ||||
|         return bp | ||||
|  | ||||
|     def append(self, value: "sanic.Blueprint") -> None: | ||||
|     def append(self, value: Blueprint) -> None: | ||||
|         """ | ||||
|         The Abstract class `MutableSequence` leverages this append method to | ||||
|         perform the `BlueprintGroup.append` operation. | ||||
|         :param value: New `Blueprint` object. | ||||
|         :return: None | ||||
|         """ | ||||
|         self._blueprints.append(self._sanitize_blueprint(bp=value)) | ||||
|         self._blueprints.append(value) | ||||
|  | ||||
|     def insert(self, index: int, item: "sanic.Blueprint") -> None: | ||||
|     def insert(self, index: int, item: Blueprint) -> None: | ||||
|         """ | ||||
|         The Abstract class `MutableSequence` leverages this insert method to | ||||
|         perform the `BlueprintGroup.append` operation. | ||||
| @@ -197,7 +205,7 @@ class BlueprintGroup(MutableSequence): | ||||
|         :param item: New `Blueprint` object. | ||||
|         :return: None | ||||
|         """ | ||||
|         self._blueprints.insert(index, self._sanitize_blueprint(item)) | ||||
|         self._blueprints.insert(index, item) | ||||
|  | ||||
|     def middleware(self, *args, **kwargs): | ||||
|         """ | ||||
|   | ||||
| @@ -62,18 +62,20 @@ class Blueprint(BaseSanic): | ||||
|         "strict_slashes", | ||||
|         "url_prefix", | ||||
|         "version", | ||||
|         "version_prefix", | ||||
|         "websocket_routes", | ||||
|     ) | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         name: str, | ||||
|         name: str = None, | ||||
|         url_prefix: Optional[str] = None, | ||||
|         host: Optional[str] = None, | ||||
|         version: Optional[int] = None, | ||||
|         version: Optional[Union[int, str, float]] = None, | ||||
|         strict_slashes: Optional[bool] = None, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         super().__init__() | ||||
|         super().__init__(name=name) | ||||
|  | ||||
|         self._apps: Set[Sanic] = set() | ||||
|         self.ctx = SimpleNamespace() | ||||
| @@ -81,12 +83,16 @@ class Blueprint(BaseSanic): | ||||
|         self.host = host | ||||
|         self.listeners: Dict[str, List[ListenerType]] = {} | ||||
|         self.middlewares: List[MiddlewareType] = [] | ||||
|         self.name = name | ||||
|         self.routes: List[Route] = [] | ||||
|         self.statics: List[RouteHandler] = [] | ||||
|         self.strict_slashes = strict_slashes | ||||
|         self.url_prefix = url_prefix | ||||
|         self.url_prefix = ( | ||||
|             url_prefix[:-1] | ||||
|             if url_prefix and url_prefix.endswith("/") | ||||
|             else url_prefix | ||||
|         ) | ||||
|         self.version = version | ||||
|         self.version_prefix = version_prefix | ||||
|         self.websocket_routes: List[Route] = [] | ||||
|  | ||||
|     def __repr__(self) -> str: | ||||
| @@ -139,7 +145,13 @@ class Blueprint(BaseSanic): | ||||
|         return super().signal(event, *args, **kwargs) | ||||
|  | ||||
|     @staticmethod | ||||
|     def group(*blueprints, url_prefix="", version=None, strict_slashes=None): | ||||
|     def group( | ||||
|         *blueprints, | ||||
|         url_prefix="", | ||||
|         version=None, | ||||
|         strict_slashes=None, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Create a list of blueprints, optionally grouping them under a | ||||
|         general URL prefix. | ||||
| @@ -156,8 +168,6 @@ class Blueprint(BaseSanic): | ||||
|             for i in nested: | ||||
|                 if isinstance(i, (list, tuple)): | ||||
|                     yield from chain(i) | ||||
|                 elif isinstance(i, BlueprintGroup): | ||||
|                     yield from i.blueprints | ||||
|                 else: | ||||
|                     yield i | ||||
|  | ||||
| @@ -165,6 +175,7 @@ class Blueprint(BaseSanic): | ||||
|             url_prefix=url_prefix, | ||||
|             version=version, | ||||
|             strict_slashes=strict_slashes, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|         for bp in chain(blueprints): | ||||
|             bps.append(bp) | ||||
| @@ -182,6 +193,9 @@ class Blueprint(BaseSanic): | ||||
|  | ||||
|         self._apps.add(app) | ||||
|         url_prefix = options.get("url_prefix", self.url_prefix) | ||||
|         opt_version = options.get("version", None) | ||||
|         opt_strict_slashes = options.get("strict_slashes", None) | ||||
|         opt_version_prefix = options.get("version_prefix", self.version_prefix) | ||||
|  | ||||
|         routes = [] | ||||
|         middleware = [] | ||||
| @@ -196,12 +210,22 @@ class Blueprint(BaseSanic): | ||||
|             # Prepend the blueprint URI prefix if available | ||||
|             uri = url_prefix + future.uri if url_prefix else future.uri | ||||
|  | ||||
|             strict_slashes = ( | ||||
|                 self.strict_slashes | ||||
|                 if future.strict_slashes is None | ||||
|                 and self.strict_slashes is not None | ||||
|                 else future.strict_slashes | ||||
|             version_prefix = self.version_prefix | ||||
|             for prefix in ( | ||||
|                 future.version_prefix, | ||||
|                 opt_version_prefix, | ||||
|             ): | ||||
|                 if prefix and prefix != "/v": | ||||
|                     version_prefix = prefix | ||||
|                     break | ||||
|  | ||||
|             version = self._extract_value( | ||||
|                 future.version, opt_version, self.version | ||||
|             ) | ||||
|             strict_slashes = self._extract_value( | ||||
|                 future.strict_slashes, opt_strict_slashes, self.strict_slashes | ||||
|             ) | ||||
|  | ||||
|             name = app._generate_name(future.name) | ||||
|  | ||||
|             apply_route = FutureRoute( | ||||
| @@ -211,13 +235,14 @@ class Blueprint(BaseSanic): | ||||
|                 future.host or self.host, | ||||
|                 strict_slashes, | ||||
|                 future.stream, | ||||
|                 future.version or self.version, | ||||
|                 version, | ||||
|                 name, | ||||
|                 future.ignore_body, | ||||
|                 future.websocket, | ||||
|                 future.subprotocols, | ||||
|                 future.unquote, | ||||
|                 future.static, | ||||
|                 version_prefix, | ||||
|             ) | ||||
|  | ||||
|             route = app._apply_route(apply_route) | ||||
| @@ -254,8 +279,6 @@ class Blueprint(BaseSanic): | ||||
|             app._apply_signal(signal) | ||||
|  | ||||
|         self.routes = [route for route in routes if isinstance(route, Route)] | ||||
|  | ||||
|         # Deprecate these in 21.6 | ||||
|         self.websocket_routes = [ | ||||
|             route for route in self.routes if route.ctx.websocket | ||||
|         ] | ||||
| @@ -284,3 +307,12 @@ class Blueprint(BaseSanic): | ||||
|             return_when=asyncio.FIRST_COMPLETED, | ||||
|             timeout=timeout, | ||||
|         ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _extract_value(*values): | ||||
|         value = values[-1] | ||||
|         for v in values: | ||||
|             if v is not None: | ||||
|                 value = v | ||||
|                 break | ||||
|         return value | ||||
|   | ||||
							
								
								
									
										102
									
								
								sanic/config.py
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								sanic/config.py
									
									
									
									
									
								
							| @@ -1,7 +1,10 @@ | ||||
| from inspect import isclass | ||||
| from os import environ | ||||
| from pathlib import Path | ||||
| from typing import Any, Union | ||||
| from typing import Any, Dict, Optional, Union | ||||
| from warnings import warn | ||||
|  | ||||
| from sanic.http import Http | ||||
|  | ||||
| from .utils import load_module_from_file_location, str_to_bool | ||||
|  | ||||
| @@ -15,33 +18,64 @@ BASE_LOGO = """ | ||||
| """ | ||||
|  | ||||
| DEFAULT_CONFIG = { | ||||
|     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes | ||||
|     "REQUEST_BUFFER_QUEUE_SIZE": 100, | ||||
|     "ACCESS_LOG": True, | ||||
|     "EVENT_AUTOREGISTER": False, | ||||
|     "FALLBACK_ERROR_FORMAT": "html", | ||||
|     "FORWARDED_FOR_HEADER": "X-Forwarded-For", | ||||
|     "FORWARDED_SECRET": None, | ||||
|     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,  # 15 sec | ||||
|     "KEEP_ALIVE_TIMEOUT": 5,  # 5 seconds | ||||
|     "KEEP_ALIVE": True, | ||||
|     "PROXIES_COUNT": None, | ||||
|     "REAL_IP_HEADER": None, | ||||
|     "REGISTER": True, | ||||
|     "REQUEST_BUFFER_SIZE": 65536,  # 64 KiB | ||||
|     "REQUEST_MAX_HEADER_SIZE": 8192,  # 8 KiB, but 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 | ||||
|     "KEEP_ALIVE": True, | ||||
|     "KEEP_ALIVE_TIMEOUT": 5,  # 5 seconds | ||||
|     "WEBSOCKET_MAX_SIZE": 2 ** 20,  # 1 megabyte | ||||
|     "WEBSOCKET_MAX_QUEUE": 32, | ||||
|     "WEBSOCKET_MAX_SIZE": 2 ** 20,  # 1 megabyte | ||||
|     "WEBSOCKET_PING_INTERVAL": 20, | ||||
|     "WEBSOCKET_PING_TIMEOUT": 20, | ||||
|     "WEBSOCKET_READ_LIMIT": 2 ** 16, | ||||
|     "WEBSOCKET_WRITE_LIMIT": 2 ** 16, | ||||
|     "WEBSOCKET_PING_TIMEOUT": 20, | ||||
|     "WEBSOCKET_PING_INTERVAL": 20, | ||||
|     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,  # 15 sec | ||||
|     "ACCESS_LOG": True, | ||||
|     "FORWARDED_SECRET": None, | ||||
|     "REAL_IP_HEADER": None, | ||||
|     "PROXIES_COUNT": None, | ||||
|     "FORWARDED_FOR_HEADER": "X-Forwarded-For", | ||||
|     "REQUEST_ID_HEADER": "X-Request-ID", | ||||
|     "FALLBACK_ERROR_FORMAT": "html", | ||||
|     "REGISTER": True, | ||||
| } | ||||
|  | ||||
|  | ||||
| class Config(dict): | ||||
|     def __init__(self, defaults=None, load_env=True, keep_alive=None): | ||||
|     ACCESS_LOG: bool | ||||
|     EVENT_AUTOREGISTER: bool | ||||
|     FALLBACK_ERROR_FORMAT: str | ||||
|     FORWARDED_FOR_HEADER: str | ||||
|     FORWARDED_SECRET: Optional[str] | ||||
|     GRACEFUL_SHUTDOWN_TIMEOUT: float | ||||
|     KEEP_ALIVE_TIMEOUT: int | ||||
|     KEEP_ALIVE: bool | ||||
|     PROXIES_COUNT: Optional[int] | ||||
|     REAL_IP_HEADER: Optional[str] | ||||
|     REGISTER: bool | ||||
|     REQUEST_BUFFER_SIZE: int | ||||
|     REQUEST_MAX_HEADER_SIZE: int | ||||
|     REQUEST_ID_HEADER: str | ||||
|     REQUEST_MAX_SIZE: int | ||||
|     REQUEST_TIMEOUT: int | ||||
|     RESPONSE_TIMEOUT: int | ||||
|     WEBSOCKET_MAX_QUEUE: int | ||||
|     WEBSOCKET_MAX_SIZE: int | ||||
|     WEBSOCKET_PING_INTERVAL: int | ||||
|     WEBSOCKET_PING_TIMEOUT: int | ||||
|     WEBSOCKET_READ_LIMIT: int | ||||
|     WEBSOCKET_WRITE_LIMIT: int | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         defaults: Dict[str, Union[str, bool, int, float, None]] = None, | ||||
|         load_env: Optional[Union[bool, str]] = True, | ||||
|         env_prefix: Optional[str] = SANIC_PREFIX, | ||||
|         keep_alive: Optional[bool] = None, | ||||
|     ): | ||||
|         defaults = defaults or {} | ||||
|         super().__init__({**DEFAULT_CONFIG, **defaults}) | ||||
|  | ||||
| @@ -50,9 +84,22 @@ class Config(dict): | ||||
|         if keep_alive is not None: | ||||
|             self.KEEP_ALIVE = keep_alive | ||||
|  | ||||
|         if load_env: | ||||
|             prefix = SANIC_PREFIX if load_env is True else load_env | ||||
|             self.load_environment_vars(prefix=prefix) | ||||
|         if env_prefix != SANIC_PREFIX: | ||||
|             if env_prefix: | ||||
|                 self.load_environment_vars(env_prefix) | ||||
|         elif load_env is not True: | ||||
|             if load_env: | ||||
|                 self.load_environment_vars(prefix=load_env) | ||||
|             warn( | ||||
|                 "Use of load_env is deprecated and will be removed in " | ||||
|                 "21.12. Modify the configuration prefix by passing " | ||||
|                 "env_prefix instead.", | ||||
|                 DeprecationWarning, | ||||
|             ) | ||||
|         else: | ||||
|             self.load_environment_vars(SANIC_PREFIX) | ||||
|  | ||||
|         self._configure_header_size() | ||||
|  | ||||
|     def __getattr__(self, attr): | ||||
|         try: | ||||
| @@ -62,6 +109,19 @@ class Config(dict): | ||||
|  | ||||
|     def __setattr__(self, attr, value): | ||||
|         self[attr] = value | ||||
|         if attr in ( | ||||
|             "REQUEST_MAX_HEADER_SIZE", | ||||
|             "REQUEST_BUFFER_SIZE", | ||||
|             "REQUEST_MAX_SIZE", | ||||
|         ): | ||||
|             self._configure_header_size() | ||||
|  | ||||
|     def _configure_header_size(self): | ||||
|         Http.set_header_max_size( | ||||
|             self.REQUEST_MAX_HEADER_SIZE, | ||||
|             self.REQUEST_BUFFER_SIZE - 4096, | ||||
|             self.REQUEST_MAX_SIZE, | ||||
|         ) | ||||
|  | ||||
|     def load_environment_vars(self, prefix=SANIC_PREFIX): | ||||
|         """ | ||||
|   | ||||
| @@ -1,2 +1,28 @@ | ||||
| HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE") | ||||
| from enum import Enum, auto | ||||
|  | ||||
|  | ||||
| class HTTPMethod(str, Enum): | ||||
|     def _generate_next_value_(name, start, count, last_values): | ||||
|         return name.upper() | ||||
|  | ||||
|     def __eq__(self, value: object) -> bool: | ||||
|         value = str(value).upper() | ||||
|         return super().__eq__(value) | ||||
|  | ||||
|     def __hash__(self) -> int: | ||||
|         return hash(self.value) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.value | ||||
|  | ||||
|     GET = auto() | ||||
|     POST = auto() | ||||
|     PUT = auto() | ||||
|     HEAD = auto() | ||||
|     OPTIONS = auto() | ||||
|     PATCH = auto() | ||||
|     DELETE = auto() | ||||
|  | ||||
|  | ||||
| HTTP_METHODS = tuple(HTTPMethod.__members__.values()) | ||||
| DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" | ||||
|   | ||||
| @@ -366,7 +366,7 @@ def exception_response( | ||||
|                 except InvalidUsage: | ||||
|                     renderer = HTMLRenderer | ||||
|  | ||||
|                 content_type, *_ = request.headers.get( | ||||
|                 content_type, *_ = request.headers.getone( | ||||
|                     "content-type", "" | ||||
|                 ).split(";") | ||||
|                 renderer = RENDERERS_BY_CONTENT_TYPE.get( | ||||
|   | ||||
| @@ -3,26 +3,18 @@ from typing import Optional, Union | ||||
| from sanic.helpers import STATUS_CODES | ||||
|  | ||||
|  | ||||
| _sanic_exceptions = {} | ||||
|  | ||||
|  | ||||
| def add_status_code(code, quiet=None): | ||||
|     """ | ||||
|     Decorator used for adding exceptions to :class:`SanicException`. | ||||
|     """ | ||||
|  | ||||
|     def class_decorator(cls): | ||||
|         cls.status_code = code | ||||
|         if quiet or quiet is None and code != 500: | ||||
|             cls.quiet = True | ||||
|         _sanic_exceptions[code] = cls | ||||
|         return cls | ||||
|  | ||||
|     return class_decorator | ||||
|  | ||||
|  | ||||
| class SanicException(Exception): | ||||
|     def __init__(self, message, status_code=None, quiet=None): | ||||
|     def __init__( | ||||
|         self, | ||||
|         message: Optional[Union[str, bytes]] = None, | ||||
|         status_code: Optional[int] = None, | ||||
|         quiet: Optional[bool] = None, | ||||
|     ) -> None: | ||||
|  | ||||
|         if message is None and status_code is not None: | ||||
|             msg: bytes = STATUS_CODES.get(status_code, b"") | ||||
|             message = msg.decode("utf8") | ||||
|  | ||||
|         super().__init__(message) | ||||
|  | ||||
|         if status_code is not None: | ||||
| @@ -33,45 +25,45 @@ class SanicException(Exception): | ||||
|             self.quiet = True | ||||
|  | ||||
|  | ||||
| @add_status_code(404) | ||||
| class NotFound(SanicException): | ||||
|     """ | ||||
|     **Status**: 404 Not Found | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 404 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| @add_status_code(400) | ||||
| class InvalidUsage(SanicException): | ||||
|     """ | ||||
|     **Status**: 400 Bad Request | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 400 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| @add_status_code(405) | ||||
| class MethodNotSupported(SanicException): | ||||
|     """ | ||||
|     **Status**: 405 Method Not Allowed | ||||
|     """ | ||||
|  | ||||
|     status_code = 405 | ||||
|     quiet = True | ||||
|  | ||||
|     def __init__(self, message, method, allowed_methods): | ||||
|         super().__init__(message) | ||||
|         self.headers = {"Allow": ", ".join(allowed_methods)} | ||||
|  | ||||
|  | ||||
| @add_status_code(500) | ||||
| class ServerError(SanicException): | ||||
|     """ | ||||
|     **Status**: 500 Internal Server Error | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 500 | ||||
|  | ||||
|  | ||||
| @add_status_code(503) | ||||
| class ServiceUnavailable(SanicException): | ||||
|     """ | ||||
|     **Status**: 503 Service Unavailable | ||||
| @@ -80,7 +72,8 @@ class ServiceUnavailable(SanicException): | ||||
|     down for maintenance). Generally, this is a temporary state. | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 503 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class URLBuildError(ServerError): | ||||
| @@ -88,7 +81,7 @@ class URLBuildError(ServerError): | ||||
|     **Status**: 500 Internal Server Error | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 500 | ||||
|  | ||||
|  | ||||
| class FileNotFound(NotFound): | ||||
| @@ -102,7 +95,6 @@ class FileNotFound(NotFound): | ||||
|         self.relative_url = relative_url | ||||
|  | ||||
|  | ||||
| @add_status_code(408) | ||||
| class RequestTimeout(SanicException): | ||||
|     """The Web server (running the Web site) thinks that there has been too | ||||
|     long an interval of time between 1) the establishment of an IP | ||||
| @@ -112,16 +104,17 @@ class RequestTimeout(SanicException): | ||||
|     server has 'timed out' on that particular socket connection. | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 408 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| @add_status_code(413) | ||||
| class PayloadTooLarge(SanicException): | ||||
|     """ | ||||
|     **Status**: 413 Payload Too Large | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 413 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class HeaderNotFound(InvalidUsage): | ||||
| @@ -129,36 +122,39 @@ class HeaderNotFound(InvalidUsage): | ||||
|     **Status**: 400 Bad Request | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 400 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| @add_status_code(416) | ||||
| class ContentRangeError(SanicException): | ||||
|     """ | ||||
|     **Status**: 416 Range Not Satisfiable | ||||
|     """ | ||||
|  | ||||
|     status_code = 416 | ||||
|     quiet = True | ||||
|  | ||||
|     def __init__(self, message, content_range): | ||||
|         super().__init__(message) | ||||
|         self.headers = {"Content-Range": f"bytes */{content_range.total}"} | ||||
|  | ||||
|  | ||||
| @add_status_code(417) | ||||
| class HeaderExpectationFailed(SanicException): | ||||
|     """ | ||||
|     **Status**: 417 Expectation Failed | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 417 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| @add_status_code(403) | ||||
| class Forbidden(SanicException): | ||||
|     """ | ||||
|     **Status**: 403 Forbidden | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 403 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class InvalidRangeType(ContentRangeError): | ||||
| @@ -166,7 +162,8 @@ class InvalidRangeType(ContentRangeError): | ||||
|     **Status**: 416 Range Not Satisfiable | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|     status_code = 416 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class PyFileError(Exception): | ||||
| @@ -174,7 +171,6 @@ class PyFileError(Exception): | ||||
|         super().__init__("could not execute config file %s", file) | ||||
|  | ||||
|  | ||||
| @add_status_code(401) | ||||
| class Unauthorized(SanicException): | ||||
|     """ | ||||
|     **Status**: 401 Unauthorized | ||||
| @@ -210,6 +206,9 @@ class Unauthorized(SanicException): | ||||
|                            realm="Restricted Area") | ||||
|     """ | ||||
|  | ||||
|     status_code = 401 | ||||
|     quiet = True | ||||
|  | ||||
|     def __init__(self, message, status_code=None, scheme=None, **kwargs): | ||||
|         super().__init__(message, status_code) | ||||
|  | ||||
| @@ -241,9 +240,13 @@ def abort(status_code: int, message: Optional[Union[str, bytes]] = None): | ||||
|     :param status_code: The HTTP status code to return. | ||||
|     :param message: The HTTP response body. Defaults to the messages in | ||||
|     """ | ||||
|     if message is None: | ||||
|         msg: bytes = STATUS_CODES[status_code] | ||||
|         # These are stored as bytes in the STATUS_CODES dict | ||||
|         message = msg.decode("utf8") | ||||
|     sanic_exception = _sanic_exceptions.get(status_code, SanicException) | ||||
|     raise sanic_exception(message=message, status_code=status_code) | ||||
|     import warnings | ||||
|  | ||||
|     warnings.warn( | ||||
|         "sanic.exceptions.abort has been marked as deprecated, and will be " | ||||
|         "removed in release 21.12.\n To migrate your code, simply replace " | ||||
|         "abort(status_code, msg) with raise SanicException(msg, status_code), " | ||||
|         "or even better, raise an appropriate SanicException subclass." | ||||
|     ) | ||||
|  | ||||
|     raise SanicException(message=message, status_code=status_code) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ from sanic.exceptions import ( | ||||
|     HeaderNotFound, | ||||
|     InvalidRangeType, | ||||
| ) | ||||
| from sanic.log import logger | ||||
| from sanic.log import error_logger | ||||
| from sanic.response import text | ||||
|  | ||||
|  | ||||
| @@ -25,7 +25,6 @@ class ErrorHandler: | ||||
|  | ||||
|     handlers = None | ||||
|     cached_handlers = None | ||||
|     _missing = object() | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.handlers = [] | ||||
| @@ -45,7 +44,9 @@ class ErrorHandler: | ||||
|  | ||||
|         :return: None | ||||
|         """ | ||||
|         # self.handlers to be deprecated and removed in version 21.12 | ||||
|         self.handlers.append((exception, handler)) | ||||
|         self.cached_handlers[exception] = handler | ||||
|  | ||||
|     def lookup(self, exception): | ||||
|         """ | ||||
| @@ -61,14 +62,19 @@ class ErrorHandler: | ||||
|  | ||||
|         :return: Registered function if found ``None`` otherwise | ||||
|         """ | ||||
|         handler = self.cached_handlers.get(type(exception), self._missing) | ||||
|         if handler is self._missing: | ||||
|             for exception_class, handler in self.handlers: | ||||
|                 if isinstance(exception, exception_class): | ||||
|                     self.cached_handlers[type(exception)] = handler | ||||
|                     return handler | ||||
|             self.cached_handlers[type(exception)] = None | ||||
|             handler = None | ||||
|         exception_class = type(exception) | ||||
|         if exception_class in self.cached_handlers: | ||||
|             return self.cached_handlers[exception_class] | ||||
|  | ||||
|         for ancestor in type.mro(exception_class): | ||||
|             if ancestor in self.cached_handlers: | ||||
|                 handler = self.cached_handlers[ancestor] | ||||
|                 self.cached_handlers[exception_class] = handler | ||||
|                 return handler | ||||
|             if ancestor is BaseException: | ||||
|                 break | ||||
|         self.cached_handlers[exception_class] = None | ||||
|         handler = None | ||||
|         return handler | ||||
|  | ||||
|     def response(self, request, exception): | ||||
| @@ -101,7 +107,7 @@ class ErrorHandler: | ||||
|             response_message = ( | ||||
|                 "Exception raised in exception handler " '"%s" for uri: %s' | ||||
|             ) | ||||
|             logger.exception(response_message, handler.__name__, url) | ||||
|             error_logger.exception(response_message, handler.__name__, url) | ||||
|  | ||||
|             if self.debug: | ||||
|                 return text(response_message % (handler.__name__, url), 500) | ||||
| @@ -137,7 +143,9 @@ class ErrorHandler: | ||||
|                 url = "unknown" | ||||
|  | ||||
|             self.log(format_exc()) | ||||
|             logger.exception("Exception occurred while handling uri: %s", url) | ||||
|             error_logger.exception( | ||||
|                 "Exception occurred while handling uri: %s", url | ||||
|             ) | ||||
|  | ||||
|         return exception_response(request, exception, self.debug) | ||||
|  | ||||
| @@ -165,7 +173,7 @@ class ContentRangeHandler: | ||||
|  | ||||
|     def __init__(self, request, stats): | ||||
|         self.total = stats.st_size | ||||
|         _range = request.headers.get("Range") | ||||
|         _range = request.headers.getone("range", None) | ||||
|         if _range is None: | ||||
|             raise HeaderNotFound("Range Header Not Found") | ||||
|         unit, _, value = tuple(map(str.strip, _range.partition("="))) | ||||
|   | ||||
| @@ -102,7 +102,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]: | ||||
|     """Parse traditional proxy headers.""" | ||||
|     real_ip_header = config.REAL_IP_HEADER | ||||
|     proxies_count = config.PROXIES_COUNT | ||||
|     addr = real_ip_header and headers.get(real_ip_header) | ||||
|     addr = real_ip_header and headers.getone(real_ip_header, None) | ||||
|     if not addr and proxies_count: | ||||
|         assert proxies_count > 0 | ||||
|         try: | ||||
| @@ -131,7 +131,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]: | ||||
|             ("port", "x-forwarded-port"), | ||||
|             ("path", "x-forwarded-path"), | ||||
|         ): | ||||
|             yield key, headers.get(header) | ||||
|             yield key, headers.getone(header, None) | ||||
|  | ||||
|     return fwd_normalize(options()) | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ from sanic.exceptions import ( | ||||
| ) | ||||
| from sanic.headers import format_http1_response | ||||
| from sanic.helpers import has_message_body | ||||
| from sanic.log import access_logger, logger | ||||
| from sanic.log import access_logger, error_logger, logger | ||||
|  | ||||
|  | ||||
| class Stage(Enum): | ||||
| @@ -64,6 +64,9 @@ class Http: | ||||
|     :raises RuntimeError: | ||||
|     """ | ||||
|  | ||||
|     HEADER_CEILING = 16_384 | ||||
|     HEADER_MAX_SIZE = 0 | ||||
|  | ||||
|     __slots__ = [ | ||||
|         "_send", | ||||
|         "_receive_more", | ||||
| @@ -82,6 +85,7 @@ class Http: | ||||
|         "request_max_size", | ||||
|         "response", | ||||
|         "response_func", | ||||
|         "response_size", | ||||
|         "response_bytes_left", | ||||
|         "upgrade_websocket", | ||||
|     ] | ||||
| @@ -91,19 +95,23 @@ class Http: | ||||
|         self._receive_more = protocol.receive_more | ||||
|         self.recv_buffer = protocol.recv_buffer | ||||
|         self.protocol = protocol | ||||
|         self.expecting_continue: bool = False | ||||
|         self.keep_alive = True | ||||
|         self.stage: Stage = Stage.IDLE | ||||
|         self.init_for_request() | ||||
|  | ||||
|     def init_for_request(self): | ||||
|         """Init/reset all per-request variables.""" | ||||
|         self.exception = None | ||||
|         self.expecting_continue: bool = False | ||||
|         self.head_only = None | ||||
|         self.request_body = None | ||||
|         self.request_bytes = None | ||||
|         self.request_bytes_left = None | ||||
|         self.request_max_size = protocol.request_max_size | ||||
|         self.keep_alive = True | ||||
|         self.head_only = None | ||||
|         self.request_max_size = self.protocol.request_max_size | ||||
|         self.request: Request = None | ||||
|         self.response: BaseHTTPResponse = None | ||||
|         self.exception = None | ||||
|         self.url = None | ||||
|         self.upgrade_websocket = False | ||||
|         self.url = None | ||||
|  | ||||
|     def __bool__(self): | ||||
|         """Test if request handling is in progress""" | ||||
| @@ -143,8 +151,11 @@ class Http: | ||||
|             # Try to consume any remaining request body | ||||
|             if self.request_body: | ||||
|                 if self.response and 200 <= self.response.status < 300: | ||||
|                     logger.error(f"{self.request} body not consumed.") | ||||
|  | ||||
|                     error_logger.error(f"{self.request} body not consumed.") | ||||
|                 # Limit the size because the handler may have set it infinite | ||||
|                 self.request_max_size = min( | ||||
|                     self.request_max_size, self.protocol.request_max_size | ||||
|                 ) | ||||
|                 try: | ||||
|                     async for _ in self: | ||||
|                         pass | ||||
| @@ -156,11 +167,19 @@ class Http: | ||||
|                     await sleep(0.001) | ||||
|                     self.keep_alive = False | ||||
|  | ||||
|             # Clean up to free memory and for the next request | ||||
|             if self.request: | ||||
|                 self.request.stream = None | ||||
|                 if self.response: | ||||
|                     self.response.stream = None | ||||
|  | ||||
|             self.init_for_request() | ||||
|  | ||||
|             # Exit and disconnect if no more requests can be taken | ||||
|             if self.stage is not Stage.IDLE or not self.keep_alive: | ||||
|                 break | ||||
|  | ||||
|             # Wait for next request | ||||
|             # Wait for the next request | ||||
|             if not self.recv_buffer: | ||||
|                 await self._receive_more() | ||||
|  | ||||
| @@ -168,7 +187,6 @@ class Http: | ||||
|         """ | ||||
|         Receive and parse request header into self.request. | ||||
|         """ | ||||
|         HEADER_MAX_SIZE = min(8192, self.request_max_size) | ||||
|         # Receive until full header is in buffer | ||||
|         buf = self.recv_buffer | ||||
|         pos = 0 | ||||
| @@ -179,12 +197,12 @@ class Http: | ||||
|                 break | ||||
|  | ||||
|             pos = max(0, len(buf) - 3) | ||||
|             if pos >= HEADER_MAX_SIZE: | ||||
|             if pos >= self.HEADER_MAX_SIZE: | ||||
|                 break | ||||
|  | ||||
|             await self._receive_more() | ||||
|  | ||||
|         if pos >= HEADER_MAX_SIZE: | ||||
|         if pos >= self.HEADER_MAX_SIZE: | ||||
|             raise PayloadTooLarge("Request header exceeds the size limit") | ||||
|  | ||||
|         # Parse header content | ||||
| @@ -218,7 +236,9 @@ class Http: | ||||
|             raise InvalidUsage("Bad Request") | ||||
|  | ||||
|         headers_instance = Header(headers) | ||||
|         self.upgrade_websocket = headers_instance.get("upgrade") == "websocket" | ||||
|         self.upgrade_websocket = ( | ||||
|             headers_instance.getone("upgrade", "").lower() == "websocket" | ||||
|         ) | ||||
|  | ||||
|         # Prepare a Request object | ||||
|         request = self.protocol.request_class( | ||||
| @@ -235,7 +255,7 @@ class Http: | ||||
|         self.request_bytes_left = self.request_bytes = 0 | ||||
|         if request_body: | ||||
|             headers = request.headers | ||||
|             expect = headers.get("expect") | ||||
|             expect = headers.getone("expect", None) | ||||
|  | ||||
|             if expect is not None: | ||||
|                 if expect.lower() == "100-continue": | ||||
| @@ -243,7 +263,7 @@ class Http: | ||||
|                 else: | ||||
|                     raise HeaderExpectationFailed(f"Unknown Expect: {expect}") | ||||
|  | ||||
|             if headers.get("transfer-encoding") == "chunked": | ||||
|             if headers.getone("transfer-encoding", None) == "chunked": | ||||
|                 self.request_body = "chunked" | ||||
|                 pos -= 2  # One CRLF stays in buffer | ||||
|             else: | ||||
| @@ -270,6 +290,7 @@ class Http: | ||||
|         size = len(data) | ||||
|         headers = res.headers | ||||
|         status = res.status | ||||
|         self.response_size = size | ||||
|  | ||||
|         if not isinstance(status, int) or status < 200: | ||||
|             raise RuntimeError(f"Invalid response status {status!r}") | ||||
| @@ -424,7 +445,9 @@ class Http: | ||||
|         req, res = self.request, self.response | ||||
|         extra = { | ||||
|             "status": getattr(res, "status", 0), | ||||
|             "byte": getattr(self, "response_bytes_left", -1), | ||||
|             "byte": getattr( | ||||
|                 self, "response_bytes_left", getattr(self, "response_size", -1) | ||||
|             ), | ||||
|             "host": "UNKNOWN", | ||||
|             "request": "nil", | ||||
|         } | ||||
| @@ -478,8 +501,6 @@ class Http: | ||||
|                 self.keep_alive = False | ||||
|                 raise InvalidUsage("Bad chunked encoding") | ||||
|  | ||||
|             del buf[: pos + 2] | ||||
|  | ||||
|             if size <= 0: | ||||
|                 self.request_body = None | ||||
|  | ||||
| @@ -487,8 +508,17 @@ class Http: | ||||
|                     self.keep_alive = False | ||||
|                     raise InvalidUsage("Bad chunked encoding") | ||||
|  | ||||
|                 # Consume CRLF, chunk size 0 and the two CRLF that follow | ||||
|                 pos += 4 | ||||
|                 # Might need to wait for the final CRLF | ||||
|                 while len(buf) < pos: | ||||
|                     await self._receive_more() | ||||
|                 del buf[:pos] | ||||
|                 return None | ||||
|  | ||||
|             # Remove CRLF, chunk size and the CRLF that follows | ||||
|             del buf[: pos + 2] | ||||
|  | ||||
|             self.request_bytes_left = size | ||||
|             self.request_bytes += size | ||||
|  | ||||
| @@ -535,3 +565,10 @@ class Http: | ||||
|     @property | ||||
|     def send(self): | ||||
|         return self.response_func | ||||
|  | ||||
|     @classmethod | ||||
|     def set_header_max_size(cls, *sizes: int): | ||||
|         cls.HEADER_MAX_SIZE = min( | ||||
|             *sizes, | ||||
|             cls.HEADER_CEILING, | ||||
|         ) | ||||
|   | ||||
| @@ -35,7 +35,7 @@ class ListenerMixin: | ||||
|         """ | ||||
|         Create a listener from a decorated function. | ||||
|  | ||||
|         To be used as a deocrator: | ||||
|         To be used as a decorator: | ||||
|  | ||||
|         .. code-block:: python | ||||
|  | ||||
|   | ||||
| @@ -26,10 +26,11 @@ from sanic.views import CompositionView | ||||
|  | ||||
|  | ||||
| class RouteMixin: | ||||
|     name: str | ||||
|  | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         self._future_routes: Set[FutureRoute] = set() | ||||
|         self._future_statics: Set[FutureStatic] = set() | ||||
|         self.name = "" | ||||
|         self.strict_slashes: Optional[bool] = False | ||||
|  | ||||
|     def _apply_route(self, route: FutureRoute) -> List[Route]: | ||||
| @@ -45,7 +46,7 @@ class RouteMixin: | ||||
|         host: Optional[str] = None, | ||||
|         strict_slashes: Optional[bool] = None, | ||||
|         stream: bool = False, | ||||
|         version: Optional[int] = None, | ||||
|         version: Optional[Union[int, str, float]] = None, | ||||
|         name: Optional[str] = None, | ||||
|         ignore_body: bool = False, | ||||
|         apply: bool = True, | ||||
| @@ -53,6 +54,7 @@ class RouteMixin: | ||||
|         websocket: bool = False, | ||||
|         unquote: bool = False, | ||||
|         static: bool = False, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Decorate a function to be registered as a route | ||||
| @@ -66,12 +68,14 @@ class RouteMixin: | ||||
|         :param name: user defined route name for url_for | ||||
|         :param ignore_body: whether the handler should ignore request | ||||
|             body (eg. GET requests) | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: tuple of routes, decorated function | ||||
|         """ | ||||
|  | ||||
|         # Fix case where the user did not prefix the URL with a / | ||||
|         # and will probably get confused as to why it's not working | ||||
|         if not uri.startswith("/"): | ||||
|         if not uri.startswith("/") and (uri or hasattr(self, "router")): | ||||
|             uri = "/" + uri | ||||
|  | ||||
|         if strict_slashes is None: | ||||
| @@ -92,6 +96,7 @@ class RouteMixin: | ||||
|             nonlocal subprotocols | ||||
|             nonlocal websocket | ||||
|             nonlocal static | ||||
|             nonlocal version_prefix | ||||
|  | ||||
|             if isinstance(handler, tuple): | ||||
|                 # if a handler fn is already wrapped in a route, the handler | ||||
| @@ -128,6 +133,7 @@ class RouteMixin: | ||||
|                 subprotocols, | ||||
|                 unquote, | ||||
|                 static, | ||||
|                 version_prefix, | ||||
|             ) | ||||
|  | ||||
|             self._future_routes.add(route) | ||||
| @@ -154,7 +160,9 @@ class RouteMixin: | ||||
|             if apply: | ||||
|                 self._apply_route(route) | ||||
|  | ||||
|             return route, handler | ||||
|             if static: | ||||
|                 return route, handler | ||||
|             return handler | ||||
|  | ||||
|         return decorator | ||||
|  | ||||
| @@ -168,6 +176,7 @@ class RouteMixin: | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         stream: bool = False, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """A helper method to register class instance or | ||||
|         functions as a handler to the application url | ||||
| @@ -182,6 +191,8 @@ class RouteMixin: | ||||
|         :param version: | ||||
|         :param name: user defined route name for url_for | ||||
|         :param stream: boolean specifying if the handler is a stream handler | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: function or class instance | ||||
|         """ | ||||
|         # Handle HTTPMethodView differently | ||||
| @@ -214,6 +225,7 @@ class RouteMixin: | ||||
|             stream=stream, | ||||
|             version=version, | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|         )(handler) | ||||
|         return handler | ||||
|  | ||||
| @@ -226,6 +238,7 @@ class RouteMixin: | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         ignore_body: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Add an API URL under the **GET** *HTTP* method | ||||
| @@ -236,6 +249,8 @@ class RouteMixin: | ||||
|             URLs need to terminate with a */* | ||||
|         :param version: API Version | ||||
|         :param name: Unique name that can be used to identify the Route | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -246,6 +261,7 @@ class RouteMixin: | ||||
|             version=version, | ||||
|             name=name, | ||||
|             ignore_body=ignore_body, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|  | ||||
|     def post( | ||||
| @@ -256,6 +272,7 @@ class RouteMixin: | ||||
|         stream: bool = False, | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Add an API URL under the **POST** *HTTP* method | ||||
| @@ -266,6 +283,8 @@ class RouteMixin: | ||||
|             URLs need to terminate with a */* | ||||
|         :param version: API Version | ||||
|         :param name: Unique name that can be used to identify the Route | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -276,6 +295,7 @@ class RouteMixin: | ||||
|             stream=stream, | ||||
|             version=version, | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|  | ||||
|     def put( | ||||
| @@ -286,6 +306,7 @@ class RouteMixin: | ||||
|         stream: bool = False, | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Add an API URL under the **PUT** *HTTP* method | ||||
| @@ -296,6 +317,8 @@ class RouteMixin: | ||||
|             URLs need to terminate with a */* | ||||
|         :param version: API Version | ||||
|         :param name: Unique name that can be used to identify the Route | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -306,6 +329,7 @@ class RouteMixin: | ||||
|             stream=stream, | ||||
|             version=version, | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|  | ||||
|     def head( | ||||
| @@ -316,6 +340,7 @@ class RouteMixin: | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         ignore_body: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Add an API URL under the **HEAD** *HTTP* method | ||||
| @@ -334,6 +359,8 @@ class RouteMixin: | ||||
|         :param ignore_body: whether the handler should ignore request | ||||
|             body (eg. GET requests), defaults to True | ||||
|         :type ignore_body: bool, optional | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -344,6 +371,7 @@ class RouteMixin: | ||||
|             version=version, | ||||
|             name=name, | ||||
|             ignore_body=ignore_body, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|  | ||||
|     def options( | ||||
| @@ -354,6 +382,7 @@ class RouteMixin: | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         ignore_body: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Add an API URL under the **OPTIONS** *HTTP* method | ||||
| @@ -372,6 +401,8 @@ class RouteMixin: | ||||
|         :param ignore_body: whether the handler should ignore request | ||||
|             body (eg. GET requests), defaults to True | ||||
|         :type ignore_body: bool, optional | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -382,6 +413,7 @@ class RouteMixin: | ||||
|             version=version, | ||||
|             name=name, | ||||
|             ignore_body=ignore_body, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|  | ||||
|     def patch( | ||||
| @@ -392,6 +424,7 @@ class RouteMixin: | ||||
|         stream=False, | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Add an API URL under the **PATCH** *HTTP* method | ||||
| @@ -412,6 +445,8 @@ class RouteMixin: | ||||
|         :param ignore_body: whether the handler should ignore request | ||||
|             body (eg. GET requests), defaults to True | ||||
|         :type ignore_body: bool, optional | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -422,6 +457,7 @@ class RouteMixin: | ||||
|             stream=stream, | ||||
|             version=version, | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|  | ||||
|     def delete( | ||||
| @@ -432,6 +468,7 @@ class RouteMixin: | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         ignore_body: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Add an API URL under the **DELETE** *HTTP* method | ||||
| @@ -442,6 +479,8 @@ class RouteMixin: | ||||
|             URLs need to terminate with a */* | ||||
|         :param version: API Version | ||||
|         :param name: Unique name that can be used to identify the Route | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: Object decorated with :func:`route` method | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -452,6 +491,7 @@ class RouteMixin: | ||||
|             version=version, | ||||
|             name=name, | ||||
|             ignore_body=ignore_body, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|  | ||||
|     def websocket( | ||||
| @@ -463,6 +503,7 @@ class RouteMixin: | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         apply: bool = True, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         Decorate a function to be registered as a websocket route | ||||
| @@ -474,6 +515,8 @@ class RouteMixin: | ||||
|         :param subprotocols: optional list of str with supported subprotocols | ||||
|         :param name: A unique name assigned to the URL so that it can | ||||
|                      be used with :func:`url_for` | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: tuple of routes, decorated function | ||||
|         """ | ||||
|         return self.route( | ||||
| @@ -486,6 +529,7 @@ class RouteMixin: | ||||
|             apply=apply, | ||||
|             subprotocols=subprotocols, | ||||
|             websocket=True, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|  | ||||
|     def add_websocket_route( | ||||
| @@ -497,6 +541,7 @@ class RouteMixin: | ||||
|         subprotocols=None, | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|         A helper method to register a function as a websocket route. | ||||
| @@ -513,6 +558,8 @@ class RouteMixin: | ||||
|                 handshake | ||||
|         :param name: A unique name assigned to the URL so that it can | ||||
|                 be used with :func:`url_for` | ||||
|         :param version_prefix: URL path that should be before the version | ||||
|             value; default: ``/v`` | ||||
|         :return: Objected decorated by :func:`websocket` | ||||
|         """ | ||||
|         return self.websocket( | ||||
| @@ -522,6 +569,7 @@ class RouteMixin: | ||||
|             subprotocols=subprotocols, | ||||
|             version=version, | ||||
|             name=name, | ||||
|             version_prefix=version_prefix, | ||||
|         )(handler) | ||||
|  | ||||
|     def static( | ||||
| @@ -665,7 +713,10 @@ class RouteMixin: | ||||
|                 modified_since = strftime( | ||||
|                     "%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime) | ||||
|                 ) | ||||
|                 if request.headers.get("If-Modified-Since") == modified_since: | ||||
|                 if ( | ||||
|                     request.headers.getone("if-modified-since", None) | ||||
|                     == modified_since | ||||
|                 ): | ||||
|                     return HTTPResponse(status=304) | ||||
|                 headers["Last-Modified"] = modified_since | ||||
|             _range = None | ||||
| @@ -718,16 +769,18 @@ class RouteMixin: | ||||
|                 return await file(file_path, headers=headers, _range=_range) | ||||
|         except ContentRangeError: | ||||
|             raise | ||||
|         except Exception: | ||||
|             error_logger.exception( | ||||
|                 f"File not found: path={file_or_directory}, " | ||||
|                 f"relative_url={__file_uri__}" | ||||
|             ) | ||||
|         except FileNotFoundError: | ||||
|             raise FileNotFound( | ||||
|                 "File not found", | ||||
|                 path=file_or_directory, | ||||
|                 relative_url=__file_uri__, | ||||
|             ) | ||||
|         except Exception: | ||||
|             error_logger.exception( | ||||
|                 f"Exception in static request handler:\ | ||||
|  path={file_or_directory}, " | ||||
|                 f"relative_url={__file_uri__}" | ||||
|             ) | ||||
|  | ||||
|     def _register_static( | ||||
|         self, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| from typing import Any, Callable, Dict, Set | ||||
| from typing import Any, Callable, Dict, Optional, Set | ||||
|  | ||||
| from sanic.models.futures import FutureSignal | ||||
| from sanic.models.handler_types import SignalHandler | ||||
| @@ -60,10 +60,16 @@ class SignalMixin: | ||||
|  | ||||
|     def add_signal( | ||||
|         self, | ||||
|         handler, | ||||
|         handler: Optional[Callable[..., Any]], | ||||
|         event: str, | ||||
|         condition: Dict[str, Any] = None, | ||||
|     ): | ||||
|         if not handler: | ||||
|  | ||||
|             async def noop(): | ||||
|                 ... | ||||
|  | ||||
|             handler = noop | ||||
|         self.signal(event=event, condition=condition)(handler) | ||||
|         return handler | ||||
|  | ||||
|   | ||||
| @@ -23,6 +23,7 @@ class FutureRoute(NamedTuple): | ||||
|     subprotocols: Optional[List[str]] | ||||
|     unquote: bool | ||||
|     static: bool | ||||
|     version_prefix: str | ||||
|  | ||||
|  | ||||
| class FutureListener(NamedTuple): | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import itertools | ||||
| import os | ||||
| import signal | ||||
| import subprocess | ||||
| @@ -5,6 +6,9 @@ import sys | ||||
|  | ||||
| from time import sleep | ||||
|  | ||||
| from sanic.config import BASE_LOGO | ||||
| from sanic.log import logger | ||||
|  | ||||
|  | ||||
| def _iter_module_files(): | ||||
|     """This iterates over all relevant Python files. | ||||
| @@ -56,7 +60,21 @@ def restart_with_reloader(): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def watchdog(sleep_interval): | ||||
| def _check_file(filename, mtimes): | ||||
|     need_reload = False | ||||
|  | ||||
|     mtime = os.stat(filename).st_mtime | ||||
|     old_time = mtimes.get(filename) | ||||
|     if old_time is None: | ||||
|         mtimes[filename] = mtime | ||||
|     elif mtime > old_time: | ||||
|         mtimes[filename] = mtime | ||||
|         need_reload = True | ||||
|  | ||||
|     return need_reload | ||||
|  | ||||
|  | ||||
| def watchdog(sleep_interval, app): | ||||
|     """Watch project files, restart worker process if a change happened. | ||||
|  | ||||
|     :param sleep_interval: interval in second. | ||||
| @@ -73,21 +91,25 @@ def watchdog(sleep_interval): | ||||
|  | ||||
|     worker_process = restart_with_reloader() | ||||
|  | ||||
|     if app.config.LOGO: | ||||
|         logger.debug( | ||||
|             app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO | ||||
|         ) | ||||
|  | ||||
|     try: | ||||
|         while True: | ||||
|             need_reload = False | ||||
|  | ||||
|             for filename in _iter_module_files(): | ||||
|             for filename in itertools.chain( | ||||
|                 _iter_module_files(), | ||||
|                 *(d.glob("**/*") for d in app.reload_dirs), | ||||
|             ): | ||||
|                 try: | ||||
|                     mtime = os.stat(filename).st_mtime | ||||
|                     check = _check_file(filename, mtimes) | ||||
|                 except OSError: | ||||
|                     continue | ||||
|  | ||||
|                 old_time = mtimes.get(filename) | ||||
|                 if old_time is None: | ||||
|                     mtimes[filename] = mtime | ||||
|                 elif mtime > old_time: | ||||
|                     mtimes[filename] = mtime | ||||
|                 if check: | ||||
|                     need_reload = True | ||||
|  | ||||
|             if need_reload: | ||||
|   | ||||
| @@ -125,7 +125,7 @@ class Request: | ||||
|         self._name: Optional[str] = None | ||||
|         self.app = app | ||||
|  | ||||
|         self.headers = headers | ||||
|         self.headers = Header(headers) | ||||
|         self.version = version | ||||
|         self.method = method | ||||
|         self.transport = transport | ||||
| @@ -262,7 +262,7 @@ class Request: | ||||
|             app = Sanic("MyApp", request_class=IntRequest) | ||||
|         """ | ||||
|         if not self._id: | ||||
|             self._id = self.headers.get( | ||||
|             self._id = self.headers.getone( | ||||
|                 self.app.config.REQUEST_ID_HEADER, | ||||
|                 self.__class__.generate_id(self),  # type: ignore | ||||
|             ) | ||||
| @@ -303,7 +303,7 @@ class Request: | ||||
|         :return: token related to request | ||||
|         """ | ||||
|         prefixes = ("Bearer", "Token") | ||||
|         auth_header = self.headers.get("Authorization") | ||||
|         auth_header = self.headers.getone("authorization", None) | ||||
|  | ||||
|         if auth_header is not None: | ||||
|             for prefix in prefixes: | ||||
| @@ -317,8 +317,8 @@ class Request: | ||||
|         if self.parsed_form is None: | ||||
|             self.parsed_form = RequestParameters() | ||||
|             self.parsed_files = RequestParameters() | ||||
|             content_type = self.headers.get( | ||||
|                 "Content-Type", DEFAULT_HTTP_CONTENT_TYPE | ||||
|             content_type = self.headers.getone( | ||||
|                 "content-type", DEFAULT_HTTP_CONTENT_TYPE | ||||
|             ) | ||||
|             content_type, parameters = parse_content_header(content_type) | ||||
|             try: | ||||
| @@ -378,9 +378,12 @@ class Request: | ||||
|         :type errors: str | ||||
|         :return: RequestParameters | ||||
|         """ | ||||
|         if not self.parsed_args[ | ||||
|             (keep_blank_values, strict_parsing, encoding, errors) | ||||
|         ]: | ||||
|         if ( | ||||
|             keep_blank_values, | ||||
|             strict_parsing, | ||||
|             encoding, | ||||
|             errors, | ||||
|         ) not in self.parsed_args: | ||||
|             if self.query_string: | ||||
|                 self.parsed_args[ | ||||
|                     (keep_blank_values, strict_parsing, encoding, errors) | ||||
| @@ -434,9 +437,12 @@ class Request: | ||||
|         :type errors: str | ||||
|         :return: list | ||||
|         """ | ||||
|         if not self.parsed_not_grouped_args[ | ||||
|             (keep_blank_values, strict_parsing, encoding, errors) | ||||
|         ]: | ||||
|         if ( | ||||
|             keep_blank_values, | ||||
|             strict_parsing, | ||||
|             encoding, | ||||
|             errors, | ||||
|         ) not in self.parsed_not_grouped_args: | ||||
|             if self.query_string: | ||||
|                 self.parsed_not_grouped_args[ | ||||
|                     (keep_blank_values, strict_parsing, encoding, errors) | ||||
| @@ -465,7 +471,7 @@ class Request: | ||||
|         """ | ||||
|  | ||||
|         if self._cookies is None: | ||||
|             cookie = self.headers.get("Cookie") | ||||
|             cookie = self.headers.getone("cookie", None) | ||||
|             if cookie is not None: | ||||
|                 cookies: SimpleCookie = SimpleCookie() | ||||
|                 cookies.load(cookie) | ||||
| @@ -482,7 +488,7 @@ class Request: | ||||
|         :return: Content-Type header form the request | ||||
|         :rtype: str | ||||
|         """ | ||||
|         return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE) | ||||
|         return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE) | ||||
|  | ||||
|     @property | ||||
|     def match_info(self): | ||||
| @@ -499,7 +505,7 @@ class Request: | ||||
|         :return: peer ip of the socket | ||||
|         :rtype: str | ||||
|         """ | ||||
|         return self.conn_info.client if self.conn_info else "" | ||||
|         return self.conn_info.client_ip if self.conn_info else "" | ||||
|  | ||||
|     @property | ||||
|     def port(self) -> int: | ||||
| @@ -581,7 +587,7 @@ class Request: | ||||
|  | ||||
|         if ( | ||||
|             self.app.websocket_enabled | ||||
|             and self.headers.get("upgrade") == "websocket" | ||||
|             and self.headers.getone("upgrade", "").lower() == "websocket" | ||||
|         ): | ||||
|             scheme = "ws" | ||||
|         else: | ||||
| @@ -608,7 +614,9 @@ class Request: | ||||
|         server_name = self.app.config.get("SERVER_NAME") | ||||
|         if server_name: | ||||
|             return server_name.split("//", 1)[-1].split("/", 1)[0] | ||||
|         return str(self.forwarded.get("host") or self.headers.get("host", "")) | ||||
|         return str( | ||||
|             self.forwarded.get("host") or self.headers.getone("host", "") | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def server_name(self) -> str: | ||||
|   | ||||
| @@ -143,7 +143,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): | ||||
|  | ||||
|     .. warning:: | ||||
|  | ||||
|         **Deprecated** and set for removal in v21.6. You can now achieve the | ||||
|         **Deprecated** and set for removal in v21.12. You can now achieve the | ||||
|         same functionality without a callback. | ||||
|  | ||||
|         .. code-block:: python | ||||
| @@ -174,12 +174,16 @@ class StreamingHTTPResponse(BaseHTTPResponse): | ||||
|         status: int = 200, | ||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||
|         content_type: str = "text/plain; charset=utf-8", | ||||
|         chunked="deprecated", | ||||
|         ignore_deprecation_notice: bool = False, | ||||
|     ): | ||||
|         if chunked != "deprecated": | ||||
|         if not ignore_deprecation_notice: | ||||
|             warn( | ||||
|                 "The chunked argument has been deprecated and will be " | ||||
|                 "removed in v21.6" | ||||
|                 "Use of the StreamingHTTPResponse is deprecated in v21.6, and " | ||||
|                 "will be removed in v21.12. Please upgrade your streaming " | ||||
|                 "response implementation. You can learn more here: " | ||||
|                 "https://sanicframework.org/en/guide/advanced/streaming.html" | ||||
|                 "#response-streaming. If you use the builtin stream() or " | ||||
|                 "file_stream() methods, this upgrade will be be done for you." | ||||
|             ) | ||||
|  | ||||
|         super().__init__() | ||||
| @@ -203,6 +207,9 @@ class StreamingHTTPResponse(BaseHTTPResponse): | ||||
|             self.streaming_fn = None | ||||
|         await super().send(*args, **kwargs) | ||||
|  | ||||
|     async def eof(self): | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| class HTTPResponse(BaseHTTPResponse): | ||||
|     """ | ||||
| @@ -235,6 +242,15 @@ class HTTPResponse(BaseHTTPResponse): | ||||
|         self.headers = Header(headers or {}) | ||||
|         self._cookies = None | ||||
|  | ||||
|     async def eof(self): | ||||
|         await self.send("", True) | ||||
|  | ||||
|     async def __aenter__(self): | ||||
|         return self.send | ||||
|  | ||||
|     async def __aexit__(self, *_): | ||||
|         await self.eof() | ||||
|  | ||||
|  | ||||
| def empty( | ||||
|     status=204, headers: Optional[Dict[str, str]] = None | ||||
| @@ -396,7 +412,6 @@ async def file_stream( | ||||
|     mime_type: Optional[str] = None, | ||||
|     headers: Optional[Dict[str, str]] = None, | ||||
|     filename: Optional[str] = None, | ||||
|     chunked="deprecated", | ||||
|     _range: Optional[Range] = None, | ||||
| ) -> StreamingHTTPResponse: | ||||
|     """Return a streaming response object with file data. | ||||
| @@ -409,12 +424,6 @@ async def file_stream( | ||||
|     :param chunked: Deprecated | ||||
|     :param _range: | ||||
|     """ | ||||
|     if chunked != "deprecated": | ||||
|         warn( | ||||
|             "The chunked argument has been deprecated and will be " | ||||
|             "removed in v21.6" | ||||
|         ) | ||||
|  | ||||
|     headers = headers or {} | ||||
|     if filename: | ||||
|         headers.setdefault( | ||||
| @@ -453,6 +462,7 @@ async def file_stream( | ||||
|         status=status, | ||||
|         headers=headers, | ||||
|         content_type=mime_type, | ||||
|         ignore_deprecation_notice=True, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @@ -461,7 +471,6 @@ def stream( | ||||
|     status: int = 200, | ||||
|     headers: Optional[Dict[str, str]] = None, | ||||
|     content_type: str = "text/plain; charset=utf-8", | ||||
|     chunked="deprecated", | ||||
| ): | ||||
|     """Accepts an coroutine `streaming_fn` which can be used to | ||||
|     write chunks to a streaming response. Returns a `StreamingHTTPResponse`. | ||||
| @@ -482,17 +491,12 @@ def stream( | ||||
|     :param headers: Custom Headers. | ||||
|     :param chunked: Deprecated | ||||
|     """ | ||||
|     if chunked != "deprecated": | ||||
|         warn( | ||||
|             "The chunked argument has been deprecated and will be " | ||||
|             "removed in v21.6" | ||||
|         ) | ||||
|  | ||||
|     return StreamingHTTPResponse( | ||||
|         streaming_fn, | ||||
|         headers=headers, | ||||
|         content_type=content_type, | ||||
|         status=status, | ||||
|         ignore_deprecation_notice=True, | ||||
|     ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class Router(BaseRouter): | ||||
|             return self.resolve( | ||||
|                 path=path, | ||||
|                 method=method, | ||||
|                 extra={"host": host}, | ||||
|                 extra={"host": host} if host else None, | ||||
|             ) | ||||
|         except RoutingNotFound as e: | ||||
|             raise NotFound("Requested URL {} not found".format(e.path)) | ||||
| @@ -73,6 +73,7 @@ class Router(BaseRouter): | ||||
|         name: Optional[str] = None, | ||||
|         unquote: bool = False, | ||||
|         static: bool = False, | ||||
|         version_prefix: str = "/v", | ||||
|     ) -> Union[Route, List[Route]]: | ||||
|         """ | ||||
|         Add a handler to the router | ||||
| @@ -103,12 +104,12 @@ class Router(BaseRouter): | ||||
|         """ | ||||
|         if version is not None: | ||||
|             version = str(version).strip("/").lstrip("v") | ||||
|             uri = "/".join([f"/v{version}", uri.lstrip("/")]) | ||||
|             uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")]) | ||||
|  | ||||
|         params = dict( | ||||
|             path=uri, | ||||
|             handler=handler, | ||||
|             methods=methods, | ||||
|             methods=frozenset(map(str, methods)) if methods else None, | ||||
|             name=name, | ||||
|             strict=strict_slashes, | ||||
|             unquote=unquote, | ||||
| @@ -161,7 +162,7 @@ class Router(BaseRouter): | ||||
|  | ||||
|     @property | ||||
|     def routes_all(self): | ||||
|         return self.routes | ||||
|         return {route.parts: route for route in self.routes} | ||||
|  | ||||
|     @property | ||||
|     def routes_static(self): | ||||
|   | ||||
| @@ -39,7 +39,7 @@ from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows | ||||
| from sanic.config import Config | ||||
| from sanic.exceptions import RequestTimeout, ServiceUnavailable | ||||
| from sanic.http import Http, Stage | ||||
| from sanic.log import logger | ||||
| from sanic.log import error_logger, logger | ||||
| from sanic.models.protocol_types import TransportProtocol | ||||
| from sanic.request import Request | ||||
|  | ||||
| @@ -65,6 +65,7 @@ class ConnInfo: | ||||
|     __slots__ = ( | ||||
|         "client_port", | ||||
|         "client", | ||||
|         "client_ip", | ||||
|         "ctx", | ||||
|         "peername", | ||||
|         "server_port", | ||||
| @@ -78,6 +79,7 @@ class ConnInfo: | ||||
|         self.peername = None | ||||
|         self.server = self.client = "" | ||||
|         self.server_port = self.client_port = 0 | ||||
|         self.client_ip = "" | ||||
|         self.sockname = addr = transport.get_extra_info("sockname") | ||||
|         self.ssl: bool = bool(transport.get_extra_info("sslcontext")) | ||||
|  | ||||
| @@ -96,6 +98,7 @@ class ConnInfo: | ||||
|  | ||||
|         if isinstance(addr, tuple): | ||||
|             self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]" | ||||
|             self.client_ip = addr[0] | ||||
|             self.client_port = addr[1] | ||||
|  | ||||
|  | ||||
| @@ -122,7 +125,6 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         "response_timeout", | ||||
|         "keep_alive_timeout", | ||||
|         "request_max_size", | ||||
|         "request_buffer_queue_size", | ||||
|         "request_class", | ||||
|         "error_handler", | ||||
|         # enable or disable access log purpose | ||||
| @@ -165,9 +167,6 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         self.request_handler = self.app.handle_request | ||||
|         self.error_handler = self.app.error_handler | ||||
|         self.request_timeout = self.app.config.REQUEST_TIMEOUT | ||||
|         self.request_buffer_queue_size = ( | ||||
|             self.app.config.REQUEST_BUFFER_QUEUE_SIZE | ||||
|         ) | ||||
|         self.response_timeout = self.app.config.RESPONSE_TIMEOUT | ||||
|         self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT | ||||
|         self.request_max_size = self.app.config.REQUEST_MAX_SIZE | ||||
| @@ -199,11 +198,11 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         except CancelledError: | ||||
|             pass | ||||
|         except Exception: | ||||
|             logger.exception("protocol.connection_task uncaught") | ||||
|             error_logger.exception("protocol.connection_task uncaught") | ||||
|         finally: | ||||
|             if self.app.debug and self._http: | ||||
|                 ip = self.transport.get_extra_info("peername") | ||||
|                 logger.error( | ||||
|                 error_logger.error( | ||||
|                     "Connection lost before response written" | ||||
|                     f" @ {ip} {self._http.request}" | ||||
|                 ) | ||||
| @@ -212,7 +211,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             try: | ||||
|                 self.close() | ||||
|             except BaseException: | ||||
|                 logger.exception("Closing failed") | ||||
|                 error_logger.exception("Closing failed") | ||||
|  | ||||
|     async def receive_more(self): | ||||
|         """ | ||||
| @@ -234,11 +233,16 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             if stage is Stage.IDLE and duration > self.keep_alive_timeout: | ||||
|                 logger.debug("KeepAlive Timeout. Closing connection.") | ||||
|             elif stage is Stage.REQUEST and duration > self.request_timeout: | ||||
|                 logger.debug("Request Timeout. Closing connection.") | ||||
|                 self._http.exception = RequestTimeout("Request Timeout") | ||||
|             elif stage is Stage.HANDLER and self._http.upgrade_websocket: | ||||
|                 logger.debug("Handling websocket. Timeouts disabled.") | ||||
|                 return | ||||
|             elif ( | ||||
|                 stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED) | ||||
|                 and duration > self.response_timeout | ||||
|             ): | ||||
|                 logger.debug("Response Timeout. Closing connection.") | ||||
|                 self._http.exception = ServiceUnavailable("Response Timeout") | ||||
|             else: | ||||
|                 interval = ( | ||||
| @@ -253,7 +257,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|                 return | ||||
|             self._task.cancel() | ||||
|         except Exception: | ||||
|             logger.exception("protocol.check_timeouts") | ||||
|             error_logger.exception("protocol.check_timeouts") | ||||
|  | ||||
|     async def send(self, data): | ||||
|         """ | ||||
| @@ -299,7 +303,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             self.recv_buffer = bytearray() | ||||
|             self.conn_info = ConnInfo(self.transport, unix=self._unix) | ||||
|         except Exception: | ||||
|             logger.exception("protocol.connect_made") | ||||
|             error_logger.exception("protocol.connect_made") | ||||
|  | ||||
|     def connection_lost(self, exc): | ||||
|         try: | ||||
| @@ -308,7 +312,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             if self._task: | ||||
|                 self._task.cancel() | ||||
|         except Exception: | ||||
|             logger.exception("protocol.connection_lost") | ||||
|             error_logger.exception("protocol.connection_lost") | ||||
|  | ||||
|     def pause_writing(self): | ||||
|         self._can_write.clear() | ||||
| @@ -332,7 +336,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             if self._data_received: | ||||
|                 self._data_received.set() | ||||
|         except Exception: | ||||
|             logger.exception("protocol.data_received") | ||||
|             error_logger.exception("protocol.data_received") | ||||
|  | ||||
|  | ||||
| def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): | ||||
| @@ -551,7 +555,7 @@ def serve( | ||||
|     try: | ||||
|         http_server = loop.run_until_complete(server_coroutine) | ||||
|     except BaseException: | ||||
|         logger.exception("Unable to start server") | ||||
|         error_logger.exception("Unable to start server") | ||||
|         return | ||||
|  | ||||
|     trigger_events(after_start, loop) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import asyncio | ||||
| from inspect import isawaitable | ||||
| from typing import Any, Dict, List, Optional, Tuple, Union | ||||
|  | ||||
| from sanic_routing import BaseRouter, Route  # type: ignore | ||||
| from sanic_routing import BaseRouter, Route, RouteGroup  # type: ignore | ||||
| from sanic_routing.exceptions import NotFound  # type: ignore | ||||
| from sanic_routing.utils import path_to_parts  # type: ignore | ||||
|  | ||||
| @@ -20,17 +20,11 @@ RESERVED_NAMESPACES = ( | ||||
|  | ||||
|  | ||||
| class Signal(Route): | ||||
|     def get_handler(self, raw_path, method, _): | ||||
|         method = method or self.router.DEFAULT_METHOD | ||||
|         raw_path = raw_path.lstrip(self.router.delimiter) | ||||
|         try: | ||||
|             return self.handlers[raw_path][method] | ||||
|         except (IndexError, KeyError): | ||||
|             raise self.router.method_handler_exception( | ||||
|                 f"Method '{method}' not found on {self}", | ||||
|                 method=method, | ||||
|                 allowed_methods=set(self.methods[raw_path]), | ||||
|             ) | ||||
|     ... | ||||
|  | ||||
|  | ||||
| class SignalGroup(RouteGroup): | ||||
|     ... | ||||
|  | ||||
|  | ||||
| class SignalRouter(BaseRouter): | ||||
| @@ -38,6 +32,7 @@ class SignalRouter(BaseRouter): | ||||
|         super().__init__( | ||||
|             delimiter=".", | ||||
|             route_class=Signal, | ||||
|             group_class=SignalGroup, | ||||
|             stacking=True, | ||||
|         ) | ||||
|         self.ctx.loop = None | ||||
| @@ -49,7 +44,13 @@ class SignalRouter(BaseRouter): | ||||
|     ): | ||||
|         extra = condition or {} | ||||
|         try: | ||||
|             return self.resolve(f".{event}", extra=extra) | ||||
|             group, param_basket = self.find_route( | ||||
|                 f".{event}", | ||||
|                 self.DEFAULT_METHOD, | ||||
|                 self, | ||||
|                 {"__params__": {}, "__matches__": {}}, | ||||
|                 extra=extra, | ||||
|             ) | ||||
|         except NotFound: | ||||
|             message = "Could not find signal %s" | ||||
|             terms: List[Union[str, Optional[Dict[str, str]]]] = [event] | ||||
| @@ -58,16 +59,26 @@ class SignalRouter(BaseRouter): | ||||
|                 terms.append(extra) | ||||
|             raise NotFound(message % tuple(terms)) | ||||
|  | ||||
|         params = param_basket["__params__"] | ||||
|         if not params: | ||||
|             params = { | ||||
|                 param.name: param_basket["__matches__"][idx] | ||||
|                 for idx, param in group.params.items() | ||||
|             } | ||||
|  | ||||
|         return group, [route.handler for route in group], params | ||||
|  | ||||
|     async def _dispatch( | ||||
|         self, | ||||
|         event: str, | ||||
|         context: Optional[Dict[str, Any]] = None, | ||||
|         condition: Optional[Dict[str, str]] = None, | ||||
|     ) -> None: | ||||
|         signal, handlers, params = self.get(event, condition=condition) | ||||
|         group, handlers, params = self.get(event, condition=condition) | ||||
|  | ||||
|         signal_event = signal.ctx.event | ||||
|         signal_event.set() | ||||
|         events = [signal.ctx.event for signal in group] | ||||
|         for signal_event in events: | ||||
|             signal_event.set() | ||||
|         if context: | ||||
|             params.update(context) | ||||
|  | ||||
| @@ -78,7 +89,8 @@ class SignalRouter(BaseRouter): | ||||
|                     if isawaitable(maybe_coroutine): | ||||
|                         await maybe_coroutine | ||||
|         finally: | ||||
|             signal_event.clear() | ||||
|             for signal_event in events: | ||||
|                 signal_event.clear() | ||||
|  | ||||
|     async def dispatch( | ||||
|         self, | ||||
| @@ -116,7 +128,7 @@ class SignalRouter(BaseRouter): | ||||
|             handler, | ||||
|             requirements=condition, | ||||
|             name=name, | ||||
|             overwrite=True, | ||||
|             append=True, | ||||
|         )  # type: ignore | ||||
|  | ||||
|     def finalize(self, do_compile: bool = True): | ||||
| @@ -125,7 +137,7 @@ class SignalRouter(BaseRouter): | ||||
|         except RuntimeError: | ||||
|             raise RuntimeError("Cannot finalize signals outside of event loop") | ||||
|  | ||||
|         for signal in self.routes.values(): | ||||
|         for signal in self.routes: | ||||
|             signal.ctx.event = asyncio.Event() | ||||
|  | ||||
|         return super().finalize(do_compile=do_compile) | ||||
|   | ||||
							
								
								
									
										21
									
								
								sanic/simple.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								sanic/simple.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| from pathlib import Path | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.exceptions import SanicException | ||||
| from sanic.response import redirect | ||||
|  | ||||
|  | ||||
| def create_simple_server(directory: Path): | ||||
|     if not directory.is_dir(): | ||||
|         raise SanicException( | ||||
|             "Cannot setup Sanic Simple Server without a path to a directory" | ||||
|         ) | ||||
|  | ||||
|     app = Sanic("SimpleServer") | ||||
|     app.static("/", directory, name="main") | ||||
|  | ||||
|     @app.get("/") | ||||
|     def index(_): | ||||
|         return redirect(app.url_for("main", filename="index.html")) | ||||
|  | ||||
|     return app | ||||
| @@ -105,6 +105,7 @@ def load_module_from_file_location( | ||||
|             _mod_spec = spec_from_file_location( | ||||
|                 name, location, *args, **kwargs | ||||
|             ) | ||||
|             assert _mod_spec is not None  # type assertion for mypy | ||||
|             module = module_from_spec(_mod_spec) | ||||
|             _mod_spec.loader.exec_module(module)  # type: ignore | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,25 @@ | ||||
| from typing import Any, Callable, List | ||||
| from __future__ import annotations | ||||
|  | ||||
| from typing import ( | ||||
|     TYPE_CHECKING, | ||||
|     Any, | ||||
|     Callable, | ||||
|     Iterable, | ||||
|     List, | ||||
|     Optional, | ||||
|     Union, | ||||
| ) | ||||
| from warnings import warn | ||||
|  | ||||
| from sanic.constants import HTTP_METHODS | ||||
| from sanic.exceptions import InvalidUsage | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from sanic import Sanic | ||||
|     from sanic.blueprints import Blueprint | ||||
|  | ||||
|  | ||||
| class HTTPMethodView: | ||||
|     """Simple class based implementation of view for the sanic. | ||||
|     You should implement methods (get, post, put, patch, delete) for the class | ||||
| @@ -40,6 +56,31 @@ class HTTPMethodView: | ||||
|  | ||||
|     decorators: List[Callable[[Callable[..., Any]], Callable[..., Any]]] = [] | ||||
|  | ||||
|     def __init_subclass__( | ||||
|         cls, | ||||
|         attach: Optional[Union[Sanic, Blueprint]] = None, | ||||
|         uri: str = "", | ||||
|         methods: Iterable[str] = frozenset({"GET"}), | ||||
|         host: Optional[str] = None, | ||||
|         strict_slashes: Optional[bool] = None, | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         stream: bool = False, | ||||
|         version_prefix: str = "/v", | ||||
|     ) -> None: | ||||
|         if attach: | ||||
|             cls.attach( | ||||
|                 attach, | ||||
|                 uri=uri, | ||||
|                 methods=methods, | ||||
|                 host=host, | ||||
|                 strict_slashes=strict_slashes, | ||||
|                 version=version, | ||||
|                 name=name, | ||||
|                 stream=stream, | ||||
|                 version_prefix=version_prefix, | ||||
|             ) | ||||
|  | ||||
|     def dispatch_request(self, request, *args, **kwargs): | ||||
|         handler = getattr(self, request.method.lower(), None) | ||||
|         return handler(request, *args, **kwargs) | ||||
| @@ -65,6 +106,31 @@ class HTTPMethodView: | ||||
|         view.__name__ = cls.__name__ | ||||
|         return view | ||||
|  | ||||
|     @classmethod | ||||
|     def attach( | ||||
|         cls, | ||||
|         to: Union[Sanic, Blueprint], | ||||
|         uri: str, | ||||
|         methods: Iterable[str] = frozenset({"GET"}), | ||||
|         host: Optional[str] = None, | ||||
|         strict_slashes: Optional[bool] = None, | ||||
|         version: Optional[int] = None, | ||||
|         name: Optional[str] = None, | ||||
|         stream: bool = False, | ||||
|         version_prefix: str = "/v", | ||||
|     ) -> None: | ||||
|         to.add_route( | ||||
|             cls.as_view(), | ||||
|             uri=uri, | ||||
|             methods=methods, | ||||
|             host=host, | ||||
|             strict_slashes=strict_slashes, | ||||
|             version=version, | ||||
|             name=name, | ||||
|             stream=stream, | ||||
|             version_prefix=version_prefix, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def stream(func): | ||||
|     func.is_stream = True | ||||
| @@ -91,6 +157,11 @@ class CompositionView: | ||||
|     def __init__(self): | ||||
|         self.handlers = {} | ||||
|         self.name = self.__class__.__name__ | ||||
|         warn( | ||||
|             "CompositionView has been deprecated and will be removed in " | ||||
|             "v21.12. Please update your view to HTTPMethodView.", | ||||
|             DeprecationWarning, | ||||
|         ) | ||||
|  | ||||
|     def __name__(self): | ||||
|         return self.name | ||||
|   | ||||
| @@ -14,9 +14,13 @@ from websockets import (  # type: ignore | ||||
|     ConnectionClosed, | ||||
|     InvalidHandshake, | ||||
|     WebSocketCommonProtocol, | ||||
|     handshake, | ||||
| ) | ||||
|  | ||||
| # Despite the "legacy" namespace, the primary maintainer of websockets | ||||
| # committed to maintaining backwards-compatibility until 2026 and will | ||||
| # consider extending it if sanic continues depending on this module. | ||||
| from websockets.legacy import handshake | ||||
|  | ||||
| from sanic.exceptions import InvalidUsage | ||||
| from sanic.server import HttpProtocol | ||||
|  | ||||
| @@ -37,7 +41,7 @@ class WebSocketProtocol(HttpProtocol): | ||||
|         websocket_write_limit=2 ** 16, | ||||
|         websocket_ping_interval=20, | ||||
|         websocket_ping_timeout=20, | ||||
|         **kwargs | ||||
|         **kwargs, | ||||
|     ): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.websocket = None | ||||
| @@ -126,7 +130,9 @@ class WebSocketProtocol(HttpProtocol): | ||||
|             ping_interval=self.websocket_ping_interval, | ||||
|             ping_timeout=self.websocket_ping_timeout, | ||||
|         ) | ||||
|         # Following two lines are required for websockets 8.x | ||||
|         # we use WebSocketCommonProtocol because we don't want the handshake | ||||
|         # logic from WebSocketServerProtocol; however, we must tell it that | ||||
|         # we're running on the server side | ||||
|         self.websocket.is_client = False | ||||
|         self.websocket.side = "server" | ||||
|         self.websocket.subprotocol = subprotocol | ||||
| @@ -148,7 +154,7 @@ class WebSocketConnection: | ||||
|     ) -> None: | ||||
|         self._send = send | ||||
|         self._receive = receive | ||||
|         self.subprotocols = subprotocols or [] | ||||
|         self._subprotocols = subprotocols or [] | ||||
|  | ||||
|     async def send(self, data: Union[str, bytes], *args, **kwargs) -> None: | ||||
|         message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"} | ||||
| @@ -172,13 +178,28 @@ class WebSocketConnection: | ||||
|  | ||||
|     receive = recv | ||||
|  | ||||
|     async def accept(self) -> None: | ||||
|     async def accept(self, subprotocols: Optional[List[str]] = None) -> None: | ||||
|         subprotocol = None | ||||
|         if subprotocols: | ||||
|             for subp in subprotocols: | ||||
|                 if subp in self.subprotocols: | ||||
|                     subprotocol = subp | ||||
|                     break | ||||
|  | ||||
|         await self._send( | ||||
|             { | ||||
|                 "type": "websocket.accept", | ||||
|                 "subprotocol": ",".join(list(self.subprotocols)), | ||||
|                 "subprotocol": subprotocol, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     async def close(self) -> None: | ||||
|         pass | ||||
|  | ||||
|     @property | ||||
|     def subprotocols(self): | ||||
|         return self._subprotocols | ||||
|  | ||||
|     @subprotocols.setter | ||||
|     def subprotocols(self, subprotocols: Optional[List[str]] = None): | ||||
|         self._subprotocols = subprotocols or [] | ||||
|   | ||||
							
								
								
									
										6
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								setup.py
									
									
									
									
									
								
							| @@ -83,17 +83,17 @@ ujson = "ujson>=1.35" + env_dependency | ||||
| uvloop = "uvloop>=0.5.3" + env_dependency | ||||
|  | ||||
| requirements = [ | ||||
|     "sanic-routing", | ||||
|     "sanic-routing~=0.7", | ||||
|     "httptools>=0.0.10", | ||||
|     uvloop, | ||||
|     ujson, | ||||
|     "aiofiles>=0.6.0", | ||||
|     "websockets>=8.1,<9.0", | ||||
|     "websockets>=9.0", | ||||
|     "multidict>=5.0,<6.0", | ||||
| ] | ||||
|  | ||||
| tests_require = [ | ||||
|     "sanic-testing", | ||||
|     "sanic-testing>=0.7.0b1", | ||||
|     "pytest==5.2.1", | ||||
|     "multidict>=5.0,<6.0", | ||||
|     "gunicorn==20.0.4", | ||||
|   | ||||
| @@ -15,6 +15,7 @@ from sanic.constants import HTTP_METHODS | ||||
| from sanic.router import Router | ||||
|  | ||||
|  | ||||
| slugify = re.compile(r"[^a-zA-Z0-9_\-]") | ||||
| random.seed("Pack my box with five dozen liquor jugs.") | ||||
| Sanic.test_mode = True | ||||
|  | ||||
| @@ -140,5 +141,5 @@ def url_param_generator(): | ||||
|  | ||||
| @pytest.fixture(scope="function") | ||||
| def app(request): | ||||
|     app = Sanic(request.node.name) | ||||
|     app = Sanic(slugify.sub("-", request.node.name)) | ||||
|     return app | ||||
|   | ||||
							
								
								
									
										36
									
								
								tests/fake/server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								tests/fake/server.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import json | ||||
| import logging | ||||
|  | ||||
| from sanic import Sanic, text | ||||
| from sanic.log import LOGGING_CONFIG_DEFAULTS, logger | ||||
|  | ||||
|  | ||||
| LOGGING_CONFIG = {**LOGGING_CONFIG_DEFAULTS} | ||||
| LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s" | ||||
| LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG" | ||||
|  | ||||
| app = Sanic(__name__, log_config=LOGGING_CONFIG) | ||||
|  | ||||
|  | ||||
| @app.get("/") | ||||
| async def handler(request): | ||||
|     return text(request.ip) | ||||
|  | ||||
|  | ||||
| @app.before_server_start | ||||
| async def app_info_dump(app: Sanic, _): | ||||
|     app_data = { | ||||
|         "access_log": app.config.ACCESS_LOG, | ||||
|         "auto_reload": app.auto_reload, | ||||
|         "debug": app.debug, | ||||
|     } | ||||
|     logger.info(json.dumps(app_data)) | ||||
|  | ||||
|  | ||||
| @app.after_server_start | ||||
| async def shutdown(app: Sanic, _): | ||||
|     app.stop() | ||||
|  | ||||
|  | ||||
| def create_app(): | ||||
|     return app | ||||
| @@ -9,6 +9,7 @@ from unittest.mock import Mock, patch | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.config import Config | ||||
| from sanic.exceptions import SanicException | ||||
| from sanic.response import text | ||||
|  | ||||
| @@ -276,7 +277,7 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog): | ||||
|     assert response.status == 500 | ||||
|     assert "Mock SanicException" in response.text | ||||
|     assert ( | ||||
|         "sanic.root", | ||||
|         "sanic.error", | ||||
|         logging.ERROR, | ||||
|         f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'", | ||||
|     ) in caplog.record_tuples | ||||
| @@ -389,7 +390,7 @@ def test_app_no_registry_env(): | ||||
|  | ||||
|  | ||||
| def test_app_set_attribute_warning(app): | ||||
|     with pytest.warns(UserWarning) as record: | ||||
|     with pytest.warns(DeprecationWarning) as record: | ||||
|         app.foo = 1 | ||||
|  | ||||
|     assert len(record) == 1 | ||||
| @@ -412,3 +413,42 @@ def test_subclass_initialisation(): | ||||
|         pass | ||||
|  | ||||
|     CustomSanic("test_subclass_initialisation") | ||||
|  | ||||
|  | ||||
| def test_bad_custom_config(): | ||||
|     with pytest.raises( | ||||
|         SanicException, | ||||
|         match=( | ||||
|             "When instantiating Sanic with config, you cannot also pass " | ||||
|             "load_env or env_prefix" | ||||
|         ), | ||||
|     ): | ||||
|         Sanic("test", config=1, load_env=1) | ||||
|     with pytest.raises( | ||||
|         SanicException, | ||||
|         match=( | ||||
|             "When instantiating Sanic with config, you cannot also pass " | ||||
|             "load_env or env_prefix" | ||||
|         ), | ||||
|     ): | ||||
|         Sanic("test", config=1, env_prefix=1) | ||||
|  | ||||
|  | ||||
| def test_custom_config(): | ||||
|     class CustomConfig(Config): | ||||
|         ... | ||||
|  | ||||
|     config = CustomConfig() | ||||
|     app = Sanic("custom", config=config) | ||||
|  | ||||
|     assert app.config == config | ||||
|  | ||||
|  | ||||
| def test_custom_context(): | ||||
|     class CustomContext: | ||||
|         ... | ||||
|  | ||||
|     ctx = CustomContext() | ||||
|     app = Sanic("custom", ctx=ctx) | ||||
|  | ||||
|     assert app.ctx == ctx | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import asyncio | ||||
| import sys | ||||
|  | ||||
| from collections import deque, namedtuple | ||||
|  | ||||
| @@ -219,7 +218,7 @@ async def test_websocket_accept_with_no_subprotocols( | ||||
|  | ||||
|     message = message_stack.popleft() | ||||
|     assert message["type"] == "websocket.accept" | ||||
|     assert message["subprotocol"] == "" | ||||
|     assert message["subprotocol"] is None | ||||
|     assert "bytes" not in message | ||||
|  | ||||
|  | ||||
| @@ -228,7 +227,7 @@ async def test_websocket_accept_with_subprotocol(send, receive, message_stack): | ||||
|     subprotocols = ["graphql-ws"] | ||||
|  | ||||
|     ws = WebSocketConnection(send, receive, subprotocols) | ||||
|     await ws.accept() | ||||
|     await ws.accept(subprotocols) | ||||
|  | ||||
|     assert len(message_stack) == 1 | ||||
|  | ||||
| @@ -245,13 +244,13 @@ async def test_websocket_accept_with_multiple_subprotocols( | ||||
|     subprotocols = ["graphql-ws", "hello", "world"] | ||||
|  | ||||
|     ws = WebSocketConnection(send, receive, subprotocols) | ||||
|     await ws.accept() | ||||
|     await ws.accept(["hello", "world"]) | ||||
|  | ||||
|     assert len(message_stack) == 1 | ||||
|  | ||||
|     message = message_stack.popleft() | ||||
|     assert message["type"] == "websocket.accept" | ||||
|     assert message["subprotocol"] == "graphql-ws,hello,world" | ||||
|     assert message["subprotocol"] == "hello" | ||||
|     assert "bytes" not in message | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -41,3 +41,62 @@ def test_bp_repr_with_values(bp): | ||||
|         'Blueprint(name="my_bp", url_prefix="/foo", host="example.com", ' | ||||
|         "version=3, strict_slashes=True)" | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "name", | ||||
|     ( | ||||
|         "something", | ||||
|         "some-thing", | ||||
|         "some_thing", | ||||
|         "Something", | ||||
|         "SomeThing", | ||||
|         "Some-Thing", | ||||
|         "Some_Thing", | ||||
|         "SomeThing123", | ||||
|         "something123", | ||||
|         "some-thing123", | ||||
|         "some_thing123", | ||||
|         "some-Thing123", | ||||
|         "some_Thing123", | ||||
|     ), | ||||
| ) | ||||
| def test_names_okay(name): | ||||
|     app = Sanic(name) | ||||
|     bp = Blueprint(name) | ||||
|  | ||||
|     assert app.name == name | ||||
|     assert bp.name == name | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "name", | ||||
|     ( | ||||
|         "123something", | ||||
|         "some thing", | ||||
|         "something!", | ||||
|     ), | ||||
| ) | ||||
| def test_names_not_okay(name): | ||||
|     app_message = ( | ||||
|         f"Sanic instance named '{name}' uses a format that isdeprecated. " | ||||
|         "Starting in version 21.12, Sanic objects must be named only using " | ||||
|         "alphanumeric characters, _, or -." | ||||
|     ) | ||||
|     bp_message = ( | ||||
|         f"Blueprint instance named '{name}' uses a format that isdeprecated. " | ||||
|         "Starting in version 21.12, Blueprint objects must be named only using " | ||||
|         "alphanumeric characters, _, or -." | ||||
|     ) | ||||
|  | ||||
|     with pytest.warns(DeprecationWarning) as app_e: | ||||
|         app = Sanic(name) | ||||
|  | ||||
|     with pytest.warns(DeprecationWarning) as bp_e: | ||||
|         bp = Blueprint(name) | ||||
|  | ||||
|     assert app.name == name | ||||
|     assert bp.name == name | ||||
|  | ||||
|     assert app_e[0].message.args[0] == app_message | ||||
|     assert bp_e[0].message.args[0] == bp_message | ||||
|   | ||||
| @@ -200,7 +200,7 @@ def test_bp_group_as_nested_group(): | ||||
|     blueprint_group_1 = Blueprint.group( | ||||
|         Blueprint.group(blueprint_1, blueprint_2) | ||||
|     ) | ||||
|     assert len(blueprint_group_1) == 2 | ||||
|     assert len(blueprint_group_1) == 1 | ||||
|  | ||||
|  | ||||
| def test_blueprint_group_insert(): | ||||
| @@ -215,6 +215,61 @@ def test_blueprint_group_insert(): | ||||
|     group.insert(0, blueprint_1) | ||||
|     group.insert(0, blueprint_2) | ||||
|     group.insert(0, blueprint_3) | ||||
|     assert group.blueprints[1].strict_slashes is False | ||||
|     assert group.blueprints[2].strict_slashes is True | ||||
|     assert group.blueprints[0].url_prefix == "/test" | ||||
|  | ||||
|     @blueprint_1.route("/") | ||||
|     def blueprint_1_default_route(request): | ||||
|         return text("BP1_OK") | ||||
|  | ||||
|     @blueprint_2.route("/") | ||||
|     def blueprint_2_default_route(request): | ||||
|         return text("BP2_OK") | ||||
|  | ||||
|     @blueprint_3.route("/") | ||||
|     def blueprint_3_default_route(request): | ||||
|         return text("BP3_OK") | ||||
|  | ||||
|     app = Sanic("PropTest") | ||||
|     app.blueprint(group) | ||||
|     app.router.finalize() | ||||
|  | ||||
|     routes = [(route.path, route.strict) for route in app.router.routes] | ||||
|  | ||||
|     assert len(routes) == 3 | ||||
|     assert ("v1/test/bp1/", True) in routes | ||||
|     assert ("v1.3/test/bp2", False) in routes | ||||
|     assert ("v1.3/test", False) in routes | ||||
|  | ||||
|  | ||||
| def test_bp_group_properties(): | ||||
|     blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") | ||||
|     blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") | ||||
|     group = Blueprint.group( | ||||
|         blueprint_1, | ||||
|         blueprint_2, | ||||
|         version=1, | ||||
|         version_prefix="/api/v", | ||||
|         url_prefix="/grouped", | ||||
|         strict_slashes=True, | ||||
|     ) | ||||
|     primary = Blueprint.group(group, url_prefix="/primary") | ||||
|  | ||||
|     @blueprint_1.route("/") | ||||
|     def blueprint_1_default_route(request): | ||||
|         return text("BP1_OK") | ||||
|  | ||||
|     @blueprint_2.route("/") | ||||
|     def blueprint_2_default_route(request): | ||||
|         return text("BP2_OK") | ||||
|  | ||||
|     app = Sanic("PropTest") | ||||
|     app.blueprint(group) | ||||
|     app.blueprint(primary) | ||||
|     app.router.finalize() | ||||
|  | ||||
|     routes = [route.path for route in app.router.routes] | ||||
|  | ||||
|     assert len(routes) == 4 | ||||
|     assert "api/v1/grouped/bp1/" in routes | ||||
|     assert "api/v1/grouped/bp2/" in routes | ||||
|     assert "api/v1/primary/grouped/bp1" in routes | ||||
|     assert "api/v1/primary/grouped/bp2" in routes | ||||
|   | ||||
| @@ -1028,7 +1028,7 @@ def test_blueprint_registered_multiple_apps(): | ||||
|  | ||||
| def test_bp_set_attribute_warning(): | ||||
|     bp = Blueprint("bp") | ||||
|     with pytest.warns(UserWarning) as record: | ||||
|     with pytest.warns(DeprecationWarning) as record: | ||||
|         bp.foo = 1 | ||||
|  | ||||
|     assert len(record) == 1 | ||||
|   | ||||
							
								
								
									
										133
									
								
								tests/test_cli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								tests/test_cli.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| import json | ||||
| import subprocess | ||||
|  | ||||
| from pathlib import Path | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic_routing import __version__ as __routing_version__ | ||||
|  | ||||
| from sanic import __version__ | ||||
| from sanic.config import BASE_LOGO | ||||
|  | ||||
|  | ||||
| def capture(command): | ||||
|     proc = subprocess.Popen( | ||||
|         command, | ||||
|         stdout=subprocess.PIPE, | ||||
|         stderr=subprocess.PIPE, | ||||
|         cwd=Path(__file__).parent, | ||||
|     ) | ||||
|     try: | ||||
|         out, err = proc.communicate(timeout=0.5) | ||||
|     except subprocess.TimeoutExpired: | ||||
|         proc.kill() | ||||
|         out, err = proc.communicate() | ||||
|     return out, err, proc.returncode | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "appname", | ||||
|     ( | ||||
|         "fake.server.app", | ||||
|         "fake.server:app", | ||||
|         "fake.server:create_app()", | ||||
|         "fake.server.create_app()", | ||||
|     ), | ||||
| ) | ||||
| def test_server_run(appname): | ||||
|     command = ["sanic", appname] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     firstline = lines[6] | ||||
|  | ||||
|     assert exitcode != 1 | ||||
|     assert firstline == b"Goin' Fast @ http://127.0.0.1:8000" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "cmd", | ||||
|     ( | ||||
|         ("--host=localhost", "--port=9999"), | ||||
|         ("-H", "localhost", "-p", "9999"), | ||||
|     ), | ||||
| ) | ||||
| def test_host_port(cmd): | ||||
|     command = ["sanic", "fake.server.app", *cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|     firstline = lines[6] | ||||
|  | ||||
|     assert exitcode != 1 | ||||
|     assert firstline == b"Goin' Fast @ http://localhost:9999" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "num,cmd", | ||||
|     ( | ||||
|         (1, (f"--workers={1}",)), | ||||
|         (2, (f"--workers={2}",)), | ||||
|         (4, (f"--workers={4}",)), | ||||
|         (1, ("-w", "1")), | ||||
|         (2, ("-w", "2")), | ||||
|         (4, ("-w", "4")), | ||||
|     ), | ||||
| ) | ||||
| def test_num_workers(num, cmd): | ||||
|     command = ["sanic", "fake.server.app", *cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     worker_lines = [line for line in lines if b"worker" in line] | ||||
|     assert exitcode != 1 | ||||
|     assert len(worker_lines) == num * 2 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("cmd", ("--debug", "-d")) | ||||
| def test_debug(cmd): | ||||
|     command = ["sanic", "fake.server.app", cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     app_info = lines[9] | ||||
|     info = json.loads(app_info) | ||||
|  | ||||
|     assert (b"\n".join(lines[:6])).decode("utf-8") == BASE_LOGO | ||||
|     assert info["debug"] is True | ||||
|     assert info["auto_reload"] is True | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("cmd", ("--auto-reload", "-r")) | ||||
| def test_auto_reload(cmd): | ||||
|     command = ["sanic", "fake.server.app", cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     app_info = lines[9] | ||||
|     info = json.loads(app_info) | ||||
|  | ||||
|     assert info["debug"] is False | ||||
|     assert info["auto_reload"] is True | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "cmd,expected", (("--access-log", True), ("--no-access-log", False)) | ||||
| ) | ||||
| def test_access_logs(cmd, expected): | ||||
|     command = ["sanic", "fake.server.app", cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     lines = out.split(b"\n") | ||||
|  | ||||
|     app_info = lines[9] | ||||
|     info = json.loads(app_info) | ||||
|  | ||||
|     assert info["access_log"] is expected | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("cmd", ("--version", "-v")) | ||||
| def test_version(cmd): | ||||
|     command = ["sanic", cmd] | ||||
|     out, err, exitcode = capture(command) | ||||
|     version_string = f"Sanic {__version__}; Routing {__routing_version__}\n" | ||||
|  | ||||
|     assert out == version_string.encode("utf-8") | ||||
| @@ -59,14 +59,14 @@ def test_load_from_object_string_exception(app): | ||||
|         app.config.load("test_config.Config.test") | ||||
|  | ||||
|  | ||||
| def test_auto_load_env(): | ||||
| def test_auto_env_prefix(): | ||||
|     environ["SANIC_TEST_ANSWER"] = "42" | ||||
|     app = Sanic(name=__name__) | ||||
|     assert app.config.TEST_ANSWER == 42 | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| def test_auto_load_bool_env(): | ||||
| def test_auto_bool_env_prefix(): | ||||
|     environ["SANIC_TEST_ANSWER"] = "True" | ||||
|     app = Sanic(name=__name__) | ||||
|     assert app.config.TEST_ANSWER is True | ||||
| @@ -80,6 +80,12 @@ def test_dont_load_env(): | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("load_env", [None, False, "", "MYAPP_"]) | ||||
| def test_load_env_deprecation(load_env): | ||||
|     with pytest.warns(DeprecationWarning, match=r"21\.12"): | ||||
|         _ = Sanic(name=__name__, load_env=load_env) | ||||
|  | ||||
|  | ||||
| def test_load_env_prefix(): | ||||
|     environ["MYAPP_TEST_ANSWER"] = "42" | ||||
|     app = Sanic(name=__name__, load_env="MYAPP_") | ||||
| @@ -87,6 +93,14 @@ def test_load_env_prefix(): | ||||
|     del environ["MYAPP_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("env_prefix", [None, ""]) | ||||
| def test_empty_load_env_prefix(env_prefix): | ||||
|     environ["SANIC_TEST_ANSWER"] = "42" | ||||
|     app = Sanic(name=__name__, env_prefix=env_prefix) | ||||
|     assert getattr(app.config, "TEST_ANSWER", None) is None | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| def test_load_env_prefix_float_values(): | ||||
|     environ["MYAPP_TEST_ROI"] = "2.3" | ||||
|     app = Sanic(name=__name__, load_env="MYAPP_") | ||||
| @@ -101,6 +115,27 @@ def test_load_env_prefix_string_value(): | ||||
|     del environ["MYAPP_TEST_TOKEN"] | ||||
|  | ||||
|  | ||||
| def test_env_prefix(): | ||||
|     environ["MYAPP_TEST_ANSWER"] = "42" | ||||
|     app = Sanic(name=__name__, env_prefix="MYAPP_") | ||||
|     assert app.config.TEST_ANSWER == 42 | ||||
|     del environ["MYAPP_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| def test_env_prefix_float_values(): | ||||
|     environ["MYAPP_TEST_ROI"] = "2.3" | ||||
|     app = Sanic(name=__name__, env_prefix="MYAPP_") | ||||
|     assert app.config.TEST_ROI == 2.3 | ||||
|     del environ["MYAPP_TEST_ROI"] | ||||
|  | ||||
|  | ||||
| def test_env_prefix_string_value(): | ||||
|     environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken" | ||||
|     app = Sanic(name=__name__, env_prefix="MYAPP_") | ||||
|     assert app.config.TEST_TOKEN == "somerandomtesttoken" | ||||
|     del environ["MYAPP_TEST_TOKEN"] | ||||
|  | ||||
|  | ||||
| def test_load_from_file(app): | ||||
|     config = dedent( | ||||
|         """ | ||||
|   | ||||
							
								
								
									
										28
									
								
								tests/test_constants.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								tests/test_constants.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| from crypt import methods | ||||
|  | ||||
| from sanic import text | ||||
| from sanic.constants import HTTP_METHODS, HTTPMethod | ||||
|  | ||||
|  | ||||
| def test_string_compat(): | ||||
|     assert "GET" == HTTPMethod.GET | ||||
|     assert "GET" in HTTP_METHODS | ||||
|     assert "get" == HTTPMethod.GET | ||||
|     assert "get" in HTTP_METHODS | ||||
|  | ||||
|     assert HTTPMethod.GET.lower() == "get" | ||||
|     assert HTTPMethod.GET.upper() == "GET" | ||||
|  | ||||
|  | ||||
| def test_use_in_routes(app): | ||||
|     @app.route("/", methods=[HTTPMethod.GET, HTTPMethod.POST]) | ||||
|     def handler(_): | ||||
|         return text("It works") | ||||
|  | ||||
|     _, response = app.test_client.get("/") | ||||
|     assert response.status == 200 | ||||
|     assert response.text == "It works" | ||||
|  | ||||
|     _, response = app.test_client.post("/") | ||||
|     assert response.status == 200 | ||||
|     assert response.text == "It works" | ||||
| @@ -1,3 +1,5 @@ | ||||
| import warnings | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from bs4 import BeautifulSoup | ||||
| @@ -7,6 +9,7 @@ from sanic.exceptions import ( | ||||
|     Forbidden, | ||||
|     InvalidUsage, | ||||
|     NotFound, | ||||
|     SanicException, | ||||
|     ServerError, | ||||
|     Unauthorized, | ||||
|     abort, | ||||
| @@ -68,16 +71,19 @@ def exception_app(): | ||||
|  | ||||
|     @app.route("/abort/401") | ||||
|     def handler_401_error(request): | ||||
|         abort(401) | ||||
|         raise SanicException(status_code=401) | ||||
|  | ||||
|     @app.route("/abort") | ||||
|     def handler_500_error(request): | ||||
|         raise SanicException(status_code=500) | ||||
|  | ||||
|     @app.route("/old_abort") | ||||
|     def handler_old_abort_error(request): | ||||
|         abort(500) | ||||
|         return text("OK") | ||||
|  | ||||
|     @app.route("/abort/message") | ||||
|     def handler_abort_message(request): | ||||
|         abort(500, message="Abort") | ||||
|         raise SanicException(message="Custom Message", status_code=500) | ||||
|  | ||||
|     @app.route("/divide_by_zero") | ||||
|     def handle_unhandled_exception(request): | ||||
| @@ -208,14 +214,21 @@ def test_exception_in_exception_handler_debug_on(exception_app): | ||||
|     assert response.body.startswith(b"Exception raised in exception ") | ||||
|  | ||||
|  | ||||
| def test_abort(exception_app): | ||||
|     """Test the abort function""" | ||||
| def test_sanic_exception(exception_app): | ||||
|     """Test sanic exceptions are handled""" | ||||
|     request, response = exception_app.test_client.get("/abort/401") | ||||
|     assert response.status == 401 | ||||
|  | ||||
|     request, response = exception_app.test_client.get("/abort") | ||||
|     assert response.status == 500 | ||||
|     # check fallback message | ||||
|     assert "Internal Server Error" in response.text | ||||
|  | ||||
|     request, response = exception_app.test_client.get("/abort/message") | ||||
|     assert response.status == 500 | ||||
|     assert "Abort" in response.text | ||||
|     assert "Custom Message" in response.text | ||||
|  | ||||
|     with warnings.catch_warnings(record=True) as w: | ||||
|         request, response = exception_app.test_client.get("/old_abort") | ||||
|     assert response.status == 500 | ||||
|     assert len(w) == 1 and "deprecated" in w[0].message.args[0] | ||||
|   | ||||
| @@ -7,6 +7,13 @@ from sanic.exceptions import PayloadTooLarge | ||||
| from sanic.http import Http | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def raised_ceiling(): | ||||
|     Http.HEADER_CEILING = 32_768 | ||||
|     yield | ||||
|     Http.HEADER_CEILING = 16_384 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "input, expected", | ||||
|     [ | ||||
| @@ -76,15 +83,75 @@ async def test_header_size_exceeded(): | ||||
|         recv_buffer += b"123" | ||||
|  | ||||
|     protocol = Mock() | ||||
|     Http.set_header_max_size(1) | ||||
|     http = Http(protocol) | ||||
|     http._receive_more = _receive_more | ||||
|     http.request_max_size = 1 | ||||
|     http.recv_buffer = recv_buffer | ||||
|  | ||||
|     with pytest.raises(PayloadTooLarge): | ||||
|         await http.http1_request_header() | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_header_size_increased_okay(): | ||||
|     recv_buffer = bytearray() | ||||
|  | ||||
|     async def _receive_more(): | ||||
|         nonlocal recv_buffer | ||||
|         recv_buffer += b"123" | ||||
|  | ||||
|     protocol = Mock() | ||||
|     Http.set_header_max_size(12_288) | ||||
|     http = Http(protocol) | ||||
|     http._receive_more = _receive_more | ||||
|     http.recv_buffer = recv_buffer | ||||
|  | ||||
|     with pytest.raises(PayloadTooLarge): | ||||
|         await http.http1_request_header() | ||||
|  | ||||
|     assert len(recv_buffer) == 12_291 | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_header_size_exceeded_maxed_out(): | ||||
|     recv_buffer = bytearray() | ||||
|  | ||||
|     async def _receive_more(): | ||||
|         nonlocal recv_buffer | ||||
|         recv_buffer += b"123" | ||||
|  | ||||
|     protocol = Mock() | ||||
|     Http.set_header_max_size(18_432) | ||||
|     http = Http(protocol) | ||||
|     http._receive_more = _receive_more | ||||
|     http.recv_buffer = recv_buffer | ||||
|  | ||||
|     with pytest.raises(PayloadTooLarge): | ||||
|         await http.http1_request_header() | ||||
|  | ||||
|     assert len(recv_buffer) == 16_389 | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_header_size_exceeded_raised_ceiling(raised_ceiling): | ||||
|     recv_buffer = bytearray() | ||||
|  | ||||
|     async def _receive_more(): | ||||
|         nonlocal recv_buffer | ||||
|         recv_buffer += b"123" | ||||
|  | ||||
|     protocol = Mock() | ||||
|     http = Http(protocol) | ||||
|     Http.set_header_max_size(65_536) | ||||
|     http._receive_more = _receive_more | ||||
|     http.recv_buffer = recv_buffer | ||||
|  | ||||
|     with pytest.raises(PayloadTooLarge): | ||||
|         await http.http1_request_header() | ||||
|  | ||||
|     assert len(recv_buffer) == 32_772 | ||||
|  | ||||
|  | ||||
| def test_raw_headers(app): | ||||
|     app.route("/")(lambda _: text("")) | ||||
|     request, _ = app.test_client.get( | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import asyncio | ||||
| import platform | ||||
|  | ||||
| from asyncio import sleep as aio_sleep | ||||
| from json import JSONDecodeError | ||||
| @@ -241,7 +242,9 @@ def test_keep_alive_timeout_reuse(): | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     bool(environ.get("SANIC_NO_UVLOOP")) or OS_IS_WINDOWS, | ||||
|     bool(environ.get("SANIC_NO_UVLOOP")) | ||||
|     or OS_IS_WINDOWS | ||||
|     or platform.system() != "Linux", | ||||
|     reason="Not testable with current client", | ||||
| ) | ||||
| def test_keep_alive_client_timeout(): | ||||
|   | ||||
| @@ -113,9 +113,9 @@ def test_logging_pass_customer_logconfig(): | ||||
| def test_log_connection_lost(app, debug, monkeypatch): | ||||
|     """ Should not log Connection lost exception on non debug """ | ||||
|     stream = StringIO() | ||||
|     root = logging.getLogger("sanic.root") | ||||
|     root.addHandler(logging.StreamHandler(stream)) | ||||
|     monkeypatch.setattr(sanic.server, "logger", root) | ||||
|     error = logging.getLogger("sanic.error") | ||||
|     error.addHandler(logging.StreamHandler(stream)) | ||||
|     monkeypatch.setattr(sanic.server, "error_logger", error) | ||||
|  | ||||
|     @app.route("/conn_lost") | ||||
|     async def conn_lost(request): | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import logging | ||||
| from asyncio import CancelledError | ||||
| from itertools import count | ||||
|  | ||||
| from sanic.exceptions import NotFound, SanicException | ||||
| from sanic.exceptions import NotFound | ||||
| from sanic.request import Request | ||||
| from sanic.response import HTTPResponse, text | ||||
|  | ||||
| @@ -156,7 +156,7 @@ def test_middleware_response_raise_cancelled_error(app, caplog): | ||||
|  | ||||
|         assert response.status == 503 | ||||
|         assert ( | ||||
|             "sanic.root", | ||||
|             "sanic.error", | ||||
|             logging.ERROR, | ||||
|             "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", | ||||
|         ) not in caplog.record_tuples | ||||
| @@ -174,7 +174,7 @@ def test_middleware_response_raise_exception(app, caplog): | ||||
|     assert response.status == 404 | ||||
|     # 404 errors are not logged | ||||
|     assert ( | ||||
|         "sanic.root", | ||||
|         "sanic.error", | ||||
|         logging.ERROR, | ||||
|         "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", | ||||
|     ) not in caplog.record_tuples | ||||
|   | ||||
| @@ -209,13 +209,13 @@ def test_named_static_routes(): | ||||
|         return text("OK2") | ||||
|  | ||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)][0].name == "app.route_test" | ||||
|     assert app.url_for("route_test") == "/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler1") | ||||
|  | ||||
|     assert app.router.routes_all[("pizazz",)].name == "app.route_pizazz" | ||||
|     assert app.router.routes_static[("pizazz",)].name == "app.route_pizazz" | ||||
|     assert app.router.routes_static[("pizazz",)][0].name == "app.route_pizazz" | ||||
|     assert app.url_for("route_pizazz") == "/pizazz" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler2") | ||||
| @@ -234,7 +234,7 @@ def test_named_dynamic_route(): | ||||
|         app.router.routes_all[ | ||||
|             ( | ||||
|                 "folder", | ||||
|                 "<name>", | ||||
|                 "<name:str>", | ||||
|             ) | ||||
|         ].name | ||||
|         == "app.route_dynamic" | ||||
| @@ -347,13 +347,13 @@ def test_static_add_named_route(): | ||||
|     app.add_route(handler2, "/test2", name="route_test2") | ||||
|  | ||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)][0].name == "app.route_test" | ||||
|     assert app.url_for("route_test") == "/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler1") | ||||
|  | ||||
|     assert app.router.routes_all[("test2",)].name == "app.route_test2" | ||||
|     assert app.router.routes_static[("test2",)].name == "app.route_test2" | ||||
|     assert app.router.routes_static[("test2",)][0].name == "app.route_test2" | ||||
|     assert app.url_for("route_test2") == "/test2" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler2") | ||||
| @@ -369,7 +369,8 @@ def test_dynamic_add_named_route(): | ||||
|  | ||||
|     app.add_route(handler, "/folder/<name>", name="route_dynamic") | ||||
|     assert ( | ||||
|         app.router.routes_all[("folder", "<name>")].name == "app.route_dynamic" | ||||
|         app.router.routes_all[("folder", "<name:str>")].name | ||||
|         == "app.route_dynamic" | ||||
|     ) | ||||
|     assert app.url_for("route_dynamic", name="test") == "/folder/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|   | ||||
							
								
								
									
										105
									
								
								tests/test_pipelining.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								tests/test_pipelining.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| from httpx import AsyncByteStream | ||||
| from sanic_testing.reusable import ReusableClient | ||||
|  | ||||
| from sanic.response import json, text | ||||
|  | ||||
|  | ||||
| def test_no_body_requests(app): | ||||
|     @app.get("/") | ||||
|     async def handler(request): | ||||
|         return json( | ||||
|             { | ||||
|                 "request_id": str(request.id), | ||||
|                 "connection_id": id(request.conn_info), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     client = ReusableClient(app, port=1234) | ||||
|  | ||||
|     with client: | ||||
|         _, response1 = client.get("/") | ||||
|         _, response2 = client.get("/") | ||||
|  | ||||
|     assert response1.status == response2.status == 200 | ||||
|     assert response1.json["request_id"] != response2.json["request_id"] | ||||
|     assert response1.json["connection_id"] == response2.json["connection_id"] | ||||
|  | ||||
|  | ||||
| def test_json_body_requests(app): | ||||
|     @app.post("/") | ||||
|     async def handler(request): | ||||
|         return json( | ||||
|             { | ||||
|                 "request_id": str(request.id), | ||||
|                 "connection_id": id(request.conn_info), | ||||
|                 "foo": request.json.get("foo"), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     client = ReusableClient(app, port=1234) | ||||
|  | ||||
|     with client: | ||||
|         _, response1 = client.post("/", json={"foo": True}) | ||||
|         _, response2 = client.post("/", json={"foo": True}) | ||||
|  | ||||
|     assert response1.status == response2.status == 200 | ||||
|     assert response1.json["foo"] is response2.json["foo"] is True | ||||
|     assert response1.json["request_id"] != response2.json["request_id"] | ||||
|     assert response1.json["connection_id"] == response2.json["connection_id"] | ||||
|  | ||||
|  | ||||
| def test_streaming_body_requests(app): | ||||
|     @app.post("/", stream=True) | ||||
|     async def handler(request): | ||||
|         data = [part.decode("utf-8") async for part in request.stream] | ||||
|         return json( | ||||
|             { | ||||
|                 "request_id": str(request.id), | ||||
|                 "connection_id": id(request.conn_info), | ||||
|                 "data": data, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     data = ["hello", "world"] | ||||
|  | ||||
|     class Data(AsyncByteStream): | ||||
|         def __init__(self, data): | ||||
|             self.data = data | ||||
|  | ||||
|         async def __aiter__(self): | ||||
|             for value in self.data: | ||||
|                 yield value.encode("utf-8") | ||||
|  | ||||
|     client = ReusableClient(app, port=1234) | ||||
|  | ||||
|     with client: | ||||
|         _, response1 = client.post("/", data=Data(data)) | ||||
|         _, response2 = client.post("/", data=Data(data)) | ||||
|  | ||||
|     assert response1.status == response2.status == 200 | ||||
|     assert response1.json["data"] == response2.json["data"] == data | ||||
|     assert response1.json["request_id"] != response2.json["request_id"] | ||||
|     assert response1.json["connection_id"] == response2.json["connection_id"] | ||||
|  | ||||
|  | ||||
| def test_bad_headers(app): | ||||
|     @app.get("/") | ||||
|     async def handler(request): | ||||
|         return text("") | ||||
|  | ||||
|     @app.on_response | ||||
|     async def reqid(request, response): | ||||
|         response.headers["x-request-id"] = request.id | ||||
|  | ||||
|     client = ReusableClient(app, port=1234) | ||||
|     bad_headers = {"bad": "bad" * 5_000} | ||||
|  | ||||
|     with client: | ||||
|         _, response1 = client.get("/") | ||||
|         _, response2 = client.get("/", headers=bad_headers) | ||||
|  | ||||
|     assert response1.status == 200 | ||||
|     assert response2.status == 413 | ||||
|     assert ( | ||||
|         response1.headers["x-request-id"] != response2.headers["x-request-id"] | ||||
|     ) | ||||
| @@ -1,4 +1,4 @@ | ||||
| from urllib.parse import quote, unquote | ||||
| from urllib.parse import quote | ||||
|  | ||||
| import pytest | ||||
|  | ||||
|   | ||||
| @@ -23,6 +23,8 @@ try: | ||||
| except ImportError: | ||||
|     flags = 0 | ||||
|  | ||||
| TIMER_DELAY = 2 | ||||
|  | ||||
|  | ||||
| def terminate(proc): | ||||
|     if flags: | ||||
| @@ -56,6 +58,40 @@ def write_app(filename, **runargs): | ||||
|     return text | ||||
|  | ||||
|  | ||||
| def write_json_config_app(filename, jsonfile, **runargs): | ||||
|     with open(filename, "w") as f: | ||||
|         f.write( | ||||
|             dedent( | ||||
|                 f"""\ | ||||
|             import os | ||||
|             from sanic import Sanic | ||||
|             import json | ||||
|  | ||||
|             app = Sanic(__name__) | ||||
|             with open("{jsonfile}", "r") as f: | ||||
|                 config = json.load(f) | ||||
|             app.config.update_config(config) | ||||
|  | ||||
|             app.route("/")(lambda x: x) | ||||
|  | ||||
|             @app.listener("after_server_start") | ||||
|             def complete(*args): | ||||
|                 print("complete", os.getpid(), app.config.FOO) | ||||
|  | ||||
|             if __name__ == "__main__": | ||||
|                 app.run(**{runargs!r}) | ||||
|             """ | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def write_file(filename): | ||||
|     text = secrets.token_urlsafe() | ||||
|     with open(filename, "w") as f: | ||||
|         f.write(f"""{{"FOO": "{text}"}}""") | ||||
|     return text | ||||
|  | ||||
|  | ||||
| def scanner(proc): | ||||
|     for line in proc.stdout: | ||||
|         line = line.decode().strip() | ||||
| @@ -90,9 +126,10 @@ async def test_reloader_live(runargs, mode): | ||||
|     with TemporaryDirectory() as tmpdir: | ||||
|         filename = os.path.join(tmpdir, "reloader.py") | ||||
|         text = write_app(filename, **runargs) | ||||
|         proc = Popen(argv[mode], cwd=tmpdir, stdout=PIPE, creationflags=flags) | ||||
|         command = argv[mode] | ||||
|         proc = Popen(command, cwd=tmpdir, stdout=PIPE, creationflags=flags) | ||||
|         try: | ||||
|             timeout = Timer(5, terminate, [proc]) | ||||
|             timeout = Timer(TIMER_DELAY, terminate, [proc]) | ||||
|             timeout.start() | ||||
|             # Python apparently keeps using the old source sometimes if | ||||
|             # we don't sleep before rewrite (pycache timestamp problem?) | ||||
| @@ -107,3 +144,40 @@ async def test_reloader_live(runargs, mode): | ||||
|             terminate(proc) | ||||
|             with suppress(TimeoutExpired): | ||||
|                 proc.wait(timeout=3) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "runargs, mode", | ||||
|     [ | ||||
|         (dict(port=42102, auto_reload=True), "script"), | ||||
|         (dict(port=42103, debug=True), "module"), | ||||
|         ({}, "sanic"), | ||||
|     ], | ||||
| ) | ||||
| async def test_reloader_live_with_dir(runargs, mode): | ||||
|     with TemporaryDirectory() as tmpdir: | ||||
|         filename = os.path.join(tmpdir, "reloader.py") | ||||
|         config_file = os.path.join(tmpdir, "config.json") | ||||
|         runargs["reload_dir"] = tmpdir | ||||
|         write_json_config_app(filename, config_file, **runargs) | ||||
|         text = write_file(config_file) | ||||
|         command = argv[mode] | ||||
|         if mode == "sanic": | ||||
|             command += ["--reload-dir", tmpdir] | ||||
|         proc = Popen(command, cwd=tmpdir, stdout=PIPE, creationflags=flags) | ||||
|         try: | ||||
|             timeout = Timer(TIMER_DELAY, terminate, [proc]) | ||||
|             timeout.start() | ||||
|             # Python apparently keeps using the old source sometimes if | ||||
|             # we don't sleep before rewrite (pycache timestamp problem?) | ||||
|             sleep(1) | ||||
|             line = scanner(proc) | ||||
|             assert text in next(line) | ||||
|             # Edit source code and try again | ||||
|             text = write_file(config_file) | ||||
|             assert text in next(line) | ||||
|         finally: | ||||
|             timeout.cancel() | ||||
|             terminate(proc) | ||||
|             with suppress(TimeoutExpired): | ||||
|                 proc.wait(timeout=3) | ||||
|   | ||||
| @@ -20,6 +20,7 @@ def test_request_id_generates_from_request(monkeypatch): | ||||
|     monkeypatch.setattr(Request, "generate_id", Mock()) | ||||
|     Request.generate_id.return_value = 1 | ||||
|     request = Request(b"/", {}, None, "GET", None, Mock()) | ||||
|     request.app.config.REQUEST_ID_HEADER = "foo" | ||||
|  | ||||
|     for _ in range(10): | ||||
|         request.id | ||||
| @@ -28,6 +29,7 @@ def test_request_id_generates_from_request(monkeypatch): | ||||
|  | ||||
| def test_request_id_defaults_uuid(): | ||||
|     request = Request(b"/", {}, None, "GET", None, Mock()) | ||||
|     request.app.config.REQUEST_ID_HEADER = "foo" | ||||
|  | ||||
|     assert isinstance(request.id, UUID) | ||||
|  | ||||
| @@ -104,7 +106,7 @@ def test_route_assigned_to_request(app): | ||||
|         return response.empty() | ||||
|  | ||||
|     request, _ = app.test_client.get("/") | ||||
|     assert request.route is list(app.router.routes.values())[0] | ||||
|     assert request.route is list(app.router.routes)[0] | ||||
|  | ||||
|  | ||||
| def test_protocol_attribute(app): | ||||
| @@ -120,3 +122,21 @@ def test_protocol_attribute(app): | ||||
|     _ = app.test_client.get("/", headers=headers) | ||||
|  | ||||
|     assert isinstance(retrieved, HttpProtocol) | ||||
|  | ||||
|  | ||||
| def test_ipv6_address_is_not_wrapped(app): | ||||
|     @app.get("/") | ||||
|     async def get(request): | ||||
|         return response.json( | ||||
|             { | ||||
|                 "client_ip": request.conn_info.client_ip, | ||||
|                 "client": request.conn_info.client, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     request, resp = app.test_client.get("/", host="::1") | ||||
|  | ||||
|     assert request.route is list(app.router.routes)[0] | ||||
|     assert resp.json["client"] == "[::1]" | ||||
|     assert resp.json["client_ip"] == "::1" | ||||
|     assert request.ip == "::1" | ||||
|   | ||||
| @@ -8,7 +8,6 @@ import pytest | ||||
| from sanic import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.response import json, text | ||||
| from sanic.server import HttpProtocol | ||||
| from sanic.views import CompositionView, HTTPMethodView | ||||
| from sanic.views import stream as stream_decorator | ||||
|  | ||||
|   | ||||
| @@ -1,21 +1,8 @@ | ||||
| import asyncio | ||||
|  | ||||
| from typing import cast | ||||
|  | ||||
| import httpcore | ||||
| import httpx | ||||
|  | ||||
| from httpcore._async.base import ( | ||||
|     AsyncByteStream, | ||||
|     AsyncHTTPTransport, | ||||
|     ConnectionState, | ||||
|     NewConnectionRequired, | ||||
| ) | ||||
| from httpcore._async.connection import AsyncHTTPConnection | ||||
| from httpcore._async.connection_pool import ResponseByteStream | ||||
| from httpcore._exceptions import LocalProtocolError, UnsupportedProtocol | ||||
| from httpcore._types import TimeoutDict | ||||
| from httpcore._utils import url_to_origin | ||||
| from sanic_testing.testing import SanicTestClient | ||||
|  | ||||
| from sanic import Sanic | ||||
|   | ||||
| @@ -253,6 +253,31 @@ async def test_empty_json_asgi(app): | ||||
|     assert response.body == b"null" | ||||
|  | ||||
|  | ||||
| def test_echo_json(app): | ||||
|     @app.post("/") | ||||
|     async def handler(request): | ||||
|         return json(request.json) | ||||
|  | ||||
|     data = {"foo": "bar"} | ||||
|     request, response = app.test_client.post("/", json=data) | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.json == data | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_echo_json_asgi(app): | ||||
|     @app.post("/") | ||||
|     async def handler(request): | ||||
|         return json(request.json) | ||||
|  | ||||
|     data = {"foo": "bar"} | ||||
|     request, response = await app.asgi_client.post("/", json=data) | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.json == data | ||||
|  | ||||
|  | ||||
| def test_invalid_json(app): | ||||
|     @app.post("/") | ||||
|     async def handler(request): | ||||
| @@ -292,6 +317,17 @@ def test_query_string(app): | ||||
|     assert request.args.get("test3", default="My value") == "My value" | ||||
|  | ||||
|  | ||||
| def test_popped_stays_popped(app): | ||||
|     @app.route("/") | ||||
|     async def handler(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     request, response = app.test_client.get("/", params=[("test1", "1")]) | ||||
|  | ||||
|     assert request.args.pop("test1") == ["1"] | ||||
|     assert "test1" not in request.args | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_query_string_asgi(app): | ||||
|     @app.route("/") | ||||
| @@ -2159,3 +2195,70 @@ def test_safe_method_with_body(app): | ||||
|     assert request.body == data.encode("utf-8") | ||||
|     assert request.json.get("test") == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|  | ||||
| def test_conflicting_body_methods_overload(app): | ||||
|     @app.put("/") | ||||
|     @app.put("/p/") | ||||
|     @app.put("/p/<foo>") | ||||
|     async def put(request, foo=None): | ||||
|         return json( | ||||
|             {"name": request.route.name, "body": str(request.body), "foo": foo} | ||||
|         ) | ||||
|  | ||||
|     @app.delete("/p/<foo>") | ||||
|     async def delete(request, foo): | ||||
|         return json( | ||||
|             {"name": request.route.name, "body": str(request.body), "foo": foo} | ||||
|         ) | ||||
|  | ||||
|     payload = {"test": "OK"} | ||||
|     data = str(json_dumps(payload).encode()) | ||||
|  | ||||
|     _, response = app.test_client.put("/", json=payload) | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "name": "test_conflicting_body_methods_overload.put", | ||||
|         "foo": None, | ||||
|         "body": data, | ||||
|     } | ||||
|     _, response = app.test_client.put("/p", json=payload) | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "name": "test_conflicting_body_methods_overload.put", | ||||
|         "foo": None, | ||||
|         "body": data, | ||||
|     } | ||||
|     _, response = app.test_client.put("/p/test", json=payload) | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "name": "test_conflicting_body_methods_overload.put", | ||||
|         "foo": "test", | ||||
|         "body": data, | ||||
|     } | ||||
|     _, response = app.test_client.delete("/p/test") | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "name": "test_conflicting_body_methods_overload.delete", | ||||
|         "foo": "test", | ||||
|         "body": str("".encode()), | ||||
|     } | ||||
|  | ||||
|  | ||||
| def test_handler_overload(app): | ||||
|     @app.get("/long/sub/route/param_a/<param_a:str>/param_b/<param_b:str>") | ||||
|     @app.post("/long/sub/route/") | ||||
|     def handler(request, **kwargs): | ||||
|         return json(kwargs) | ||||
|  | ||||
|     _, response = app.test_client.get( | ||||
|         "/long/sub/route/param_a/foo/param_b/bar" | ||||
|     ) | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "param_a": "foo", | ||||
|         "param_b": "bar", | ||||
|     } | ||||
|     _, response = app.test_client.post("/long/sub/route") | ||||
|     assert response.status == 200 | ||||
|     assert response.json == {} | ||||
|   | ||||
| @@ -1,23 +1,19 @@ | ||||
| import asyncio | ||||
| import inspect | ||||
| import os | ||||
| import warnings | ||||
|  | ||||
| from collections import namedtuple | ||||
| from mimetypes import guess_type | ||||
| from random import choice | ||||
| from unittest.mock import MagicMock | ||||
| from urllib.parse import unquote | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from aiofiles import os as async_os | ||||
| from sanic_testing.testing import HOST, PORT | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import ( | ||||
|     HTTPResponse, | ||||
|     StreamingHTTPResponse, | ||||
|     empty, | ||||
|     file, | ||||
|     file_stream, | ||||
| @@ -26,7 +22,6 @@ from sanic.response import ( | ||||
|     stream, | ||||
|     text, | ||||
| ) | ||||
| from sanic.server import HttpProtocol | ||||
|  | ||||
|  | ||||
| JSON_DATA = {"ok": True} | ||||
| @@ -65,7 +60,9 @@ def test_method_not_allowed(): | ||||
|     } | ||||
|  | ||||
|     request, response = app.test_client.post("/") | ||||
|     assert set(response.headers["Allow"].split(", ")) == {"GET", "HEAD"} | ||||
|     assert set(response.headers["Allow"].split(", ")) == { | ||||
|         "GET", | ||||
|     } | ||||
|  | ||||
|     app.router.reset() | ||||
|  | ||||
| @@ -78,7 +75,6 @@ def test_method_not_allowed(): | ||||
|     assert set(response.headers["Allow"].split(", ")) == { | ||||
|         "GET", | ||||
|         "POST", | ||||
|         "HEAD", | ||||
|     } | ||||
|     assert response.headers["Content-Length"] == "0" | ||||
|  | ||||
| @@ -87,7 +83,6 @@ def test_method_not_allowed(): | ||||
|     assert set(response.headers["Allow"].split(", ")) == { | ||||
|         "GET", | ||||
|         "POST", | ||||
|         "HEAD", | ||||
|     } | ||||
|     assert response.headers["Content-Length"] == "0" | ||||
|  | ||||
| @@ -229,7 +224,6 @@ def non_chunked_streaming_app(app): | ||||
|             sample_streaming_fn, | ||||
|             headers={"Content-Length": "7"}, | ||||
|             content_type="text/csv", | ||||
|             chunked=False, | ||||
|         ) | ||||
|  | ||||
|     return app | ||||
| @@ -256,11 +250,7 @@ async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): | ||||
|  | ||||
|  | ||||
| def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | ||||
|     with pytest.warns(UserWarning) as record: | ||||
|         request, response = non_chunked_streaming_app.test_client.get("/") | ||||
|  | ||||
|     assert len(record) == 1 | ||||
|     assert "removed in v21.6" in record[0].message.args[0] | ||||
|     request, response = non_chunked_streaming_app.test_client.get("/") | ||||
|  | ||||
|     assert "Transfer-Encoding" not in response.headers | ||||
|     assert response.headers["Content-Type"] == "text/csv" | ||||
| @@ -534,3 +524,19 @@ def test_empty_response(app): | ||||
|     request, response = app.test_client.get("/test") | ||||
|     assert response.content_type is None | ||||
|     assert response.body == b"" | ||||
|  | ||||
|  | ||||
| def test_direct_response_stream(app): | ||||
|     @app.route("/") | ||||
|     async def test(request): | ||||
|         response = await request.respond(content_type="text/csv") | ||||
|         await response.send("foo,") | ||||
|         await response.send("bar") | ||||
|         await response.eof() | ||||
|         return response | ||||
|  | ||||
|     _, response = app.test_client.get("/") | ||||
|     assert response.text == "foo,bar" | ||||
|     assert response.headers["Transfer-Encoding"] == "chunked" | ||||
|     assert response.headers["Content-Type"] == "text/csv" | ||||
|     assert "Content-Length" not in response.headers | ||||
|   | ||||
| @@ -1,7 +1,11 @@ | ||||
| import asyncio | ||||
| import logging | ||||
|  | ||||
| from time import sleep | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.exceptions import ServiceUnavailable | ||||
| from sanic.log import LOGGING_CONFIG_DEFAULTS | ||||
| from sanic.response import text | ||||
|  | ||||
|  | ||||
| @@ -13,6 +17,8 @@ response_timeout_app.config.RESPONSE_TIMEOUT = 1 | ||||
| response_timeout_default_app.config.RESPONSE_TIMEOUT = 1 | ||||
| response_handler_cancelled_app.config.RESPONSE_TIMEOUT = 1 | ||||
|  | ||||
| response_handler_cancelled_app.ctx.flag = False | ||||
|  | ||||
|  | ||||
| @response_timeout_app.route("/1") | ||||
| async def handler_1(request): | ||||
| @@ -25,32 +31,17 @@ def handler_exception(request, exception): | ||||
|     return text("Response Timeout from error_handler.", 503) | ||||
|  | ||||
|  | ||||
| def test_server_error_response_timeout(): | ||||
|     request, response = response_timeout_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert response.text == "Response Timeout from error_handler." | ||||
|  | ||||
|  | ||||
| @response_timeout_default_app.route("/1") | ||||
| async def handler_2(request): | ||||
|     await asyncio.sleep(2) | ||||
|     return text("OK") | ||||
|  | ||||
|  | ||||
| def test_default_server_error_response_timeout(): | ||||
|     request, response = response_timeout_default_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert "Response Timeout" in response.text | ||||
|  | ||||
|  | ||||
| response_handler_cancelled_app.flag = False | ||||
|  | ||||
|  | ||||
| @response_handler_cancelled_app.exception(asyncio.CancelledError) | ||||
| def handler_cancelled(request, exception): | ||||
|     # If we get a CancelledError, it means sanic has already sent a response, | ||||
|     # we should not ever have to handle a CancelledError. | ||||
|     response_handler_cancelled_app.flag = True | ||||
|     response_handler_cancelled_app.ctx.flag = True | ||||
|     return text("App received CancelledError!", 500) | ||||
|     # The client will never receive this response, because the socket | ||||
|     # is already closed when we get a CancelledError. | ||||
| @@ -62,8 +53,44 @@ async def handler_3(request): | ||||
|     return text("OK") | ||||
|  | ||||
|  | ||||
| def test_server_error_response_timeout(): | ||||
|     request, response = response_timeout_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert response.text == "Response Timeout from error_handler." | ||||
|  | ||||
|  | ||||
| def test_default_server_error_response_timeout(): | ||||
|     request, response = response_timeout_default_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert "Response Timeout" in response.text | ||||
|  | ||||
|  | ||||
| def test_response_handler_cancelled(): | ||||
|     request, response = response_handler_cancelled_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert "Response Timeout" in response.text | ||||
|     assert response_handler_cancelled_app.flag is False | ||||
|     assert response_handler_cancelled_app.ctx.flag is False | ||||
|  | ||||
|  | ||||
| def test_response_timeout_not_applied(caplog): | ||||
|     modified_config = LOGGING_CONFIG_DEFAULTS | ||||
|     modified_config["loggers"]["sanic.root"]["level"] = "DEBUG" | ||||
|  | ||||
|     app = Sanic("test_logging", log_config=modified_config) | ||||
|     app.config.RESPONSE_TIMEOUT = 1 | ||||
|     app.ctx.event = asyncio.Event() | ||||
|  | ||||
|     @app.websocket("/ws") | ||||
|     async def ws_handler(request, ws): | ||||
|         sleep(2) | ||||
|         await asyncio.sleep(0) | ||||
|         request.app.ctx.event.set() | ||||
|  | ||||
|     with caplog.at_level(logging.DEBUG): | ||||
|         _ = app.test_client.websocket("/ws") | ||||
|     assert app.ctx.event.is_set() | ||||
|     assert ( | ||||
|         "sanic.root", | ||||
|         10, | ||||
|         "Handling websocket. Timeouts disabled.", | ||||
|     ) in caplog.record_tuples | ||||
|   | ||||
| @@ -258,7 +258,7 @@ def test_route_strict_slash(app): | ||||
| def test_route_invalid_parameter_syntax(app): | ||||
|     with pytest.raises(ValueError): | ||||
|  | ||||
|         @app.get("/get/<:string>", strict_slashes=True) | ||||
|         @app.get("/get/<:str>", strict_slashes=True) | ||||
|         def handler(request): | ||||
|             return text("OK") | ||||
|  | ||||
| @@ -478,7 +478,7 @@ def test_dynamic_route(app): | ||||
| def test_dynamic_route_string(app): | ||||
|     results = [] | ||||
|  | ||||
|     @app.route("/folder/<name:string>") | ||||
|     @app.route("/folder/<name:str>") | ||||
|     async def handler(request, name): | ||||
|         results.append(name) | ||||
|         return text("OK") | ||||
| @@ -513,7 +513,7 @@ def test_dynamic_route_int(app): | ||||
| def test_dynamic_route_number(app): | ||||
|     results = [] | ||||
|  | ||||
|     @app.route("/weight/<weight:number>") | ||||
|     @app.route("/weight/<weight:float>") | ||||
|     async def handler(request, weight): | ||||
|         results.append(weight) | ||||
|         return text("OK") | ||||
| @@ -543,9 +543,6 @@ def test_dynamic_route_regex(app): | ||||
|     async def handler(request, folder_id): | ||||
|         return text("OK") | ||||
|  | ||||
|     app.router.finalize() | ||||
|     print(app.router.find_route_src) | ||||
|  | ||||
|     request, response = app.test_client.get("/folder/test") | ||||
|     assert response.status == 200 | ||||
|  | ||||
| @@ -587,6 +584,8 @@ def test_dynamic_route_path(app): | ||||
|     async def handler(request, path): | ||||
|         return text("OK") | ||||
|  | ||||
|     app.router.finalize() | ||||
|  | ||||
|     request, response = app.test_client.get("/path/1/info") | ||||
|     assert response.status == 200 | ||||
|  | ||||
| @@ -824,7 +823,7 @@ def test_dynamic_add_route_string(app): | ||||
|         results.append(name) | ||||
|         return text("OK") | ||||
|  | ||||
|     app.add_route(handler, "/folder/<name:string>") | ||||
|     app.add_route(handler, "/folder/<name:str>") | ||||
|     request, response = app.test_client.get("/folder/test123") | ||||
|  | ||||
|     assert response.text == "OK" | ||||
| @@ -860,7 +859,7 @@ def test_dynamic_add_route_number(app): | ||||
|         results.append(weight) | ||||
|         return text("OK") | ||||
|  | ||||
|     app.add_route(handler, "/weight/<weight:number>") | ||||
|     app.add_route(handler, "/weight/<weight:float>") | ||||
|  | ||||
|     request, response = app.test_client.get("/weight/12345") | ||||
|     assert response.text == "OK" | ||||
| @@ -1008,14 +1007,8 @@ def test_unmergeable_overload_routes(app): | ||||
|     async def handler2(request): | ||||
|         return text("OK1") | ||||
|  | ||||
|     assert ( | ||||
|         len( | ||||
|             dict(list(app.router.static_routes.values())[0].handlers)[ | ||||
|                 "overload_whole" | ||||
|             ] | ||||
|         ) | ||||
|         == 3 | ||||
|     ) | ||||
|     assert len(app.router.static_routes) == 1 | ||||
|     assert len(app.router.static_routes[("overload_whole",)].methods) == 3 | ||||
|  | ||||
|     request, response = app.test_client.get("/overload_whole") | ||||
|     assert response.text == "OK1" | ||||
| @@ -1073,7 +1066,8 @@ def test_uri_with_different_method_and_different_params(app): | ||||
|         return json({"action": action}) | ||||
|  | ||||
|     request, response = app.test_client.get("/ads/1234") | ||||
|     assert response.status == 405 | ||||
|     assert response.status == 200 | ||||
|     assert response.json == {"ad_id": "1234"} | ||||
|  | ||||
|     request, response = app.test_client.post("/ads/post") | ||||
|     assert response.status == 200 | ||||
| @@ -1175,3 +1169,59 @@ def test_route_with_bad_named_param(app): | ||||
|  | ||||
|     with pytest.raises(SanicException): | ||||
|         app.router.finalize() | ||||
|  | ||||
|  | ||||
| def test_routes_with_and_without_slash_definitions(app): | ||||
|     bar = Blueprint("bar", url_prefix="bar") | ||||
|     baz = Blueprint("baz", url_prefix="/baz") | ||||
|     fizz = Blueprint("fizz", url_prefix="fizz/") | ||||
|     buzz = Blueprint("buzz", url_prefix="/buzz/") | ||||
|  | ||||
|     instances = ( | ||||
|         (app, "foo"), | ||||
|         (bar, "bar"), | ||||
|         (baz, "baz"), | ||||
|         (fizz, "fizz"), | ||||
|         (buzz, "buzz"), | ||||
|     ) | ||||
|  | ||||
|     for instance, term in instances: | ||||
|         route = f"/{term}" if isinstance(instance, Sanic) else "" | ||||
|  | ||||
|         @instance.get(route, strict_slashes=True) | ||||
|         def get_without(request): | ||||
|             return text(f"{term}_without") | ||||
|  | ||||
|         @instance.get(f"{route}/", strict_slashes=True) | ||||
|         def get_with(request): | ||||
|             return text(f"{term}_with") | ||||
|  | ||||
|         @instance.post(route, strict_slashes=True) | ||||
|         def post_without(request): | ||||
|             return text(f"{term}_without") | ||||
|  | ||||
|         @instance.post(f"{route}/", strict_slashes=True) | ||||
|         def post_with(request): | ||||
|             return text(f"{term}_with") | ||||
|  | ||||
|     app.blueprint(bar) | ||||
|     app.blueprint(baz) | ||||
|     app.blueprint(fizz) | ||||
|     app.blueprint(buzz) | ||||
|  | ||||
|     for _, term in instances: | ||||
|         _, response = app.test_client.get(f"/{term}") | ||||
|         assert response.status == 200 | ||||
|         assert response.text == f"{term}_without" | ||||
|  | ||||
|         _, response = app.test_client.get(f"/{term}/") | ||||
|         assert response.status == 200 | ||||
|         assert response.text == f"{term}_with" | ||||
|  | ||||
|         _, response = app.test_client.post(f"/{term}") | ||||
|         assert response.status == 200 | ||||
|         assert response.text == f"{term}_without" | ||||
|  | ||||
|         _, response = app.test_client.post(f"/{term}/") | ||||
|         assert response.status == 200 | ||||
|         assert response.text == f"{term}_with" | ||||
|   | ||||
| @@ -28,7 +28,8 @@ def test_add_signal_decorator(app): | ||||
|     async def async_signal(*_): | ||||
|         ... | ||||
|  | ||||
|     assert len(app.signal_router.routes) == 1 | ||||
|     assert len(app.signal_router.routes) == 2 | ||||
|     assert len(app.signal_router.dynamic_routes) == 1 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
| @@ -79,13 +80,13 @@ async def test_dispatch_signal_triggers_triggers_event(app): | ||||
|     def sync_signal(*args): | ||||
|         nonlocal app | ||||
|         nonlocal counter | ||||
|         signal, *_ = app.signal_router.get("foo.bar.baz") | ||||
|         counter += signal.ctx.event.is_set() | ||||
|         group, *_ = app.signal_router.get("foo.bar.baz") | ||||
|         for signal in group: | ||||
|             counter += signal.ctx.event.is_set() | ||||
|  | ||||
|     app.signal_router.finalize() | ||||
|  | ||||
|     await app.dispatch("foo.bar.baz") | ||||
|     signal, *_ = app.signal_router.get("foo.bar.baz") | ||||
|  | ||||
|     assert counter == 1 | ||||
|  | ||||
| @@ -224,7 +225,7 @@ async def test_dispatch_signal_triggers_event_on_bp(app): | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|     app.signal_router.finalize() | ||||
|     signal, *_ = app.signal_router.get( | ||||
|     signal_group, *_ = app.signal_router.get( | ||||
|         "foo.bar.baz", condition={"blueprint": "bp"} | ||||
|     ) | ||||
|  | ||||
| @@ -233,7 +234,8 @@ async def test_dispatch_signal_triggers_event_on_bp(app): | ||||
|     assert isawaitable(waiter) | ||||
|  | ||||
|     fut = asyncio.ensure_future(do_wait()) | ||||
|     signal.ctx.event.set() | ||||
|     for signal in signal_group: | ||||
|         signal.ctx.event.set() | ||||
|     await fut | ||||
|  | ||||
|     assert bp_counter == 1 | ||||
| @@ -255,17 +257,60 @@ def test_bad_finalize(app): | ||||
|     assert counter == 0 | ||||
|  | ||||
|  | ||||
| def test_event_not_exist(app): | ||||
| @pytest.mark.asyncio | ||||
| async def test_event_not_exist(app): | ||||
|     with pytest.raises(NotFound, match="Could not find signal does.not.exist"): | ||||
|         app.event("does.not.exist") | ||||
|         await app.event("does.not.exist") | ||||
|  | ||||
|  | ||||
| def test_event_not_exist_on_bp(app): | ||||
| @pytest.mark.asyncio | ||||
| async def test_event_not_exist_on_bp(app): | ||||
|     bp = Blueprint("bp") | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     with pytest.raises(NotFound, match="Could not find signal does.not.exist"): | ||||
|         bp.event("does.not.exist") | ||||
|         await bp.event("does.not.exist") | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_event_not_exist_with_autoregister(app): | ||||
|     app.config.EVENT_AUTOREGISTER = True | ||||
|     try: | ||||
|         await app.event("does.not.exist", timeout=0.1) | ||||
|     except asyncio.TimeoutError: | ||||
|         ... | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_dispatch_signal_triggers_non_exist_event_with_autoregister(app): | ||||
|     @app.signal("some.stand.in") | ||||
|     async def signal_handler(): | ||||
|         ... | ||||
|  | ||||
|     app.config.EVENT_AUTOREGISTER = True | ||||
|     app_counter = 0 | ||||
|     app.signal_router.finalize() | ||||
|  | ||||
|     async def do_wait(): | ||||
|         nonlocal app_counter | ||||
|         await app.event("foo.bar.baz") | ||||
|         app_counter += 1 | ||||
|  | ||||
|     fut = asyncio.ensure_future(do_wait()) | ||||
|     await app.dispatch("foo.bar.baz") | ||||
|     await fut | ||||
|  | ||||
|     assert app_counter == 1 | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_dispatch_not_exist(app): | ||||
|     @app.signal("do.something.start") | ||||
|     async def signal_handler(): | ||||
|         ... | ||||
|  | ||||
|     app.signal_router.finalize() | ||||
|     await app.dispatch("does.not.exist") | ||||
|  | ||||
|  | ||||
| def test_event_on_bp_not_registered(): | ||||
|   | ||||
| @@ -1,11 +1,16 @@ | ||||
| import inspect | ||||
| import logging | ||||
| import os | ||||
|  | ||||
| from collections import Counter | ||||
| from pathlib import Path | ||||
| from time import gmtime, strftime | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic import text | ||||
| from sanic.exceptions import FileNotFound | ||||
|  | ||||
|  | ||||
| @pytest.fixture(scope="module") | ||||
| def static_file_directory(): | ||||
| @@ -454,3 +459,51 @@ def test_nested_dir(app, static_file_directory): | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.text == "foo\n" | ||||
|  | ||||
|  | ||||
| def test_stack_trace_on_not_found(app, static_file_directory, caplog): | ||||
|     app.static("/static", static_file_directory) | ||||
|  | ||||
|     with caplog.at_level(logging.INFO): | ||||
|         _, response = app.test_client.get("/static/non_existing_file.file") | ||||
|  | ||||
|     counter = Counter([r[1] for r in caplog.record_tuples]) | ||||
|  | ||||
|     assert response.status == 404 | ||||
|     assert counter[logging.INFO] == 5 | ||||
|     assert counter[logging.ERROR] == 0 | ||||
|  | ||||
|  | ||||
| def test_no_stack_trace_on_not_found(app, static_file_directory, caplog): | ||||
|     app.static("/static", static_file_directory) | ||||
|  | ||||
|     @app.exception(FileNotFound) | ||||
|     async def file_not_found(request, exception): | ||||
|         return text(f"No file: {request.path}", status=404) | ||||
|  | ||||
|     with caplog.at_level(logging.INFO): | ||||
|         _, response = app.test_client.get("/static/non_existing_file.file") | ||||
|  | ||||
|     counter = Counter([r[1] for r in caplog.record_tuples]) | ||||
|  | ||||
|     assert response.status == 404 | ||||
|     assert counter[logging.INFO] == 5 | ||||
|     assert logging.ERROR not in counter | ||||
|     assert response.text == "No file: /static/non_existing_file.file" | ||||
|  | ||||
|  | ||||
| def test_multiple_statics(app, static_file_directory): | ||||
|     app.static("/file", get_file_path(static_file_directory, "test.file")) | ||||
|     app.static("/png", get_file_path(static_file_directory, "python.png")) | ||||
|  | ||||
|     _, response = app.test_client.get("/file") | ||||
|     assert response.status == 200 | ||||
|     assert response.body == get_file_content( | ||||
|         static_file_directory, "test.file" | ||||
|     ) | ||||
|  | ||||
|     _, response = app.test_client.get("/png") | ||||
|     assert response.status == 200 | ||||
|     assert response.body == get_file_content( | ||||
|         static_file_directory, "python.png" | ||||
|     ) | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import asyncio | ||||
|  | ||||
| from time import monotonic as current_time | ||||
| from unittest.mock import Mock | ||||
|  | ||||
| import pytest | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import asyncio | ||||
| import logging | ||||
| import os | ||||
| import platform | ||||
| import subprocess | ||||
| import sys | ||||
|  | ||||
| @@ -175,6 +176,10 @@ def test_unix_connection_multiple_workers(): | ||||
|     app_multi.run(host="myhost.invalid", unix=SOCKPATH, workers=2) | ||||
|  | ||||
|  | ||||
| @pytest.mark.xfail( | ||||
|     condition=platform.system() != "Linux", | ||||
|     reason="Flaky Test on Non Linux Infra", | ||||
| ) | ||||
| async def test_zero_downtime(): | ||||
|     """Graceful server termination and socket replacement on restarts""" | ||||
|     from signal import SIGINT | ||||
|   | ||||
| @@ -143,7 +143,7 @@ def test_fails_url_build_if_params_not_passed(app): | ||||
|  | ||||
| COMPLEX_PARAM_URL = ( | ||||
|     "/<foo:int>/<four_letter_string:[A-z]{4}>/" | ||||
|     "<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>" | ||||
|     "<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:float>" | ||||
| ) | ||||
| PASSING_KWARGS = { | ||||
|     "foo": 4, | ||||
| @@ -168,7 +168,7 @@ def test_fails_with_int_message(app): | ||||
|  | ||||
|     expected_error = ( | ||||
|         r'Value "not_int" for parameter `foo` ' | ||||
|         r"does not match pattern for type `int`: ^-?\d+" | ||||
|         r"does not match pattern for type `int`: ^-?\d+$" | ||||
|     ) | ||||
|     assert str(e.value) == expected_error | ||||
|  | ||||
| @@ -223,7 +223,7 @@ def test_fails_with_number_message(app): | ||||
|  | ||||
| @pytest.mark.parametrize("number", [3, -3, 13.123, -13.123]) | ||||
| def test_passes_with_negative_number_message(app, number): | ||||
|     @app.route("path/<possibly_neg:number>/another-word") | ||||
|     @app.route("path/<possibly_neg:float>/another-word") | ||||
|     def good(request, possibly_neg): | ||||
|         assert isinstance(possibly_neg, (int, float)) | ||||
|         return text(f"this should pass with `{possibly_neg}`") | ||||
|   | ||||
							
								
								
									
										141
									
								
								tests/test_versioning.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								tests/test_versioning.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| import pytest | ||||
|  | ||||
| from sanic import Blueprint, text | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def handler(): | ||||
|     def handler(_): | ||||
|         return text("Done.") | ||||
|  | ||||
|     return handler | ||||
|  | ||||
|  | ||||
| def test_route(app, handler): | ||||
|     app.route("/", version=1)(handler) | ||||
|  | ||||
|     _, response = app.test_client.get("/v1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_bp(app, handler): | ||||
|     bp = Blueprint(__file__, version=1) | ||||
|     bp.route("/")(handler) | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     _, response = app.test_client.get("/v1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_bp_use_route(app, handler): | ||||
|     bp = Blueprint(__file__, version=1) | ||||
|     bp.route("/", version=1.1)(handler) | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     _, response = app.test_client.get("/v1.1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_bp_group(app, handler): | ||||
|     bp = Blueprint(__file__) | ||||
|     bp.route("/")(handler) | ||||
|     group = Blueprint.group(bp, version=1) | ||||
|     app.blueprint(group) | ||||
|  | ||||
|     _, response = app.test_client.get("/v1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_bp_group_use_bp(app, handler): | ||||
|     bp = Blueprint(__file__, version=1.1) | ||||
|     bp.route("/")(handler) | ||||
|     group = Blueprint.group(bp, version=1) | ||||
|     app.blueprint(group) | ||||
|  | ||||
|     _, response = app.test_client.get("/v1.1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_bp_group_use_registration(app, handler): | ||||
|     bp = Blueprint(__file__, version=1.1) | ||||
|     bp.route("/")(handler) | ||||
|     group = Blueprint.group(bp, version=1) | ||||
|     app.blueprint(group, version=1.2) | ||||
|  | ||||
|     _, response = app.test_client.get("/v1.2") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_bp_group_use_route(app, handler): | ||||
|     bp = Blueprint(__file__, version=1.1) | ||||
|     bp.route("/", version=1.3)(handler) | ||||
|     group = Blueprint.group(bp, version=1) | ||||
|     app.blueprint(group, version=1.2) | ||||
|  | ||||
|     _, response = app.test_client.get("/v1.3") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_version_prefix_route(app, handler): | ||||
|     app.route("/", version=1, version_prefix="/api/v")(handler) | ||||
|  | ||||
|     _, response = app.test_client.get("/api/v1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_version_prefix_bp(app, handler): | ||||
|     bp = Blueprint(__file__, version=1, version_prefix="/api/v") | ||||
|     bp.route("/")(handler) | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     _, response = app.test_client.get("/api/v1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_version_prefix_bp_use_route(app, handler): | ||||
|     bp = Blueprint(__file__, version=1, version_prefix="/ignore/v") | ||||
|     bp.route("/", version=1.1, version_prefix="/api/v")(handler) | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     _, response = app.test_client.get("/api/v1.1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_version_prefix_bp_group(app, handler): | ||||
|     bp = Blueprint(__file__) | ||||
|     bp.route("/")(handler) | ||||
|     group = Blueprint.group(bp, version=1, version_prefix="/api/v") | ||||
|     app.blueprint(group) | ||||
|  | ||||
|     _, response = app.test_client.get("/api/v1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_version_prefix_bp_group_use_bp(app, handler): | ||||
|     bp = Blueprint(__file__, version=1.1, version_prefix="/api/v") | ||||
|     bp.route("/")(handler) | ||||
|     group = Blueprint.group(bp, version=1, version_prefix="/ignore/v") | ||||
|     app.blueprint(group) | ||||
|  | ||||
|     _, response = app.test_client.get("/api/v1.1") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_version_prefix_bp_group_use_registration(app, handler): | ||||
|     bp = Blueprint(__file__, version=1.1, version_prefix="/alsoignore/v") | ||||
|     bp.route("/")(handler) | ||||
|     group = Blueprint.group(bp, version=1, version_prefix="/ignore/v") | ||||
|     app.blueprint(group, version=1.2, version_prefix="/api/v") | ||||
|  | ||||
|     _, response = app.test_client.get("/api/v1.2") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_version_prefix_bp_group_use_route(app, handler): | ||||
|     bp = Blueprint(__file__, version=1.1, version_prefix="/alsoignore/v") | ||||
|     bp.route("/", version=1.3, version_prefix="/api/v")(handler) | ||||
|     group = Blueprint.group(bp, version=1, version_prefix="/ignore/v") | ||||
|     app.blueprint(group, version=1.2, version_prefix="/stillignoring/v") | ||||
|  | ||||
|     _, response = app.test_client.get("/api/v1.3") | ||||
|     assert response.status == 200 | ||||
| @@ -77,6 +77,56 @@ def test_with_bp(app): | ||||
|     assert response.text == "I am get method" | ||||
|  | ||||
|  | ||||
| def test_with_attach(app): | ||||
|     class DummyView(HTTPMethodView): | ||||
|         def get(self, request): | ||||
|             return text("I am get method") | ||||
|  | ||||
|     DummyView.attach(app, "/") | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|  | ||||
|     assert response.text == "I am get method" | ||||
|  | ||||
|  | ||||
| def test_with_sub_init(app): | ||||
|     class DummyView(HTTPMethodView, attach=app, uri="/"): | ||||
|         def get(self, request): | ||||
|             return text("I am get method") | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|  | ||||
|     assert response.text == "I am get method" | ||||
|  | ||||
|  | ||||
| def test_with_attach_and_bp(app): | ||||
|     bp = Blueprint("test_text") | ||||
|  | ||||
|     class DummyView(HTTPMethodView): | ||||
|         def get(self, request): | ||||
|             return text("I am get method") | ||||
|  | ||||
|     DummyView.attach(bp, "/") | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|     request, response = app.test_client.get("/") | ||||
|  | ||||
|     assert response.text == "I am get method" | ||||
|  | ||||
|  | ||||
| def test_with_sub_init_and_bp(app): | ||||
|     bp = Blueprint("test_text") | ||||
|  | ||||
|     class DummyView(HTTPMethodView, attach=bp, uri="/"): | ||||
|         def get(self, request): | ||||
|             return text("I am get method") | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|     request, response = app.test_client.get("/") | ||||
|  | ||||
|     assert response.text == "I am get method" | ||||
|  | ||||
|  | ||||
| def test_with_bp_with_url_prefix(app): | ||||
|     bp = Blueprint("test_text", url_prefix="/test1") | ||||
|  | ||||
| @@ -218,15 +268,15 @@ def test_composition_view_runs_methods_as_expected(app, method): | ||||
|         assert response.status == 200 | ||||
|         assert response.text == "first method" | ||||
|  | ||||
|         # response = view(request) | ||||
|         # assert response.body.decode() == "first method" | ||||
|         response = view(request) | ||||
|         assert response.body.decode() == "first method" | ||||
|  | ||||
|     # if method in ["DELETE", "PATCH"]: | ||||
|     #     request, response = getattr(app.test_client, method.lower())("/") | ||||
|     #     assert response.text == "second method" | ||||
|     if method in ["DELETE", "PATCH"]: | ||||
|         request, response = getattr(app.test_client, method.lower())("/") | ||||
|         assert response.text == "second method" | ||||
|  | ||||
|     #     response = view(request) | ||||
|     #     assert response.body.decode() == "second method" | ||||
|         response = view(request) | ||||
|         assert response.body.decode() == "second method" | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("method", HTTP_METHODS) | ||||
| @@ -244,3 +294,12 @@ def test_composition_view_rejects_invalid_methods(app, method): | ||||
|     if method in ["DELETE", "PATCH"]: | ||||
|         request, response = getattr(app.test_client, method.lower())("/") | ||||
|         assert response.status == 405 | ||||
|  | ||||
|  | ||||
| def test_composition_view_deprecation(): | ||||
|     message = ( | ||||
|         "CompositionView has been deprecated and will be removed in v21.12. " | ||||
|         "Please update your view to HTTPMethodView." | ||||
|     ) | ||||
|     with pytest.warns(DeprecationWarning, match=message): | ||||
|         CompositionView() | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user