Compare commits
	
		
			68 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 05002d7ee4 | ||
|   | b4360d4a20 | ||
|   | 3b85b3bbad | ||
|   | 6e55e73da1 | ||
|   | 89d942451f | ||
|   | 4d6205e6fe | ||
|   | 1684b0b986 | ||
|   | 4f5faa4a3c | ||
|   | cbb77b536a | ||
|   | 35c76253bf | ||
|   | 8d86c3c598 | ||
|   | 97635111af | ||
|   | 7f3fe40cd4 | ||
|   | ea34bcd849 | ||
|   | 05f758583b | ||
|   | 760c74a293 | ||
|   | 9def46beb8 | ||
|   | 04be8e95a5 | ||
|   | 78ced20fc7 | ||
|   | c3003413d3 | ||
|   | fe3fdc5d83 | ||
|   | b66fb6f9e8 | ||
|   | bf6175fb20 | ||
|   | 7475897a03 | ||
|   | 58ca887be4 | ||
|   | 449bc417a3 | ||
|   | 262f89f2b6 | ||
|   | 38337446cf | ||
|   | ac1331ea4c | ||
|   | 2b947e831f | ||
|   | 112715eb80 | ||
|   | ea9cf365bc | ||
|   | b9b3b4051a | ||
|   | ecb6db29e6 | ||
|   | 6515dde64b | ||
|   | 01d2a2aa3c | ||
|   | 39e12accb8 | ||
|   | 39fe6ea5b1 | ||
|   | fc4b7df088 | ||
|   | 35f28f7a64 | ||
|   | 614be40438 | ||
|   | bde0428d0c | ||
|   | 63567c2ae4 | ||
|   | ec10f337b6 | ||
|   | d0f0e73e96 | ||
|   | b4fe2c8a6b | ||
|   | 33da0771d1 | ||
|   | 75994cd915 | ||
|   | c0839afdde | ||
|   | 5961da3f57 | ||
|   | 41f1809351 | ||
|   | 5fbdcb62e4 | ||
|   | 677b83e9f8 | ||
|   | 6a5c8becac | ||
|   | fd23b99d60 | ||
|   | 634b586df3 | ||
|   | 4ca3e98082 | ||
|   | d18a776964 | ||
|   | d6b4d7d265 | ||
|   | 33ee4c21b3 | ||
|   | a026cd7195 | ||
|   | 7b1bce8d90 | ||
|   | 217a7c5161 | ||
|   | 2949e3422d | ||
|   | 16ea99b0c0 | ||
|   | 19b84ce9f0 | ||
|   | 12521cd5b4 | ||
|   | f41435fae3 | 
| @@ -17,6 +17,12 @@ environment: | |||||||
|       PYTHON_VERSION: "3.8.x" |       PYTHON_VERSION: "3.8.x" | ||||||
|       PYTHON_ARCH: "64" |       PYTHON_ARCH: "64" | ||||||
|  |  | ||||||
|  |     # - TOXENV: py39-no-ext | ||||||
|  |     #   PYTHON: "C:\\Python39-x64\\python" | ||||||
|  |     #   PYTHONPATH: "C:\\Python39-x64" | ||||||
|  |     #   PYTHON_VERSION: "3.9.x" | ||||||
|  |     #   PYTHON_ARCH: "64" | ||||||
|  |  | ||||||
| init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" | init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" | ||||||
|  |  | ||||||
| install: | install: | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | # These are supported funding model platforms | ||||||
|  |  | ||||||
|  | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] | ||||||
|  | patreon: # Replace with a single Patreon username | ||||||
|  | open_collective: sanic-org # Replace with a single Open Collective username | ||||||
|  | ko_fi: # Replace with a single Ko-fi username | ||||||
|  | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel | ||||||
|  | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry | ||||||
|  | liberapay: # Replace with a single Liberapay username | ||||||
|  | issuehunt: # Replace with a single IssueHunt username | ||||||
|  | otechie: # Replace with a single Otechie username | ||||||
|  | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] | ||||||
							
								
								
									
										40
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | name: "CodeQL" | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |   schedule: | ||||||
|  |     - cron: '25 16 * * 0' | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   analyze: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: Analyze | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |  | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         language: [ 'python' ] | ||||||
|  |  | ||||||
|  |     steps: | ||||||
|  |     - name: Checkout repository | ||||||
|  |       uses: actions/checkout@v2 | ||||||
|  |  | ||||||
|  |     - name: Initialize CodeQL | ||||||
|  |       uses: github/codeql-action/init@v1 | ||||||
|  |       with: | ||||||
|  |         languages: ${{ matrix.language }} | ||||||
|  |  | ||||||
|  |     - name: Autobuild | ||||||
|  |       uses: github/codeql-action/autobuild@v1 | ||||||
|  |  | ||||||
|  |     - name: Perform CodeQL Analysis | ||||||
|  |       uses: github/codeql-action/analyze@v1 | ||||||
							
								
								
									
										37
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | name: Coverage check | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     tags: | ||||||
|  |       - "!*" # Do not execute on tags | ||||||
|  |   pull_request: | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  | jobs: | ||||||
|  |   test: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     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" | ||||||
							
								
								
									
										36
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | name: Security Analysis | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   bandit: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: type-check-${{ matrix.config.python-version }} | ||||||
|  |     runs-on: ${{ matrix.os }} | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         os: [ubuntu-latest] | ||||||
|  |         config: | ||||||
|  |           - { python-version: 3.7, tox-env: security} | ||||||
|  |           - { python-version: 3.8, tox-env: security} | ||||||
|  |           - { python-version: 3.9, tox-env: security} | ||||||
|  |           - { python-version: "3.10", tox-env: security} | ||||||
|  |     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 }}" | ||||||
							
								
								
									
										32
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | name: Document Linter | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   docsLinter: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: Lint Documentation | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       matrix: | ||||||
|  |         config: | ||||||
|  |           - {python-version: "3.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 }}" | ||||||
							
								
								
									
										33
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | name: Linter Checks | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   linter: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: lint | ||||||
|  |     runs-on: ${{ matrix.os }} | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         os: [ubuntu-latest] | ||||||
|  |         config: | ||||||
|  |           - { python-version: 3.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" | ||||||
							
								
								
									
										35
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | name: Python 3.7 Tests | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   testPy37: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||||
|  |     runs-on: ${{ matrix.os }} | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: true | ||||||
|  |       matrix: | ||||||
|  |         #         os: [ubuntu-latest, macos-latest] | ||||||
|  |         os: [ubuntu-latest] | ||||||
|  |         config: | ||||||
|  |           - { python-version: 3.7, tox-env: py37 } | ||||||
|  |           - { python-version: 3.7, tox-env: py37-no-ext } | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout the Repository | ||||||
|  |         uses: actions/checkout@v2 | ||||||
|  |         id: checkout-branch | ||||||
|  |  | ||||||
|  |       - name: Run Unit Tests | ||||||
|  |         uses: harshanarayana/custom-actions@main | ||||||
|  |         with: | ||||||
|  |           python-version: ${{ matrix.config.python-version }} | ||||||
|  |           test-infra-tool: tox | ||||||
|  |           test-infra-version: latest | ||||||
|  |           action: tests | ||||||
|  |           test-additional-args: "-e=${{ matrix.config.tox-env }}" | ||||||
|  |           test-failure-retry: "3" | ||||||
							
								
								
									
										35
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | name: Python 3.8 Tests | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   testPy38: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||||
|  |     runs-on: ${{ matrix.os }} | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: true | ||||||
|  |       matrix: | ||||||
|  |         # os: [ubuntu-latest, macos-latest] | ||||||
|  |         os: [ubuntu-latest] | ||||||
|  |         config: | ||||||
|  |           - { python-version: 3.8, tox-env: py38 } | ||||||
|  |           - { python-version: 3.8, tox-env: py38-no-ext } | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout the Repository | ||||||
|  |         uses: actions/checkout@v2 | ||||||
|  |         id: checkout-branch | ||||||
|  |  | ||||||
|  |       - name: Run Unit Tests | ||||||
|  |         uses: harshanarayana/custom-actions@main | ||||||
|  |         with: | ||||||
|  |           python-version: ${{ matrix.config.python-version }} | ||||||
|  |           test-infra-tool: tox | ||||||
|  |           test-infra-version: latest | ||||||
|  |           action: tests | ||||||
|  |           test-additional-args: "-e=${{ matrix.config.tox-env }}" | ||||||
|  |           test-failure-retry: "3" | ||||||
							
								
								
									
										47
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | name: Python 3.9 Tests | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   testPy39: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||||
|  |     runs-on: ${{ matrix.os }} | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: true | ||||||
|  |       matrix: | ||||||
|  |         # os: [ubuntu-latest, macos-latest] | ||||||
|  |         os: [ubuntu-latest] | ||||||
|  |         config: | ||||||
|  |           - { | ||||||
|  |               python-version: 3.9, | ||||||
|  |               tox-env: py39, | ||||||
|  |               ignore-error-flake: "false", | ||||||
|  |               command-timeout: "0", | ||||||
|  |             } | ||||||
|  |           - { | ||||||
|  |               python-version: 3.9, | ||||||
|  |               tox-env: py39-no-ext, | ||||||
|  |               ignore-error-flake: "true", | ||||||
|  |               command-timeout: "600000", | ||||||
|  |             } | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout the Repository | ||||||
|  |         uses: actions/checkout@v2 | ||||||
|  |         id: checkout-branch | ||||||
|  |  | ||||||
|  |       - name: Run Unit Tests | ||||||
|  |         uses: harshanarayana/custom-actions@main | ||||||
|  |         with: | ||||||
|  |           python-version: ${{ matrix.config.python-version }} | ||||||
|  |           test-infra-tool: tox | ||||||
|  |           test-infra-version: latest | ||||||
|  |           action: tests | ||||||
|  |           test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''" | ||||||
|  |           experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}" | ||||||
|  |           command-timeout: "${{ matrix.config.command-timeout }}" | ||||||
|  |           test-failure-retry: "3" | ||||||
							
								
								
									
										35
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | name: Typing Checks | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   typeChecking: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: type-check-${{ matrix.config.python-version }} | ||||||
|  |     runs-on: ${{ matrix.os }} | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         os: [ubuntu-latest] | ||||||
|  |         config: | ||||||
|  |           # - { python-version: 3.7, tox-env: type-checking} | ||||||
|  |           - { python-version: 3.8, tox-env: type-checking} | ||||||
|  |           - { python-version: 3.9, tox-env: type-checking} | ||||||
|  |     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 }}" | ||||||
							
								
								
									
										37
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | name: Run Unit Tests on Windows | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   testsOnWindows: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: ut-${{ matrix.config.tox-env }} | ||||||
|  |     runs-on: windows-latest | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         config: | ||||||
|  |           - { python-version: 3.7, tox-env: py37-no-ext } | ||||||
|  |           - { python-version: 3.8, tox-env: py38-no-ext } | ||||||
|  |           - { python-version: 3.9, tox-env: py39-no-ext } | ||||||
|  |           - { python-version: 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" | ||||||
							
								
								
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -31,6 +31,16 @@ matrix: | |||||||
|       dist: xenial |       dist: xenial | ||||||
|       sudo: true |       sudo: true | ||||||
|       name: "Python 3.8 without Extensions" |       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 |     - env: TOX_ENV=type-checking | ||||||
|       python: 3.6 |       python: 3.6 | ||||||
|       name: "Python 3.6 Type checks" |       name: "Python 3.6 Type checks" | ||||||
| @@ -40,6 +50,10 @@ matrix: | |||||||
|     - env: TOX_ENV=type-checking |     - env: TOX_ENV=type-checking | ||||||
|       python: 3.8 |       python: 3.8 | ||||||
|       name: "Python 3.8 Type checks" |       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=lint |     - env: TOX_ENV=lint | ||||||
|       python: 3.6 |       python: 3.6 | ||||||
|       name: "Python 3.6 Linter checks" |       name: "Python 3.6 Linter checks" | ||||||
| @@ -61,23 +75,28 @@ matrix: | |||||||
|       dist: xenial |       dist: xenial | ||||||
|       sudo: true |       sudo: true | ||||||
|       name: "Python 3.8 Bandit security scan" |       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 |     - env: TOX_ENV=docs | ||||||
|       python: 3.7 |       python: 3.7 | ||||||
|       dist: xenial |       dist: xenial | ||||||
|       sudo: true |       sudo: true | ||||||
|       name: "Python 3.7 Documentation tests" |       name: "Python 3.7 Documentation tests" | ||||||
|     - env: TOX_ENV=pyNightly |     - env: TOX_ENV=pyNightly | ||||||
|       python: 'nightly' |       python: "nightly" | ||||||
|       name: "Python nightly with Extensions" |       name: "Python nightly with Extensions" | ||||||
|     - env: TOX_ENV=pyNightly-no-ext |     - env: TOX_ENV=pyNightly-no-ext | ||||||
|       python: 'nightly' |       python: "nightly" | ||||||
|       name: "Python nightly without Extensions" |       name: "Python nightly without Extensions" | ||||||
|   allow_failures: |   allow_failures: | ||||||
|     - env: TOX_ENV=pyNightly |     - env: TOX_ENV=pyNightly | ||||||
|       python: 'nightly' |       python: "nightly" | ||||||
|       name: "Python nightly with Extensions" |       name: "Python nightly with Extensions" | ||||||
|     - env: TOX_ENV=pyNightly-no-ext |     - env: TOX_ENV=pyNightly-no-ext | ||||||
|       python: 'nightly' |       python: "nightly" | ||||||
|       name: "Python nightly without Extensions" |       name: "Python nightly without Extensions" | ||||||
| install: | install: | ||||||
|   - pip install -U tox |   - pip install -U tox | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							
							
						
						
									
										122
									
								
								CHANGELOG.rst
									
									
									
									
									
								
							| @@ -1,3 +1,125 @@ | |||||||
|  | Version 20.12.5 | ||||||
|  | =============== | ||||||
|  |  | ||||||
|  | Bugfixes | ||||||
|  | ******** | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |    `#2366 <https://github.com/sanic-org/sanic/pull/2366>`_ | ||||||
|  |    websocket dependency for websockets 9.1 security fix | ||||||
|  |  | ||||||
|  | Version 20.12.0 | ||||||
|  | =============== | ||||||
|  |  | ||||||
|  | Features | ||||||
|  | ******** | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1945 <https://github.com/huge-success/sanic/pull/1945>`_ | ||||||
|  |     Static route more verbose if file not found | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1954 <https://github.com/huge-success/sanic/pull/1954>`_ | ||||||
|  |     Fix static routes registration on a blueprint | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1961 <https://github.com/huge-success/sanic/pull/1961>`_ | ||||||
|  |     Add Python 3.9 support | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1962 <https://github.com/huge-success/sanic/pull/1962>`_ | ||||||
|  |     Sanic CLI upgrade | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1967 <https://github.com/huge-success/sanic/pull/1967>`_ | ||||||
|  |     Update aiofile version requirements | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1969 <https://github.com/huge-success/sanic/pull/1969>`_ | ||||||
|  |     Update multidict version requirements | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1970 <https://github.com/huge-success/sanic/pull/1970>`_ | ||||||
|  |     Add py.typed file | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1972 <https://github.com/huge-success/sanic/pull/1972>`_ | ||||||
|  |     Speed optimization in request handler | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1979 <https://github.com/huge-success/sanic/pull/1979>`_ | ||||||
|  |     Add app registry and Sanic class level app retrieval | ||||||
|  |  | ||||||
|  | Bugfixes | ||||||
|  | ******** | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1965 <https://github.com/huge-success/sanic/pull/1965>`_ | ||||||
|  |     Fix Chunked Transport-Encoding in ASGI streaming response | ||||||
|  |  | ||||||
|  | Deprecations and Removals | ||||||
|  | ************************* | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1981 <https://github.com/huge-success/sanic/pull/1981>`_ | ||||||
|  |     Cleanup and remove deprecated code | ||||||
|  |  | ||||||
|  | Developer infrastructure | ||||||
|  | ************************ | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1956 <https://github.com/huge-success/sanic/pull/1956>`_ | ||||||
|  |     Fix load module test | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1973 <https://github.com/huge-success/sanic/pull/1973>`_ | ||||||
|  |     Transition Travis from .org to .com | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1986 <https://github.com/huge-success/sanic/pull/1986>`_ | ||||||
|  |     Update tox requirements | ||||||
|  |  | ||||||
|  | Improved Documentation | ||||||
|  | ********************** | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1951 <https://github.com/huge-success/sanic/pull/1951>`_ | ||||||
|  |     Documentation improvements | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1983 <https://github.com/huge-success/sanic/pull/1983>`_ | ||||||
|  |     Remove duplicate contents in testing.rst | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1984 <https://github.com/huge-success/sanic/pull/1984>`_ | ||||||
|  |     Fix typo in routing.rst | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Version 20.9.1 | ||||||
|  | =============== | ||||||
|  |  | ||||||
|  | Bugfixes | ||||||
|  | ******** | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1954 <https://github.com/huge-success/sanic/pull/1954>`_ | ||||||
|  |     Fix static route registration on blueprints | ||||||
|  |   * | ||||||
|  |     `#1957 <https://github.com/huge-success/sanic/pull/1957>`_ | ||||||
|  |     Removes duplicate headers in ASGI streaming body | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Version 19.12.3 | ||||||
|  | =============== | ||||||
|  |  | ||||||
|  | Bugfixes | ||||||
|  | ******** | ||||||
|  |  | ||||||
|  |   * | ||||||
|  |     `#1959 <https://github.com/huge-success/sanic/pull/1959>`_ | ||||||
|  |     Removes duplicate headers in ASGI streaming body | ||||||
|  |  | ||||||
|  |  | ||||||
| Version 20.9.0 | Version 20.9.0 | ||||||
| =============== | =============== | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,8 +26,8 @@ Sanic | Build fast. Run fast. | |||||||
|    :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge |    :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge | ||||||
| .. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg | .. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg | ||||||
|     :target: https://codecov.io/gh/huge-success/sanic |     :target: https://codecov.io/gh/huge-success/sanic | ||||||
| .. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master | .. |Build Status| image:: https://travis-ci.com/huge-success/sanic.svg?branch=master | ||||||
|    :target: https://travis-ci.org/huge-success/sanic |    :target: https://travis-ci.com/huge-success/sanic | ||||||
| .. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true | .. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true | ||||||
|    :target: https://ci.appveyor.com/project/huge-success/sanic |    :target: https://ci.appveyor.com/project/huge-success/sanic | ||||||
| .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest | .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								changelogs/1970.misc.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelogs/1970.misc.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | Adds py.typed file to expose type information to other packages. | ||||||
| @@ -60,3 +60,26 @@ Open the address `http://0.0.0.0:8000 <http://0.0.0.0:8000>`_ in your web browse | |||||||
| the message *Hello world!*. | the message *Hello world!*. | ||||||
|  |  | ||||||
| You now have a working Sanic server! | You now have a working Sanic server! | ||||||
|  |  | ||||||
|  | 5. Application registry | ||||||
|  | ----------------------- | ||||||
|  |  | ||||||
|  | When you instantiate a Sanic instance, that can be retrieved at a later time from the Sanic app registry. This can be useful, for example, if you need to access your Sanic instance from a location where it is not otherwise accessible. | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     # ./path/to/server.py | ||||||
|  |     from sanic import Sanic | ||||||
|  |  | ||||||
|  |     app = Sanic("my_awesome_server") | ||||||
|  |  | ||||||
|  |     # ./path/to/somewhere_else.py | ||||||
|  |     from sanic import Sanic | ||||||
|  |  | ||||||
|  |     app = Sanic.get_app("my_awesome_server") | ||||||
|  |  | ||||||
|  | If you call ``Sanic.get_app("non-existing")`` on an app that does not exist, it will raise ``SanicException`` by default. You can, instead, force the method to return a new instance of ``Sanic`` with that name: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     app = Sanic.get_app("my_awesome_server", force_create=True) | ||||||
|   | |||||||
| @@ -133,7 +133,7 @@ which allows the handler function to work with any of the HTTP methods in the li | |||||||
|     async def get_handler(request): |     async def get_handler(request): | ||||||
|         return text('GET request - {}'.format(request.args)) |         return text('GET request - {}'.format(request.args)) | ||||||
|  |  | ||||||
| There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default. | There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is also a route with no host, it will be the default. | ||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
|  |  | ||||||
|   | |||||||
| @@ -88,5 +88,5 @@ When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of ` | |||||||
|  |  | ||||||
|     app = Sanic(__name__) |     app = Sanic(__name__) | ||||||
|  |  | ||||||
|     chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB |     chunk_size = 1024 * 1024 * 8 # Set chunk size to 8MiB | ||||||
|     app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size) |     app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size) | ||||||
|   | |||||||
| @@ -58,10 +58,6 @@ More information about | |||||||
| the available arguments to `httpx` can be found | the available arguments to `httpx` can be found | ||||||
| [in the documentation for `httpx <https://www.encode.io/httpx/>`_. | [in the documentation for `httpx <https://www.encode.io/httpx/>`_. | ||||||
|  |  | ||||||
| Additionally, Sanic has an asynchronous testing client. The difference is that the async client will not stand up an |  | ||||||
| instance of your application, but will instead reach inside it using ASGI. All listeners and middleware are still |  | ||||||
| executed. |  | ||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
|     @pytest.mark.asyncio |     @pytest.mark.asyncio | ||||||
|     async def test_index_returns_200(): |     async def test_index_returns_200(): | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ from pathlib import Path | |||||||
|  |  | ||||||
| from sanic import Sanic, response | from sanic import Sanic, response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -42,7 +43,9 @@ async def handler_file(request): | |||||||
|  |  | ||||||
| @app.route("/file_stream") | @app.route("/file_stream") | ||||||
| async def handler_file_stream(request): | async def handler_file_stream(request): | ||||||
|     return await response.file_stream(Path("../") / "setup.py", chunk_size=1024) |     return await response.file_stream( | ||||||
|  |         Path("../") / "setup.py", chunk_size=1024 | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/stream", stream=True) | @app.route("/stream", stream=True) | ||||||
|   | |||||||
| @@ -1,28 +1,83 @@ | |||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from argparse import ArgumentParser | from argparse import ArgumentParser, RawDescriptionHelpFormatter | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
| from typing import Any, Dict, Optional | from typing import Any, Dict, Optional | ||||||
|  |  | ||||||
|  | from sanic import __version__ | ||||||
| from sanic.app import Sanic | from sanic.app import Sanic | ||||||
|  | from sanic.config import BASE_LOGO | ||||||
| from sanic.log import logger | from sanic.log import logger | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SanicArgumentParser(ArgumentParser): | ||||||
|  |     def add_bool_arguments(self, *args, **kwargs): | ||||||
|  |         group = self.add_mutually_exclusive_group() | ||||||
|  |         group.add_argument(*args, action="store_true", **kwargs) | ||||||
|  |         kwargs["help"] = "no " + kwargs["help"] | ||||||
|  |         group.add_argument( | ||||||
|  |             "--no-" + args[0][2:], *args[1:], action="store_false", **kwargs | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|     parser = ArgumentParser(prog="sanic") |     parser = SanicArgumentParser( | ||||||
|     parser.add_argument("--host", dest="host", type=str, default="127.0.0.1") |         prog="sanic", | ||||||
|     parser.add_argument("--port", dest="port", type=int, default=8000) |         description=BASE_LOGO, | ||||||
|     parser.add_argument("--unix", dest="unix", type=str, default="") |         formatter_class=RawDescriptionHelpFormatter, | ||||||
|  |     ) | ||||||
|  |     parser.add_argument( | ||||||
|  |         "-H", | ||||||
|  |         "--host", | ||||||
|  |         dest="host", | ||||||
|  |         type=str, | ||||||
|  |         default="127.0.0.1", | ||||||
|  |         help="host address [default 127.0.0.1]", | ||||||
|  |     ) | ||||||
|  |     parser.add_argument( | ||||||
|  |         "-p", | ||||||
|  |         "--port", | ||||||
|  |         dest="port", | ||||||
|  |         type=int, | ||||||
|  |         default=8000, | ||||||
|  |         help="port to serve on [default 8000]", | ||||||
|  |     ) | ||||||
|  |     parser.add_argument( | ||||||
|  |         "-u", | ||||||
|  |         "--unix", | ||||||
|  |         dest="unix", | ||||||
|  |         type=str, | ||||||
|  |         default="", | ||||||
|  |         help="location of unix socket", | ||||||
|  |     ) | ||||||
|     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." | ||||||
|     ) |     ) | ||||||
|     parser.add_argument("--workers", dest="workers", type=int, default=1) |     parser.add_argument( | ||||||
|  |         "-w", | ||||||
|  |         "--workers", | ||||||
|  |         dest="workers", | ||||||
|  |         type=int, | ||||||
|  |         default=1, | ||||||
|  |         help="number of worker processes [default 1]", | ||||||
|  |     ) | ||||||
|     parser.add_argument("--debug", dest="debug", action="store_true") |     parser.add_argument("--debug", dest="debug", action="store_true") | ||||||
|     parser.add_argument("module") |     parser.add_bool_arguments( | ||||||
|  |         "--access-logs", dest="access_log", help="display access logs" | ||||||
|  |     ) | ||||||
|  |     parser.add_argument( | ||||||
|  |         "-v", | ||||||
|  |         "--version", | ||||||
|  |         action="version", | ||||||
|  |         version=f"Sanic {__version__}", | ||||||
|  |     ) | ||||||
|  |     parser.add_argument( | ||||||
|  |         "module", help="path to your Sanic app. Example: path.to.server:app" | ||||||
|  |     ) | ||||||
|     args = parser.parse_args() |     args = parser.parse_args() | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
| @@ -30,9 +85,12 @@ 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) | ||||||
|  |  | ||||||
|         module_parts = args.module.split(".") |         if ":" in args.module: | ||||||
|         module_name = ".".join(module_parts[:-1]) |             module_name, app_name = args.module.rsplit(":", 1) | ||||||
|         app_name = module_parts[-1] |         else: | ||||||
|  |             module_parts = args.module.split(".") | ||||||
|  |             module_name = ".".join(module_parts[:-1]) | ||||||
|  |             app_name = module_parts[-1] | ||||||
|  |  | ||||||
|         module = import_module(module_name) |         module = import_module(module_name) | ||||||
|         app = getattr(module, app_name, None) |         app = getattr(module, app_name, None) | ||||||
| @@ -57,6 +115,7 @@ def main(): | |||||||
|             unix=args.unix, |             unix=args.unix, | ||||||
|             workers=args.workers, |             workers=args.workers, | ||||||
|             debug=args.debug, |             debug=args.debug, | ||||||
|  |             access_log=args.access_log, | ||||||
|             ssl=ssl, |             ssl=ssl, | ||||||
|         ) |         ) | ||||||
|     except ImportError as e: |     except ImportError as e: | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "20.9.1" | __version__ = "20.12.7" | ||||||
|   | |||||||
							
								
								
									
										158
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										158
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -2,17 +2,18 @@ import logging | |||||||
| import logging.config | import logging.config | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
| import warnings | import sys | ||||||
|  |  | ||||||
| from asyncio import CancelledError, Protocol, ensure_future, get_event_loop | from asyncio import CancelledError, Protocol, ensure_future, get_event_loop | ||||||
| from collections import defaultdict, deque | from collections import defaultdict, deque | ||||||
| from functools import partial | from functools import partial | ||||||
| from inspect import getmodulename, isawaitable, signature, stack | from inspect import isawaitable, signature | ||||||
| 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 | ||||||
| from typing import Any, Dict, Optional, Type, Union | from typing import Any, Dict, Optional, Type, Union | ||||||
| from urllib.parse import urlencode, urlunparse | from urllib.parse import urlencode, urlunparse | ||||||
|  | from warnings import warn | ||||||
|  |  | ||||||
| from sanic import reloader_helpers | from sanic import reloader_helpers | ||||||
| from sanic.asgi import ASGIApp | from sanic.asgi import ASGIApp | ||||||
| @@ -38,6 +39,9 @@ from sanic.websocket import ConnectionClosed, WebSocketProtocol | |||||||
|  |  | ||||||
|  |  | ||||||
| class Sanic: | class Sanic: | ||||||
|  |     _app_registry: Dict[str, "Sanic"] = {} | ||||||
|  |     test_mode = False | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         name=None, |         name=None, | ||||||
| @@ -48,27 +52,35 @@ class Sanic: | |||||||
|         strict_slashes=False, |         strict_slashes=False, | ||||||
|         log_config=None, |         log_config=None, | ||||||
|         configure_logging=True, |         configure_logging=True, | ||||||
|  |         register=None, | ||||||
|     ): |     ): | ||||||
|  |  | ||||||
|         # Get name from previous stack frame |         # Get name from previous stack frame | ||||||
|         if name is None: |         if name is None: | ||||||
|             warnings.warn( |             raise SanicException( | ||||||
|                 "Sanic(name=None) is deprecated and None value support " |                 "Sanic instance cannot be unnamed. " | ||||||
|                 "for `name` will be removed in the next release. " |  | ||||||
|                 "Please use Sanic(name='your_application_name') instead.", |                 "Please use Sanic(name='your_application_name') instead.", | ||||||
|                 DeprecationWarning, |  | ||||||
|                 stacklevel=2, |  | ||||||
|             ) |             ) | ||||||
|             frame_records = stack()[1] |  | ||||||
|             name = getmodulename(frame_records[1]) |  | ||||||
|  |  | ||||||
|         # 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 sys.version_info >= (3, 10): | ||||||
|  |             error_logger.error( | ||||||
|  |                 "Unsupported version of Python has been detected.\n\nPython " | ||||||
|  |                 f"version {sys.version} is not supported by this version of " | ||||||
|  |                 "Sanic. There is a security advisory that has been issued for " | ||||||
|  |                 "Sanic v20.12 while running Python 3.10+. You should either " | ||||||
|  |                 "use a supported version of Python (v3.6 - v3.9) or upgrade " | ||||||
|  |                 "Sanic to v21+.\n\nPlease see https://github.com/sanic-org/" | ||||||
|  |                 "sanic/security/advisories/GHSA-7p79-6x2v-5h88 for " | ||||||
|  |                 "more information.\n" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         self.name = name |         self.name = name | ||||||
|         self.asgi = False |         self.asgi = False | ||||||
|         self.router = router or Router() |         self.router = router or Router(self) | ||||||
|         self.request_class = request_class |         self.request_class = request_class | ||||||
|         self.error_handler = error_handler or ErrorHandler() |         self.error_handler = error_handler or ErrorHandler() | ||||||
|         self.config = Config(load_env=load_env) |         self.config = Config(load_env=load_env) | ||||||
| @@ -90,7 +102,12 @@ class Sanic: | |||||||
|         self.named_response_middleware = {} |         self.named_response_middleware = {} | ||||||
|         # Register alternative method names |         # Register alternative method names | ||||||
|         self.go_fast = self.run |         self.go_fast = self.run | ||||||
|         self.test_mode = False |  | ||||||
|  |         if register is not None: | ||||||
|  |             self.config.REGISTER = register | ||||||
|  |  | ||||||
|  |         if self.config.REGISTER: | ||||||
|  |             self.__class__.register_app(self) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def loop(self): |     def loop(self): | ||||||
| @@ -491,9 +508,7 @@ class Sanic: | |||||||
|             websocket_handler = partial( |             websocket_handler = partial( | ||||||
|                 self._websocket_handler, handler, subprotocols=subprotocols |                 self._websocket_handler, handler, subprotocols=subprotocols | ||||||
|             ) |             ) | ||||||
|             websocket_handler.__name__ = ( |             websocket_handler.__name__ = handler.__name__ | ||||||
|                 "websocket_handler_" + handler.__name__ |  | ||||||
|             ) |  | ||||||
|             routes.extend( |             routes.extend( | ||||||
|                 self.router.add( |                 self.router.add( | ||||||
|                     uri=uri, |                     uri=uri, | ||||||
| @@ -714,28 +729,6 @@ class Sanic: | |||||||
|             self._blueprint_order.append(blueprint) |             self._blueprint_order.append(blueprint) | ||||||
|         blueprint.register(self, options) |         blueprint.register(self, options) | ||||||
|  |  | ||||||
|     def register_blueprint(self, *args, **kwargs): |  | ||||||
|         """ |  | ||||||
|         Proxy method provided for invoking the :func:`blueprint` method |  | ||||||
|  |  | ||||||
|         .. note:: |  | ||||||
|             To be deprecated in 1.0. Use :func:`blueprint` instead. |  | ||||||
|  |  | ||||||
|         :param args: Blueprint object or (list, tuple) thereof |  | ||||||
|         :param kwargs: option dictionary with blueprint defaults |  | ||||||
|         :return: None |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if self.debug: |  | ||||||
|             warnings.simplefilter("default") |  | ||||||
|         warnings.warn( |  | ||||||
|             "Use of register_blueprint will be deprecated in " |  | ||||||
|             "version 1.0.  Please use the blueprint method" |  | ||||||
|             " instead", |  | ||||||
|             DeprecationWarning, |  | ||||||
|         ) |  | ||||||
|         return self.blueprint(*args, **kwargs) |  | ||||||
|  |  | ||||||
|     def url_for(self, view_name: str, **kwargs): |     def url_for(self, view_name: str, **kwargs): | ||||||
|         r"""Build a URL based on a view name and the values provided. |         r"""Build a URL based on a view name and the values provided. | ||||||
|  |  | ||||||
| @@ -766,6 +759,24 @@ class Sanic: | |||||||
|             kw.update(name=view_name) |             kw.update(name=view_name) | ||||||
|  |  | ||||||
|         uri, route = self.router.find_route_by_view_name(view_name, **kw) |         uri, route = self.router.find_route_by_view_name(view_name, **kw) | ||||||
|  |  | ||||||
|  |         # TODO(laggardkernel): this fix should be removed in v21.3. | ||||||
|  |         # Try again without the unnecessary prefix "websocket_handler_", | ||||||
|  |         # which was added by accident on non-blueprint handlers. GH-2021 | ||||||
|  |         if not (uri and route) and view_name.startswith("websocket_handler_"): | ||||||
|  |             view_name = view_name[18:] | ||||||
|  |             uri, route = self.router.find_route_by_view_name(view_name, **kw) | ||||||
|  |             if uri and route: | ||||||
|  |                 warn( | ||||||
|  |                     "The bug of adding unnecessary `websocket_handler_` " | ||||||
|  |                     "prefix in param `view_name` for non-blueprint handlers " | ||||||
|  |                     "is fixed. This backward support will be removed in " | ||||||
|  |                     "v21.3. Please update `Sanic.url_for()` callings in your " | ||||||
|  |                     "code soon.", | ||||||
|  |                     DeprecationWarning, | ||||||
|  |                     stacklevel=2, | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|         if not (uri and route): |         if not (uri and route): | ||||||
|             raise URLBuildError( |             raise URLBuildError( | ||||||
|                 f"Endpoint with name `{view_name}` was not found" |                 f"Endpoint with name `{view_name}` was not found" | ||||||
| @@ -900,7 +911,9 @@ class Sanic: | |||||||
|         name = None |         name = None | ||||||
|         try: |         try: | ||||||
|             # Fetch handler from router |             # Fetch handler from router | ||||||
|             handler, args, kwargs, uri, name = self.router.get(request) |             handler, args, kwargs, uri, name, endpoint = self.router.get( | ||||||
|  |                 request | ||||||
|  |             ) | ||||||
|  |  | ||||||
|             # -------------------------------------------- # |             # -------------------------------------------- # | ||||||
|             # Request Middleware |             # Request Middleware | ||||||
| @@ -922,16 +935,8 @@ class Sanic: | |||||||
|                             "handler from the router" |                             "handler from the router" | ||||||
|                         ) |                         ) | ||||||
|                     ) |                     ) | ||||||
|                 else: |  | ||||||
|                     if not getattr(handler, "__blueprintname__", False): |                 request.endpoint = endpoint | ||||||
|                         request.endpoint = self._build_endpoint_name( |  | ||||||
|                             handler.__name__ |  | ||||||
|                         ) |  | ||||||
|                     else: |  | ||||||
|                         request.endpoint = self._build_endpoint_name( |  | ||||||
|                             getattr(handler, "__blueprintname__", ""), |  | ||||||
|                             handler.__name__, |  | ||||||
|                         ) |  | ||||||
|  |  | ||||||
|                 # Run response handler |                 # Run response handler | ||||||
|                 response = handler(request, *args, **kwargs) |                 response = handler(request, *args, **kwargs) | ||||||
| @@ -1032,7 +1037,6 @@ class Sanic: | |||||||
|         workers: int = 1, |         workers: int = 1, | ||||||
|         protocol: Optional[Type[Protocol]] = None, |         protocol: Optional[Type[Protocol]] = None, | ||||||
|         backlog: int = 100, |         backlog: int = 100, | ||||||
|         stop_event: Any = None, |  | ||||||
|         register_sys_signals: bool = True, |         register_sys_signals: bool = True, | ||||||
|         access_log: Optional[bool] = None, |         access_log: Optional[bool] = None, | ||||||
|         unix: Optional[str] = None, |         unix: Optional[str] = None, | ||||||
| @@ -1062,9 +1066,6 @@ class Sanic: | |||||||
|         :param backlog: a number of unaccepted connections that the system |         :param backlog: a number of unaccepted connections that the system | ||||||
|                         will allow before refusing new connections |                         will allow before refusing new connections | ||||||
|         :type backlog: int |         :type backlog: int | ||||||
|         :param stop_event: event to be triggered |  | ||||||
|                            before stopping the app - deprecated |  | ||||||
|         :type stop_event: None |  | ||||||
|         :param register_sys_signals: Register SIG* events |         :param register_sys_signals: Register SIG* events | ||||||
|         :type register_sys_signals: bool |         :type register_sys_signals: bool | ||||||
|         :param access_log: Enables writing access logs (slows server) |         :param access_log: Enables writing access logs (slows server) | ||||||
| @@ -1092,13 +1093,6 @@ class Sanic: | |||||||
|             protocol = ( |             protocol = ( | ||||||
|                 WebSocketProtocol if self.websocket_enabled else HttpProtocol |                 WebSocketProtocol if self.websocket_enabled else HttpProtocol | ||||||
|             ) |             ) | ||||||
|         if stop_event is not None: |  | ||||||
|             if debug: |  | ||||||
|                 warnings.simplefilter("default") |  | ||||||
|             warnings.warn( |  | ||||||
|                 "stop_event will be removed from future versions.", |  | ||||||
|                 DeprecationWarning, |  | ||||||
|             ) |  | ||||||
|         # if access_log is passed explicitly change config.ACCESS_LOG |         # if access_log is passed explicitly change config.ACCESS_LOG | ||||||
|         if access_log is not None: |         if access_log is not None: | ||||||
|             self.config.ACCESS_LOG = access_log |             self.config.ACCESS_LOG = access_log | ||||||
| @@ -1155,7 +1149,6 @@ class Sanic: | |||||||
|         sock: Optional[socket] = None, |         sock: Optional[socket] = None, | ||||||
|         protocol: Type[Protocol] = None, |         protocol: Type[Protocol] = None, | ||||||
|         backlog: int = 100, |         backlog: int = 100, | ||||||
|         stop_event: Any = None, |  | ||||||
|         access_log: Optional[bool] = None, |         access_log: Optional[bool] = None, | ||||||
|         unix: Optional[str] = None, |         unix: Optional[str] = None, | ||||||
|         return_asyncio_server=False, |         return_asyncio_server=False, | ||||||
| @@ -1188,9 +1181,6 @@ class Sanic: | |||||||
|         :param backlog: a number of unaccepted connections that the system |         :param backlog: a number of unaccepted connections that the system | ||||||
|                         will allow before refusing new connections |                         will allow before refusing new connections | ||||||
|         :type backlog: int |         :type backlog: int | ||||||
|         :param stop_event: event to be triggered |  | ||||||
|                            before stopping the app - deprecated |  | ||||||
|         :type stop_event: None |  | ||||||
|         :param access_log: Enables writing access logs (slows server) |         :param access_log: Enables writing access logs (slows server) | ||||||
|         :type access_log: bool |         :type access_log: bool | ||||||
|         :param return_asyncio_server: flag that defines whether there's a need |         :param return_asyncio_server: flag that defines whether there's a need | ||||||
| @@ -1210,13 +1200,6 @@ class Sanic: | |||||||
|             protocol = ( |             protocol = ( | ||||||
|                 WebSocketProtocol if self.websocket_enabled else HttpProtocol |                 WebSocketProtocol if self.websocket_enabled else HttpProtocol | ||||||
|             ) |             ) | ||||||
|         if stop_event is not None: |  | ||||||
|             if debug: |  | ||||||
|                 warnings.simplefilter("default") |  | ||||||
|             warnings.warn( |  | ||||||
|                 "stop_event will be removed from future versions.", |  | ||||||
|                 DeprecationWarning, |  | ||||||
|             ) |  | ||||||
|         # if access_log is passed explicitly change config.ACCESS_LOG |         # if access_log is passed explicitly change config.ACCESS_LOG | ||||||
|         if access_log is not None: |         if access_log is not None: | ||||||
|             self.config.ACCESS_LOG = access_log |             self.config.ACCESS_LOG = access_log | ||||||
| @@ -1298,7 +1281,6 @@ class Sanic: | |||||||
|         loop=None, |         loop=None, | ||||||
|         protocol=HttpProtocol, |         protocol=HttpProtocol, | ||||||
|         backlog=100, |         backlog=100, | ||||||
|         stop_event=None, |  | ||||||
|         register_sys_signals=True, |         register_sys_signals=True, | ||||||
|         run_async=False, |         run_async=False, | ||||||
|         auto_reload=False, |         auto_reload=False, | ||||||
| @@ -1313,13 +1295,6 @@ class Sanic: | |||||||
|             context = create_default_context(purpose=Purpose.CLIENT_AUTH) |             context = create_default_context(purpose=Purpose.CLIENT_AUTH) | ||||||
|             context.load_cert_chain(cert, keyfile=key) |             context.load_cert_chain(cert, keyfile=key) | ||||||
|             ssl = context |             ssl = context | ||||||
|         if stop_event is not None: |  | ||||||
|             if debug: |  | ||||||
|                 warnings.simplefilter("default") |  | ||||||
|             warnings.warn( |  | ||||||
|                 "stop_event will be removed from future versions.", |  | ||||||
|                 DeprecationWarning, |  | ||||||
|             ) |  | ||||||
|         if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0: |         if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0: | ||||||
|             raise ValueError( |             raise ValueError( | ||||||
|                 "PROXIES_COUNT cannot be negative. " |                 "PROXIES_COUNT cannot be negative. " | ||||||
| @@ -1454,12 +1429,41 @@ class Sanic: | |||||||
|         asgi_app = await ASGIApp.create(self, scope, receive, send) |         asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||||
|         await asgi_app() |         await asgi_app() | ||||||
|  |  | ||||||
|  |     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|     # Configuration |     # Configuration | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|  |  | ||||||
|     def update_config(self, config: Union[bytes, str, dict, Any]): |     def update_config(self, config: Union[bytes, str, dict, Any]): | ||||||
|         """Update app.config. |         """Update app.config. | ||||||
|  |  | ||||||
|         Please refer to config.py::Config.update_config for documentation.""" |         Please refer to config.py::Config.update_config for documentation.""" | ||||||
|  |  | ||||||
|         self.config.update_config(config) |         self.config.update_config(config) | ||||||
|  |  | ||||||
|  |     # -------------------------------------------------------------------- # | ||||||
|  |     # Class methods | ||||||
|  |     # -------------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def register_app(cls, app: "Sanic") -> None: | ||||||
|  |         """Register a Sanic instance""" | ||||||
|  |         if not isinstance(app, cls): | ||||||
|  |             raise SanicException("Registered app must be an instance of Sanic") | ||||||
|  |  | ||||||
|  |         name = app.name | ||||||
|  |         if name in cls._app_registry and not cls.test_mode: | ||||||
|  |             raise SanicException(f'Sanic app name "{name}" already in use.') | ||||||
|  |  | ||||||
|  |         cls._app_registry[name] = app | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_app(cls, name: str, *, force_create: bool = False) -> "Sanic": | ||||||
|  |         """Retrieve an instantiated Sanic instance""" | ||||||
|  |         try: | ||||||
|  |             return cls._app_registry[name] | ||||||
|  |         except KeyError: | ||||||
|  |             if force_create: | ||||||
|  |                 return cls(name) | ||||||
|  |             raise SanicException(f'Sanic app name "{name}" not found.') | ||||||
|   | |||||||
| @@ -312,13 +312,19 @@ class ASGIApp: | |||||||
|         callback = None if self.ws else self.stream_callback |         callback = None if self.ws else self.stream_callback | ||||||
|         await handler(self.request, None, callback) |         await handler(self.request, None, callback) | ||||||
|  |  | ||||||
|     async def stream_callback(self, response: HTTPResponse) -> None: |     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||||
|  |  | ||||||
|  |     async def stream_callback( | ||||||
|  |         self, response: Union[HTTPResponse, StreamingHTTPResponse] | ||||||
|  |     ) -> None: | ||||||
|         """ |         """ | ||||||
|         Write the response. |         Write the response. | ||||||
|         """ |         """ | ||||||
|         headers: List[Tuple[bytes, bytes]] = [] |         headers: List[Tuple[bytes, bytes]] = [] | ||||||
|         cookies: Dict[str, str] = {} |         cookies: Dict[str, str] = {} | ||||||
|  |         content_length: List[str] = [] | ||||||
|         try: |         try: | ||||||
|  |             content_length = response.headers.popall("content-length", []) | ||||||
|             cookies = { |             cookies = { | ||||||
|                 v.key: v |                 v.key: v | ||||||
|                 for _, v in list( |                 for _, v in list( | ||||||
| @@ -351,12 +357,22 @@ class ASGIApp: | |||||||
|             ] |             ] | ||||||
|  |  | ||||||
|         response.asgi = True |         response.asgi = True | ||||||
|  |         is_streaming = isinstance(response, StreamingHTTPResponse) | ||||||
|         if "content-length" not in response.headers and not isinstance( |         if is_streaming and getattr(response, "chunked", False): | ||||||
|             response, StreamingHTTPResponse |             # disable sanic chunking, this is done at the ASGI-server level | ||||||
|         ): |             setattr(response, "chunked", False) | ||||||
|  |             # content-length header is removed to signal to the ASGI-server | ||||||
|  |             # to use automatic-chunking if it supports it | ||||||
|  |         elif len(content_length) > 0: | ||||||
|             headers += [ |             headers += [ | ||||||
|                 (b"content-length", str(len(response.body)).encode("latin-1")) |                 (b"content-length", str(content_length[0]).encode("latin-1")) | ||||||
|  |             ] | ||||||
|  |         elif not is_streaming: | ||||||
|  |             headers += [ | ||||||
|  |                 ( | ||||||
|  |                     b"content-length", | ||||||
|  |                     str(len(getattr(response, "body", b""))).encode("latin-1"), | ||||||
|  |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|         if "content-type" not in response.headers: |         if "content-type" not in response.headers: | ||||||
|   | |||||||
| @@ -20,7 +20,6 @@ if use_trio: | |||||||
|     def stat_async(path): |     def stat_async(path): | ||||||
|         return Path(path).stat() |         return Path(path).stat() | ||||||
|  |  | ||||||
|  |  | ||||||
| else: | else: | ||||||
|     from aiofiles import open as aio_open  # type: ignore |     from aiofiles import open as aio_open  # type: ignore | ||||||
|     from aiofiles.os import stat as stat_async  # type: ignore  # noqa: F401 |     from aiofiles.os import stat as stat_async  # type: ignore  # noqa: F401 | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ DEFAULT_CONFIG = { | |||||||
|     "PROXIES_COUNT": None, |     "PROXIES_COUNT": None, | ||||||
|     "FORWARDED_FOR_HEADER": "X-Forwarded-For", |     "FORWARDED_FOR_HEADER": "X-Forwarded-For", | ||||||
|     "FALLBACK_ERROR_FORMAT": "html", |     "FALLBACK_ERROR_FORMAT": "html", | ||||||
|  |     "REGISTER": True, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -179,8 +179,8 @@ def abort(status_code, message=None): | |||||||
|     message appropriate for the given status code, unless provided. |     message appropriate for the given status code, unless provided. | ||||||
|  |  | ||||||
|     :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 |     :param message: The HTTP response body. Defaults to the messages in | ||||||
|                     in response.py for the given status code. |     STATUS_CODES from sanic.helpers for the given status code. | ||||||
|     """ |     """ | ||||||
|     if message is None: |     if message is None: | ||||||
|         message = STATUS_CODES.get(status_code) |         message = STATUS_CODES.get(status_code) | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								sanic/py.typed
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								sanic/py.typed
									
									
									
									
									
										Normal file
									
								
							| @@ -50,7 +50,7 @@ class StreamBuffer: | |||||||
|         self._queue = asyncio.Queue(buffer_size) |         self._queue = asyncio.Queue(buffer_size) | ||||||
|  |  | ||||||
|     async def read(self): |     async def read(self): | ||||||
|         """ Stop reading when gets None """ |         """Stop reading when gets None""" | ||||||
|         payload = await self._queue.get() |         payload = await self._queue.get() | ||||||
|         self._queue.task_done() |         self._queue.task_done() | ||||||
|         return payload |         return payload | ||||||
| @@ -136,15 +136,18 @@ class Request: | |||||||
|         return f"<{class_name}: {self.method} {self.path}>" |         return f"<{class_name}: {self.method} {self.path}>" | ||||||
|  |  | ||||||
|     def body_init(self): |     def body_init(self): | ||||||
|         """.. deprecated:: 20.3""" |         """.. deprecated:: 20.3 | ||||||
|  |         To be removed in 21.3""" | ||||||
|         self.body = [] |         self.body = [] | ||||||
|  |  | ||||||
|     def body_push(self, data): |     def body_push(self, data): | ||||||
|         """.. deprecated:: 20.3""" |         """.. deprecated:: 20.3 | ||||||
|  |         To be removed in 21.3""" | ||||||
|         self.body.append(data) |         self.body.append(data) | ||||||
|  |  | ||||||
|     def body_finish(self): |     def body_finish(self): | ||||||
|         """.. deprecated:: 20.3""" |         """.. deprecated:: 20.3 | ||||||
|  |         To be removed in 21.3""" | ||||||
|         self.body = b"".join(self.body) |         self.body = b"".join(self.body) | ||||||
|  |  | ||||||
|     async def receive_body(self): |     async def receive_body(self): | ||||||
| @@ -262,9 +265,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) | ||||||
| @@ -318,9 +324,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) | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| import warnings |  | ||||||
|  |  | ||||||
| from functools import partial | from functools import partial | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path | ||||||
| @@ -26,6 +24,8 @@ class BaseHTTPResponse: | |||||||
|         self.asgi = False |         self.asgi = False | ||||||
|  |  | ||||||
|     def _encode_body(self, data): |     def _encode_body(self, data): | ||||||
|  |         if data is None: | ||||||
|  |             return b"" | ||||||
|         return data.encode() if hasattr(data, "encode") else data |         return data.encode() if hasattr(data, "encode") else data | ||||||
|  |  | ||||||
|     def _parse_headers(self): |     def _parse_headers(self): | ||||||
| @@ -45,7 +45,7 @@ class BaseHTTPResponse: | |||||||
|         body=b"", |         body=b"", | ||||||
|     ): |     ): | ||||||
|         """.. deprecated:: 20.3: |         """.. deprecated:: 20.3: | ||||||
|         This function is not public API and will be removed.""" |         This function is not public API and will be removed in 21.3.""" | ||||||
|  |  | ||||||
|         # self.headers get priority over content_type |         # self.headers get priority over content_type | ||||||
|         if self.content_type and "Content-Type" not in self.headers: |         if self.content_type and "Content-Type" not in self.headers: | ||||||
| @@ -100,6 +100,8 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|         """ |         """ | ||||||
|         data = self._encode_body(data) |         data = self._encode_body(data) | ||||||
|  |  | ||||||
|  |         # `chunked` will always be False in ASGI-mode, even if the underlying | ||||||
|  |         # ASGI Transport implements Chunked transport. That does it itself. | ||||||
|         if self.chunked: |         if self.chunked: | ||||||
|             await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) |             await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) | ||||||
|         else: |         else: | ||||||
| @@ -147,22 +149,15 @@ class HTTPResponse(BaseHTTPResponse): | |||||||
|         status=200, |         status=200, | ||||||
|         headers=None, |         headers=None, | ||||||
|         content_type=None, |         content_type=None, | ||||||
|         body_bytes=b"", |  | ||||||
|     ): |     ): | ||||||
|         super().__init__() |         super().__init__() | ||||||
|  |  | ||||||
|         self.content_type = content_type |         self.content_type = content_type | ||||||
|         self.body = body_bytes if body is None else self._encode_body(body) |         self.body = self._encode_body(body) | ||||||
|         self.status = status |         self.status = status | ||||||
|         self.headers = Header(headers or {}) |         self.headers = Header(headers or {}) | ||||||
|         self._cookies = None |         self._cookies = None | ||||||
|  |  | ||||||
|         if body_bytes: |  | ||||||
|             warnings.warn( |  | ||||||
|                 "Parameter `body_bytes` is deprecated, use `body` instead", |  | ||||||
|                 DeprecationWarning, |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): |     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): | ||||||
|         body = b"" |         body = b"" | ||||||
|         if has_message_body(self.status): |         if has_message_body(self.status): | ||||||
| @@ -226,20 +221,10 @@ def text( | |||||||
|     :param content_type: the content type (string) of the response |     :param content_type: the content type (string) of the response | ||||||
|     """ |     """ | ||||||
|     if not isinstance(body, str): |     if not isinstance(body, str): | ||||||
|         warnings.warn( |         raise TypeError( | ||||||
|             "Types other than str will be deprecated in future versions for" |             f"Bad body type. Expected str, got {type(body).__name__})" | ||||||
|             f" response.text, got type {type(body).__name__})", |  | ||||||
|             DeprecationWarning, |  | ||||||
|         ) |         ) | ||||||
|     # Type conversions are deprecated and quite b0rked but still supported for |  | ||||||
|     # text() until applications get fixed. This try-except should be removed. |  | ||||||
|     try: |  | ||||||
|         # Avoid repr(body).encode() b0rkage for body that is already encoded. |  | ||||||
|         # memoryview used only to test bytes-ishness. |  | ||||||
|         with memoryview(body): |  | ||||||
|             pass |  | ||||||
|     except TypeError: |  | ||||||
|         body = f"{body}"  # no-op if body is already str |  | ||||||
|     return HTTPResponse( |     return HTTPResponse( | ||||||
|         body, status=status, headers=headers, content_type=content_type |         body, status=status, headers=headers, content_type=content_type | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -11,7 +11,16 @@ from sanic.views import CompositionView | |||||||
|  |  | ||||||
|  |  | ||||||
| Route = namedtuple( | Route = namedtuple( | ||||||
|     "Route", ["handler", "methods", "pattern", "parameters", "name", "uri"] |     "Route", | ||||||
|  |     [ | ||||||
|  |         "handler", | ||||||
|  |         "methods", | ||||||
|  |         "pattern", | ||||||
|  |         "parameters", | ||||||
|  |         "name", | ||||||
|  |         "uri", | ||||||
|  |         "endpoint", | ||||||
|  |     ], | ||||||
| ) | ) | ||||||
| Parameter = namedtuple("Parameter", ["name", "cast"]) | Parameter = namedtuple("Parameter", ["name", "cast"]) | ||||||
|  |  | ||||||
| @@ -79,7 +88,8 @@ class Router: | |||||||
|     routes_always_check = None |     routes_always_check = None | ||||||
|     parameter_pattern = re.compile(r"<(.+?)>") |     parameter_pattern = re.compile(r"<(.+?)>") | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self, app): | ||||||
|  |         self.app = app | ||||||
|         self.routes_all = {} |         self.routes_all = {} | ||||||
|         self.routes_names = {} |         self.routes_names = {} | ||||||
|         self.routes_static_files = {} |         self.routes_static_files = {} | ||||||
| @@ -299,11 +309,15 @@ class Router: | |||||||
|  |  | ||||||
|             handler_name = f"{bp_name}.{name or handler.__name__}" |             handler_name = f"{bp_name}.{name or handler.__name__}" | ||||||
|         else: |         else: | ||||||
|             handler_name = name or getattr(handler, "__name__", None) |             handler_name = name or getattr( | ||||||
|  |                 handler, "__name__", handler.__class__.__name__ | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         if route: |         if route: | ||||||
|             route = merge_route(route, methods, handler) |             route = merge_route(route, methods, handler) | ||||||
|         else: |         else: | ||||||
|  |             endpoint = self.app._build_endpoint_name(handler_name) | ||||||
|  |  | ||||||
|             route = Route( |             route = Route( | ||||||
|                 handler=handler, |                 handler=handler, | ||||||
|                 methods=methods, |                 methods=methods, | ||||||
| @@ -311,6 +325,7 @@ class Router: | |||||||
|                 parameters=parameters, |                 parameters=parameters, | ||||||
|                 name=handler_name, |                 name=handler_name, | ||||||
|                 uri=uri, |                 uri=uri, | ||||||
|  |                 endpoint=endpoint, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         self.routes_all[uri] = route |         self.routes_all[uri] = route | ||||||
| @@ -449,7 +464,8 @@ class Router: | |||||||
|         route_handler = route.handler |         route_handler = route.handler | ||||||
|         if hasattr(route_handler, "handlers"): |         if hasattr(route_handler, "handlers"): | ||||||
|             route_handler = route_handler.handlers[method] |             route_handler = route_handler.handlers[method] | ||||||
|         return route_handler, [], kwargs, route.uri, route.name |  | ||||||
|  |         return route_handler, [], kwargs, route.uri, route.name, route.endpoint | ||||||
|  |  | ||||||
|     def is_stream_handler(self, request): |     def is_stream_handler(self, request): | ||||||
|         """Handler for request is stream or not. |         """Handler for request is stream or not. | ||||||
|   | |||||||
| @@ -169,7 +169,11 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.request_class = self.app.request_class or Request |         self.request_class = self.app.request_class or Request | ||||||
|         self.is_request_stream = self.app.is_request_stream |         self.is_request_stream = self.app.is_request_stream | ||||||
|         self._is_stream_handler = False |         self._is_stream_handler = False | ||||||
|         self._not_paused = asyncio.Event(loop=deprecated_loop) |         self._not_paused = ( | ||||||
|  |             asyncio.Event() | ||||||
|  |             if sys.version_info >= (3, 10) | ||||||
|  |             else asyncio.Event(loop=deprecated_loop) | ||||||
|  |         ) | ||||||
|         self._total_request_size = 0 |         self._total_request_size = 0 | ||||||
|         self._request_timeout_handler = None |         self._request_timeout_handler = None | ||||||
|         self._response_timeout_handler = None |         self._response_timeout_handler = None | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| from functools import partial, wraps | from functools import partial, wraps | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path, sep | ||||||
| from re import sub | from pathlib import Path | ||||||
| from time import gmtime, strftime | from time import gmtime, strftime | ||||||
| from urllib.parse import unquote | from urllib.parse import unquote | ||||||
|  |  | ||||||
| @@ -13,6 +13,7 @@ from sanic.exceptions import ( | |||||||
|     InvalidUsage, |     InvalidUsage, | ||||||
| ) | ) | ||||||
| from sanic.handlers import ContentRangeHandler | from sanic.handlers import ContentRangeHandler | ||||||
|  | from sanic.log import error_logger | ||||||
| from sanic.response import HTTPResponse, file, file_stream | from sanic.response import HTTPResponse, file, file_stream | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -25,24 +26,41 @@ async def _static_request_handler( | |||||||
|     content_type=None, |     content_type=None, | ||||||
|     file_uri=None, |     file_uri=None, | ||||||
| ): | ): | ||||||
|     # Using this to determine if the URL is trying to break out of the path |  | ||||||
|     # served.  os.path.realpath seems to be very slow |  | ||||||
|     if file_uri and "../" in file_uri: |  | ||||||
|         raise InvalidUsage("Invalid URL") |  | ||||||
|     # Merge served directory and requested file if provided |     # Merge served directory and requested file if provided | ||||||
|     # Strip all / that in the beginning of the URL to help prevent python |     file_path_raw = Path(unquote(file_or_directory)) | ||||||
|     # from herping a derp and treating the uri as an absolute path |     root_path = file_path = file_path_raw.resolve() | ||||||
|     root_path = file_path = file_or_directory |     not_found = FileNotFound( | ||||||
|     if file_uri: |         "File not found", | ||||||
|         file_path = path.join(file_or_directory, sub("^[/]*", "", file_uri)) |         path=file_or_directory, | ||||||
|  |         relative_url=file_uri, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     if file_uri: | ||||||
|  |         # Strip all / that in the beginning of the URL to help prevent | ||||||
|  |         # python from herping a derp and treating the uri as an | ||||||
|  |         # absolute path | ||||||
|  |         unquoted_file_uri = unquote(file_uri).lstrip("/") | ||||||
|  |         file_path_raw = Path(file_or_directory, unquoted_file_uri) | ||||||
|  |         file_path = file_path_raw.resolve() | ||||||
|  |         if ( | ||||||
|  |             file_path < root_path and not file_path_raw.is_symlink() | ||||||
|  |         ) or ".." in file_path_raw.parts: | ||||||
|  |             error_logger.exception( | ||||||
|  |                 f"File not found: path={file_or_directory}, " | ||||||
|  |                 f"relative_url={file_uri}" | ||||||
|  |             ) | ||||||
|  |             raise not_found | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         file_path.relative_to(root_path) | ||||||
|  |     except ValueError: | ||||||
|  |         if not file_path_raw.is_symlink(): | ||||||
|  |             error_logger.exception( | ||||||
|  |                 f"File not found: path={file_or_directory}, " | ||||||
|  |                 f"relative_url={file_uri}" | ||||||
|  |             ) | ||||||
|  |             raise not_found | ||||||
|  |  | ||||||
|     # URL decode the path sent by the browser otherwise we won't be able to |  | ||||||
|     # match filenames which got encoded (filenames with spaces etc) |  | ||||||
|     file_path = path.abspath(unquote(file_path)) |  | ||||||
|     if not file_path.startswith(path.abspath(unquote(root_path))): |  | ||||||
|         raise FileNotFound( |  | ||||||
|             "File not found", path=file_or_directory, relative_url=file_uri |  | ||||||
|         ) |  | ||||||
|     try: |     try: | ||||||
|         headers = {} |         headers = {} | ||||||
|         # Check if the client has been sent this file before |         # Check if the client has been sent this file before | ||||||
| @@ -94,6 +112,10 @@ async def _static_request_handler( | |||||||
|     except ContentRangeError: |     except ContentRangeError: | ||||||
|         raise |         raise | ||||||
|     except Exception: |     except Exception: | ||||||
|  |         error_logger.exception( | ||||||
|  |             f"File not found: path={file_or_directory}, " | ||||||
|  |             f"relative_url={file_uri}" | ||||||
|  |         ) | ||||||
|         raise FileNotFound( |         raise FileNotFound( | ||||||
|             "File not found", path=file_or_directory, relative_url=file_uri |             "File not found", path=file_or_directory, relative_url=file_uri | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -90,6 +90,7 @@ class CompositionView: | |||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.handlers = {} |         self.handlers = {} | ||||||
|  |         self.name = self.__class__.__name__ | ||||||
|  |  | ||||||
|     def add(self, methods, handler, stream=False): |     def add(self, methods, handler, stream=False): | ||||||
|         if stream: |         if stream: | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								setup.py
									
									
									
									
									
								
							| @@ -5,6 +5,7 @@ import codecs | |||||||
| import os | import os | ||||||
| import re | import re | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from distutils.util import strtobool | from distutils.util import strtobool | ||||||
|  |  | ||||||
| from setuptools import setup | from setuptools import setup | ||||||
| @@ -24,6 +25,7 @@ class PyTest(TestCommand): | |||||||
|  |  | ||||||
|     def run_tests(self): |     def run_tests(self): | ||||||
|         import shlex |         import shlex | ||||||
|  |  | ||||||
|         import pytest |         import pytest | ||||||
|  |  | ||||||
|         errno = pytest.main(shlex.split(self.pytest_args)) |         errno = pytest.main(shlex.split(self.pytest_args)) | ||||||
| @@ -38,7 +40,9 @@ def open_local(paths, mode="r", encoding="utf8"): | |||||||
|  |  | ||||||
| with open_local(["sanic", "__version__.py"], encoding="latin1") as fp: | with open_local(["sanic", "__version__.py"], encoding="latin1") as fp: | ||||||
|     try: |     try: | ||||||
|         version = re.findall(r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M)[0] |         version = re.findall( | ||||||
|  |             r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M | ||||||
|  |         )[0] | ||||||
|     except IndexError: |     except IndexError: | ||||||
|         raise RuntimeError("Unable to determine version.") |         raise RuntimeError("Unable to determine version.") | ||||||
|  |  | ||||||
| @@ -53,10 +57,12 @@ setup_kwargs = { | |||||||
|     "author": "Sanic Community", |     "author": "Sanic Community", | ||||||
|     "author_email": "admhpkns@gmail.com", |     "author_email": "admhpkns@gmail.com", | ||||||
|     "description": ( |     "description": ( | ||||||
|         "A web server and web framework that's written to go fast. Build fast. Run fast." |         "A web server and web framework that's written to go fast. " | ||||||
|  |         "Build fast. Run fast." | ||||||
|     ), |     ), | ||||||
|     "long_description": long_description, |     "long_description": long_description, | ||||||
|     "packages": ["sanic"], |     "packages": ["sanic"], | ||||||
|  |     "package_data": {"sanic": ["py.typed"]}, | ||||||
|     "platforms": "any", |     "platforms": "any", | ||||||
|     "python_requires": ">=3.6", |     "python_requires": ">=3.6", | ||||||
|     "classifiers": [ |     "classifiers": [ | ||||||
| @@ -66,11 +72,14 @@ setup_kwargs = { | |||||||
|         "Programming Language :: Python :: 3.6", |         "Programming Language :: Python :: 3.6", | ||||||
|         "Programming Language :: Python :: 3.7", |         "Programming Language :: Python :: 3.7", | ||||||
|         "Programming Language :: Python :: 3.8", |         "Programming Language :: Python :: 3.8", | ||||||
|  |         "Programming Language :: Python :: 3.9", | ||||||
|     ], |     ], | ||||||
|     "entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]}, |     "entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]}, | ||||||
| } | } | ||||||
|  |  | ||||||
| env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | env_dependency = ( | ||||||
|  |     '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | ||||||
|  | ) | ||||||
| ujson = "ujson>=1.35" + env_dependency | ujson = "ujson>=1.35" + env_dependency | ||||||
| uvloop = "uvloop>=0.5.3" + env_dependency | uvloop = "uvloop>=0.5.3" + env_dependency | ||||||
|  |  | ||||||
| @@ -78,24 +87,25 @@ requirements = [ | |||||||
|     "httptools>=0.0.10", |     "httptools>=0.0.10", | ||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
|     "aiofiles>=0.3.0", |     "aiofiles>=0.6.0", | ||||||
|     "websockets>=8.1,<9.0", |     "websockets>=8.1,<=9.1", | ||||||
|     "multidict==5.0.0", |     "multidict>=5.0,<6.0", | ||||||
|     "httpx==0.15.4", |     "httpx==0.15.4", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| tests_require = [ | tests_require = [ | ||||||
|     "pytest==5.2.1", |     "pytest==5.2.1", | ||||||
|     "multidict==5.0.0", |     "multidict>=5.0,<6.0", | ||||||
|     "gunicorn", |     "gunicorn==20.0.4", | ||||||
|     "pytest-cov", |     "pytest-cov", | ||||||
|     "httpcore==0.3.0", |     "httpcore==0.11.*", | ||||||
|     "beautifulsoup4", |     "beautifulsoup4", | ||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
|     "pytest-sanic", |     "pytest-sanic", | ||||||
|     "pytest-sugar", |     "pytest-sugar", | ||||||
|     "pytest-benchmark", |     "pytest-benchmark", | ||||||
|  |     "pytest-dependency", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| docs_require = [ | docs_require = [ | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ from sanic.router import RouteExists, Router | |||||||
|  |  | ||||||
|  |  | ||||||
| random.seed("Pack my box with five dozen liquor jugs.") | random.seed("Pack my box with five dozen liquor jugs.") | ||||||
|  | Sanic.test_mode = True | ||||||
|  |  | ||||||
| if sys.platform in ["win32", "cygwin"]: | if sys.platform in ["win32", "cygwin"]: | ||||||
|     collect_ignore = ["test_worker.py"] |     collect_ignore = ["test_worker.py"] | ||||||
| @@ -95,10 +96,10 @@ class RouteStringGenerator: | |||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(scope="function") | @pytest.fixture(scope="function") | ||||||
| def sanic_router(): | def sanic_router(app): | ||||||
|     # noinspection PyProtectedMember |     # noinspection PyProtectedMember | ||||||
|     def _setup(route_details: tuple) -> (Router, tuple): |     def _setup(route_details: tuple) -> (Router, tuple): | ||||||
|         router = Router() |         router = Router(app) | ||||||
|         added_router = [] |         added_router = [] | ||||||
|         for method, route in route_details: |         for method, route in route_details: | ||||||
|             try: |             try: | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import logging | |||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
|  | from os import environ | ||||||
| from unittest.mock import patch | from unittest.mock import patch | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
| @@ -117,7 +118,7 @@ def test_app_route_raise_value_error(app): | |||||||
|  |  | ||||||
| def test_app_handle_request_handler_is_none(app, monkeypatch): | def test_app_handle_request_handler_is_none(app, monkeypatch): | ||||||
|     def mockreturn(*args, **kwargs): |     def mockreturn(*args, **kwargs): | ||||||
|         return None, [], {}, "", "" |         return None, [], {}, "", "", None | ||||||
|  |  | ||||||
|     # Not sure how to make app.router.get() return None, so use mock here. |     # Not sure how to make app.router.get() return None, so use mock here. | ||||||
|     monkeypatch.setattr(app.router, "get", mockreturn) |     monkeypatch.setattr(app.router, "get", mockreturn) | ||||||
| @@ -258,7 +259,7 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_name_required(): | def test_app_name_required(): | ||||||
|     with pytest.deprecated_call(): |     with pytest.raises(SanicException): | ||||||
|         Sanic() |         Sanic() | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -274,14 +275,50 @@ def test_app_has_test_mode_sync(): | |||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
| # @pytest.mark.asyncio | def test_app_registry(): | ||||||
| # async def test_app_has_test_mode_async(): |     instance = Sanic("test") | ||||||
| #     app = Sanic("test") |     assert Sanic._app_registry["test"] is instance | ||||||
|  |  | ||||||
| #     @app.get("/") |  | ||||||
| #     async def handler(request): |  | ||||||
| #         assert request.app.test_mode |  | ||||||
| #         return text("test") |  | ||||||
|  |  | ||||||
| #     _, response = await app.asgi_client.get("/") | def test_app_registry_wrong_type(): | ||||||
| #     assert response.status == 200 |     with pytest.raises(SanicException): | ||||||
|  |         Sanic.register_app(1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_app_registry_name_reuse(): | ||||||
|  |     Sanic("test") | ||||||
|  |     Sanic.test_mode = False | ||||||
|  |     with pytest.raises(SanicException): | ||||||
|  |         Sanic("test") | ||||||
|  |     Sanic.test_mode = True | ||||||
|  |     Sanic("test") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_app_registry_retrieval(): | ||||||
|  |     instance = Sanic("test") | ||||||
|  |     assert Sanic.get_app("test") is instance | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_get_app_does_not_exist(): | ||||||
|  |     with pytest.raises(SanicException): | ||||||
|  |         Sanic.get_app("does-not-exist") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_get_app_does_not_exist_force_create(): | ||||||
|  |     assert isinstance( | ||||||
|  |         Sanic.get_app("does-not-exist", force_create=True), Sanic | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_app_no_registry(): | ||||||
|  |     Sanic("no-register", register=False) | ||||||
|  |     with pytest.raises(SanicException): | ||||||
|  |         Sanic.get_app("no-register") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_app_no_registry_env(): | ||||||
|  |     environ["SANIC_REGISTER"] = "False" | ||||||
|  |     Sanic("no-register") | ||||||
|  |     with pytest.raises(SanicException): | ||||||
|  |         Sanic.get_app("no-register") | ||||||
|  |     del environ["SANIC_REGISTER"] | ||||||
|   | |||||||
| @@ -1,6 +1,3 @@ | |||||||
| import asyncio |  | ||||||
| import sys |  | ||||||
|  |  | ||||||
| from collections import deque, namedtuple | from collections import deque, namedtuple | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
| @@ -82,14 +79,6 @@ def test_listeners_triggered(app): | |||||||
|     with pytest.warns(UserWarning): |     with pytest.warns(UserWarning): | ||||||
|         server.run() |         server.run() | ||||||
|  |  | ||||||
|     all_tasks = ( |  | ||||||
|         asyncio.Task.all_tasks() |  | ||||||
|         if sys.version_info < (3, 7) |  | ||||||
|         else asyncio.all_tasks(asyncio.get_event_loop()) |  | ||||||
|     ) |  | ||||||
|     for task in all_tasks: |  | ||||||
|         task.cancel() |  | ||||||
|  |  | ||||||
|     assert before_server_start |     assert before_server_start | ||||||
|     assert after_server_start |     assert after_server_start | ||||||
|     assert before_server_stop |     assert before_server_stop | ||||||
| @@ -132,14 +121,6 @@ def test_listeners_triggered_async(app): | |||||||
|     with pytest.warns(UserWarning): |     with pytest.warns(UserWarning): | ||||||
|         server.run() |         server.run() | ||||||
|  |  | ||||||
|     all_tasks = ( |  | ||||||
|         asyncio.Task.all_tasks() |  | ||||||
|         if sys.version_info < (3, 7) |  | ||||||
|         else asyncio.all_tasks(asyncio.get_event_loop()) |  | ||||||
|     ) |  | ||||||
|     for task in all_tasks: |  | ||||||
|         task.cancel() |  | ||||||
|  |  | ||||||
|     assert before_server_start |     assert before_server_start | ||||||
|     assert after_server_start |     assert after_server_start | ||||||
|     assert before_server_stop |     assert before_server_stop | ||||||
|   | |||||||
| @@ -735,6 +735,7 @@ def test_static_blueprint_name(app: Sanic, static_file_directory, file_name): | |||||||
|     _, response = app.test_client.get("/static/test.file/") |     _, response = app.test_client.get("/static/test.file/") | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("file_name", ["test.file"]) | @pytest.mark.parametrize("file_name", ["test.file"]) | ||||||
| def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | ||||||
|     current_file = inspect.getfile(inspect.currentframe()) |     current_file = inspect.getfile(inspect.currentframe()) | ||||||
| @@ -745,7 +746,7 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | |||||||
|  |  | ||||||
|     bp = Blueprint(name="test_mw", url_prefix="") |     bp = Blueprint(name="test_mw", url_prefix="") | ||||||
|  |  | ||||||
|     @bp.middleware('request') |     @bp.middleware("request") | ||||||
|     def bp_mw1(request): |     def bp_mw1(request): | ||||||
|         nonlocal triggered |         nonlocal triggered | ||||||
|         triggered = True |         triggered = True | ||||||
| @@ -754,7 +755,7 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | |||||||
|         "/test.file", |         "/test.file", | ||||||
|         get_file_path(static_file_directory, file_name), |         get_file_path(static_file_directory, file_name), | ||||||
|         strict_slashes=True, |         strict_slashes=True, | ||||||
|         name="static" |         name="static", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     app.blueprint(bp) |     app.blueprint(bp) | ||||||
| @@ -824,21 +825,6 @@ def test_duplicate_blueprint(app): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("debug", [True, False, None]) |  | ||||||
| def test_register_blueprint(app, debug): |  | ||||||
|     bp = Blueprint("bp") |  | ||||||
|  |  | ||||||
|     app.debug = debug |  | ||||||
|     with pytest.warns(DeprecationWarning) as record: |  | ||||||
|         app.register_blueprint(bp) |  | ||||||
|  |  | ||||||
|     assert record[0].message.args[0] == ( |  | ||||||
|         "Use of register_blueprint will be deprecated in " |  | ||||||
|         "version 1.0.  Please use the blueprint method" |  | ||||||
|         " instead" |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_strict_slashes_behavior_adoption(app): | def test_strict_slashes_behavior_adoption(app): | ||||||
|     app.strict_slashes = True |     app.strict_slashes = True | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ from sanic.exceptions import PyFileError | |||||||
|  |  | ||||||
| @contextmanager | @contextmanager | ||||||
| def temp_path(): | def temp_path(): | ||||||
|     """ a simple cross platform replacement for NamedTemporaryFile """ |     """a simple cross platform replacement for NamedTemporaryFile""" | ||||||
|     with TemporaryDirectory() as td: |     with TemporaryDirectory() as td: | ||||||
|         yield Path(td, "file") |         yield Path(td, "file") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,7 +20,9 @@ def test_load_module_from_file_location(loaded_module_from_file_location): | |||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.dependency(depends=["test_load_module_from_file_location"]) | @pytest.mark.dependency(depends=["test_load_module_from_file_location"]) | ||||||
| def test_loaded_module_from_file_location_name(loaded_module_from_file_location,): | def test_loaded_module_from_file_location_name( | ||||||
|  |     loaded_module_from_file_location, | ||||||
|  | ): | ||||||
|     name = loaded_module_from_file_location.__name__ |     name = loaded_module_from_file_location.__name__ | ||||||
|     if "C:\\" in name: |     if "C:\\" in name: | ||||||
|         name = name.split("\\")[-1] |         name = name.split("\\")[-1] | ||||||
|   | |||||||
| @@ -102,7 +102,7 @@ def test_logging_pass_customer_logconfig(): | |||||||
|  |  | ||||||
| @pytest.mark.parametrize("debug", (True, False)) | @pytest.mark.parametrize("debug", (True, False)) | ||||||
| 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") |     root = logging.getLogger("sanic.root") | ||||||
|     root.addHandler(logging.StreamHandler(stream)) |     root.addHandler(logging.StreamHandler(stream)) | ||||||
|   | |||||||
| @@ -290,6 +290,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("/") | ||||||
|   | |||||||
| @@ -41,7 +41,8 @@ def test_response_body_not_a_string(app): | |||||||
|         return text(random_num) |         return text(random_num) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/hello") |     request, response = app.test_client.get("/hello") | ||||||
|     assert response.text == str(random_num) |     assert response.status == 500 | ||||||
|  |     assert b"Internal Server Error" in response.body | ||||||
|  |  | ||||||
|  |  | ||||||
| async def sample_streaming_fn(response): | async def sample_streaming_fn(response): | ||||||
| @@ -238,7 +239,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app): | |||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): | async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): | ||||||
|     request, response = await streaming_app.asgi_client.get("/") |     request, response = await streaming_app.asgi_client.get("/") | ||||||
|     assert response.text == "4\r\nfoo,\r\n3\r\nbar\r\n0\r\n\r\n" |     assert response.text == "foo,bar" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | ||||||
| @@ -624,17 +625,3 @@ 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_response_body_bytes_deprecated(app): |  | ||||||
|     with warnings.catch_warnings(record=True) as w: |  | ||||||
|         warnings.simplefilter("always") |  | ||||||
|  |  | ||||||
|         HTTPResponse(body_bytes=b"bytes") |  | ||||||
|  |  | ||||||
|         assert len(w) == 1 |  | ||||||
|         assert issubclass(w[0].category, DeprecationWarning) |  | ||||||
|         assert ( |  | ||||||
|             "Parameter `body_bytes` is deprecated, use `body` instead" |  | ||||||
|             in str(w[0].message) |  | ||||||
|         ) |  | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| import inspect | import inspect | ||||||
| import os | import os | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  | from pathlib import Path | ||||||
| from time import gmtime, strftime | from time import gmtime, strftime | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
|  | from sanic.app import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(scope="module") | @pytest.fixture(scope="module") | ||||||
| def static_file_directory(): | def static_file_directory(): | ||||||
| @@ -15,6 +19,22 @@ def static_file_directory(): | |||||||
|     return static_directory |     return static_directory | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture(scope="module") | ||||||
|  | def double_dotted_directory_file(static_file_directory: str): | ||||||
|  |     """Generate double dotted directory and its files""" | ||||||
|  |     if sys.platform == "win32": | ||||||
|  |         raise Exception("Windows doesn't support double dotted directories") | ||||||
|  |  | ||||||
|  |     file_path = Path(static_file_directory) / "dotted.." / "dot.txt" | ||||||
|  |     double_dotted_dir = file_path.parent | ||||||
|  |     Path.mkdir(double_dotted_dir, exist_ok=True) | ||||||
|  |     with open(file_path, "w") as f: | ||||||
|  |         f.write("DOT\n") | ||||||
|  |     yield file_path | ||||||
|  |     Path.unlink(file_path) | ||||||
|  |     Path.rmdir(double_dotted_dir) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_file_path(static_file_directory, file_name): | def get_file_path(static_file_directory, file_name): | ||||||
|     return os.path.join(static_file_directory, file_name) |     return os.path.join(static_file_directory, file_name) | ||||||
|  |  | ||||||
| @@ -374,3 +394,43 @@ 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 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.skipif( | ||||||
|  |     sys.platform == "win32", | ||||||
|  |     reason="Windows does not support double dotted directories", | ||||||
|  | ) | ||||||
|  | def test_dotted_dir_ok( | ||||||
|  |     app: Sanic, static_file_directory: str, double_dotted_directory_file: Path | ||||||
|  | ): | ||||||
|  |     app.static("/foo", static_file_directory) | ||||||
|  |     dot_relative_path = str( | ||||||
|  |         double_dotted_directory_file.relative_to(static_file_directory) | ||||||
|  |     ) | ||||||
|  |     _, response = app.test_client.get("/foo/" + dot_relative_path) | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.body == b"DOT\n" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_breakout(app: Sanic, static_file_directory: str): | ||||||
|  |     app.static("/foo", static_file_directory) | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/foo/..%2Ffake/server.py") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/foo/..%2Fstatic/test.file") | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.skipif( | ||||||
|  |     sys.platform != "win32", reason="Block backslash on Windows only" | ||||||
|  | ) | ||||||
|  | def test_double_backslash_prohibited_on_win32( | ||||||
|  |     app: Sanic, static_file_directory: str | ||||||
|  | ): | ||||||
|  |     app.static("/foo", static_file_directory) | ||||||
|  |  | ||||||
|  |     _, response = app.test_client.get("/foo/static/..\\static/test.file") | ||||||
|  |     assert response.status == 404 | ||||||
|  |     _, response = app.test_client.get("/foo/static\\../static/test.file") | ||||||
|  |     assert response.status == 404 | ||||||
|   | |||||||
| @@ -348,3 +348,13 @@ def test_methodview_naming(methodview_app): | |||||||
|  |  | ||||||
|     assert viewone_url == "/view_one" |     assert viewone_url == "/view_one" | ||||||
|     assert viewtwo_url == "/view_two" |     assert viewtwo_url == "/view_two" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_url_for_with_websocket_handlers(app): | ||||||
|  |     # Test for a specific bugfix in GH-2021 | ||||||
|  |     @app.websocket("/ws") | ||||||
|  |     async def my_handler(request, ws): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     assert app.url_for("my_handler") == "/ws" | ||||||
|  |     assert app.url_for("websocket_handler_my_handler") == "/ws" | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,26 +1,26 @@ | |||||||
| [tox] | [tox] | ||||||
| envlist = py36, py37, py38, pyNightly, {py36,py37,py38,pyNightly}-no-ext, lint, check, security, docs | envlist = py36, py37, py38, py39, pyNightly, {py36,py37,py38,py39,pyNightly}-no-ext, lint, check, security, docs | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
| usedevelop = True | usedevelop = True | ||||||
| setenv = | setenv = | ||||||
|     {py36,py37,py38,pyNightly}-no-ext: SANIC_NO_UJSON=1 |     {py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UJSON=1 | ||||||
|     {py36,py37,py38,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 |     {py36,py37,py38,py39,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 | ||||||
| deps = | deps = | ||||||
|     coverage |     coverage==5.3 | ||||||
|     pytest==5.2.1 |     pytest==5.2.1 | ||||||
|     pytest-cov |     pytest-cov | ||||||
|     pytest-sanic |     pytest-sanic | ||||||
|     pytest-sugar |     pytest-sugar | ||||||
|     pytest-benchmark |     pytest-benchmark | ||||||
|     pytest-dependency |     pytest-dependency | ||||||
|     httpcore==0.3.0 |     httpcore==0.11.* | ||||||
|     httpx==0.15.4 |     httpx==0.15.4 | ||||||
|     chardet<=2.3.0 |     multidict>=5.0,<6.0 | ||||||
|     beautifulsoup4 |     beautifulsoup4 | ||||||
|     gunicorn |     gunicorn==20.0.4 | ||||||
|     uvicorn |     uvicorn | ||||||
|     websockets>=8.1,<9.0 |     websockets>=8.1,<=9.1 | ||||||
| commands = | commands = | ||||||
|     pytest {posargs:tests --cov sanic} |     pytest {posargs:tests --cov sanic} | ||||||
|     - coverage combine --append |     - coverage combine --append | ||||||
| @@ -76,7 +76,7 @@ deps = | |||||||
|     recommonmark>=0.5.0 |     recommonmark>=0.5.0 | ||||||
|     docutils |     docutils | ||||||
|     pygments |     pygments | ||||||
|     gunicorn |     gunicorn==20.0.4 | ||||||
|  |  | ||||||
| commands = | commands = | ||||||
|     make docs-test |     make docs-test | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user