Compare commits
	
		
			49 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 | ||
|   | 7be5f0ed3d | ||
|   | 938d2b5923 | ||
|   | 13630a79ad | 
							
								
								
									
										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] | [run] | ||||||
| branch = True | branch = True | ||||||
| source = sanic | 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] | [html] | ||||||
| directory = coverage | 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" | name: "CodeQL" | ||||||
|  |  | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [ master ] |     branches: [ main ] | ||||||
|   pull_request: |   pull_request: | ||||||
|     # The branches below must be a subset of the branches above |     branches: [ main ] | ||||||
|     branches: [ master ] |  | ||||||
|   schedule: |   schedule: | ||||||
|     - cron: '25 16 * * 0' |     - cron: '25 16 * * 0' | ||||||
|  |  | ||||||
| @@ -29,39 +17,18 @@ jobs: | |||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         language: [ 'python' ] |         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: |     steps: | ||||||
|     - name: Checkout repository |     - name: Checkout repository | ||||||
|       uses: actions/checkout@v2 |       uses: actions/checkout@v2 | ||||||
|  |  | ||||||
|     # Initializes the CodeQL tools for scanning. |  | ||||||
|     - name: Initialize CodeQL |     - name: Initialize CodeQL | ||||||
|       uses: github/codeql-action/init@v1 |       uses: github/codeql-action/init@v1 | ||||||
|       with: |       with: | ||||||
|         languages: ${{ matrix.language }} |         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 |     - name: Autobuild | ||||||
|       uses: github/codeql-action/autobuild@v1 |       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 |     - name: Perform CodeQL Analysis | ||||||
|       uses: github/codeql-action/analyze@v1 |       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.* | ||||||
| coverage | coverage | ||||||
|  | coverage.xml | ||||||
| .tox | .tox | ||||||
| settings.py | settings.py | ||||||
| .idea/* | .idea/* | ||||||
| @@ -18,3 +19,6 @@ build/* | |||||||
| .DS_Store | .DS_Store | ||||||
| dist/* | dist/* | ||||||
| pip-wheel-metadata/ | 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" |  | ||||||
							
								
								
									
										113
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							| @@ -1,3 +1,116 @@ | |||||||
|  | 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 | ||||||
|  | -------------- | ||||||
|  |  | ||||||
|  | Bugfixes | ||||||
|  | ******** | ||||||
|  |  | ||||||
|  |   * `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_ | ||||||
|  |     Static files inside subfolders are not accessible (404) | ||||||
|  |  | ||||||
| Version 21.3.0 | Version 21.3.0 | ||||||
| -------------- | -------------- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -87,7 +87,7 @@ Permform ``flake8``\ , ``black`` and ``isort`` checks. | |||||||
|    tox -e lint |    tox -e lint | ||||||
|  |  | ||||||
| Run type annotation checks | Run type annotation checks | ||||||
| --------------- | -------------------------- | ||||||
|  |  | ||||||
| ``tox`` environment -> ``[testenv:type-checking]`` | ``tox`` environment -> ``[testenv:type-checking]`` | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								Makefile
									
									
									
									
									
								
							| @@ -49,6 +49,9 @@ test: clean | |||||||
| test-coverage: clean | test-coverage: clean | ||||||
| 	python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append " | 	python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append " | ||||||
|  |  | ||||||
|  | view-coverage: | ||||||
|  | 	sanic ./coverage --simple | ||||||
|  |  | ||||||
| install: | install: | ||||||
| 	python setup.py install | 	python setup.py install | ||||||
|  |  | ||||||
| @@ -85,8 +88,7 @@ docs-test: docs-clean | |||||||
| 	cd docs && make dummy | 	cd docs && make dummy | ||||||
|  |  | ||||||
| docs-serve: | docs-serve: | ||||||
| 	# python -m http.server --directory=./docs/_build/html 9999 | 	sphinx-autobuild docs docs/_build/html --port 9999 --watch ./ | ||||||
| 	sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic |  | ||||||
|  |  | ||||||
| changelog: | changelog: | ||||||
| 	python scripts/changelog.py | 	python scripts/changelog.py | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.rst
									
									
									
									
									
								
							| @@ -11,7 +11,7 @@ Sanic | Build fast. Run fast. | |||||||
|     :stub-columns: 1 |     :stub-columns: 1 | ||||||
|  |  | ||||||
|     * - Build |     * - Build | ||||||
|       - | |Build Status| |AppVeyor Build Status| |Codecov| |       - | |Py39Test| |Py38Test| |Py37Test| |Codecov| | ||||||
|     * - Docs |     * - Docs | ||||||
|       - | |UserGuide| |Documentation| |       - | |UserGuide| |Documentation| | ||||||
|     * - Package |     * - Package | ||||||
| @@ -29,10 +29,12 @@ Sanic | Build fast. Run fast. | |||||||
|    :target: https://discord.gg/FARQzAEMAA |    :target: https://discord.gg/FARQzAEMAA | ||||||
| .. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg | .. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg | ||||||
|     :target: https://codecov.io/gh/sanic-org/sanic |     :target: https://codecov.io/gh/sanic-org/sanic | ||||||
| .. |Build Status| image:: https://travis-ci.com/sanic-org/sanic.svg?branch=master | .. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main | ||||||
|    :target: https://travis-ci.com/sanic-org/sanic |    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml | ||||||
| .. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true | .. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main | ||||||
|    :target: https://ci.appveyor.com/project/sanic-org/sanic |    :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 | .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest | ||||||
|    :target: http://sanic.readthedocs.io/en/latest/?badge=latest |    :target: http://sanic.readthedocs.io/en/latest/?badge=latest | ||||||
| .. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg | .. |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 \ | FROM sanicframework/sanic-build:${BASE_IMAGE_TAG} | ||||||
|         curl \ |  | ||||||
|         bash \ |  | ||||||
|         build-base \ |  | ||||||
|         ca-certificates \ |  | ||||||
|         git \ |  | ||||||
|         bzip2-dev \ |  | ||||||
|         linux-headers \ |  | ||||||
|         ncurses-dev \ |  | ||||||
|         openssl \ |  | ||||||
|         openssl-dev \ |  | ||||||
|         readline-dev \ |  | ||||||
|         sqlite-dev |  | ||||||
|  |  | ||||||
|  | RUN apk update | ||||||
| RUN update-ca-certificates | RUN update-ca-certificates | ||||||
| RUN rm -rf /var/cache/apk/* |  | ||||||
|  |  | ||||||
| ENV PYENV_ROOT="/root/.pyenv" | RUN pip install sanic | ||||||
| ENV PATH="$PYENV_ROOT/bin:$PATH" | RUN apk del build-base | ||||||
|  |  | ||||||
| ADD . /app |  | ||||||
| WORKDIR /app |  | ||||||
|  |  | ||||||
| RUN /app/docker/bin/install_python.sh 3.5.4 3.6.4 |  | ||||||
|  |  | ||||||
| ENTRYPOINT ["./docker/bin/entrypoint.sh"] |  | ||||||
|   | |||||||
							
								
								
									
										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 | 📑 API Reference | ||||||
| ================ | ================ | ||||||
|  |  | ||||||
| sanic.app | .. toctree:: | ||||||
| --------- |    :maxdepth: 2 | ||||||
|  |  | ||||||
| .. automodule:: sanic.app |    api/app | ||||||
|     :members: |    api/blueprints | ||||||
|     :show-inheritance: |    api/core | ||||||
|     :inherited-members: |    api/exceptions | ||||||
|  |    api/router | ||||||
| sanic.blueprints |    api/server | ||||||
| ---------------- |    api/utility | ||||||
|  |  | ||||||
| .. 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: |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| ♥️ Contributing | ♥️ Contributing | ||||||
| =============== | ============== | ||||||
|  |  | ||||||
| .. include:: ../../CONTRIBUTING.rst | .. include:: ../../CONTRIBUTING.rst | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								examples/static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								examples/static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										
											BIN
										
									
								
								examples/static/images/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								examples/static/images/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										2
									
								
								examples/static/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								examples/static/robots.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | User-agent: * | ||||||
|  | Disallow: / | ||||||
							
								
								
									
										6
									
								
								examples/static_assets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								examples/static_assets.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  | app.static("/", "./static") | ||||||
							
								
								
									
										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.__version__ import __version__ | ||||||
| from sanic.app import Sanic | from sanic.app import Sanic | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
|  | from sanic.constants import HTTPMethod | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import HTTPResponse, html, json, text | from sanic.response import HTTPResponse, html, json, text | ||||||
|  |  | ||||||
| @@ -9,6 +10,7 @@ __all__ = ( | |||||||
|     "__version__", |     "__version__", | ||||||
|     "Sanic", |     "Sanic", | ||||||
|     "Blueprint", |     "Blueprint", | ||||||
|  |     "HTTPMethod", | ||||||
|     "HTTPResponse", |     "HTTPResponse", | ||||||
|     "Request", |     "Request", | ||||||
|     "html", |     "html", | ||||||
|   | |||||||
| @@ -1,21 +1,25 @@ | |||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from argparse import ArgumentParser, RawDescriptionHelpFormatter | from argparse import ArgumentParser, RawTextHelpFormatter | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
|  | from pathlib import Path | ||||||
| from typing import Any, Dict, Optional | from typing import Any, Dict, Optional | ||||||
|  |  | ||||||
|  | from sanic_routing import __version__ as __routing_version__  # type: ignore | ||||||
|  |  | ||||||
| from sanic import __version__ | from sanic import __version__ | ||||||
| from sanic.app import Sanic | from sanic.app import Sanic | ||||||
| from sanic.config import BASE_LOGO | 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): | class SanicArgumentParser(ArgumentParser): | ||||||
|     def add_bool_arguments(self, *args, **kwargs): |     def add_bool_arguments(self, *args, **kwargs): | ||||||
|         group = self.add_mutually_exclusive_group() |         group = self.add_mutually_exclusive_group() | ||||||
|         group.add_argument(*args, action="store_true", **kwargs) |         group.add_argument(*args, action="store_true", **kwargs) | ||||||
|         kwargs["help"] = "no " + kwargs["help"] |         kwargs["help"] = f"no {kwargs['help']}\n " | ||||||
|         group.add_argument( |         group.add_argument( | ||||||
|             "--no-" + args[0][2:], *args[1:], action="store_false", **kwargs |             "--no-" + args[0][2:], *args[1:], action="store_false", **kwargs | ||||||
|         ) |         ) | ||||||
| @@ -25,7 +29,30 @@ def main(): | |||||||
|     parser = SanicArgumentParser( |     parser = SanicArgumentParser( | ||||||
|         prog="sanic", |         prog="sanic", | ||||||
|         description=BASE_LOGO, |         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( |     parser.add_argument( | ||||||
|         "-H", |         "-H", | ||||||
| @@ -33,7 +60,7 @@ def main(): | |||||||
|         dest="host", |         dest="host", | ||||||
|         type=str, |         type=str, | ||||||
|         default="127.0.0.1", |         default="127.0.0.1", | ||||||
|         help="host address [default 127.0.0.1]", |         help="Host address [default 127.0.0.1]", | ||||||
|     ) |     ) | ||||||
|     parser.add_argument( |     parser.add_argument( | ||||||
|         "-p", |         "-p", | ||||||
| @@ -41,7 +68,7 @@ def main(): | |||||||
|         dest="port", |         dest="port", | ||||||
|         type=int, |         type=int, | ||||||
|         default=8000, |         default=8000, | ||||||
|         help="port to serve on [default 8000]", |         help="Port to serve on [default 8000]", | ||||||
|     ) |     ) | ||||||
|     parser.add_argument( |     parser.add_argument( | ||||||
|         "-u", |         "-u", | ||||||
| @@ -49,13 +76,16 @@ def main(): | |||||||
|         dest="unix", |         dest="unix", | ||||||
|         type=str, |         type=str, | ||||||
|         default="", |         default="", | ||||||
|         help="location of unix socket", |         help="location of unix socket\n ", | ||||||
|     ) |     ) | ||||||
|     parser.add_argument( |     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( |     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( |     parser.add_argument( | ||||||
|         "-w", |         "-w", | ||||||
| @@ -63,20 +93,31 @@ def main(): | |||||||
|         dest="workers", |         dest="workers", | ||||||
|         type=int, |         type=int, | ||||||
|         default=1, |         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_argument("-d", "--debug", dest="debug", action="store_true") | ||||||
|     parser.add_bool_arguments( |     parser.add_argument( | ||||||
|         "--access-logs", dest="access_log", help="display access logs" |         "-r", | ||||||
|  |         "--reload", | ||||||
|  |         "--auto-reload", | ||||||
|  |         dest="auto_reload", | ||||||
|  |         action="store_true", | ||||||
|  |         help="Watch source directory for file changes and reload on changes", | ||||||
|     ) |     ) | ||||||
|     parser.add_argument( |     parser.add_argument( | ||||||
|         "-v", |         "-R", | ||||||
|         "--version", |         "--reload-dir", | ||||||
|         action="version", |         dest="path", | ||||||
|         version=f"Sanic {__version__}", |         action="append", | ||||||
|  |         help="Extra directories to watch and reload on changes\n ", | ||||||
|     ) |     ) | ||||||
|     parser.add_argument( |     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() |     args = parser.parse_args() | ||||||
|  |  | ||||||
| @@ -85,47 +126,71 @@ def main(): | |||||||
|         if module_path not in sys.path: |         if module_path not in sys.path: | ||||||
|             sys.path.append(module_path) |             sys.path.append(module_path) | ||||||
|  |  | ||||||
|         if ":" in args.module: |         if args.simple: | ||||||
|             module_name, app_name = args.module.rsplit(":", 1) |             path = Path(args.module) | ||||||
|  |             app = create_simple_server(path) | ||||||
|         else: |         else: | ||||||
|             module_parts = args.module.split(".") |             delimiter = ":" if ":" in args.module else "." | ||||||
|             module_name = ".".join(module_parts[:-1]) |             module_name, app_name = args.module.rsplit(delimiter, 1) | ||||||
|             app_name = module_parts[-1] |  | ||||||
|  |  | ||||||
|         module = import_module(module_name) |             if app_name.endswith("()"): | ||||||
|         app = getattr(module, app_name, None) |                 args.factory = True | ||||||
|         app_name = type(app).__name__ |                 app_name = app_name[:-2] | ||||||
|  |  | ||||||
|         if not isinstance(app, Sanic): |             module = import_module(module_name) | ||||||
|             raise ValueError( |             app = getattr(module, app_name, None) | ||||||
|                 f"Module is not a Sanic app, it is a {app_name}.  " |             if args.factory: | ||||||
|                 f"Perhaps you meant {args.module}.app?" |                 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: |         if args.cert is not None or args.key is not None: | ||||||
|             ssl = { |             ssl: Optional[Dict[str, Any]] = { | ||||||
|                 "cert": args.cert, |                 "cert": args.cert, | ||||||
|                 "key": args.key, |                 "key": args.key, | ||||||
|             }  # type: Optional[Dict[str, Any]] |             } | ||||||
|         else: |         else: | ||||||
|             ssl = None |             ssl = None | ||||||
|  |  | ||||||
|         app.run( |         kwargs = { | ||||||
|             host=args.host, |             "host": args.host, | ||||||
|             port=args.port, |             "port": args.port, | ||||||
|             unix=args.unix, |             "unix": args.unix, | ||||||
|             workers=args.workers, |             "workers": args.workers, | ||||||
|             debug=args.debug, |             "debug": args.debug, | ||||||
|             access_log=args.access_log, |             "access_log": args.access_log, | ||||||
|             ssl=ssl, |             "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: |     except ImportError as e: | ||||||
|         logger.error( |         if module_name.startswith(e.name): | ||||||
|             f"No module named {e.name} found.\n" |             error_logger.error( | ||||||
|             f"  Example File: project/sanic_server.py -> app\n" |                 f"No module named {e.name} found.\n" | ||||||
|             f"  Example Module: project.sanic_server.app" |                 "  Example File: project/sanic_server.py -> app\n" | ||||||
|         ) |                 "  Example Module: project.sanic_server.app" | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             raise e | ||||||
|     except ValueError: |     except ValueError: | ||||||
|         logger.exception("Failed to run app") |         error_logger.exception("Failed to run app") | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "21.3.0" | __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 collections import defaultdict, deque | ||||||
| from functools import partial | from functools import partial | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
|  | from pathlib import Path | ||||||
| from socket import socket | from socket import socket | ||||||
| from ssl import Purpose, SSLContext, create_default_context | from ssl import Purpose, SSLContext, create_default_context | ||||||
| from traceback import format_exc | from traceback import format_exc | ||||||
| @@ -43,7 +44,7 @@ from sanic.asgi import ASGIApp | |||||||
| from sanic.base import BaseSanic | from sanic.base import BaseSanic | ||||||
| from sanic.blueprint_group import BlueprintGroup | from sanic.blueprint_group import BlueprintGroup | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
| from sanic.config import BASE_LOGO, Config | from sanic.config import BASE_LOGO, SANIC_PREFIX, Config | ||||||
| from sanic.exceptions import ( | from sanic.exceptions import ( | ||||||
|     InvalidUsage, |     InvalidUsage, | ||||||
|     SanicException, |     SanicException, | ||||||
| @@ -78,6 +79,7 @@ class Sanic(BaseSanic): | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     __fake_slots__ = ( |     __fake_slots__ = ( | ||||||
|  |         "_asgi_app", | ||||||
|         "_app_registry", |         "_app_registry", | ||||||
|         "_asgi_client", |         "_asgi_client", | ||||||
|         "_blueprint_order", |         "_blueprint_order", | ||||||
| @@ -89,6 +91,7 @@ class Sanic(BaseSanic): | |||||||
|         "_future_signals", |         "_future_signals", | ||||||
|         "_test_client", |         "_test_client", | ||||||
|         "_test_manager", |         "_test_manager", | ||||||
|  |         "auto_reload", | ||||||
|         "asgi", |         "asgi", | ||||||
|         "blueprints", |         "blueprints", | ||||||
|         "config", |         "config", | ||||||
| @@ -103,6 +106,7 @@ class Sanic(BaseSanic): | |||||||
|         "name", |         "name", | ||||||
|         "named_request_middleware", |         "named_request_middleware", | ||||||
|         "named_response_middleware", |         "named_response_middleware", | ||||||
|  |         "reload_dirs", | ||||||
|         "request_class", |         "request_class", | ||||||
|         "request_middleware", |         "request_middleware", | ||||||
|         "response_middleware", |         "response_middleware", | ||||||
| @@ -121,10 +125,13 @@ class Sanic(BaseSanic): | |||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         name: str = None, |         name: str = None, | ||||||
|  |         config: Optional[Config] = None, | ||||||
|  |         ctx: Optional[Any] = None, | ||||||
|         router: Optional[Router] = None, |         router: Optional[Router] = None, | ||||||
|         signal_router: Optional[SignalRouter] = None, |         signal_router: Optional[SignalRouter] = None, | ||||||
|         error_handler: Optional[ErrorHandler] = None, |         error_handler: Optional[ErrorHandler] = None, | ||||||
|         load_env: bool = True, |         load_env: Union[bool, str] = True, | ||||||
|  |         env_prefix: Optional[str] = SANIC_PREFIX, | ||||||
|         request_class: Optional[Type[Request]] = None, |         request_class: Optional[Type[Request]] = None, | ||||||
|         strict_slashes: bool = False, |         strict_slashes: bool = False, | ||||||
|         log_config: Optional[Dict[str, Any]] = None, |         log_config: Optional[Dict[str, Any]] = None, | ||||||
| @@ -132,34 +139,38 @@ class Sanic(BaseSanic): | |||||||
|         register: Optional[bool] = None, |         register: Optional[bool] = None, | ||||||
|         dumps: Optional[Callable[..., str]] = None, |         dumps: Optional[Callable[..., str]] = None, | ||||||
|     ) -> 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 |         # logging | ||||||
|         if configure_logging: |         if configure_logging: | ||||||
|             logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) |             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._asgi_client = None | ||||||
|         self._blueprint_order: List[Blueprint] = [] |         self._blueprint_order: List[Blueprint] = [] | ||||||
|         self._test_client = None |         self._test_client = None | ||||||
|         self._test_manager = None |         self._test_manager = None | ||||||
|         self.asgi = False |         self.asgi = False | ||||||
|  |         self.auto_reload = False | ||||||
|         self.blueprints: Dict[str, Blueprint] = {} |         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.configure_logging = configure_logging | ||||||
|         self.ctx = SimpleNamespace() |         self.ctx = ctx or SimpleNamespace() | ||||||
|         self.debug = None |         self.debug = None | ||||||
|         self.error_handler = error_handler or ErrorHandler() |         self.error_handler = error_handler or ErrorHandler() | ||||||
|         self.is_running = False |         self.is_running = False | ||||||
|         self.is_stopping = False |         self.is_stopping = False | ||||||
|         self.listeners: Dict[str, List[ListenerType]] = defaultdict(list) |         self.listeners: Dict[str, List[ListenerType]] = defaultdict(list) | ||||||
|         self.name = name |  | ||||||
|         self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} |         self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} | ||||||
|         self.named_response_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_class = request_class | ||||||
|         self.request_middleware: Deque[MiddlewareType] = deque() |         self.request_middleware: Deque[MiddlewareType] = deque() | ||||||
|         self.response_middleware: Deque[MiddlewareType] = deque() |         self.response_middleware: Deque[MiddlewareType] = deque() | ||||||
| @@ -175,7 +186,6 @@ class Sanic(BaseSanic): | |||||||
|  |  | ||||||
|         if register is not None: |         if register is not None: | ||||||
|             self.config.REGISTER = register |             self.config.REGISTER = register | ||||||
|  |  | ||||||
|         if self.config.REGISTER: |         if self.config.REGISTER: | ||||||
|             self.__class__.register_app(self) |             self.__class__.register_app(self) | ||||||
|  |  | ||||||
| @@ -374,11 +384,19 @@ class Sanic(BaseSanic): | |||||||
|             condition=condition, |             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) |         signal = self.signal_router.name_index.get(event) | ||||||
|         if not signal: |         if not signal: | ||||||
|             raise NotFound("Could not find signal %s" % event) |             if self.config.EVENT_AUTOREGISTER: | ||||||
|         return wait_for(signal.ctx.event.wait(), timeout=timeout) |                 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): |     def enable_websocket(self, enable=True): | ||||||
|         """Enable or disable the support for websocket. |         """Enable or disable the support for websocket. | ||||||
| @@ -402,7 +420,33 @@ class Sanic(BaseSanic): | |||||||
|         """ |         """ | ||||||
|         if isinstance(blueprint, (list, tuple, BlueprintGroup)): |         if isinstance(blueprint, (list, tuple, BlueprintGroup)): | ||||||
|             for item in blueprint: |             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 |             return | ||||||
|         if blueprint.name in self.blueprints: |         if blueprint.name in self.blueprints: | ||||||
|             assert self.blueprints[blueprint.name] is blueprint, ( |             assert self.blueprints[blueprint.name] is blueprint, ( | ||||||
| @@ -567,7 +611,12 @@ class Sanic(BaseSanic): | |||||||
|             # determine if the parameter supplied by the caller |             # determine if the parameter supplied by the caller | ||||||
|             # passes the test in the URL |             # passes the test in the URL | ||||||
|             if param_info.pattern: |             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 not passes_pattern: | ||||||
|                     if param_info.cast != str: |                     if param_info.cast != str: | ||||||
|                         msg = ( |                         msg = ( | ||||||
| @@ -575,13 +624,13 @@ class Sanic(BaseSanic): | |||||||
|                             f"for parameter `{param_info.name}` does " |                             f"for parameter `{param_info.name}` does " | ||||||
|                             "not match pattern for type " |                             "not match pattern for type " | ||||||
|                             f"`{param_info.cast.__name__}`: " |                             f"`{param_info.cast.__name__}`: " | ||||||
|                             f"{param_info.pattern.pattern}" |                             f"{pattern.pattern}" | ||||||
|                         ) |                         ) | ||||||
|                     else: |                     else: | ||||||
|                         msg = ( |                         msg = ( | ||||||
|                             f'Value "{supplied_param}" for parameter ' |                             f'Value "{supplied_param}" for parameter ' | ||||||
|                             f"`{param_info.name}` does not satisfy " |                             f"`{param_info.name}` does not satisfy " | ||||||
|                             f"pattern {param_info.pattern.pattern}" |                             f"pattern {pattern.pattern}" | ||||||
|                         ) |                         ) | ||||||
|                     raise URLBuildError(msg) |                     raise URLBuildError(msg) | ||||||
|  |  | ||||||
| @@ -664,11 +713,6 @@ class Sanic(BaseSanic): | |||||||
|         exception handling must be done here |         exception handling must be done here | ||||||
|  |  | ||||||
|         :param request: HTTP Request object |         :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 |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|         # Define `response` var here to remove warnings about |         # Define `response` var here to remove warnings about | ||||||
| @@ -677,7 +721,9 @@ class Sanic(BaseSanic): | |||||||
|         try: |         try: | ||||||
|             # Fetch handler from router |             # Fetch handler from router | ||||||
|             route, handler, kwargs = self.router.get( |             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 |             request._match_info = kwargs | ||||||
| @@ -725,17 +771,14 @@ class Sanic(BaseSanic): | |||||||
|  |  | ||||||
|             if response: |             if response: | ||||||
|                 response = await request.respond(response) |                 response = await request.respond(response) | ||||||
|             else: |             elif not hasattr(handler, "is_websocket"): | ||||||
|                 response = request.stream.response  # type: ignore |                 response = request.stream.response  # type: ignore | ||||||
|             # Make sure that response is finished / run StreamingHTTP callback |  | ||||||
|  |  | ||||||
|  |             # Make sure that response is finished / run StreamingHTTP callback | ||||||
|             if isinstance(response, BaseHTTPResponse): |             if isinstance(response, BaseHTTPResponse): | ||||||
|                 await response.send(end_stream=True) |                 await response.send(end_stream=True) | ||||||
|             else: |             else: | ||||||
|                 try: |                 if not hasattr(handler, "is_websocket"): | ||||||
|                     # Fastest method for checking if the property exists |  | ||||||
|                     handler.is_websocket  # type: ignore |  | ||||||
|                 except AttributeError: |  | ||||||
|                     raise ServerError( |                     raise ServerError( | ||||||
|                         f"Invalid response type {response!r} " |                         f"Invalid response type {response!r} " | ||||||
|                         "(need HTTPResponse)" |                         "(need HTTPResponse)" | ||||||
| @@ -762,6 +805,7 @@ class Sanic(BaseSanic): | |||||||
|  |  | ||||||
|         if self.asgi: |         if self.asgi: | ||||||
|             ws = request.transport.get_websocket_connection() |             ws = request.transport.get_websocket_connection() | ||||||
|  |             await ws.accept(subprotocols) | ||||||
|         else: |         else: | ||||||
|             protocol = request.transport.get_protocol() |             protocol = request.transport.get_protocol() | ||||||
|             protocol.app = self |             protocol.app = self | ||||||
| @@ -834,6 +878,7 @@ class Sanic(BaseSanic): | |||||||
|         access_log: Optional[bool] = None, |         access_log: Optional[bool] = None, | ||||||
|         unix: Optional[str] = None, |         unix: Optional[str] = None, | ||||||
|         loop: None = None, |         loop: None = None, | ||||||
|  |         reload_dir: Optional[Union[List[str], str]] = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """ |         """ | ||||||
|         Run the HTTP Server and listen until keyboard interrupt or term |         Run the HTTP Server and listen until keyboard interrupt or term | ||||||
| @@ -868,6 +913,18 @@ class Sanic(BaseSanic): | |||||||
|         :type unix: str |         :type unix: str | ||||||
|         :return: Nothing |         :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: |         if loop is not None: | ||||||
|             raise TypeError( |             raise TypeError( | ||||||
|                 "loop is not a valid argument. To use an existing loop, " |                 "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: |         if auto_reload or auto_reload is None and debug: | ||||||
|  |             self.auto_reload = True | ||||||
|             if os.environ.get("SANIC_SERVER_RUNNING") != "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: |         if sock is None: | ||||||
|             host, port = host or "127.0.0.1", port or 8000 |             host, port = host or "127.0.0.1", port or 8000 | ||||||
| @@ -1177,6 +1235,10 @@ class Sanic(BaseSanic): | |||||||
|             else: |             else: | ||||||
|                 logger.info(f"Goin' Fast @ {proto}://{host}:{port}") |                 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 |         return server_settings | ||||||
|  |  | ||||||
|     def _build_endpoint_name(self, *parts): |     def _build_endpoint_name(self, *parts): | ||||||
|   | |||||||
| @@ -140,7 +140,6 @@ class ASGIApp: | |||||||
|                 instance.ws = instance.transport.create_websocket_connection( |                 instance.ws = instance.transport.create_websocket_connection( | ||||||
|                     send, receive |                     send, receive | ||||||
|                 ) |                 ) | ||||||
|                 await instance.ws.accept() |  | ||||||
|             else: |             else: | ||||||
|                 raise ServerError("Received unknown ASGI scope") |                 raise ServerError("Received unknown ASGI scope") | ||||||
|  |  | ||||||
| @@ -164,10 +163,12 @@ class ASGIApp: | |||||||
|         Read and stream the body in chunks from an incoming ASGI message. |         Read and stream the body in chunks from an incoming ASGI message. | ||||||
|         """ |         """ | ||||||
|         message = await self.transport.receive() |         message = await self.transport.receive() | ||||||
|  |         body = message.get("body", b"") | ||||||
|         if not message.get("more_body", False): |         if not message.get("more_body", False): | ||||||
|             self.request_body = False |             self.request_body = False | ||||||
|             return None |             if not body: | ||||||
|         return message.get("body", b"") |                 return None | ||||||
|  |         return body | ||||||
|  |  | ||||||
|     async def __aiter__(self): |     async def __aiter__(self): | ||||||
|         while self.request_body: |         while self.request_body: | ||||||
|   | |||||||
| @@ -1,6 +1,9 @@ | |||||||
|  | import re | ||||||
|  |  | ||||||
| from typing import Any, Tuple | from typing import Any, Tuple | ||||||
| from warnings import warn | from warnings import warn | ||||||
|  |  | ||||||
|  | from sanic.exceptions import SanicException | ||||||
| from sanic.mixins.exceptions import ExceptionMixin | from sanic.mixins.exceptions import ExceptionMixin | ||||||
| from sanic.mixins.listeners import ListenerMixin | from sanic.mixins.listeners import ListenerMixin | ||||||
| from sanic.mixins.middleware import MiddlewareMixin | from sanic.mixins.middleware import MiddlewareMixin | ||||||
| @@ -8,6 +11,9 @@ from sanic.mixins.routes import RouteMixin | |||||||
| from sanic.mixins.signals import SignalMixin | from sanic.mixins.signals import SignalMixin | ||||||
|  |  | ||||||
|  |  | ||||||
|  | VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$") | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseSanic( | class BaseSanic( | ||||||
|     RouteMixin, |     RouteMixin, | ||||||
|     MiddlewareMixin, |     MiddlewareMixin, | ||||||
| @@ -17,7 +23,25 @@ class BaseSanic( | |||||||
| ): | ): | ||||||
|     __fake_slots__: Tuple[str, ...] |     __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__: |         for base in BaseSanic.__bases__: | ||||||
|             base.__init__(self, *args, **kwargs)  # type: ignore |             base.__init__(self, *args, **kwargs)  # type: ignore | ||||||
|  |  | ||||||
| @@ -36,6 +60,7 @@ class BaseSanic( | |||||||
|                 f"Setting variables on {self.__class__.__name__} instances is " |                 f"Setting variables on {self.__class__.__name__} instances is " | ||||||
|                 "deprecated and will be removed in version 21.9. You should " |                 "deprecated and will be removed in version 21.9. You should " | ||||||
|                 f"change your {self.__class__.__name__} instance to use " |                 f"change your {self.__class__.__name__} instance to use " | ||||||
|                 f"instance.ctx.{name} instead." |                 f"instance.ctx.{name} instead.", | ||||||
|  |                 DeprecationWarning, | ||||||
|             ) |             ) | ||||||
|         super().__setattr__(name, value) |         super().__setattr__(name, value) | ||||||
|   | |||||||
| @@ -1,7 +1,11 @@ | |||||||
| from collections.abc import MutableSequence | from __future__ import annotations | ||||||
| from typing import List, Optional, Union |  | ||||||
|  |  | ||||||
| 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): | class BlueprintGroup(MutableSequence): | ||||||
| @@ -54,9 +58,21 @@ class BlueprintGroup(MutableSequence): | |||||||
|         app.blueprint(bpg) |         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 |         Create a new Blueprint Group | ||||||
|  |  | ||||||
| @@ -65,13 +81,14 @@ class BlueprintGroup(MutableSequence): | |||||||
|             inherited by each of the Blueprint |             inherited by each of the Blueprint | ||||||
|         :param strict_slashes: URL Strict slash behavior indicator |         :param strict_slashes: URL Strict slash behavior indicator | ||||||
|         """ |         """ | ||||||
|         self._blueprints = [] |         self._blueprints: List[Blueprint] = [] | ||||||
|         self._url_prefix = url_prefix |         self._url_prefix = url_prefix | ||||||
|         self._version = version |         self._version = version | ||||||
|  |         self._version_prefix = version_prefix | ||||||
|         self._strict_slashes = strict_slashes |         self._strict_slashes = strict_slashes | ||||||
|  |  | ||||||
|     @property |     @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 |         Retrieve the URL prefix being used for the Current Blueprint Group | ||||||
|  |  | ||||||
| @@ -80,7 +97,7 @@ class BlueprintGroup(MutableSequence): | |||||||
|         return self._url_prefix |         return self._url_prefix | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def blueprints(self) -> List["sanic.Blueprint"]: |     def blueprints(self) -> List[Blueprint]: | ||||||
|         """ |         """ | ||||||
|         Retrieve a list of all the available blueprints under this group. |         Retrieve a list of all the available blueprints under this group. | ||||||
|  |  | ||||||
| @@ -107,6 +124,15 @@ class BlueprintGroup(MutableSequence): | |||||||
|         """ |         """ | ||||||
|         return self._strict_slashes |         return self._strict_slashes | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def version_prefix(self) -> str: | ||||||
|  |         """ | ||||||
|  |         Version prefix; defaults to ``/v`` | ||||||
|  |  | ||||||
|  |         :return: str | ||||||
|  |         """ | ||||||
|  |         return self._version_prefix | ||||||
|  |  | ||||||
|     def __iter__(self): |     def __iter__(self): | ||||||
|         """ |         """ | ||||||
|         Tun the class Blueprint Group into an Iterable item |         Tun the class Blueprint Group into an Iterable item | ||||||
| @@ -161,34 +187,16 @@ class BlueprintGroup(MutableSequence): | |||||||
|         """ |         """ | ||||||
|         return len(self._blueprints) |         return len(self._blueprints) | ||||||
|  |  | ||||||
|     def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint": |     def append(self, value: Blueprint) -> None: | ||||||
|         """ |  | ||||||
|         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: |  | ||||||
|         """ |         """ | ||||||
|         The Abstract class `MutableSequence` leverages this append method to |         The Abstract class `MutableSequence` leverages this append method to | ||||||
|         perform the `BlueprintGroup.append` operation. |         perform the `BlueprintGroup.append` operation. | ||||||
|         :param value: New `Blueprint` object. |         :param value: New `Blueprint` object. | ||||||
|         :return: None |         :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 |         The Abstract class `MutableSequence` leverages this insert method to | ||||||
|         perform the `BlueprintGroup.append` operation. |         perform the `BlueprintGroup.append` operation. | ||||||
| @@ -197,7 +205,7 @@ class BlueprintGroup(MutableSequence): | |||||||
|         :param item: New `Blueprint` object. |         :param item: New `Blueprint` object. | ||||||
|         :return: None |         :return: None | ||||||
|         """ |         """ | ||||||
|         self._blueprints.insert(index, self._sanitize_blueprint(item)) |         self._blueprints.insert(index, item) | ||||||
|  |  | ||||||
|     def middleware(self, *args, **kwargs): |     def middleware(self, *args, **kwargs): | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -62,18 +62,20 @@ class Blueprint(BaseSanic): | |||||||
|         "strict_slashes", |         "strict_slashes", | ||||||
|         "url_prefix", |         "url_prefix", | ||||||
|         "version", |         "version", | ||||||
|  |         "version_prefix", | ||||||
|         "websocket_routes", |         "websocket_routes", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         name: str, |         name: str = None, | ||||||
|         url_prefix: Optional[str] = None, |         url_prefix: Optional[str] = None, | ||||||
|         host: Optional[str] = None, |         host: Optional[str] = None, | ||||||
|         version: Optional[int] = None, |         version: Optional[Union[int, str, float]] = None, | ||||||
|         strict_slashes: Optional[bool] = None, |         strict_slashes: Optional[bool] = None, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         super().__init__() |         super().__init__(name=name) | ||||||
|  |  | ||||||
|         self._apps: Set[Sanic] = set() |         self._apps: Set[Sanic] = set() | ||||||
|         self.ctx = SimpleNamespace() |         self.ctx = SimpleNamespace() | ||||||
| @@ -81,12 +83,16 @@ class Blueprint(BaseSanic): | |||||||
|         self.host = host |         self.host = host | ||||||
|         self.listeners: Dict[str, List[ListenerType]] = {} |         self.listeners: Dict[str, List[ListenerType]] = {} | ||||||
|         self.middlewares: List[MiddlewareType] = [] |         self.middlewares: List[MiddlewareType] = [] | ||||||
|         self.name = name |  | ||||||
|         self.routes: List[Route] = [] |         self.routes: List[Route] = [] | ||||||
|         self.statics: List[RouteHandler] = [] |         self.statics: List[RouteHandler] = [] | ||||||
|         self.strict_slashes = strict_slashes |         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 = version | ||||||
|  |         self.version_prefix = version_prefix | ||||||
|         self.websocket_routes: List[Route] = [] |         self.websocket_routes: List[Route] = [] | ||||||
|  |  | ||||||
|     def __repr__(self) -> str: |     def __repr__(self) -> str: | ||||||
| @@ -139,7 +145,13 @@ class Blueprint(BaseSanic): | |||||||
|         return super().signal(event, *args, **kwargs) |         return super().signal(event, *args, **kwargs) | ||||||
|  |  | ||||||
|     @staticmethod |     @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 |         Create a list of blueprints, optionally grouping them under a | ||||||
|         general URL prefix. |         general URL prefix. | ||||||
| @@ -156,8 +168,6 @@ class Blueprint(BaseSanic): | |||||||
|             for i in nested: |             for i in nested: | ||||||
|                 if isinstance(i, (list, tuple)): |                 if isinstance(i, (list, tuple)): | ||||||
|                     yield from chain(i) |                     yield from chain(i) | ||||||
|                 elif isinstance(i, BlueprintGroup): |  | ||||||
|                     yield from i.blueprints |  | ||||||
|                 else: |                 else: | ||||||
|                     yield i |                     yield i | ||||||
|  |  | ||||||
| @@ -165,6 +175,7 @@ class Blueprint(BaseSanic): | |||||||
|             url_prefix=url_prefix, |             url_prefix=url_prefix, | ||||||
|             version=version, |             version=version, | ||||||
|             strict_slashes=strict_slashes, |             strict_slashes=strict_slashes, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         ) |         ) | ||||||
|         for bp in chain(blueprints): |         for bp in chain(blueprints): | ||||||
|             bps.append(bp) |             bps.append(bp) | ||||||
| @@ -182,6 +193,9 @@ class Blueprint(BaseSanic): | |||||||
|  |  | ||||||
|         self._apps.add(app) |         self._apps.add(app) | ||||||
|         url_prefix = options.get("url_prefix", self.url_prefix) |         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 = [] |         routes = [] | ||||||
|         middleware = [] |         middleware = [] | ||||||
| @@ -196,12 +210,22 @@ class Blueprint(BaseSanic): | |||||||
|             # Prepend the blueprint URI prefix if available |             # Prepend the blueprint URI prefix if available | ||||||
|             uri = url_prefix + future.uri if url_prefix else future.uri |             uri = url_prefix + future.uri if url_prefix else future.uri | ||||||
|  |  | ||||||
|             strict_slashes = ( |             version_prefix = self.version_prefix | ||||||
|                 self.strict_slashes |             for prefix in ( | ||||||
|                 if future.strict_slashes is None |                 future.version_prefix, | ||||||
|                 and self.strict_slashes is not None |                 opt_version_prefix, | ||||||
|                 else future.strict_slashes |             ): | ||||||
|  |                 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) |             name = app._generate_name(future.name) | ||||||
|  |  | ||||||
|             apply_route = FutureRoute( |             apply_route = FutureRoute( | ||||||
| @@ -211,13 +235,14 @@ class Blueprint(BaseSanic): | |||||||
|                 future.host or self.host, |                 future.host or self.host, | ||||||
|                 strict_slashes, |                 strict_slashes, | ||||||
|                 future.stream, |                 future.stream, | ||||||
|                 future.version or self.version, |                 version, | ||||||
|                 name, |                 name, | ||||||
|                 future.ignore_body, |                 future.ignore_body, | ||||||
|                 future.websocket, |                 future.websocket, | ||||||
|                 future.subprotocols, |                 future.subprotocols, | ||||||
|                 future.unquote, |                 future.unquote, | ||||||
|                 future.static, |                 future.static, | ||||||
|  |                 version_prefix, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             route = app._apply_route(apply_route) |             route = app._apply_route(apply_route) | ||||||
| @@ -254,8 +279,6 @@ class Blueprint(BaseSanic): | |||||||
|             app._apply_signal(signal) |             app._apply_signal(signal) | ||||||
|  |  | ||||||
|         self.routes = [route for route in routes if isinstance(route, Route)] |         self.routes = [route for route in routes if isinstance(route, Route)] | ||||||
|  |  | ||||||
|         # Deprecate these in 21.6 |  | ||||||
|         self.websocket_routes = [ |         self.websocket_routes = [ | ||||||
|             route for route in self.routes if route.ctx.websocket |             route for route in self.routes if route.ctx.websocket | ||||||
|         ] |         ] | ||||||
| @@ -284,3 +307,12 @@ class Blueprint(BaseSanic): | |||||||
|             return_when=asyncio.FIRST_COMPLETED, |             return_when=asyncio.FIRST_COMPLETED, | ||||||
|             timeout=timeout, |             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 inspect import isclass | ||||||
| from os import environ | from os import environ | ||||||
| from pathlib import Path | 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 | from .utils import load_module_from_file_location, str_to_bool | ||||||
|  |  | ||||||
| @@ -15,33 +18,64 @@ BASE_LOGO = """ | |||||||
| """ | """ | ||||||
|  |  | ||||||
| DEFAULT_CONFIG = { | DEFAULT_CONFIG = { | ||||||
|     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes |     "ACCESS_LOG": True, | ||||||
|     "REQUEST_BUFFER_QUEUE_SIZE": 100, |     "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_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 |     "REQUEST_TIMEOUT": 60,  # 60 seconds | ||||||
|     "RESPONSE_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_QUEUE": 32, | ||||||
|  |     "WEBSOCKET_MAX_SIZE": 2 ** 20,  # 1 megabyte | ||||||
|  |     "WEBSOCKET_PING_INTERVAL": 20, | ||||||
|  |     "WEBSOCKET_PING_TIMEOUT": 20, | ||||||
|     "WEBSOCKET_READ_LIMIT": 2 ** 16, |     "WEBSOCKET_READ_LIMIT": 2 ** 16, | ||||||
|     "WEBSOCKET_WRITE_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): | 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 {} |         defaults = defaults or {} | ||||||
|         super().__init__({**DEFAULT_CONFIG, **defaults}) |         super().__init__({**DEFAULT_CONFIG, **defaults}) | ||||||
|  |  | ||||||
| @@ -50,9 +84,22 @@ class Config(dict): | |||||||
|         if keep_alive is not None: |         if keep_alive is not None: | ||||||
|             self.KEEP_ALIVE = keep_alive |             self.KEEP_ALIVE = keep_alive | ||||||
|  |  | ||||||
|         if load_env: |         if env_prefix != SANIC_PREFIX: | ||||||
|             prefix = SANIC_PREFIX if load_env is True else load_env |             if env_prefix: | ||||||
|             self.load_environment_vars(prefix=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): |     def __getattr__(self, attr): | ||||||
|         try: |         try: | ||||||
| @@ -62,6 +109,19 @@ class Config(dict): | |||||||
|  |  | ||||||
|     def __setattr__(self, attr, value): |     def __setattr__(self, attr, value): | ||||||
|         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): |     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" | DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" | ||||||
|   | |||||||
| @@ -366,7 +366,7 @@ def exception_response( | |||||||
|                 except InvalidUsage: |                 except InvalidUsage: | ||||||
|                     renderer = HTMLRenderer |                     renderer = HTMLRenderer | ||||||
|  |  | ||||||
|                 content_type, *_ = request.headers.get( |                 content_type, *_ = request.headers.getone( | ||||||
|                     "content-type", "" |                     "content-type", "" | ||||||
|                 ).split(";") |                 ).split(";") | ||||||
|                 renderer = RENDERERS_BY_CONTENT_TYPE.get( |                 renderer = RENDERERS_BY_CONTENT_TYPE.get( | ||||||
|   | |||||||
| @@ -3,26 +3,18 @@ from typing import Optional, Union | |||||||
| from sanic.helpers import STATUS_CODES | 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): | 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) |         super().__init__(message) | ||||||
|  |  | ||||||
|         if status_code is not None: |         if status_code is not None: | ||||||
| @@ -33,45 +25,45 @@ class SanicException(Exception): | |||||||
|             self.quiet = True |             self.quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(404) |  | ||||||
| class NotFound(SanicException): | class NotFound(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 404 Not Found |     **Status**: 404 Not Found | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 404 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(400) |  | ||||||
| class InvalidUsage(SanicException): | class InvalidUsage(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 400 Bad Request |     **Status**: 400 Bad Request | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 400 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(405) |  | ||||||
| class MethodNotSupported(SanicException): | class MethodNotSupported(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 405 Method Not Allowed |     **Status**: 405 Method Not Allowed | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     status_code = 405 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|     def __init__(self, message, method, allowed_methods): |     def __init__(self, message, method, allowed_methods): | ||||||
|         super().__init__(message) |         super().__init__(message) | ||||||
|         self.headers = {"Allow": ", ".join(allowed_methods)} |         self.headers = {"Allow": ", ".join(allowed_methods)} | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(500) |  | ||||||
| class ServerError(SanicException): | class ServerError(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 500 Internal Server Error |     **Status**: 500 Internal Server Error | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 500 | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(503) |  | ||||||
| class ServiceUnavailable(SanicException): | class ServiceUnavailable(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 503 Service Unavailable |     **Status**: 503 Service Unavailable | ||||||
| @@ -80,7 +72,8 @@ class ServiceUnavailable(SanicException): | |||||||
|     down for maintenance). Generally, this is a temporary state. |     down for maintenance). Generally, this is a temporary state. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 503 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| class URLBuildError(ServerError): | class URLBuildError(ServerError): | ||||||
| @@ -88,7 +81,7 @@ class URLBuildError(ServerError): | |||||||
|     **Status**: 500 Internal Server Error |     **Status**: 500 Internal Server Error | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 500 | ||||||
|  |  | ||||||
|  |  | ||||||
| class FileNotFound(NotFound): | class FileNotFound(NotFound): | ||||||
| @@ -102,7 +95,6 @@ class FileNotFound(NotFound): | |||||||
|         self.relative_url = relative_url |         self.relative_url = relative_url | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(408) |  | ||||||
| class RequestTimeout(SanicException): | class RequestTimeout(SanicException): | ||||||
|     """The Web server (running the Web site) thinks that there has been too |     """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 |     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. |     server has 'timed out' on that particular socket connection. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 408 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(413) |  | ||||||
| class PayloadTooLarge(SanicException): | class PayloadTooLarge(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 413 Payload Too Large |     **Status**: 413 Payload Too Large | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 413 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| class HeaderNotFound(InvalidUsage): | class HeaderNotFound(InvalidUsage): | ||||||
| @@ -129,36 +122,39 @@ class HeaderNotFound(InvalidUsage): | |||||||
|     **Status**: 400 Bad Request |     **Status**: 400 Bad Request | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 400 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(416) |  | ||||||
| class ContentRangeError(SanicException): | class ContentRangeError(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 416 Range Not Satisfiable |     **Status**: 416 Range Not Satisfiable | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     status_code = 416 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|     def __init__(self, message, content_range): |     def __init__(self, message, content_range): | ||||||
|         super().__init__(message) |         super().__init__(message) | ||||||
|         self.headers = {"Content-Range": f"bytes */{content_range.total}"} |         self.headers = {"Content-Range": f"bytes */{content_range.total}"} | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(417) |  | ||||||
| class HeaderExpectationFailed(SanicException): | class HeaderExpectationFailed(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 417 Expectation Failed |     **Status**: 417 Expectation Failed | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 417 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(403) |  | ||||||
| class Forbidden(SanicException): | class Forbidden(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 403 Forbidden |     **Status**: 403 Forbidden | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 403 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| class InvalidRangeType(ContentRangeError): | class InvalidRangeType(ContentRangeError): | ||||||
| @@ -166,7 +162,8 @@ class InvalidRangeType(ContentRangeError): | |||||||
|     **Status**: 416 Range Not Satisfiable |     **Status**: 416 Range Not Satisfiable | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |     status_code = 416 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| class PyFileError(Exception): | class PyFileError(Exception): | ||||||
| @@ -174,7 +171,6 @@ class PyFileError(Exception): | |||||||
|         super().__init__("could not execute config file %s", file) |         super().__init__("could not execute config file %s", file) | ||||||
|  |  | ||||||
|  |  | ||||||
| @add_status_code(401) |  | ||||||
| class Unauthorized(SanicException): | class Unauthorized(SanicException): | ||||||
|     """ |     """ | ||||||
|     **Status**: 401 Unauthorized |     **Status**: 401 Unauthorized | ||||||
| @@ -210,6 +206,9 @@ class Unauthorized(SanicException): | |||||||
|                            realm="Restricted Area") |                            realm="Restricted Area") | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     status_code = 401 | ||||||
|  |     quiet = True | ||||||
|  |  | ||||||
|     def __init__(self, message, status_code=None, scheme=None, **kwargs): |     def __init__(self, message, status_code=None, scheme=None, **kwargs): | ||||||
|         super().__init__(message, status_code) |         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 status_code: The HTTP status code to return. | ||||||
|     :param message: The HTTP response body. Defaults to the messages in |     :param message: The HTTP response body. Defaults to the messages in | ||||||
|     """ |     """ | ||||||
|     if message is None: |     import warnings | ||||||
|         msg: bytes = STATUS_CODES[status_code] |  | ||||||
|         # These are stored as bytes in the STATUS_CODES dict |     warnings.warn( | ||||||
|         message = msg.decode("utf8") |         "sanic.exceptions.abort has been marked as deprecated, and will be " | ||||||
|     sanic_exception = _sanic_exceptions.get(status_code, SanicException) |         "removed in release 21.12.\n To migrate your code, simply replace " | ||||||
|     raise sanic_exception(message=message, status_code=status_code) |         "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, |     HeaderNotFound, | ||||||
|     InvalidRangeType, |     InvalidRangeType, | ||||||
| ) | ) | ||||||
| from sanic.log import logger | from sanic.log import error_logger | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -25,7 +25,6 @@ class ErrorHandler: | |||||||
|  |  | ||||||
|     handlers = None |     handlers = None | ||||||
|     cached_handlers = None |     cached_handlers = None | ||||||
|     _missing = object() |  | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.handlers = [] |         self.handlers = [] | ||||||
| @@ -45,7 +44,9 @@ class ErrorHandler: | |||||||
|  |  | ||||||
|         :return: None |         :return: None | ||||||
|         """ |         """ | ||||||
|  |         # self.handlers to be deprecated and removed in version 21.12 | ||||||
|         self.handlers.append((exception, handler)) |         self.handlers.append((exception, handler)) | ||||||
|  |         self.cached_handlers[exception] = handler | ||||||
|  |  | ||||||
|     def lookup(self, exception): |     def lookup(self, exception): | ||||||
|         """ |         """ | ||||||
| @@ -61,14 +62,19 @@ class ErrorHandler: | |||||||
|  |  | ||||||
|         :return: Registered function if found ``None`` otherwise |         :return: Registered function if found ``None`` otherwise | ||||||
|         """ |         """ | ||||||
|         handler = self.cached_handlers.get(type(exception), self._missing) |         exception_class = type(exception) | ||||||
|         if handler is self._missing: |         if exception_class in self.cached_handlers: | ||||||
|             for exception_class, handler in self.handlers: |             return self.cached_handlers[exception_class] | ||||||
|                 if isinstance(exception, exception_class): |  | ||||||
|                     self.cached_handlers[type(exception)] = handler |         for ancestor in type.mro(exception_class): | ||||||
|                     return handler |             if ancestor in self.cached_handlers: | ||||||
|             self.cached_handlers[type(exception)] = None |                 handler = self.cached_handlers[ancestor] | ||||||
|             handler = None |                 self.cached_handlers[exception_class] = handler | ||||||
|  |                 return handler | ||||||
|  |             if ancestor is BaseException: | ||||||
|  |                 break | ||||||
|  |         self.cached_handlers[exception_class] = None | ||||||
|  |         handler = None | ||||||
|         return handler |         return handler | ||||||
|  |  | ||||||
|     def response(self, request, exception): |     def response(self, request, exception): | ||||||
| @@ -101,7 +107,7 @@ class ErrorHandler: | |||||||
|             response_message = ( |             response_message = ( | ||||||
|                 "Exception raised in exception handler " '"%s" for uri: %s' |                 "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: |             if self.debug: | ||||||
|                 return text(response_message % (handler.__name__, url), 500) |                 return text(response_message % (handler.__name__, url), 500) | ||||||
| @@ -137,7 +143,9 @@ class ErrorHandler: | |||||||
|                 url = "unknown" |                 url = "unknown" | ||||||
|  |  | ||||||
|             self.log(format_exc()) |             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) |         return exception_response(request, exception, self.debug) | ||||||
|  |  | ||||||
| @@ -165,7 +173,7 @@ class ContentRangeHandler: | |||||||
|  |  | ||||||
|     def __init__(self, request, stats): |     def __init__(self, request, stats): | ||||||
|         self.total = stats.st_size |         self.total = stats.st_size | ||||||
|         _range = request.headers.get("Range") |         _range = request.headers.getone("range", None) | ||||||
|         if _range is None: |         if _range is None: | ||||||
|             raise HeaderNotFound("Range Header Not Found") |             raise HeaderNotFound("Range Header Not Found") | ||||||
|         unit, _, value = tuple(map(str.strip, _range.partition("="))) |         unit, _, value = tuple(map(str.strip, _range.partition("="))) | ||||||
|   | |||||||
| @@ -102,7 +102,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]: | |||||||
|     """Parse traditional proxy headers.""" |     """Parse traditional proxy headers.""" | ||||||
|     real_ip_header = config.REAL_IP_HEADER |     real_ip_header = config.REAL_IP_HEADER | ||||||
|     proxies_count = config.PROXIES_COUNT |     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: |     if not addr and proxies_count: | ||||||
|         assert proxies_count > 0 |         assert proxies_count > 0 | ||||||
|         try: |         try: | ||||||
| @@ -131,7 +131,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]: | |||||||
|             ("port", "x-forwarded-port"), |             ("port", "x-forwarded-port"), | ||||||
|             ("path", "x-forwarded-path"), |             ("path", "x-forwarded-path"), | ||||||
|         ): |         ): | ||||||
|             yield key, headers.get(header) |             yield key, headers.getone(header, None) | ||||||
|  |  | ||||||
|     return fwd_normalize(options()) |     return fwd_normalize(options()) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ from sanic.exceptions import ( | |||||||
| ) | ) | ||||||
| from sanic.headers import format_http1_response | from sanic.headers import format_http1_response | ||||||
| from sanic.helpers import has_message_body | 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): | class Stage(Enum): | ||||||
| @@ -64,6 +64,9 @@ class Http: | |||||||
|     :raises RuntimeError: |     :raises RuntimeError: | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     HEADER_CEILING = 16_384 | ||||||
|  |     HEADER_MAX_SIZE = 0 | ||||||
|  |  | ||||||
|     __slots__ = [ |     __slots__ = [ | ||||||
|         "_send", |         "_send", | ||||||
|         "_receive_more", |         "_receive_more", | ||||||
| @@ -82,6 +85,7 @@ class Http: | |||||||
|         "request_max_size", |         "request_max_size", | ||||||
|         "response", |         "response", | ||||||
|         "response_func", |         "response_func", | ||||||
|  |         "response_size", | ||||||
|         "response_bytes_left", |         "response_bytes_left", | ||||||
|         "upgrade_websocket", |         "upgrade_websocket", | ||||||
|     ] |     ] | ||||||
| @@ -91,19 +95,23 @@ class Http: | |||||||
|         self._receive_more = protocol.receive_more |         self._receive_more = protocol.receive_more | ||||||
|         self.recv_buffer = protocol.recv_buffer |         self.recv_buffer = protocol.recv_buffer | ||||||
|         self.protocol = protocol |         self.protocol = protocol | ||||||
|         self.expecting_continue: bool = False |         self.keep_alive = True | ||||||
|         self.stage: Stage = Stage.IDLE |         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_body = None | ||||||
|         self.request_bytes = None |         self.request_bytes = None | ||||||
|         self.request_bytes_left = None |         self.request_bytes_left = None | ||||||
|         self.request_max_size = protocol.request_max_size |         self.request_max_size = self.protocol.request_max_size | ||||||
|         self.keep_alive = True |  | ||||||
|         self.head_only = None |  | ||||||
|         self.request: Request = None |         self.request: Request = None | ||||||
|         self.response: BaseHTTPResponse = None |         self.response: BaseHTTPResponse = None | ||||||
|         self.exception = None |  | ||||||
|         self.url = None |  | ||||||
|         self.upgrade_websocket = False |         self.upgrade_websocket = False | ||||||
|  |         self.url = None | ||||||
|  |  | ||||||
|     def __bool__(self): |     def __bool__(self): | ||||||
|         """Test if request handling is in progress""" |         """Test if request handling is in progress""" | ||||||
| @@ -143,8 +151,11 @@ class Http: | |||||||
|             # Try to consume any remaining request body |             # Try to consume any remaining request body | ||||||
|             if self.request_body: |             if self.request_body: | ||||||
|                 if self.response and 200 <= self.response.status < 300: |                 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: |                 try: | ||||||
|                     async for _ in self: |                     async for _ in self: | ||||||
|                         pass |                         pass | ||||||
| @@ -156,11 +167,19 @@ class Http: | |||||||
|                     await sleep(0.001) |                     await sleep(0.001) | ||||||
|                     self.keep_alive = False |                     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 |             # Exit and disconnect if no more requests can be taken | ||||||
|             if self.stage is not Stage.IDLE or not self.keep_alive: |             if self.stage is not Stage.IDLE or not self.keep_alive: | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
|             # Wait for next request |             # Wait for the next request | ||||||
|             if not self.recv_buffer: |             if not self.recv_buffer: | ||||||
|                 await self._receive_more() |                 await self._receive_more() | ||||||
|  |  | ||||||
| @@ -168,7 +187,6 @@ class Http: | |||||||
|         """ |         """ | ||||||
|         Receive and parse request header into self.request. |         Receive and parse request header into self.request. | ||||||
|         """ |         """ | ||||||
|         HEADER_MAX_SIZE = min(8192, self.request_max_size) |  | ||||||
|         # Receive until full header is in buffer |         # Receive until full header is in buffer | ||||||
|         buf = self.recv_buffer |         buf = self.recv_buffer | ||||||
|         pos = 0 |         pos = 0 | ||||||
| @@ -179,12 +197,12 @@ class Http: | |||||||
|                 break |                 break | ||||||
|  |  | ||||||
|             pos = max(0, len(buf) - 3) |             pos = max(0, len(buf) - 3) | ||||||
|             if pos >= HEADER_MAX_SIZE: |             if pos >= self.HEADER_MAX_SIZE: | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
|             await self._receive_more() |             await self._receive_more() | ||||||
|  |  | ||||||
|         if pos >= HEADER_MAX_SIZE: |         if pos >= self.HEADER_MAX_SIZE: | ||||||
|             raise PayloadTooLarge("Request header exceeds the size limit") |             raise PayloadTooLarge("Request header exceeds the size limit") | ||||||
|  |  | ||||||
|         # Parse header content |         # Parse header content | ||||||
| @@ -218,7 +236,9 @@ class Http: | |||||||
|             raise InvalidUsage("Bad Request") |             raise InvalidUsage("Bad Request") | ||||||
|  |  | ||||||
|         headers_instance = Header(headers) |         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 |         # Prepare a Request object | ||||||
|         request = self.protocol.request_class( |         request = self.protocol.request_class( | ||||||
| @@ -235,7 +255,7 @@ class Http: | |||||||
|         self.request_bytes_left = self.request_bytes = 0 |         self.request_bytes_left = self.request_bytes = 0 | ||||||
|         if request_body: |         if request_body: | ||||||
|             headers = request.headers |             headers = request.headers | ||||||
|             expect = headers.get("expect") |             expect = headers.getone("expect", None) | ||||||
|  |  | ||||||
|             if expect is not None: |             if expect is not None: | ||||||
|                 if expect.lower() == "100-continue": |                 if expect.lower() == "100-continue": | ||||||
| @@ -243,7 +263,7 @@ class Http: | |||||||
|                 else: |                 else: | ||||||
|                     raise HeaderExpectationFailed(f"Unknown Expect: {expect}") |                     raise HeaderExpectationFailed(f"Unknown Expect: {expect}") | ||||||
|  |  | ||||||
|             if headers.get("transfer-encoding") == "chunked": |             if headers.getone("transfer-encoding", None) == "chunked": | ||||||
|                 self.request_body = "chunked" |                 self.request_body = "chunked" | ||||||
|                 pos -= 2  # One CRLF stays in buffer |                 pos -= 2  # One CRLF stays in buffer | ||||||
|             else: |             else: | ||||||
| @@ -270,6 +290,7 @@ class Http: | |||||||
|         size = len(data) |         size = len(data) | ||||||
|         headers = res.headers |         headers = res.headers | ||||||
|         status = res.status |         status = res.status | ||||||
|  |         self.response_size = size | ||||||
|  |  | ||||||
|         if not isinstance(status, int) or status < 200: |         if not isinstance(status, int) or status < 200: | ||||||
|             raise RuntimeError(f"Invalid response status {status!r}") |             raise RuntimeError(f"Invalid response status {status!r}") | ||||||
| @@ -424,7 +445,9 @@ class Http: | |||||||
|         req, res = self.request, self.response |         req, res = self.request, self.response | ||||||
|         extra = { |         extra = { | ||||||
|             "status": getattr(res, "status", 0), |             "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", |             "host": "UNKNOWN", | ||||||
|             "request": "nil", |             "request": "nil", | ||||||
|         } |         } | ||||||
| @@ -478,8 +501,6 @@ class Http: | |||||||
|                 self.keep_alive = False |                 self.keep_alive = False | ||||||
|                 raise InvalidUsage("Bad chunked encoding") |                 raise InvalidUsage("Bad chunked encoding") | ||||||
|  |  | ||||||
|             del buf[: pos + 2] |  | ||||||
|  |  | ||||||
|             if size <= 0: |             if size <= 0: | ||||||
|                 self.request_body = None |                 self.request_body = None | ||||||
|  |  | ||||||
| @@ -487,8 +508,17 @@ class Http: | |||||||
|                     self.keep_alive = False |                     self.keep_alive = False | ||||||
|                     raise InvalidUsage("Bad chunked encoding") |                     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 |                 return None | ||||||
|  |  | ||||||
|  |             # Remove CRLF, chunk size and the CRLF that follows | ||||||
|  |             del buf[: pos + 2] | ||||||
|  |  | ||||||
|             self.request_bytes_left = size |             self.request_bytes_left = size | ||||||
|             self.request_bytes += size |             self.request_bytes += size | ||||||
|  |  | ||||||
| @@ -535,3 +565,10 @@ class Http: | |||||||
|     @property |     @property | ||||||
|     def send(self): |     def send(self): | ||||||
|         return self.response_func |         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. |         Create a listener from a decorated function. | ||||||
|  |  | ||||||
|         To be used as a deocrator: |         To be used as a decorator: | ||||||
|  |  | ||||||
|         .. code-block:: python |         .. code-block:: python | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,10 +26,11 @@ from sanic.views import CompositionView | |||||||
|  |  | ||||||
|  |  | ||||||
| class RouteMixin: | class RouteMixin: | ||||||
|  |     name: str | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         self._future_routes: Set[FutureRoute] = set() |         self._future_routes: Set[FutureRoute] = set() | ||||||
|         self._future_statics: Set[FutureStatic] = set() |         self._future_statics: Set[FutureStatic] = set() | ||||||
|         self.name = "" |  | ||||||
|         self.strict_slashes: Optional[bool] = False |         self.strict_slashes: Optional[bool] = False | ||||||
|  |  | ||||||
|     def _apply_route(self, route: FutureRoute) -> List[Route]: |     def _apply_route(self, route: FutureRoute) -> List[Route]: | ||||||
| @@ -45,7 +46,7 @@ class RouteMixin: | |||||||
|         host: Optional[str] = None, |         host: Optional[str] = None, | ||||||
|         strict_slashes: Optional[bool] = None, |         strict_slashes: Optional[bool] = None, | ||||||
|         stream: bool = False, |         stream: bool = False, | ||||||
|         version: Optional[int] = None, |         version: Optional[Union[int, str, float]] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         ignore_body: bool = False, |         ignore_body: bool = False, | ||||||
|         apply: bool = True, |         apply: bool = True, | ||||||
| @@ -53,6 +54,7 @@ class RouteMixin: | |||||||
|         websocket: bool = False, |         websocket: bool = False, | ||||||
|         unquote: bool = False, |         unquote: bool = False, | ||||||
|         static: bool = False, |         static: bool = False, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Decorate a function to be registered as a route |         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 name: user defined route name for url_for | ||||||
|         :param ignore_body: whether the handler should ignore request |         :param ignore_body: whether the handler should ignore request | ||||||
|             body (eg. GET requests) |             body (eg. GET requests) | ||||||
|  |         :param version_prefix: URL path that should be before the version | ||||||
|  |             value; default: ``/v`` | ||||||
|         :return: tuple of routes, decorated function |         :return: tuple of routes, decorated function | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         # Fix case where the user did not prefix the URL with a / |         # Fix case where the user did not prefix the URL with a / | ||||||
|         # and will probably get confused as to why it's not working |         # 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 |             uri = "/" + uri | ||||||
|  |  | ||||||
|         if strict_slashes is None: |         if strict_slashes is None: | ||||||
| @@ -92,6 +96,7 @@ class RouteMixin: | |||||||
|             nonlocal subprotocols |             nonlocal subprotocols | ||||||
|             nonlocal websocket |             nonlocal websocket | ||||||
|             nonlocal static |             nonlocal static | ||||||
|  |             nonlocal version_prefix | ||||||
|  |  | ||||||
|             if isinstance(handler, tuple): |             if isinstance(handler, tuple): | ||||||
|                 # if a handler fn is already wrapped in a route, the handler |                 # if a handler fn is already wrapped in a route, the handler | ||||||
| @@ -128,6 +133,7 @@ class RouteMixin: | |||||||
|                 subprotocols, |                 subprotocols, | ||||||
|                 unquote, |                 unquote, | ||||||
|                 static, |                 static, | ||||||
|  |                 version_prefix, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             self._future_routes.add(route) |             self._future_routes.add(route) | ||||||
| @@ -154,7 +160,9 @@ class RouteMixin: | |||||||
|             if apply: |             if apply: | ||||||
|                 self._apply_route(route) |                 self._apply_route(route) | ||||||
|  |  | ||||||
|             return route, handler |             if static: | ||||||
|  |                 return route, handler | ||||||
|  |             return handler | ||||||
|  |  | ||||||
|         return decorator |         return decorator | ||||||
|  |  | ||||||
| @@ -168,6 +176,7 @@ class RouteMixin: | |||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         stream: bool = False, |         stream: bool = False, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """A helper method to register class instance or |         """A helper method to register class instance or | ||||||
|         functions as a handler to the application url |         functions as a handler to the application url | ||||||
| @@ -182,6 +191,8 @@ class RouteMixin: | |||||||
|         :param version: |         :param version: | ||||||
|         :param name: user defined route name for url_for |         :param name: user defined route name for url_for | ||||||
|         :param stream: boolean specifying if the handler is a stream handler |         :param stream: boolean specifying if the handler is a stream handler | ||||||
|  |         :param version_prefix: URL path that should be before the version | ||||||
|  |             value; default: ``/v`` | ||||||
|         :return: function or class instance |         :return: function or class instance | ||||||
|         """ |         """ | ||||||
|         # Handle HTTPMethodView differently |         # Handle HTTPMethodView differently | ||||||
| @@ -214,6 +225,7 @@ class RouteMixin: | |||||||
|             stream=stream, |             stream=stream, | ||||||
|             version=version, |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         )(handler) |         )(handler) | ||||||
|         return handler |         return handler | ||||||
|  |  | ||||||
| @@ -226,6 +238,7 @@ class RouteMixin: | |||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         ignore_body: bool = True, |         ignore_body: bool = True, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **GET** *HTTP* method |         Add an API URL under the **GET** *HTTP* method | ||||||
| @@ -236,6 +249,8 @@ class RouteMixin: | |||||||
|             URLs need to terminate with a */* |             URLs need to terminate with a */* | ||||||
|         :param version: API Version |         :param version: API Version | ||||||
|         :param name: Unique name that can be used to identify the Route |         :param name: Unique name that can be used to identify the Route | ||||||
|  |         :param version_prefix: URL path that should be before the version | ||||||
|  |             value; default: ``/v`` | ||||||
|         :return: Object decorated with :func:`route` method |         :return: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -246,6 +261,7 @@ class RouteMixin: | |||||||
|             version=version, |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|             ignore_body=ignore_body, |             ignore_body=ignore_body, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def post( |     def post( | ||||||
| @@ -256,6 +272,7 @@ class RouteMixin: | |||||||
|         stream: bool = False, |         stream: bool = False, | ||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **POST** *HTTP* method |         Add an API URL under the **POST** *HTTP* method | ||||||
| @@ -266,6 +283,8 @@ class RouteMixin: | |||||||
|             URLs need to terminate with a */* |             URLs need to terminate with a */* | ||||||
|         :param version: API Version |         :param version: API Version | ||||||
|         :param name: Unique name that can be used to identify the Route |         :param name: Unique name that can be used to identify the Route | ||||||
|  |         :param version_prefix: URL path that should be before the version | ||||||
|  |             value; default: ``/v`` | ||||||
|         :return: Object decorated with :func:`route` method |         :return: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -276,6 +295,7 @@ class RouteMixin: | |||||||
|             stream=stream, |             stream=stream, | ||||||
|             version=version, |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def put( |     def put( | ||||||
| @@ -286,6 +306,7 @@ class RouteMixin: | |||||||
|         stream: bool = False, |         stream: bool = False, | ||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **PUT** *HTTP* method |         Add an API URL under the **PUT** *HTTP* method | ||||||
| @@ -296,6 +317,8 @@ class RouteMixin: | |||||||
|             URLs need to terminate with a */* |             URLs need to terminate with a */* | ||||||
|         :param version: API Version |         :param version: API Version | ||||||
|         :param name: Unique name that can be used to identify the Route |         :param name: Unique name that can be used to identify the Route | ||||||
|  |         :param version_prefix: URL path that should be before the version | ||||||
|  |             value; default: ``/v`` | ||||||
|         :return: Object decorated with :func:`route` method |         :return: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -306,6 +329,7 @@ class RouteMixin: | |||||||
|             stream=stream, |             stream=stream, | ||||||
|             version=version, |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def head( |     def head( | ||||||
| @@ -316,6 +340,7 @@ class RouteMixin: | |||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         ignore_body: bool = True, |         ignore_body: bool = True, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **HEAD** *HTTP* method |         Add an API URL under the **HEAD** *HTTP* method | ||||||
| @@ -334,6 +359,8 @@ class RouteMixin: | |||||||
|         :param ignore_body: whether the handler should ignore request |         :param ignore_body: whether the handler should ignore request | ||||||
|             body (eg. GET requests), defaults to True |             body (eg. GET requests), defaults to True | ||||||
|         :type ignore_body: bool, optional |         :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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -344,6 +371,7 @@ class RouteMixin: | |||||||
|             version=version, |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|             ignore_body=ignore_body, |             ignore_body=ignore_body, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def options( |     def options( | ||||||
| @@ -354,6 +382,7 @@ class RouteMixin: | |||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         ignore_body: bool = True, |         ignore_body: bool = True, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **OPTIONS** *HTTP* method |         Add an API URL under the **OPTIONS** *HTTP* method | ||||||
| @@ -372,6 +401,8 @@ class RouteMixin: | |||||||
|         :param ignore_body: whether the handler should ignore request |         :param ignore_body: whether the handler should ignore request | ||||||
|             body (eg. GET requests), defaults to True |             body (eg. GET requests), defaults to True | ||||||
|         :type ignore_body: bool, optional |         :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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -382,6 +413,7 @@ class RouteMixin: | |||||||
|             version=version, |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|             ignore_body=ignore_body, |             ignore_body=ignore_body, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def patch( |     def patch( | ||||||
| @@ -392,6 +424,7 @@ class RouteMixin: | |||||||
|         stream=False, |         stream=False, | ||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **PATCH** *HTTP* method |         Add an API URL under the **PATCH** *HTTP* method | ||||||
| @@ -412,6 +445,8 @@ class RouteMixin: | |||||||
|         :param ignore_body: whether the handler should ignore request |         :param ignore_body: whether the handler should ignore request | ||||||
|             body (eg. GET requests), defaults to True |             body (eg. GET requests), defaults to True | ||||||
|         :type ignore_body: bool, optional |         :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: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -422,6 +457,7 @@ class RouteMixin: | |||||||
|             stream=stream, |             stream=stream, | ||||||
|             version=version, |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def delete( |     def delete( | ||||||
| @@ -432,6 +468,7 @@ class RouteMixin: | |||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         ignore_body: bool = True, |         ignore_body: bool = True, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Add an API URL under the **DELETE** *HTTP* method |         Add an API URL under the **DELETE** *HTTP* method | ||||||
| @@ -442,6 +479,8 @@ class RouteMixin: | |||||||
|             URLs need to terminate with a */* |             URLs need to terminate with a */* | ||||||
|         :param version: API Version |         :param version: API Version | ||||||
|         :param name: Unique name that can be used to identify the Route |         :param name: Unique name that can be used to identify the Route | ||||||
|  |         :param version_prefix: URL path that should be before the version | ||||||
|  |             value; default: ``/v`` | ||||||
|         :return: Object decorated with :func:`route` method |         :return: Object decorated with :func:`route` method | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -452,6 +491,7 @@ class RouteMixin: | |||||||
|             version=version, |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|             ignore_body=ignore_body, |             ignore_body=ignore_body, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def websocket( |     def websocket( | ||||||
| @@ -463,6 +503,7 @@ class RouteMixin: | |||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         apply: bool = True, |         apply: bool = True, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Decorate a function to be registered as a websocket route |         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 subprotocols: optional list of str with supported subprotocols | ||||||
|         :param name: A unique name assigned to the URL so that it can |         :param name: A unique name assigned to the URL so that it can | ||||||
|                      be used with :func:`url_for` |                      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: tuple of routes, decorated function | ||||||
|         """ |         """ | ||||||
|         return self.route( |         return self.route( | ||||||
| @@ -486,6 +529,7 @@ class RouteMixin: | |||||||
|             apply=apply, |             apply=apply, | ||||||
|             subprotocols=subprotocols, |             subprotocols=subprotocols, | ||||||
|             websocket=True, |             websocket=True, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def add_websocket_route( |     def add_websocket_route( | ||||||
| @@ -497,6 +541,7 @@ class RouteMixin: | |||||||
|         subprotocols=None, |         subprotocols=None, | ||||||
|         version: Optional[int] = None, |         version: Optional[int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         A helper method to register a function as a websocket route. |         A helper method to register a function as a websocket route. | ||||||
| @@ -513,6 +558,8 @@ class RouteMixin: | |||||||
|                 handshake |                 handshake | ||||||
|         :param name: A unique name assigned to the URL so that it can |         :param name: A unique name assigned to the URL so that it can | ||||||
|                 be used with :func:`url_for` |                 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: Objected decorated by :func:`websocket` | ||||||
|         """ |         """ | ||||||
|         return self.websocket( |         return self.websocket( | ||||||
| @@ -522,6 +569,7 @@ class RouteMixin: | |||||||
|             subprotocols=subprotocols, |             subprotocols=subprotocols, | ||||||
|             version=version, |             version=version, | ||||||
|             name=name, |             name=name, | ||||||
|  |             version_prefix=version_prefix, | ||||||
|         )(handler) |         )(handler) | ||||||
|  |  | ||||||
|     def static( |     def static( | ||||||
| @@ -665,7 +713,10 @@ class RouteMixin: | |||||||
|                 modified_since = strftime( |                 modified_since = strftime( | ||||||
|                     "%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime) |                     "%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) |                     return HTTPResponse(status=304) | ||||||
|                 headers["Last-Modified"] = modified_since |                 headers["Last-Modified"] = modified_since | ||||||
|             _range = None |             _range = None | ||||||
| @@ -718,16 +769,18 @@ class RouteMixin: | |||||||
|                 return await file(file_path, headers=headers, _range=_range) |                 return await file(file_path, headers=headers, _range=_range) | ||||||
|         except ContentRangeError: |         except ContentRangeError: | ||||||
|             raise |             raise | ||||||
|         except Exception: |         except FileNotFoundError: | ||||||
|             error_logger.exception( |  | ||||||
|                 f"File not found: path={file_or_directory}, " |  | ||||||
|                 f"relative_url={__file_uri__}" |  | ||||||
|             ) |  | ||||||
|             raise FileNotFound( |             raise FileNotFound( | ||||||
|                 "File not found", |                 "File not found", | ||||||
|                 path=file_or_directory, |                 path=file_or_directory, | ||||||
|                 relative_url=__file_uri__, |                 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( |     def _register_static( | ||||||
|         self, |         self, | ||||||
| @@ -776,7 +829,7 @@ class RouteMixin: | |||||||
|         # If we're not trying to match a file directly, |         # If we're not trying to match a file directly, | ||||||
|         # serve from the folder |         # serve from the folder | ||||||
|         if not path.isfile(file_or_directory): |         if not path.isfile(file_or_directory): | ||||||
|             uri += "/<__file_uri__>" |             uri += "/<__file_uri__:path>" | ||||||
|  |  | ||||||
|         # special prefix for static files |         # special prefix for static files | ||||||
|         # if not static.name.startswith("_static_"): |         # if not static.name.startswith("_static_"): | ||||||
|   | |||||||
| @@ -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.futures import FutureSignal | ||||||
| from sanic.models.handler_types import SignalHandler | from sanic.models.handler_types import SignalHandler | ||||||
| @@ -60,10 +60,16 @@ class SignalMixin: | |||||||
|  |  | ||||||
|     def add_signal( |     def add_signal( | ||||||
|         self, |         self, | ||||||
|         handler, |         handler: Optional[Callable[..., Any]], | ||||||
|         event: str, |         event: str, | ||||||
|         condition: Dict[str, Any] = None, |         condition: Dict[str, Any] = None, | ||||||
|     ): |     ): | ||||||
|  |         if not handler: | ||||||
|  |  | ||||||
|  |             async def noop(): | ||||||
|  |                 ... | ||||||
|  |  | ||||||
|  |             handler = noop | ||||||
|         self.signal(event=event, condition=condition)(handler) |         self.signal(event=event, condition=condition)(handler) | ||||||
|         return handler |         return handler | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ class FutureRoute(NamedTuple): | |||||||
|     subprotocols: Optional[List[str]] |     subprotocols: Optional[List[str]] | ||||||
|     unquote: bool |     unquote: bool | ||||||
|     static: bool |     static: bool | ||||||
|  |     version_prefix: str | ||||||
|  |  | ||||||
|  |  | ||||||
| class FutureListener(NamedTuple): | class FutureListener(NamedTuple): | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import itertools | ||||||
| import os | import os | ||||||
| import signal | import signal | ||||||
| import subprocess | import subprocess | ||||||
| @@ -5,6 +6,9 @@ import sys | |||||||
|  |  | ||||||
| from time import sleep | from time import sleep | ||||||
|  |  | ||||||
|  | from sanic.config import BASE_LOGO | ||||||
|  | from sanic.log import logger | ||||||
|  |  | ||||||
|  |  | ||||||
| def _iter_module_files(): | def _iter_module_files(): | ||||||
|     """This iterates over all relevant Python 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. |     """Watch project files, restart worker process if a change happened. | ||||||
|  |  | ||||||
|     :param sleep_interval: interval in second. |     :param sleep_interval: interval in second. | ||||||
| @@ -73,21 +91,25 @@ def watchdog(sleep_interval): | |||||||
|  |  | ||||||
|     worker_process = restart_with_reloader() |     worker_process = restart_with_reloader() | ||||||
|  |  | ||||||
|  |     if app.config.LOGO: | ||||||
|  |         logger.debug( | ||||||
|  |             app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         while True: |         while True: | ||||||
|             need_reload = False |             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: |                 try: | ||||||
|                     mtime = os.stat(filename).st_mtime |                     check = _check_file(filename, mtimes) | ||||||
|                 except OSError: |                 except OSError: | ||||||
|                     continue |                     continue | ||||||
|  |  | ||||||
|                 old_time = mtimes.get(filename) |                 if check: | ||||||
|                 if old_time is None: |  | ||||||
|                     mtimes[filename] = mtime |  | ||||||
|                 elif mtime > old_time: |  | ||||||
|                     mtimes[filename] = mtime |  | ||||||
|                     need_reload = True |                     need_reload = True | ||||||
|  |  | ||||||
|             if need_reload: |             if need_reload: | ||||||
|   | |||||||
| @@ -125,7 +125,7 @@ class Request: | |||||||
|         self._name: Optional[str] = None |         self._name: Optional[str] = None | ||||||
|         self.app = app |         self.app = app | ||||||
|  |  | ||||||
|         self.headers = headers |         self.headers = Header(headers) | ||||||
|         self.version = version |         self.version = version | ||||||
|         self.method = method |         self.method = method | ||||||
|         self.transport = transport |         self.transport = transport | ||||||
| @@ -262,7 +262,7 @@ class Request: | |||||||
|             app = Sanic("MyApp", request_class=IntRequest) |             app = Sanic("MyApp", request_class=IntRequest) | ||||||
|         """ |         """ | ||||||
|         if not self._id: |         if not self._id: | ||||||
|             self._id = self.headers.get( |             self._id = self.headers.getone( | ||||||
|                 self.app.config.REQUEST_ID_HEADER, |                 self.app.config.REQUEST_ID_HEADER, | ||||||
|                 self.__class__.generate_id(self),  # type: ignore |                 self.__class__.generate_id(self),  # type: ignore | ||||||
|             ) |             ) | ||||||
| @@ -303,7 +303,7 @@ class Request: | |||||||
|         :return: token related to request |         :return: token related to request | ||||||
|         """ |         """ | ||||||
|         prefixes = ("Bearer", "Token") |         prefixes = ("Bearer", "Token") | ||||||
|         auth_header = self.headers.get("Authorization") |         auth_header = self.headers.getone("authorization", None) | ||||||
|  |  | ||||||
|         if auth_header is not None: |         if auth_header is not None: | ||||||
|             for prefix in prefixes: |             for prefix in prefixes: | ||||||
| @@ -317,8 +317,8 @@ class Request: | |||||||
|         if self.parsed_form is None: |         if self.parsed_form is None: | ||||||
|             self.parsed_form = RequestParameters() |             self.parsed_form = RequestParameters() | ||||||
|             self.parsed_files = RequestParameters() |             self.parsed_files = RequestParameters() | ||||||
|             content_type = self.headers.get( |             content_type = self.headers.getone( | ||||||
|                 "Content-Type", DEFAULT_HTTP_CONTENT_TYPE |                 "content-type", DEFAULT_HTTP_CONTENT_TYPE | ||||||
|             ) |             ) | ||||||
|             content_type, parameters = parse_content_header(content_type) |             content_type, parameters = parse_content_header(content_type) | ||||||
|             try: |             try: | ||||||
| @@ -378,9 +378,12 @@ class Request: | |||||||
|         :type errors: str |         :type errors: str | ||||||
|         :return: RequestParameters |         :return: RequestParameters | ||||||
|         """ |         """ | ||||||
|         if not self.parsed_args[ |         if ( | ||||||
|             (keep_blank_values, strict_parsing, encoding, errors) |             keep_blank_values, | ||||||
|         ]: |             strict_parsing, | ||||||
|  |             encoding, | ||||||
|  |             errors, | ||||||
|  |         ) not in self.parsed_args: | ||||||
|             if self.query_string: |             if self.query_string: | ||||||
|                 self.parsed_args[ |                 self.parsed_args[ | ||||||
|                     (keep_blank_values, strict_parsing, encoding, errors) |                     (keep_blank_values, strict_parsing, encoding, errors) | ||||||
| @@ -434,9 +437,12 @@ class Request: | |||||||
|         :type errors: str |         :type errors: str | ||||||
|         :return: list |         :return: list | ||||||
|         """ |         """ | ||||||
|         if not self.parsed_not_grouped_args[ |         if ( | ||||||
|             (keep_blank_values, strict_parsing, encoding, errors) |             keep_blank_values, | ||||||
|         ]: |             strict_parsing, | ||||||
|  |             encoding, | ||||||
|  |             errors, | ||||||
|  |         ) not in self.parsed_not_grouped_args: | ||||||
|             if self.query_string: |             if self.query_string: | ||||||
|                 self.parsed_not_grouped_args[ |                 self.parsed_not_grouped_args[ | ||||||
|                     (keep_blank_values, strict_parsing, encoding, errors) |                     (keep_blank_values, strict_parsing, encoding, errors) | ||||||
| @@ -465,7 +471,7 @@ class Request: | |||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         if self._cookies is None: |         if self._cookies is None: | ||||||
|             cookie = self.headers.get("Cookie") |             cookie = self.headers.getone("cookie", None) | ||||||
|             if cookie is not None: |             if cookie is not None: | ||||||
|                 cookies: SimpleCookie = SimpleCookie() |                 cookies: SimpleCookie = SimpleCookie() | ||||||
|                 cookies.load(cookie) |                 cookies.load(cookie) | ||||||
| @@ -482,7 +488,7 @@ class Request: | |||||||
|         :return: Content-Type header form the request |         :return: Content-Type header form the request | ||||||
|         :rtype: str |         :rtype: str | ||||||
|         """ |         """ | ||||||
|         return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE) |         return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def match_info(self): |     def match_info(self): | ||||||
| @@ -499,7 +505,7 @@ class Request: | |||||||
|         :return: peer ip of the socket |         :return: peer ip of the socket | ||||||
|         :rtype: str |         :rtype: str | ||||||
|         """ |         """ | ||||||
|         return self.conn_info.client if self.conn_info else "" |         return self.conn_info.client_ip if self.conn_info else "" | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def port(self) -> int: |     def port(self) -> int: | ||||||
| @@ -581,7 +587,7 @@ class Request: | |||||||
|  |  | ||||||
|         if ( |         if ( | ||||||
|             self.app.websocket_enabled |             self.app.websocket_enabled | ||||||
|             and self.headers.get("upgrade") == "websocket" |             and self.headers.getone("upgrade", "").lower() == "websocket" | ||||||
|         ): |         ): | ||||||
|             scheme = "ws" |             scheme = "ws" | ||||||
|         else: |         else: | ||||||
| @@ -608,7 +614,9 @@ class Request: | |||||||
|         server_name = self.app.config.get("SERVER_NAME") |         server_name = self.app.config.get("SERVER_NAME") | ||||||
|         if server_name: |         if server_name: | ||||||
|             return server_name.split("//", 1)[-1].split("/", 1)[0] |             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 |     @property | ||||||
|     def server_name(self) -> str: |     def server_name(self) -> str: | ||||||
|   | |||||||
| @@ -143,7 +143,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|  |  | ||||||
|     .. warning:: |     .. 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. |         same functionality without a callback. | ||||||
|  |  | ||||||
|         .. code-block:: python |         .. code-block:: python | ||||||
| @@ -174,12 +174,16 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|         status: int = 200, |         status: int = 200, | ||||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, |         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||||
|         content_type: str = "text/plain; charset=utf-8", |         content_type: str = "text/plain; charset=utf-8", | ||||||
|         chunked="deprecated", |         ignore_deprecation_notice: bool = False, | ||||||
|     ): |     ): | ||||||
|         if chunked != "deprecated": |         if not ignore_deprecation_notice: | ||||||
|             warn( |             warn( | ||||||
|                 "The chunked argument has been deprecated and will be " |                 "Use of the StreamingHTTPResponse is deprecated in v21.6, and " | ||||||
|                 "removed in v21.6" |                 "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__() |         super().__init__() | ||||||
| @@ -203,6 +207,9 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|             self.streaming_fn = None |             self.streaming_fn = None | ||||||
|         await super().send(*args, **kwargs) |         await super().send(*args, **kwargs) | ||||||
|  |  | ||||||
|  |     async def eof(self): | ||||||
|  |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
| class HTTPResponse(BaseHTTPResponse): | class HTTPResponse(BaseHTTPResponse): | ||||||
|     """ |     """ | ||||||
| @@ -235,6 +242,15 @@ class HTTPResponse(BaseHTTPResponse): | |||||||
|         self.headers = Header(headers or {}) |         self.headers = Header(headers or {}) | ||||||
|         self._cookies = None |         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( | def empty( | ||||||
|     status=204, headers: Optional[Dict[str, str]] = None |     status=204, headers: Optional[Dict[str, str]] = None | ||||||
| @@ -396,7 +412,6 @@ async def file_stream( | |||||||
|     mime_type: Optional[str] = None, |     mime_type: Optional[str] = None, | ||||||
|     headers: Optional[Dict[str, str]] = None, |     headers: Optional[Dict[str, str]] = None, | ||||||
|     filename: Optional[str] = None, |     filename: Optional[str] = None, | ||||||
|     chunked="deprecated", |  | ||||||
|     _range: Optional[Range] = None, |     _range: Optional[Range] = None, | ||||||
| ) -> StreamingHTTPResponse: | ) -> StreamingHTTPResponse: | ||||||
|     """Return a streaming response object with file data. |     """Return a streaming response object with file data. | ||||||
| @@ -409,12 +424,6 @@ async def file_stream( | |||||||
|     :param chunked: Deprecated |     :param chunked: Deprecated | ||||||
|     :param _range: |     :param _range: | ||||||
|     """ |     """ | ||||||
|     if chunked != "deprecated": |  | ||||||
|         warn( |  | ||||||
|             "The chunked argument has been deprecated and will be " |  | ||||||
|             "removed in v21.6" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     headers = headers or {} |     headers = headers or {} | ||||||
|     if filename: |     if filename: | ||||||
|         headers.setdefault( |         headers.setdefault( | ||||||
| @@ -453,6 +462,7 @@ async def file_stream( | |||||||
|         status=status, |         status=status, | ||||||
|         headers=headers, |         headers=headers, | ||||||
|         content_type=mime_type, |         content_type=mime_type, | ||||||
|  |         ignore_deprecation_notice=True, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -461,7 +471,6 @@ def stream( | |||||||
|     status: int = 200, |     status: int = 200, | ||||||
|     headers: Optional[Dict[str, str]] = None, |     headers: Optional[Dict[str, str]] = None, | ||||||
|     content_type: str = "text/plain; charset=utf-8", |     content_type: str = "text/plain; charset=utf-8", | ||||||
|     chunked="deprecated", |  | ||||||
| ): | ): | ||||||
|     """Accepts an coroutine `streaming_fn` which can be used to |     """Accepts an coroutine `streaming_fn` which can be used to | ||||||
|     write chunks to a streaming response. Returns a `StreamingHTTPResponse`. |     write chunks to a streaming response. Returns a `StreamingHTTPResponse`. | ||||||
| @@ -482,17 +491,12 @@ def stream( | |||||||
|     :param headers: Custom Headers. |     :param headers: Custom Headers. | ||||||
|     :param chunked: Deprecated |     :param chunked: Deprecated | ||||||
|     """ |     """ | ||||||
|     if chunked != "deprecated": |  | ||||||
|         warn( |  | ||||||
|             "The chunked argument has been deprecated and will be " |  | ||||||
|             "removed in v21.6" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     return StreamingHTTPResponse( |     return StreamingHTTPResponse( | ||||||
|         streaming_fn, |         streaming_fn, | ||||||
|         headers=headers, |         headers=headers, | ||||||
|         content_type=content_type, |         content_type=content_type, | ||||||
|         status=status, |         status=status, | ||||||
|  |         ignore_deprecation_notice=True, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ class Router(BaseRouter): | |||||||
|             return self.resolve( |             return self.resolve( | ||||||
|                 path=path, |                 path=path, | ||||||
|                 method=method, |                 method=method, | ||||||
|                 extra={"host": host}, |                 extra={"host": host} if host else None, | ||||||
|             ) |             ) | ||||||
|         except RoutingNotFound as e: |         except RoutingNotFound as e: | ||||||
|             raise NotFound("Requested URL {} not found".format(e.path)) |             raise NotFound("Requested URL {} not found".format(e.path)) | ||||||
| @@ -73,6 +73,7 @@ class Router(BaseRouter): | |||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         unquote: bool = False, |         unquote: bool = False, | ||||||
|         static: bool = False, |         static: bool = False, | ||||||
|  |         version_prefix: str = "/v", | ||||||
|     ) -> Union[Route, List[Route]]: |     ) -> Union[Route, List[Route]]: | ||||||
|         """ |         """ | ||||||
|         Add a handler to the router |         Add a handler to the router | ||||||
| @@ -103,12 +104,12 @@ class Router(BaseRouter): | |||||||
|         """ |         """ | ||||||
|         if version is not None: |         if version is not None: | ||||||
|             version = str(version).strip("/").lstrip("v") |             version = str(version).strip("/").lstrip("v") | ||||||
|             uri = "/".join([f"/v{version}", uri.lstrip("/")]) |             uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")]) | ||||||
|  |  | ||||||
|         params = dict( |         params = dict( | ||||||
|             path=uri, |             path=uri, | ||||||
|             handler=handler, |             handler=handler, | ||||||
|             methods=methods, |             methods=frozenset(map(str, methods)) if methods else None, | ||||||
|             name=name, |             name=name, | ||||||
|             strict=strict_slashes, |             strict=strict_slashes, | ||||||
|             unquote=unquote, |             unquote=unquote, | ||||||
| @@ -161,7 +162,7 @@ class Router(BaseRouter): | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def routes_all(self): |     def routes_all(self): | ||||||
|         return self.routes |         return {route.parts: route for route in self.routes} | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def routes_static(self): |     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.config import Config | ||||||
| from sanic.exceptions import RequestTimeout, ServiceUnavailable | from sanic.exceptions import RequestTimeout, ServiceUnavailable | ||||||
| from sanic.http import Http, Stage | 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.models.protocol_types import TransportProtocol | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
|  |  | ||||||
| @@ -65,6 +65,7 @@ class ConnInfo: | |||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
|         "client_port", |         "client_port", | ||||||
|         "client", |         "client", | ||||||
|  |         "client_ip", | ||||||
|         "ctx", |         "ctx", | ||||||
|         "peername", |         "peername", | ||||||
|         "server_port", |         "server_port", | ||||||
| @@ -78,6 +79,7 @@ class ConnInfo: | |||||||
|         self.peername = None |         self.peername = None | ||||||
|         self.server = self.client = "" |         self.server = self.client = "" | ||||||
|         self.server_port = self.client_port = 0 |         self.server_port = self.client_port = 0 | ||||||
|  |         self.client_ip = "" | ||||||
|         self.sockname = addr = transport.get_extra_info("sockname") |         self.sockname = addr = transport.get_extra_info("sockname") | ||||||
|         self.ssl: bool = bool(transport.get_extra_info("sslcontext")) |         self.ssl: bool = bool(transport.get_extra_info("sslcontext")) | ||||||
|  |  | ||||||
| @@ -96,6 +98,7 @@ class ConnInfo: | |||||||
|  |  | ||||||
|         if isinstance(addr, tuple): |         if isinstance(addr, tuple): | ||||||
|             self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]" |             self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]" | ||||||
|  |             self.client_ip = addr[0] | ||||||
|             self.client_port = addr[1] |             self.client_port = addr[1] | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -122,7 +125,6 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         "response_timeout", |         "response_timeout", | ||||||
|         "keep_alive_timeout", |         "keep_alive_timeout", | ||||||
|         "request_max_size", |         "request_max_size", | ||||||
|         "request_buffer_queue_size", |  | ||||||
|         "request_class", |         "request_class", | ||||||
|         "error_handler", |         "error_handler", | ||||||
|         # enable or disable access log purpose |         # enable or disable access log purpose | ||||||
| @@ -165,9 +167,6 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.request_handler = self.app.handle_request |         self.request_handler = self.app.handle_request | ||||||
|         self.error_handler = self.app.error_handler |         self.error_handler = self.app.error_handler | ||||||
|         self.request_timeout = self.app.config.REQUEST_TIMEOUT |         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.response_timeout = self.app.config.RESPONSE_TIMEOUT | ||||||
|         self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT |         self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT | ||||||
|         self.request_max_size = self.app.config.REQUEST_MAX_SIZE |         self.request_max_size = self.app.config.REQUEST_MAX_SIZE | ||||||
| @@ -199,11 +198,11 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         except CancelledError: |         except CancelledError: | ||||||
|             pass |             pass | ||||||
|         except Exception: |         except Exception: | ||||||
|             logger.exception("protocol.connection_task uncaught") |             error_logger.exception("protocol.connection_task uncaught") | ||||||
|         finally: |         finally: | ||||||
|             if self.app.debug and self._http: |             if self.app.debug and self._http: | ||||||
|                 ip = self.transport.get_extra_info("peername") |                 ip = self.transport.get_extra_info("peername") | ||||||
|                 logger.error( |                 error_logger.error( | ||||||
|                     "Connection lost before response written" |                     "Connection lost before response written" | ||||||
|                     f" @ {ip} {self._http.request}" |                     f" @ {ip} {self._http.request}" | ||||||
|                 ) |                 ) | ||||||
| @@ -212,7 +211,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             try: |             try: | ||||||
|                 self.close() |                 self.close() | ||||||
|             except BaseException: |             except BaseException: | ||||||
|                 logger.exception("Closing failed") |                 error_logger.exception("Closing failed") | ||||||
|  |  | ||||||
|     async def receive_more(self): |     async def receive_more(self): | ||||||
|         """ |         """ | ||||||
| @@ -234,11 +233,16 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             if stage is Stage.IDLE and duration > self.keep_alive_timeout: |             if stage is Stage.IDLE and duration > self.keep_alive_timeout: | ||||||
|                 logger.debug("KeepAlive Timeout. Closing connection.") |                 logger.debug("KeepAlive Timeout. Closing connection.") | ||||||
|             elif stage is Stage.REQUEST and duration > self.request_timeout: |             elif stage is Stage.REQUEST and duration > self.request_timeout: | ||||||
|  |                 logger.debug("Request Timeout. Closing connection.") | ||||||
|                 self._http.exception = RequestTimeout("Request Timeout") |                 self._http.exception = RequestTimeout("Request Timeout") | ||||||
|  |             elif stage is Stage.HANDLER and self._http.upgrade_websocket: | ||||||
|  |                 logger.debug("Handling websocket. Timeouts disabled.") | ||||||
|  |                 return | ||||||
|             elif ( |             elif ( | ||||||
|                 stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED) |                 stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED) | ||||||
|                 and duration > self.response_timeout |                 and duration > self.response_timeout | ||||||
|             ): |             ): | ||||||
|  |                 logger.debug("Response Timeout. Closing connection.") | ||||||
|                 self._http.exception = ServiceUnavailable("Response Timeout") |                 self._http.exception = ServiceUnavailable("Response Timeout") | ||||||
|             else: |             else: | ||||||
|                 interval = ( |                 interval = ( | ||||||
| @@ -253,7 +257,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|                 return |                 return | ||||||
|             self._task.cancel() |             self._task.cancel() | ||||||
|         except Exception: |         except Exception: | ||||||
|             logger.exception("protocol.check_timeouts") |             error_logger.exception("protocol.check_timeouts") | ||||||
|  |  | ||||||
|     async def send(self, data): |     async def send(self, data): | ||||||
|         """ |         """ | ||||||
| @@ -299,7 +303,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             self.recv_buffer = bytearray() |             self.recv_buffer = bytearray() | ||||||
|             self.conn_info = ConnInfo(self.transport, unix=self._unix) |             self.conn_info = ConnInfo(self.transport, unix=self._unix) | ||||||
|         except Exception: |         except Exception: | ||||||
|             logger.exception("protocol.connect_made") |             error_logger.exception("protocol.connect_made") | ||||||
|  |  | ||||||
|     def connection_lost(self, exc): |     def connection_lost(self, exc): | ||||||
|         try: |         try: | ||||||
| @@ -308,7 +312,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             if self._task: |             if self._task: | ||||||
|                 self._task.cancel() |                 self._task.cancel() | ||||||
|         except Exception: |         except Exception: | ||||||
|             logger.exception("protocol.connection_lost") |             error_logger.exception("protocol.connection_lost") | ||||||
|  |  | ||||||
|     def pause_writing(self): |     def pause_writing(self): | ||||||
|         self._can_write.clear() |         self._can_write.clear() | ||||||
| @@ -332,7 +336,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             if self._data_received: |             if self._data_received: | ||||||
|                 self._data_received.set() |                 self._data_received.set() | ||||||
|         except Exception: |         except Exception: | ||||||
|             logger.exception("protocol.data_received") |             error_logger.exception("protocol.data_received") | ||||||
|  |  | ||||||
|  |  | ||||||
| def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): | def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop): | ||||||
| @@ -551,7 +555,7 @@ def serve( | |||||||
|     try: |     try: | ||||||
|         http_server = loop.run_until_complete(server_coroutine) |         http_server = loop.run_until_complete(server_coroutine) | ||||||
|     except BaseException: |     except BaseException: | ||||||
|         logger.exception("Unable to start server") |         error_logger.exception("Unable to start server") | ||||||
|         return |         return | ||||||
|  |  | ||||||
|     trigger_events(after_start, loop) |     trigger_events(after_start, loop) | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import asyncio | |||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from typing import Any, Dict, List, Optional, Tuple, Union | from typing import Any, Dict, List, Optional, Tuple, Union | ||||||
|  |  | ||||||
| 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.exceptions import NotFound  # type: ignore | ||||||
| from sanic_routing.utils import path_to_parts  # type: ignore | from sanic_routing.utils import path_to_parts  # type: ignore | ||||||
|  |  | ||||||
| @@ -20,17 +20,11 @@ RESERVED_NAMESPACES = ( | |||||||
|  |  | ||||||
|  |  | ||||||
| class Signal(Route): | 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: | class SignalGroup(RouteGroup): | ||||||
|             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 SignalRouter(BaseRouter): | class SignalRouter(BaseRouter): | ||||||
| @@ -38,6 +32,7 @@ class SignalRouter(BaseRouter): | |||||||
|         super().__init__( |         super().__init__( | ||||||
|             delimiter=".", |             delimiter=".", | ||||||
|             route_class=Signal, |             route_class=Signal, | ||||||
|  |             group_class=SignalGroup, | ||||||
|             stacking=True, |             stacking=True, | ||||||
|         ) |         ) | ||||||
|         self.ctx.loop = None |         self.ctx.loop = None | ||||||
| @@ -49,7 +44,13 @@ class SignalRouter(BaseRouter): | |||||||
|     ): |     ): | ||||||
|         extra = condition or {} |         extra = condition or {} | ||||||
|         try: |         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: |         except NotFound: | ||||||
|             message = "Could not find signal %s" |             message = "Could not find signal %s" | ||||||
|             terms: List[Union[str, Optional[Dict[str, str]]]] = [event] |             terms: List[Union[str, Optional[Dict[str, str]]]] = [event] | ||||||
| @@ -58,16 +59,26 @@ class SignalRouter(BaseRouter): | |||||||
|                 terms.append(extra) |                 terms.append(extra) | ||||||
|             raise NotFound(message % tuple(terms)) |             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( |     async def _dispatch( | ||||||
|         self, |         self, | ||||||
|         event: str, |         event: str, | ||||||
|         context: Optional[Dict[str, Any]] = None, |         context: Optional[Dict[str, Any]] = None, | ||||||
|         condition: Optional[Dict[str, str]] = None, |         condition: Optional[Dict[str, str]] = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         signal, handlers, params = self.get(event, condition=condition) |         group, handlers, params = self.get(event, condition=condition) | ||||||
|  |  | ||||||
|         signal_event = signal.ctx.event |         events = [signal.ctx.event for signal in group] | ||||||
|         signal_event.set() |         for signal_event in events: | ||||||
|  |             signal_event.set() | ||||||
|         if context: |         if context: | ||||||
|             params.update(context) |             params.update(context) | ||||||
|  |  | ||||||
| @@ -78,7 +89,8 @@ class SignalRouter(BaseRouter): | |||||||
|                     if isawaitable(maybe_coroutine): |                     if isawaitable(maybe_coroutine): | ||||||
|                         await maybe_coroutine |                         await maybe_coroutine | ||||||
|         finally: |         finally: | ||||||
|             signal_event.clear() |             for signal_event in events: | ||||||
|  |                 signal_event.clear() | ||||||
|  |  | ||||||
|     async def dispatch( |     async def dispatch( | ||||||
|         self, |         self, | ||||||
| @@ -116,7 +128,7 @@ class SignalRouter(BaseRouter): | |||||||
|             handler, |             handler, | ||||||
|             requirements=condition, |             requirements=condition, | ||||||
|             name=name, |             name=name, | ||||||
|             overwrite=True, |             append=True, | ||||||
|         )  # type: ignore |         )  # type: ignore | ||||||
|  |  | ||||||
|     def finalize(self, do_compile: bool = True): |     def finalize(self, do_compile: bool = True): | ||||||
| @@ -125,7 +137,7 @@ class SignalRouter(BaseRouter): | |||||||
|         except RuntimeError: |         except RuntimeError: | ||||||
|             raise RuntimeError("Cannot finalize signals outside of event loop") |             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() |             signal.ctx.event = asyncio.Event() | ||||||
|  |  | ||||||
|         return super().finalize(do_compile=do_compile) |         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( |             _mod_spec = spec_from_file_location( | ||||||
|                 name, location, *args, **kwargs |                 name, location, *args, **kwargs | ||||||
|             ) |             ) | ||||||
|  |             assert _mod_spec is not None  # type assertion for mypy | ||||||
|             module = module_from_spec(_mod_spec) |             module = module_from_spec(_mod_spec) | ||||||
|             _mod_spec.loader.exec_module(module)  # type: ignore |             _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.constants import HTTP_METHODS | ||||||
| from sanic.exceptions import InvalidUsage | from sanic.exceptions import InvalidUsage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from sanic import Sanic | ||||||
|  |     from sanic.blueprints import Blueprint | ||||||
|  |  | ||||||
|  |  | ||||||
| class HTTPMethodView: | class HTTPMethodView: | ||||||
|     """Simple class based implementation of view for the sanic. |     """Simple class based implementation of view for the sanic. | ||||||
|     You should implement methods (get, post, put, patch, delete) for the class |     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]]] = [] |     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): |     def dispatch_request(self, request, *args, **kwargs): | ||||||
|         handler = getattr(self, request.method.lower(), None) |         handler = getattr(self, request.method.lower(), None) | ||||||
|         return handler(request, *args, **kwargs) |         return handler(request, *args, **kwargs) | ||||||
| @@ -65,6 +106,31 @@ class HTTPMethodView: | |||||||
|         view.__name__ = cls.__name__ |         view.__name__ = cls.__name__ | ||||||
|         return view |         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): | def stream(func): | ||||||
|     func.is_stream = True |     func.is_stream = True | ||||||
| @@ -91,6 +157,11 @@ class CompositionView: | |||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.handlers = {} |         self.handlers = {} | ||||||
|         self.name = self.__class__.__name__ |         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): |     def __name__(self): | ||||||
|         return self.name |         return self.name | ||||||
|   | |||||||
| @@ -14,9 +14,13 @@ from websockets import (  # type: ignore | |||||||
|     ConnectionClosed, |     ConnectionClosed, | ||||||
|     InvalidHandshake, |     InvalidHandshake, | ||||||
|     WebSocketCommonProtocol, |     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.exceptions import InvalidUsage | ||||||
| from sanic.server import HttpProtocol | from sanic.server import HttpProtocol | ||||||
|  |  | ||||||
| @@ -37,7 +41,7 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|         websocket_write_limit=2 ** 16, |         websocket_write_limit=2 ** 16, | ||||||
|         websocket_ping_interval=20, |         websocket_ping_interval=20, | ||||||
|         websocket_ping_timeout=20, |         websocket_ping_timeout=20, | ||||||
|         **kwargs |         **kwargs, | ||||||
|     ): |     ): | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|         self.websocket = None |         self.websocket = None | ||||||
| @@ -126,7 +130,9 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|             ping_interval=self.websocket_ping_interval, |             ping_interval=self.websocket_ping_interval, | ||||||
|             ping_timeout=self.websocket_ping_timeout, |             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.is_client = False | ||||||
|         self.websocket.side = "server" |         self.websocket.side = "server" | ||||||
|         self.websocket.subprotocol = subprotocol |         self.websocket.subprotocol = subprotocol | ||||||
| @@ -148,7 +154,7 @@ class WebSocketConnection: | |||||||
|     ) -> None: |     ) -> None: | ||||||
|         self._send = send |         self._send = send | ||||||
|         self._receive = receive |         self._receive = receive | ||||||
|         self.subprotocols = subprotocols or [] |         self._subprotocols = subprotocols or [] | ||||||
|  |  | ||||||
|     async def send(self, data: Union[str, bytes], *args, **kwargs) -> None: |     async def send(self, data: Union[str, bytes], *args, **kwargs) -> None: | ||||||
|         message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"} |         message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"} | ||||||
| @@ -172,13 +178,28 @@ class WebSocketConnection: | |||||||
|  |  | ||||||
|     receive = recv |     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( |         await self._send( | ||||||
|             { |             { | ||||||
|                 "type": "websocket.accept", |                 "type": "websocket.accept", | ||||||
|                 "subprotocol": ",".join(list(self.subprotocols)), |                 "subprotocol": subprotocol, | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     async def close(self) -> None: |     async def close(self) -> None: | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def subprotocols(self): | ||||||
|  |         return self._subprotocols | ||||||
|  |  | ||||||
|  |     @subprotocols.setter | ||||||
|  |     def subprotocols(self, subprotocols: Optional[List[str]] = None): | ||||||
|  |         self._subprotocols = subprotocols or [] | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								setup.py
									
									
									
									
									
								
							| @@ -8,7 +8,7 @@ import sys | |||||||
|  |  | ||||||
| from distutils.util import strtobool | from distutils.util import strtobool | ||||||
|  |  | ||||||
| from setuptools import setup, find_packages | from setuptools import find_packages, setup | ||||||
| from setuptools.command.test import test as TestCommand | from setuptools.command.test import test as TestCommand | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -52,7 +52,7 @@ with open_local(["README.rst"]) as rm: | |||||||
| setup_kwargs = { | setup_kwargs = { | ||||||
|     "name": "sanic", |     "name": "sanic", | ||||||
|     "version": version, |     "version": version, | ||||||
|     "url": "http://github.com/huge-success/sanic/", |     "url": "http://github.com/sanic-org/sanic/", | ||||||
|     "license": "MIT", |     "license": "MIT", | ||||||
|     "author": "Sanic Community", |     "author": "Sanic Community", | ||||||
|     "author_email": "admhpkns@gmail.com", |     "author_email": "admhpkns@gmail.com", | ||||||
| @@ -83,17 +83,17 @@ ujson = "ujson>=1.35" + env_dependency | |||||||
| uvloop = "uvloop>=0.5.3" + env_dependency | uvloop = "uvloop>=0.5.3" + env_dependency | ||||||
|  |  | ||||||
| requirements = [ | requirements = [ | ||||||
|     "sanic-routing", |     "sanic-routing~=0.7", | ||||||
|     "httptools>=0.0.10", |     "httptools>=0.0.10", | ||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
|     "aiofiles>=0.6.0", |     "aiofiles>=0.6.0", | ||||||
|     "websockets>=8.1,<9.0", |     "websockets>=9.0", | ||||||
|     "multidict>=5.0,<6.0", |     "multidict>=5.0,<6.0", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| tests_require = [ | tests_require = [ | ||||||
|     "sanic-testing", |     "sanic-testing>=0.7.0b1", | ||||||
|     "pytest==5.2.1", |     "pytest==5.2.1", | ||||||
|     "multidict>=5.0,<6.0", |     "multidict>=5.0,<6.0", | ||||||
|     "gunicorn==20.0.4", |     "gunicorn==20.0.4", | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ from sanic.constants import HTTP_METHODS | |||||||
| from sanic.router import Router | from sanic.router import Router | ||||||
|  |  | ||||||
|  |  | ||||||
|  | slugify = re.compile(r"[^a-zA-Z0-9_\-]") | ||||||
| random.seed("Pack my box with five dozen liquor jugs.") | random.seed("Pack my box with five dozen liquor jugs.") | ||||||
| Sanic.test_mode = True | Sanic.test_mode = True | ||||||
|  |  | ||||||
| @@ -140,5 +141,5 @@ def url_param_generator(): | |||||||
|  |  | ||||||
| @pytest.fixture(scope="function") | @pytest.fixture(scope="function") | ||||||
| def app(request): | def app(request): | ||||||
|     app = Sanic(request.node.name) |     app = Sanic(slugify.sub("-", request.node.name)) | ||||||
|     return app |     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 | ||||||
							
								
								
									
										1
									
								
								tests/static/nested/dir/foo.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/static/nested/dir/foo.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | foo | ||||||
| @@ -9,6 +9,7 @@ from unittest.mock import Mock, patch | |||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
|  | from sanic.config import Config | ||||||
| from sanic.exceptions import SanicException | from sanic.exceptions import SanicException | ||||||
| from sanic.response import text | 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 response.status == 500 | ||||||
|     assert "Mock SanicException" in response.text |     assert "Mock SanicException" in response.text | ||||||
|     assert ( |     assert ( | ||||||
|         "sanic.root", |         "sanic.error", | ||||||
|         logging.ERROR, |         logging.ERROR, | ||||||
|         f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'", |         f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'", | ||||||
|     ) in caplog.record_tuples |     ) in caplog.record_tuples | ||||||
| @@ -389,7 +390,7 @@ def test_app_no_registry_env(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_set_attribute_warning(app): | def test_app_set_attribute_warning(app): | ||||||
|     with pytest.warns(UserWarning) as record: |     with pytest.warns(DeprecationWarning) as record: | ||||||
|         app.foo = 1 |         app.foo = 1 | ||||||
|  |  | ||||||
|     assert len(record) == 1 |     assert len(record) == 1 | ||||||
| @@ -412,3 +413,42 @@ def test_subclass_initialisation(): | |||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     CustomSanic("test_subclass_initialisation") |     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 asyncio | ||||||
| import sys |  | ||||||
|  |  | ||||||
| from collections import deque, namedtuple | from collections import deque, namedtuple | ||||||
|  |  | ||||||
| @@ -219,7 +218,7 @@ async def test_websocket_accept_with_no_subprotocols( | |||||||
|  |  | ||||||
|     message = message_stack.popleft() |     message = message_stack.popleft() | ||||||
|     assert message["type"] == "websocket.accept" |     assert message["type"] == "websocket.accept" | ||||||
|     assert message["subprotocol"] == "" |     assert message["subprotocol"] is None | ||||||
|     assert "bytes" not in message |     assert "bytes" not in message | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -228,7 +227,7 @@ async def test_websocket_accept_with_subprotocol(send, receive, message_stack): | |||||||
|     subprotocols = ["graphql-ws"] |     subprotocols = ["graphql-ws"] | ||||||
|  |  | ||||||
|     ws = WebSocketConnection(send, receive, subprotocols) |     ws = WebSocketConnection(send, receive, subprotocols) | ||||||
|     await ws.accept() |     await ws.accept(subprotocols) | ||||||
|  |  | ||||||
|     assert len(message_stack) == 1 |     assert len(message_stack) == 1 | ||||||
|  |  | ||||||
| @@ -245,13 +244,13 @@ async def test_websocket_accept_with_multiple_subprotocols( | |||||||
|     subprotocols = ["graphql-ws", "hello", "world"] |     subprotocols = ["graphql-ws", "hello", "world"] | ||||||
|  |  | ||||||
|     ws = WebSocketConnection(send, receive, subprotocols) |     ws = WebSocketConnection(send, receive, subprotocols) | ||||||
|     await ws.accept() |     await ws.accept(["hello", "world"]) | ||||||
|  |  | ||||||
|     assert len(message_stack) == 1 |     assert len(message_stack) == 1 | ||||||
|  |  | ||||||
|     message = message_stack.popleft() |     message = message_stack.popleft() | ||||||
|     assert message["type"] == "websocket.accept" |     assert message["type"] == "websocket.accept" | ||||||
|     assert message["subprotocol"] == "graphql-ws,hello,world" |     assert message["subprotocol"] == "hello" | ||||||
|     assert "bytes" not in message |     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", ' |         'Blueprint(name="my_bp", url_prefix="/foo", host="example.com", ' | ||||||
|         "version=3, strict_slashes=True)" |         "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_1 = Blueprint.group( | ||||||
|         Blueprint.group(blueprint_1, blueprint_2) |         Blueprint.group(blueprint_1, blueprint_2) | ||||||
|     ) |     ) | ||||||
|     assert len(blueprint_group_1) == 2 |     assert len(blueprint_group_1) == 1 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_blueprint_group_insert(): | def test_blueprint_group_insert(): | ||||||
| @@ -215,6 +215,61 @@ def test_blueprint_group_insert(): | |||||||
|     group.insert(0, blueprint_1) |     group.insert(0, blueprint_1) | ||||||
|     group.insert(0, blueprint_2) |     group.insert(0, blueprint_2) | ||||||
|     group.insert(0, blueprint_3) |     group.insert(0, blueprint_3) | ||||||
|     assert group.blueprints[1].strict_slashes is False |  | ||||||
|     assert group.blueprints[2].strict_slashes is True |     @blueprint_1.route("/") | ||||||
|     assert group.blueprints[0].url_prefix == "/test" |     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(): | def test_bp_set_attribute_warning(): | ||||||
|     bp = Blueprint("bp") |     bp = Blueprint("bp") | ||||||
|     with pytest.warns(UserWarning) as record: |     with pytest.warns(DeprecationWarning) as record: | ||||||
|         bp.foo = 1 |         bp.foo = 1 | ||||||
|  |  | ||||||
|     assert len(record) == 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") |         app.config.load("test_config.Config.test") | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_auto_load_env(): | def test_auto_env_prefix(): | ||||||
|     environ["SANIC_TEST_ANSWER"] = "42" |     environ["SANIC_TEST_ANSWER"] = "42" | ||||||
|     app = Sanic(name=__name__) |     app = Sanic(name=__name__) | ||||||
|     assert app.config.TEST_ANSWER == 42 |     assert app.config.TEST_ANSWER == 42 | ||||||
|     del environ["SANIC_TEST_ANSWER"] |     del environ["SANIC_TEST_ANSWER"] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_auto_load_bool_env(): | def test_auto_bool_env_prefix(): | ||||||
|     environ["SANIC_TEST_ANSWER"] = "True" |     environ["SANIC_TEST_ANSWER"] = "True" | ||||||
|     app = Sanic(name=__name__) |     app = Sanic(name=__name__) | ||||||
|     assert app.config.TEST_ANSWER is True |     assert app.config.TEST_ANSWER is True | ||||||
| @@ -80,6 +80,12 @@ def test_dont_load_env(): | |||||||
|     del environ["SANIC_TEST_ANSWER"] |     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(): | def test_load_env_prefix(): | ||||||
|     environ["MYAPP_TEST_ANSWER"] = "42" |     environ["MYAPP_TEST_ANSWER"] = "42" | ||||||
|     app = Sanic(name=__name__, load_env="MYAPP_") |     app = Sanic(name=__name__, load_env="MYAPP_") | ||||||
| @@ -87,6 +93,14 @@ def test_load_env_prefix(): | |||||||
|     del environ["MYAPP_TEST_ANSWER"] |     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(): | def test_load_env_prefix_float_values(): | ||||||
|     environ["MYAPP_TEST_ROI"] = "2.3" |     environ["MYAPP_TEST_ROI"] = "2.3" | ||||||
|     app = Sanic(name=__name__, load_env="MYAPP_") |     app = Sanic(name=__name__, load_env="MYAPP_") | ||||||
| @@ -101,6 +115,27 @@ def test_load_env_prefix_string_value(): | |||||||
|     del environ["MYAPP_TEST_TOKEN"] |     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): | def test_load_from_file(app): | ||||||
|     config = dedent( |     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 | import pytest | ||||||
|  |  | ||||||
| from bs4 import BeautifulSoup | from bs4 import BeautifulSoup | ||||||
| @@ -7,6 +9,7 @@ from sanic.exceptions import ( | |||||||
|     Forbidden, |     Forbidden, | ||||||
|     InvalidUsage, |     InvalidUsage, | ||||||
|     NotFound, |     NotFound, | ||||||
|  |     SanicException, | ||||||
|     ServerError, |     ServerError, | ||||||
|     Unauthorized, |     Unauthorized, | ||||||
|     abort, |     abort, | ||||||
| @@ -68,16 +71,19 @@ def exception_app(): | |||||||
|  |  | ||||||
|     @app.route("/abort/401") |     @app.route("/abort/401") | ||||||
|     def handler_401_error(request): |     def handler_401_error(request): | ||||||
|         abort(401) |         raise SanicException(status_code=401) | ||||||
|  |  | ||||||
|     @app.route("/abort") |     @app.route("/abort") | ||||||
|     def handler_500_error(request): |     def handler_500_error(request): | ||||||
|  |         raise SanicException(status_code=500) | ||||||
|  |  | ||||||
|  |     @app.route("/old_abort") | ||||||
|  |     def handler_old_abort_error(request): | ||||||
|         abort(500) |         abort(500) | ||||||
|         return text("OK") |  | ||||||
|  |  | ||||||
|     @app.route("/abort/message") |     @app.route("/abort/message") | ||||||
|     def handler_abort_message(request): |     def handler_abort_message(request): | ||||||
|         abort(500, message="Abort") |         raise SanicException(message="Custom Message", status_code=500) | ||||||
|  |  | ||||||
|     @app.route("/divide_by_zero") |     @app.route("/divide_by_zero") | ||||||
|     def handle_unhandled_exception(request): |     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 ") |     assert response.body.startswith(b"Exception raised in exception ") | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_abort(exception_app): | def test_sanic_exception(exception_app): | ||||||
|     """Test the abort function""" |     """Test sanic exceptions are handled""" | ||||||
|     request, response = exception_app.test_client.get("/abort/401") |     request, response = exception_app.test_client.get("/abort/401") | ||||||
|     assert response.status == 401 |     assert response.status == 401 | ||||||
|  |  | ||||||
|     request, response = exception_app.test_client.get("/abort") |     request, response = exception_app.test_client.get("/abort") | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|  |     # check fallback message | ||||||
|  |     assert "Internal Server Error" in response.text | ||||||
|  |  | ||||||
|     request, response = exception_app.test_client.get("/abort/message") |     request, response = exception_app.test_client.get("/abort/message") | ||||||
|     assert response.status == 500 |     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 | from sanic.http import Http | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture | ||||||
|  | def raised_ceiling(): | ||||||
|  |     Http.HEADER_CEILING = 32_768 | ||||||
|  |     yield | ||||||
|  |     Http.HEADER_CEILING = 16_384 | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "input, expected", |     "input, expected", | ||||||
|     [ |     [ | ||||||
| @@ -76,15 +83,75 @@ async def test_header_size_exceeded(): | |||||||
|         recv_buffer += b"123" |         recv_buffer += b"123" | ||||||
|  |  | ||||||
|     protocol = Mock() |     protocol = Mock() | ||||||
|  |     Http.set_header_max_size(1) | ||||||
|     http = Http(protocol) |     http = Http(protocol) | ||||||
|     http._receive_more = _receive_more |     http._receive_more = _receive_more | ||||||
|     http.request_max_size = 1 |  | ||||||
|     http.recv_buffer = recv_buffer |     http.recv_buffer = recv_buffer | ||||||
|  |  | ||||||
|     with pytest.raises(PayloadTooLarge): |     with pytest.raises(PayloadTooLarge): | ||||||
|         await http.http1_request_header() |         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): | def test_raw_headers(app): | ||||||
|     app.route("/")(lambda _: text("")) |     app.route("/")(lambda _: text("")) | ||||||
|     request, _ = app.test_client.get( |     request, _ = app.test_client.get( | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  | import platform | ||||||
|  |  | ||||||
| from asyncio import sleep as aio_sleep | from asyncio import sleep as aio_sleep | ||||||
| from json import JSONDecodeError | from json import JSONDecodeError | ||||||
| @@ -241,7 +242,9 @@ def test_keep_alive_timeout_reuse(): | |||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @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", |     reason="Not testable with current client", | ||||||
| ) | ) | ||||||
| def test_keep_alive_client_timeout(): | def test_keep_alive_client_timeout(): | ||||||
|   | |||||||
| @@ -113,9 +113,9 @@ def test_logging_pass_customer_logconfig(): | |||||||
| def test_log_connection_lost(app, debug, monkeypatch): | def test_log_connection_lost(app, debug, monkeypatch): | ||||||
|     """ Should not log Connection lost exception on non debug """ |     """ Should not log Connection lost exception on non debug """ | ||||||
|     stream = StringIO() |     stream = StringIO() | ||||||
|     root = logging.getLogger("sanic.root") |     error = logging.getLogger("sanic.error") | ||||||
|     root.addHandler(logging.StreamHandler(stream)) |     error.addHandler(logging.StreamHandler(stream)) | ||||||
|     monkeypatch.setattr(sanic.server, "logger", root) |     monkeypatch.setattr(sanic.server, "error_logger", error) | ||||||
|  |  | ||||||
|     @app.route("/conn_lost") |     @app.route("/conn_lost") | ||||||
|     async def conn_lost(request): |     async def conn_lost(request): | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import logging | |||||||
| from asyncio import CancelledError | from asyncio import CancelledError | ||||||
| from itertools import count | from itertools import count | ||||||
|  |  | ||||||
| from sanic.exceptions import NotFound, SanicException | from sanic.exceptions import NotFound | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import HTTPResponse, text | from sanic.response import HTTPResponse, text | ||||||
|  |  | ||||||
| @@ -156,7 +156,7 @@ def test_middleware_response_raise_cancelled_error(app, caplog): | |||||||
|  |  | ||||||
|         assert response.status == 503 |         assert response.status == 503 | ||||||
|         assert ( |         assert ( | ||||||
|             "sanic.root", |             "sanic.error", | ||||||
|             logging.ERROR, |             logging.ERROR, | ||||||
|             "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", |             "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", | ||||||
|         ) not in caplog.record_tuples |         ) not in caplog.record_tuples | ||||||
| @@ -174,7 +174,7 @@ def test_middleware_response_raise_exception(app, caplog): | |||||||
|     assert response.status == 404 |     assert response.status == 404 | ||||||
|     # 404 errors are not logged |     # 404 errors are not logged | ||||||
|     assert ( |     assert ( | ||||||
|         "sanic.root", |         "sanic.error", | ||||||
|         logging.ERROR, |         logging.ERROR, | ||||||
|         "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", |         "Exception occurred while handling uri: 'http://127.0.0.1:42101/'", | ||||||
|     ) not in caplog.record_tuples |     ) not in caplog.record_tuples | ||||||
|   | |||||||
| @@ -209,13 +209,13 @@ def test_named_static_routes(): | |||||||
|         return text("OK2") |         return text("OK2") | ||||||
|  |  | ||||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" |     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" |     assert app.url_for("route_test") == "/test" | ||||||
|     with pytest.raises(URLBuildError): |     with pytest.raises(URLBuildError): | ||||||
|         app.url_for("handler1") |         app.url_for("handler1") | ||||||
|  |  | ||||||
|     assert app.router.routes_all[("pizazz",)].name == "app.route_pizazz" |     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" |     assert app.url_for("route_pizazz") == "/pizazz" | ||||||
|     with pytest.raises(URLBuildError): |     with pytest.raises(URLBuildError): | ||||||
|         app.url_for("handler2") |         app.url_for("handler2") | ||||||
| @@ -234,7 +234,7 @@ def test_named_dynamic_route(): | |||||||
|         app.router.routes_all[ |         app.router.routes_all[ | ||||||
|             ( |             ( | ||||||
|                 "folder", |                 "folder", | ||||||
|                 "<name>", |                 "<name:str>", | ||||||
|             ) |             ) | ||||||
|         ].name |         ].name | ||||||
|         == "app.route_dynamic" |         == "app.route_dynamic" | ||||||
| @@ -347,13 +347,13 @@ def test_static_add_named_route(): | |||||||
|     app.add_route(handler2, "/test2", name="route_test2") |     app.add_route(handler2, "/test2", name="route_test2") | ||||||
|  |  | ||||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" |     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" |     assert app.url_for("route_test") == "/test" | ||||||
|     with pytest.raises(URLBuildError): |     with pytest.raises(URLBuildError): | ||||||
|         app.url_for("handler1") |         app.url_for("handler1") | ||||||
|  |  | ||||||
|     assert app.router.routes_all[("test2",)].name == "app.route_test2" |     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" |     assert app.url_for("route_test2") == "/test2" | ||||||
|     with pytest.raises(URLBuildError): |     with pytest.raises(URLBuildError): | ||||||
|         app.url_for("handler2") |         app.url_for("handler2") | ||||||
| @@ -369,7 +369,8 @@ def test_dynamic_add_named_route(): | |||||||
|  |  | ||||||
|     app.add_route(handler, "/folder/<name>", name="route_dynamic") |     app.add_route(handler, "/folder/<name>", name="route_dynamic") | ||||||
|     assert ( |     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" |     assert app.url_for("route_dynamic", name="test") == "/folder/test" | ||||||
|     with pytest.raises(URLBuildError): |     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 | import pytest | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,6 +23,8 @@ try: | |||||||
| except ImportError: | except ImportError: | ||||||
|     flags = 0 |     flags = 0 | ||||||
|  |  | ||||||
|  | TIMER_DELAY = 2 | ||||||
|  |  | ||||||
|  |  | ||||||
| def terminate(proc): | def terminate(proc): | ||||||
|     if flags: |     if flags: | ||||||
| @@ -56,6 +58,40 @@ def write_app(filename, **runargs): | |||||||
|     return text |     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): | def scanner(proc): | ||||||
|     for line in proc.stdout: |     for line in proc.stdout: | ||||||
|         line = line.decode().strip() |         line = line.decode().strip() | ||||||
| @@ -90,9 +126,10 @@ async def test_reloader_live(runargs, mode): | |||||||
|     with TemporaryDirectory() as tmpdir: |     with TemporaryDirectory() as tmpdir: | ||||||
|         filename = os.path.join(tmpdir, "reloader.py") |         filename = os.path.join(tmpdir, "reloader.py") | ||||||
|         text = write_app(filename, **runargs) |         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: |         try: | ||||||
|             timeout = Timer(5, terminate, [proc]) |             timeout = Timer(TIMER_DELAY, terminate, [proc]) | ||||||
|             timeout.start() |             timeout.start() | ||||||
|             # Python apparently keeps using the old source sometimes if |             # Python apparently keeps using the old source sometimes if | ||||||
|             # we don't sleep before rewrite (pycache timestamp problem?) |             # we don't sleep before rewrite (pycache timestamp problem?) | ||||||
| @@ -107,3 +144,40 @@ async def test_reloader_live(runargs, mode): | |||||||
|             terminate(proc) |             terminate(proc) | ||||||
|             with suppress(TimeoutExpired): |             with suppress(TimeoutExpired): | ||||||
|                 proc.wait(timeout=3) |                 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()) |     monkeypatch.setattr(Request, "generate_id", Mock()) | ||||||
|     Request.generate_id.return_value = 1 |     Request.generate_id.return_value = 1 | ||||||
|     request = Request(b"/", {}, None, "GET", None, Mock()) |     request = Request(b"/", {}, None, "GET", None, Mock()) | ||||||
|  |     request.app.config.REQUEST_ID_HEADER = "foo" | ||||||
|  |  | ||||||
|     for _ in range(10): |     for _ in range(10): | ||||||
|         request.id |         request.id | ||||||
| @@ -28,6 +29,7 @@ def test_request_id_generates_from_request(monkeypatch): | |||||||
|  |  | ||||||
| def test_request_id_defaults_uuid(): | def test_request_id_defaults_uuid(): | ||||||
|     request = Request(b"/", {}, None, "GET", None, Mock()) |     request = Request(b"/", {}, None, "GET", None, Mock()) | ||||||
|  |     request.app.config.REQUEST_ID_HEADER = "foo" | ||||||
|  |  | ||||||
|     assert isinstance(request.id, UUID) |     assert isinstance(request.id, UUID) | ||||||
|  |  | ||||||
| @@ -104,7 +106,7 @@ def test_route_assigned_to_request(app): | |||||||
|         return response.empty() |         return response.empty() | ||||||
|  |  | ||||||
|     request, _ = app.test_client.get("/") |     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): | def test_protocol_attribute(app): | ||||||
| @@ -120,3 +122,21 @@ def test_protocol_attribute(app): | |||||||
|     _ = app.test_client.get("/", headers=headers) |     _ = app.test_client.get("/", headers=headers) | ||||||
|  |  | ||||||
|     assert isinstance(retrieved, HttpProtocol) |     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 import Sanic | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
| from sanic.response import json, text | from sanic.response import json, text | ||||||
| from sanic.server import HttpProtocol |  | ||||||
| from sanic.views import CompositionView, HTTPMethodView | from sanic.views import CompositionView, HTTPMethodView | ||||||
| from sanic.views import stream as stream_decorator | from sanic.views import stream as stream_decorator | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,21 +1,8 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  |  | ||||||
| from typing import cast |  | ||||||
|  |  | ||||||
| import httpcore | import httpcore | ||||||
| import httpx | 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_testing.testing import SanicTestClient | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
|   | |||||||
| @@ -253,6 +253,31 @@ async def test_empty_json_asgi(app): | |||||||
|     assert response.body == b"null" |     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): | def test_invalid_json(app): | ||||||
|     @app.post("/") |     @app.post("/") | ||||||
|     async def handler(request): |     async def handler(request): | ||||||
| @@ -292,6 +317,17 @@ def test_query_string(app): | |||||||
|     assert request.args.get("test3", default="My value") == "My value" |     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 | @pytest.mark.asyncio | ||||||
| async def test_query_string_asgi(app): | async def test_query_string_asgi(app): | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
| @@ -2159,3 +2195,70 @@ def test_safe_method_with_body(app): | |||||||
|     assert request.body == data.encode("utf-8") |     assert request.body == data.encode("utf-8") | ||||||
|     assert request.json.get("test") == "OK" |     assert request.json.get("test") == "OK" | ||||||
|     assert response.body == b"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 asyncio | ||||||
| import inspect | import inspect | ||||||
| import os | import os | ||||||
| import warnings |  | ||||||
|  |  | ||||||
| from collections import namedtuple | from collections import namedtuple | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from random import choice | from random import choice | ||||||
| from unittest.mock import MagicMock |  | ||||||
| from urllib.parse import unquote | from urllib.parse import unquote | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from aiofiles import os as async_os | from aiofiles import os as async_os | ||||||
| from sanic_testing.testing import HOST, PORT |  | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import ( | from sanic.response import ( | ||||||
|     HTTPResponse, |     HTTPResponse, | ||||||
|     StreamingHTTPResponse, |  | ||||||
|     empty, |     empty, | ||||||
|     file, |     file, | ||||||
|     file_stream, |     file_stream, | ||||||
| @@ -26,7 +22,6 @@ from sanic.response import ( | |||||||
|     stream, |     stream, | ||||||
|     text, |     text, | ||||||
| ) | ) | ||||||
| from sanic.server import HttpProtocol |  | ||||||
|  |  | ||||||
|  |  | ||||||
| JSON_DATA = {"ok": True} | JSON_DATA = {"ok": True} | ||||||
| @@ -65,7 +60,9 @@ def test_method_not_allowed(): | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     request, response = app.test_client.post("/") |     request, response = app.test_client.post("/") | ||||||
|     assert set(response.headers["Allow"].split(", ")) == {"GET", "HEAD"} |     assert set(response.headers["Allow"].split(", ")) == { | ||||||
|  |         "GET", | ||||||
|  |     } | ||||||
|  |  | ||||||
|     app.router.reset() |     app.router.reset() | ||||||
|  |  | ||||||
| @@ -78,7 +75,6 @@ def test_method_not_allowed(): | |||||||
|     assert set(response.headers["Allow"].split(", ")) == { |     assert set(response.headers["Allow"].split(", ")) == { | ||||||
|         "GET", |         "GET", | ||||||
|         "POST", |         "POST", | ||||||
|         "HEAD", |  | ||||||
|     } |     } | ||||||
|     assert response.headers["Content-Length"] == "0" |     assert response.headers["Content-Length"] == "0" | ||||||
|  |  | ||||||
| @@ -87,7 +83,6 @@ def test_method_not_allowed(): | |||||||
|     assert set(response.headers["Allow"].split(", ")) == { |     assert set(response.headers["Allow"].split(", ")) == { | ||||||
|         "GET", |         "GET", | ||||||
|         "POST", |         "POST", | ||||||
|         "HEAD", |  | ||||||
|     } |     } | ||||||
|     assert response.headers["Content-Length"] == "0" |     assert response.headers["Content-Length"] == "0" | ||||||
|  |  | ||||||
| @@ -229,7 +224,6 @@ def non_chunked_streaming_app(app): | |||||||
|             sample_streaming_fn, |             sample_streaming_fn, | ||||||
|             headers={"Content-Length": "7"}, |             headers={"Content-Length": "7"}, | ||||||
|             content_type="text/csv", |             content_type="text/csv", | ||||||
|             chunked=False, |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     return app |     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): | 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("/") | ||||||
|         request, response = non_chunked_streaming_app.test_client.get("/") |  | ||||||
|  |  | ||||||
|     assert len(record) == 1 |  | ||||||
|     assert "removed in v21.6" in record[0].message.args[0] |  | ||||||
|  |  | ||||||
|     assert "Transfer-Encoding" not in response.headers |     assert "Transfer-Encoding" not in response.headers | ||||||
|     assert response.headers["Content-Type"] == "text/csv" |     assert response.headers["Content-Type"] == "text/csv" | ||||||
| @@ -534,3 +524,19 @@ def test_empty_response(app): | |||||||
|     request, response = app.test_client.get("/test") |     request, response = app.test_client.get("/test") | ||||||
|     assert response.content_type is None |     assert response.content_type is None | ||||||
|     assert response.body == b"" |     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 asyncio | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | from time import sleep | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.exceptions import ServiceUnavailable | from sanic.exceptions import ServiceUnavailable | ||||||
|  | from sanic.log import LOGGING_CONFIG_DEFAULTS | ||||||
| from sanic.response import text | 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_timeout_default_app.config.RESPONSE_TIMEOUT = 1 | ||||||
| response_handler_cancelled_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") | @response_timeout_app.route("/1") | ||||||
| async def handler_1(request): | async def handler_1(request): | ||||||
| @@ -25,32 +31,17 @@ def handler_exception(request, exception): | |||||||
|     return text("Response Timeout from error_handler.", 503) |     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") | @response_timeout_default_app.route("/1") | ||||||
| async def handler_2(request): | async def handler_2(request): | ||||||
|     await asyncio.sleep(2) |     await asyncio.sleep(2) | ||||||
|     return text("OK") |     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) | @response_handler_cancelled_app.exception(asyncio.CancelledError) | ||||||
| def handler_cancelled(request, exception): | def handler_cancelled(request, exception): | ||||||
|     # If we get a CancelledError, it means sanic has already sent a response, |     # If we get a CancelledError, it means sanic has already sent a response, | ||||||
|     # we should not ever have to handle a CancelledError. |     # 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) |     return text("App received CancelledError!", 500) | ||||||
|     # The client will never receive this response, because the socket |     # The client will never receive this response, because the socket | ||||||
|     # is already closed when we get a CancelledError. |     # is already closed when we get a CancelledError. | ||||||
| @@ -62,8 +53,44 @@ async def handler_3(request): | |||||||
|     return text("OK") |     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(): | def test_response_handler_cancelled(): | ||||||
|     request, response = response_handler_cancelled_app.test_client.get("/1") |     request, response = response_handler_cancelled_app.test_client.get("/1") | ||||||
|     assert response.status == 503 |     assert response.status == 503 | ||||||
|     assert "Response Timeout" in response.text |     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): | def test_route_invalid_parameter_syntax(app): | ||||||
|     with pytest.raises(ValueError): |     with pytest.raises(ValueError): | ||||||
|  |  | ||||||
|         @app.get("/get/<:string>", strict_slashes=True) |         @app.get("/get/<:str>", strict_slashes=True) | ||||||
|         def handler(request): |         def handler(request): | ||||||
|             return text("OK") |             return text("OK") | ||||||
|  |  | ||||||
| @@ -478,7 +478,7 @@ def test_dynamic_route(app): | |||||||
| def test_dynamic_route_string(app): | def test_dynamic_route_string(app): | ||||||
|     results = [] |     results = [] | ||||||
|  |  | ||||||
|     @app.route("/folder/<name:string>") |     @app.route("/folder/<name:str>") | ||||||
|     async def handler(request, name): |     async def handler(request, name): | ||||||
|         results.append(name) |         results.append(name) | ||||||
|         return text("OK") |         return text("OK") | ||||||
| @@ -513,7 +513,7 @@ def test_dynamic_route_int(app): | |||||||
| def test_dynamic_route_number(app): | def test_dynamic_route_number(app): | ||||||
|     results = [] |     results = [] | ||||||
|  |  | ||||||
|     @app.route("/weight/<weight:number>") |     @app.route("/weight/<weight:float>") | ||||||
|     async def handler(request, weight): |     async def handler(request, weight): | ||||||
|         results.append(weight) |         results.append(weight) | ||||||
|         return text("OK") |         return text("OK") | ||||||
| @@ -543,9 +543,6 @@ def test_dynamic_route_regex(app): | |||||||
|     async def handler(request, folder_id): |     async def handler(request, folder_id): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     app.router.finalize() |  | ||||||
|     print(app.router.find_route_src) |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/folder/test") |     request, response = app.test_client.get("/folder/test") | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
| @@ -587,6 +584,8 @@ def test_dynamic_route_path(app): | |||||||
|     async def handler(request, path): |     async def handler(request, path): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|  |     app.router.finalize() | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/path/1/info") |     request, response = app.test_client.get("/path/1/info") | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
| @@ -824,7 +823,7 @@ def test_dynamic_add_route_string(app): | |||||||
|         results.append(name) |         results.append(name) | ||||||
|         return text("OK") |         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") |     request, response = app.test_client.get("/folder/test123") | ||||||
|  |  | ||||||
|     assert response.text == "OK" |     assert response.text == "OK" | ||||||
| @@ -860,7 +859,7 @@ def test_dynamic_add_route_number(app): | |||||||
|         results.append(weight) |         results.append(weight) | ||||||
|         return text("OK") |         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") |     request, response = app.test_client.get("/weight/12345") | ||||||
|     assert response.text == "OK" |     assert response.text == "OK" | ||||||
| @@ -1008,14 +1007,8 @@ def test_unmergeable_overload_routes(app): | |||||||
|     async def handler2(request): |     async def handler2(request): | ||||||
|         return text("OK1") |         return text("OK1") | ||||||
|  |  | ||||||
|     assert ( |     assert len(app.router.static_routes) == 1 | ||||||
|         len( |     assert len(app.router.static_routes[("overload_whole",)].methods) == 3 | ||||||
|             dict(list(app.router.static_routes.values())[0].handlers)[ |  | ||||||
|                 "overload_whole" |  | ||||||
|             ] |  | ||||||
|         ) |  | ||||||
|         == 3 |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/overload_whole") |     request, response = app.test_client.get("/overload_whole") | ||||||
|     assert response.text == "OK1" |     assert response.text == "OK1" | ||||||
| @@ -1073,7 +1066,8 @@ def test_uri_with_different_method_and_different_params(app): | |||||||
|         return json({"action": action}) |         return json({"action": action}) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/ads/1234") |     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") |     request, response = app.test_client.post("/ads/post") | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
| @@ -1175,3 +1169,59 @@ def test_route_with_bad_named_param(app): | |||||||
|  |  | ||||||
|     with pytest.raises(SanicException): |     with pytest.raises(SanicException): | ||||||
|         app.router.finalize() |         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(*_): |     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( | @pytest.mark.parametrize( | ||||||
| @@ -79,13 +80,13 @@ async def test_dispatch_signal_triggers_triggers_event(app): | |||||||
|     def sync_signal(*args): |     def sync_signal(*args): | ||||||
|         nonlocal app |         nonlocal app | ||||||
|         nonlocal counter |         nonlocal counter | ||||||
|         signal, *_ = app.signal_router.get("foo.bar.baz") |         group, *_ = app.signal_router.get("foo.bar.baz") | ||||||
|         counter += signal.ctx.event.is_set() |         for signal in group: | ||||||
|  |             counter += signal.ctx.event.is_set() | ||||||
|  |  | ||||||
|     app.signal_router.finalize() |     app.signal_router.finalize() | ||||||
|  |  | ||||||
|     await app.dispatch("foo.bar.baz") |     await app.dispatch("foo.bar.baz") | ||||||
|     signal, *_ = app.signal_router.get("foo.bar.baz") |  | ||||||
|  |  | ||||||
|     assert counter == 1 |     assert counter == 1 | ||||||
|  |  | ||||||
| @@ -224,7 +225,7 @@ async def test_dispatch_signal_triggers_event_on_bp(app): | |||||||
|  |  | ||||||
|     app.blueprint(bp) |     app.blueprint(bp) | ||||||
|     app.signal_router.finalize() |     app.signal_router.finalize() | ||||||
|     signal, *_ = app.signal_router.get( |     signal_group, *_ = app.signal_router.get( | ||||||
|         "foo.bar.baz", condition={"blueprint": "bp"} |         "foo.bar.baz", condition={"blueprint": "bp"} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @@ -233,7 +234,8 @@ async def test_dispatch_signal_triggers_event_on_bp(app): | |||||||
|     assert isawaitable(waiter) |     assert isawaitable(waiter) | ||||||
|  |  | ||||||
|     fut = asyncio.ensure_future(do_wait()) |     fut = asyncio.ensure_future(do_wait()) | ||||||
|     signal.ctx.event.set() |     for signal in signal_group: | ||||||
|  |         signal.ctx.event.set() | ||||||
|     await fut |     await fut | ||||||
|  |  | ||||||
|     assert bp_counter == 1 |     assert bp_counter == 1 | ||||||
| @@ -255,17 +257,60 @@ def test_bad_finalize(app): | |||||||
|     assert counter == 0 |     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"): |     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") |     bp = Blueprint("bp") | ||||||
|     app.blueprint(bp) |     app.blueprint(bp) | ||||||
|  |  | ||||||
|     with pytest.raises(NotFound, match="Could not find signal does.not.exist"): |     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(): | def test_event_on_bp_not_registered(): | ||||||
|   | |||||||
| @@ -1,11 +1,16 @@ | |||||||
| import inspect | import inspect | ||||||
|  | import logging | ||||||
| import os | import os | ||||||
|  |  | ||||||
|  | from collections import Counter | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from time import gmtime, strftime | from time import gmtime, strftime | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
|  | from sanic import text | ||||||
|  | from sanic.exceptions import FileNotFound | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(scope="module") | @pytest.fixture(scope="module") | ||||||
| def static_file_directory(): | def static_file_directory(): | ||||||
| @@ -445,3 +450,60 @@ def test_static_name(app, static_file_directory, static_name, file_name): | |||||||
|     request, response = app.test_client.get(f"/static/{file_name}") |     request, response = app.test_client.get(f"/static/{file_name}") | ||||||
|  |  | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_nested_dir(app, static_file_directory): | ||||||
|  |     app.static("/static", static_file_directory) | ||||||
|  |  | ||||||
|  |     request, response = app.test_client.get("/static/nested/dir/foo.txt") | ||||||
|  |  | ||||||
|  |     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" | ||||||
|  |     ) | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user